- 方法级审查:CodeLens 触发 + 单次 AI 调用(规则匹配 + 6 维度深度审查),新增 method-extractor / status-cache / codeLensProvider - 模板导入:severity 保留原始值 + 占位 id、去重对照统一 known-rules、重复提示条双语翻译、箭头展开/折叠 UI、520 条静态规则补 zh/ja 翻译 - SQL:sql-lint 重命名 sqlfluff + sqlfluff.dialect 方言可配置 + 默认方言调整 - ESLint:v9 flat config 接线修复(overrideConfigFile)+ legacy 迁移提示 - AI:空响应 EmptyContentError + 重试一次 + max_tokens 截断专用报错 - JSP:整文件检查走 PMD JSP 规则集 + scriptlet 包装解析 + 行号映射 - 诊断按 severity + 行号排序
1154 lines
42 KiB
TypeScript
1154 lines
42 KiB
TypeScript
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<PreviewDecision | null> {
|
||
return new Promise((resolve) => {
|
||
const panel = vscode.window.createWebviewPanel(
|
||
'ruleImportPreview',
|
||
t('importPreview.title'),
|
||
vscode.ViewColumn.Active,
|
||
{ enableScripts: true },
|
||
);
|
||
|
||
const keepRule: Record<string, boolean> = {};
|
||
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<string, boolean>,
|
||
context: vscode.ExtensionContext,
|
||
panel: vscode.WebviewPanel,
|
||
): Promise<void> {
|
||
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<string, string> = {
|
||
error: '#f48771',
|
||
warning: '#d29922',
|
||
info: '#58a6ff',
|
||
};
|
||
|
||
function renderPreviewHtml(
|
||
result: ConversionResult,
|
||
keepRule: Record<string, boolean>,
|
||
): 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
|
||
? `<div class="summary-bar" style="border-color:rgba(88,166,255,0.3);color:#58a6ff;">${t('import.template.skipped', { 0: String(result.skippedCount) })}</div>`
|
||
: '';
|
||
|
||
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<string, Array<{ id: string; description: string; descriptionZh?: string; descriptionJa?: string }>>)[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 = `
|
||
<div class="dup-banner dup-exact">
|
||
<div class="dup-banner-title">${t('import.dupExactTitle')}</div>
|
||
<div class="dup-banner-text">${t('import.dupExactText', { 0: dupLabel(rule.duplicateOf) })}</div>
|
||
${dupDesc ? `<div class="dup-banner-desc">${t('import.dupDescriptionLabel', { 0: dupDesc })}</div>` : ''}
|
||
<div class="dup-banner-hint">${t('import.dupExactHint')}</div>
|
||
</div>
|
||
`;
|
||
statusBadge = `<span class="badge badge-exact">${kept ? t('import.badgeRestored') : t('import.badgeWillComment')}</span>`;
|
||
} else if (rule.duplicateLevel === 'overlap') {
|
||
const dupDesc = resolveDupDescription(rule.duplicateOf);
|
||
duplicateInfo = `
|
||
<div class="dup-banner dup-overlap">
|
||
<div class="dup-banner-title">${t('import.dupOverlapTitle')}</div>
|
||
<div class="dup-banner-text">${t('import.dupOverlapText', { 0: dupLabel(rule.duplicateOf) })}</div>
|
||
${dupDesc ? `<div class="dup-banner-desc">${t('import.dupDescriptionLabel', { 0: dupDesc })}</div>` : ''}
|
||
${rule.duplicateReason ? `<div class="dup-banner-reason">${t('import.overlapReason', { 0: rule.duplicateReason })}</div>` : ''}
|
||
</div>
|
||
`;
|
||
statusBadge = `<span class="badge badge-overlap">${kept ? t('importPreview.keep') : t('importPreview.comment')}</span>`;
|
||
} else {
|
||
statusBadge = `<span class="badge badge-none">${t('importPreview.keep')}</span>`;
|
||
}
|
||
} else {
|
||
statusBadge = `<span class="badge" style="background:rgba(248,81,73,0.15);color:#f48771;">${t('import.cannotImport')}</span>`;
|
||
}
|
||
|
||
const tagDisplay = (tags: string[] | undefined) => tags && tags.length > 0 ? tags.map(tag => `<span class="tag" data-value="${tag}">${tag}<span class="tag-remove" data-tag="${tag}">×</span></span>`).join('') : '';
|
||
|
||
const issueByField = new Map<string, ValidationIssue>();
|
||
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)
|
||
? `<div class="field-error-msg">${t('import.issuePrefix')} ${issueByField.get(field)!.message}</div>`
|
||
: '';
|
||
|
||
const actionArea = isError
|
||
? `<button class="add-btn" data-addbtn="${rule.id}" onclick="event.stopPropagation();addErrorRule(this)">${t('import.add')}</button>`
|
||
: `<div class="keep-toggle">
|
||
<button class="toggle-btn ${kept ? 'active' : ''}" data-action="keep" onclick="event.stopPropagation();toggleKeep(this, true)">${t('importPreview.keep')}</button>
|
||
<button class="toggle-btn ${!kept ? 'active' : ''}" data-action="comment" onclick="event.stopPropagation();toggleKeep(this, false)">${t('importPreview.comment')}</button>
|
||
</div>`;
|
||
|
||
const bodyDisplay = isError ? 'block' : 'none';
|
||
const expandIcon = '▼';
|
||
|
||
return `
|
||
<div class="rule-card${isError ? ' expanded' : ''}" data-ruleid="${rule.id}"${isError ? ' data-error="true"' : ''}>
|
||
<div class="rule-card-header" onclick="toggleCard(this)">
|
||
<div class="rule-card-summary">
|
||
<input class="rule-id-input${/^rule-\d+$/.test(rule.id) ? ' placeholder-id' : ''}" value="${rule.id}" onchange="syncId(this)" onclick="event.stopPropagation()">
|
||
<span class="rule-severity-tag" style="background:${color}20;color:${color};border:1px solid ${color}40;">${sevIssue ? (rule.originalSeverity || t('import.severityMissing')) : rule.severity}</span>
|
||
<span class="rule-desc-preview">${rule.description}</span>
|
||
</div>
|
||
<div class="rule-card-meta">
|
||
${statusBadge}
|
||
<span class="expand-icon">${expandIcon}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="rule-card-body" id="body-${rule.id}" style="display:${bodyDisplay};">
|
||
<div class="edit-header">
|
||
<div class="id-row">
|
||
<span class="id-display-label">${t('import.idLabel')}</span>
|
||
<input class="id-display-input${/^rule-\d+$/.test(rule.id) ? ' placeholder-id' : ''}" value="${rule.id}" onchange="syncId(this)">
|
||
${/^rule-\d+$/.test(rule.id) ? `<span class="placeholder-hint">${t('import.placeholderIdHint')}</span>` : ''}
|
||
</div>
|
||
${actionArea}
|
||
</div>
|
||
|
||
${duplicateInfo}
|
||
|
||
<div class="edit-field${issueCls('severity')}">
|
||
<label>${t('import.severityLabel')}</label>
|
||
<select class="${inputCls('severity')}" onchange="updateRule(this,'severity',this.value)">
|
||
${sevIssue ? `<option value="" disabled selected>${t('import.severitySelectHint')}</option>` : ''}
|
||
${SEVERITY_OPTIONS.map(s => `<option value="${s}" ${!sevIssue && s === rule.severity ? 'selected' : ''}>${s}</option>`).join('')}
|
||
</select>
|
||
${issueMsg('severity')}
|
||
</div>
|
||
|
||
<div class="edit-field${issueCls('description')}">
|
||
<label>${t('import.descriptionLabel')}</label>
|
||
<textarea rows="2" class="${inputCls('description')}" onchange="updateRule(this,'description',this.value)">${rule.description}</textarea>
|
||
${issueMsg('description')}
|
||
</div>
|
||
|
||
<div class="edit-field${issueCls('message')}">
|
||
<label>${t('import.messageLabel')}</label>
|
||
<textarea rows="2" class="${inputCls('message')}" onchange="updateRule(this,'message',this.value)">${rule.message}</textarea>
|
||
${issueMsg('message')}
|
||
</div>
|
||
|
||
<div class="edit-field">
|
||
<label>${t('import.languagesLabel')}</label>
|
||
<div class="tag-input-wrapper">
|
||
<div class="tag-list" data-ruleid="${rule.id}" data-field="languages">
|
||
${tagDisplay(rule.languages)}
|
||
</div>
|
||
<input class="tag-input" data-ruleid="${rule.id}" data-field="languages" placeholder="${t('import.tagPlaceholder')}" value="">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="edit-field">
|
||
<label>${t('import.excludeLanguagesLabel')}</label>
|
||
<div class="tag-input-wrapper">
|
||
<div class="tag-list" data-ruleid="${rule.id}" data-field="excludeLanguages">
|
||
${tagDisplay(rule.excludeLanguages)}
|
||
</div>
|
||
<input class="tag-input" data-ruleid="${rule.id}" data-field="excludeLanguages" placeholder="${t('import.tagPlaceholder')}" value="">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderErrorSection(rules: ImportableRule[]): string {
|
||
const sectionId = 'section-error';
|
||
return `
|
||
<div class="section-wrapper expanded" data-section-wrap="error" style="margin-bottom:12px;${rules.length === 0 ? 'display:none;' : ''}">
|
||
<div class="section-header" onclick="toggleSection('${sectionId}')">
|
||
<span style="font-size:14px;">🚫</span>
|
||
<span class="section-title" data-section-title="error"></span>
|
||
<span class="section-arrow">▼</span>
|
||
</div>
|
||
<div id="${sectionId}" data-section="error" style="display:block;">
|
||
${rules.map(r => renderRuleCard(r, true)).join('')}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
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 `
|
||
<div class="section-wrapper${show ? ' expanded' : ''}" data-section-wrap="${key}" style="margin-bottom:12px;${count === 0 ? 'display:none;' : ''}">
|
||
<div class="section-header" onclick="toggleSection('${sectionId}')">
|
||
<span style="font-size:14px;">${icon}</span>
|
||
<span class="section-title" data-section-title="${key}"></span>
|
||
<span class="section-arrow">▼</span>
|
||
</div>
|
||
<div id="${sectionId}" data-section="${key}" style="display:${show ? 'block' : 'none'};">
|
||
${rules.map(r => renderRuleCard(r)).join('')}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
return `<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body {
|
||
font-family: var(--vscode-font-family);
|
||
font-size: var(--vscode-font-size);
|
||
color: var(--vscode-foreground);
|
||
background: var(--vscode-editor-background);
|
||
padding: 16px; line-height: 1.5;
|
||
}
|
||
.header {
|
||
border-bottom: 1px solid var(--vscode-panel-border);
|
||
padding-bottom: 12px; margin-bottom: 12px;
|
||
}
|
||
.header-title { font-size: 16px; font-weight: 700; margin-bottom: 4px; }
|
||
.header-sub { font-size: 12px; color: var(--vscode-descriptionForeground); }
|
||
.summary {
|
||
display: flex; gap: 12px; margin-bottom: 16px;
|
||
}
|
||
.summary-item {
|
||
padding: 8px 12px; border-radius: 6px; font-size: 12px;
|
||
border: 1px solid var(--vscode-panel-border);
|
||
background: var(--vscode-sideBar-background, var(--vscode-editor-background));
|
||
}
|
||
.summary-bar {
|
||
margin-bottom: 16px; padding: 8px 12px; border-radius: 6px;
|
||
font-size: 12px; background: rgba(139,92,246,0.1);
|
||
border: 1px solid rgba(139,92,246,0.3); color: #a78bfa;
|
||
}
|
||
.summary-bar.warn {
|
||
border-color: rgba(210,153,34,0.3); color: #d29922;
|
||
background: rgba(210,153,34,0.1);
|
||
}
|
||
.actions {
|
||
display: flex; gap: 8px; padding-top: 12px;
|
||
border-top: 1px solid var(--vscode-panel-border);
|
||
justify-content: flex-end;
|
||
}
|
||
.btn {
|
||
padding: 6px 16px; border: 1px solid var(--vscode-panel-border);
|
||
background: var(--vscode-button-secondaryBackground);
|
||
color: var(--vscode-button-secondaryForeground);
|
||
border-radius: 6px; cursor: pointer; font-size: 12px;
|
||
}
|
||
.btn:hover { background: var(--vscode-button-secondaryHoverBackground); }
|
||
.btn-primary { background: #7c3aed; color: #fff; border-color: #7c3aed; }
|
||
.btn-primary:hover { background: #8b5cf6; }
|
||
.section-header {
|
||
display: flex; align-items: center; gap: 8px; margin-bottom: 8px;
|
||
cursor: pointer; padding: 4px 0;
|
||
}
|
||
.section-title { font-weight: 600; font-size: 13px; }
|
||
.section-arrow {
|
||
display: inline-block;
|
||
font-size: 10px;
|
||
color: var(--vscode-descriptionForeground);
|
||
transform: rotate(-90deg);
|
||
transition: transform 0.15s ease;
|
||
}
|
||
.rule-card {
|
||
border: 1px solid var(--vscode-panel-border);
|
||
border-radius: 8px; margin-bottom: 8px; overflow: hidden;
|
||
}
|
||
.rule-card-header {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 10px 12px; cursor: pointer; gap: 8px;
|
||
}
|
||
.rule-card-header:hover { background: var(--vscode-list-hoverBackground); }
|
||
.rule-card-summary {
|
||
display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0;
|
||
}
|
||
.rule-id-input {
|
||
font-family: monospace; font-size: 13px; font-weight: 600; white-space: nowrap;
|
||
background: transparent;
|
||
border: 1px solid transparent;
|
||
border-radius: 3px;
|
||
color: var(--vscode-foreground);
|
||
padding: 1px 4px; outline: none;
|
||
width: auto; min-width: 120px;
|
||
}
|
||
.rule-id-input:focus {
|
||
border-color: var(--vscode-focusBorder, #7c3aed);
|
||
background: var(--vscode-input-background);
|
||
}
|
||
.rule-id-input.placeholder-id {
|
||
border-color: #f59e0b !important;
|
||
box-shadow: 0 0 0 1px rgba(245, 158, 11, 0.3);
|
||
}
|
||
.rule-severity-tag {
|
||
padding: 1px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; white-space: nowrap;
|
||
}
|
||
.rule-desc-preview {
|
||
font-size: 12px; color: var(--vscode-descriptionForeground);
|
||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||
}
|
||
.rule-card-meta {
|
||
display: flex; align-items: center; gap: 8px; flex-shrink: 0;
|
||
}
|
||
.expand-icon {
|
||
display: inline-block;
|
||
font-size: 10px;
|
||
color: var(--vscode-descriptionForeground);
|
||
transform: rotate(-90deg);
|
||
transition: transform 0.15s ease;
|
||
}
|
||
.rule-card.expanded .expand-icon,
|
||
.section-wrapper.expanded .section-arrow {
|
||
transform: rotate(0deg);
|
||
}
|
||
.badge {
|
||
padding: 1px 8px; border-radius: 10px; font-size: 11px; font-weight: 600;
|
||
}
|
||
.badge-exact { background: rgba(248,81,73,0.15); color: #f48771; }
|
||
.badge-overlap { background: rgba(210,153,34,0.15); color: #d29922; }
|
||
.badge-none { background: rgba(35,134,54,0.15); color: #3fb950; }
|
||
.rule-card-body {
|
||
padding: 0 12px 12px; border-top: 1px solid var(--vscode-panel-border);
|
||
}
|
||
.edit-header {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 10px 0 8px;
|
||
}
|
||
.id-row {
|
||
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
||
}
|
||
.id-row .field-error-msg {
|
||
flex-basis: 100%; margin-left: 0;
|
||
}
|
||
.keep-toggle { display: flex; gap: 4px; }
|
||
.toggle-btn {
|
||
padding: 3px 12px; border-radius: 4px; cursor: pointer; font-size: 11px;
|
||
border: 1px solid var(--vscode-panel-border);
|
||
background: transparent; color: var(--vscode-foreground);
|
||
}
|
||
.toggle-btn.active {
|
||
background: rgba(35,134,54,0.15); color: #3fb950; border-color: rgba(35,134,54,0.3);
|
||
}
|
||
.toggle-btn.active[data-action="comment"] {
|
||
background: rgba(248,81,73,0.15); color: #f48771; border-color: rgba(248,81,73,0.3);
|
||
}
|
||
.add-btn {
|
||
padding: 4px 14px; border-radius: 4px; cursor: pointer; font-size: 11px;
|
||
border: 1px solid rgba(35,134,54,0.4);
|
||
background: rgba(35,134,54,0.15); color: #3fb950;
|
||
}
|
||
.add-btn:hover { background: rgba(35,134,54,0.25); }
|
||
.add-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||
.field-error-input {
|
||
border-color: rgba(248,81,73,0.7) !important;
|
||
box-shadow: 0 0 0 1px rgba(248,81,73,0.25);
|
||
}
|
||
.field-error-msg {
|
||
color: #f48771; font-size: 11px; margin-top: 4px;
|
||
}
|
||
.edit-field { margin-top: 10px; }
|
||
.edit-field label {
|
||
display: block; font-size: 11px; font-weight: 600;
|
||
color: var(--vscode-descriptionForeground); margin-bottom: 4px;
|
||
text-transform: uppercase; letter-spacing: 0.5px;
|
||
}
|
||
.id-display-label {
|
||
font-size: 11px; font-weight: 600;
|
||
color: var(--vscode-descriptionForeground);
|
||
text-transform: uppercase; letter-spacing: 0.5px;
|
||
}
|
||
.id-display-input {
|
||
font-family: monospace; font-size: 13px; font-weight: 600;
|
||
background: var(--vscode-input-background);
|
||
border: 1px solid var(--vscode-panel-border);
|
||
border-radius: 3px;
|
||
color: var(--vscode-input-foreground);
|
||
padding: 4px 8px; outline: none;
|
||
width: auto; min-width: 200px;
|
||
}
|
||
.id-display-input:focus {
|
||
border-color: var(--vscode-focusBorder, #7c3aed);
|
||
}
|
||
.id-display-input.placeholder-id {
|
||
border-color: #f59e0b !important;
|
||
box-shadow: 0 0 0 1px rgba(245, 158, 11, 0.3);
|
||
}
|
||
.placeholder-hint {
|
||
color: #d29922; font-size: 11px;
|
||
}
|
||
.edit-field select, .edit-field textarea {
|
||
width: 100%; padding: 6px 8px; border-radius: 4px;
|
||
border: 1px solid var(--vscode-panel-border);
|
||
background: var(--vscode-input-background);
|
||
color: var(--vscode-input-foreground);
|
||
font-family: var(--vscode-font-family);
|
||
font-size: var(--vscode-font-size);
|
||
}
|
||
.edit-field textarea { resize: vertical; }
|
||
.tag-input-wrapper {
|
||
border: 1px solid var(--vscode-panel-border);
|
||
border-radius: 4px; padding: 4px 6px;
|
||
background: var(--vscode-input-background);
|
||
display: flex; flex-wrap: wrap; gap: 4px; align-items: center;
|
||
}
|
||
.tag-list { display: flex; flex-wrap: wrap; gap: 4px; }
|
||
.tag {
|
||
display: inline-flex; align-items: center; gap: 3px;
|
||
padding: 1px 6px; border-radius: 3px; font-size: 11px;
|
||
background: rgba(88,166,255,0.15); color: #58a6ff;
|
||
border: 1px solid rgba(88,166,255,0.3);
|
||
}
|
||
.tag-remove {
|
||
cursor: pointer; font-size: 13px; line-height: 1; opacity: 0.7;
|
||
}
|
||
.tag-remove:hover { opacity: 1; }
|
||
.tag-input {
|
||
border: none; outline: none; flex: 1; min-width: 80px;
|
||
background: transparent; color: var(--vscode-input-foreground);
|
||
font-size: 12px; padding: 2px 0;
|
||
}
|
||
.tag-input::placeholder { color: var(--vscode-input-placeholderForeground); }
|
||
.validation-error {
|
||
padding: 8px 12px; margin-bottom: 12px; border-radius: 6px;
|
||
background: rgba(248,81,73,0.15); color: #f48771;
|
||
border: 1px solid rgba(248,81,73,0.3); font-size: 12px;
|
||
}
|
||
.dup-banner {
|
||
border-radius: 6px; padding: 8px 12px; margin-top: 10px;
|
||
font-size: 12px; line-height: 1.6;
|
||
}
|
||
.dup-exact {
|
||
background: rgba(248,81,73,0.1);
|
||
border: 1px solid rgba(248,81,73,0.3);
|
||
border-left: 3px solid #f48771;
|
||
}
|
||
.dup-overlap {
|
||
background: rgba(210,153,34,0.1);
|
||
border: 1px solid rgba(210,153,34,0.3);
|
||
border-left: 3px solid #d29922;
|
||
}
|
||
.dup-banner-title { font-weight: 600; }
|
||
.dup-exact .dup-banner-title { color: #f48771; }
|
||
.dup-overlap .dup-banner-title { color: #d29922; }
|
||
.dup-banner-desc { color: var(--vscode-foreground); }
|
||
.dup-banner-reason { color: #d29922; }
|
||
.dup-banner-hint { color: var(--vscode-descriptionForeground); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<div class="header-title">${t('importPreview.title')}</div>
|
||
<div class="header-sub">${t('importPreview.source', { 0: result.sourceFileName, 1: String(result.rules.length) })}</div>
|
||
</div>
|
||
<div class="summary">
|
||
<div class="summary-item" style="border-color:rgba(248,81,73,0.3);color:#f48771;">${t('import.exactDuplicate', { 0: `<span id="count-exact">${exactRules.length}</span>` })}</div>
|
||
<div class="summary-item" style="border-color:rgba(210,153,34,0.3);color:#d29922;">${t('import.overlapDuplicate', { 0: `<span id="count-overlap">${overlapRules.length}</span>` })}</div>
|
||
<div class="summary-item" style="border-color:rgba(35,134,54,0.3);color:#3fb950;">${t('import.noDuplicate', { 0: `<span id="count-none">${noneRules.length}</span>` })}</div>
|
||
</div>
|
||
<div class="summary-bar" id="statusBar">
|
||
${t('import.statusBar', { 0: `<b id="keepCount">${totalKept}</b>`, 1: `<b id="commentCount">${totalCommented}</b>` })}
|
||
<span id="editHint" style="display:none;">${t('import.editedHint', { 0: '<b id="editCount">0</b>' })}</span>
|
||
</div>
|
||
<div id="addHint" class="summary-bar warn" style="display:none;"></div>
|
||
|
||
<div id="validationError" class="validation-error" style="display:none;"></div>
|
||
|
||
${skippedHint}
|
||
${renderErrorSection(errorRules)}
|
||
${renderSection(t('import.sectionExact'), '⛔', 'exact', exactRules)}
|
||
${renderSection(t('import.sectionOverlap'), '⚠️', 'overlap', overlapRules)}
|
||
${renderSection(t('import.sectionNone'), '✅', 'none', noneRules)}
|
||
|
||
<div id="emptyValidHint" class="validation-error" style="display:none;">${t('import.emptyValidRules')}</div>
|
||
|
||
<div class="actions">
|
||
<button class="btn" onclick="cancel()">${t('importPreview.cancel')}</button>
|
||
<button class="btn btn-primary" id="confirmBtn" onclick="doConfirm()">${t('importPreview.confirm')}</button>
|
||
</div>
|
||
|
||
<script>
|
||
const vscode = acquireVsCodeApi();
|
||
const editedRules = {};
|
||
let addedRules = 0;
|
||
let addingRuleId = null;
|
||
const VALIDATION_DESC_EMPTY = ${JSON.stringify(t('import.validationDescEmpty'))};
|
||
const VALIDATION_MSG_EMPTY = ${JSON.stringify(t('import.validationMsgEmpty'))};
|
||
const VALIDATION_ID_EMPTY = ${JSON.stringify(t('import.validationIdEmpty'))};
|
||
const ADD_TEXT = ${JSON.stringify(t('import.add'))};
|
||
const ADDING_TEXT = ${JSON.stringify(t('import.adding'))};
|
||
const KEEP_TEXT = ${JSON.stringify(t('importPreview.keep'))};
|
||
const COMMENT_TEXT = ${JSON.stringify(t('importPreview.comment'))};
|
||
const WILL_COMMENT_TEXT = ${JSON.stringify(t('import.badgeWillComment'))};
|
||
const DEDUP_FALLBACK_TEXT = ${JSON.stringify(t('import.addDedupFallback'))};
|
||
const CUSTOM_PREFIX = ${JSON.stringify(t('import.customRulePrefix'))};
|
||
const DUPLICATE_OF_TEXT = ${JSON.stringify(t('import.duplicateOf'))};
|
||
const OVERLAP_WITH_TEXT = ${JSON.stringify(t('import.overlapWith'))};
|
||
const OVERLAP_REASON_TEXT = ${JSON.stringify(t('import.overlapReason'))};
|
||
const RULE_COUNT_TEMPLATE = ${JSON.stringify(t('import.ruleCount'))};
|
||
const SECTION_TITLES = {
|
||
error: ${JSON.stringify(t('import.sectionInvalid'))},
|
||
exact: ${JSON.stringify(t('import.sectionExact'))},
|
||
overlap: ${JSON.stringify(t('import.sectionOverlap'))},
|
||
none: ${JSON.stringify(t('import.sectionNone'))},
|
||
};
|
||
|
||
function setSectionTitle(key, count) {
|
||
const el = document.querySelector('[data-section-title="' + key + '"]');
|
||
if (el) {
|
||
el.textContent = SECTION_TITLES[key] + '(' + RULE_COUNT_TEMPLATE.replace('{0}', count) + ')';
|
||
}
|
||
}
|
||
|
||
function fmt(tpl, v) {
|
||
return tpl.replace('{0}', v);
|
||
}
|
||
|
||
function syncId(el) {
|
||
const card = el.closest('.rule-card');
|
||
if (!card) return;
|
||
const originalId = card.dataset.ruleid;
|
||
const newValue = el.value;
|
||
const headerInput = card.querySelector('.rule-id-input');
|
||
const panelInput = card.querySelector('.id-display-input');
|
||
if (headerInput) headerInput.value = newValue;
|
||
if (panelInput) panelInput.value = newValue;
|
||
|
||
if (!editedRules[originalId]) editedRules[originalId] = {};
|
||
editedRules[originalId].id = newValue;
|
||
updateEditHint();
|
||
|
||
const isPlaceholder = /^rule-\\d+$/.test(newValue);
|
||
[headerInput, panelInput].forEach(input => {
|
||
if (input) {
|
||
input.classList.toggle('placeholder-id', isPlaceholder);
|
||
}
|
||
});
|
||
}
|
||
|
||
function toggleCard(el) {
|
||
const card = el.closest('.rule-card');
|
||
if (!card) return;
|
||
const body = card.querySelector('.rule-card-body');
|
||
if (body.style.display === 'none') {
|
||
body.style.display = 'block';
|
||
card.classList.add('expanded');
|
||
} else {
|
||
body.style.display = 'none';
|
||
card.classList.remove('expanded');
|
||
}
|
||
}
|
||
|
||
function toggleSection(id) {
|
||
const el = document.getElementById(id);
|
||
const wrap = el.closest('.section-wrapper');
|
||
if (el.style.display === 'none') {
|
||
el.style.display = 'block';
|
||
wrap.classList.add('expanded');
|
||
} else {
|
||
el.style.display = 'none';
|
||
wrap.classList.remove('expanded');
|
||
}
|
||
}
|
||
|
||
function toggleKeep(el, keep) {
|
||
const card = el.closest('.rule-card');
|
||
if (!card) return;
|
||
const ruleId = card.dataset.ruleid;
|
||
vscode.postMessage({ type: 'toggleRule', ruleId, keep });
|
||
const btns = card.querySelectorAll('.toggle-btn');
|
||
btns.forEach(b => b.classList.toggle('active', (keep && b.dataset.action === 'keep') || (!keep && b.dataset.action === 'comment')));
|
||
updateSummary();
|
||
}
|
||
|
||
function updateRule(el, field, value) {
|
||
const card = el.closest('.rule-card');
|
||
if (!card) return;
|
||
const ruleId = card.dataset.ruleid;
|
||
if (!editedRules[ruleId]) {
|
||
editedRules[ruleId] = {};
|
||
}
|
||
editedRules[ruleId][field] = value;
|
||
updateEditHint();
|
||
}
|
||
|
||
function collectCardRule(card) {
|
||
if (!card) return null;
|
||
const idInput = card.querySelector('.id-display-input');
|
||
const severityEl = card.querySelector('.edit-field select');
|
||
const textareas = card.querySelectorAll('.edit-field textarea');
|
||
const descEl = textareas[0];
|
||
const msgEl = textareas[1];
|
||
const langList = card.querySelector('.tag-list[data-field="languages"]');
|
||
const exclList = card.querySelector('.tag-list[data-field="excludeLanguages"]');
|
||
|
||
return {
|
||
id: idInput ? idInput.value.trim() : card.dataset.ruleid,
|
||
severity: severityEl ? severityEl.value : 'warning',
|
||
description: descEl ? descEl.value : '',
|
||
message: msgEl ? msgEl.value : '',
|
||
languages: langList ? Array.from(langList.querySelectorAll('.tag')).map(tag => tag.dataset.value) : [],
|
||
excludeLanguages: exclList ? Array.from(exclList.querySelectorAll('.tag')).map(tag => tag.dataset.value) : [],
|
||
};
|
||
}
|
||
|
||
function collectEditedRules() {
|
||
const result = [];
|
||
document.querySelectorAll('.rule-card').forEach(card => {
|
||
if (card.hasAttribute('data-error')) { return; }
|
||
const rule = collectCardRule(card);
|
||
if (rule) { result.push(rule); }
|
||
});
|
||
return result;
|
||
}
|
||
|
||
function addErrorRule(el) {
|
||
if (addingRuleId) { return; }
|
||
const card = el.closest('.rule-card');
|
||
if (!card) return;
|
||
const btn = el;
|
||
if (btn.disabled) { return; }
|
||
btn.disabled = true;
|
||
btn.textContent = ADDING_TEXT;
|
||
const ruleId = card.dataset.ruleid;
|
||
addingRuleId = ruleId;
|
||
const rule = collectCardRule(card);
|
||
if (!rule) {
|
||
addingRuleId = null;
|
||
btn.disabled = false;
|
||
btn.textContent = ADD_TEXT;
|
||
return;
|
||
}
|
||
vscode.postMessage({ type: 'addErrorRule', ruleId, rule });
|
||
}
|
||
|
||
function fieldElement(card, field) {
|
||
if (field === 'id') return card.querySelector('.id-display-input');
|
||
if (field === 'severity') return card.querySelector('.edit-field select');
|
||
const tas = card.querySelectorAll('.edit-field textarea');
|
||
return field === 'description' ? (tas[0] || null) : (tas[1] || null);
|
||
}
|
||
|
||
function setFieldError(card, field, message) {
|
||
const el = fieldElement(card, field);
|
||
if (!el) return;
|
||
el.classList.add('field-error-input');
|
||
const wrap = el.closest('.edit-field, .id-row');
|
||
if (!wrap) return;
|
||
wrap.classList.add('field-error');
|
||
let msg = wrap.querySelector('.field-error-msg');
|
||
if (!msg) {
|
||
msg = document.createElement('div');
|
||
msg.className = 'field-error-msg';
|
||
wrap.appendChild(msg);
|
||
}
|
||
msg.textContent = '⚠ ' + message;
|
||
}
|
||
|
||
function clearFieldError(card, field) {
|
||
const el = fieldElement(card, field);
|
||
if (!el) return;
|
||
el.classList.remove('field-error-input');
|
||
const wrap = el.closest('.edit-field, .id-row');
|
||
if (wrap) {
|
||
wrap.classList.remove('field-error');
|
||
const msg = wrap.querySelector('.field-error-msg');
|
||
if (msg) { msg.remove(); }
|
||
}
|
||
}
|
||
|
||
function clearCardFieldErrors(card) {
|
||
card.querySelectorAll('.field-error-input').forEach(function (el) {
|
||
el.classList.remove('field-error-input');
|
||
});
|
||
card.querySelectorAll('.field-error').forEach(function (wrap) {
|
||
wrap.classList.remove('field-error');
|
||
const msg = wrap.querySelector('.field-error-msg');
|
||
if (msg) { msg.remove(); }
|
||
});
|
||
}
|
||
|
||
function liveClear(event) {
|
||
const card = event.target.closest('.rule-card');
|
||
if (!card || !card.hasAttribute('data-error')) return;
|
||
const target = event.target;
|
||
if (target.classList.contains('id-display-input') || target.classList.contains('rule-id-input')) {
|
||
if (target.value.trim()) { clearFieldError(card, 'id'); }
|
||
} else if (target.tagName === 'SELECT') {
|
||
clearFieldError(card, 'severity');
|
||
} else if (target.tagName === 'TEXTAREA') {
|
||
const tas = card.querySelectorAll('.edit-field textarea');
|
||
const field = tas[0] === target ? 'description' : (tas[1] === target ? 'message' : null);
|
||
if (field && target.value.trim()) { clearFieldError(card, field); }
|
||
}
|
||
}
|
||
|
||
function showCardError(ruleId, field, message) {
|
||
const card = document.querySelector('.rule-card[data-ruleid="' + ruleId + '"]');
|
||
if (card && field) {
|
||
setFieldError(card, field, message);
|
||
}
|
||
const btn = document.querySelector('[data-addbtn="' + ruleId + '"]');
|
||
if (btn) {
|
||
btn.disabled = false;
|
||
btn.textContent = ADD_TEXT;
|
||
}
|
||
addingRuleId = null;
|
||
}
|
||
|
||
function dupInfoHtml(level, dupOf, reason) {
|
||
const prefix = dupOf && dupOf.startsWith('custom/')
|
||
? CUSTOM_PREFIX + ' ' + dupOf.slice(7)
|
||
: (dupOf || 'unknown');
|
||
if (level === 'exact') {
|
||
return '<div style="color:#8b949e;font-size:12px;margin-top:4px;">' + fmt(DUPLICATE_OF_TEXT, prefix) + '</div>';
|
||
}
|
||
if (level === 'overlap') {
|
||
let html = '<div style="color:#d29922;font-size:12px;margin-top:4px;">' + fmt(OVERLAP_WITH_TEXT, prefix) + '</div>';
|
||
if (reason) {
|
||
html += '<div style="color:#8b949e;font-size:12px;margin-top:2px;">' + fmt(OVERLAP_REASON_TEXT, reason) + '</div>';
|
||
}
|
||
return html;
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function moveCardToSection(msg) {
|
||
const card = document.querySelector('.rule-card[data-ruleid="' + msg.ruleId + '"]');
|
||
if (!card) return;
|
||
|
||
card.dataset.ruleid = msg.id;
|
||
const headerInput = card.querySelector('.rule-id-input');
|
||
const panelInput = card.querySelector('.id-display-input');
|
||
if (headerInput) headerInput.value = msg.id;
|
||
if (panelInput) panelInput.value = msg.id;
|
||
const isPlaceholder = /^rule-\\d+$/.test(msg.id);
|
||
[headerInput, panelInput].forEach(el => {
|
||
if (el) el.classList.toggle('placeholder-id', isPlaceholder);
|
||
});
|
||
|
||
card.removeAttribute('data-error');
|
||
clearCardFieldErrors(card);
|
||
|
||
const addBtn = card.querySelector('.add-btn');
|
||
if (addBtn) { addBtn.remove(); }
|
||
|
||
const kept = msg.duplicateLevel !== 'exact';
|
||
const toggle = document.createElement('div');
|
||
toggle.className = 'keep-toggle';
|
||
|
||
function makeToggleBtn(action, active, label) {
|
||
const btn = document.createElement('button');
|
||
btn.className = 'toggle-btn' + (active ? ' active' : '');
|
||
btn.dataset.action = action;
|
||
btn.textContent = label;
|
||
btn.addEventListener('click', function (ev) {
|
||
ev.stopPropagation();
|
||
toggleKeep(this, action === 'keep');
|
||
});
|
||
return btn;
|
||
}
|
||
|
||
toggle.appendChild(makeToggleBtn('keep', kept, KEEP_TEXT));
|
||
toggle.appendChild(makeToggleBtn('comment', !kept, COMMENT_TEXT));
|
||
card.querySelector('.edit-header').appendChild(toggle);
|
||
|
||
const meta = card.querySelector('.rule-card-meta');
|
||
const oldBadge = meta.querySelector('.badge');
|
||
if (oldBadge) { oldBadge.remove(); }
|
||
const badge = document.createElement('span');
|
||
badge.className = 'badge';
|
||
if (msg.duplicateLevel === 'exact') {
|
||
badge.classList.add('badge-exact');
|
||
badge.textContent = WILL_COMMENT_TEXT;
|
||
} else if (msg.duplicateLevel === 'overlap') {
|
||
badge.classList.add('badge-overlap');
|
||
badge.textContent = KEEP_TEXT;
|
||
} else {
|
||
badge.classList.add('badge-none');
|
||
badge.textContent = KEEP_TEXT;
|
||
}
|
||
const icon = meta.querySelector('.expand-icon');
|
||
meta.insertBefore(badge, icon);
|
||
|
||
const body = card.querySelector('.rule-card-body');
|
||
const infoHtml = dupInfoHtml(msg.duplicateLevel, msg.duplicateOf, msg.duplicateReason);
|
||
if (infoHtml) {
|
||
const infoDiv = document.createElement('div');
|
||
infoDiv.innerHTML = infoHtml;
|
||
const firstField = body.querySelector('.edit-field');
|
||
body.insertBefore(infoDiv, firstField);
|
||
}
|
||
|
||
const section = msg.duplicateLevel === 'exact'
|
||
? 'exact'
|
||
: (msg.duplicateLevel === 'overlap' ? 'overlap' : 'none');
|
||
const target = document.querySelector('[data-section="' + section + '"]');
|
||
if (target) {
|
||
const wrap = target.closest('[data-section-wrap]');
|
||
if (wrap) {
|
||
wrap.style.display = '';
|
||
wrap.classList.add('expanded');
|
||
}
|
||
target.style.display = 'block';
|
||
target.appendChild(card);
|
||
}
|
||
|
||
addedRules++;
|
||
if (msg.dedupFailed) {
|
||
showAddHint(DEDUP_FALLBACK_TEXT);
|
||
}
|
||
updateSectionCounts();
|
||
updateSummary();
|
||
updateEditHint();
|
||
}
|
||
|
||
function showAddHint(text) {
|
||
const el = document.getElementById('addHint');
|
||
el.textContent = text;
|
||
el.style.display = 'block';
|
||
setTimeout(function () { el.style.display = 'none'; }, 5000);
|
||
}
|
||
|
||
function updateSectionCounts() {
|
||
const sections = ['error', 'exact', 'overlap', 'none'];
|
||
const counts = {};
|
||
let totalValid = 0;
|
||
for (const key of sections) {
|
||
const container = document.querySelector('[data-section="' + key + '"]');
|
||
const count = container ? container.querySelectorAll('.rule-card').length : 0;
|
||
counts[key] = count;
|
||
if (key !== 'error') { totalValid += count; }
|
||
setSectionTitle(key, count);
|
||
if (container) {
|
||
const wrap = container.closest('[data-section-wrap]');
|
||
if (wrap) { wrap.style.display = count > 0 ? '' : 'none'; }
|
||
}
|
||
}
|
||
document.getElementById('count-exact').textContent = counts.exact;
|
||
document.getElementById('count-overlap').textContent = counts.overlap;
|
||
document.getElementById('count-none').textContent = counts.none;
|
||
|
||
const btn = document.getElementById('confirmBtn');
|
||
const hint = document.getElementById('emptyValidHint');
|
||
if (totalValid === 0) {
|
||
btn.disabled = true;
|
||
btn.style.opacity = '0.5';
|
||
btn.style.cursor = 'not-allowed';
|
||
hint.style.display = 'block';
|
||
} else {
|
||
btn.disabled = false;
|
||
btn.style.opacity = '';
|
||
btn.style.cursor = '';
|
||
hint.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
window.addEventListener('message', function (e) {
|
||
const msg = e.data;
|
||
if (!msg) { return; }
|
||
if (msg.type === 'addError') {
|
||
showCardError(msg.ruleId, msg.field, msg.message);
|
||
} else if (msg.type === 'ruleAdded') {
|
||
moveCardToSection(msg);
|
||
}
|
||
});
|
||
|
||
function validate() {
|
||
const rules = collectEditedRules();
|
||
for (const rule of rules) {
|
||
if (!rule.id || !rule.id.trim()) {
|
||
return VALIDATION_ID_EMPTY;
|
||
}
|
||
if (!rule.description || !rule.description.trim()) {
|
||
return VALIDATION_DESC_EMPTY.replace('{0}', rule.id);
|
||
}
|
||
if (!rule.message || !rule.message.trim()) {
|
||
return VALIDATION_MSG_EMPTY.replace('{0}', rule.id);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function doConfirm() {
|
||
const err = validate();
|
||
if (err) {
|
||
const errEl = document.getElementById('validationError');
|
||
errEl.textContent = err;
|
||
errEl.style.display = 'block';
|
||
return;
|
||
}
|
||
const edited = collectEditedRules();
|
||
const hasEdits = Object.keys(editedRules).length > 0;
|
||
const withData = (hasEdits || addedRules > 0) ? edited : undefined;
|
||
vscode.postMessage({ type: 'confirm', editedRules: withData });
|
||
}
|
||
|
||
function cancel() {
|
||
vscode.postMessage({ type: 'cancel' });
|
||
}
|
||
|
||
function updateSummary() {
|
||
let keepCount = 0, commentCount = 0;
|
||
document.querySelectorAll('.rule-card').forEach(card => {
|
||
if (card.hasAttribute('data-error')) { return; }
|
||
const keepBtns = card.querySelectorAll('.toggle-btn');
|
||
let isKept = true;
|
||
keepBtns.forEach(b => {
|
||
if (b.classList.contains('active') && b.dataset.action === 'comment') isKept = false;
|
||
});
|
||
if (isKept) keepCount++; else commentCount++;
|
||
});
|
||
document.getElementById('keepCount').textContent = keepCount;
|
||
document.getElementById('commentCount').textContent = commentCount;
|
||
}
|
||
|
||
function updateEditHint() {
|
||
const count = Object.keys(editedRules).length;
|
||
const hint = document.getElementById('editHint');
|
||
const countEl = document.getElementById('editCount');
|
||
if (count > 0) {
|
||
hint.style.display = 'inline';
|
||
countEl.textContent = count;
|
||
} else {
|
||
hint.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.target.classList.contains('tag-input') && e.key === 'Enter') {
|
||
e.preventDefault();
|
||
const input = e.target;
|
||
const val = input.value.trim();
|
||
if (!val) return;
|
||
const ruleId = input.dataset.ruleid;
|
||
const field = input.dataset.field;
|
||
const list = input.parentElement.querySelector('.tag-list');
|
||
|
||
const existing = list.querySelectorAll('.tag');
|
||
const exists = Array.from(existing).some(tag => tag.dataset.value === val);
|
||
if (exists) { input.value = ''; return; }
|
||
|
||
const tag = document.createElement('span');
|
||
tag.className = 'tag';
|
||
tag.dataset.value = val;
|
||
tag.innerHTML = val + '<span class="tag-remove">×</span>';
|
||
list.appendChild(tag);
|
||
input.value = '';
|
||
|
||
const tags = Array.from(list.querySelectorAll('.tag')).map(tag => tag.dataset.value);
|
||
if (!editedRules[ruleId]) editedRules[ruleId] = {};
|
||
editedRules[ruleId][field] = tags;
|
||
updateEditHint();
|
||
}
|
||
});
|
||
|
||
document.addEventListener('click', function(e) {
|
||
if (e.target.classList.contains('tag-remove')) {
|
||
const tag = e.target.closest('.tag');
|
||
const list = tag.closest('.tag-list');
|
||
const ruleId = list.dataset.ruleid;
|
||
const field = list.dataset.field;
|
||
tag.remove();
|
||
const remaining = Array.from(list.querySelectorAll('.tag')).map(tag => tag.dataset.value);
|
||
if (!editedRules[ruleId]) editedRules[ruleId] = {};
|
||
editedRules[ruleId][field] = remaining;
|
||
updateEditHint();
|
||
}
|
||
});
|
||
|
||
document.addEventListener('input', liveClear);
|
||
document.addEventListener('change', liveClear);
|
||
|
||
updateSectionCounts();
|
||
</script>
|
||
</body>
|
||
</html>`;
|
||
}
|