feat: 方法级代码审查 + 模板导入/预览增强 + SQLFluff 方言 + AI 空响应报错修复
- 方法级审查: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 + 行号排序
This commit is contained in:
+636
-140
@@ -1,9 +1,13 @@
|
||||
import * as vscode from 'vscode';
|
||||
import type { ConversionResult, PreviewDecision, ImportableRule } from './import-types';
|
||||
import { t } from '../i18n/messages';
|
||||
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(
|
||||
@@ -22,9 +26,11 @@ export async function showImportPreview(
|
||||
|
||||
panel.webview.html = renderPreviewHtml(result, keepRule);
|
||||
|
||||
panel.webview.onDidReceiveMessage((msg) => {
|
||||
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,
|
||||
@@ -42,6 +48,108 @@ export async function showImportPreview(
|
||||
});
|
||||
}
|
||||
|
||||
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',
|
||||
@@ -69,92 +177,151 @@ function renderPreviewHtml(
|
||||
? `<div class="summary-bar" style="border-color:rgba(88,166,255,0.3);color:#58a6ff;">${t('import.template.skipped', { 0: String(result.skippedCount) })}</div>`
|
||||
: '';
|
||||
|
||||
const hasValidRules = cleanRules.length > 0;
|
||||
const emptyValidHint = !hasValidRules
|
||||
? `<div class="validation-error" style="display:block;">${t('import.emptyValidRules')}</div>`
|
||||
: '';
|
||||
function dupLabel(dupOf: string | undefined): string {
|
||||
return dupOf?.startsWith('custom/')
|
||||
? `${t('import.customRulePrefix')} ${dupOf.slice(7)}`
|
||||
: (dupOf ?? 'unknown');
|
||||
}
|
||||
|
||||
const confirmBtnAttrs = hasValidRules
|
||||
? 'onclick="doConfirm()"'
|
||||
: 'disabled style="opacity:0.5;cursor:not-allowed;"';
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
const customRules = workspaceRoot ? loadActiveRules(workspaceRoot) : [];
|
||||
|
||||
function renderRuleCard(rule: ImportableRule): string {
|
||||
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 color = SEVERITY_COLORS[rule.severity] || '#8b949e';
|
||||
const editRule = rule;
|
||||
const sevIssue = !!rule.validationIssues?.some(i => i.field === 'severity');
|
||||
const color = sevIssue ? '#f48771' : (SEVERITY_COLORS[rule.severity] || '#8b949e');
|
||||
|
||||
let duplicateInfo = '';
|
||||
if (rule.duplicateLevel === 'exact') {
|
||||
const prefix = rule.duplicateOf?.startsWith('custom/') ? `${t('import.customRulePrefix')} ${rule.duplicateOf.slice(7)}` : (rule.duplicateOf ?? 'unknown');
|
||||
duplicateInfo = `<div style="color:#8b949e;font-size:12px;margin-top:4px;">${t('import.duplicateOf', { 0: prefix })}</div>`;
|
||||
} else if (rule.duplicateLevel === 'overlap') {
|
||||
const prefix = rule.duplicateOf?.startsWith('custom/') ? `${t('import.customRulePrefix')} ${rule.duplicateOf.slice(7)}` : (rule.duplicateOf ?? 'unknown');
|
||||
duplicateInfo = `
|
||||
<div style="color:#d29922;font-size:12px;margin-top:4px;">${t('import.overlapWith', { 0: prefix })}</div>
|
||||
${rule.duplicateReason ? `<div style="color:#8b949e;font-size:12px;margin-top:2px;">${t('import.overlapReason', { 0: rule.duplicateReason })}</div>` : ''}
|
||||
`;
|
||||
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 statusBadge = rule.duplicateLevel === 'exact'
|
||||
? `<span class="badge badge-exact">${kept ? t('import.badgeRestored') : t('import.badgeWillComment')}</span>`
|
||||
: rule.duplicateLevel === 'overlap'
|
||||
? `<span class="badge badge-overlap">${kept ? t('importPreview.keep') : t('importPreview.comment')}</span>`
|
||||
: `<span class="badge badge-none">${t('importPreview.keep')}</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 tagValue = (tags: string[] | undefined) => tags && tags.length > 0 ? tags.join(',') : '';
|
||||
const tagDisplay = (tags: string[] | undefined) => tags && tags.length > 0 ? tags.map(t => `<span class="tag" data-value="${t}">${t}<span class="tag-remove" data-tag="${t}">×</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" data-ruleid="${rule.id}">
|
||||
<div class="rule-card-header" onclick="toggleCard('${rule.id}')">
|
||||
<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('${rule.id}', this.value)" onclick="event.stopPropagation()">
|
||||
<span class="rule-severity-tag" style="background:${color}20;color:${color};border:1px solid ${color}40;">${editRule.severity}</span>
|
||||
<span class="rule-desc-preview">${editRule.description}</span>
|
||||
<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">▼</span>
|
||||
<span class="expand-icon">${expandIcon}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rule-card-body" id="body-${rule.id}" style="display:none;">
|
||||
<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('${rule.id}', this.value)">
|
||||
<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>
|
||||
<div class="keep-toggle">
|
||||
<button class="toggle-btn ${kept ? 'active' : ''}" data-action="keep" onclick="event.stopPropagation();toggleKeep('${rule.id}', true)">${t('importPreview.keep')}</button>
|
||||
<button class="toggle-btn ${!kept ? 'active' : ''}" data-action="comment" onclick="event.stopPropagation();toggleKeep('${rule.id}', false)">${t('importPreview.comment')}</button>
|
||||
</div>
|
||||
${actionArea}
|
||||
</div>
|
||||
|
||||
${duplicateInfo}
|
||||
|
||||
<div class="edit-field">
|
||||
<div class="edit-field${issueCls('severity')}">
|
||||
<label>${t('import.severityLabel')}</label>
|
||||
<select onchange="updateRule('${rule.id}','severity',this.value)">
|
||||
${SEVERITY_OPTIONS.map(s => `<option value="${s}" ${s === editRule.severity ? 'selected' : ''}>${s}</option>`).join('')}
|
||||
<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">
|
||||
<div class="edit-field${issueCls('description')}">
|
||||
<label>${t('import.descriptionLabel')}</label>
|
||||
<textarea rows="2" onchange="updateRule('${rule.id}','description',this.value)">${editRule.description}</textarea>
|
||||
<textarea rows="2" class="${inputCls('description')}" onchange="updateRule(this,'description',this.value)">${rule.description}</textarea>
|
||||
${issueMsg('description')}
|
||||
</div>
|
||||
|
||||
<div class="edit-field">
|
||||
<div class="edit-field${issueCls('message')}">
|
||||
<label>${t('import.messageLabel')}</label>
|
||||
<textarea rows="2" onchange="updateRule('${rule.id}','message',this.value)">${editRule.message}</textarea>
|
||||
<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(editRule.languages)}
|
||||
${tagDisplay(rule.languages)}
|
||||
</div>
|
||||
<input class="tag-input" data-ruleid="${rule.id}" data-field="languages" placeholder="${t('import.tagPlaceholder')}" value="">
|
||||
</div>
|
||||
@@ -164,7 +331,7 @@ function renderPreviewHtml(
|
||||
<label>${t('import.excludeLanguagesLabel')}</label>
|
||||
<div class="tag-input-wrapper">
|
||||
<div class="tag-list" data-ruleid="${rule.id}" data-field="excludeLanguages">
|
||||
${tagDisplay(editRule.excludeLanguages)}
|
||||
${tagDisplay(rule.excludeLanguages)}
|
||||
</div>
|
||||
<input class="tag-input" data-ruleid="${rule.id}" data-field="excludeLanguages" placeholder="${t('import.tagPlaceholder')}" value="">
|
||||
</div>
|
||||
@@ -174,59 +341,35 @@ function renderPreviewHtml(
|
||||
`;
|
||||
}
|
||||
|
||||
function renderErrorCard(rule: ImportableRule): string {
|
||||
const issues = (rule.validationIssues || []).map(i =>
|
||||
`<div style="color:#f48771;font-size:12px;margin-bottom:4px;">${t('import.issuePrefix')} ${i.message}</div>`
|
||||
).join('');
|
||||
|
||||
return `
|
||||
<div class="rule-card" data-error="true" style="opacity:0.7;border-color:rgba(248,81,73,0.3);">
|
||||
<div class="rule-card-header" style="cursor:default;">
|
||||
<div class="rule-card-summary">
|
||||
<span style="font-family:monospace;font-size:13px;font-weight:600;">${rule.id}</span>
|
||||
<span style="color:#f48771;font-size:11px;font-weight:600;">${t('import.cannotImport')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rule-card-body" style="border-top:1px solid rgba(248,81,73,0.15);padding-top:8px;">
|
||||
${issues}
|
||||
<div style="color:#8b949e;font-size:11px;margin-top:6px;">
|
||||
severity: ${rule.severity} | description: ${rule.description} | message: ${rule.message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderErrorSection(rules: ImportableRule[]): string {
|
||||
if (rules.length === 0) { return ''; }
|
||||
const sectionId = 'section-error';
|
||||
return `
|
||||
<div style="margin-bottom:12px;">
|
||||
<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">${t('import.sectionInvalid')}(${rules.length})</span>
|
||||
<span class="section-title" data-section-title="error"></span>
|
||||
<span class="section-arrow">▼</span>
|
||||
</div>
|
||||
<div id="${sectionId}">
|
||||
${rules.map(renderErrorCard).join('')}
|
||||
<div id="${sectionId}" data-section="error" style="display:block;">
|
||||
${rules.map(r => renderRuleCard(r, true)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSection(title: string, icon: string, rules: ImportableRule[], _defaultExpanded: boolean): string {
|
||||
if (rules.length === 0) { return ''; }
|
||||
const sectionId = `section-${title.replace(/\s/g, '')}`;
|
||||
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 style="margin-bottom:12px;">
|
||||
<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">${title}(${t('import.ruleCount', { 0: rules.length })})</span>
|
||||
<span class="section-arrow">▶</span>
|
||||
<span class="section-title" data-section-title="${key}"></span>
|
||||
<span class="section-arrow">▼</span>
|
||||
</div>
|
||||
<div id="${sectionId}" style="display:${show ? 'block' : 'none'};">
|
||||
${rules.map(renderRuleCard).join('')}
|
||||
<div id="${sectionId}" data-section="${key}" style="display:${show ? 'block' : 'none'};">
|
||||
${rules.map(r => renderRuleCard(r)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -265,6 +408,10 @@ body {
|
||||
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);
|
||||
@@ -284,7 +431,13 @@ body {
|
||||
cursor: pointer; padding: 4px 0;
|
||||
}
|
||||
.section-title { font-weight: 600; font-size: 13px; }
|
||||
.section-arrow { font-size: 10px; color: var(--vscode-descriptionForeground); }
|
||||
.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;
|
||||
@@ -324,7 +477,17 @@ body {
|
||||
.rule-card-meta {
|
||||
display: flex; align-items: center; gap: 8px; flex-shrink: 0;
|
||||
}
|
||||
.expand-icon { font-size: 10px; color: var(--vscode-descriptionForeground); }
|
||||
.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;
|
||||
}
|
||||
@@ -339,7 +502,10 @@ body {
|
||||
padding: 10px 0 8px;
|
||||
}
|
||||
.id-row {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
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 {
|
||||
@@ -353,6 +519,20 @@ body {
|
||||
.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;
|
||||
@@ -420,6 +600,26 @@ body {
|
||||
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>
|
||||
@@ -428,40 +628,73 @@ body {
|
||||
<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: exactRules.length })}</div>
|
||||
<div class="summary-item" style="border-color:rgba(210,153,34,0.3);color:#d29922;">${t('import.overlapDuplicate', { 0: overlapRules.length })}</div>
|
||||
<div class="summary-item" style="border-color:rgba(35,134,54,0.3);color:#3fb950;">${t('import.noDuplicate', { 0: noneRules.length })}</div>
|
||||
<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'), '⛔', exactRules, false)}
|
||||
${renderSection(t('import.sectionOverlap'), '⚠️', overlapRules, true)}
|
||||
${renderSection(t('import.sectionNone'), '✅', noneRules, false)}
|
||||
${renderSection(t('import.sectionExact'), '⛔', 'exact', exactRules)}
|
||||
${renderSection(t('import.sectionOverlap'), '⚠️', 'overlap', overlapRules)}
|
||||
${renderSection(t('import.sectionNone'), '✅', 'none', noneRules)}
|
||||
|
||||
${emptyValidHint}
|
||||
<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" ${confirmBtnAttrs}>${t('importPreview.confirm')}</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 syncId(originalId, newValue) {
|
||||
const card = document.querySelector('.rule-card[data-ruleid="' + originalId + '"]');
|
||||
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;
|
||||
@@ -472,48 +705,52 @@ function syncId(originalId, newValue) {
|
||||
updateEditHint();
|
||||
|
||||
const isPlaceholder = /^rule-\\d+$/.test(newValue);
|
||||
[headerInput, panelInput].forEach(el => {
|
||||
if (el) {
|
||||
el.classList.toggle('placeholder-id', isPlaceholder);
|
||||
[headerInput, panelInput].forEach(input => {
|
||||
if (input) {
|
||||
input.classList.toggle('placeholder-id', isPlaceholder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleCard(ruleId) {
|
||||
const body = document.getElementById('body-' + ruleId);
|
||||
const card = body.closest('.rule-card');
|
||||
const icon = card.querySelector('.expand-icon');
|
||||
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';
|
||||
icon.textContent = '▲';
|
||||
card.classList.add('expanded');
|
||||
} else {
|
||||
body.style.display = 'none';
|
||||
icon.textContent = '▼';
|
||||
card.classList.remove('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSection(id) {
|
||||
const el = document.getElementById(id);
|
||||
const arrow = el.previousElementSibling.querySelector('.section-arrow');
|
||||
const wrap = el.closest('.section-wrapper');
|
||||
if (el.style.display === 'none') {
|
||||
el.style.display = 'block';
|
||||
arrow.textContent = '▼';
|
||||
wrap.classList.add('expanded');
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
arrow.textContent = '▶';
|
||||
wrap.classList.remove('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleKeep(ruleId, keep) {
|
||||
vscode.postMessage({ type: 'toggleRule', ruleId, keep });
|
||||
const card = document.querySelector('.rule-card[data-ruleid="' + ruleId + '"]');
|
||||
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(ruleId, field, value) {
|
||||
function updateRule(el, field, value) {
|
||||
const card = el.closest('.rule-card');
|
||||
if (!card) return;
|
||||
const ruleId = card.dataset.ruleid;
|
||||
if (!editedRules[ruleId]) {
|
||||
editedRules[ruleId] = {};
|
||||
}
|
||||
@@ -521,34 +758,288 @@ function updateRule(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 originalId = card.dataset.ruleid;
|
||||
const idInput = card.querySelector('.id-display-input');
|
||||
const ruleId = idInput ? idInput.value.trim() || originalId : originalId;
|
||||
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"]');
|
||||
|
||||
const rule = {
|
||||
id: ruleId,
|
||||
severity: severityEl ? severityEl.value : 'warning',
|
||||
description: descEl ? descEl.value : '',
|
||||
message: msgEl ? msgEl.value : '',
|
||||
languages: langList ? Array.from(langList.querySelectorAll('.tag')).map(t => t.dataset.value) : [],
|
||||
excludeLanguages: exclList ? Array.from(exclList.querySelectorAll('.tag')).map(t => t.dataset.value) : [],
|
||||
};
|
||||
|
||||
result.push(rule);
|
||||
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) {
|
||||
@@ -575,7 +1066,8 @@ function doConfirm() {
|
||||
}
|
||||
const edited = collectEditedRules();
|
||||
const hasEdits = Object.keys(editedRules).length > 0;
|
||||
vscode.postMessage({ type: 'confirm', editedRules: hasEdits ? edited : undefined });
|
||||
const withData = (hasEdits || addedRules > 0) ? edited : undefined;
|
||||
vscode.postMessage({ type: 'confirm', editedRules: withData });
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
@@ -586,7 +1078,6 @@ function updateSummary() {
|
||||
let keepCount = 0, commentCount = 0;
|
||||
document.querySelectorAll('.rule-card').forEach(card => {
|
||||
if (card.hasAttribute('data-error')) { return; }
|
||||
const ruleId = card.dataset.ruleid;
|
||||
const keepBtns = card.querySelectorAll('.toggle-btn');
|
||||
let isKept = true;
|
||||
keepBtns.forEach(b => {
|
||||
@@ -621,7 +1112,7 @@ document.addEventListener('keydown', function(e) {
|
||||
const list = input.parentElement.querySelector('.tag-list');
|
||||
|
||||
const existing = list.querySelectorAll('.tag');
|
||||
const exists = Array.from(existing).some(t => t.dataset.value === val);
|
||||
const exists = Array.from(existing).some(tag => tag.dataset.value === val);
|
||||
if (exists) { input.value = ''; return; }
|
||||
|
||||
const tag = document.createElement('span');
|
||||
@@ -631,7 +1122,7 @@ document.addEventListener('keydown', function(e) {
|
||||
list.appendChild(tag);
|
||||
input.value = '';
|
||||
|
||||
const tags = Array.from(list.querySelectorAll('.tag')).map(t => t.dataset.value);
|
||||
const tags = Array.from(list.querySelectorAll('.tag')).map(tag => tag.dataset.value);
|
||||
if (!editedRules[ruleId]) editedRules[ruleId] = {};
|
||||
editedRules[ruleId][field] = tags;
|
||||
updateEditHint();
|
||||
@@ -645,12 +1136,17 @@ document.addEventListener('click', function(e) {
|
||||
const ruleId = list.dataset.ruleid;
|
||||
const field = list.dataset.field;
|
||||
tag.remove();
|
||||
const remaining = Array.from(list.querySelectorAll('.tag')).map(t => t.dataset.value);
|
||||
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>`;
|
||||
|
||||
Reference in New Issue
Block a user