Воссоздаём расширение для VS Code

Воссоздаём расширение для VS Code

surf_web

Привет, меня зовут Лиза, Frontend-разработчик в Surf. Лучший способ усвоить что-то новое — применить знания на практике, создавая что-то интересное своими руками. В этой статье мы шаг за шагом воссоздадим популярное расширение для VS Code.

Я выбрала Pomodoro-таймер в качестве примера, потому что он обладает всеми необходимыми функциями для демонстрации процесса разработки расширений.

Что будет в нашем расширении

Наше расширение будет включать в себя:

Два основных экрана:

  • Экран настроек.
  • Экран самого таймера.

Интерактивные элементы:

  • Поля ввода для установки длительности каждого этапа (рабочий интервал, короткий и длинный перерывы).
  • Кнопки управления: старт, пауза, сброс.
  • Визуализация прогресса таймера.

Удобное расположение: расширение будет интегрировано в сайдбар VS Code, как и встроенные панели.

Дополнительные функции:

  • Уведомления — всплывающие сообщения в углу экрана о смене этапов.
  • Звуковые сигналы — аудио-оповещения при переходе между этапами.

Гибкие настройки — возможность задать длительность для нескольких рабочих сессий, коротких перерывов и одного длинного перерыва после завершения цикла.

Визуальная концепция — минималистичный дизайн с четким индикатором прогресса (здесь можно добавить скриншот или более детальное описание интерфейса).


Как это будет работать

Настройка длительности

Пользователь сможет установить:

  • Длительность рабочего интервала (например, 25 минут).
  • Длительность короткого перерыва (например, 5 минут).
  • Длительность длинного перерыва (например, 15 минут, после 4 рабочих циклов).

Автоматическое переключение

Таймер будет автоматически переключаться между этапами (работа, короткий перерыв, длинный перерыв).

Оповещения

VS Code будет уведомлять пользователя о смене этапов с помощью звука и всплывающих нотификаций.


План разработки

Разработка будет разбита на отдельные этапы, каждому из которых будет соответствовать отдельная ветка на GitHub. Названия веток будут соответствовать формату: part_n.

Приступим к работе.


Ветка part_1

Для начала работы над расширением мы воспользуемся генератором yo code. На данном этапе мы сосредоточимся на исследовании функциональности, поэтому отбросим бандлинг.

Ответим на вопросы генератора следующим образом:

  • What type of extension do you want to create? — New Extension (TypeScript)
  • Which bundler to use? — unbundled

Размещение расширения — сайдбар VS Code

Наше приложение будет располагаться в сайдбаре VS Code, что обеспечит удобный доступ к функциям Pomodoro-таймера. Мы будем использовать Primary Sidebar для максимальной интеграции в пользовательский интерфейс.

  • Для размещения нашего пользовательского интерфейса в сайдбаре мы будем использовать Webview. Оно позволяет отображать веб-контент (HTML, CSS, JavaScript) внутри VS Code, что дает нам полную гибкость в создании дизайна и интерактивных элементов.
  • Рекомендую ознакомиться с официальным руководством по дизайну сайдбаров, чтобы наше расширение выглядело и ощущалось как нативное.

Интеграция Webview в сайдбар

Для успешного размещения Webview в сайдбаре нам потребуется создать специальный класс для управления его состоянием и содержимым.

Мы создадим класс SidebarProvider, который будет отвечать за прокидывание Webview в сайдбар. Файл для этого класса будет располагаться рядом с extension.ts.

import * as vscode from 'vscode';

export class SidebarProvider implements vscode.WebviewViewProvider {
    private _view?: vscode.WebviewView;
    // Здесь будет храниться ссылка на вебвью
    // Этот метод вызывается, когда VS Code создает вебвью
    public resolveWebviewView(webviewView: vscode.WebviewView) {
        this._view = webviewView; // Сохраняем ссылку на вебвью
        webviewView.webview.html = this._getHtmlForWebview();
    //Загружаем HTML
    }

    // Генерируем HTML, который будет отображаться в вебвью (взято с официального источника для примера)
    private _getHtmlForWebview() {
    return `<!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Cat Coding</title>
    </head>
    <body>
        <img src="https://media.giphy.com/media/JIX9t2ozdTN9s/giphy.gif" width="300" />
    </body>
    </html>`;
    }
}

Класс SidebarProvider является ключевым элементом для размещения нашего интерфейса в сайдбаре VS Code. Он реализует интерфейс vscode.WebviewViewProvider, предоставляемый API VS Code, что позволяет VS Code создавать и управлять нашим Webview.

Давайте разберем основные части этого класса:

_view?: vscode.WebviewView — эта приватная переменная будет хранить ссылку на текущий экземпляр WebviewView. Это важно, если нам понадобится взаимодействовать с Webview из других частей расширения (например, отправлять ему сообщения или обновлять его содержимое).

resolveWebviewView(webviewView: vscode.WebviewView) — это главный метод класса SidebarProvider. VS Code вызывает его, когда:

  • Пользователь впервые открывает сайдбар, содержащий наше расширение.
  • VS Code готов отрисовать Webview в сайдбаре.

Внутри этого метода мы сохраняем переданную ссылку на webviewView в нашей приватной переменной _view, а затем присваиваем HTML-содержимое свойству webviewView.webview.html, вызывая метод this._getHtmlForWebview().

_getHtmlForWebview() Этот приватный метод отвечает за генерацию HTML-кода, который будет отображаться внутри Webview. В текущем примере мы возвращаем простую HTML-страницу, содержащую GIF-анимацию «кота-программиста». Это базовый пример, который демонстрирует, как можно встроить любой веб-контент в Webview. В дальнейшем мы заменим его на полноценный интерфейс Pomodoro-таймера.

В данном случае это простая страница с гифкой работающего кота.

import * as vscode from 'vscode'; // Импортируем API VS Code
const { SidebarProvider } = require('./SidebarProvider');
// Подключаем класс SidebarProvider
export function activate(context: vscode.ExtensionContext) {
    // Создаём экземпляр SidebarProvider и передаём ему
    // URI расширения (чтобы вебвью могло загружать локальные файлы)
    const sidebarProvider = new SidebarProvider(context.extensionUri);
    // Регистрируем вебвью в сайдбаре:
    // 'myExtension-sidebar' — это ID, который должен совпадать с package.json
          context.subscriptions.push(
          vscode.window.registerWebviewViewProvider('myExtension-sidebar', sidebarProvider)
    );
}

При запуске расширения VS Code вызывает функцию activate. Внутри неё создаётся SidebarProvider, который будет управлять содержимым сайдбара.

Далее, чтобы всё заработало, нам нужно отредактировать секцию contributes в package.json. Сразу же добавим иконку для activityBar. Больше информации по иконкам тут.

"contributes": {
    "viewsContainers": {
    "activitybar": [
        {
              "id": "myExtension-sidebar-view",
              "title": "CatoDoro",
              "icon": "resources/cat.png"
        }
    ]
    },
    "views": {
    "myExtension-sidebar-view": [
        {
              "type": "webview",
              "id": "myExtension-sidebar",
              "name": "CatoDoro",
              "icon": "resources/cat.png"
        }
    ]
    },
    "files": [
    "out",
    "resources"
    ],
    "commands": []
}

viewsContainers — этот раздел добавляет новую вкладку в Activity Bar (левую панель VS Code):

  • id — уникальный идентификатор для нашего контейнера.
  • title — текст, который появится при наведении курсора на иконку.
  • icon — путь к иконке (в данном случае, файл находится в resources/cat.png).

views — здесь определяется содержимое созданной вкладки:

  • myExtension-sidebar-view — это ссылка на контейнер, который мы определили в viewsContainers.
  • type: "webview" — указывает, что содержимое будет представлено как вебвью.
  • name — название, которое будет отображаться для вкладки внутри самого сайдбара.

Теперь пришло время запустить нашу первую часть. Откройте файл extension.ts в редакторе и запустите его в режиме отладки, нажав F5.

Ура! В сайдбаре уже активно «клацает» котик.

Не будем уступать ему в стремлении и перейдём ко второй части.


Ветка part_2

Пока оставим в сайдбаре заглушку с картинкой, а сами займёмся разработкой Webview.

Как мы будем делать сборку? (Возможно, это можно будет обернуть в монорепозиторий, но сейчас это факультативно).

Важно отметить, что нам удобно писать код компонентами и управлять состоянием. Однако для Node.js нам потребуется чистый JavaScript после сборки.

Выбранный стек технологий:

  • Preact + Vite — для быстрого старта (это лёгкий аналог React с похожим синтаксисом).
  • Styled Components — чтобы сосредоточиться на логике, а не на многочисленных CSS-файлах.
  • Day.js — для удобной работы со временем (таймеры, форматирование).

Это решение «в лоб» – позднее можно будет перейти на React + CSS-модули + date-fns без необходимости переписывать основную логику.

Главное сейчас — создать работающий прототип, а не идеальный код.

Webview будет размещено в отдельной папке под названием -webApp. Все зависимости, исходные файлы и результаты сборки будут храниться внутри этой папки.

Начнём с установки зависимостей. Preact + Vite проще установить через команду:

npm init preact

В рутовый tsconfig добавим exclude

"exclude": [
    "webApp",
    "node_modules"
    ]

Поправим vite.config.js

import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';

export default defineConfig({
    plugins: [preact()],
    root: 'src',
    build: {
        // Папка для собранных файлов (относительно корня проекта)
        outDir: '../dist',
        manifest: true,
        rollupOptions: {
        input: 'src/index.html',
        output: {
        entryFileNames: `webview-build/[name].js`,
        chunkFileNames: `webview-build/[name].js`,
        assetFileNames: `webview-build/[name].[ext]`,
        },
        },
    },
    base: '',
});

При работе с расширением нас будет интересовать только билд Webview, с UI можно будет взаимодействовать в режиме разработки.

Далее нам необходимо предоставить расширению доступ к нашему Webview. Попрощаемся с картинкой котика.

И вернёмся в extension.ts. Мы «прокинем» в sidebarProvider путь к папке расширения (context.extensionUri).

const sidebarProvider = new SidebarProvider(context.extensionUri);

Этот параметр (context.extensionUri) нужен для нескольких целей:

  1. Доступ к файлам расширения
    Он позволяет вебвью загружать локальные ресурсы (такие как HTML, CSS, JavaScript файлы, а также иконки) непосредственно из папки вашего расширения.
  2. Безопасность
    VS Code требует явного указания путей к этим ресурсам через метод asWebviewUri. Это сделано для повышения безопасности, предотвращая несанкционированный доступ к посторонним файлам вне директории расширения.
  3. Генерация правильных URI
    Этот параметр помогает преобразовать относительные пути (например, media/style.css) в полноценные абсолютные URI, которые корректно понимаются и обрабатываются внутри Webview.

В самом Webview потребуется внести немного больше изменений.

export class SidebarProvider implements vscode.WebviewViewProvider {
    private _view?: vscode.WebviewView;
    // Конструктор получает URI папки расширения
    constructor(private readonly _extensionUri: vscode.Uri) {}
    public resolveWebviewView(webviewView: vscode.WebviewView) {
    this._view = webviewView;
    // Настраиваем опции вебвью:
    webviewView.webview.options = {
        enableScripts: true, // Разрешаем выполнение JavaScript
        localResourceRoots: [ // Указываем, откуда можно загружать ресурсы
            this._extensionUri // Только из папки расширения
        ],
    };
    // Устанавливаем HTML-контент для вебвью
    webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
    }
}

// Метод для получения HTML-контента
private _getHtmlForWebview(webview: vscode.Webview) {
    // Формируем путь к HTML-файлу в папке расширения
    const htmlPath = path.join(
    this._extensionUri.fsPath,
    'webApp',
    'dist',
    'index.html'
    );
    // Проверка существования файла
    if (!fs.existsSync(htmlPath)) {
    throw new Error(`HTML file not found at ${htmlPath}`);
    }
    // Читаем содержимое HTML-файла
    let html = fs.readFileSync(htmlPath, 'utf8');
    // Модифицируем HTML, добавляя базовый URL для корректной загрузки
ресурсов
    html = html.replace(
    '</head>',
    `<base href="${webview.asWebviewUri(
    vscode.Uri.joinPath(this._extensionUri, 'webApp', 'dist')
    )}"/>
    </head>`
    );
    return html; // Возвращаем модифицированный HTML
}

Давай разберём ключевые моменты:

Локальные ресурсы

  • localResourceRoots — это важная опция, которая ограничивает доступ Webview только к файлам, расположенным в папке вашего расширения. Это мера безопасности.
  • asWebviewUri — этот метод преобразует обычные файловые пути в специальные URI-адреса, которые понимает и корректно обрабатывает среда VS Code для вебвью.

Безопасность

  • enableScripts: true — эта настройка явно разрешает выполнение JavaScript-кода внутри Webview. Без неё скрипты не будут работать.
  • Все ресурсы загружаются только из разрешенных путей, что минимизирует риски безопасности.

Работа с HTML

  • Базовый URL (<base href>) — крайне важен для корректной работы относительных путей к CSS и JavaScript файлам внутри вашего вебвью. Без него браузер не будет знать, откуда загружать эти ресурсы.
  • HTML-файл вебвью читается напрямую из файловой системы вашего расширения.

Теперь пришло время запустить билд, а затем расширение в режиме отладки. Ура! У нас получилось добавить шаблонную страничку в сайдбар.

Ещё больше полезной информации ищи в нашем Telegram-канале.


Report Page