بناء أدوات المطورين
إنشاء إضافات VSCode
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
- Create Azure DevOps organization at https://dev.azure.com
- Create Personal Access Token (PAT) with Marketplace permissions
- Create publisher at https://marketplace.visualstudio.com/manage
Key Takeaways
- Activation events control when your extension loads
- Commands are the primary way users interact with extensions
- Configuration should be exposed via settings
- Webviews enable rich UI but use them sparingly
- 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
إعداد الناشر
- أنشئ منظمة Azure DevOps على https://dev.azure.com
- أنشئ رمز وصول شخصي (PAT) مع أذونات السوق
- أنشئ ناشر على https://marketplace.visualstudio.com/manage
النقاط الرئيسية
- أحداث التنشيط تتحكم في متى تُحمّل إضافتك
- الأوامر هي الطريقة الأساسية لتفاعل المستخدمين مع الإضافات
- التكوين يجب عرضه عبر الإعدادات
- Webviews تُمكّن واجهة مستخدم غنية لكن استخدمها باعتدال
- عروض الشجرة تندمج بشكل طبيعي في الشريط الجانبي لـ VSCode