- 适配器 i18n 接入(eslint/pmd/sql-lint/stylelint) - Provider 动态注册机制(registry.ts + providers.json + factory 重构) - SetupView 全面重构(setupView.ts 新增 600+ 行) - i18n 消息扩展(messages.ts +210 行) - 规则导入流程优化(import-service / prompt-builder) - 新增 PMD jars 依赖及测试用例
264 lines
11 KiB
Markdown
264 lines
11 KiB
Markdown
# 规则导入预览编辑设计
|
||
|
||
> 适用项目:vscode-code-reviewer
|
||
> 设计日期:2026-07-25
|
||
> 参考文档:`docs/superpowers/specs/2026-07-23-rule-prefilter-design.md` Part B(导入去重)
|
||
|
||
## 1. 现状
|
||
|
||
`import-preview.ts` 中预览 Webview 面板仅支持:
|
||
|
||
- 按 exact/overlap/none 三区展示规则卡片
|
||
- overlap 规则可切换「保留/注释」
|
||
- exact 规则有「恢复」按钮
|
||
- **无字段编辑能力**
|
||
|
||
用户确认后,`buildFinalYaml` 对原始 `yamlContent` 做行级操作(加/去 `#` 前缀)。编辑后的字段值无法传入。
|
||
|
||
## 2. 目标
|
||
|
||
导入预览中支持:
|
||
|
||
- 每条规则卡片默认证折叠,点击展开完整编辑表单
|
||
- 可编辑字段:severity(下拉框)、description(多行文本)、message(多行文本)、languages(标签式输入)、excludeLanguages(标签式输入)
|
||
- id 只读显示(不可编辑)
|
||
- 所有规则类型(exact/overlap/none)均可编辑
|
||
- 展开表单右上角显示「保留/注释」切换,两种状态下都可编辑字段
|
||
- 确认后基于修改后的 rules 数组直接生成最终 YAML 写盘
|
||
|
||
## 3. 数据流变更
|
||
|
||
```
|
||
现状:
|
||
importService.convert() → ConversionResult → showImportPreview() → PreviewDecision{keepRule}
|
||
↓
|
||
buildFinalYaml(操作原始 yamlContent 行)
|
||
|
||
改造后:
|
||
importService.convert() → ConversionResult → showImportPreview() → PreviewDecision{keepRule, editedRules}
|
||
↓
|
||
renderRulesToYaml(从 rules 数组生成新 YAML)
|
||
```
|
||
|
||
关键变化:写盘不再基于原始 `yamlContent` 文本行操作,而是从(可能被用户编辑过的)rules 数组渲染出完整 YAML。
|
||
|
||
## 4. 数据结构变更
|
||
|
||
### 4.1 PreviewDecision 扩展
|
||
|
||
```typescript
|
||
// src/rules/import-types.ts
|
||
|
||
export interface PreviewDecision {
|
||
keepRule: Record<string, boolean>;
|
||
confirmed: boolean;
|
||
editedRules?: ImportableRule[]; // 用户编辑后的完整规则数组(含未修改的规则)
|
||
}
|
||
```
|
||
|
||
`editedRules` 为 `undefined` 表示用户未做任何字段修改,回退到原始 rules。非 `undefined` 时用于生成最终 YAML。
|
||
|
||
## 5. 预览 Webview 重设计
|
||
|
||
### 5.1 卡片结构
|
||
|
||
```
|
||
┌─────────────────────────────────────────────┐
|
||
│ no-console-log [▼ 展开] │ ← 折叠态:id + severity 标签 + 描述摘要
|
||
│ ⚠ warning │
|
||
│ 禁止在 console.log 中输出敏感信息 │
|
||
└─────────────────────────────────────────────┘
|
||
|
||
展开态: ← 点击卡片任意位置展开
|
||
┌─────────────────────────────────────────────┐
|
||
│ no-console-log [保留] [注释] │ ← 顶部:id + 保留/注释切换
|
||
├─────────────────────────────────────────────┤
|
||
│ severity │
|
||
│ [▼ error ▼] ← 下拉框 │
|
||
│ │
|
||
│ description │
|
||
│ ┌─────────────────────────────────────────┐│
|
||
│ │ 禁止在 console.log 中输出敏感信息 ││ ← textarea
|
||
│ │ ││
|
||
│ └─────────────────────────────────────────┘│
|
||
│ │
|
||
│ message │
|
||
│ ┌─────────────────────────────────────────┐│
|
||
│ │ 检测到敏感信息输出到 console,请移除 ││ ← textarea
|
||
│ │ ││
|
||
│ └─────────────────────────────────────────┘│
|
||
│ │
|
||
│ languages │
|
||
│ [java ×] [typescript ×] [▌ ] │ ← 标签式输入
|
||
│ │
|
||
│ excludeLanguages │
|
||
│ [▌ ] │ ← 标签式输入
|
||
└─────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 5.2 交互行为
|
||
|
||
| 操作 | 行为 |
|
||
|------|------|
|
||
| 点击折叠卡片 | 展开表单,其余卡片不受影响 |
|
||
| 展开状态下再次点击顶部 | 折叠回摘要 |
|
||
| 修改字段 | 实时保存在前端 `modifiedRules` map 中 |
|
||
| 切换保留/注释 | 更新 keepRule,不影响已编辑的字段值 |
|
||
| 点「确认导入」 | 发送 `confirm` 消息 + 全部修改后的 rules 数据 |
|
||
| 点「取消」 | 不写盘 |
|
||
|
||
### 5.3 标签式输入实现
|
||
|
||
languages / excludeLanguages 使用纯 HTML/CSS/JS 实现:
|
||
|
||
- 文本输入框 + 已添加标签的行内显示
|
||
- 输入语言名后按 `Enter` 或 `,` 添加为标签
|
||
- 标签显示为 chip 样式,右侧 `×` 按钮删除
|
||
- 去重(同名不重复添加)
|
||
- 支持粘贴逗号分隔列表
|
||
|
||
### 5.4 字段校验(确认时)
|
||
|
||
| 字段 | 规则 |
|
||
|------|------|
|
||
| severity | 必须是 `error` / `warning` / `info` 之一(下拉框天然保证) |
|
||
| description | 非空,trim() 后长度 > 0 |
|
||
| message | 非空,trim() 后长度 > 0 |
|
||
| languages | 可选,每个值非空字符串 |
|
||
| excludeLanguages | 可选,每个值非空字符串 |
|
||
|
||
校验不通过时弹 `vscode.window.showErrorMessage` 提示具体字段名,不关闭面板。
|
||
|
||
## 6. YAML 生成函数
|
||
|
||
新增 `renderRulesToYaml` 替代原有行级操作逻辑:
|
||
|
||
```typescript
|
||
// src/rules/import-service.ts
|
||
|
||
function renderRulesToYaml(
|
||
rules: ImportableRule[],
|
||
decision: PreviewDecision,
|
||
): string {
|
||
const lines: string[] = [];
|
||
|
||
for (const rule of rules) {
|
||
const keep = decision.keepRule[rule.id] ?? rule.duplicateLevel !== 'exact';
|
||
|
||
// 构造该规则的标准 YAML 行
|
||
const ruleLines: string[] = [];
|
||
ruleLines.push(`- id: ${rule.id}`);
|
||
ruleLines.push(` severity: ${rule.severity}`);
|
||
ruleLines.push(` description: ${rule.description}`);
|
||
ruleLines.push(` message: ${rule.message}`);
|
||
if (rule.languages && rule.languages.length > 0) {
|
||
ruleLines.push(` languages: [${rule.languages.join(', ')}]`);
|
||
}
|
||
if (rule.excludeLanguages && rule.excludeLanguages.length > 0) {
|
||
ruleLines.push(` excludeLanguages: [${rule.excludeLanguages.join(', ')}]`);
|
||
}
|
||
|
||
if (keep) {
|
||
lines.push(...ruleLines);
|
||
} else {
|
||
// 注释化:加注释头 + 每行加 #
|
||
const dupLevel = rule.duplicateLevel ?? 'none';
|
||
const dupOf = rule.duplicateOf ?? 'manual';
|
||
if (dupLevel === 'exact') {
|
||
lines.push(`# [DUPLICATE: exact] 重复 ${dupOf}(检测目标完全一致)`);
|
||
} else if (dupLevel === 'overlap') {
|
||
lines.push(`# [DUPLICATE: overlap] 与 ${dupOf} 部分重叠`);
|
||
if (rule.duplicateReason) {
|
||
lines.push(`# 重叠原因:${rule.duplicateReason}`);
|
||
}
|
||
} else {
|
||
lines.push(`# [手动注释] 用户选择不启用此规则`);
|
||
}
|
||
lines.push(`# 如需启用,删除以下每行开头的 # 即可`);
|
||
for (const rl of ruleLines) {
|
||
lines.push(`# ${rl}`);
|
||
}
|
||
}
|
||
|
||
lines.push(''); // 规则间空行
|
||
}
|
||
|
||
return lines.join('\n');
|
||
}
|
||
```
|
||
|
||
`buildFinalYaml` 改为判断入口:
|
||
|
||
```typescript
|
||
export function buildFinalYaml(
|
||
yamlContent: string,
|
||
rules: ImportableRule[],
|
||
decision: PreviewDecision,
|
||
): string {
|
||
if (decision.editedRules && decision.editedRules.length > 0) {
|
||
// 用户编辑过字段,从编辑后的 rules 渲染
|
||
return renderRulesToYaml(decision.editedRules, decision);
|
||
}
|
||
// 无字段编辑,使用原始 yamlContent(保持向后兼容)
|
||
return buildFinalYamlFromRaw(yamlContent, rules, decision);
|
||
}
|
||
|
||
// 原 buildFinalYaml 逻辑重命名为 buildFinalYamlFromRaw
|
||
```
|
||
|
||
## 7. 前端消息协议扩展
|
||
|
||
```typescript
|
||
// 新增消息类型
|
||
interface UpdateRuleMessage {
|
||
type: 'updateRule';
|
||
ruleId: string;
|
||
rule: ImportableRule; // 该规则的完整最新字段值
|
||
}
|
||
|
||
// confirm 消息扩展:确认时携带编辑数据
|
||
interface ConfirmMessage {
|
||
type: 'confirm';
|
||
editedRules?: ImportableRule[]; // 附加全部规则的最新字段值
|
||
}
|
||
```
|
||
|
||
## 8. 文件变更清单
|
||
|
||
| 文件 | 操作 | 内容 |
|
||
|------|------|------|
|
||
| `src/rules/import-types.ts` | 修改 | `PreviewDecision` 新增 `editedRules?: ImportableRule[]` |
|
||
| `src/rules/import-preview.ts` | 重写 | 可折叠卡片 + 展开编辑表单 + 校验 + 传递编辑数据 |
|
||
| `src/rules/import-service.ts` | 修改 | 新增 `renderRulesToYaml`,`buildFinalYaml` 做入口判断 |
|
||
| `src/views/setupView.ts` | 不改 | `applyConversion` 调用不变(PreviewDecision 接口向后兼容) |
|
||
| `src/test/import-dedup.test.ts` | 修改 | 补充编辑后生成 YAML 的测试用例 |
|
||
|
||
## 9. 测试用例
|
||
|
||
追加到 `src/test/import-dedup.test.ts`:
|
||
|
||
| # | 用例 | 输入 | 预期 |
|
||
|---|------|------|------|
|
||
| 13 | 编辑 description 后确认 | rule 的 description 被修改为新值 | 最终 YAML 中该规则的 description 为新值 |
|
||
| 14 | 编辑 severity 后确认 | rule 的 severity 从 warning 改为 error | 最终 YAML 中 severity 为 error |
|
||
| 15 | 编辑 languages 后确认 | 添加 javascript 到 languages | 最终 YAML 含 `languages: [javascript]` |
|
||
| 16 | 编辑后切换为注释 | 编辑字段后 toggle 为注释 | 规则被注释,注释内容为编辑后的值 |
|
||
| 17 | 无编辑场景回退 | `editedRules` 为 undefined | 行为与当前 `buildFinalYamlFromRaw` 一致 |
|
||
|
||
## 10. 实施顺序
|
||
|
||
1. `import-types.ts` — PreviewDecision 扩展
|
||
2. `import-service.ts` — 新增 `renderRulesToYaml`,改造 `buildFinalYaml`
|
||
3. `import-preview.ts` — 重写 Webview(卡片展开 + 编辑表单 + 校验 + 传递编辑数据)
|
||
4. `import-dedup.test.ts` — 补充测试
|
||
|
||
## 11. 设计决策记录
|
||
|
||
| 决策项 | 选择 | 理由 |
|
||
|--------|------|------|
|
||
| 编辑数据传递方式 | confirm 时携带 editedRules 数组 | 无需逐字段实时回传,减少消息往返 |
|
||
| 无编辑时行为 | 回退到原始 yamlContent 行操作 | 保证未编辑场景零变化、零风险 |
|
||
| languages 输入 | 标签式 chip 输入(Enter/, 添加) | 比逗号分隔文本框更直观,防格式错误 |
|
||
| 展开方式 | 点击卡片切换展开/折叠 | 简单直接,不增加额外按钮 |
|
||
| 保留/注释与编辑 | 共存,互不影响 | 出用户需求:保留/注释状态不影响字段编辑 |
|