Từ kinh nghiệm phát triển 15+ VS Code extensions trong 3 năm qua, tôi nhận ra một thực tế: việc tích hợp AI vào workflow lập trình không còn là lựa chọn mà là xu hướng bắt buộc. Trong bài viết này, tôi sẽ chia sẻ chi tiết cách xây dựng một VS Code extension hoàn chỉnh với khả năng code completion thông minh, đồng thời so sánh các giải pháp API để bạn tối ưu chi phí và hiệu suất.
Tại Sao Cần Tích Hợp AI Vào VS Code?
Theo khảo sát Stack Overflow 2025, 73% developer đã sử dụng ít nhất một công cụ AI-assisted coding. Không phải vì hype, mà vì hiệu suất thực sự được cải thiện:
- Giảm 40-60% thời gian viết code boilerplate
- Tự động hoàn thành function phức tạp từ docstring
- Phát hiện lỗi ngữ pháp và logic ngay khi gõ
- Chuyển đổi ngôn ngữ code giữa các ngôn ngữ lập trình
So Sánh Giải Pháp API Cho Code Completion
Trước khi đi vào code, hãy cùng xem bảng so sánh chi tiết các nhà cung cấp API phổ biến nhất hiện nay:
| Tiêu chí | HolySheep AI | Anthropic Official | OpenAI Official | DeepSeek |
|---|---|---|---|---|
| Claude Sonnet 4.5 | $15/MTok | $15/MTok | - | - |
| GPT-4.1 | $8/MTok | - | $15/MTok | - |
| Gemini 2.5 Flash | $2.50/MTok | - | - | - |
| DeepSeek V3.2 | $0.42/MTok | - | - | $0.27/MTok |
| Độ trễ trung bình | <50ms | 120-200ms | 80-150ms | 60-100ms |
| Phương thức thanh toán | WeChat, Alipay, USDT | Thẻ quốc tế | Thẻ quốc tế | Thẻ quốc tế |
| Tín dụng miễn phí | Có ($5-$20) | Có ($5) | Có ($5) | Có ($10) |
| API Endpoint | https://api.holysheep.ai/v1 | api.anthropic.com | api.openai.com | api.deepseek.com |
Kiến Trúc VS Code Extension Với AI Code Completion
1. Thiết Lập Dự Án
Đầu tiên, khởi tạo project với Yeoman generator:
npm install -g yo generator-code
yo code
Chọn "New Extension (TypeScript)"
Điền thông tin:
- Extension name: ai-code-complete
- Identifier: ai-code-complete
- Description: AI-powered code completion
- Publisher: your-name
- Enable lightweight mode: No
cd ai-code-complete
npm install
2. Cấu Trúc Project
ai-code-complete/
├── src/
│ ├── extension.ts # Entry point
│ ├── providers/
│ │ ├── inlineCompletionProvider.ts
│ │ └── hoverProvider.ts
│ ├── services/
│ │ ├── apiService.ts # HolySheep API integration
│ │ └── configService.ts
│ ├── utils/
│ │ └── contextBuilder.ts # Build prompt context
│ └── types/
│ └── index.ts
├── package.json
├── tsconfig.json
└── vsc-extension-quickstart.md
3. API Service - Kết Nối HolySheep AI
Đây là phần quan trọng nhất - kết nối với HolySheep AI để nhận code suggestions:
// src/services/apiService.ts
import * as vscode from 'vscode';
import fetch from 'node-fetch';
interface CompletionRequest {
model: string;
messages: Array<{
role: 'system' | 'user' | 'assistant';
content: string;
}>;
max_tokens: number;
temperature: number;
stream: boolean;
}
interface CompletionResponse {
id: string;
choices: Array<{
message: {
content: string;
};
finish_reason: string;
}>;
usage: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}
export class ApiService {
private apiKey: string;
private baseUrl: string = 'https://api.holysheep.ai/v1';
private model: string;
private requestTimeout: number = 10000; // 10 seconds
private retryCount: number = 3;
private retryDelay: number = 1000; // 1 second
constructor() {
this.apiKey = vscode.workspace.getConfiguration('aiCodeComplete')
.get('apiKey', '');
this.model = vscode.workspace.getConfiguration('aiCodeComplete')
.get('model', 'claude-sonnet-4.5');
}
public setApiKey(key: string): void {
this.apiKey = key;
}
public setModel(model: string): void {
this.model = model;
}
public async getCompletion(
context: string,
currentCode: string,
cursorPosition: vscode.Position,
documentLanguage: string
): Promise {
if (!this.apiKey) {
throw new Error('API key not configured. Please set aiCodeComplete.apiKey');
}
const systemPrompt = `You are an expert code completion assistant.
Given the current code context and cursor position, provide the most relevant code completion.
Only output the completion code, no explanations.
Language: ${documentLanguage}
Max completion length: 200 tokens`;
const userMessage = Context:\n${context}\n\nCurrent code:\n${currentCode}\n\nCursor position: line ${cursorPosition.line + 1}, column ${cursorPosition.character}\n\nProvide the next line(s) of code to complete:;
const requestBody: CompletionRequest = {
model: this.model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userMessage }
],
max_tokens: 200,
temperature: 0.3,
stream: false
};
return this.executeWithRetry(requestBody);
}
private async executeWithRetry(
requestBody: CompletionRequest,
attempt: number = 1
): Promise {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.requestTimeout);
try {
const startTime = Date.now();
const response = await fetch(${this.baseUrl}/chat/completions, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': Bearer ${this.apiKey}
},
body: JSON.stringify(requestBody),
signal: controller.signal
});
clearTimeout(timeoutId);
const latency = Date.now() - startTime;
if (vscode.workspace.getConfiguration('aiCodeComplete')
.get('showLatency', false)) {
vscode.window.showInformationMessage(
API Response Time: ${latency}ms
);
}
if (!response.ok) {
const errorBody = await response.text();
throw new Error(API Error ${response.status}: ${errorBody});
}
const data: CompletionResponse = await response.json();
if (data.choices && data.choices.length > 0) {
return data.choices[0].message.content.trim();
}
return null;
} catch (error) {
clearTimeout(timeoutId);
if (attempt < this.retryCount) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
vscode.window.showWarningMessage(
Request failed (attempt ${attempt}/${this.retryCount}): ${errorMessage}. Retrying...
);
await this.delay(this.retryDelay * attempt);
return this.executeWithRetry(requestBody, attempt + 1);
}
throw error;
}
}
private delay(ms: number): Promise {
return new Promise(resolve => setTimeout(resolve, ms));
}
public async testConnection(): Promise<{ success: boolean; latency: number; model: string }> {
const startTime = Date.now();
try {
const result = await this.getCompletion(
'Test connection',
'console.log("Hello")',
new vscode.Position(0, 17),
'javascript'
);
return {
success: result !== null,
latency: Date.now() - startTime,
model: this.model
};
} catch {
return {
success: false,
latency: Date.now() - startTime,
model: this.model
};
}
}
}
4. Inline Completion Provider
Provider này sẽ xử lý việc hiển thị gợi ý code trực tiếp trong editor:
// src/providers/inlineCompletionProvider.ts
import * as vscode from 'vscode';
import { ApiService } from '../services/apiService';
import { ContextBuilder } from '../utils/contextBuilder';
export class InlineCompletionProvider implements vscode.InlineCompletionItemProvider {
private apiService: ApiService;
private contextBuilder: ContextBuilder;
private debounceTimer: NodeJS.Timeout | null = null;
private debounceDelay: number = 300;
private lastRequestId: number = 0;
constructor(apiService: ApiService) {
this.apiService = apiService;
this.contextBuilder = new ContextBuilder();
}
public async provideInlineCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
context: vscode.InlineCompletionContext,
token: vscode.CancellationToken
): Promise {
// Debounce requests
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
return new Promise((resolve) => {
this.debounceTimer = setTimeout(async () => {
const result = await this.handleCompletionRequest(
document,
position,
token
);
resolve(result);
}, this.debounceDelay);
});
}
private async handleCompletionRequest(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise {
const currentRequestId = ++this.lastRequestId;
// Check if inline completion was triggered explicitly
const config = vscode.workspace.getConfiguration('aiCodeComplete');
const minCharacters = config.get('minCharactersBeforeTrigger', 3);
const textBeforeCursor = document.lineAt(position).text
.substring(0, position.character);
if (textBeforeCursor.length < minCharacters) {
return [];
}
// Don't trigger on comments or strings (unless in specific languages)
const languageId = document.languageId;
if (this.shouldIgnoreContext(document, position, languageId)) {
return [];
}
try {
// Build context (include nearby functions/classes)
const context = await this.contextBuilder.buildContext(
document,
position,
config.get('contextLinesBefore', 10),
config.get('contextLinesAfter', 5)
);
const currentCode = document.getText();
const completion = await this.apiService.getCompletion(
context,
document.getText(),
position,
languageId
);
// Check if this request is still the latest
if (currentRequestId !== this.lastRequestId || token.isCancellationRequested) {
return [];
}
if (!completion) {
return [];
}
// Convert completion to InlineCompletionItem
const range = new vscode.Range(position, position);
const item = new vscode.InlineCompletionItem(
completion,
range,
{
title: 'AI Code Completion',
command: 'aiCodeComplete.acceptCompletion'
}
);
// Add command to track accepted completions
item.command = {
title: 'Accept Completion',
command: 'aiCodeComplete.trackAcceptance',
arguments: [completion, Date.now()]
};
return [item];
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
// Don't show error for common cases
if (!errorMessage.includes('not configured') &&
!errorMessage.includes('timeout')) {
vscode.window.showErrorMessage(
AI Completion Error: ${errorMessage}
);
}
return [];
}
}
private shouldIgnoreContext(
document: vscode.TextDocument,
position: vscode.Position,
languageId: string
): boolean {
const lineText = document.lineAt(position).text;
const textBeforeCursor = lineText.substring(0, position.character).trim();
// Ignore in comments (except for Python where # is comment)
if (languageId !== 'python' &&
(textBeforeCursor.startsWith('//') ||
textBeforeCursor.startsWith('/*') ||
textBeforeCursor.startsWith('#'))) {
return true;
}
// Ignore inside strings (basic check)
const quoteCount = (textBeforeCursor.match(/["']/g) || []).length;
if (quoteCount % 2 === 1) {
return true;
}
return false;
}
}
5. Context Builder - Tạo Prompt Context Thông Minh
// src/utils/contextBuilder.ts
import * as vscode from 'vscode';
export class ContextBuilder {
public async buildContext(
document: vscode.TextDocument,
position: vscode.Position,
linesBefore: number,
linesAfter: number
): Promise {
const contextParts: string[] = [];
// Current file name
contextParts.push(File: ${document.fileName});
// Language
contextParts.push(Language: ${document.languageId});
// Get current function/class context using regex
const functionContext = this.getCurrentFunctionContext(
document.getText(),
position.line
);
if (functionContext) {
contextParts.push(Current function/class:\n${functionContext});
}
// Import/require statements
const imports = this.getImports(document.getText(), document.languageId);
if (imports.length > 0) {
contextParts.push(Imports:\n${imports.join('\n')});
}
// Type definitions (for typed languages)
const typeDefs = this.getTypeDefinitions(
document.getText(),
document.languageId,
position.line
);
if (typeDefs.length > 0) {
contextParts.push(Related types:\n${typeDefs.join('\n')});
}
return contextParts.join('\n\n');
}
private getCurrentFunctionContext(
documentText: string,
currentLine: number
): string | null {
const lines = documentText.split('\n');
// Simple function detection patterns
const functionPatterns = [
/^function\s+\w+/, // JavaScript function
/^(async\s+)?function\s+\w+/, // Async function
/^\s*(public|private|protected)?\s*(static)?\s*\w+\s*\(/, // Java/C# method
/^\s*def\s+\w+/, // Python function
/^\s*fn\s+\w+/, // Rust function
/^\s*func\s+\w+/, // Go function
/^\s*fun\s+\w+/, // Kotlin function
];
// Find the function containing current line
let functionStart = -1;
let functionIndent = 0;
for (let i = currentLine; i >= 0; i--) {
const line = lines[i];
const trimmedLine = line.trim();
for (const pattern of functionPatterns) {
if (pattern.test(trimmedLine)) {
functionStart = i;
functionIndent = line.search(/\S/);
break;
}
}
if (functionStart !== -1) break;
}
if (functionStart === -1) return null;
// Get function signature
const signature = lines[functionStart].trim();
// Try to get some lines of the function body
let body = '';
const maxBodyLines = 15;
for (let i = functionStart + 1; i < Math.min(lines.length, functionStart + maxBodyLines); i++) {
const line = lines[i];
const trimmedLine = line.trim();
// Stop at next function or class definition
if (/^(class|interface|function|def|fn|func|public|private)/.test(trimmedLine)) {
break;
}
// Calculate indentation to know when function ends
const currentIndent = line.search(/\S/);
if (currentIndent <= functionIndent && trimmedLine.length > 0) {
break;
}
body += line + '\n';
}
return signature + '\n' + body;
}
private getImports(documentText: string, languageId: string): string[] {
const imports: string[] = [];
const lines = documentText.split('\n');
const importPatterns: Record = {
javascript: [/^import\s+.+from\s+['"].+['"]/, /^const\s+.+\s+=\s+require\(/],
typescript: [/^import\s+.+from\s+['"].+['"]/, /^import\s+['"].+['"]/],
python: [/^import\s+\w+/, /^from\s+\w+\s+import/],
java: [/^import\s+.+;/],
go: [/^import\s+\(/, /^import\s+"/],
rust: [/^use\s+.+;/],
};
const patterns = importPatterns[languageId] || importPatterns.javascript;
for (const line of lines) {
for (const pattern of patterns) {
if (pattern.test(line.trim())) {
imports.push(line.trim());
break;
}
}
}
return imports.slice(0, 20); // Limit to 20 imports
}
private getTypeDefinitions(
documentText: string,
languageId: string,
currentLine: number
): string[] {
const types: string[] = [];
const lines = documentText.split('\n');
const typePatterns: Record = {
typescript: [/^interface\s+\w+/, /^type\s+\w+\s*=/, /^enum\s+\w+/],
javascript: [/^\s*class\s+\w+/],
python: [/^class\s+\w+:/, /^@dataclass/, /^\s*\w+:\s*\w+/],
java: [/^public\s+class\s+/, /^interface\s+\w+/, /^enum\s+\w+/],
};
const patterns = typePatterns[languageId] || [];
// Get types defined near current line
for (let i = Math.max(0, currentLine - 50); i < Math.min(lines.length, currentLine + 50); i++) {
for (const pattern of patterns) {
if (pattern.test(lines[i].trim())) {
types.push(lines[i].trim());
break;
}
}
}
return types.slice(0, 10); // Limit to 10 types
}
}
6. Extension Entry Point
// src/extension.ts
import * as vscode from 'vscode';
import { ApiService } from './services/apiService';
import { InlineCompletionProvider } from './providers/inlineCompletionProvider';
let apiService: ApiService;
let inlineCompletionProvider: InlineCompletionProvider;
let statusBarItem: vscode.StatusBarItem;
export function activate(context: vscode.ExtensionContext) {
// Initialize services
apiService = new ApiService();
inlineCompletionProvider = new InlineCompletionProvider(apiService);
// Register inline completion provider
vscode.languages.registerInlineCompletionItemProvider(
{ pattern: '**' }, // All languages
inlineCompletionProvider
);
// Create status bar item
statusBarItem = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Left,
100
);
statusBarItem.text = '$(AI) AI Complete';
statusBarItem.tooltip = 'AI Code Completion';
statusBarItem.show();
statusBarItem.command = 'aiCodeComplete.toggleStatus';
// Register commands
const commands = [
vscode.commands.registerCommand('aiCodeComplete.configure', async () => {
const apiKey = await vscode.window.showInputBox({
prompt: 'Enter your HolySheep AI API Key',
password: true,
ignoreFocusOut: true
});
if (apiKey) {
apiService.setApiKey(apiKey);
await context.secrets.store('aiCodeCompleteApiKey', apiKey);
vscode.window.showInformationMessage('API key configured successfully!');
// Test connection
const result = await apiService.testConnection();
if (result.success) {
vscode.window.showInformationMessage(
Connected to ${result.model} (${result.latency}ms)
);
}
}
}),
vscode.commands.registerCommand('aiCodeComplete.toggleStatus', async () => {
const config = vscode.workspace.getConfiguration('aiCodeComplete');
const enabled = config.get('enabled', true);
await config.update('enabled', !enabled, true);
statusBarItem.text = !enabled ? '$(AI) AI Complete' : '$(AI) Disabled';
vscode.window.showInformationMessage(
AI Completion ${!enabled ? 'enabled' : 'disabled'}
);
}),
vscode.commands.registerCommand('aiCodeComplete.changeModel', async () => {
const models = [
{ label: 'Claude Sonnet 4.5', value: 'claude-sonnet-4.5' },
{ label: 'GPT-4.1', value: 'gpt-4.1' },
{ label: 'Gemini 2.5 Flash', value: 'gemini-2.5-flash' },
{ label: 'DeepSeek V3.2', value: 'deepseek-v3.2' },
];
const selected = await vscode.window.showQuickPick(models, {
placeHolder: 'Select AI model'
});
if (selected) {
apiService.setModel(selected.value);
const config = vscode.workspace.getConfiguration('aiCodeComplete');
await config.update('model', selected.value, true);
vscode.window.showInformationMessage(Model changed to ${selected.label});
}
}),
vscode.commands.registerCommand('aiCodeComplete.trackAcceptance',
(completion: string, timestamp: number) => {
const config = vscode.workspace.getConfiguration('aiCodeComplete');
if (config.get('logAcceptedCompletions', true)) {
console.log([AI Complete] Accepted: "${completion.substring(0, 50)}..." at ${timestamp});
}
}
)
];
// Add commands to subscriptions
commands.forEach(cmd => context.subscriptions.push(cmd));
// Load saved API key
loadSavedApiKey(context);
// Show welcome message for new users
const isFirstTime = context.globalState.get('aiCodeComplete.firstTime', true);
if (isFirstTime) {
vscode.window.showInformationMessage(
'Welcome to AI Code Complete! Run "AI Code Complete: Configure" to set up your API key.',
'Configure Now'
).then(selection => {
if (selection === 'Configure Now') {
vscode.commands.executeCommand('aiCodeComplete.configure');
}
});
context.globalState.update('aiCodeComplete.firstTime', false);
}
}
async function loadSavedApiKey(context: vscode.ExtensionContext) {
try {
const savedKey = await context.secrets.get('aiCodeCompleteApiKey');
if (savedKey) {
apiService.setApiKey(savedKey);
}
} catch {
// Secrets not available in web extension
}
}
export function deactivate() {
if (statusBarItem) {
statusBarItem.dispose();
}
}
7. Package.json Configuration
{
"name": "ai-code-complete",
"displayName": "AI Code Complete",
"description": "AI-powered code completion using Claude, GPT, and Gemini",
"version": "1.0.0",
"publisher": "your-name",
"engines": {
"vscode": "^1.85.0"
},
"categories": [
"Programming Languages",
"Intellisense",
"AI"
],
"keywords": [
"ai",
"code completion",
"claude",
"gpt",
"copilot"
],
"activationEvents": [
"*"
],
"main": "./out/extension.js",
"contributes": {
"configuration": {
"title": "AI Code Complete",
"properties": {
"aiCodeComplete.apiKey": {
"type": "string",
"default": "",
"description": "Your HolySheep AI API key. Get yours at https://www.holysheep.ai/register"
},
"aiCodeComplete.model": {
"type": "string",
"default": "claude-sonnet-4.5",
"enum": [
"claude-sonnet-4.5",
"gpt-4.1",
"gemini-2.5-flash",
"deepseek-v3.2"
],
"description": "AI model to use for completion"
},
"aiCodeComplete.enabled": {
"type": "boolean",
"default": true,
"description": "Enable/disable AI code completion"
},
"aiCodeComplete.minCharactersBeforeTrigger": {
"type": "number",
"default": 3,
"description": "Minimum characters before triggering completion"
},
"aiCodeComplete.contextLinesBefore": {
"type": "number",
"default": 10,
"description": "Lines of context before cursor to include"
},
"aiCodeComplete.contextLinesAfter": {
"type": "number",
"default": 5,
"description": "Lines of context after cursor to include"
},
"aiCodeComplete.showLatency": {
"type": "boolean",
"default": false,
"description": "Show API response latency"
},
"aiCodeComplete.logAcceptedCompletions": {
"type": "boolean",
"default": true,
"description": "Log accepted completions to console"
}
}
},
"commands": [
{
"command": "aiCodeComplete.configure",
"title": "AI Code Complete: Configure",
"category": "AI Code Complete"
},
{
"command": "aiCodeComplete.toggleStatus",
"title": "AI Code Complete: Toggle",
"category": "AI Code Complete"
},
{
"command": "aiCodeComplete.changeModel",
"title": "AI Code Complete: Change Model",
"category": "AI Code Complete"
}
]
},
"dependencies": {
"node-fetch": "^2.7.0"
},
"devDependencies": {
"@types/node": "^18.19.0",
"@types/node-fetch": "^2.6.11",
"@types/vscode": "^1.85.0",
"@vscode/test-electron": "^2.3.8",
"typescript": "^5.3.3"
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"test": "node ./out/test/runTest.js"
}
}
Đăng Ký API Key HolySheep AI
Để sử dụng extension, bạn cần đăng ký tài khoản và lấy API key từ HolySheep AI. Quy trình đăng ký chỉ mất 2 phút và bạn sẽ nhận được tín dụng miễn phí $5-$20 để bắt đầu sử dụng.
Phù Hợp / Không Phù Hợp Với Ai
| 🎯 Nên Sử Dụng | 🚫 Không Nên Sử Dụng |
|---|---|
|
|
Giá Và ROI
Hãy tính toán chi phí tiết kiệm khi sử dụng HolySheep AI so với official API:
| Model | HolySheep AI | Official API | Tiết Kiệm | Chi Phí/Tháng (100K Tokens) |
|---|---|---|---|---|
| Claude Sonnet 4.5 | $15/MTok | $15/MTok | Tương đương | $1.50 |
| GPT-4.1 |