بناء أدوات المطورين

إنشاء إضافات VSCode

5 دقيقة للقراءة

Project Goal: Build a VSCode extension that solves a real developer problem and publish it to the marketplace.

Why Build VSCode Extensions?

VSCode extensions have massive reach:

  • 30M+ daily active users - Huge potential impact
  • Rich API - Access to editor, terminals, file system
  • TypeScript native - Type-safe development
  • Quick iteration - Built-in debugging and reload
  • Marketplace distribution - Easy publishing and updates

Extension Setup Prompt

Create a VSCode extension with:

## Extension Type
Choose one:
- Language support (syntax, snippets, intellisense)
- Productivity tool (commands, views, automation)
- Theme/UI customization
- Integration (external service, API)

## Tech Stack
- TypeScript
- VSCode Extension API
- esbuild for bundling

## Project Structure

/src extension.ts # Extension entry point /commands # Command implementations /providers # Language/completion providers /views # Webview and tree views /utils # Helper functions /webview # Webview UI (if needed) index.html main.ts package.json # Extension manifest tsconfig.json


## Initial Features
1. Activation event configuration
2. At least one command
3. Status bar item or tree view
4. Configuration settings

Project Initialization

# Install Yeoman and VSCode extension generator
npm install -g yo generator-code

# Generate extension scaffold
yo code

# Or create manually
mkdir my-extension && cd my-extension
npm init -y

Extension Manifest

// package.json
{
  "name": "my-awesome-extension",
  "displayName": "My Awesome Extension",
  "description": "A productivity extension built with AI",
  "version": "0.0.1",
  "publisher": "your-publisher-name",
  "engines": {
    "vscode": "^1.95.0"
  },
  "categories": ["Other"],
  "activationEvents": [],
  "main": "./dist/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "myExtension.helloWorld",
        "title": "Hello World",
        "category": "My Extension"
      },
      {
        "command": "myExtension.runAnalysis",
        "title": "Run Analysis",
        "category": "My Extension",
        "icon": "$(play)"
      }
    ],
    "keybindings": [
      {
        "command": "myExtension.runAnalysis",
        "key": "ctrl+shift+a",
        "mac": "cmd+shift+a",
        "when": "editorTextFocus"
      }
    ],
    "menus": {
      "editor/context": [
        {
          "command": "myExtension.runAnalysis",
          "group": "myExtension",
          "when": "editorHasSelection"
        }
      ],
      "editor/title": [
        {
          "command": "myExtension.runAnalysis",
          "group": "navigation"
        }
      ]
    },
    "configuration": {
      "title": "My Extension",
      "properties": {
        "myExtension.enabled": {
          "type": "boolean",
          "default": true,
          "description": "Enable the extension"
        },
        "myExtension.apiKey": {
          "type": "string",
          "default": "",
          "description": "API key for external service"
        },
        "myExtension.autoRun": {
          "type": "boolean",
          "default": false,
          "description": "Automatically run analysis on save"
        }
      }
    },
    "viewsContainers": {
      "activitybar": [
        {
          "id": "myExtensionSidebar",
          "title": "My Extension",
          "icon": "resources/icon.svg"
        }
      ]
    },
    "views": {
      "myExtensionSidebar": [
        {
          "id": "myExtensionResults",
          "name": "Results"
        }
      ]
    }
  },
  "scripts": {
    "vscode:prepublish": "npm run compile",
    "compile": "esbuild src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node",
    "watch": "npm run compile -- --watch",
    "lint": "eslint src --ext ts"
  },
  "devDependencies": {
    "@types/node": "^22.9.0",
    "@types/vscode": "^1.95.0",
    "@typescript-eslint/eslint-plugin": "^8.13.0",
    "@typescript-eslint/parser": "^8.13.0",
    "esbuild": "^0.24.0",
    "eslint": "^9.14.0",
    "typescript": "^5.6.3"
  }
}

Extension Entry Point

// src/extension.ts
import * as vscode from 'vscode';
import { registerCommands } from './commands';
import { ResultsProvider } from './providers/results-provider';
import { StatusBarManager } from './utils/status-bar';

let statusBar: StatusBarManager;

export function activate(context: vscode.ExtensionContext) {
  console.log('My Extension is now active!');

  // Initialize status bar
  statusBar = new StatusBarManager();
  context.subscriptions.push(statusBar);

  // Register commands
  registerCommands(context, statusBar);

  // Register tree view
  const resultsProvider = new ResultsProvider();
  vscode.window.registerTreeDataProvider(
    'myExtensionResults',
    resultsProvider
  );

  // Register event listeners
  context.subscriptions.push(
    vscode.workspace.onDidSaveTextDocument((document) => {
      const config = vscode.workspace.getConfiguration('myExtension');
      if (config.get('autoRun')) {
        vscode.commands.executeCommand('myExtension.runAnalysis');
      }
    })
  );

  // Show welcome message on first activation
  const hasShownWelcome = context.globalState.get('hasShownWelcome');
  if (!hasShownWelcome) {
    vscode.window
      .showInformationMessage(
        'My Extension is ready! Run analysis with Cmd+Shift+A',
        'Open Settings'
      )
      .then((selection) => {
        if (selection === 'Open Settings') {
          vscode.commands.executeCommand(
            'workbench.action.openSettings',
            'myExtension'
          );
        }
      });
    context.globalState.update('hasShownWelcome', true);
  }
}

export function deactivate() {
  console.log('My Extension is now deactivated');
}

Implementing Commands

// src/commands/index.ts
import * as vscode from 'vscode';
import { StatusBarManager } from '../utils/status-bar';
import { analyzeCode } from './analyze';

export function registerCommands(
  context: vscode.ExtensionContext,
  statusBar: StatusBarManager
) {
  // Hello World command
  context.subscriptions.push(
    vscode.commands.registerCommand('myExtension.helloWorld', () => {
      vscode.window.showInformationMessage('Hello from My Extension!');
    })
  );

  // Run Analysis command
  context.subscriptions.push(
    vscode.commands.registerCommand('myExtension.runAnalysis', async () => {
      const editor = vscode.window.activeTextEditor;
      if (!editor) {
        vscode.window.showWarningMessage('No active editor');
        return;
      }

      statusBar.showProgress('Analyzing...');

      try {
        const text = editor.selection.isEmpty
          ? editor.document.getText()
          : editor.document.getText(editor.selection);

        const results = await analyzeCode(text);

        // Show results in output channel
        const outputChannel = vscode.window.createOutputChannel('My Extension');
        outputChannel.clear();
        outputChannel.appendLine('Analysis Results:');
        outputChannel.appendLine(JSON.stringify(results, null, 2));
        outputChannel.show();

        statusBar.showSuccess(`Found ${results.length} items`);
      } catch (error) {
        statusBar.showError('Analysis failed');
        vscode.window.showErrorMessage(
          `Analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`
        );
      }
    })
  );
}
// src/commands/analyze.ts
import * as vscode from 'vscode';

interface AnalysisResult {
  type: string;
  message: string;
  line: number;
  severity: 'info' | 'warning' | 'error';
}

export async function analyzeCode(code: string): Promise<AnalysisResult[]> {
  const config = vscode.workspace.getConfiguration('myExtension');
  const apiKey = config.get<string>('apiKey');

  // Simulate analysis (replace with actual logic)
  return new Promise((resolve) => {
    setTimeout(() => {
      const results: AnalysisResult[] = [];

      // Example: Find TODO comments
      const lines = code.split('\n');
      lines.forEach((line, index) => {
        if (line.includes('TODO')) {
          results.push({
            type: 'todo',
            message: line.trim(),
            line: index + 1,
            severity: 'info',
          });
        }
        if (line.includes('FIXME')) {
          results.push({
            type: 'fixme',
            message: line.trim(),
            line: index + 1,
            severity: 'warning',
          });
        }
      });

      resolve(results);
    }, 500);
  });
}

Status Bar Management

// src/utils/status-bar.ts
import * as vscode from 'vscode';

export class StatusBarManager implements vscode.Disposable {
  private statusBarItem: vscode.StatusBarItem;
  private timeout: NodeJS.Timeout | undefined;

  constructor() {
    this.statusBarItem = vscode.window.createStatusBarItem(
      vscode.StatusBarAlignment.Right,
      100
    );
    this.statusBarItem.command = 'myExtension.runAnalysis';
    this.showReady();
    this.statusBarItem.show();
  }

  showReady() {
    this.statusBarItem.text = '$(check) My Extension';
    this.statusBarItem.tooltip = 'Click to run analysis';
    this.statusBarItem.backgroundColor = undefined;
  }

  showProgress(message: string) {
    this.clearTimeout();
    this.statusBarItem.text = `$(sync~spin) ${message}`;
    this.statusBarItem.tooltip = 'Analysis in progress...';
  }

  showSuccess(message: string) {
    this.clearTimeout();
    this.statusBarItem.text = `$(check) ${message}`;
    this.statusBarItem.tooltip = 'Analysis complete';
    this.timeout = setTimeout(() => this.showReady(), 3000);
  }

  showError(message: string) {
    this.clearTimeout();
    this.statusBarItem.text = `$(error) ${message}`;
    this.statusBarItem.tooltip = 'Analysis failed';
    this.statusBarItem.backgroundColor = new vscode.ThemeColor(
      'statusBarItem.errorBackground'
    );
    this.timeout = setTimeout(() => this.showReady(), 5000);
  }

  private clearTimeout() {
    if (this.timeout) {
      clearTimeout(this.timeout);
      this.timeout = undefined;
    }
  }

  dispose() {
    this.clearTimeout();
    this.statusBarItem.dispose();
  }
}

Tree View Provider

// src/providers/results-provider.ts
import * as vscode from 'vscode';

interface ResultItem {
  label: string;
  description?: string;
  icon?: string;
  children?: ResultItem[];
}

export class ResultsProvider
  implements vscode.TreeDataProvider<ResultItem>
{
  private _onDidChangeTreeData = new vscode.EventEmitter<
    ResultItem | undefined
  >();
  readonly onDidChangeTreeData = this._onDidChangeTreeData.event;

  private results: ResultItem[] = [];

  refresh(): void {
    this._onDidChangeTreeData.fire(undefined);
  }

  setResults(results: ResultItem[]): void {
    this.results = results;
    this.refresh();
  }

  getTreeItem(element: ResultItem): vscode.TreeItem {
    const treeItem = new vscode.TreeItem(
      element.label,
      element.children
        ? vscode.TreeItemCollapsibleState.Expanded
        : vscode.TreeItemCollapsibleState.None
    );

    treeItem.description = element.description;

    if (element.icon) {
      treeItem.iconPath = new vscode.ThemeIcon(element.icon);
    }

    return treeItem;
  }

  getChildren(element?: ResultItem): ResultItem[] {
    if (!element) {
      return this.results;
    }
    return element.children || [];
  }
}

Webview Panel

// src/providers/webview-panel.ts
import * as vscode from 'vscode';

export class ResultsPanel {
  public static currentPanel: ResultsPanel | undefined;
  private readonly panel: vscode.WebviewPanel;
  private disposables: vscode.Disposable[] = [];

  private constructor(
    panel: vscode.WebviewPanel,
    extensionUri: vscode.Uri
  ) {
    this.panel = panel;

    // Set the webview's initial html content
    this.panel.webview.html = this.getHtmlContent();

    // Handle messages from the webview
    this.panel.webview.onDidReceiveMessage(
      (message) => {
        switch (message.command) {
          case 'analyze':
            vscode.commands.executeCommand('myExtension.runAnalysis');
            break;
          case 'openSettings':
            vscode.commands.executeCommand(
              'workbench.action.openSettings',
              'myExtension'
            );
            break;
        }
      },
      null,
      this.disposables
    );

    // Handle panel disposal
    this.panel.onDidDispose(() => this.dispose(), null, this.disposables);
  }

  public static createOrShow(extensionUri: vscode.Uri) {
    const column = vscode.window.activeTextEditor
      ? vscode.window.activeTextEditor.viewColumn
      : undefined;

    if (ResultsPanel.currentPanel) {
      ResultsPanel.currentPanel.panel.reveal(column);
      return;
    }

    const panel = vscode.window.createWebviewPanel(
      'myExtensionResults',
      'My Extension Results',
      column || vscode.ViewColumn.One,
      {
        enableScripts: true,
        retainContextWhenHidden: true,
      }
    );

    ResultsPanel.currentPanel = new ResultsPanel(panel, extensionUri);
  }

  public updateResults(results: any[]) {
    this.panel.webview.postMessage({
      type: 'updateResults',
      results,
    });
  }

  private getHtmlContent(): string {
    return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Results</title>
  <style>
    body {
      font-family: var(--vscode-font-family);
      padding: 20px;
      color: var(--vscode-foreground);
    }
    .results-container {
      margin-top: 20px;
    }
    .result-item {
      padding: 10px;
      margin: 5px 0;
      background: var(--vscode-editor-background);
      border-radius: 4px;
    }
    button {
      background: var(--vscode-button-background);
      color: var(--vscode-button-foreground);
      border: none;
      padding: 8px 16px;
      cursor: pointer;
      border-radius: 4px;
    }
    button:hover {
      background: var(--vscode-button-hoverBackground);
    }
  </style>
</head>
<body>
  <h1>Analysis Results</h1>
  <button onclick="runAnalysis()">Run Analysis</button>
  <button onclick="openSettings()">Settings</button>

  <div class="results-container" id="results">
    <p>Click "Run Analysis" to start</p>
  </div>

  <script>
    const vscode = acquireVsCodeApi();

    function runAnalysis() {
      vscode.postMessage({ command: 'analyze' });
    }

    function openSettings() {
      vscode.postMessage({ command: 'openSettings' });
    }

    window.addEventListener('message', event => {
      const message = event.data;
      if (message.type === 'updateResults') {
        const container = document.getElementById('results');
        container.innerHTML = message.results
          .map(r => '<div class="result-item">' + r.message + '</div>')
          .join('');
      }
    });
  </script>
</body>
</html>`;
  }

  private dispose() {
    ResultsPanel.currentPanel = undefined;
    this.panel.dispose();
    this.disposables.forEach((d) => d.dispose());
  }
}

Publishing to Marketplace

# Install vsce (Visual Studio Code Extensions CLI)
npm install -g @vscode/vsce

# Package the extension
vsce package

# Login to publisher account
vsce login your-publisher-name

# Publish
vsce publish

# Or publish with version bump
vsce publish minor

Publisher Setup

  1. Create Azure DevOps organization at https://dev.azure.com
  2. Create Personal Access Token (PAT) with Marketplace permissions
  3. Create publisher at https://marketplace.visualstudio.com/manage

Key Takeaways

  1. Activation events control when your extension loads
  2. Commands are the primary way users interact with extensions
  3. Configuration should be exposed via settings
  4. Webviews enable rich UI but use them sparingly
  5. Tree views integrate naturally into VSCode's sidebar

إنشاء إضافات VSCode

هدف المشروع: بناء إضافة VSCode تحل مشكلة مطورين حقيقية ونشرها على السوق.

لماذا نبني إضافات VSCode؟

إضافات VSCode لها وصول هائل:

  • 30+ مليون مستخدم نشط يومياً - تأثير محتمل ضخم
  • API غني - الوصول للمحرر والطرفيات ونظام الملفات
  • TypeScript أصلي - تطوير آمن الأنواع
  • تكرار سريع - تصحيح أخطاء وإعادة تحميل مدمجين
  • توزيع السوق - نشر وتحديثات سهلة

برومبت إعداد الإضافة

أنشئ إضافة VSCode مع:

## نوع الإضافة
اختر واحداً:
- دعم اللغة (بناء الجملة، قصاصات، الإكمال الذكي)
- أداة إنتاجية (أوامر، عروض، أتمتة)
- تخصيص الثيم/واجهة المستخدم
- تكامل (خدمة خارجية، API)

## التقنيات
- TypeScript
- VSCode Extension API
- esbuild للتجميع

## هيكل المشروع

/src extension.ts # نقطة دخول الإضافة /commands # تنفيذات الأوامر /providers # مزودو اللغة/الإكمال /views # Webview وعروض الشجرة /utils # دوال المساعدة /webview # واجهة Webview (إذا لزم) index.html main.ts package.json # بيان الإضافة tsconfig.json


## الميزات الأولية
1. تكوين حدث التنشيط
2. أمر واحد على الأقل
3. عنصر شريط الحالة أو عرض الشجرة
4. إعدادات التكوين

تهيئة المشروع

# تثبيت Yeoman ومولد إضافات VSCode
npm install -g yo generator-code

# توليد هيكل الإضافة
yo code

# أو إنشاء يدوياً
mkdir my-extension && cd my-extension
npm init -y

بيان الإضافة

// package.json
{
  "name": "my-awesome-extension",
  "displayName": "إضافتي الرائعة",
  "description": "إضافة إنتاجية مبنية مع AI",
  "version": "0.0.1",
  "publisher": "your-publisher-name",
  "engines": {
    "vscode": "^1.95.0"
  },
  "categories": ["Other"],
  "activationEvents": [],
  "main": "./dist/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "myExtension.helloWorld",
        "title": "مرحباً بالعالم",
        "category": "إضافتي"
      },
      {
        "command": "myExtension.runAnalysis",
        "title": "تشغيل التحليل",
        "category": "إضافتي",
        "icon": "$(play)"
      }
    ],
    "keybindings": [
      {
        "command": "myExtension.runAnalysis",
        "key": "ctrl+shift+a",
        "mac": "cmd+shift+a",
        "when": "editorTextFocus"
      }
    ],
    "configuration": {
      "title": "إضافتي",
      "properties": {
        "myExtension.enabled": {
          "type": "boolean",
          "default": true,
          "description": "تفعيل الإضافة"
        },
        "myExtension.apiKey": {
          "type": "string",
          "default": "",
          "description": "مفتاح API للخدمة الخارجية"
        },
        "myExtension.autoRun": {
          "type": "boolean",
          "default": false,
          "description": "تشغيل التحليل تلقائياً عند الحفظ"
        }
      }
    }
  },
  "scripts": {
    "vscode:prepublish": "npm run compile",
    "compile": "esbuild src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node",
    "watch": "npm run compile -- --watch",
    "lint": "eslint src --ext ts"
  }
}

نقطة دخول الإضافة

// src/extension.ts
import * as vscode from 'vscode';
import { registerCommands } from './commands';
import { ResultsProvider } from './providers/results-provider';
import { StatusBarManager } from './utils/status-bar';

let statusBar: StatusBarManager;

export function activate(context: vscode.ExtensionContext) {
  console.log('إضافتي نشطة الآن!');

  // تهيئة شريط الحالة
  statusBar = new StatusBarManager();
  context.subscriptions.push(statusBar);

  // تسجيل الأوامر
  registerCommands(context, statusBar);

  // تسجيل عرض الشجرة
  const resultsProvider = new ResultsProvider();
  vscode.window.registerTreeDataProvider(
    'myExtensionResults',
    resultsProvider
  );

  // تسجيل مستمعي الأحداث
  context.subscriptions.push(
    vscode.workspace.onDidSaveTextDocument((document) => {
      const config = vscode.workspace.getConfiguration('myExtension');
      if (config.get('autoRun')) {
        vscode.commands.executeCommand('myExtension.runAnalysis');
      }
    })
  );

  // عرض رسالة الترحيب عند التنشيط الأول
  const hasShownWelcome = context.globalState.get('hasShownWelcome');
  if (!hasShownWelcome) {
    vscode.window
      .showInformationMessage(
        'إضافتي جاهزة! شغّل التحليل بـ Cmd+Shift+A',
        'فتح الإعدادات'
      )
      .then((selection) => {
        if (selection === 'فتح الإعدادات') {
          vscode.commands.executeCommand(
            'workbench.action.openSettings',
            'myExtension'
          );
        }
      });
    context.globalState.update('hasShownWelcome', true);
  }
}

export function deactivate() {
  console.log('إضافتي معطلة الآن');
}

تنفيذ الأوامر

// src/commands/index.ts
import * as vscode from 'vscode';
import { StatusBarManager } from '../utils/status-bar';
import { analyzeCode } from './analyze';

export function registerCommands(
  context: vscode.ExtensionContext,
  statusBar: StatusBarManager
) {
  // أمر مرحباً بالعالم
  context.subscriptions.push(
    vscode.commands.registerCommand('myExtension.helloWorld', () => {
      vscode.window.showInformationMessage('مرحباً من إضافتي!');
    })
  );

  // أمر تشغيل التحليل
  context.subscriptions.push(
    vscode.commands.registerCommand('myExtension.runAnalysis', async () => {
      const editor = vscode.window.activeTextEditor;
      if (!editor) {
        vscode.window.showWarningMessage('لا يوجد محرر نشط');
        return;
      }

      statusBar.showProgress('جاري التحليل...');

      try {
        const text = editor.selection.isEmpty
          ? editor.document.getText()
          : editor.document.getText(editor.selection);

        const results = await analyzeCode(text);

        // عرض النتائج في قناة المخرجات
        const outputChannel = vscode.window.createOutputChannel('إضافتي');
        outputChannel.clear();
        outputChannel.appendLine('نتائج التحليل:');
        outputChannel.appendLine(JSON.stringify(results, null, 2));
        outputChannel.show();

        statusBar.showSuccess(`وجد ${results.length} عناصر`);
      } catch (error) {
        statusBar.showError('فشل التحليل');
        vscode.window.showErrorMessage(
          `فشل التحليل: ${error instanceof Error ? error.message : 'خطأ غير معروف'}`
        );
      }
    })
  );
}

إدارة شريط الحالة

// src/utils/status-bar.ts
import * as vscode from 'vscode';

export class StatusBarManager implements vscode.Disposable {
  private statusBarItem: vscode.StatusBarItem;
  private timeout: NodeJS.Timeout | undefined;

  constructor() {
    this.statusBarItem = vscode.window.createStatusBarItem(
      vscode.StatusBarAlignment.Right,
      100
    );
    this.statusBarItem.command = 'myExtension.runAnalysis';
    this.showReady();
    this.statusBarItem.show();
  }

  showReady() {
    this.statusBarItem.text = '$(check) إضافتي';
    this.statusBarItem.tooltip = 'انقر لتشغيل التحليل';
    this.statusBarItem.backgroundColor = undefined;
  }

  showProgress(message: string) {
    this.clearTimeout();
    this.statusBarItem.text = `$(sync~spin) ${message}`;
    this.statusBarItem.tooltip = 'التحليل قيد التقدم...';
  }

  showSuccess(message: string) {
    this.clearTimeout();
    this.statusBarItem.text = `$(check) ${message}`;
    this.statusBarItem.tooltip = 'اكتمل التحليل';
    this.timeout = setTimeout(() => this.showReady(), 3000);
  }

  showError(message: string) {
    this.clearTimeout();
    this.statusBarItem.text = `$(error) ${message}`;
    this.statusBarItem.tooltip = 'فشل التحليل';
    this.statusBarItem.backgroundColor = new vscode.ThemeColor(
      'statusBarItem.errorBackground'
    );
    this.timeout = setTimeout(() => this.showReady(), 5000);
  }

  private clearTimeout() {
    if (this.timeout) {
      clearTimeout(this.timeout);
      this.timeout = undefined;
    }
  }

  dispose() {
    this.clearTimeout();
    this.statusBarItem.dispose();
  }
}

النشر على السوق

# تثبيت vsce (CLI إضافات Visual Studio Code)
npm install -g @vscode/vsce

# تجميع الإضافة
vsce package

# تسجيل الدخول لحساب الناشر
vsce login your-publisher-name

# النشر
vsce publish

# أو النشر مع رفع الإصدار
vsce publish minor

إعداد الناشر

  1. أنشئ منظمة Azure DevOps على https://dev.azure.com
  2. أنشئ رمز وصول شخصي (PAT) مع أذونات السوق
  3. أنشئ ناشر على https://marketplace.visualstudio.com/manage

النقاط الرئيسية

  1. أحداث التنشيط تتحكم في متى تُحمّل إضافتك
  2. الأوامر هي الطريقة الأساسية لتفاعل المستخدمين مع الإضافات
  3. التكوين يجب عرضه عبر الإعدادات
  4. Webviews تُمكّن واجهة مستخدم غنية لكن استخدمها باعتدال
  5. عروض الشجرة تندمج بشكل طبيعي في الشريط الجانبي لـ VSCode

اختبار

الوحدة 3: بناء أدوات المطورين

خذ الاختبار