Files
2026Technology-Competition/docs/superpowers/specs/2026-07-25-import-preview-edit-design.md
T
范智鹏 effcf30802 feat: 适配器 i18n + Provider 动态注册 + SetupView 重构 + jars 资源
- 适配器 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 依赖及测试用例
2026-07-28 22:57:15 +08:00

11 KiB
Raw Blame History

规则导入预览编辑设计

适用项目: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 扩展

// src/rules/import-types.ts

export interface PreviewDecision {
  keepRule: Record<string, boolean>;
  confirmed: boolean;
  editedRules?: ImportableRule[];  // 用户编辑后的完整规则数组(含未修改的规则)
}

editedRulesundefined 表示用户未做任何字段修改,回退到原始 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 替代原有行级操作逻辑:

// 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 改为判断入口:

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. 前端消息协议扩展

// 新增消息类型
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 修改 新增 renderRulesToYamlbuildFinalYaml 做入口判断
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/, 添加) 比逗号分隔文本框更直观,防格式错误
展开方式 点击卡片切换展开/折叠 简单直接,不增加额外按钮
保留/注释与编辑 共存,互不影响 出用户需求:保留/注释状态不影响字段编辑