AI-ассистент в вашей IDE: Как написать плагин для VS Code с нуля

26.06.2026 13:00

Коллега молча удалил блок кода. Copilot снова предложил использовать axios.get для запроса к биллингу, проигнорировав наш внутренний CoreNetworkClient, обязательную подпись HMAC и еще десяток корпоративных стандартов. В этот момент рушится главная иллюзия современной разработки: вера в то, что универсальный AI-ассистент от техногиганта нельзя превзойти в принципе.

Copilot великолепен на открытом коде. Но в узкой нише энтерпрайза, где правят многолетнее легаси, самописные фреймворки и закрытые внутренние API, он превращается в уверенного в себе стажера-фантазера. Он не знает вашего контекста. И никогда не узнает, если вы ему его не скормите.

Мы решили написать собственный плагин для VS Code. Не ради конкуренции с Microsoft, а чтобы получить инструмент, который знает наш код лучше нас самих.

Анатомия расширения: Extension Host и API

Разработка под VS Code начинается с понимания жесткой архитектурной границы: ваш код работает в изолированном процессе Node.js, который называется Extension Host. Вы не можете напрямую трогать DOM редактора или вмешиваться в его внутренний цикл отрисовки. Все взаимодействие идет строго через объект vscode.

Генерация каркаса занимает минуту: npx yo code. Дальше начинается инженерия. Чтобы сделать автодополнение в стиле Copilot (серый текст впереди курсора), нужно реализовать интерфейс vscode.InlineCompletionItemProvider.

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
 const provider = new EnterpriseCompletionProvider;
 context.subscriptions.push(
 vscode.languages.registerInlineCompletionItemProvider({ pattern: '**' }, provider)
 );
}

Звучит тривиально. Но метод provideInlineCompletionItems вызывается при каждом нажатии клавиши. Если вы будете отправлять запрос к LLM на каждый чих, вы сожжете бюджет отдела за неделю, а редактор будет висеть в ожидании ответа из-за исчерпания лимитов соединений. Первый шаг — жесткий дебаунс на 300-500 миллисекунд и обязательная отмена предыдущих сетевых запросов через CancellationToken.

Контекст решает всё: Парсинг AST

Отправить весь открытый файл в промпт — ленивое и неэффективное решение. Файл может весить 5000 строк. Нам нужен хирургически точный контекст.

Мы подключили typescript API для парсинга абстрактного синтаксического дерева (AST). Когда разработчик задерживает курсор внутри метода, плагин поднимается по дереву, находит объявление текущего класса, собирает интерфейсы всех внедренных зависимостей и вытаскивает их сигнатуры.

import * as ts from 'typescript';

function getContextAtPosition(document: vscode.TextDocument, position: vscode.Position) {
 const sourceText = document.getText;
 const sourceFile = ts.createSourceFile(
 document.fileName,
 sourceText,
 ts.ScriptTarget.Latest,
 true
 );

 const offset = document.offsetAt(position);
 let currentNode: ts.Node | null = null;

 const visit = (node: ts.Node) => {
 if (node.pos <= offset && node.end >= offset) {
 currentNode = node;
 ts.forEachChild(node, visit);
 }
 };
 visit(sourceFile);

 return extractSignatures(currentNode);
}

Парсер собирает только то, что действительно нужно: типы аргументов, возвращаемые значения, соседние методы в классе. Мы сжимаем контекст до 2-3 тысяч токенов чистой, концентрированной логики.

Здесь мы столкнулись с главной проблемой AST в реальном времени: код в редакторе почти всегда синтаксически невалиден. Разработчик пишет строку, скобки еще не закрыты. Стандартный парсер ломается. Пришлось внедрять error-tolerant парсинг и регулярные выражения для восстановления битых узлов дерева, чтобы вытащить хоть какой-то контекст из хаоса незавершенного кода.

Подключение мозгов: RouterAPI

Собрав контекст, нужно отправить его модели. Привязываться к одному провайдеру вроде OpenAI — архитектурная ошибка. Завтра выйдет новый Claude, который пишет код лучше, или руководство потребует перевести все на локальные модели из-за NDA.

Мы используем RouterAPI (наш внутренний шлюз). Он предоставляет совместимый с OpenAI интерфейс, но под капотом маршрутизирует запросы. Для сложных архитектурных задач мы стучимся в Claude 3.5 Sonnet. Для быстрого автодополнения — в Llama 3 70B или специализированные кодерские модели.

import OpenAI from 'openai';

const client = new OpenAI({
 baseURL: 'https://RouterAPI.internal/v1',
 apiKey: process.env.ROUTER_API_KEY
});

async function fetchCompletion(prompt: string, context: string, token: vscode.CancellationToken) {
 const response = await client.chat.completions.create({
 model: 'router/coding-fast',
 messages: [
 { role: 'system', content: `Ты senior-разработчик. Используй эти внутренние API: ${context}` },
 { role: 'user', content: prompt }
 ],
 temperature: 0.1,
 stream: true
 }, { signal: token.isCancellationRequested ? abortController.signal : undefined });

 // Сборка чанков и возврат vscode.InlineCompletionItem
}

Интеграция с RouterAPI решила проблему зоопарка ключей и лимитов. Шлюз прозрачно нормализует стоимость токенов от разных провайдеров и сводит их к единому внутреннему биллингу. Если Anthropic лежит, RouterAPI моментально перекидывает запрос на резервный OpenAI, сохраняя стриминг ответа. Разработчик в IDE видит лишь непрерывное появление символов.

WebView: Когда автодополнения мало

Иногда нужно не просто дописать строку, а попросить: "Оберни этот вызов в нашу стандартную транзакцию с ретраями". Для этого мы добавили боковую панель чата.

В VS Code это делается через WebView API. По сути, вы рендерите iframe с HTML/CSS/JS внутри редактора. Главная боль здесь — двусторонний обмен сообщениями. WebView изолирован от Extension Host. Вы общаетесь исключительно через асинхронный postMessage.

Мы написали небольшой React-интерфейс, который компилируется в один бандл и загружается в WebView. Пользователь выделяет код, нажимает шорткат, и выделенный фрагмент летит через postMessage в Extension Host, оттуда — через RouterAPI к модели, и возвращается обратно.

Применение изменений из чата обратно в редактор потребовало использования vscode.WorkspaceEdit. Это транзакционный API: вы собираете набор замен, а затем вызываете vscode.workspace.applyEdit(edit). Если другой процесс или сам разработчик изменил файл за те секунды, пока модель генерировала ответ, транзакция безопасно откатится. Это спасает от повреждения исходников.

Суровая реальность

Мы не создали убийцу Copilot. Мы создали узкоспециализированный инструмент, который требует постоянной поддержки.

Сетевые задержки бесят: если ответ идет дольше 800 миллисекунд, программист уже успевает написать строку сам, и внезапно всплывающая подсказка только сбивает фокус. Парсер AST требует регулярной доработки под новые фичи TypeScript. Поддержка собственного расширения — это полноценный внутренний продукт со своим бэклогом и багами.

Но когда ассистент выдает 40 строк идеального кода, использующего наши внутренние утилиты, правильные DTO и корпоративный логгер с первой попытки — команда экономит часы нудной работы. Иллюзия того, что чужой продукт всегда лучше, исчезает. Вы получаете ровно то, что построили сами, со всеми недостатками и колоссальным преимуществом полного контроля над контекстом.

Теги

Ещё по теме