VS Code сам по себе не нуждается в представлении, однако многие программисты, привыкшие в нём разрабатывать, упускают одну очень полезную вещь. Благодаря встроенным возможностям по разработке расширений можно легко автоматизировать многие рутинные задачи — например, те, что выполняются в командной строке.

В этой статье, второй в серии материалов о нестандартных возможностях VS Code, разберём инструменты для создания интерактивных расширений, которые я применяю в работе над решениями productivity suite платформы МойОфис. Под катом мы рассмотрим веб-панели и их разновидность – веб-представления, а также другие стандартные средства VS Code API, такие, например, как элементы строки состояния (кнопки и сообщения).


Напомню, в прошлый раз я рассказывал о том, что VS Code даёт возможность создавать расширения прямо внутри себя. Мы познакомились с понятием команды в VS Code и научились использовать древовидные представления, а также изучили создание расширения от каркаса до установочного пакета и разработали простое расширение, которое, используя команды conan, показывает структуру (иерархию зависимостей) пакетов в проекте.

Следующий шаг – выстраивание взаимодействия между пользователем и расширением. В этой статье мы изучим, что предлагает VS Code API для повышения интерактивности расширений: простые элементы взаимодействия из пространства имён vscode.window VS Code API, веб-панели (WebviewPanel) и их разновидность – веб-представления (WebviewView) и меню в них, элементы строки состояния (StatusBarItem) и пользовательские панели вывода.

Веб-панели и веб-представления

Начнём обзор с самого технически сложного (на самом деле сложным его можно назвать лишь в сравнении с другими) способа организовать взаимодействие расширения с пользователем — панели с произвольным наполнением HTML/CSS или веб-панели, в которых можно использовать все привычные элементы ввода (например, input, button, select) под управлением JavaScript.

У Майкрософт есть подробная документация на эту тему, где на относительно простом, но, на мой взгляд, несколько отвлечённом от практики примере, рассматривается создание и использование панели в области редактирования. Создать её можно, например, так:

const panel = vscode.window.createWebviewPanel(
  NewProjectView.viewType,
  "Create New Project",
  column || vscode.ViewColumn.One,
  getWebviewOptions(extensionUri)
);

Больше подробностей можно найти в документации по ссылке выше. Мы же рассмотрим более интересный и сложный случай.

Но прежде чем начать, давайте посмотрим, где в интерфейсе VS Code можно будет найти наше расширение. В прошлой статье мы уже упоминали о самой левой панели, которая содержит различные иконки. Выбирая их, мы активируем тот или иной инструмент, который будет показываться в области между этой панелью и редакторами: например, мы можем выбрать показ дерева файлов проекта (Explorer, иконка «две страницы»), поиск (иконка в виде лупы), систему контроля версий, управление расширениями или другие возможности.

«Другими возможностями» в нашем случае будет иконка, обозначающая расширение, которое мы разрабатываем. На картинке ниже мы видим изображение лампы накаливания. Оно подсвечено, так как мы выбрали наше расширения как текущую активность.

Это значит, что наше расширение будет доступно как одна из «активностей» между которыми можно переключаться.

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

Веб-панели... следует использовать с осторожностью и только когда другие стандартные средства VS Code API недостаточны. Они используют большое количество ресурсов и работают в изоляции от других частей расширения. Плохо спроектированная веб-панель может легко испортить впечатление при использовании.

И действительно, VS Code может легко перестать отвечать из-за неправильной работы расширения. Правда, это может произойти и без использования веб-представлений (см. ниже про пользовательские панели вывода).

Веб-панели: начинаем знакомство

Начнём с простого — создадим вкладку с гипертекстовой панелью, которая не содержит интерактивных элементов, но, тем не менее, будет способна динамически менять своё содержимое.

Добавим в package.json в раздел contributes подраздела views такое описание:

{
  "id": "mo.sysInfo",
  "type": "webview",
  "initialSize": 390,
  "name": "System Info"
}

Начальный размер (initialSize) задавать не обязательно, но зачастую желательно. Особенно, если у вас в расширении много панелей в этой области.

Затем нам нужно создать класс представления этой панели, реализовав его интерфейс vscode.WebviewViewProvider. Реализация сводится к одному методу, который в нашем случае будет выглядеть так:

  public async resolveWebviewView(
        webviewView: vscode.WebviewView,
        context: vscode.WebviewViewResolveContext,
        _token: vscode.CancellationToken,
    ) {
        this._view = webviewView;
        webviewView.webview.options = getWebviewOptions(this._extensionUri);
        let message = '<p>Preparing system info...';
        webviewView.webview.html = getSimpleHtmlPage(message);
    }

...но на самом деле, нам потребуется добавить в этот класс кое-что ещё:

export class CheckInfoView implements vscode.WebviewViewProvider {
    public static readonly viewType = "mo.sysInfo";
    public static message: string = '';
    private _view?: vscode.WebviewView;
    constructor(
        private readonly _extensionUri: vscode.Uri,
    ) { }

Теперь разберём приведённые выше куски кода подробнее. В методе resolveWebviewView нам нужно сделать небольшую дополнительную настройку представления через установку свойства webviewView.webview.options.

Код функции для настройки панели может выглядеть так:

export function getWebviewOptions(extensionUri: vscode.Uri): vscode.WebviewOptions {
  return {
    // Enable javascript in the webview
    enableScripts: true,

    // And restrict the webview to only loading content from our extension's `media` directory.
    localResourceRoots: [vscode.Uri.joinPath(extensionUri, 'media')]
  };
}

Комментарии внутри тела функции должны быть понятны. Но на всякий случай поясню, что здесь мы разрешаем использовать javascript (в данном примере это делать необязательно и, скорее всего, не нужно и в дальнейшем, если только вам это не требуется для функционирования панели) и ограничиваем доступ к ресурсам на диске до подкаталога media. Он будет находиться внутри каталога, где установлено наше расширение.

В случае с Линуксом это будет, как правило, $HOME/.vscode/extensions/<название расширения>. Иногда, в процессе разработки и отладки, бывает полезно туда заглянуть, но в большинстве случаев делать это не обязательно.

Теперь мы создаём наполнение панели, которое в нашем случае пока сводится к простейшей разметке с использованием html. Для начала выведем простое текстовое сообщение о подготовке информации о системе. Для этого нужно присвоить его в виде произвольного размеченного гипертекста (html) к webviewView.webview.html.

Теперь для активации панели при запуске расширения нужно добавить следующий фрагмент кода:

const sysInfoViewProvider = new CheckInfoView(context.extensionUri);
context.subscriptions.push(
  vscode.window.registerWebviewViewProvider(CheckInfoView.viewType, 
                                            sysInfoViewProvider));

Перезапустим приложение и убедимся, что у нашего расширения появилась новая вкладка SYSTEM INFO.

Пока мы видим только текст нашего сообщения. Теперь мы можем попробовать изменить содержимое панели динамически, присвоив новое значение свойству webviewView.webview.html по мере поступления информации.

Для этого добавим в метод resolveWebviewView код для сбора информации о системе. Например, если мы пишем расширение для работы с conan, нам следует сначала проверить его доступность в системе. Мы можем написать, например, такой метод:

public async runCheck(): Promise<boolean> {
    if (this._view === undefined)
    {
        return false;
    }
    let conanCheck = checkForConan();
    if (conanCheck === undefined || conanCheck.startsWith('error:')) {
        const message = 'Conan not found on your machine"';
        this._view.webview.html = getSimpleHtmlPage("<p color=red>" + message + "<p color=red>Cannot proceed without conan");
        return false;
    } else {
        const message = `<p>${conanCheck} successfully found on your machine.`;
        this._view.webview.html = getSimpleHtmlPage(message);
    }
    return true;
}

После этого можно добавить его вызов в resolveWebviewView:

CheckInfoView._checkOk = await this.runCheck();

При этом не забываем пометить resolveWebviewView как асинхронный (async).

Перезапускаем наше расширение (F5 – с отладкой или Ctrl-F5 – без отладки) и убеждаемся, что во вкладке у нас появляется сообщение о присутствии или отсутствии нужного нам модуля или пакета. Если процесс будет долгим, вы даже сможете заметить, как изменяется наполнение вкладки.

Вот и всё! Не так уж и сложно, правда?

Но, как вы понимаете, такой простой вариант реализации веб-панели подойдет лишь для ограниченного количества применений. Чаще всего нам требуются панели с полями, которые можно менять, и значения, из которых можно сохранять в файлы, читать при перезапуске и, самое главное, использовать для настройки работы нашего расширения.

Веб-панели: следующий шаг – работаем с полями ввода

Создадим ещё один класс панели с полями, которые можно будет редактировать. Он также должен быть создан с реализацией интерфейса vscode.WebviewViewProvider, но выглядеть будет уже посложнее.

Как было сказано выше, веб-панель (или веб-представление) — это страница HTML, в том числе с элементами ввода, знакомыми всем по работе с формами в HTML (input, select, button и т.п.), но с некоторыми добавлениями, которые не всегда встретишь на обычной странице. Общение между кодом расширения и элементами ввода панели тоже происходит не самым привычным образом.

   private static _getHtmlForWebview(webview: vscode.Webview, extensionUri: vscode.Uri) {
    // Local path to main script run in the webview
    const scriptPathOnDisk = vscode.Uri.joinPath(extensionUri, 'media', 'main.js');

    // And the uri we use to load this script in the webview
    const scriptUri = webview.asWebviewUri(scriptPathOnDisk);

    // Do the same for the stylesheet.
    const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'media', 'styles.css'));
    let profiles = getOptionsString(ProjectSettingsView.conanProfiles);

    const profilesStr = getConanProfileHtml(profiles);
    const nonce = getNonce();

    return `<!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <!--

В фрагменте выше показан пример функции, где мы сначала создаём несколько переменных, которые будут использоваться внутри HTML-разметки панели. Например, в пути к файлам на диске (с помощью метода asWebviewUri) и другие.

Теперь переходим к формированию собственно HTML-разметки панели:

    return `<!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <!--
            Use a content security policy to only allow loading styles from our extension directory,
            and only allow scripts that have a specific nonce.
            (See the 'webview-sample' extension sample for img-src content security policy examples)
        -->
        <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; script-src 'nonce-${nonce}';">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link href="${styleUri}" rel="stylesheet">
        <title>Preset settings</title>
        </head>
        <body>
        <table>
        <!-- tr><td>
        <label for="build_type">Build Type:</label>
        <td>
        <select id="build_type" name="build_type" size=1>
        <option>debug</option>
        <option>release</option-->
        <tr><td>

Это был заголовок и начало тела страницы, где мы видим использование значений различных переменных, созданных ранее (например, nonce, scriptUri, styleUri). Эти переменные нужны, чтобы безопасно получить доступ к ресурсам.

Остаток будет состоять из таблицы с полями. Можно, конечно, попробовать сделать что-то более хитрое и интересное, но это уже на усмотрение создателей.

Нам осталось рассмотреть, как может выглядеть обслуживание полей в JavaScript. Фрагмент ниже добавляет использование файла, который содержит нужный для этого код.

  </table>
        <script nonce="${nonce}" src="${scriptUri}">
        </script>
  </body>

Обратите внимание, что мы использовали переменную scriptUri для задания пути и переменную nonce для подтверждения разрешения доступа к файлу с кодом JavaScript. Для доступа к отправке сообщений нам нужно будет использовать не самую очевидную конструкцию:

const vscode = acquireVsCodeApi();

Она потребуется для обмена сообщениями между довольно изолированным миром html-панели и кодом нашего расширения (обратите внимание на вызов метода postMessage() в примере ниже):

function sendLog(msg) {
  vscode.postMessage({ name: 'log', value: msg });
}

И вы, наверное, догадываетесь, что метод postMessage() может принимать произвольный тип данных.

Несколько практических приёмов, которые могут вам пригодится

На картинке изображен вариант назначения обработчиков для полей, который сводится к посылке тех или иных сообщений через метод postMessage() в зависимости от типа поля:

 sendLog('Start view/panel: begin' );

    fields = [];
    fields = addFields('input', fields);
    fields = addFields('select', fields);

    fields.forEach(element => {
        const elementIt = document.getElementById(element);
        if (elementIt) {
            if (elementIt.type === 'checkbox') {
                elementIt.addEventListener('change', () => {
                    fieldChecked(element);
                });
            }
            else if (elementIt.type === 'button') {
                elementIt.addEventListener('click', () => {
                    vscode.postMessage({ name: element, value: true });
                });
            }
            else {
                elementIt.addEventListener('change', () => {
                    fieldChanged(element);
                });
            }
            sendLog(`Handler for ${element} is set OK`);
        }
        else {
            sendLog(`Handler for ${element} NOT set`);
        }
    });
    sendLog('Start view/panel: end');

Пример, как могут выглядеть эти функции:

function fieldChecked(name) {
        vscode.postMessage({ name: name, value: document.getElementById(name).checked });
    }

    function fieldChanged(name) {
        vscode.postMessage({ name: name, value: document.getElementById(name).value });
    }

Информация о полях собирается с помощью, например, такой функции:

 function addFields(tagName, fields) {
        inputMap = document.getElementsByTagName(tagName);
        for (i = 0; i < inputMap.length; i++) {
            fields.push(inputMap[i].id);
        }
        return fields;
    }

И пример обработчика для установки значений в полях панели:

 window.addEventListener('message', event => {
        const message = event.data;
        const element = document.getElementById(message.name);
        if (message.name === 'status') {
            element.innerHTML = message.value;
            console.log(element.innerHTML + " status");
        }
        else if (element) {
            element.value = message.value;
            console.log('Set ' + message.name + ' to ' + element.value);
        }
        else {
            console.log(element + " not found");
        }
    });

Как видите, всё в пределах программы первого класса школы веб-программистов, за исключением использования vscode.postMessage для обмена информацией между условной формой и кодом панели для обработки ввода в эту форму.

На картинке ниже вы можете видеть пример вкладки с различными элементами ввода.

Также обратите внимание на элемент со всплывающей подсказкой (edit profile). В первой части мы уже упоминали о меню в панелях. В данном случае мы видим другую разновидность (элемент меню в заголовке не зависит от контекста как в случае с деревом):

{
  "command": "mgt.conanEditProfile",
  "when": "view == mo.manageProjectSettings",
  "group": "navigation"
},

Но в этом случае должна быть определена команда с атрибутом "icon":

{
  "command": "mgt.conanEditProfile",
  "icon": "$(go-to-file)",
  "category": "profile",
  "title": "edit profile"
},
Используем отладчик

Скорее всего, вам не удастся сразу написать идеальный класс панели. Особенно с учётом всех нюансов использования нескольких сред: HTML-разметка, JavaScript для обработки событий и самого расширения. Поэтому вам рано или поздно понадобится отладчик.

Для вызова отладчика внутри панели используйте комбинацию Ctrl-Shift-P и начните набирать ключевые слова вроде Toggle Developer. После этого, скорее всего, вы увидите в появившемся выпадающем списке подсказку Developer: Toggle Developer Tools. Либо вы можете сразу воспользоваться комбинацией Ctrl-Shift-I, выбрать его и увидеть панель с отладчиками – они знакомы многим как Инструменты РазработчикаDeveloper Tools.

Майкрософт предупреждает, что если вы используете VS Code старше 1.56 (т.е. версия меньше) или пытаетесь отлаживать веб-представление, которое устанавливает enableFindWidget (см. WebviewPanelOptions), то вам нужно использовать Developer: Open Webview Developer Tools. Тогда у вас откроется панель с отладчиком (и другими инструментами разработчика) конкретно для данного представления.

Последнее – утверждение от Майкрософта, но я не почувствовал особой разницы: при запуске отладчик может поставить вас на ваш main.js (как показано ниже), а может не поставить, и вам придётся поискать его в одном из ответвлений довольно обширного дерева.

Другие средства VS Code API

На этом тему панелей и представлений можно считать раскрытой. Теперь рассмотрим какие еще средства взаимодействия с пользователем доступны разработчику в VS Code. А именно диалоговые сообщения, пользовательские панели вывода и панели состояния и другие.

Диалоговые сообщения

Самый простой способ взаимодействия, который можно использовать в расширениях — простые элементы ввода: всплывающие сообщения, сообщения с вариантами ответов, поле текстового ввода, выпадающий список.

Если вы имели дело с Code, то наверняка с ними сталкивались. Например, всплывающие сообщения мы использовали в прошлой статье. Их разновидность – сообщения об ошибках, предупреждения и просто информационные сообщения. Для каждого из перечисленных видов можно использовать такие вызовы:

vscode.window.showErrorMessage
vscode.window.showInformationMessage
vscode.window.showWarningMessage
let answer = await vscode.window.showWarningMessage("Conan not found, do you want to install it?", 
                                                    "Yes", "No", "Я не знаю");

У этих вызовов одинаковый набор аргументов: само сообщение и массив из текстовых строк, из которых можно сделать выбор. Наверняка, вы уже сталкивались с подобными всплывающими диалогами, да и в предыдущей статье мы их уже использовали.

Можно также сделать сообщение модальным (то есть, блокирующим главное окно) с помощью дополнительного аргумента в MessageOptions.
Для тех кто не является специалистом по TypeScript (как и автор), напомню, что нужно добавить await, если хотите получить результат выбора, а не просто вывести сообщение.

Инструменты, помимо диалогов

Средства взаимодействия не ограничиваются вышеперечисленными диалогами. Вашему расширению также могут быть доступны поля ввода, выпадающие списки, диалоги выбора папки проекта и диалоги открыть/сохранить.

Для работы с ними рекомендованы следующие функции из пространства vscode.window. (есть и другие, но рекомендованы именно эти):

vscode.window.showInputBox
vscode.window.showQuickPick
vscode.window.showWorkspaceFolderPick
vscode.window.showOpenDialog/vscode.window.showSaveDialog

Будет нелишним дать пример использования для showOpenDialog и ему подобных:

const workspaceFolder = vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri : undefined;
const opt: vscode.OpenDialogOptions = { defaultUri: workspaceFolder, canSelectFiles: false, canSelectFolders: true, canSelectMany: false, title: "Select folder" };
let folderWhereToCreate: vscode.Uri[] | undefined = await vscode.window.showOpenDialog(opt);

Пользовательские панели вывода

Вы наверняка замечали, что при стандартном расположении, в самом низу под областью редактирования время от времени всплывает панель с вкладками «Проблемы», «Вывод», «Терминал» и т.п.

Расширение тоже может создать такую вкладку простым вызовом:

let npsOutputChannel = vscode.window.createOutputChannel('NPS/DCS log', 'json');

Вывод в такую панель можно сделать, например, так:

npsOutputChannel.appendLine('NPS output ')

Если у вас несколько таких панелей, то программно можно переключаться между ними выводом пустой строки с помощью этой же конструкции.

Следует обратить внимание, что массированный вывод информации в эти панели лучше делать буферизированно (то есть порциями, по мере накопления), иначе очень легко заблокировать работу не только самого VS Code, но и всю операционную систему наглухо зависшим редактором (я уже писал об этом выше в разделе про работу веб-панелей).

Панель состояния

Ещё один простой способ взаимодействия с вашим расширением – через элементы на панели состояния, которая расположена в самом низу окна VS Code.

Пример создания активного элемента (кнопки)

function createBarItem(context: vscode.ExtensionContext,
	cmdId: string, color: string | vscode.ThemeColor,
	text: string, tooltip: string): vscode.StatusBarItem {
	const barItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 30);
	barItem.command = cmdId;
	context.subscriptions.push(barItem);
	barItem.text = text;
	barItem.tooltip = tooltip;
	barItem.color = color;
	return barItem;
}

На всякий случай, число 30 в вызове createStatusBarItem – это приоритет (чем больше, тем выше) в сравнении с другими элементами.

Остальное должно быть понятно и так, в том числе из следующего фрагмента:

let msg = vscode.window.setStatusBarMessage("$(sync~spin) Go find conan!");

let cmdId = 'conan-pkg.version';
let barItem = createBarItem(context, cmdId, 'darkblue', "$(loading~spin) Version Info", "Package Version Info");
barItem.show();

Здесь стоит напомнить, что строка с наименованием "$(info) Version Info", включает в себя указание на встроенное изображение $(info) , об использовании которых я упоминал в предыдущей статье. Их полный список можно найти по ссылке.

Недавно я узнал из обновленной документации, что появилась возможность задавать с помощью суффикса ~spin анимацию для нескольких из них:

  • sync

  • loading

  • gear

т.е. при задании $(sync~spin) мы получим (картинка из документации):

Ещё одно нововведение – возможность окрасить фон элемента, обозначив предупреждение или ошибку. Цвет фона при этом задаётся несколько более сложным образом, нежели основной:

Выше приведена выдержка из node_modules/@types/vscode/index.d.ts (кстати, ещё одним существенным плюсом с точки зрения разработки расширений в VS Code является возможность быстро перейти непосредственно к спецификациям API в самом Code).

Возможные результаты применения на картинках ниже (из документации):

Сообщения

Для вывода сообщения в строку состояния используется функция vscode.window.setStatusBarMessage с одним аргументом типа «строка».

Пример вывода с последующим удалением через вызов метода dispose().

В результате применения элемента (фактически, кнопки) из примера выше и сообщения мы можем увидеть следующую картину в самой нижней области:

Как вы поняли из кода последнего фрагмента сообщение ("Go find conan!") исчезнет с панели по истечении заданного времени (1500 миллисекунд).

В заключение

В этой и предыдущей статьях мы рассмотрели несколько способов взаимодействия между пользователем и расширением разной степени сложности. Для автоматизации повседневных задач этих средств будет более чем достаточно. Мне, например, хватает.

В следующей статье мы рассмотрим частности, которые вы можете внедрить в свои расширения, чтобы они выглядели «не хуже, чем у Майкрософт» или стали ещё более дружественными по отношению к пользователю.

Исходный код учебного расширения из статьи полностью доступен здесь.

Комментарии (4)


  1. VitaminND
    18.02.2025 19:51

    Спасибо большое! Очень пригодилось!


    1. roboboroed316 Автор
      18.02.2025 19:51

      Спасибо за спасибо!) Всегда рад!) А можно спросить, что именно пригодилось?)


    1. roboboroed316 Автор
      18.02.2025 19:51

      И над чем конкретно работаете тоже, если не секрет, конечно)


  1. VitaminND
    18.02.2025 19:51

    Пригодилось все - я не очень опытный в VSCode ))

    Делаю расширение для моделирования в БД - таблицы, Data Vault и прочее в этом духе