AtomCode 插件开发实战:从零构建一个完整插件

引言

插件系统是 AtomCode 的核心扩展机制之一,通过插件可以极大地丰富 AtomCode 的功能。本文将详细介绍如何从零开始开发一个完整的 AtomCode 插件,包括插件架构、API 使用、调试技巧,以及如何将插件发布到市场。

一、插件架构深度解析

1.1 插件系统架构

// 插件核心接口
interface IPlugin {
  id: string;
  name: string;
  version: string;
  description: string;
  author: string;
  
  // 生命周期
  onInit(context: IPluginContext): Promise<void>;
  onDestroy(): Promise<void>;
  
  // 事件钩子
  onFileOpen?(file: IFile): void;
  onFileSave?(file: IFile): void;
  onSelectionChange?(selection: ISelection): void;
  onLanguageChange?(language: string): void;
  
  // AI 增强
  onAIComplete?(prompt: string): string | undefined;
  onCodeGenerate?(code: string): string;
}

// 插件上下文
interface IPluginContext {
  // 编辑器 API
  editor: IEditorAPI;
  
  // AI API
  ai: IAIAPI;
  
  // 项目 API
  project: IProjectAPI;
  
  // 通知 API
  notifications: INotificationAPI;
  
  // 存储 API
  storage: IStorageAPI;
  
  // 命令注册
  registerCommand(command: ICommand): void;
  
  // 设置注册
  registerSetting(setting: ISetting): void;
}

1.2 插件生命周期

class PluginLifecycle {
  // 插件加载流程
  static async load(plugin: IPlugin, context: IPluginContext) {
    // 1. 验证插件
    await this.validate(plugin);
    
    // 2. 创建沙箱环境
    const sandbox = await this.createSandbox();
    
    // 3. 初始化插件
    await plugin.onInit(context);
    
    // 4. 注册事件监听
    this.registerEventListeners(plugin, context);
    
    // 5. 激活插件
    plugin.isActive = true;
  }
  
  // 插件卸载流程
  static async unload(plugin: IPlugin) {
    // 1. 移除事件监听
    this.removeEventListeners(plugin);
    
    // 2. 销毁插件
    await plugin.onDestroy();
    
    // 3. 清理资源
    await this.cleanup(plugin);
  }
}

二、开发第一个插件

2.1 插件规划

插件名称code-quality-analyzer

插件功能

  • 实时分析代码质量
  • 检测代码异味
  • 提供优化建议
  • 生成质量报告

技术栈

  • TypeScript
  • AST 分析(@babel/parser)
  • Lint 规则引擎

2.2 项目结构

code-quality-analyzer/
├── src/
│   ├── index.ts              # 插件入口
│   ├── analyzer/             # 分析器
│   │   ├── complexity.ts     # 复杂度分析
│   │   ├── duplication.ts    # 重复代码检测
│   │   ├── naming.ts         # 命名规范检查
│   │   └── style.ts          # 代码风格检查
│   ├── rules/                # 规则
│   │   ├── cyclomatic.ts     # 圈复杂度规则
│   │   ├── long-method.ts    # 长方法规则
│   │   └── magic-number.ts   # 魔法数字规则
│   ├── reporters/            # 报告生成
│   │   ├── console.ts        # 控制台输出
│   │   ├── html.ts           # HTML 报告
│   │   └── json.ts           # JSON 报告
│   └── utils/                # 工具函数
│       ├── ast.ts            # AST 工具
│       ├── config.ts         # 配置管理
│       └── logger.ts         # 日志工具
├── package.json
├── tsconfig.json
├── plugin.json
└── README.md

2.3 核心代码实现

插件入口
// src/index.ts
import { IPlugin, IPluginContext } from '@atomcode/core';
import { ComplexityAnalyzer } from './analyzer/complexity';
import { DuplicationDetector } from './analyzer/duplication';
import { NamingChecker } from './analyzer/naming';
import { StyleChecker } from './analyzer/style';
import { ConsoleReporter } from './reporters/console';
import { HtmlReporter } from './reporters/html';
import { JsonReporter } from './reporters/json';
import { ConfigManager } from './utils/config';

class CodeQualityAnalyzer implements IPlugin {
  id = 'code-quality-analyzer';
  name = 'Code Quality Analyzer';
  version = '1.0.0';
  description = 'Real-time code quality analysis tool';
  author = 'Your Name';
  
  private analyzers: CodeAnalyzer[] = [];
  private reporters: Reporter[] = [];
  private config: ConfigManager;
  
  constructor() {
    this.config = new ConfigManager();
  }
  
  async onInit(context: IPluginContext): Promise<void> {
    console.log('Code Quality Analyzer initialized');
    
    // 初始化分析器
    this.analyzers = [
      new ComplexityAnalyzer(),
      new DuplicationDetector(),
      new NamingChecker(),
      new StyleChecker()
    ];
    
    // 初始化报告器
    this.reporters = [
      new ConsoleReporter(),
      new HtmlReporter(),
      new JsonReporter()
    ];
    
    // 注册命令
    this.registerCommands(context);
    
    // 注册设置
    this.registerSettings(context);
    
    // 加载配置
    await this.config.load();
    
    // 分析当前活动文件
    const activeFile = context.editor.getActiveFile();
    if (activeFile) {
      await this.analyzeFile(activeFile, context);
    }
  }
  
  async onDestroy(): Promise<void> {
    console.log('Code Quality Analyzer destroyed');
    await this.config.save();
  }
  
  async onFileSave(file: IFile): Promise<void> {
    if (this.config.isAutoAnalyzeEnabled()) {
      const context = this.getContext();
      await this.analyzeFile(file, context);
    }
  }
  
  onLanguageChange(language: string): void {
    const supportedLanguages = ['typescript', 'javascript', 'python'];
    this.isActive = supportedLanguages.includes(language);
  }
  
  private registerCommands(context: IPluginContext) {
    context.registerCommand({
      id: 'analyze-code-quality',
      name: 'Analyze Code Quality',
      shortcut: 'Ctrl+Shift+Q',
      description: 'Analyze code quality of current file',
      execute: async () => {
        const file = context.editor.getActiveFile();
        if (file) {
          const issues = await this.analyzeFile(file, context);
          this.reporters[0].report(issues);
        }
      }
    });
    
    context.registerCommand({
      id: 'generate-quality-report',
      name: 'Generate Quality Report',
      shortcut: 'Ctrl+Shift+R',
      description: 'Generate detailed quality report',
      execute: async () => {
        const files = context.project.getProjectFiles();
        const allIssues: QualityIssue[] = [];
        
        for (const file of files) {
          const issues = await this.analyzeFile(file, context);
          allIssues.push(...issues);
        }
        
        const report = this.reporters[1].generate(allIssues);
        context.editor.openPreview(report);
      }
    });
  }
  
  private registerSettings(context: IPluginContext) {
    context.registerSetting({
      id: 'auto-analyze',
      name: 'Auto Analyze',
      type: 'boolean',
      default: true,
      description: 'Automatically analyze code quality on save'
    });
    
    context.registerSetting({
      id: 'complexity-threshold',
      name: 'Complexity Threshold',
      type: 'number',
      default: 10,
      min: 1,
      max: 50,
      description: 'Maximum allowed cyclomatic complexity'
    });
  }
  
  async analyzeFile(file: IFile, context: IPluginContext): Promise<QualityIssue[]> {
    const code = file.getContent();
    const language = file.getLanguage();
    const issues: QualityIssue[] = [];
    
    for (const analyzer of this.analyzers) {
      if (analyzer.supports(language)) {
        const foundIssues = await analyzer.analyze(code, {
          filepath: file.getPath(),
          config: this.config.getAll()
        });
        issues.push(...foundIssues);
      }
    }
    
    // 显示问题
    context.editor.showDiagnostics(issues.map(issue => ({
      range: issue.location,
      severity: issue.severity,
      message: issue.message,
      source: 'code-quality-analyzer'
    })));
    
    // 发送通知
    if (issues.length > 0) {
      context.notifications.show({
        type: 'info',
        title: 'Code Quality Analysis',
        message: `Found ${issues.length} issues in ${file.getName()}`,
        action: 'View Report',
        onAction: () => this.reporters[0].report(issues)
      });
    }
    
    return issues;
  }
}

export default CodeQualityAnalyzer;
复杂度分析器
// src/analyzer/complexity.ts
import { CodeAnalyzer, AnalysisContext, QualityIssue } from './types';
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import * as t from '@babel/types';

interface ComplexityConfig {
  threshold: number;
  includeNested: boolean;
}

export class ComplexityAnalyzer implements CodeAnalyzer {
  name = 'complexity';
  private config: ComplexityConfig;
  
  constructor(config?: Partial<ComplexityConfig>) {
    this.config = {
      threshold: config?.threshold ?? 10,
      includeNested: config?.includeNested ?? true
    };
  }
  
  supports(language: string): boolean {
    return ['typescript', 'javascript'].includes(language);
  }
  
  async analyze(code: string, context: AnalysisContext): Promise<QualityIssue[]> {
    const issues: QualityIssue[] = [];
    
    try {
      const ast = parse(code, {
        sourceType: 'module',
        plugins: ['typescript', 'decorators-legacy']
      });
      
      traverse(ast, {
        FunctionDeclaration(path) {
          const complexity = this.calculateComplexity(path.node);
          
          if (complexity > this.config.threshold) {
            issues.push({
              id: `complexity-${path.node.start}`,
              type: 'complexity',
              severity: complexity > this.config.threshold * 2 ? 'error' : 'warning',
              message: `Function has cyclomatic complexity of ${complexity} (threshold: ${this.config.threshold})`,
              location: {
                startLine: path.node.loc?.start.line ?? 0,
                endLine: path.node.loc?.end.line ?? 0,
                startColumn: path.node.loc?.start.column ?? 0,
                endColumn: path.node.loc?.end.column ?? 0
              },
              suggestion: 'Consider breaking this function into smaller, more manageable pieces',
              code: code.substring(path.node.start ?? 0, path.node.end ?? 0)
            });
          }
        }
      });
    } catch (error) {
      console.error(`Error analyzing complexity in ${context.filepath}:`, error);
    }
    
    return issues;
  }
  
  private calculateComplexity(node: t.Function): number {
    let complexity = 1;
    
    const countNodes = (currentNode: t.Node) => {
      if (t.isIfStatement(currentNode)) complexity++;
      if (t.isForStatement(currentNode)) complexity++;
      if (t.isWhileStatement(currentNode)) complexity++;
      if (t.isDoWhileStatement(currentNode)) complexity++;
      if (t.isSwitchCase(currentNode)) complexity++;
      if (t.isLogicalExpression(currentNode)) complexity++;
      if (t.isConditionalExpression(currentNode)) complexity++;
      if (t.isCatchClause(currentNode)) complexity++;
      
      for (const key in currentNode) {
        if (key === 'loc' || key === 'start' || key === 'end') continue;
        const child = (currentNode as any)[key];
        if (child && typeof child === 'object' && child.type) {
          countNodes(child);
        }
      }
    };
    
    countNodes(node);
    return complexity;
  }
}
重复代码检测
// src/analyzer/duplication.ts
import { CodeAnalyzer, AnalysisContext, QualityIssue } from './types';

interface DuplicationConfig {
  minLines: number;
  maxLines: number;
  minTokens: number;
}

export class DuplicationDetector implements CodeAnalyzer {
  name = 'duplication';
  private config: DuplicationConfig;
  
  private tokenCache: Map<string, Token[]> = new Map();
  
  constructor(config?: Partial<DuplicationConfig>) {
    this.config = {
      minLines: config?.minLines ?? 5,
      maxLines: config?.maxLines ?? 100,
      minTokens: config?.minTokens ?? 30
    };
  }
  
  supports(language: string): boolean {
    return true;
  }
  
  async analyze(code: string, context: AnalysisContext): Promise<QualityIssue[]> {
    const issues: QualityIssue[] = [];
    const tokens = this.tokenize(code);
    const normalizedTokens = this.normalizeTokens(tokens);
    const patterns = this.findRepeatedPatterns(normalizedTokens);
    
    for (const pattern of patterns) {
      if (pattern.count >= 2) {
        issues.push({
          id: `duplication-${pattern.hash}`,
          type: 'duplication',
          severity: pattern.count > 3 ? 'error' : 'warning',
          message: `Found ${pattern.count} occurrences of similar code pattern (${pattern.lines} lines)`,
          location: {
            startLine: pattern.occurrences[0].startLine,
            endLine: pattern.occurrences[0].endLine,
            startColumn: 0,
            endColumn: 0
          },
          suggestion: 'Consider extracting this code into a reusable function or variable',
          code: pattern.code,
          relatedLocations: pattern.occurrences.slice(1).map(occ => ({
            startLine: occ.startLine,
            endLine: occ.endLine,
            startColumn: 0,
            endColumn: 0
          }))
        });
      }
    }
    
    return issues;
  }
  
  private tokenize(code: string): Token[] {
    const tokens: Token[] = [];
    const regex = /\b(?:[a-zA-Z_$][\w$]*|\d+|\.\.\.|=>|[{}()[\];,.:]|==|===|!=|!==|<=|>=|&&|\|\||[+\-*/%])\b/g;
    let match;
    
    while ((match = regex.exec(code)) !== null) {
      tokens.push({
        type: this.classifyToken(match[0]),
        value: match[0],
        position: match.index
      });
    }
    
    return tokens;
  }
  
  private normalizeTokens(tokens: Token[]): NormalizedToken[] {
    return tokens.map(token => ({
      type: token.type,
      normalized: this.normalizeValue(token.value),
      position: token.position
    }));
  }
  
  private normalizeValue(value: string): string {
    if (/^\d+$/.test(value)) return '<NUM>';
    if (/^['"`].*['"`]$/.test(value)) return '<STR>';
    if (/^[a-z_]+$/.test(value)) return '<IDENT>';
    return value;
  }
  
  private findRepeatedPatterns(tokens: NormalizedToken[]): Pattern[] {
    const patterns: Map<string, Pattern> = new Map();
    
    for (let length = this.config.minTokens; length <= this.config.maxTokens; length++) {
      for (let i = 0; i <= tokens.length - length; i++) {
        const segment = tokens.slice(i, i + length);
        const hash = this.hashTokens(segment);
        
        if (!patterns.has(hash)) {
          patterns.set(hash, {
            hash,
            count: 0,
            occurrences: [],
            lines: 0,
            code: ''
          });
        }
        
        const pattern = patterns.get(hash)!;
        pattern.count++;
      }
    }
    
    return Array.from(patterns.values())
      .filter(p => p.count >= 2 && p.lines >= this.config.minLines);
  }
  
  private hashTokens(tokens: NormalizedToken[]): string {
    return tokens.map(t => `${t.type}:${t.normalized}`).join('|');
  }
}
控制台报告器
// src/reporters/console.ts
import { Reporter, QualityIssue } from '../analyzer/types';

export class ConsoleReporter implements Reporter {
  name = 'console';
  
  report(issues: QualityIssue[]): void {
    if (issues.length === 0) {
      console.log('✅ No code quality issues found!');
      return;
    }
    
    console.log(`\n📊 Code Quality Report - Found ${issues.length} issues\n`);
    
    const grouped = this.groupBySeverity(issues);
    
    for (const [severity, severityIssues] of Object.entries(grouped)) {
      const icon = this.getSeverityIcon(severity as QualityIssue['severity']);
      const color = this.getSeverityColor(severity as QualityIssue['severity']);
      
      console.log(`${color}${icon} ${severity.toUpperCase()}: ${severityIssues.length} issues${this.reset()}\n`);
      
      for (const issue of severityIssues) {
        console.log(`  ${issue.message}`);
        console.log(`  Location: Line ${issue.location.startLine}-${issue.location.endLine}`);
        console.log(`  Type: ${issue.type}`);
        console.log(`  Suggestion: ${issue.suggestion}`);
        console.log('');
      }
    }
    
    console.log(this.generateSummary(issues));
  }
  
  generate(issues: QualityIssue[]): string {
    return this.report(issues) as unknown as string;
  }
  
  private groupBySeverity(issues: QualityIssue[]): Record<string, QualityIssue[]> {
    const grouped: Record<string, QualityIssue[]> = {};
    
    for (const issue of issues) {
      if (!grouped[issue.severity]) {
        grouped[issue.severity] = [];
      }
      grouped[issue.severity].push(issue);
    }
    
    return grouped;
  }
  
  private generateSummary(issues: QualityIssue[]): string {
    const total = issues.length;
    const byType: Record<string, number> = {};
    
    for (const issue of issues) {
      byType[issue.type] = (byType[issue.type] || 0) + 1;
    }
    
    let summary = '📈 Summary:\n';
    summary += `  Total Issues: ${total}\n`;
    
    for (const [type, count] of Object.entries(byType)) {
      summary += `  - ${type}: ${count}\n`;
    }
    
    return summary;
  }
  
  private getSeverityIcon(severity: QualityIssue['severity']): string {
    const icons = { error: '🔴', warning: '🟡', info: '🔵' };
    return icons[severity] || '⚪';
  }
  
  private getSeverityColor(severity: QualityIssue['severity']): string {
    const colors = {
      error: '\x1b[31m',
      warning: '\x1b[33m',
      info: '\x1b[34m'
    };
    return colors[severity] || '\x1b[0m';
  }
  
  private reset(): string {
    return '\x1b[0m';
  }
}
HTML 报告器
// src/reporters/html.ts
import { Reporter, QualityIssue } from '../analyzer/types';

export class HtmlReporter implements Reporter {
  name = 'html';
  
  report(issues: QualityIssue[]): string {
    return this.generate(issues);
  }
  
  generate(issues: QualityIssue[]): string {
    const summary = this.generateSummary(issues);
    const groupedIssues = this.groupByFile(issues);
    const charts = this.generateCharts(issues);
    
    return `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Code Quality Report</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; }
    .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
    .header { background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 40px; border-radius: 12px; margin-bottom: 20px; }
    .header h1 { font-size: 2em; margin-bottom: 10px; }
    .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 20px; }
    .stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
    .stat-number { font-size: 2em; font-weight: bold; }
    .stat-label { color: #666; }
    .severity-error { color: #e74c3c; }
    .severity-warning { color: #f39c12; }
    .severity-info { color: #3498db; }
    .file-section { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
    .file-section h2 { margin-bottom: 15px; }
    .issue { padding: 15px; border-left: 4px solid; margin-bottom: 10px; border-radius: 4px; }
    .issue.error { border-color: #e74c3c; background: #ffebee; }
    .issue.warning { border-color: #f39c12; background: #fff3e0; }
    .issue-info { font-size: 0.9em; color: #666; margin-top: 5px; }
    .issue-suggestion { margin-top: 10px; padding: 10px; background: #f8f9fa; border-radius: 4px; }
    .code-block { margin-top: 10px; padding: 10px; background: #282c34; color: #abb2bf; border-radius: 4px; overflow-x: auto; }
  </style>
</head>
<body>
  <div class="container">
    ${this.generateHeader(issues)}
    <div class="stats">${this.generateStatsCards(issues)}</div>
    ${this.generateFileSections(groupedIssues)}
  </div>
</body>
</html>
    `;
  }
  
  private generateHeader(issues: QualityIssue[]): string {
    const total = issues.length;
    const date = new Date().toLocaleDateString();
    return `
      <div class="header">
        <h1>📊 Code Quality Report</h1>
        <p>Generated on ${date} | Total Issues: ${total}</p>
      </div>
    `;
  }
  
  private generateStatsCards(issues: QualityIssue[]): string {
    const errors = issues.filter(i => i.severity === 'error').length;
    const warnings = issues.filter(i => i.severity === 'warning').length;
    const infos = issues.filter(i => i.severity === 'info').length;
    
    return `
      <div class="stat-card">
        <div class="stat-number severity-error">${errors}</div>
        <div class="stat-label">Errors</div>
      </div>
      <div class="stat-card">
        <div class="stat-number severity-warning">${warnings}</div>
        <div class="stat-label">Warnings</div>
      </div>
      <div class="stat-card">
        <div class="stat-number severity-info">${infos}</div>
        <div class="stat-label">Info</div>
      </div>
      <div class="stat-card">
        <div class="stat-number">${issues.length > 0 ? (100 - Math.round((issues.filter(i => i.severity === 'error').length / issues.length) * 100)) : 100}%</div>
        <div class="stat-label">Quality Score</div>
      </div>
    `;
  }
  
  private groupByFile(issues: QualityIssue[]): Record<string, QualityIssue[]> {
    const grouped: Record<string, QualityIssue[]> = {};
    
    for (const issue of issues) {
      const filepath = (issue as any).filepath || 'unknown';
      if (!grouped[filepath]) {
        grouped[filepath] = [];
      }
      grouped[filepath].push(issue);
    }
    
    return grouped;
  }
  
  private generateFileSections(grouped: Record<string, QualityIssue[]>): string {
    return Object.entries(grouped)
      .map(([filepath, issues]) => `
        <div class="file-section">
          <h2>📁 ${filepath}</h2>
          ${issues.map(issue => this.generateIssueHTML(issue)).join('')}
        </div>
      `)
      .join('');
  }
  
  private generateIssueHTML(issue: QualityIssue): string {
    return `
      <div class="issue ${issue.severity}">
        <strong>${issue.message}</strong>
        <div class="issue-info">
          Type: ${issue.type} | Line: ${issue.location.startLine}-${issue.location.endLine}
        </div>
        <div class="issue-suggestion">
          💡 <strong>Suggestion:</strong> ${issue.suggestion}
        </div>
        ${issue.code ? `<pre class="code-block"><code>${escapeHtml(issue.code)}</code></pre>` : ''}
      </div>
    `;
  }
  
  private generateCharts(issues: QualityIssue[]): string {
    return '';
  }
  
  private generateSummary(issues: QualityIssue[]): string {
    return `Generated report for ${issues.length} issues`;
  }
}

function escapeHtml(text: string): string {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

三、插件配置与调试

3.1 插件配置文件

// plugin.json
{
  "id": "code-quality-analyzer",
  "name": "Code Quality Analyzer",
  "version": "1.0.0",
  "description": "Real-time code quality analysis tool for AtomCode",
  "author": "Your Name",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "keywords": [
    "code-quality",
    "linting",
    "analysis",
    "refactoring"
  ],
  "engines": {
    "atomcode": ">=1.0.0"
  },
  "categories": [
    "Linters",
    "Code Quality"
  ],
  "activationEvents": [
    "onLanguage:typescript",
    "onLanguage:javascript"
  ],
  "contributes": {
    "commands": [
      {
        "id": "analyze-code-quality",
        "name": "Analyze Code Quality",
        "category": "Code Quality",
        "description": "Analyze code quality of current file"
      },
      {
        "id": "generate-quality-report",
        "name": "Generate Quality Report",
        "category": "Code Quality",
        "description": "Generate detailed quality report for project"
      }
    ],
    "configuration": {
      "title": "Code Quality Analyzer Configuration",
      "properties": {
        "autoAnalyze": {
          "type": "boolean",
          "default": true,
          "description": "Automatically analyze code quality on file save"
        },
        "complexityThreshold": {
          "type": "number",
          "default": 10,
          "min": 1,
          "max": 50,
          "description": "Maximum allowed cyclomatic complexity"
        },
        "minDuplicationLines": {
          "type": "number",
          "default": 5,
          "min": 2,
          "description": "Minimum lines for duplication detection"
        }
      }
    }
  }
}

3.2 插件编译

# 编译 TypeScript
npx tsc

# 或者使用 rollup
npx rollup -c rollup.config.js

# 编译后目录结构
dist/
├── index.js
├── index.d.ts
├── analyzer/
│   ├── complexity.js
│   ├── duplication.js
│   ├── naming.js
│   └── style.js
├── reporters/
│   ├── console.js
│   ├── html.js
│   └── json.js
└── utils/
    ├── ast.js
    ├── config.js
    └── logger.js

3.3 插件安装与调试

# 开发模式安装
cd ~/.atomcode/plugins
ln -s /path/to/code-quality-analyzer .

# 重启 AtomCode
# 或使用命令面板 > Reload Window

# 调试模式
# 打开 Chrome DevTools
Ctrl+Shift+I

# 查看插件日志
# 输出面板 > 选择 "Plugin Logs"

调试技巧

// 使用插件上下文的日志 API
context.notifications.show({
  type: 'debug',
  title: 'Debug Info',
  message: `Analyzing file: ${file.getPath()}`
});

// 使用 console.log
console.log('Plugin debug:', {
  file: file.getPath(),
  language: file.getLanguage(),
  issues: issues.length
});

// 使用断点调试
// 在 VS Code 中附加调试器

四、发布插件

4.1 打包插件

// package.json
{
  "name": "code-quality-analyzer",
  "version": "1.0.0",
  "description": "Real-time code quality analysis tool for AtomCode",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "watch": "tsc --watch",
    "package": "node scripts/package.js",
    "publish": "npm publish"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0",
    "@babel/parser": "^7.24.0",
    "@babel/traverse": "^7.24.0",
    "@babel/types": "^7.24.0"
  }
}

4.2 发布到市场

  1. 创建 AtomCode 账号

  2. 提交插件审核

    • 填写插件信息
    • 上传插件包
    • 等待审核(通常 3-5 个工作日)
  3. 发布到市场

    • 审核通过后自动发布
    • 用户可以在插件市场搜索并安装

4.3 版本管理

# 更新版本号
npm version patch  # 1.0.0 -> 1.0.1
npm version minor  # 1.0.1 -> 1.1.0
npm version major  # 1.1.0 -> 2.0.0

# 发布新版本
npm publish

# 更新 changelog
# CHANGELOG.md

五、进阶技巧

5.1 与 AI 模型集成

class AIEnhancedAnalyzer {
  async analyzeWithAI(code: string, context: IPluginContext): Promise<QualityIssue[]> {
    // 使用 AI 分析代码
    const aiPrompt = `
      Analyze the following code for quality issues:
      
      \`\`\`typescript
      ${code}
      \`\`\`
      
      Please identify:
      1. Code smells and anti-patterns
      2. Potential bugs
      3. Performance issues
      4. Security concerns
      
      Format the response as JSON array with fields:
      { type, severity, message, suggestion }
    `;
    
    const aiResponse = await context.ai.generate(aiPrompt);
    const aiIssues = this.parseAIResponse(aiResponse);
    
    // 结合规则引擎的结果
    const ruleEngineIssues = await this.ruleEngine.analyze(code);
    
    return [...ruleEngineIssues, ...aiIssues];
  }
  
  private parseAIResponse(response: string): QualityIssue[] {
    try {
      const jsonStr = response.match(/```json\n([\s\S]*?)\n```/)?.[1] || response;
      const parsed = JSON.parse(jsonStr);
      
      return parsed.map((item: any) => ({
        id: `ai-${Date.now()}`,
        type: item.type,
        severity: item.severity,
        message: item.message,
        location: { startLine: 0, endLine: 0, startColumn: 0, endColumn: 0 },
        suggestion: item.suggestion
      }));
    } catch {
      console.error('Failed to parse AI response');
      return [];
    }
  }
}

5.2 性能优化

class OptimizedAnalyzer {
  private cache = new Map<string, AnalysisResult>();
  private debounceTimer: NodeJS.Timeout | null = null;
  
  async analyzeOptimized(code: string, file: IFile): Promise<QualityIssue[]> {
    const cacheKey = `${file.getPath()}:${this.hash(code)}`;
    
    // 检查缓存
    const cached = this.cache.get(cacheKey);
    if (cached) {
      return cached.issues;
    }
    
    // 防抖处理
    return new Promise((resolve) => {
      if (this.debounceTimer) {
        clearTimeout(this.debounceTimer);
      }
      
      this.debounceTimer = setTimeout(async () => {
        const issues = await this.analyze(code, file);
        this.cache.set(cacheKey, { issues, timestamp: Date.now() });
        resolve(issues);
      }, 300);
    });
  }
  
  private hash(code: string): string {
    let hash = 0;
    for (let i = 0; i < code.length; i++) {
      hash = ((hash << 5) - hash) + code.charCodeAt(i);
      hash = hash & hash;
    }
    return Math.abs(hash).toString(36);
  }
}

六、总结与展望

通过本篇文章,你已经学会了:

  1. ✅ AtomCode 插件架构和 API
  2. ✅ 从零开发完整的代码质量分析插件
  3. ✅ 插件调试和配置
  4. ✅ 插件发布流程

进阶建议

  • 探索更多插件 API
  • 创建与你的工作流匹配的插件
  • 参与 AtomCode 插件社区
  • 将开源贡献作为学习的一部分

现在就开始你的 AtomCode 插件开发之旅吧!🚀


本文为原创内容,基于作者实际插件开发经验整理。如需转载,请注明出处。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐