代码编辑器诊断系统完全指南

编辑器功能诊断系统

诊断系统是代码编辑器的"健康监护仪",它实时检测代码中的错误、警告和信息提示,帮助开发者在编写阶段就发现和修复问题。从诊断数据模型到问题面板渲染,从快速修复到诊断聚合,本文系统介绍编辑器诊断系统的完整实现方案。

诊断数据模型

诊断对象设计

诊断(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整合调度器、聚合器、修复提供者和面板模型,提供从检测到修复的完整诊断工作流

诊断系统是编辑器从"文本编辑器"升级为"代码编辑器"的关键标志。完善的诊断系统不仅能够发现问题,更能通过快速修复帮助开发者即时解决问题。从语法错误到类型不匹配,从代码风格到跨文件依赖,多层次、多来源、带修复的诊断体系,是专业级代码编辑器的必备能力。