诊断系统是代码编辑器的"健康监护仪",它实时检测代码中的错误、警告和信息提示,帮助开发者在编写阶段就发现和修复问题。从诊断数据模型到问题面板渲染,从快速修复到诊断聚合,本文系统介绍编辑器诊断系统的完整实现方案。
诊断数据模型
诊断对象设计
诊断(Diagnostic)是诊断系统的核心数据结构,它描述了代码中一个具体的问题。一个完整的诊断对象需要包含位置、严重性、消息和关联信息:
// 诊断数据模型
class Diagnostic {
constructor(options) {
// 必需字段
this.range = options.range; // 问题所在范围
this.message = options.message; // 问题描述
this.severity = options.severity; // 严重程度
// 可选字段
this.code = options.code; // 诊断代码
this.source = options.source; // 来源(如'tslint', 'eslint')
this.relatedInformation = options.relatedInformation || [];
this.tags = options.tags || []; // 诊断标签
this.data = options.data; // 扩展数据
}
}
// 诊断严重级别
const DiagnosticSeverity = {
Error: 0, // 错误 - 阻止编译/运行
Warning: 1, // 警告 - 可能的问题
Information: 2, // 信息 - 建议性提示
Hint: 3 // 提示 - 代码改进建议
};
// 诊断标签
const DiagnosticTag = {
Unnecessary: 1, // 未使用的代码
Deprecated: 2 // 已弃用的API
};
// 诊断范围
class DiagnosticRange {
constructor(startLine, startCol, endLine, endCol) {
this.start = { line: startLine, character: startCol };
this.end = { line: endLine, character: endCol };
}
contains(position) {
if (position.line < this.start.line ||
position.line > this.end.line) return false;
if (position.line === this.start.line &&
position.character < this.start.character) return false;
if (position.line === this.end.line &&
position.character > this.end.character) return false;
return true;
}
intersects(other) {
return !(this.end.line < other.start.line ||
(this.end.line === other.start.line &&
this.end.character < other.start.character) ||
other.end.line < this.start.line ||
(other.end.line === this.start.line &&
other.end.character < this.start.character));
}
}
// 关联信息 - 指向其他位置的相关诊断
class DiagnosticRelatedInformation {
constructor(uri, range, message) {
this.location = { uri, range };
this.message = message;
}
}
诊断集合管理
// 诊断集合 - 管理单个来源的诊断
class DiagnosticCollection {
constructor(name) {
this.name = name;
this.diagnostics = new Map(); // uri → Diagnostic[]
this.onChangeListeners = new Set();
}
// 设置某个URI的诊断(替换式更新)
set(uri, diagnostics) {
const oldDiagnostics = this.diagnostics.get(uri) || [];
this.diagnostics.set(uri, diagnostics);
// 通知变更
this.fireChange(uri, oldDiagnostics, diagnostics);
}
// 增量更新 - 仅更新变化的诊断
update(uri, changes) {
const current = this.diagnostics.get(uri) || [];
const updated = this.applyChanges(current, changes);
this.diagnostics.set(uri, updated);
this.fireChange(uri, current, updated);
}
applyChanges(current, changes) {
const result = [...current];
for (const change of changes) {
if (change.type === 'add') {
result.push(change.diagnostic);
} else if (change.type === 'remove') {
const idx = result.indexOf(change.diagnostic);
if (idx !== -1) result.splice(idx, 1);
} else if (change.type === 'replace') {
const idx = result.indexOf(change.oldDiagnostic);
if (idx !== -1) result[idx] = change.newDiagnostic;
}
}
return result;
}
// 获取某个URI的诊断
get(uri) {
return this.diagnostics.get(uri) || [];
}
// 获取所有诊断
getAll() {
const all = [];
for (const [uri, diagnostics] of this.diagnostics) {
for (const d of diagnostics) {
all.push({ ...d, uri });
}
}
return all;
}
// 删除某个URI的诊断
delete(uri) {
const old = this.diagnostics.get(uri) || [];
this.diagnostics.delete(uri);
this.fireChange(uri, old, []);
}
// 清空所有诊断
clear() {
const oldUris = [...this.diagnostics.keys()];
this.diagnostics.clear();
for (const uri of oldUris) {
this.fireChange(uri, [], []);
}
}
onChange(listener) {
this.onChangeListeners.add(listener);
return () => this.onChangeListeners.delete(listener);
}
fireChange(uri, oldDiagnostics, newDiagnostics) {
for (const listener of this.onChangeListeners) {
listener({ uri, oldDiagnostics, newDiagnostics });
}
}
}
诊断收集与调度
诊断调度器
诊断调度器负责协调多个诊断源的运行时机和结果聚合,避免重复计算和资源竞争:
// 诊断调度器
class DiagnosticScheduler {
constructor() {
this.providers = new Map(); // providerId → DiagnosticProvider
this.collections = new Map(); // providerId → DiagnosticCollection
this.debounceTimers = new Map();
this.debounceDelay = 300; // 300ms防抖
this.versionMap = new Map(); // uri → version
}
// 注册诊断提供者
registerProvider(provider) {
this.providers.set(provider.id, provider);
this.collections.set(
provider.id,
new DiagnosticCollection(provider.name)
);
// 订阅文档变更
if (provider.triggerKind === 'onChange') {
provider.onDocumentChange((uri, version) => {
this.scheduleCheck(provider.id, uri, version);
});
}
}
// 调度诊断检查
scheduleCheck(providerId, uri, version) {
// 取消之前的定时器
const timerKey = `${providerId}:${uri}`;
if (this.debounceTimers.has(timerKey)) {
clearTimeout(this.debounceTimers.get(timerKey));
}
// 防抖调度
const provider = this.providers.get(providerId);
const delay = provider.debounceDelay || this.debounceDelay;
this.debounceTimers.set(timerKey, setTimeout(async () => {
this.debounceTimers.delete(timerKey);
// 版本检查 - 避免处理过期结果
if (this.versionMap.get(uri) !== version) {
return;
}
await this.runProvider(providerId, uri);
}, delay));
}
// 执行诊断提供者
async runProvider(providerId, uri) {
const provider = this.providers.get(providerId);
const collection = this.collections.get(providerId);
try {
const diagnostics = await provider.provideDiagnostics(uri);
collection.set(uri, diagnostics);
} catch (err) {
console.error(`Diagnostic provider ${providerId} failed:`, err);
}
}
// 手动触发全量检查
async checkAll(uri) {
const promises = [];
for (const [providerId] of this.providers) {
promises.push(this.runProvider(providerId, uri));
}
await Promise.all(promises);
}
// 获取某个URI的聚合诊断
getAggregatedDiagnostics(uri) {
const all = [];
for (const [, collection] of this.collections) {
all.push(...collection.get(uri));
}
return all.sort((a, b) => a.severity - b.severity);
}
}
诊断提供者接口
// 诊断提供者基类
class DiagnosticProvider {
constructor(options) {
this.id = options.id;
this.name = options.name;
this.triggerKind = options.triggerKind || 'onChange';
this.debounceDelay = options.debounceDelay || 300;
this.filePatterns = options.filePatterns || ['**/*'];
}
// 子类必须实现
async provideDiagnostics(uri) {
throw new Error('Not implemented');
}
// 判断是否处理该文件
canHandle(uri) {
return this.filePatterns.some(pattern =>
this.matchPattern(uri, pattern)
);
}
matchPattern(uri, pattern) {
const regex = new RegExp(
pattern.replace('**', '.*').replace('*', '[^/]+')
);
return regex.test(uri);
}
}
// ESLint诊断提供者示例
class ESLintDiagnosticProvider extends DiagnosticProvider {
constructor() {
super({
id: 'eslint',
name: 'ESLint',
triggerKind: 'onChange',
filePatterns: ['**/*.js', '**/*.ts', '**/*.jsx', '**/*.tsx']
});
this.linter = null;
}
async init() {
this.linter = await loadESLint();
}
async provideDiagnostics(uri) {
const content = await readFileContent(uri);
const results = this.linter.verify(content, {
useEslintrc: true,
filename: uri
});
return results.map(result => new Diagnostic({
range: new DiagnosticRange(
result.line - 1,
result.column - 1,
result.endLine ? result.endLine - 1 : result.line - 1,
result.endColumn ? result.endColumn - 1 : result.column
),
message: result.message,
severity: result.severity === 2
? DiagnosticSeverity.Error
: DiagnosticSeverity.Warning,
code: result.ruleId,
source: 'eslint'
}));
}
}
// TypeScript诊断提供者示例
class TypeScriptDiagnosticProvider extends DiagnosticProvider {
constructor() {
super({
id: 'typescript',
name: 'TypeScript',
triggerKind: 'onChange',
debounceDelay: 500,
filePatterns: ['**/*.ts', '**/*.tsx']
});
}
async provideDiagnostics(uri) {
const diagnostics = [];
const syntactic = this.languageService.getSyntacticDiagnostics(uri);
const semantic = this.languageService.getSemanticDiagnostics(uri);
const suggestion = this.languageService.getSuggestionDiagnostics(uri);
const allResults = [...syntactic, ...semantic, ...suggestion];
for (const d of allResults) {
const tags = [];
if (d.reportsUnnecessary) tags.push(DiagnosticTag.Unnecessary);
if (d.reportsDeprecated) tags.push(DiagnosticTag.Deprecated);
diagnostics.push(new Diagnostic({
range: this.tsRangeToRange(d),
message: this.flattenMessageText(d.messageText),
severity: this.mapSeverity(d.category),
code: d.code,
source: 'ts',
tags,
relatedInformation: d.relatedInformation?.map(ri => ({
location: {
uri: ri.file.fileName,
range: this.tsRangeToRange(ri)
},
message: ri.messageText
})) || []
}));
}
return diagnostics;
}
mapSeverity(category) {
switch (category) {
case 0: return DiagnosticSeverity.Warning;
case 1: return DiagnosticSeverity.Error;
case 2: return DiagnosticSeverity.Suggestion;
case 3: return DiagnosticSeverity.Information;
default: return DiagnosticSeverity.Error;
}
}
flattenMessageText(messageText) {
if (typeof messageText === 'string') return messageText;
// 链式诊断消息
const parts = [];
let current = messageText;
while (current) {
parts.push(current.messageText);
current = current.next;
}
return parts.join('\n');
}
}
问题面板实现
问题面板数据管理
// 问题面板模型
class ProblemsPanelModel {
constructor(diagnosticManager) {
this.diagnosticManager = diagnosticManager;
this.filters = {
severity: new Set([
DiagnosticSeverity.Error,
DiagnosticSeverity.Warning,
DiagnosticSeverity.Information,
DiagnosticSeverity.Hint
]),
text: '',
source: null
};
this.groupByFile = true;
this.listeners = new Set();
// 监听诊断变更
diagnosticManager.onDidChangeDiagnostics(() => {
this.refresh();
});
}
// 获取过滤后的诊断列表
getFilteredDiagnostics() {
let all = this.diagnosticManager.getAllDiagnostics();
// 按严重性过滤
all = all.filter(d => this.filters.severity.has(d.severity));
// 按文本过滤
if (this.filters.text) {
const lowerFilter = this.filters.text.toLowerCase();
all = all.filter(d =>
d.message.toLowerCase().includes(lowerFilter) ||
d.source?.toLowerCase().includes(lowerFilter) ||
String(d.code).toLowerCase().includes(lowerFilter)
);
}
// 按来源过滤
if (this.filters.source) {
all = all.filter(d => d.source === this.filters.source);
}
return all;
}
// 按文件分组
getGroupedDiagnostics() {
const filtered = this.getFilteredDiagnostics();
const groups = new Map();
for (const d of filtered) {
if (!groups.has(d.uri)) {
groups.set(d.uri, []);
}
groups.get(d.uri).push(d);
}
// 每组内按严重性和行号排序
for (const [, diagnostics] of groups) {
diagnostics.sort((a, b) =>
a.severity - b.severity ||
a.range.start.line - b.range.start.line
);
}
return groups;
}
// 获取统计摘要
getSummary() {
const all = this.getFilteredDiagnostics();
return {
errors: all.filter(d => d.severity === DiagnosticSeverity.Error).length,
warnings: all.filter(d => d.severity === DiagnosticSeverity.Warning).length,
info: all.filter(d => d.severity === DiagnosticSeverity.Information).length,
hints: all.filter(d => d.severity === DiagnosticSeverity.Hint).length,
total: all.length,
fileCount: new Set(all.map(d => d.uri)).size
};
}
refresh() {
for (const listener of this.listeners) {
listener(this.getSummary(), this.getGroupedDiagnostics());
}
}
}
问题面板视图
// 问题面板渲染器
class ProblemsPanelRenderer {
constructor(container, model) {
this.container = container;
this.model = model;
this.expandedFiles = new Set();
this.selectedDiagnostic = null;
model.onUpdate(() => this.render());
}
render() {
const summary = this.model.getSummary();
const groups = this.model.getGroupedDiagnostics();
let html = this.renderSummary(summary);
for (const [uri, diagnostics] of groups) {
html += this.renderFileGroup(uri, diagnostics);
}
this.container.innerHTML = html;
this.bindEvents();
}
renderSummary(summary) {
return `
<div class="problems-summary">
<span class="error-count">${summary.errors} 错误</span>
<span class="warning-count">${summary.warnings} 警告</span>
<span class="info-count">${summary.info} 提示</span>
</div>
`;
}
renderFileGroup(uri, diagnostics) {
const fileName = uri.split('/').pop();
const isExpanded = this.expandedFiles.has(uri);
const errorCount = diagnostics.filter(
d => d.severity === DiagnosticSeverity.Error
).length;
const warningCount = diagnostics.filter(
d => d.severity === DiagnosticSeverity.Warning
).length;
let html = `
<div class="file-group" data-uri="${uri}">
<div class="file-header">
<span class="toggle">${isExpanded ? '▼' : '▶'}</span>
<span class="file-name">${fileName}</span>
<span class="count">${errorCount}E ${warningCount}W</span>
</div>
`;
if (isExpanded) {
for (const d of diagnostics) {
html += this.renderDiagnostic(d);
}
}
html += '</div>';
return html;
}
renderDiagnostic(diagnostic) {
const severityIcon = {
[0]: '✕', // Error
[1]: '⚠', // Warning
[2]: 'ℹ', // Info
[3]: '💡' // Hint
};
return `
<div class="diagnostic-item severity-${diagnostic.severity}"
data-line="${diagnostic.range.start.line}"
data-col="${diagnostic.range.start.character}">
<span class="severity-icon">${severityIcon[diagnostic.severity]}</span>
<span class="message">${diagnostic.message}</span>
<span class="location">Ln ${diagnostic.range.start.line + 1}, Col ${diagnostic.range.start.character + 1}</span>
${diagnostic.source ? `[${diagnostic.source}]` : ''}
${diagnostic.code ? `(${diagnostic.code})` : ''}
</div>
`;
}
}
错误检测策略
多层次错误检测
完善的诊断系统应在多个层次进行错误检测,从语法到语义,从本地到全局:
// 多层次错误检测引擎
class MultiLevelErrorDetector {
constructor() {
this.detectors = {
syntactic: new SyntacticDetector(),
semantic: new SemanticDetector(),
style: new StyleDetector(),
crossFile: new CrossFileDetector()
};
}
// 语法级检测 - 最快,可在输入时运行
async detectSyntactic(sourceCode, languageId) {
const diagnostics = [];
// 基本括号匹配检查
const bracketStack = [];
const pairs = { '(': ')', '[': ']', '{': '}' };
const openers = new Set(Object.keys(pairs));
let line = 0, col = 0;
for (let i = 0; i < sourceCode.length; i++) {
const ch = sourceCode[i];
if (ch === '\n') { line++; col = 0; continue; }
if (openers.has(ch)) {
bracketStack.push({ char: ch, line, col });
} else if (Object.values(pairs).includes(ch)) {
const expected = pairs[bracketStack[bracketStack.length - 1]?.char];
if (ch !== expected) {
diagnostics.push(new Diagnostic({
range: new DiagnosticRange(line, col, line, col + 1),
message: `Unexpected '${ch}', expected '${expected}'`,
severity: DiagnosticSeverity.Error,
source: 'syntax'
}));
}
bracketStack.pop();
}
col++;
}
// 未闭合的括号
for (const bracket of bracketStack) {
diagnostics.push(new Diagnostic({
range: new DiagnosticRange(
bracket.line, bracket.col, bracket.line, bracket.col + 1
),
message: `Unclosed '${bracket.char}'`,
severity: DiagnosticSeverity.Error,
source: 'syntax'
}));
}
return diagnostics;
}
// 语义级检测 - 需要类型信息
async detectSemantic(uri) {
return this.detectors.semantic.check(uri);
}
// 风格检测 - 代码规范
async detectStyle(sourceCode) {
return this.detectors.style.check(sourceCode);
}
// 跨文件检测 - 项目级问题
async detectCrossFile(projectRoot) {
return this.detectors.crossFile.check(projectRoot);
}
}
快速修复系统
CodeAction模型
// 快速修复模型
class CodeAction {
constructor(options) {
this.title = options.title; // 修复标题
this.kind = options.kind; // 修复类型
this.diagnostics = options.diagnostics; // 关联的诊断
this.edit = options.edit; // 文本编辑操作
this.isPreferred = options.isPreferred || false;
this.disabled = options.disabled; // 禁用原因
}
}
const CodeActionKind = {
QuickFix: 'quickfix',
Refactor: 'refactor',
RefactorExtract: 'refactor.extract',
RefactorInline: 'refactor.inline',
RefactorRewrite: 'refactor.rewrite',
Source: 'source',
SourceOrganizeImports: 'source.organizeImports'
};
// 快速修复提供者
class CodeActionProvider {
constructor() {
this.providers = [];
}
register(provider) {
this.providers.push(provider);
}
async getCodeActions(uri, range, diagnostics, context) {
const actions = [];
for (const provider of this.providers) {
if (context.only && !provider.provides(context.only)) {
continue;
}
const result = await provider.provideCodeActions(
uri, range, diagnostics, context
);
if (result) {
actions.push(...result);
}
}
return actions.sort((a, b) => {
// 优先显示首选修复
if (a.isPreferred && !b.isPreferred) return -1;
if (!a.isPreferred && b.isPreferred) return 1;
// quickfix排在refactor前
return a.kind.localeCompare(b.kind);
});
}
}
// 内置快速修复:移除未使用变量
class RemoveUnusedVariableFix {
provides(kind) { return kind === CodeActionKind.QuickFix; }
async provideCodeActions(uri, range, diagnostics) {
const actions = [];
for (const d of diagnostics) {
if (d.tags?.includes(DiagnosticTag.Unnecessary)) {
actions.push(new CodeAction({
title: `移除未使用的变量`,
kind: CodeActionKind.QuickFix,
diagnostics: [d],
isPreferred: true,
edit: {
changes: {
[uri]: [{
range: this.expandToFullStatement(d.range),
newText: ''
}]
}
}
}));
}
}
return actions;
}
expandToFullStatement(range) {
// 扩展到整行,移除整个声明语句
return new DiagnosticRange(
range.start.line, 0,
range.end.line + 1, 0
);
}
}
// 内置快速修复:添加缺失的导入
class AddMissingImportFix {
provides(kind) { return kind === CodeActionKind.QuickFix; }
async provideCodeActions(uri, range, diagnostics) {
const actions = [];
for (const d of diagnostics) {
const match = d.message.match(/Cannot find name '(\w+)'/);
if (!match) continue;
const name = match[1];
const imports = await this.findImports(name, uri);
for (const imp of imports) {
actions.push(new CodeAction({
title: `导入 '${name}' (来自 '${imp.module}')`,
kind: CodeActionKind.QuickFix,
diagnostics: [d],
edit: {
changes: {
[uri]: [{
range: new DiagnosticRange(0, 0, 0, 0),
newText: `import { ${name} } from '${imp.module}';\n`
}]
}
}
}));
}
}
return actions;
}
async findImports(name, uri) {
// 查找模块导出列表中的符号
const results = [];
// ... 查找逻辑省略
return results;
}
}
诊断聚合与去重
多源诊断聚合
// 诊断聚合器 - 合并多个来源的诊断结果
class DiagnosticAggregator {
constructor() {
this.collections = new Map(); // source → Map(uri → Diagnostic[])
this.listeners = new Set();
}
// 添加某个源的诊断
setDiagnostics(source, uri, diagnostics) {
if (!this.collections.has(source)) {
this.collections.set(source, new Map());
}
this.collections.get(source).set(uri, diagnostics);
this.fireChange(uri);
}
// 获取聚合后的诊断(带去重)
getAggregatedDiagnostics(uri) {
const all = [];
for (const [, uriMap] of this.collections) {
const diagnostics = uriMap.get(uri) || [];
all.push(...diagnostics);
}
// 去重:相同范围+相同消息的诊断只保留一个
return this.deduplicate(all);
}
deduplicate(diagnostics) {
const seen = new Set();
const unique = [];
for (const d of diagnostics) {
const key = this.getDiagnosticKey(d);
if (!seen.has(key)) {
seen.add(key);
unique.push(d);
}
}
return unique.sort((a, b) =>
a.range.start.line - b.range.start.line ||
a.range.start.character - b.range.start.character ||
a.severity - b.severity
);
}
getDiagnosticKey(d) {
return [
d.range.start.line,
d.range.start.character,
d.range.end.line,
d.range.end.character,
d.severity,
d.message,
d.code || ''
].join('|');
}
fireChange(uri) {
const diagnostics = this.getAggregatedDiagnostics(uri);
for (const listener of this.listeners) {
listener(uri, diagnostics);
}
}
}
// 诊断优先级策略
class DiagnosticPriorityStrategy {
// 当多个源报告同一位置的诊断时,决定保留哪个
resolveConflict(diagnostics) {
if (diagnostics.length <= 1) return diagnostics;
// 策略1:保留最高严重性
const maxSeverity = Math.min(
...diagnostics.map(d => d.severity)
);
let filtered = diagnostics.filter(
d => d.severity === maxSeverity
);
if (filtered.length <= 1) return filtered;
// 策略2:优先有修复建议的
const withFix = filtered.filter(d => d.hasFix);
if (withFix.length > 0) filtered = withFix;
// 策略3:按源优先级
const sourcePriority = {
'typescript': 1,
'eslint': 2,
'syntax': 3
};
filtered.sort((a, b) =>
(sourcePriority[a.source] || 99) -
(sourcePriority[b.source] || 99)
);
return [filtered[0]];
}
}
实战:完整诊断系统集成
诊断管理器
// 完整的编辑器诊断系统
class EditorDiagnosticSystem {
constructor(documentManager) {
this.docManager = documentManager;
this.scheduler = new DiagnosticScheduler();
this.aggregator = new DiagnosticAggregator();
this.codeActionProvider = new CodeActionProvider();
this.panelModel = new ProblemsPanelModel(this);
this.decorators = new DiagnosticDecorator();
this.listeners = new Set();
this.registerBuiltinProviders();
this.registerBuiltinFixes();
this.bindDocumentEvents();
}
registerBuiltinProviders() {
this.scheduler.registerProvider(
new ESLintDiagnosticProvider()
);
this.scheduler.registerProvider(
new TypeScriptDiagnosticProvider()
);
}
registerBuiltinFixes() {
this.codeActionProvider.register(
new RemoveUnusedVariableFix()
);
this.codeActionProvider.register(
new AddMissingImportFix()
);
}
bindDocumentEvents() {
this.docManager.onDidChangeDocument(async (uri, version) => {
await this.scheduler.checkAll(uri);
});
this.docManager.onDidCloseDocument((uri) => {
this.aggregator.clearForUri(uri);
});
}
// 获取某个URI的诊断
getDiagnostics(uri) {
return this.aggregator.getAggregatedDiagnostics(uri);
}
// 获取光标位置的快速修复
async getCodeActions(uri, position) {
const diagnostics = this.getDiagnosticsAtPosition(uri, position);
return this.codeActionProvider.getCodeActions(
uri,
{ start: position, end: position },
diagnostics,
{ only: CodeActionKind.QuickFix }
);
}
// 应用快速修复
async applyCodeAction(action) {
if (action.edit) {
for (const [uri, edits] of Object.entries(action.edit.changes)) {
const doc = this.docManager.getDocument(uri);
for (const edit of edits) {
await doc.applyEdit(edit.range, edit.newText);
}
}
}
}
// 获取光标位置的诊断
getDiagnosticsAtPosition(uri, position) {
return this.getDiagnostics(uri).filter(d =>
d.range.contains(position)
);
}
// 获取摘要统计
getSummary() {
return this.panelModel.getSummary();
}
// 订阅变更
onDidChangeDiagnostics(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
}
总结
- 诊断数据模型 - Diagnostic对象是诊断系统的核心,包含范围、消息、严重性、来源、标签和关联信息,DiagnosticCollection以URI为键管理诊断集合
- 诊断收集与调度 - DiagnosticScheduler协调多源诊断的运行时机,防抖避免频繁触发,版本号防止过期结果覆盖最新诊断
- 问题面板实现 - 按文件分组显示诊断,支持按严重性和文本过滤,统计摘要提供全局概览,树形结构支持展开/折叠
- 错误检测策略 - 从语法级(括号匹配)、语义级(类型检查)、风格级(代码规范)到跨文件级(项目依赖),多层次的检测覆盖不同场景
- 快速修复系统 - CodeAction模型将诊断与修复操作关联,内置移除未使用变量、添加缺失导入等常见修复,isPreferred标记推荐修复
- 诊断聚合与去重 - 多源诊断聚合后按位置+消息去重,冲突时按严重性、修复可用性和源优先级决策,确保不遗漏也不冗余
- 实战完整集成 - EditorDiagnosticSystem整合调度器、聚合器、修复提供者和面板模型,提供从检测到修复的完整诊断工作流
诊断系统是编辑器从"文本编辑器"升级为"代码编辑器"的关键标志。完善的诊断系统不仅能够发现问题,更能通过快速修复帮助开发者即时解决问题。从语法错误到类型不匹配,从代码风格到跨文件依赖,多层次、多来源、带修复的诊断体系,是专业级代码编辑器的必备能力。