VS Code: архитектура, расширения, отладка, remote и devcontainers
VS Code — редактор, который выиграл рынок. По ежегодным опросам Stack Overflow им пользуется порядка 70–75 % профессиональных разработчиков — доля, которой не было ни у одного инструмента за всю историю индустрии. Само по себе это не аргумент в его пользу: популярность измеряет распространение, а не качество. Но это повод разобраться, почему так вышло и где проходят настоящие границы применимости.
Короткий ответ: VS Code угадал уровень абстракции. Он не пытается быть IDE для конкретного языка и не пытается быть чистым текстовым редактором. Он — оболочка вокруг протоколов: LSP отвечает за язык, DAP за отладку, а сам редактор занимается текстом, UI и склейкой. Всё остальное — следствие. Разберём его как инженерную систему; контекст про слои редактора и роль LSP — в обзоре трека.
Часть 1. Архитектура
Откуда взялась многопроцессность
Предшественником VS Code по духу был Atom от GitHub — тоже Electron, тоже расширяемость через JavaScript. Atom проиграл по одной ключевой причине: пакеты в нём исполнялись в том же процессе, что и интерфейс. Любое расширение с синхронным циклом или тяжёлым regex подвешивало курсор. Пользователь видел «редактор тормозит», а виноват был плагин, поставленный три недели назад и забытый.
VS Code с самого начала сделал противоположный выбор: расширения живут в отдельном процессе, Extension Host, и общаются с интерфейсом только асинхронными сообщениями. Это стоило удобства авторам расширений — API целиком построен на промисах, синхронно дёрнуть UI невозможно — но купило главное свойство: чужой код не может заблокировать ввод текста.
Отсюда практическое следствие: тормоза бывают трёх разных сортов, и лечатся они по-разному.
| Симптом | Где искать | Чем диагностировать |
|---|---|---|
| Лаг при наборе, дёрганый скролл | renderer: декорации, огромный файл, minimap | отключить minimap и editor.renderWhitespace |
| Автодополнение приходит через секунду | extension host или языковой сервер | Developer: Show Running Extensions |
| Старт окна 15+ секунд | активация расширений | Developer: Startup Performance, Help: Start Extension Bisect |
| «Висит» при сохранении | форматтер, ФС-watcher, антивирус | files.watcherExclude, точечный formatOnSave |
Extension Bisect — недооценённая команда: бинарный поиск по установленным расширениям
(отключает половину и спрашивает «стало лучше?»). За 4–5 итераций находит виновника из сорока.
Monaco и модель текста
Редакторская часть — Monaco — доступна отдельной
библиотекой; на ней сделаны и веб-версия VS Code, и редакторы внутри чужих продуктов. Три её
решения определяют поведение. Виртуализация: в DOM живут только видимые строки плюс запас,
поэтому файл на 200 тысяч строк открывается приемлемо — но языковые фичи в нём отключаются по
порогам вроде editor.maxTokenizationLineLength. Двухслойная подсветка: быстрая
приблизительная по TextMate-грамматикам плюс точная семантическая от языкового сервера — вот
почему цвет иногда «доуточняется» через полсекунды. Асинхронность всего: автодополнение,
hover, подсказки типов — запросы за границу процесса, поэтому VS Code принципиально не может
гарантировать мгновенную подсказку, и весь UI спроектирован так, чтобы её отсутствие не ломало
работу.
Сравните с подходом JetBrains, где семантическая модель живёт в том же процессе, что и UI, и доступна синхронно. Это даёт другие возможности рефакторинга и другую цену — подробно в следующей статье.
Ленивая активация: главный механизм производительности
Установленные расширения по умолчанию не загружены. Каждое объявляет условия активации, и VS Code исполняет его код только когда условие выполнилось.
но не их код"] B --> C{"Есть
onStartupFinished?"} C -->|да| D["В очередь после отрисовки UI"] C -->|нет| E["Не грузим ничего"] D --> F["Окно отрисовано, ввод работает"] E --> F F --> G["Открыт main.go"] --> H["Активируем расширение
с onLanguage:go"] H --> I["Оно поднимает процесс gopls"] --> J["Индексация, затем автодополнение
и переход к определению"] F --> K["Нажат F5"] --> L["onDebugResolve поднимает
debug adapter"] style F fill:#2f6a5e,color:#fff style J fill:#31506e,color:#fff style L fill:#7a3b4b,color:#fff
Значимые на практике условия из справочника activation events:
| Событие | Когда срабатывает | Комментарий |
|---|---|---|
onLanguage:python |
открыт файл на Python | правильный выбор для языковых расширений |
workspaceContains:**/go.mod |
в проекте есть такой файл | активация «по признаку проекта», очень дёшево |
onCommand:ext.doThing |
вызвана команда | подставляется автоматически для contributes.commands |
onStartupFinished |
после отрисовки окна | приемлемо |
* |
немедленно при старте | красный флаг, тормозит всех |
Оценивая расширение перед установкой, откройте вкладку с манифестом и посмотрите
activationEvents. Звёздочка означает загрузку всегда и во всех окнах — даже если вы правите
YAML, а расширение про Kotlin. Три таких — и старт вырастает на несколько секунд.
Заметьте общий вектор всей истории продукта: LSP отдал наружу языки (2016), DAP — отладчики (2018), Remote Development — файловую систему (2019), спецификация devcontainer — окружение (2022). Ядро осталось маленьким, а экосистема выросла. Это и есть ответ на «почему выиграл именно он».
Часть 2. Расширения
Два способа что-то добавить
API делится на две принципиально разные части, и их постоянно путают. Contribution points —
декларации в package.json: никакого кода, вы пишете JSON, и VS Code добавляет пункт меню, тему,
сниппет, настройку, грамматику. Дёшево, безопасно и не требует активации. Программный
API — модуль vscode, доступный только внутри extension host: window, workspace,
languages, debug, commands; полная поверхность —
в справочнике.
Осмысленный минимум — счётчик TODO в статус-баре. В package.json объявляем
"activationEvents": ["onStartupFinished"], "main" и команду в contributes.commands; вся
остальная логика — здесь:
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
// Элемент статус-бара создаём один раз и переиспользуем
const item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
context.subscriptions.push(item);
const update = async () => {
const pattern = new RegExp(
vscode.workspace.getConfiguration('todoCounter').get<string>('pattern', 'TODO'), 'g');
const files = await vscode.workspace.findFiles(
'**/*.{ts,js,go,py}', '**/{node_modules,dist,.git}/**', 5000);
let total = 0;
for (const uri of files) {
// workspace.fs, а не node:fs — работает и в контейнере, и по SSH, и в браузере
const bytes = await vscode.workspace.fs.readFile(uri);
total += (Buffer.from(bytes).toString('utf8').match(pattern) ?? []).length;
}
item.text = `$(checklist) TODO: ${total}`;
item.show();
};
context.subscriptions.push(
vscode.commands.registerCommand('todoCounter.count', update),
// Пересчитываем при сохранении, а не на каждое нажатие клавиши
vscode.workspace.onDidSaveTextDocument(() => update())
);
update();
}
Три вещи показаны намеренно. context.subscriptions — всё, что создаёт ресурс, обязано быть
туда положено, иначе после десятка Reload Window накопятся висящие слушатели и окно начнёт
тормозить; это самая частая ошибка в самописных расширениях. vscode.workspace.fs вместо
node:fs — прямой fs работает, только когда файлы физически рядом. И никакой работы в
activate() сверх необходимого: всё, что можно отложить, откладывается. Упаковка — через
npx vsce package, получившийся .vsix можно раздавать внутри компании
(code --install-extension file.vsix) без публикации в Marketplace.
Marketplace, Open VSX и лицензии
Здесь начинается неприятная часть, о которой обычно молчат. Официальный VS Code — это проприетарная сборка открытого кода. Репозиторий microsoft/vscode под MIT, но бинарник Microsoft собирает со своим брендингом, телеметрией и лицензией, ограничивающей доступ к Marketplace продуктами Microsoft.
Отсюда: VSCodium — сборка того же кода без телеметрии и брендинга, Marketplace ей недоступен, вместо него Open VSX под управлением Eclipse Foundation. Часть популярных расширений там отсутствует или отстаёт — особенно расширения самой Microsoft: C++, C#, Pylance, Remote-SSH, Dev Containers; их лицензия разрешает использование только в продуктах Microsoft. Вывод прагматика: нужны Pylance или Remote-SSH — берите официальную сборку и принимайте лицензию; критична открытость — берите VSCodium, но заранее проверьте, что ваш язык там нормально поддержан. Для компаний есть и юридический нюанс: массовое обращение к Marketplace из сторонних сборок нарушает условия использования и при аудите иногда всплывает.
Безопасность: расширение — это чужой код на вашей машине
Расширение исполняется с правами вашего пользователя: может читать ~/.ssh и .env, ходить в
сеть, запускать процессы. Серьёзного аудита Marketplace не проводит; известны случаи
typosquatting — публикации расширений с именами вроде «Prettier — Code formatter» от постороннего
издателя.
Что реально помогает: Workspace Trust (security.workspace.trust.enabled, включён по
умолчанию) — открытие чужого репозитория в ограниченном режиме блокирует задачи и большинство
расширений; не отключайте его «чтобы не спрашивал», это единственная преграда между git clone от
незнакомца и исполнением кода. Профили — отдельный набор расширений под контекст; профиль
«аудит чужого кода» с тремя расширениями это хорошая гигиена. И банальное: проверять издателя,
число установок и дату обновления.
Рабочая эмпирика по количеству — 10–15 расширений на профиль: два-три языковых, одно про Git,
одно про тесты, форматтер, остальное вкусовщина. Всё, чем не пользовались месяц, удаляется. Каждое
лишнее — память, время старта и риск конфликта форматтеров (классика: Prettier и встроенный
форматтер спорят за editor.defaultFormatter, и файл форматируется по-разному в зависимости от
того, кто выиграл гонку).
Часть 3. Конфигурация
Иерархия настроек
Настройки разрешаются по слоям, каждый следующий перекрывает предыдущий:
default → user → remote (конкретный удалённый хост)
→ workspace (.vscode/settings.json) → folder (multi-root) → language-specific
Порядок объясняет половину «загадочного поведения». «Почему у меня табы, хотя я поставил
пробелы?» — скорее всего, в репозитории лежит .vscode/settings.json.
Рабочий settings.json
Не «конфиг мечты», а база, которая имеет смысл почти всем. Комментарии разрешены — файл читается как JSONC.
{
"editor.fontFamily": "JetBrains Mono, Menlo, Consolas, monospace",
"editor.rulers": [88, 120], // визуальная граница длины строки
"editor.minimap.enabled": false, // экономит место и такты renderer
"editor.stickyScroll.enabled": true, // сигнатура функции прилипает к верху — очень полезно
"editor.linkedEditing": true, // переименование парного HTML-тега
"editor.formatOnSave": true,
"editor.formatOnPaste": false, // почти всегда мешает
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.fixAll.eslint": "explicit"
},
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"files.eol": "\n",
// watcherExclude — прямое лекарство от 100% CPU на больших проектах
"files.watcherExclude": {
"**/node_modules/**": true, "**/.git/objects/**": true,
"**/target/**": true, "**/dist/**": true
},
"search.exclude": { "**/node_modules": true, "**/*.lock": true, "**/dist": true },
// Языковые переопределения — самый недоиспользуемый механизм в конфиге
"[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.tabSize": 4 },
"[go]": { "editor.defaultFormatter": "golang.go", "editor.insertSpaces": false },
"[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.tabSize": 2 },
"[markdown]": { "editor.wordWrap": "on" },
"terminal.integrated.scrollback": 20000,
"git.autofetch": true,
"diffEditor.ignoreTrimWhitespace": false, // иначе прячет реальные различия
"telemetry.telemetryLevel": "error",
"workbench.editor.enablePreview": false, // файлы открываются «насовсем», а не курсивом
"window.autoDetectColorScheme": true
}
Два пункта заслуживают отдельного слова. files.watcherExclude — самое частое лекарство от
«VS Code жрёт процессор»: на проектах с крупным node_modules или target наблюдение за ФС
порождает десятки тысяч дескрипторов, а на Linux можно упереться в
fs.inotify.max_user_watches. workbench.editor.enablePreview: false — мелочь, меняющая
ощущение от работы: по умолчанию файл, открытый одним кликом, показывается курсивом и заменяется
следующим, так что десять переходов подряд оставляют одну вкладку. Кому-то это нравится, кого-то
бесит; знать про переключатель полезно всем.
Клавиши и when-контексты
keybindings.json — не просто список сочетаний: каждое привязывается к контексту, и это
превращает клавиатуру в модальную систему без модальности.
[
// «Прогнать тесты» — только когда открыт файл на Python
{ "key": "ctrl+k ctrl+t", "command": "workbench.action.tasks.runTask",
"args": "pytest: текущий файл", "when": "editorLangId == python" },
// Escape в терминале возвращает фокус в редактор
{ "key": "escape", "command": "workbench.action.focusActiveEditorGroup",
"when": "terminalFocus" },
// Минус снимает встроенную привязку — единственный способ переопределить без конфликта
{ "key": "ctrl+p", "command": "-workbench.action.quickOpen" },
{ "key": "ctrl+p", "command": "workbench.action.quickOpenPreviousRecentlyUsedEditor" },
// Навигация по диагностике — то, чем пользуешься весь день
{ "key": "alt+j", "command": "editor.action.marker.next", "when": "editorFocus" },
{ "key": "alt+k", "command": "editor.action.marker.prev", "when": "editorFocus" }
]
Приходящим из Vim разумно взять VSCodeVim или neovim-интеграцию плюс "vim.handleKeys" для
точечного возврата отдельных сочетаний обратно VS Code. Что вы при этом теряете и приобретаете —
разобрано в статьях про Vim и
Neovim.
tasks.json: не забывайте, что он есть
Задачи — встроенный запускатор команд с разбором вывода. Главная ценность в problemMatcher:
ошибки компилятора превращаются в кликабельные записи в панели Problems.
{
"version": "2.0.0",
"tasks": [
{ "label": "build", "type": "shell", "command": "go build ./...",
"group": { "kind": "build", "isDefault": true },
"problemMatcher": ["$go"], // встроенный матчер под формат ошибок Go
"presentation": { "reveal": "silent", "panel": "shared" } },
{ "label": "test: watch", "type": "shell", "command": "npx vitest --watch",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"fileLocation": ["relative", "${workspaceFolder}"],
"pattern": {
"regexp": "^\\s*(.*):(\\d+):(\\d+)\\s+-\\s+(error|warning):\\s+(.*)$",
"file": 1, "line": 2, "column": 3, "severity": 4, "message": 5 },
// Без background-секции VS Code считает watch-задачу зависшей
"background": { "activeOnStart": true,
"beginsPattern": "RERUN", "endsPattern": "Waiting for file changes" }
} }
]
}
Часть 4. Отладка
Debug Adapter Protocol
Отладка устроена так же, как языковая поддержка: редактор ничего не знает про конкретный отладчик. Он говорит на DAP — JSON-протоколе поверх stdio, а debug adapter переводит сообщения в команды gdb, delve, debugpy, инспектора Node.js или чего угодно ещё.
Понимание цепочки экономит часы. «Отладчик не останавливается на точке» всегда сводится к одному
из трёх: адаптер не получил setBreakpoints (не тот файл), адаптер не смог сопоставить файл с
тем, что видит рантайм (source maps или пути внутри контейнера), либо рантайм просто не
исполняет этот код.
launch.json на реальных примерах
{
"version": "0.2.0",
"configurations": [
{ // Node/TypeScript через tsx — без отдельного шага сборки
"name": "Node: текущий файл", "type": "node", "request": "launch",
"runtimeExecutable": "npx", "runtimeArgs": ["tsx"], "program": "${file}",
"console": "integratedTerminal", "envFile": "${workspaceFolder}/.env.local",
"skipFiles": ["<node_internals>/**", "**/node_modules/**"] },
{ // Подключение к запущенному процессу — основной режим для докера
"name": "Node: attach к контейнеру", "type": "node", "request": "attach",
"address": "localhost", "port": 9229, "restart": true,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app" }, // ключевая строка: сопоставление путей
{ "name": "Python: pytest текущий файл", "type": "debugpy", "request": "launch",
"module": "pytest", "args": ["${file}", "-vv", "-x"],
"console": "integratedTerminal",
"justMyCode": false }, // иначе не зайти внутрь библиотек
{ "name": "Go: сервис с флагами", "type": "go", "request": "launch", "mode": "auto",
"program": "${workspaceFolder}/cmd/api",
"args": ["--config", "configs/dev.yaml"], "env": { "LOG_LEVEL": "debug" } }
],
"compounds": [
{ "name": "Full stack", "stopAll": true,
"configurations": ["Go: сервис с флагами", "Node: текущий файл"] }
]
}
Про skipFiles и justMyCode стоит сказать отдельно: по умолчанию отладчик прячет внутренности
рантайма и библиотек — удобно, пока вы не отлаживаете именно библиотеку. Тогда
justMyCode: false (Python) или пустой skipFiles (Node) обязательны, иначе шаг внутрь функции
молча проваливается наружу.
Точки останова, которыми не пользуются, а зря
| Тип | Как поставить | Зачем |
|---|---|---|
| Условная | правый клик на точке → Edit Breakpoint → Expression | остановиться на 5000-й итерации, а не жать F5 пять тысяч раз |
| По счётчику | там же, Hit Count | «останови на десятом вызове» |
| Logpoint | правый клик → Logpoint, текст с {выражение} |
печать в консоль без правки кода |
| По исключению | панель Breakpoints, галочки | поймать место броска, а не место, где его поймали |
| По данным | зависит от адаптера: есть в C++, C# | остановиться при изменении переменной |
Logpoints стоит попробовать всем, кто отлаживает логами: тот же поток печати, но без правки исходников — а значит, без риска закоммитить отладочный вывод и без пересборки.
Часть 5. Remote Development
Самая недооценённая функция VS Code. Идея: разделить редактор на две половины и растащить их по разным машинам. Локально остаётся то, что рисует пиксели, удалённо — всё, что трогает файлы.
Это не «редактирование по сети» вроде SSHFS или rmate. Языковой сервер работает на удалённой
машине, рядом с исходниками и зависимостями, поэтому автодополнение и переход к определению
остаются мгновенными независимо от канала: по сети едут не файлы, а результаты запросов — на
порядок меньше трафика. Реализаций одного механизма четыре: Remote-SSH (мощный сервер,
GPU-машина, доступ во внутреннюю сеть), Dev Containers (воспроизводимое окружение проекта),
WSL (Windows-машина с Linux-тулчейном) и Tunnels / vscode.dev (доступ к своей машине из
браузера без проброса портов).
extensionKind: главный источник путаницы
Каждое расширение объявляет сторону: ["ui"] — только локально, ["workspace"] — только
удалённо, оба значения — предпочитает первое, умеет второе. Если что-то «сломалось при переходе на
remote», в девяти случаях из десяти расширение оказалось не на той стороне. Лечится в
пользовательских настройках:
{
"remote.extensionKind": {
"ms-azuretools.vscode-docker": ["ui"], // докер стоит на моей машине — расширение локально
"esbenp.prettier-vscode": ["workspace"] // форматтер — обязательно рядом с кодом
}
}
Практика Remote-SSH
Host devbox
HostName 10.0.4.17
User marat
ForwardAgent yes
# Мультиплексирование: второе и последующие подключения мгновенны
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 10m
# Не отваливаться при простое — VS Code этого очень не любит
ServerAliveInterval 30
ServerAliveCountMax 6
При первом подключении на сервер выкачивается VS Code Server — сотни мегабайт в $HOME, что
на машинах с квотой на домашний каталог оказывается сюрпризом (управляется через
remote.SSH.serverInstallPath). Сервер требует glibc не ниже определённой версии: на старых
CentOS/RHEL свежий клиент просто не подключится. Зато проброс портов автоматический — подняли
сервер на :3000 внутри, VS Code заметит и предложит открыть localhost:3000 локально; это одна
из самых удобных мелочей во всём продукте. Ключи при ForwardAgent yes остаются локально — не
копируйте приватные ключи на dev-сервер.
Экономика: remote окупается, когда сборка тяжёлая, а ноутбук — нет. Компиляция монорепозитория на 32-ядерной машине против MacBook Air — это разница между двумя минутами и двадцатью, плюс исходники и доступы физически не покидают контур. Не окупается при плохом канале: задержка выше ~150 мс делает работу с терминалом некомфортной, а обрыв связи означает потерю несохранённого состояния.
Часть 6. Dev Containers
«У меня работает» — не шутка, а системная стоимость. Новый разработчик тратит от полудня до трёх дней на настройку окружения; версии Python расходятся между машинами; кто-то поставил Node 18, а CI собирает на 22. Dev Containers переносят описание окружения в репозиторий, где ему и место. Спецификация containers.dev с 2022 года открыта и не привязана к VS Code — её поддерживают GitHub Codespaces, JetBrains Gateway и отдельный CLI.
// .devcontainer/devcontainer.json
{
"name": "api-service",
"dockerComposeFile": "../docker-compose.dev.yml",
"service": "app",
"workspaceFolder": "/workspace",
// Features — переиспользуемые куски установки вместо копипасты в Dockerfile
"features": {
"ghcr.io/devcontainers/features/go:1": { "version": "1.23" },
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"customizations": {
"vscode": {
// Эти расширения ставятся ВНУТРЬ контейнера автоматически
"extensions": ["golang.go", "ms-azuretools.vscode-docker", "eamodio.gitlens"],
"settings": { "go.toolsManagement.checkForUpdates": "off" }
}
},
"forwardPorts": [8080, 5432],
"portsAttributes": {
"8080": { "label": "API", "onAutoForward": "notify" },
"5432": { "label": "Postgres", "onAutoForward": "silent" }
},
// Четыре разные фазы жизненного цикла — их постоянно путают
"onCreateCommand": "go env -w GOFLAGS=-mod=mod",
"updateContentCommand": "go mod download",
"postCreateCommand": "make dev-setup",
"postStartCommand": "git config --global --add safe.directory /workspace",
"mounts": ["source=${localEnv:HOME}/.gitconfig,target=/home/vscode/.gitconfig,type=bind"],
"remoteUser": "vscode",
"containerEnv": { "TZ": "Europe/Moscow" }
}
Различие фаз — источник большинства неработающих конфигов. Правило простое: всё, что зависит от
файлов проекта, идёт в postCreateCommand; всё, что нужно при каждом запуске, — в
postStartCommand.
Compose и производительность
Одного контейнера почти никогда не хватает — нужны БД, кэш, очередь.
# docker-compose.dev.yml
services:
app:
build: { context: ., dockerfile: .devcontainer/Dockerfile }
volumes:
- ..:/workspace:cached
# Кэши и зависимости — в именованные тома, НЕ в bind mount: иначе очень медленно
- go-build-cache:/home/vscode/.cache/go-build
- go-mod-cache:/go/pkg/mod
command: sleep infinity # контейнер просто живёт, работу ведёт VS Code
depends_on: [db, redis]
environment:
DATABASE_URL: postgres://dev:dev@db:5432/app
db:
image: postgres:16-alpine
environment: { POSTGRES_USER: dev, POSTGRES_PASSWORD: dev, POSTGRES_DB: app }
volumes: [pgdata:/var/lib/postgresql/data]
redis:
image: redis:7-alpine
volumes: { pgdata: , go-build-cache: , go-mod-cache: }
command: sleep infinity — обязательная деталь: контейнер разработки не должен запускать
приложение, иначе он завершится при падении процесса и утащит за собой сессию.
На Linux devcontainer работает почти без накладных расходов — bind mount там нативный. На macOS и
Windows файловая система пробрасывается через виртуализацию, и разница драматическая: npm ci в
bind mount идёт в разы дольше, чем в именованном томе. Рецепты по убыванию эффекта: (1) каталоги
зависимостей и кэшей — в именованные тома, как выше; (2) на macOS включить VirtioFS в Docker
Desktop; (3) флаг :cached для исходников; (4) радикальный вариант clone in volume —
репозиторий целиком живёт в томе Docker, максимальная скорость ценой недоступности файлов
локальным инструментам.
CLI и связь с CI
devcontainer CLI позволяет использовать то же описание вне редактора — и это превращает devcontainer из «удобства для IDE» в единый источник правды об окружении:
npm i -g @devcontainers/cli
devcontainer up --workspace-folder .
devcontainer exec --workspace-folder . go test ./... # ровно то окружение, что у разработчика
В GitHub Actions это даёт CI, гарантированно совпадающий с локальной средой — шаг
devcontainers/ci@v0.3 с imageName, cacheFrom и runCmd: make ci. Больше про
воспроизводимость окружений — в треке DevOps.
Часть 7. Честное сравнение
VS Code — не универсально лучший выбор.
| Критерий | VS Code | JetBrains | Neovim |
|---|---|---|---|
| Время до первой строки кода | минуты | десятки минут | часы или дни |
| Тяжёлые рефакторинги в Java/C# | слабо | эталон отрасли | слабо |
| Отладка | хорошая, единообразная через DAP | глубже, особенно JVM и .NET | требует настройки, DAP через плагины |
| Монорепозиторий на 10 млн строк | приемлемо | тяжело, индексация долгая | лучше всех, ничего не индексирует |
| Потребление памяти | 0,5–2 ГБ | 2–8 ГБ | десятки МБ |
| Remote и контейнеры | эталон отрасли | Gateway, слабее и менее стабильно | нативно, через SSH и tmux |
| Веб-версия | полноценная | ограниченная | нет |
| Настройка под себя | JSON, широко, но не глубоко | GUI, глубоко в рамках модели | без границ, ценой времени |
| Цена | бесплатно | 100–800 USD в год | бесплатно |
Где VS Code честно слаб. Крупные Java- и C#-проекты: расширения существуют и работают, но рефакторинг вроде «извлечь интерфейс из класса с сорока использованиями в двенадцати модулях» в JetBrains делается надёжно, а здесь — с оговорками; причина архитектурная, LSP описывает ограниченный набор операций, и сложные семантические преобразования в него ложатся плохо. Очень большие файлы: мегабайтный лог откроется, но с отключёнными фичами. Память под нагрузкой: три окна по три языковых сервера — несколько гигабайт, на машине с 8 ГБ тесно. Единообразие UX: каждое расширение придумывает свои настройки, команды и панели; в JetBrains функциональность интегрирована и предсказуема, здесь вы собираете IDE из деталей разных производителей — и швы видны.
Итоговое сравнение всех инструментов трека — в отдельной статье.
Типичные ошибки
- Ставить расширение «на всякий случай». Ревизия раз в квартал: удалить всё, чем не пользовались.
- Отключать Workspace Trust. Экономит два клика, открывает исполнение произвольного кода при клонировании чужого репозитория.
- Не класть
.vscode/settings.jsonиextensions.jsonв репозиторий. Командные настройки формата и рекомендованных расширений экономят часы споров в ревью. А вотlaunch.jsonс личными путями и.envтуда не надо — им место в.gitignore. - Игнорировать
files.watcherExclude— классическая причина «непонятной» нагрузки на CPU. - Отлаживать
console.log, когда есть logpoints — тот же результат без правки кода. - Bind mount для
node_modulesв devcontainer на macOS — самая частая причина «контейнеры тормозят, вернёмся к локальной установке». - Путать фазы жизненного цикла devcontainer:
npm ciвonCreateCommandне сработает, исходников там ещё может не быть. - Считать remote «редактированием по сети». Непонимание того, что языковой сервер работает
удалённо, порождает неверные ожидания от канала и неправильно расставленный
extensionKind.
Мини-итог
VS Code силён не функциями, а архитектурой: маленькое ядро, вынесенные наружу LSP и DAP,
изолированный extension host, ленивая активация. Extension host в отдельном процессе — причина,
по которой чужой код не подвешивает набор текста; цена — полностью асинхронный API. Расширения
имеют реальную стоимость (старт, память, безопасность), поэтому держите 10–15 штук, смотрите
activationEvents и не отключайте Workspace Trust. Настройки разрешаются слоями
default → user → remote → workspace → folder → language, и этим объясняется половина «загадочного
поведения». Отладка — это DAP: умение читать цепочку setBreakpoints → stopped → stackTrace
превращает «не останавливается на точке» из мистики в диагностику. Remote Development разрезает
редактор по границе «пиксели / файлы» и остаётся лучшей технологией в своём классе, а Dev
Containers переносят окружение в репозиторий и через CLI распространяются на CI — с главным
подводным камнем в виде производительности файловой системы на macOS и Windows. Слабые места
честны и известны: тяжёлые рефакторинги в статически типизированных экосистемах, очень большие
файлы, память.
Источники
- Extension API и Extension Host: advanced topics
- Language Server Protocol и Debug Adapter Protocol — спецификации и списки готовых серверов и адаптеров
- Remote Development — SSH, Containers, WSL, Tunnels
- Development Containers Specification и каталог devcontainers/features
- microsoft/vscode, особенно
Source Code Organization в
вики: там объяснено, как слои
base/platform/editor/workbenchотделены друг от друга - Open VSX Registry — открытая альтернатива Marketplace
- Monaco Editor — редакторское ядро как библиотека
- Stack Overflow Developer Survey — данные по распространённости инструментов
Что дальше
Мы разобрали редактор, который собирается из деталей и говорит с миром через протоколы. Следующая статья — про противоположную философию: среды, которые строят полную семантическую модель кода у себя внутри, и что эта модель позволяет сделать такого, чего не умеет LSP.
JetBrains IDE: индексация, рефакторинги, инспекции, когда они окупаются