import * as vscode from 'vscode'; import type { ConversionResult, PreviewDecision, ImportableRule, ValidationIssue } from './import-types'; import { dedupSingleRule } from './import-service'; import { loadActiveRules } from './yaml-parser'; import staticRules from './static-rules.json'; import { t, getLanguage } from '../i18n/messages'; export async function showImportPreview( result: ConversionResult, context: vscode.ExtensionContext, ): Promise { return new Promise((resolve) => { const panel = vscode.window.createWebviewPanel( 'ruleImportPreview', t('importPreview.title'), vscode.ViewColumn.Active, { enableScripts: true }, ); const keepRule: Record = {}; for (const rule of result.rules) { if (!rule.validationIssues?.length) { keepRule[rule.id] = rule.duplicateLevel !== 'exact'; } } panel.webview.html = renderPreviewHtml(result, keepRule); panel.webview.onDidReceiveMessage(async (msg) => { if (msg.type === 'toggleRule') { keepRule[msg.ruleId] = msg.keep; } else if (msg.type === 'addErrorRule') { await handleAddErrorRule(msg, result, keepRule, context, panel); } else if (msg.type === 'confirm') { resolve({ keepRule, confirmed: true, editedRules: msg.editedRules as ImportableRule[] | undefined, }); panel.dispose(); } else if (msg.type === 'cancel') { resolve(null); panel.dispose(); } }); panel.onDidDispose(() => resolve(null)); }); } interface RuleValidationError { field: 'id' | 'severity' | 'description' | 'message'; message: string; } function validateRule(rule: ImportableRule): RuleValidationError | null { if (!rule.id || !rule.id.trim()) { return { field: 'id', message: t('import.validationIdEmpty') }; } if (!['error', 'warning', 'info'].includes(rule.severity)) { return { field: 'severity', message: t('import.validationSeverityInvalid') }; } if (!rule.description || !rule.description.trim()) { return { field: 'description', message: t('import.validationDescEmpty', { 0: rule.id }) }; } if (!rule.message || !rule.message.trim()) { return { field: 'message', message: t('import.validationMsgEmpty', { 0: rule.id }) }; } return null; } async function handleAddErrorRule( msg: { ruleId: string; rule: ImportableRule; }, result: ConversionResult, keepRule: Record, context: vscode.ExtensionContext, panel: vscode.WebviewPanel, ): Promise { const rule = msg.rule; const validationError = validateRule(rule); if (validationError) { panel.webview.postMessage({ type: 'addError', ruleId: msg.ruleId, field: validationError.field, message: validationError.message, }); return; } const original = result.rules.find(r => r.id === msg.ruleId); if (original?.idPlaceholder && rule.id === msg.ruleId) { panel.webview.postMessage({ type: 'addError', ruleId: msg.ruleId, field: 'id', message: t('import.idMissing'), }); return; } const conflict = result.rules.some(r => !r.validationIssues?.length && r.id.toLowerCase() === rule.id.toLowerCase() ); if (conflict) { panel.webview.postMessage({ type: 'addError', ruleId: msg.ruleId, field: 'id', message: t('import.idConflict', { 0: rule.id }), }); return; } const dedup = await dedupSingleRule(rule, context); const level = dedup?.duplicateLevel ?? 'none'; const dedupFailed = !dedup; const idx = result.rules.findIndex(r => r.id === msg.ruleId); if (idx >= 0) { result.rules[idx] = { ...result.rules[idx], id: rule.id, severity: rule.severity, description: rule.description, message: rule.message, languages: rule.languages, excludeLanguages: rule.excludeLanguages, duplicateLevel: level, duplicateOf: dedup?.duplicateOf, duplicateReason: dedup?.duplicateReason, validationIssues: undefined, }; } keepRule[rule.id] = level !== 'exact'; panel.webview.postMessage({ type: 'ruleAdded', ruleId: msg.ruleId, id: rule.id, duplicateLevel: level, duplicateOf: dedup?.duplicateOf, duplicateReason: dedup?.duplicateReason, dedupFailed, }); } const SEVERITY_OPTIONS = ['error', 'warning', 'info']; const SEVERITY_COLORS: Record = { error: '#f48771', warning: '#d29922', info: '#58a6ff', }; function renderPreviewHtml( result: ConversionResult, keepRule: Record, ): string { const errorRules = result.rules.filter(r => r.validationIssues?.length); const cleanRules = result.rules.filter(r => !r.validationIssues?.length); const exactRules = cleanRules.filter(r => r.duplicateLevel === 'exact'); const overlapRules = cleanRules.filter(r => r.duplicateLevel === 'overlap'); const noneRules = cleanRules.filter( r => r.duplicateLevel !== 'exact' && r.duplicateLevel !== 'overlap' ); const totalKept = Object.values(keepRule).filter(Boolean).length; const totalCommented = Object.values(keepRule).filter(v => !v).length; const skippedHint = result.skippedCount ? `
${t('import.template.skipped', { 0: String(result.skippedCount) })}
` : ''; function dupLabel(dupOf: string | undefined): string { return dupOf?.startsWith('custom/') ? `${t('import.customRulePrefix')} ${dupOf.slice(7)}` : (dupOf ?? 'unknown'); } const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; const customRules = workspaceRoot ? loadActiveRules(workspaceRoot) : []; function resolveDupDescription(dupOf: string | undefined): string | undefined { if (!dupOf) { return undefined; } if (dupOf.startsWith('custom/')) { const id = dupOf.slice(7); return customRules.find(r => r.id === id)?.description; } const slash = dupOf.indexOf('/'); const linter = slash > 0 ? dupOf.slice(0, slash) : ''; const linterRules = (staticRules.rules as Record>)[linter]; const rule = linterRules?.find(r => r.id === dupOf); if (!rule) { return undefined; } const lang = getLanguage(); if (lang === 'zh-CN' && rule.descriptionZh) { return `${rule.description} (${rule.descriptionZh})`; } if (lang === 'ja' && rule.descriptionJa) { return `${rule.description} (${rule.descriptionJa})`; } return rule.description; } function renderRuleCard(rule: ImportableRule, isError = false): string { const kept = keepRule[rule.id]; const sevIssue = !!rule.validationIssues?.some(i => i.field === 'severity'); const color = sevIssue ? '#f48771' : (SEVERITY_COLORS[rule.severity] || '#8b949e'); let duplicateInfo = ''; let statusBadge = ''; if (!isError) { if (rule.duplicateLevel === 'exact') { const dupDesc = resolveDupDescription(rule.duplicateOf); duplicateInfo = `
${t('import.dupExactTitle')}
${t('import.dupExactText', { 0: dupLabel(rule.duplicateOf) })}
${dupDesc ? `
${t('import.dupDescriptionLabel', { 0: dupDesc })}
` : ''}
${t('import.dupExactHint')}
`; statusBadge = `${kept ? t('import.badgeRestored') : t('import.badgeWillComment')}`; } else if (rule.duplicateLevel === 'overlap') { const dupDesc = resolveDupDescription(rule.duplicateOf); duplicateInfo = `
${t('import.dupOverlapTitle')}
${t('import.dupOverlapText', { 0: dupLabel(rule.duplicateOf) })}
${dupDesc ? `
${t('import.dupDescriptionLabel', { 0: dupDesc })}
` : ''} ${rule.duplicateReason ? `
${t('import.overlapReason', { 0: rule.duplicateReason })}
` : ''}
`; statusBadge = `${kept ? t('importPreview.keep') : t('importPreview.comment')}`; } else { statusBadge = `${t('importPreview.keep')}`; } } else { statusBadge = `${t('import.cannotImport')}`; } const tagDisplay = (tags: string[] | undefined) => tags && tags.length > 0 ? tags.map(tag => `${tag}×`).join('') : ''; const issueByField = new Map(); if (isError) { for (const i of rule.validationIssues || []) { if (!issueByField.has(i.field)) { issueByField.set(i.field, i); } } } const issueCls = (field: string) => issueByField.has(field) ? ' field-error' : ''; const inputCls = (field: string) => issueByField.has(field) ? 'field-error-input ' : ''; const issueMsg = (field: string) => issueByField.has(field) ? `
${t('import.issuePrefix')} ${issueByField.get(field)!.message}
` : ''; const actionArea = isError ? `` : `
`; const bodyDisplay = isError ? 'block' : 'none'; const expandIcon = '▼'; return `
${sevIssue ? (rule.originalSeverity || t('import.severityMissing')) : rule.severity} ${rule.description}
${statusBadge} ${expandIcon}
${t('import.idLabel')} ${/^rule-\d+$/.test(rule.id) ? `${t('import.placeholderIdHint')}` : ''}
${actionArea}
${duplicateInfo}
${issueMsg('severity')}
${issueMsg('description')}
${issueMsg('message')}
${tagDisplay(rule.languages)}
${tagDisplay(rule.excludeLanguages)}
`; } function renderErrorSection(rules: ImportableRule[]): string { const sectionId = 'section-error'; return `
🚫
${rules.map(r => renderRuleCard(r, true)).join('')}
`; } function renderSection(title: string, icon: string, key: string, rules: ImportableRule[]): string { const sectionId = `section-${key}`; const count = rules.length; const show = rules.some(r => keepRule[r.id] !== undefined); return `
${icon}
${rules.map(r => renderRuleCard(r)).join('')}
`; } return `
${t('importPreview.title')}
${t('importPreview.source', { 0: result.sourceFileName, 1: String(result.rules.length) })}
${t('import.exactDuplicate', { 0: `${exactRules.length}` })}
${t('import.overlapDuplicate', { 0: `${overlapRules.length}` })}
${t('import.noDuplicate', { 0: `${noneRules.length}` })}
${t('import.statusBar', { 0: `${totalKept}`, 1: `${totalCommented}` })}
${skippedHint} ${renderErrorSection(errorRules)} ${renderSection(t('import.sectionExact'), '⛔', 'exact', exactRules)} ${renderSection(t('import.sectionOverlap'), '⚠️', 'overlap', overlapRules)} ${renderSection(t('import.sectionNone'), '✅', 'none', noneRules)}
`; }