import * as vscode from 'vscode'; import * as os from 'os'; import * as fs from 'fs'; import * as fspath from 'path'; import { LinterOutput, ReportType, AbsPath, IProgress, opeParam } from '../../../global'; import { HdlLangID, LibraryState } from '../../../global/enum'; import { hdlFile, hdlPath } from '../../../hdlFs'; import { t } from '../../../i18n'; import { getLinterConfigurationName, getLinterInstallConfigurationName, getLinterMode, getLinterName, IConfigReminder, LinterItem, LinterMode, makeLinterNamePickItem, makeLinterOptions, SupportLinterName, updateLinterConfigurationName } from './common'; import { UpdateConfigurationType } from '../../../global/lsp'; import { LanguageClient } from 'vscode-languageclient/node'; export class LinterManager { /** * @description 当前诊断器管理者绑定的语言 */ langID: HdlLangID; /** * @description 描述当前诊断器管理者支持哪些第三方诊断器 */ supportLinters: SupportLinterName[]; /** * @description 内部变量,用来存储用户选择的诊断器,也是 picker 操作结果的绑定值 */ currentLinterItem: LinterItem | undefined; /** * @description 诊断器管理者绑定的右下角的 status item */ statusBarItem: vscode.StatusBarItem; /** * @description 用户保证 start 函数的必要操作为幂等的 */ started: boolean; /** * @description 绑定的 lsp,当 started 为 true 时,该值一定不为 undefined */ lspClient: LanguageClient | undefined; constructor(langID: HdlLangID, supportLinters: SupportLinterName[]) { this.langID = langID; this.supportLinters = supportLinters; this.started = false; this.currentLinterItem = undefined; this.lspClient = undefined; // 在窗体右下角创建一个状态栏,用于显示目前激活的诊断器 this.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right); this.statusBarItem.command = this.getLinterPickCommand(); // 对切换时间进行监听,如果不是目前的语言,则隐藏 this.registerActiveTextEditorChangeEvent(langID); } /** * @description 根据 语言 或者对应选择诊断器的 command * @param langID * @returns */ public getLinterPickCommand() { switch (this.langID) { case HdlLangID.Verilog: return 'digital-ide.lsp.vlog.linter.pick'; case HdlLangID.SystemVerilog: return 'digital-ide.lsp.svlog.linter.pick'; case HdlLangID.Vhdl: return 'digital-ide.lsp.vhdl.linter.pick'; default: break; } return 'digital-ide.lsp.vlog.linter.pick'; } /** * @description 手动更新 currentLinterItem,仅在外部更新 status bar 时使用 * @param client */ public async updateCurrentLinterItem(client?: LanguageClient) { client = client ? client : this.lspClient; if (client) { const linterName = getLinterName(this.langID); this.currentLinterItem = await makeLinterNamePickItem(client, this.langID, linterName); } } /** * @description 启动诊断器 * @returns */ async start(client: LanguageClient): Promise { // 根据配置选择对应的诊断器 await this.updateCurrentLinterItem(client); // 注册内部命令 if (!this.started) { const pickerCommand = this.getLinterPickCommand(); vscode.commands.registerCommand(pickerCommand, () => { this.pickLinter(); }); } // 保证幂等 this.started = true; this.lspClient = client; // 如果当前窗口语言为绑定语言,则显示 bar;否则,隐藏它 const editor = vscode.window.activeTextEditor; if (editor && this.langID === hdlFile.getLanguageId(editor.document.fileName)) { this.updateStatusBar(); } else { this.statusBarItem.hide(); } LinterOutput.report(t('info.linter.finish-init', this.langID, this.currentLinterItem?.name || 'unknown'), { level: ReportType.Launch }); } /** * @description 更新右下角 status bar 的状态 */ public updateStatusBar() { const statusBarItem = this.statusBarItem; const currentLinterItem = this.currentLinterItem; if (currentLinterItem) { if (currentLinterItem.available) { // 当前诊断器正常 statusBarItem.backgroundColor = new vscode.ThemeColor('statusBar.background'); statusBarItem.tooltip = t('info.linter.status-bar.tooltip', currentLinterItem.name); statusBarItem.text = `$(getting-started-beginner) Linter(${currentLinterItem.name})`; LinterOutput.report(t('info.linter.finish-init', this.langID, currentLinterItem.name)); } else { // 当前诊断器不可用 statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); statusBarItem.tooltip = t('error.linter.status-bar.tooltip', currentLinterItem.name); statusBarItem.text = `$(extensions-warning-message) Linter(${currentLinterItem.name})`; LinterOutput.report(t('error.linter.status-bar.tooltip', currentLinterItem.name), { level: ReportType.Error }); // 当前诊断器不可用,遂提醒用户【配置文件】or【下载(如果有的话)】 vscode.window.showWarningMessage( t('warning.linter.cannot-get-valid-linter-invoker', currentLinterItem.name), { title: t('info.linter.config-linter-install-path'), value: "config" }, ).then(async res => { // 用户选择配置 if (res?.value === 'config') { const linterInstallConfigurationName = getLinterInstallConfigurationName(currentLinterItem.name); await vscode.commands.executeCommand('workbench.action.openSettings', linterInstallConfigurationName); } // 用户选择下载 if (res?.value === 'download') { } }); } statusBarItem.show(); } } private registerActiveTextEditorChangeEvent(langID: HdlLangID) { vscode.window.onDidChangeActiveTextEditor(editor => { if (!editor) { return; } const currentFileName = hdlPath.toSlash(editor.document.fileName); const currentID = hdlFile.getLanguageId(currentFileName); if (langID === currentID) { this.updateStatusBar(); } else { this.statusBarItem.hide(); } }); } private makePickTitle() { switch (this.langID) { case HdlLangID.Verilog: return t("info.linter.pick-for-verilog"); case HdlLangID.SystemVerilog: return t("info.linter.pick-for-system-verilog"); case HdlLangID.Vhdl: return t("info.linter.pick-for-vhdl"); default: return t("info.linter.pick-for-verilog"); } } /** * @description 为当前的语言选择一个诊断器 */ public async pickLinter() { const pickWidget = vscode.window.createQuickPick(); pickWidget.placeholder = this.makePickTitle(); pickWidget.canSelectMany = false; const client = this.lspClient; if (!client) { return; } // 制作 pick 的选项卡,选项卡的每一个子项目都经过检查 await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: t('info.command.loading'), cancellable: true }, async () => { pickWidget.items = await makeLinterOptions(client, this.langID, this.supportLinters); }); // 激活当前的 linter name 对应的选项卡 const currentLinterName = getLinterName(this.langID); const activeItems = pickWidget.items.filter(item => item.name === currentLinterName); pickWidget.activeItems = activeItems; pickWidget.onDidChangeSelection(items => { this.currentLinterItem = items[0]; }); pickWidget.onDidAccept(async () => { if (this.currentLinterItem) { // 更新后端 await client.sendRequest(UpdateConfigurationType, { configs: [ { name: getLinterConfigurationName(this.langID), value: this.currentLinterItem.name }, { name: 'path', value: this.currentLinterItem.linterPath } ], configType: 'linter' }); // 更新 vscode 配置文件,这会改变配置,顺便触发一次 this.updateStatusBar() // 详细请见 async function registerLinter(client: LanguageClient) updateLinterConfigurationName(this.langID, this.currentLinterItem.name); pickWidget.hide(); } }); pickWidget.show(); } } function getLibrarySearchPaths(path: string): string[] { const paths = new Set(); const langID = hdlFile.getLanguageId(path); const linterName = getLinterName(langID); if (linterName === 'iverilog' || linterName === 'verilator') { const userCommonLibs = opeParam.prjInfo.getLibraryCommonPaths(true, LibraryState.Remote); for (const path of userCommonLibs) { const state = fs.statSync(path); if (state.isDirectory()) { paths.add(path); } else { const parent = fspath.dirname(path); paths.add(parent); } } } return [...paths]; } export async function publishDiagnostics( client: LanguageClient, path: string ) { // 找到所有的库前缀,进行诊断(用于 verilator) // const searchPaths = getLibrarySearchPaths(path); await client.sendRequest("workspace/executeCommand", { command: 'publish-diagnostics', arguments: [path] }); } export async function clearDiagnostics( client: LanguageClient, path: string ) { await client.sendRequest("workspace/executeCommand", { command: 'clear-diagnostics', arguments: [path] }); } /** * @description 异步方法的受限并发消费 * @param arrays * @param consumer * @param poolNum */ export async function asyncConsumer( arrays: T[], consumer: (item: T) => Promise, poolNum: number, progress?: vscode.Progress ): Promise { const rets: R[] = []; const pools = []; let i = 0; progress?.report({ message: `${1}/${arrays.length}`, increment: 0 }); while (i < arrays.length) { const p = consumer(arrays[i]); pools.push({ id: i + 1, promise: p }); if (pools.length % poolNum === 0) { for (const p of pools) { const ret = await p.promise; const increment = Math.floor(p.id / arrays.length * 100); progress?.report({ message: `${p.id}/${arrays.length}`, increment }); rets.push(ret); } pools.length = 0; } i ++; } for (const p of pools) { const ret = await p.promise; const increment = Math.floor(p.id / arrays.length * 100); progress?.report({ message: `${p.id}/${arrays.length}`, increment }); rets.push(ret); } return rets; } /** * @description 刷新当前工作区所有文件的 linter 状态。仅仅在初始化和更新配置文件时需要使用。 */ export async function refreshWorkspaceDiagonastics( client: LanguageClient, lintPaths: AbsPath[], isInitialise: boolean, progress: vscode.Progress ) { const parallelChunk = Math.min(os.cpus().length, 32); const linterMode = getLinterMode(); if (linterMode === LinterMode.Full) { // full,对工作区所有文件进行诊断 const consumer = async (path: string) => { await publishDiagnostics(client, path); } await asyncConsumer(lintPaths, consumer, parallelChunk, progress); } else if (linterMode === LinterMode.Common) { // common, 只对打开文件进行操作 if (!isInitialise) { // 先清除所有的诊断结果 const clearConsumer = async (path: string) => { await clearDiagnostics(client, path); } await asyncConsumer(lintPaths, clearConsumer, parallelChunk); } // 再对激活区域进行诊断 const consumer = async (path: string) => { await publishDiagnostics(client, path); } const activeEditor = vscode.window.activeTextEditor; if (activeEditor) { const path = hdlPath.toEscapePath(activeEditor.document.fileName); await asyncConsumer([path], consumer, parallelChunk, progress); } } else { if (!isInitialise) { // shutdown, 如果是初始化阶段,什么都不需要做 const consumer = async (path: string) => { await clearDiagnostics(client, path); }; await asyncConsumer(lintPaths, consumer, parallelChunk, progress); } } } export const vlogLinterManager = new LinterManager(HdlLangID.Verilog, [ 'iverilog', 'modelsim', 'verible', 'verilator', 'vivado' ]); export const vhdlLinterManager = new LinterManager(HdlLangID.Vhdl, [ 'modelsim', 'vivado' ]); export const svlogLinterManager = new LinterManager(HdlLangID.SystemVerilog, [ 'modelsim', 'verible', 'verilator', 'vivado' ]); export const reserveLinterManager = new LinterManager(HdlLangID.Unknown, []);