From 3d8119d9c9a74e305d13ba6cc5cb9c3a282c2620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8C=83=E6=99=BA=E9=B9=8F?= Date: Tue, 18 Aug 2026 21:41:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=87=AA=E5=8A=A8=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=B8=8E=E5=AE=A1=E6=9F=A5=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E4=BF=AE=E5=A4=8D=20+=20=E9=9D=99=E6=80=81?= =?UTF-8?q?=E5=88=86=E6=9E=90=20AI=20=E7=BF=BB=E8=AF=91=E9=85=8D=E5=AF=B9?= =?UTF-8?q?=20+=20SQLFluff=20=E6=96=B9=E8=A8=80=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 自动修复:废弃 AI 修复,改 linter 原生 fix 多轮收敛;CodeAction hover + 面板修复/全部修复 + 快照 diff 撤销;hover 修复不入「已修复」列表、重新审查清空;修复/撤销后自动保存;单条修复只修目标问题(区间重叠收敛,不再连带相邻同规则) - 审查面板:内联 JS 外部化(reviewPanel.js)修复 CSP 屏蔽导致的修复按钮/行号跳转/tab 失效;面板操作不依赖文件焦点(resolveFixDocument);行号跳转定位已打开编辑器,不在面板列新开副本 - 静态分析:translatedDiagnostics 规则 ID 归一化配对 + 深度审查 prompt 强化,静态分析条目显示中文翻译与逐条 AI 建议 - 波浪线:诊断补 source/code,hover 显示快速修复链接 - SQLFluff:设置面板方言徽章(显式/全局/项目/内置来源配色) --- _AI_USAGE_LOG.md | 11 +- .../specs/2026-08-14-fixer-refactor-design.md | 265 ++++++++++++++++++ ...6-08-14-sqlfluff-dialect-display-design.md | 161 +++++++++++ package.json | 6 +- scripts/build.mjs | 2 +- scripts/copy-webview-js.mjs | 2 +- src/activation/commands.ts | 175 +++++++++++- src/adapters/eslint.ts | 1 + src/adapters/jsp.ts | 48 +--- src/adapters/sqlfluff.ts | 57 +++- src/adapters/stylelint.ts | 2 + src/ai/engine.ts | 18 ++ src/config/fixer.ts | 4 +- src/diagnostics/diagnosticMarkers.ts | 5 +- src/extension.ts | 15 +- src/fix/codeActionProvider.ts | 43 +++ src/fix/fixEngine.ts | 135 +++++++++ src/fix/fixSession.ts | 131 +++++++++ src/fixer/fixer.ts | 227 --------------- src/i18n/messages.ts | 71 ++++- src/merger/merger.ts | 46 ++- src/orchestrator/orchestrator.ts | 29 ++ src/panel/webview.ts | 124 +++++--- src/types.ts | 6 + src/utils/mockDocument.ts | 48 ++++ src/views/reviewPanel.js | 18 ++ src/views/setupView.js | 14 + src/views/setupView.ts | 11 + 28 files changed, 1320 insertions(+), 355 deletions(-) create mode 100644 docs/superpowers/specs/2026-08-14-fixer-refactor-design.md create mode 100644 docs/superpowers/specs/2026-08-14-sqlfluff-dialect-display-design.md create mode 100644 src/fix/codeActionProvider.ts create mode 100644 src/fix/fixEngine.ts create mode 100644 src/fix/fixSession.ts delete mode 100644 src/fixer/fixer.ts create mode 100644 src/utils/mockDocument.ts create mode 100644 src/views/reviewPanel.js diff --git a/_AI_USAGE_LOG.md b/_AI_USAGE_LOG.md index 5f325ad..bc89f61 100644 --- a/_AI_USAGE_LOG.md +++ b/_AI_USAGE_LOG.md @@ -202,4 +202,13 @@ | 2026-08-09 14:31 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 审查报告 #11 启动分析未捕获错误:核实属实且更严重——void analyzeOpenDocuments()/void runStaticAndApply()/scheduleAnalysis 定时器三处均为未捕获 Promise 拒绝路径,runStaticAndApply 内部无 try-catch,markers.apply 无保护;纠正审查中「runStaticAnalysis 自身有 try-catch」的事实错误(orchestrator.ts 直接 await adapter.check(),无 try-catch,唯一防线是各适配器内部 catch)。经审批在 runStaticAndApply 包 try-catch + console.error 单点保护,覆盖三处调用。lint 0 error / compile / npm test 81 passing | 中间产物:无 | src/extension.ts | deepseek-v4-flash | | 2026-08-09 14:34 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 版本升级 1.2.0 → 1.3.0:package.json version 更新;CHANGELOG.md 由模板「Unreleased-Initial release」改为 1.3.0 正式条目(修复 6 项 + 清理 3 项);整合审查报告 #3-#11 全部修复作为本版本内容 | 中间产物:无 | package.json CHANGELOG.md | deepseek-v4-flash | | 2026-08-12 22:31 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | Bug fix: JS 审查不出 no-octal / no-nonoctal-decimal-escape — 根因是 flat config 默认 sourceType=module(严格模式)下 `010` 与 `\8`/`\9` 属 JS 语法错误,eslint.ts:114 又丢弃 ruleId===null 的致命解析错误。A:致命解析错误改为产出 eslint:parse-error error 诊断(不再静默丢弃);B:ts.configs.recommended 逐项补 files 守卫(其 base 无 files 会全局挂 TS parser 并误报 TS 规则到 JS),JS 回退 espree + js.configs.recommended,并新增 JS_FILES 独立 config 块加 ecmaFeatures.jsx 防 JSX 回归。本地被删的 eslint.config.mjs 已从 git 恢复以启用 lint。新增 3 个 adapter 测试(octal→parse-error、\8→parse-error、JSX 无 parse error),npm test 84 passing / lint 0 error(仅 jsp.ts 2 个既有 curly warning)| 中间产物:①调试复现阶段 node -e 转义踩坑(PowerShell 单引号 + \8 报 Unterminated string constant),改用 String.fromCharCode 构造测试串;②ts.configs.recommended 类型试错——cfg.files 在 CompatibleConfig 上不存在致编译报错,改用 (cfg as { files?: string[] }).files 断言;③JSX 方案三选一——全局 jsx / .jsx 单独挂 TS parser / JS_FILES 独立 config 块,实测后者通过 | src/adapters/eslint.ts src/test/adapter.test.ts | deepseek-v4-flash | -| 2026-08-12 23:14 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | Bug fix: 设置面板 Stylelint 配置模式显示错误——项目根目录有 stylelint.config.mjs 时面板仍显示「内置配置」,根因是 setupView.ts 的 PROJECT_CONFIG_FILES.stylelint 清单(6 项)与 stylelint.ts 适配器 CONFIG_FILE_NAMES(8 项)不一致,漏了 stylelint.config.mjs / stylelint.config.cjs。按方案 B 最小改动补齐两个文件名,实际审查本就走项目配置无问题。lint 0 error / compile 通过 | 中间产物:曾评估方案 A(抽共享常量消除 drift,含顺带统一 ESLint legacy 清单)被用户否决,仅执行方案 B | src/views/setupView.ts | deepseek-v4-flash | \ No newline at end of file +| 2026-08-12 23:14 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | Bug fix: 设置面板 Stylelint 配置模式显示错误——项目根目录有 stylelint.config.mjs 时面板仍显示「内置配置」,根因是 setupView.ts 的 PROJECT_CONFIG_FILES.stylelint 清单(6 项)与 stylelint.ts 适配器 CONFIG_FILE_NAMES(8 项)不一致,漏了 stylelint.config.mjs / stylelint.config.cjs。按方案 B 最小改动补齐两个文件名,实际审查本就走项目配置无问题。lint 0 error / compile 通过 | 中间产物:曾评估方案 A(抽共享常量消除 drift,含顺带统一 ESLint legacy 清单)被用户否决,仅执行方案 B | src/views/setupView.ts | deepseek-v4-flash | +| 2026-08-14 22:10 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 侧边栏设置面板 SQLFluff 卡片新增只读「当前生效方言」徽章(紧跟模式徽章、同 adapter-badge-info 样式):方言解析逻辑集中在 sqlfluff.ts——SUPPORTED_DIALECTS 改 export const;hasProjectSqlfluffConfig 重构为 findProjectSqlFluffConfig(返回文件路径);新增 readDialectFromConfigFile(行扫描 [sqlfluff]/[tool.sqlfluff] 段 dialect,支持 = X 与 = "X",不引 TOML 依赖)与导出 resolveSqlFluffDialect(解析链:显式 sqlfluff.dialect > 全局 configFile > 项目配置 > 内置默认 oracle;有配置文件但无 dialect 键回退 sqlfluff 默认 ansi);setupView.ts AdapterConfigStatus 加 sqlfluffDialect 字段并在 collectAdapterStatus 填充、pushConfig i18n 注入 label;setupView.js renderAdapters 对 sqlfluff 卡片追加方言徽章;messages.ts 新增 setup.adapter.sqlfluffDialectLabel 三语。npm test 84 passing / lint 0 error(仅 jsp.ts 2 个既有 curly warning) | 中间产物:设计初稿 v1 的「自动(默认 oracle)」占位文案方案在 Stage ④ 被用户否决(用户改口「展示的方言是现在生效的方言」)→ 澄清后改为完整解析链 v2;解析深度三选一(仅设置值→oracle / 设置值→项目配置→内置 / 含全局配置文件解析)用户选最完整的含全局解析 | src/adapters/sqlfluff.ts src/views/setupView.ts src/views/setupView.js src/i18n/messages.ts docs/superpowers/specs/2026-08-14-sqlfluff-dialect-display-design.md | deepseek-v4-flash | +| 2026-08-14 22:24 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 方言徽章配色随来源变化:resolveSqlFluffDialect 返回值从 string 改为 { dialect, source }(source: explicit/global/project/builtin,导出 SqlFluffDialectSource/SqlFluffDialectInfo 类型);setupView.ts AdapterConfigStatus 加 sqlfluffDialectSource 字段、collectAdapterStatus 传解析信息、CSS 新增 .adapter-badge-explicit 蓝色(#388bfd)样式;setupView.js dialectBadgeClassMap 按 source 映射 class(explicit→蓝/global→黄 warn/project→绿 ok/builtin→紫 info),使方言徽章颜色表明控制它的配置范围。npm test 84 passing / lint 0 error / compile 通过;设计文档同步更新配色映射表 | 中间产物:显式设置徽章色三选一(同全局黄 / 独立蓝 / 按 inspect 实际作用域)用户选独立蓝 | src/adapters/sqlfluff.ts src/views/setupView.ts src/views/setupView.js docs/superpowers/specs/2026-08-14-sqlfluff-dialect-display-design.md | deepseek-v4-flash | +| 2026-08-14 23:28 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 自动修复功能重构(设计+实现):废弃 AI 修复(src/fixer/fixer.ts 删除),改为 linter 原生 --fix 单条 fix 对象(ESLint msg.fix / Stylelint w.fix)驱动。设计:多轮循环+内存模拟+单次提交、diff 反向撤销(任意顺序)、CodeAction hover 修复链接+面板修复按钮+一键修复全部、修复后重跑静态分析刷新(波浪线/CodeLens/面板已修复变绿+撤销按钮+无行号)、轮次上限默认 3 可配置、JSP/PMD/SQLFluff 一期不支持修复。实现:types.ts 增 LinterFix/LinterDiagnostic.fix;eslint/stylelint 适配器提取 fix;orchestrator 增分析结果缓存+getAdapter;新建 src/fix/(fixEngine 多轮循环内存模拟+单次提交、fixSession 多片段合并撤销、codeActionProvider hover 修复链接);commands 实现 fixIssue/fixAll/undoFix+refreshAfterFix;extension 注册 CodeAction+fixSession;webview 修复状态渲染+AI/custom 修复按钮 bug 修复;merger 只标记带 fix 诊断可修复;config fixer.maxIterations;i18n 增 fix 系列 key;测试 95 passing / lint 0 error / compile 通过。自审查发现并修复:多轮同问题撤销只回退首片段(改多片段合并+倒序一次 applyEdit)、空 fix.text 撤销定位错误 | 中间产物:fixEngine 初版 findClosestFixable/issueStillExists 双函数逻辑简化;fixSession 初版单片段记录改多片段合并;撤销定位空字符串退化处理;merger fixable 断言调整 | src/types.ts src/utils/mockDocument.ts src/adapters/eslint.ts src/adapters/stylelint.ts src/adapters/jsp.ts src/orchestrator/orchestrator.ts src/fix/fixEngine.ts src/fix/fixSession.ts src/fix/codeActionProvider.ts src/activation/commands.ts src/extension.ts src/panel/webview.ts src/merger/merger.ts src/config/fixer.ts package.json src/i18n/messages.ts src/test/fixEngine.test.ts src/test/fixSession.test.ts src/test/config.test.ts src/test/merger.test.ts docs/superpowers/specs/2026-08-14-fixer-refactor-design.md | deepseek-v4-flash | +| 2026-08-17 20:05 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 修复波浪线 hover 不显示「快速修复」链接的 bug:toVscodeDiagnostics 生成的 vscode.Diagnostic 未设置 code/source,VSCode 仅在诊断带 code 时渲染 hover 快速修复链接。补 diag.source='Code Purifier'、diag.code=ruleId。diagnostics.test.ts 补 source/code 断言。npm test 96 passing / lint 0 error / compile 通过;并用 demo-eslint/src/common.js 实测确认 13 条可修复项(no-var→let、prefer-template、no-array-constructor 等) | 中间产物:用临时 _fixcheck2.cjs 脚本实测 common.js 可修复项后删除 | src/diagnostics/diagnosticMarkers.ts src/test/diagnostics.test.ts | deepseek-v4-flash | +| 2026-08-18 19:15 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 修复「已修复」列表生命周期两处问题:①codeReviewer.fixIssue 增加 origin 字段区分来源——codeActionProvider hover 快速修复传 origin:'hover'(不写 fixSession,不入「已修复」列表);面板「修复」按钮在 webview handleMessage 传 { ...message, origin:'panel' }(照常记录可撤销);fixAll/命令面板默认记录不变。②codeReviewer.review(Ctrl+Shift+R)开头新增 fixSession.clear(document.uri),重新审查即清空该文件「已修复」列表,只保留本轮修复。webview PanelMessage 增加 origin 字段。npm test 96 passing / lint 0 error / compile 通过 | 中间产物:澄清阶段曾纠结「新审查清空已修复列表」与「hover 不入列表」二选一,用户先选 hover 不入列表、后又追加重新审查清空,两者合并实现 | src/activation/commands.ts src/fix/codeActionProvider.ts src/panel/webview.ts | deepseek-v4-flash | +| 2026-08-18 19:42 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 修复静态分析条目不显示 AI 中文翻译/建议:根因是 AI 返回的 translatedDiagnostics[].originalRuleId 常不带 eslint: 等前缀,merger 按 d.ruleId 精确匹配导致 0 命中,条目全部回退英文原文+原生 fix 文本兜底。改动:merger.ts 新增 normalizeRuleId(剥离命名空间前缀+取末段)+ findTranslation(先精确后归一化,顺序消费不重复),替换原 translationsByRule 精确 Map;engine.ts buildDeepReviewSystemPrompt 三语各追加 translatedDiagnostics 硬性要求(每一条诊断必须 1:1 翻译、originalRuleId 原样复制含前缀、translatedMessage/translatedSuggestion 必填、建议给具体可执行修复)。merger.test.ts 新增 3 用例(前缀缺失命中/slash 变体命中/精确优先)。npm test 99 passing / lint 0 error / compile 通过 | 中间产物:merger 匹配初版考虑「双 key 注册共享数组」方案因双 key 各持独立数组会导致重复消费,改为 findTranslation 扫描池+splice;eslint.config.mjs 本次会话中两次被 vsce 打包流程删除,均从 git 恢复;用户端已确认 AI 已配置连接成功且 AI tab 有内容(排除 AI 调用失败,锁定配对环节) | src/merger/merger.ts src/ai/engine.ts src/test/merger.test.ts | deepseek-v4-flash | +| 2026-08-18 20:28 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 修复审查面板点击「修复」无反应:根因是 webview.ts 审查面板用内联 + `; } - private buildLinterList(report: MergedReport, fixableSet: Set): string { - if (report.linterDiagnostics.length === 0) { + private buildLinterList(report: MergedReport, fixableSet: Set, fixedEntries: Array<{ ruleId: string; line: number; key: string }>): string { + if (report.linterDiagnostics.length === 0 && fixedEntries.length === 0) { return `
${t('report.noIssues')}
`; } const toolName = report.adapterNames.length > 0 ? report.adapterNames.join(' + ') : t('report.sourceLinter'); const hasFixable = fixableSet.size > 0; - return `
${esc(toolName)} · ${t('report.issuesCount', { 0: report.linterCount })}${hasFixable ? `` : ''}
` - + report.linterDiagnostics.map((d, i) => this.buildIssueItem(d.severity, d.ruleId, d.message, d.range.start.line, 'linter', d.suggestion, fixableSet.has(i))).join(''); + let html = `
${esc(toolName)} · ${t('report.issuesCount', { 0: report.linterCount })}${hasFixable ? `` : ''}
`; + if (report.linterDiagnostics.length === 0) { + html += `
${t('report.noIssues')}
`; + } else { + html += report.linterDiagnostics.map((d, i) => this.buildIssueItem(d.severity, d.ruleId, d.message, d.range.start.line, 'linter', d.suggestion, fixableSet.has(i))).join(''); + } + if (fixedEntries.length > 0) { + html += `
✅ ${t('report.fixedIssues')} · ${t('report.issuesCount', { 0: fixedEntries.length })}
`; + html += fixedEntries.map(f => this.buildFixedItem(f.ruleId, f.line, f.key)).join(''); + } + return html; } - private buildCustomList(report: MergedReport, fixableSet: Set): string { + private buildFixedItem(ruleId: string, line: number, key: string): string { + return `
+
+
+
+ + ${t('report.sourceLinter')} + ${esc(ruleId)} + ${t('report.fixedLabel')} + +
+
+
`; + } + + private buildCustomList(report: MergedReport): string { const filterInfo = report.customRuleFilterInfo; if (filterInfo?.skippedRequestA) { return `
${t('report.skipCustomRules')}
`; @@ -317,19 +348,18 @@ ${errorBox} if (report.customRuleDiagnostics.length === 0) { return `
${t('report.noRuleViolations')}
`; } - const hasFixable = fixableSet.size > 0; const filterLabel = filterInfo ? t('report.injectedRules', { 0: filterInfo.injected, 1: filterInfo.totalActive }) : ''; - return `
${t('report.sourceCustom')} · ${t('report.issuesCount', { 0: report.customRuleCount })}${filterLabel}${hasFixable ? `` : ''}
` - + report.customRuleDiagnostics.map((d, i) => this.buildIssueItem(d.severity, d.ruleId, d.message, d.range.start.line, 'custom', d.suggestion, fixableSet.has(i))).join(''); + return `
${t('report.sourceCustom')} · ${t('report.issuesCount', { 0: report.customRuleCount })}${filterLabel}
` + + report.customRuleDiagnostics.map((d, i) => this.buildIssueItem(d.severity, d.ruleId, d.message, d.range.start.line, 'custom', d.suggestion, false)).join(''); } private buildAIList(report: MergedReport): string { if (report.aiFindings.length === 0) { return `
${t('report.noAIFindings')}
`; } - const parts: string[] = [`
${t('report.sourceAI')} · ${t('report.itemsCount', { 0: report.aiCount })}
`]; + const parts: string[] = [`
${t('report.sourceAI')} · ${t('report.itemsCount', { 0: report.aiCount })}
`]; for (const f of report.aiFindings) { const details: string[] = []; const path = (f as { path?: string }).path; @@ -343,7 +373,7 @@ ${errorBox} if (f.suggestion) { details.push(`
💡 ${esc(f.suggestion)}
`); } - parts.push(this.buildIssueItem(f.severity, f.ruleId, f.title, f.line, 'ai', f.suggestion, true, details.join(''))); + parts.push(this.buildIssueItem(f.severity, f.ruleId, f.title, f.line, 'ai', f.suggestion, false, details.join(''))); } return parts.join(''); } @@ -373,7 +403,7 @@ ${errorBox} parts.push(`${esc(message)}`); parts.push(`L${lineNum}`); if (fixable) { - parts.push(``); + parts.push(``); } parts.push(''); @@ -392,17 +422,24 @@ ${errorBox} return parts.join(''); } - private handleMessage(message: PanelMessage): void { + private async handleMessage(message: PanelMessage): Promise { switch (message.type) { case 'navigate': - if (message.line !== undefined) { - const editor = vscode.window.activeTextEditor; - if (editor) { - const line = Math.max(0, message.line); - const range = new vscode.Range(line, 0, line, 0); - editor.selection = new vscode.Selection(range.start, range.end); - editor.revealRange(range, vscode.TextEditorRevealType.InCenter); - } + if (message.line !== undefined && this.currentReport) { + const report = this.currentReport; + const uri = vscode.Uri.file(report.filePath); + const existing = vscode.window.visibleTextEditors.find( + e => e.document.uri.fsPath === report.filePath + ); + const showOptions: vscode.TextDocumentShowOptions = + existing && existing.viewColumn !== undefined + ? { viewColumn: existing.viewColumn } + : { preview: true }; + const editor = await vscode.window.showTextDocument(uri, showOptions); + const line = Math.max(0, message.line); + const range = new vscode.Range(line, 0, line, 0); + editor.selection = new vscode.Selection(range.start, range.end); + editor.revealRange(range, vscode.TextEditorRevealType.InCenter); } break; case 'rerun': @@ -412,11 +449,14 @@ ${errorBox} vscode.commands.executeCommand('codeReviewer.exportReport'); break; case 'fix': - vscode.commands.executeCommand('codeReviewer.fixIssue', message); + vscode.commands.executeCommand('codeReviewer.fixIssue', { ...message, origin: 'panel' }); break; case 'fixAll': vscode.commands.executeCommand('codeReviewer.fixAll'); break; + case 'undo': + vscode.commands.executeCommand('codeReviewer.undoFix', message); + break; } } diff --git a/src/types.ts b/src/types.ts index b464581..5442432 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,12 +13,18 @@ export type Severity = 'error' | 'warning' | 'info'; export type AdapterStatus = 'ok' | 'tool-unavailable' | 'execution-failed'; +export interface LinterFix { + range: [number, number]; + text: string; +} + export interface LinterDiagnostic { severity: Severity; ruleId: string; message: string; range: vscode.Range; suggestion?: string; + fix?: LinterFix; } export interface AdapterResult { diff --git a/src/utils/mockDocument.ts b/src/utils/mockDocument.ts new file mode 100644 index 0000000..e478d39 --- /dev/null +++ b/src/utils/mockDocument.ts @@ -0,0 +1,48 @@ +import * as vscode from 'vscode'; + +export function mockDocument(code: string, language: string, fileName?: string): vscode.TextDocument { + const lines = code.split('\n'); + const uri = vscode.Uri.parse('untitled:virtual'); + const ext = language === 'javascript' ? 'js' : language === 'typescript' ? 'ts' : language === 'css' ? 'css' : 'java'; + return { + uri, + fileName: fileName ?? `untitled.${ext}`, + isUntitled: true, + languageId: language, + version: 1, + isDirty: false, + isClosed: false, + eol: vscode.EndOfLine.LF, + lineCount: lines.length, + getText: () => code, + lineAt: (arg: number | vscode.Position) => { + const line = typeof arg === 'number' ? arg : arg.line; + const text = lines[line] ?? ''; + return { + lineNumber: line, + text, + range: new vscode.Range(line, 0, line, text.length), + rangeIncludingLineBreak: new vscode.Range(line, 0, line, text.length), + firstNonWhitespaceCharacterIndex: text.search(/\S|$/), + isEmptyOrWhitespace: text.trim().length === 0, + }; + }, + offsetAt: (p: vscode.Position) => { + let offset = 0; + for (let i = 0; i < p.line; i++) offset += lines[i].length + 1; + return offset + p.character; + }, + positionAt: (offset: number) => { + let remaining = offset; + for (let i = 0; i < lines.length; i++) { + if (remaining <= lines[i].length) return new vscode.Position(i, remaining); + remaining -= lines[i].length + 1; + } + return new vscode.Position(lines.length - 1, lines[lines.length - 1].length); + }, + getWordRangeAtPosition: () => undefined, + validateRange: (r: vscode.Range) => r, + validatePosition: (p: vscode.Position) => p, + save: () => Promise.resolve(false), + } as unknown as vscode.TextDocument; +} diff --git a/src/views/reviewPanel.js b/src/views/reviewPanel.js new file mode 100644 index 0000000..1f1c2c2 --- /dev/null +++ b/src/views/reviewPanel.js @@ -0,0 +1,18 @@ +const vscode = acquireVsCodeApi(); + +function send(type, line, ruleId, source) { + vscode.postMessage({ type, line, ruleId, source }); +} + +function switchTab(tabId) { + document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); }); + document.querySelectorAll('.tab-content').forEach(function(tc) { tc.classList.remove('active'); }); + document.querySelector('.tab[data-tab="' + tabId + '"]').classList.add('active'); + document.getElementById('tab-' + tabId).classList.add('active'); +} + +function toggleItem(el) { + if (event.target.closest('button')) { return; } + if (event.target.closest('.item-line')) { return; } + el.classList.toggle('expanded'); +} diff --git a/src/views/setupView.js b/src/views/setupView.js index ab82bf2..8795922 100644 --- a/src/views/setupView.js +++ b/src/views/setupView.js @@ -270,6 +270,19 @@ var helpIcon = helpText ? '?' : ''; + var dialectBadgeClassMap = { + explicit: 'adapter-badge-explicit', + global: 'adapter-badge-warn', + project: 'adapter-badge-ok', + builtin: 'adapter-badge-info', + }; + var dialectBadgeClass = dialectBadgeClassMap[a.sqlfluffDialectSource] || 'adapter-badge-info'; + var dialectBadge = a.id === 'sqlfluff' + ? '' + + escapeHtml(i18n.sqlfluffDialectLabel) + ' ' + + escapeHtml(a.sqlfluffDialect || '') + + '' + : ''; return '
' + '
' + @@ -281,6 +294,7 @@ '
' + '
' + '' + modeBadge.text + '' + + dialectBadge + depBadge + configBadge + '
' + diff --git a/src/views/setupView.ts b/src/views/setupView.ts index 030bd9c..f7475e7 100644 --- a/src/views/setupView.ts +++ b/src/views/setupView.ts @@ -17,6 +17,7 @@ import { DocxConverter } from '../rules/converters/docx-converter'; import { PptxConverter } from '../rules/converters/pptx-converter'; import { t, getLanguage, onLanguageChange } from '../i18n/messages'; import { getEslintConfigPath, getStylelintConfigPath, getPMDRulesetPath, getSqlFluffConfigFile, isAdapterEnabled, setAdapterEnabled } from '../config/linter'; +import { resolveSqlFluffDialect, type SqlFluffDialectSource } from '../adapters/sqlfluff'; import { buildEslintProjectConfigText, buildStylelintProjectConfigText, @@ -39,6 +40,8 @@ interface AdapterConfigStatus { languages: string; projectConfigFileName: string; settingsTarget: string; + sqlfluffDialect?: string; + sqlfluffDialectSource?: SqlFluffDialectSource; } const SQLFLUFF_CONFIG_DIALECT = 'mysql'; @@ -334,6 +337,7 @@ export class SetupViewProvider implements vscode.WebviewViewProvider { configYes: t('setup.adapter.configYes'), configNo: t('setup.adapter.configNo'), langLabel: t('setup.adapter.langLabel'), + sqlfluffDialectLabel: t('setup.adapter.sqlfluffDialectLabel'), btnCreateConfig: t('setup.adapter.btnCreateConfig'), btnEditGlobal: t('setup.adapter.btnEditGlobal'), tooltipCreate: t('setup.adapter.tooltipCreate'), @@ -773,6 +777,7 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco .adapter-badge-ok { background: rgba(63,185,80,0.15); color: #3fb950; } .adapter-badge-warn { background: rgba(210,153,34,0.15); color: #d29922; } .adapter-badge-error { background: rgba(248,81,73,0.15); color: #f48771; } +.adapter-badge-explicit { background: rgba(56,139,253,0.15); color: #388bfd; } .adapter-languages { font-size: 10px; color: var(--vscode-descriptionForeground); margin-bottom: 4px; } .adapter-lang-label { color: var(--vscode-descriptionForeground); } .adapter-actions { display: flex; gap: 5px; } @@ -1175,6 +1180,10 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco const configured = !meta.hasExternalDependency || configMode !== 'builtin' || dependencyStatus === 'ready'; + const dialectInfo = id === 'sqlfluff' + ? resolveSqlFluffDialect(vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '') + : undefined; + statuses.push({ id, name: meta.name, @@ -1186,6 +1195,8 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco languages: t(`setup.adapter.${meta.i18nKey}Languages`), projectConfigFileName: meta.projectConfigFileName, settingsTarget: meta.settingsTarget, + sqlfluffDialect: dialectInfo?.dialect, + sqlfluffDialectSource: dialectInfo?.source, }); }