Compare commits
12 Commits
210ff6ca57
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ced06e1ca8 | |||
| a7ece0b22e | |||
| cfbe03e06e | |||
| 3da37cd591 | |||
| 853b0f9a0b | |||
|
|
4ef5b4b27a | ||
|
|
ea9c1181bb | ||
|
|
dfcb1c5656 | ||
|
|
0842268f28 | ||
|
|
bcb2c381fb | ||
|
|
784c1311f4 | ||
|
|
a98b1a0464 |
85
.github/README.md
vendored
Normal file
85
.github/README.md
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
# Template File Generator
|
||||
|
||||
Расширение для VS Code. Генерирует файлы и папки из ваших шаблонов с подстановкой переменных.
|
||||
|
||||
Подсветка синтаксиса и автодополнение переменных прямо в файлах шаблонов.
|
||||
|
||||
## Как работает
|
||||
|
||||
1. Создайте папку `.templates` в корне проекта
|
||||
2. Внутри -- каждая подпапка это шаблон
|
||||
3. Используйте переменные в именах файлов и содержимом: `{{name}}` присутствует всегда, дополнительно можно использовать любое количество своих -- `{{author}}`, `{{module}}` и т.д.
|
||||
4. ПКМ по папке в проводнике -- **Создать из шаблона...**
|
||||
|
||||
Расширение само найдёт все переменные в шаблоне и предложит заполнить каждую.
|
||||
|
||||
## Пример
|
||||
|
||||
Структура шаблона:
|
||||
|
||||
```
|
||||
.templates/
|
||||
component/
|
||||
{{name.pascalCase}}/
|
||||
{{name.pascalCase}}.tsx
|
||||
types/
|
||||
{{name.kebabCase}}.type.ts
|
||||
styles/
|
||||
{{name.kebabCase}}.module.css
|
||||
```
|
||||
|
||||
Содержимое `{{name.pascalCase}}.tsx`:
|
||||
|
||||
```tsx
|
||||
import cl from 'clsx'
|
||||
import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.type'
|
||||
import styles from './styles/{{name.kebabCase}}.module.css'
|
||||
|
||||
/**
|
||||
* <Назначение компонента {{name.pascalCase}} в 1 строке>.
|
||||
*
|
||||
* Используется для:
|
||||
* - <сценарий 1>
|
||||
* - <сценарий 2>
|
||||
*/
|
||||
export const {{name.pascalCase}} = (props: {{name.pascalCase}}Props) => {
|
||||
const { children, className, ...htmlAttr } = props
|
||||
|
||||
return (
|
||||
<div {...htmlAttr} className={cl(styles.root, className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
При вводе `user-profile` получите:
|
||||
|
||||
```
|
||||
UserProfile/
|
||||
UserProfile.tsx
|
||||
types/
|
||||
user-profile.type.ts
|
||||
styles/
|
||||
user-profile.module.css
|
||||
```
|
||||
|
||||
## Модификаторы
|
||||
|
||||
| Модификатор | `user-profile` |
|
||||
|---|---|
|
||||
| `{{name}}` | user-profile |
|
||||
| `{{name.pascalCase}}` | UserProfile |
|
||||
| `{{name.camelCase}}` | userProfile |
|
||||
| `{{name.snakeCase}}` | user_profile |
|
||||
| `{{name.kebabCase}}` | user-profile |
|
||||
| `{{name.screamingSnakeCase}}` | USER_PROFILE |
|
||||
|
||||
## Настройки
|
||||
|
||||
| Ключ | Описание | По умолчанию |
|
||||
|---|---|---|
|
||||
| `templateFileGenerator.templatesPath` | Путь к шаблонам | `.templates` |
|
||||
| `templateFileGenerator.overwriteFiles` | Перезапись существующих файлов | `false` |
|
||||
| `templateFileGenerator.inputMode` | Режим ввода: `webview` или `inputBox` | `webview` |
|
||||
| `templateFileGenerator.language` | Язык интерфейса (`ru` / `en`) | `en` |
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -130,3 +130,5 @@ dist
|
||||
.pnp.*
|
||||
|
||||
.DS_Store
|
||||
|
||||
*.vsix
|
||||
15
.vscode/tasks.json
vendored
15
.vscode/tasks.json
vendored
@@ -6,7 +6,20 @@
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"problemMatcher": "$ts-webpack-watch",
|
||||
"problemMatcher": {
|
||||
"owner": "webpack",
|
||||
"pattern": {
|
||||
"regexp": "ERROR in (.+)\\((\\d+),(\\d+)\\)",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3
|
||||
},
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "webpack .+ compilat",
|
||||
"endsPattern": "webpack .+ compiled"
|
||||
}
|
||||
},
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
out/**
|
||||
node_modules/**
|
||||
src/**
|
||||
!media/**
|
||||
.gitignore
|
||||
.yarnrc
|
||||
webpack.config.js
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to the "mytemplategenerator" extension will be documented in this file.
|
||||
All notable changes to the "templateFileGenerator" extension will be documented in this file.
|
||||
|
||||
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
|
||||
|
||||
|
||||
250
README.md
250
README.md
@@ -1,211 +1,85 @@
|
||||
[English](#english) | [Русский](#русский)
|
||||
# Template File Generator
|
||||
|
||||
---
|
||||
Расширение для VS Code. Генерирует файлы и папки из ваших шаблонов с подстановкой переменных.
|
||||
|
||||
# English
|
||||
Подсветка синтаксиса и автодополнение переменных прямо в файлах шаблонов.
|
||||
|
||||
## My Template Generator — Template-based structure generation for VSCode
|
||||
## Как работает
|
||||
|
||||

|
||||
**Features:**
|
||||
- Syntax highlighting and autocomplete for template variables in templates
|
||||
- Generate files and folders from templates with variable substitution
|
||||
- Full localization (English/Russian) for all UI, messages, and menus
|
||||
- Choose variable input mode: Webview (form) or inputBox (one by one)
|
||||
- Overwrite control: allow or forbid overwriting existing files/folders
|
||||
- Smart conflict handling: clear notifications if structure already exists
|
||||
1. Создайте папку `.templates` в корне проекта
|
||||
2. Внутри -- каждая подпапка это шаблон
|
||||
3. Используйте переменные в именах файлов и содержимом: `{{name}}` присутствует всегда, дополнительно можно использовать любое количество своих -- `{{author}}`, `{{module}}` и т.д.
|
||||
4. ПКМ по папке в проводнике -- **Создать из шаблона...**
|
||||
|
||||
Context menu "Create from template..." is available by right-clicking any folder.
|
||||

|
||||
Расширение само найдёт все переменные в шаблоне и предложит заполнить каждую.
|
||||
|
||||
Convenient UI for creation: just select a template and specify values for the variables used in the template. (The list of variables updates automatically)
|
||||

|
||||
## Пример
|
||||
|
||||
User-friendly settings UI.
|
||||

|
||||
Структура шаблона:
|
||||
|
||||
### Quick Start
|
||||
1. Create a `templates` folder in your project root.
|
||||
2. Add subfolders for different templates (e.g., `components`, `store`).
|
||||
3. Use variables like `{{name}}` or `{{name.pascalCase}}` in file/folder names and file contents.
|
||||
4. Right-click a folder in VSCode and select **Create from template...**
|
||||
5. Choose a template, fill in variables, and click "Create".
|
||||
|
||||
### Example template structure
|
||||
```
|
||||
templates/
|
||||
components/
|
||||
{{name}}/
|
||||
index.js
|
||||
style.module.css
|
||||
store/
|
||||
{{name}}Store.js
|
||||
```
|
||||
|
||||
### Supported variables and modifiers
|
||||
|
||||
You can use variables with modifiers via dot notation:
|
||||
|
||||
- `{{name}}` — as entered by user
|
||||
- `{{name.pascalCase}}` — PascalCase
|
||||
- `{{name.camelCase}}` — camelCase
|
||||
- `{{name.snakeCase}}` — snake_case
|
||||
- `{{name.kebabCase}}` — kebab-case
|
||||
- `{{name.screamingSnakeCase}}` — SCREAMING_SNAKE_CASE
|
||||
- `{{name.upperCase}}` — First letter uppercase
|
||||
- `{{name.lowerCase}}` — all lowercase
|
||||
- `{{name.upperCaseAll}}` — ALLUPPERCASE (no separators)
|
||||
- `{{name.lowerCaseAll}}` — alllowercase (no separators)
|
||||
|
||||
> When searching for variables for the form, only the name before the dot is considered. For example, `{{name}}` and `{{name.pascalCase}}` are the same variable.
|
||||
|
||||
### Example usage in template
|
||||
```
|
||||
components/
|
||||
.templates/
|
||||
component/
|
||||
{{name.pascalCase}}/
|
||||
index.js
|
||||
{{name.camelCase}}.service.js
|
||||
{{name.snakeCase}}.test.js
|
||||
```
|
||||
And in file contents:
|
||||
```
|
||||
export class {{name.pascalCase}} {}
|
||||
const name = '{{name}}';
|
||||
{{name.pascalCase}}.tsx
|
||||
types/
|
||||
{{name.kebabCase}}.type.ts
|
||||
styles/
|
||||
{{name.kebabCase}}.module.css
|
||||
```
|
||||
|
||||
### Configuration
|
||||
**To open the visual configurator, press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd>, type `Configure myTemplateGenerator...` and select the command.**
|
||||
Содержимое `{{name.pascalCase}}.tsx`:
|
||||
|
||||
Use `mytemplategenerator.json` in your project root or the visual configurator (**Configure myTemplateGenerator...**):
|
||||
```json
|
||||
{
|
||||
"templatesPath": "templates",
|
||||
"overwriteFiles": false,
|
||||
"inputMode": "webview", // or "inputBox"
|
||||
"language": "en" // or "ru"
|
||||
```tsx
|
||||
import cl from 'clsx'
|
||||
import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.type'
|
||||
import styles from './styles/{{name.kebabCase}}.module.css'
|
||||
|
||||
/**
|
||||
* <Назначение компонента {{name.pascalCase}} в 1 строке>.
|
||||
*
|
||||
* Используется для:
|
||||
* - <сценарий 1>
|
||||
* - <сценарий 2>
|
||||
*/
|
||||
export const {{name.pascalCase}} = (props: {{name.pascalCase}}Props) => {
|
||||
const { children, className, ...htmlAttr } = props
|
||||
|
||||
return (
|
||||
<div {...htmlAttr} className={cl(styles.root, className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
- **templatesPath** — path to templates folder
|
||||
- **overwriteFiles** — allow or forbid overwriting existing files/folders
|
||||
- **inputMode** — variable input mode: "webview" (form) or "inputBox" (one by one)
|
||||
- **language** — plugin UI language (en/ru)
|
||||
|
||||
### Localization
|
||||
- All UI, messages, errors, and menus are localized.
|
||||
- Webview and messages use the language from config.
|
||||
- Menu/command language depends on VSCode interface language.
|
||||
При вводе `user-profile` получите:
|
||||
|
||||
### Key commands
|
||||
- **Create from template...** — generate structure (context menu)
|
||||
- **Configure myTemplateGenerator...** — open visual configurator (command palette)
|
||||
|
||||
### Error handling & overwrite
|
||||
- If structure or file exists and overwrite is forbidden, generation is cancelled and a clear notification is shown.
|
||||
- Any file creation error stops generation and shows the reason.
|
||||
|
||||
---
|
||||
|
||||
# Русский
|
||||
|
||||
## My Template Generator — Генерация структуры из шаблонов для VSCode
|
||||
|
||||

|
||||
**Возможности:**
|
||||
- Подсветка синтаксиса и автокомплит переменных в шаблонах
|
||||
- Генерация файлов и папок по шаблонам с подстановкой переменных
|
||||
- Полная локализация (русский/английский) для всего интерфейса, сообщений и меню
|
||||
- Выбор способа ввода переменных: Webview (форма) или inputBox (по одной)
|
||||
- Контроль перезаписи: можно запретить или разрешить перезапись существующих файлов/папок
|
||||
- Умная обработка конфликтов: понятные уведомления, если структура уже существует
|
||||
|
||||
Контекстное меню "Создать из шаблона.." доступно правым кликом по любой папке.
|
||||

|
||||
|
||||
Удобный UI создания, нужно только выбрать шаблок и указать значения переменных используемых в шаблоне. (Список переменных обновляется автоматически)
|
||||

|
||||
|
||||
Удобный UI интерфейс настроек.
|
||||

|
||||
|
||||
|
||||
### Быстрый старт
|
||||
1. В корне проекта создайте папку `templates`.
|
||||
2. Внутри неё создайте подпапки для разных шаблонов (например, `components`, `store`).
|
||||
3. Внутри шаблонов используйте переменные вида `{{name}}` или `{{name.pascalCase}}` в именах файлов/папок и в содержимом файлов.
|
||||
4. Кликните правой кнопкой мыши на нужной папке в VSCode и выберите пункт **Создать из шаблона...**
|
||||
5. В появившемся окне выберите шаблон, заполните переменные и нажмите "Создать".
|
||||
|
||||
### Пример структуры шаблонов
|
||||
```
|
||||
templates/
|
||||
components/
|
||||
{{name}}/
|
||||
index.js
|
||||
style.module.css
|
||||
store/
|
||||
{{name}}Store.js
|
||||
UserProfile/
|
||||
UserProfile.tsx
|
||||
types/
|
||||
user-profile.type.ts
|
||||
styles/
|
||||
user-profile.module.css
|
||||
```
|
||||
|
||||
### Переменные и модификаторы
|
||||
## Модификаторы
|
||||
|
||||
В шаблонах можно использовать переменные с модификаторами через точку:
|
||||
| Модификатор | `user-profile` |
|
||||
|---|---|
|
||||
| `{{name}}` | user-profile |
|
||||
| `{{name.pascalCase}}` | UserProfile |
|
||||
| `{{name.camelCase}}` | userProfile |
|
||||
| `{{name.snakeCase}}` | user_profile |
|
||||
| `{{name.kebabCase}}` | user-profile |
|
||||
| `{{name.screamingSnakeCase}}` | USER_PROFILE |
|
||||
|
||||
- `{{name}}` — как ввёл пользователь
|
||||
- `{{name.pascalCase}}` — PascalCase
|
||||
- `{{name.camelCase}}` — camelCase
|
||||
- `{{name.snakeCase}}` — snake_case
|
||||
- `{{name.kebabCase}}` — kebab-case
|
||||
- `{{name.screamingSnakeCase}}` — SCREAMING_SNAKE_CASE
|
||||
- `{{name.upperCase}}` — Первая буква большая
|
||||
- `{{name.lowerCase}}` — все буквы маленькие
|
||||
- `{{name.upperCaseAll}}` — ВСЕ БУКВЫ БОЛЬШИЕ (без разделителей)
|
||||
- `{{name.lowerCaseAll}}` — все буквы маленькие (без разделителей)
|
||||
## Настройки
|
||||
|
||||
> При поиске переменных для формы учитывается только имя до точки. Например, `{{name}}` и `{{name.pascalCase}}` — это одна переменная.
|
||||
|
||||
### Пример использования в шаблоне
|
||||
```
|
||||
components/
|
||||
{{name.pascalCase}}/
|
||||
index.js
|
||||
{{name.camelCase}}.service.js
|
||||
{{name.snakeCase}}.test.js
|
||||
```
|
||||
Внутри файлов также можно использовать эти переменные:
|
||||
```
|
||||
export class {{name.pascalCase}} {}
|
||||
const name = '{{name}}';
|
||||
```
|
||||
|
||||
### Конфигурация
|
||||
**Чтобы открыть визуальный конфигуратор, нажмите <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd>, введите `Настроить myTemplateGenerator...` и выберите команду.**
|
||||
|
||||
Для гибкой настройки используйте файл `mytemplategenerator.json` в корне проекта или визуальный конфигуратор (команда **Настроить myTemplateGenerator...**):
|
||||
|
||||
```json
|
||||
{
|
||||
"templatesPath": "templates",
|
||||
"overwriteFiles": false,
|
||||
"inputMode": "webview", // или "inputBox"
|
||||
"language": "ru" // или "en"
|
||||
}
|
||||
```
|
||||
- **templatesPath** — путь к папке с шаблонами
|
||||
- **overwriteFiles** — разрешать ли перезапись существующих файлов/папок
|
||||
- **inputMode** — способ ввода переменных: "webview" (форма) или "inputBox" (по одной)
|
||||
- **language** — язык интерфейса плагина (ru/en)
|
||||
|
||||
### Локализация
|
||||
- Все сообщения, Webview, ошибки и пункты меню локализованы.
|
||||
- Язык Webview и сообщений выбирается в конфигураторе.
|
||||
- Язык пунктов меню и команд зависит от языка интерфейса VSCode.
|
||||
|
||||
### Важные команды
|
||||
- **Создать из шаблона...** — генерация структуры (контекстное меню)
|
||||
- **Настроить myTemplateGenerator...** — открыть визуальный конфигуратор (палитра команд)
|
||||
|
||||
### Обработка ошибок и перезаписи
|
||||
- Если структура или файл уже существуют и перезапись запрещена, генерация не выполняется и выводится понятное уведомление.
|
||||
- Если при создании любого файла возникает ошибка — генерация полностью прекращается, и пользователь видит причину.
|
||||
|
||||
---
|
||||
| Ключ | Описание | По умолчанию |
|
||||
|---|---|---|
|
||||
| `templateFileGenerator.templatesPath` | Путь к шаблонам | `.templates` |
|
||||
| `templateFileGenerator.overwriteFiles` | Перезапись существующих файлов | `false` |
|
||||
| `templateFileGenerator.inputMode` | Режим ввода: `webview` или `inputBox` | `webview` |
|
||||
| `templateFileGenerator.language` | Язык интерфейса (`ru` / `en`) | `en` |
|
||||
|
||||
124
media/styles.css
Normal file
124
media/styles.css
Normal file
@@ -0,0 +1,124 @@
|
||||
:root {
|
||||
--bg: #f7f7fa;
|
||||
--panel-bg: #fff;
|
||||
--text: #222;
|
||||
--label: #555;
|
||||
--input-bg: #f0f0f3;
|
||||
--input-border: #d0d0d7;
|
||||
--input-focus: #1976d2;
|
||||
--button-bg: #1976d2;
|
||||
--button-text: #fff;
|
||||
--button-hover: #1565c0;
|
||||
--border-radius: 8px;
|
||||
--shadow: 0 2px 12px rgba(0,0,0,0.07);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #181a1b;
|
||||
--panel-bg: #23272e;
|
||||
--text: #f3f3f3;
|
||||
--label: #b0b0b0;
|
||||
--input-bg: #23272e;
|
||||
--input-border: #33363b;
|
||||
--input-focus: #90caf9;
|
||||
--button-bg: #1976d2;
|
||||
--button-text: #fff;
|
||||
--button-hover: #1565c0;
|
||||
--border-radius: 8px;
|
||||
--shadow: 0 2px 12px rgba(0,0,0,0.25);
|
||||
}
|
||||
}
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: 'Segoe UI', 'Roboto', Arial, sans-serif;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.create-container, .config-container {
|
||||
max-width: 420px;
|
||||
margin: 48px auto;
|
||||
background: var(--panel-bg);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 32px 36px 28px 36px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.head-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.create-container h2, .config-container h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
label {
|
||||
color: var(--label);
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
}
|
||||
input, select {
|
||||
background: var(--input-bg);
|
||||
color: var(--text);
|
||||
border: 1.5px solid var(--input-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 8px 10px;
|
||||
font-size: 1em;
|
||||
transition: border 0.2s, box-shadow 0.2s;
|
||||
outline: none;
|
||||
}
|
||||
input:focus, select:focus {
|
||||
border-color: var(--input-focus);
|
||||
box-shadow: 0 0 0 2px var(--input-focus)33;
|
||||
}
|
||||
button, .btn {
|
||||
margin-top: 10px;
|
||||
background: var(--button-bg);
|
||||
color: var(--button-text);
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 10px 15px;
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, box-shadow 0.2s;
|
||||
box-shadow: 0 1px 4px rgba(25, 118, 210, 0.08);
|
||||
}
|
||||
button:hover, button:focus, .btn:hover, .btn:focus {
|
||||
background: var(--button-hover);
|
||||
}
|
||||
.destination {
|
||||
margin-bottom: 16px;
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
}
|
||||
.lang-select {
|
||||
display: block;
|
||||
}
|
||||
.var-hint {
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.template-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#configForm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
4014
package-lock.json
generated
4014
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
121
package.json
121
package.json
@@ -1,46 +1,113 @@
|
||||
{
|
||||
"name": "mytemplategenerator",
|
||||
"displayName": "myTemplateGenerator",
|
||||
"description": "Generate files and folders from customizable templates with variable substitution in VSCode.",
|
||||
"version": "0.0.4",
|
||||
"publisher": "MyTemplateGenerator",
|
||||
"name": "vscode-templateFileGenerator",
|
||||
"displayName": "Template File Generator | gromlab",
|
||||
"description": "Generate files and folders from templates with variable substitution and case modifiers. Syntax highlighting and autocomplete for template variables.",
|
||||
"version": "1.0.2",
|
||||
"publisher": "gromlab",
|
||||
"author": "Sergey Gromov",
|
||||
"scripts": {
|
||||
"build-app": "npm run build && npm run package-vsce",
|
||||
"compile": "webpack",
|
||||
"watch": "webpack --watch",
|
||||
"build": "webpack --mode production --devtool hidden-source-map",
|
||||
"package-vsce": "vsce package",
|
||||
"compile-tests": "tsc -p . --outDir out",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||
"lint": "eslint src",
|
||||
"test": "vscode-test"
|
||||
},
|
||||
"categories": [
|
||||
"Snippets",
|
||||
"Other"
|
||||
],
|
||||
"keywords": [
|
||||
"template",
|
||||
"scaffold",
|
||||
"generator",
|
||||
"boilerplate",
|
||||
"component",
|
||||
"file generator",
|
||||
"project structure",
|
||||
"code generation",
|
||||
"snippets"
|
||||
],
|
||||
"icon": "logo.png",
|
||||
"url": "https://github.com/gormov1122/MyTemplateGenerator",
|
||||
"homepage": "https://gromlab.ru/gromov/vscode-template-file-generator",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gromlab.ru/gromov/vscode-template-file-generator.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://gromlab.ru/gromov/vscode-template-file-generator/issues"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.60.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onStartupFinished",
|
||||
"onLanguage:javascript",
|
||||
"onLanguage:typescript",
|
||||
"onLanguage:css",
|
||||
"onLanguage:plaintext",
|
||||
"onFileSystem: file"
|
||||
"onStartupFinished"
|
||||
],
|
||||
"main": "./dist/extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "mytemplategenerator.createFromTemplate",
|
||||
"title": "%mytemplategenerator.createFromTemplate.title%"
|
||||
"command": "templateFileGenerator.createFromTemplate",
|
||||
"title": "%templateFileGenerator.createFromTemplate.title%"
|
||||
},
|
||||
{
|
||||
"command": "mytemplategenerator.configure",
|
||||
"title": "%mytemplategenerator.configure.title%"
|
||||
"command": "templateFileGenerator.configure",
|
||||
"title": "%templateFileGenerator.configure.title%"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"explorer/context": [
|
||||
{
|
||||
"command": "mytemplategenerator.createFromTemplate",
|
||||
"command": "templateFileGenerator.createFromTemplate",
|
||||
"group": "navigation@10"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configuration": {
|
||||
"title": "Template Forge",
|
||||
"properties": {
|
||||
"templateFileGenerator.templatesPath": {
|
||||
"type": "string",
|
||||
"default": ".templates",
|
||||
"description": "%templateFileGenerator.config.templatesPath.description%",
|
||||
"markdownDescription": "%templateFileGenerator.config.templatesPath.description%",
|
||||
"scope": "application"
|
||||
},
|
||||
"templateFileGenerator.overwriteFiles": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "%templateFileGenerator.config.overwriteFiles.description%",
|
||||
"markdownDescription": "%templateFileGenerator.config.overwriteFiles.description%",
|
||||
"scope": "application"
|
||||
},
|
||||
"templateFileGenerator.inputMode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"webview",
|
||||
"inputBox"
|
||||
],
|
||||
"default": "webview",
|
||||
"description": "%templateFileGenerator.config.inputMode.description%",
|
||||
"markdownDescription": "%templateFileGenerator.config.inputMode.description%",
|
||||
"scope": "application"
|
||||
},
|
||||
"templateFileGenerator.language": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ru",
|
||||
"en"
|
||||
],
|
||||
"default": "en",
|
||||
"description": "%templateFileGenerator.config.language.description%",
|
||||
"markdownDescription": "%templateFileGenerator.config.language.description%",
|
||||
"scope": "application"
|
||||
}
|
||||
}
|
||||
},
|
||||
"semanticTokenColors": [
|
||||
{
|
||||
"token": "bracket",
|
||||
@@ -56,17 +123,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run package",
|
||||
"compile": "webpack",
|
||||
"watch": "webpack --watch",
|
||||
"package": "webpack --mode production --devtool hidden-source-map",
|
||||
"compile-tests": "tsc -p . --outDir out",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||
"lint": "eslint src",
|
||||
"test": "vscode-test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "20.x",
|
||||
@@ -75,6 +131,7 @@
|
||||
"@typescript-eslint/parser": "^8.31.1",
|
||||
"@vscode/test-cli": "^0.0.11",
|
||||
"@vscode/test-electron": "^2.5.2",
|
||||
"@vscode/vsce": "^3.6.2",
|
||||
"eslint": "^9.25.1",
|
||||
"ts-loader": "^9.5.2",
|
||||
"typescript": "^5.8.3",
|
||||
@@ -82,6 +139,6 @@
|
||||
"webpack-cli": "^6.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"handlebars": "^4.7.8"
|
||||
"@gromlab/create": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"mytemplategenerator.createFromTemplate.title": "Create from template...",
|
||||
"mytemplategenerator.configure.title": "Configure myTemplateGenerator..."
|
||||
"templateFileGenerator.createFromTemplate.title": "Create from template...",
|
||||
"templateFileGenerator.configure.title": "Configure Template File Generator...",
|
||||
"templateFileGenerator.config.templatesPath.description": "Path to the templates folder (relative to the project root)",
|
||||
"templateFileGenerator.config.overwriteFiles.description": "Overwrite existing files when generating from template",
|
||||
"templateFileGenerator.config.inputMode.description": "Variable input mode: webview or inputBox",
|
||||
"templateFileGenerator.config.language.description": "Extension interface language"
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"mytemplategenerator.createFromTemplate.title": "Создать из шаблона...",
|
||||
"mytemplategenerator.configure.title": "Настроить myTemplateGenerator..."
|
||||
"templateFileGenerator.createFromTemplate.title": "Создать из шаблона...",
|
||||
"templateFileGenerator.configure.title": "Настроить генератор шаблонов...",
|
||||
"templateFileGenerator.config.templatesPath.description": "Путь к папке с шаблонами (относительно корня проекта)",
|
||||
"templateFileGenerator.config.overwriteFiles.description": "Перезаписывать ли существующие файлы при генерации",
|
||||
"templateFileGenerator.config.inputMode.description": "Режим ввода переменных: webview или inputBox",
|
||||
"templateFileGenerator.config.language.description": "Язык интерфейса расширения"
|
||||
}
|
||||
37
src/core/config.ts
Normal file
37
src/core/config.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Работа с конфигом расширения
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export interface TemplateFileGeneratorConfig {
|
||||
templatesPath: string;
|
||||
overwriteFiles: boolean;
|
||||
inputMode: 'webview' | 'inputBox';
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export function readConfig(): TemplateFileGeneratorConfig {
|
||||
const config = vscode.workspace.getConfiguration('templateFileGenerator');
|
||||
return {
|
||||
templatesPath: config.get<string>('templatesPath', '.templates'),
|
||||
overwriteFiles: config.get<boolean>('overwriteFiles', false),
|
||||
inputMode: config.get<'webview' | 'inputBox'>('inputMode', 'webview'),
|
||||
language: config.get<string>('language', 'en'),
|
||||
};
|
||||
}
|
||||
|
||||
export async function writeConfig(newConfig: Partial<TemplateFileGeneratorConfig>) {
|
||||
const config = vscode.workspace.getConfiguration('templateFileGenerator');
|
||||
if (newConfig.templatesPath !== undefined) {
|
||||
await config.update('templatesPath', newConfig.templatesPath, vscode.ConfigurationTarget.Global);
|
||||
}
|
||||
if (newConfig.overwriteFiles !== undefined) {
|
||||
await config.update('overwriteFiles', newConfig.overwriteFiles, vscode.ConfigurationTarget.Global);
|
||||
}
|
||||
if (newConfig.inputMode !== undefined) {
|
||||
await config.update('inputMode', newConfig.inputMode, vscode.ConfigurationTarget.Global);
|
||||
}
|
||||
if (newConfig.language !== undefined) {
|
||||
await config.update('language', newConfig.language, vscode.ConfigurationTarget.Global);
|
||||
}
|
||||
}
|
||||
61
src/core/i18n.ts
Normal file
61
src/core/i18n.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// Словари локализации
|
||||
|
||||
export const I18N_DICTIONARIES: Record<string, Record<string, string>> = {
|
||||
ru: {
|
||||
destinationPath: 'Путь назначения',
|
||||
chooseTemplate: 'Выберите шаблон',
|
||||
enterVariables: 'Введите значения переменных',
|
||||
varInputHint: 'без скобок {{ }}',
|
||||
create: 'Создать',
|
||||
selectTemplate: 'Шаблон',
|
||||
fileExistsNoOverwrite: 'Файл или папка уже существует и перезапись запрещена',
|
||||
fileExists: 'Файл или папка уже существует',
|
||||
createSuccess: 'Структура {{template}} успешно создана.',
|
||||
createError: 'Ошибка при создании структуры',
|
||||
noTemplates: 'В папке шаблонов нет шаблонов.',
|
||||
templatesNotFound: 'Папка шаблонов не найдена:',
|
||||
noFolders: 'Нет открытых папок рабочего пространства.',
|
||||
inputName: 'Введите имя для шаблона',
|
||||
},
|
||||
en: {
|
||||
destinationPath: 'Destination path',
|
||||
chooseTemplate: 'Choose template',
|
||||
enterVariables: 'Enter variables',
|
||||
varInputHint: 'without curly braces {{ }}',
|
||||
create: 'Create',
|
||||
selectTemplate: 'Template',
|
||||
fileExistsNoOverwrite: 'File or folder already exists and overwrite is disabled',
|
||||
fileExists: 'File or folder already exists',
|
||||
createSuccess: 'Structure {{template}} created successfully.',
|
||||
createError: 'Error creating structure',
|
||||
noTemplates: 'No templates found in templates folder.',
|
||||
templatesNotFound: 'Templates folder not found:',
|
||||
noFolders: 'No workspace folders open.',
|
||||
inputName: 'Enter name for template',
|
||||
}
|
||||
};
|
||||
|
||||
export const SETTINGS_I18N: Record<string, Record<string, string>> = {
|
||||
ru: {
|
||||
title: 'Настройки Template Forge',
|
||||
templatesPath: 'Путь к шаблонам:',
|
||||
overwriteFiles: 'Перезаписывать существующие файлы',
|
||||
inputMode: 'Способ ввода переменных:',
|
||||
inputModeWebview: 'Webview (форма)',
|
||||
inputModeInputBox: 'InputBox (по одной)',
|
||||
language: 'Язык интерфейса:',
|
||||
save: 'Сохранить'
|
||||
},
|
||||
en: {
|
||||
title: 'Template Forge Settings',
|
||||
templatesPath: 'Templates path:',
|
||||
overwriteFiles: 'Overwrite existing files',
|
||||
inputMode: 'Variable input method:',
|
||||
inputModeWebview: 'Webview (form)',
|
||||
inputModeInputBox: 'InputBox (one by one)',
|
||||
language: 'Interface language:',
|
||||
save: 'Save'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
66
src/core/templateResolver.ts
Normal file
66
src/core/templateResolver.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
// Каскадный поиск .templates по дереву каталогов
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { listTemplateNames } from '@gromlab/create';
|
||||
|
||||
/**
|
||||
* Собирает все доступные шаблоны по каскаду вверх от startDir до rootDir.
|
||||
* Ближайшие шаблоны имеют приоритет (shadowing).
|
||||
* Возвращает Map: имя шаблона → абсолютный путь к его директории.
|
||||
*/
|
||||
export function discoverTemplates(
|
||||
startDir: string,
|
||||
rootDir: string,
|
||||
templatesFolder: string = '.templates'
|
||||
): Map<string, string> {
|
||||
const result = new Map<string, string>();
|
||||
const resolvedRoot = path.resolve(rootDir);
|
||||
let current = path.resolve(startDir);
|
||||
|
||||
while (true) {
|
||||
const templatesDir = path.join(current, templatesFolder);
|
||||
if (fs.existsSync(templatesDir) && fs.statSync(templatesDir).isDirectory()) {
|
||||
const names = listTemplateNames(templatesDir);
|
||||
for (const name of names) {
|
||||
if (!result.has(name)) {
|
||||
result.set(name, path.join(templatesDir, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (current === resolvedRoot) break;
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) break;
|
||||
current = parent;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ищет конкретный шаблон по каскаду вверх от startDir до rootDir.
|
||||
* Возвращает абсолютный путь к первому .templates, содержащему шаблон, или undefined.
|
||||
*/
|
||||
export function resolveTemplate(
|
||||
templateName: string,
|
||||
startDir: string,
|
||||
rootDir: string,
|
||||
templatesFolder: string = '.templates'
|
||||
): string | undefined {
|
||||
const resolvedRoot = path.resolve(rootDir);
|
||||
let current = path.resolve(startDir);
|
||||
|
||||
while (true) {
|
||||
const candidate = path.join(current, templatesFolder, templateName);
|
||||
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
if (current === resolvedRoot) break;
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) break;
|
||||
current = parent;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
15
src/core/vars.ts
Normal file
15
src/core/vars.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// Сбор переменных от пользователя через InputBox
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export async function collectUserVars(baseVars: Set<string>): Promise<Record<string, string>> {
|
||||
const result: Record<string, string> = {};
|
||||
for (const v of baseVars) {
|
||||
const input = await vscode.window.showInputBox({
|
||||
prompt: `Введите значение для ${v}`,
|
||||
placeHolder: `{{${v}}}`
|
||||
});
|
||||
if (!input) throw new Error(`Значение для ${v} не введено`);
|
||||
result[v] = input;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
1145
src/extension.ts
1145
src/extension.ts
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,6 @@
|
||||
import * as assert from 'assert';
|
||||
|
||||
// You can import and use all API from the 'vscode' module
|
||||
// as well as import your extension to test it
|
||||
import * as vscode from 'vscode';
|
||||
import {
|
||||
toPascalCase,
|
||||
toCamelCase,
|
||||
toSnakeCase,
|
||||
toKebabCase,
|
||||
toScreamingSnakeCase,
|
||||
toUpperCaseFirst,
|
||||
toUpperCaseAll,
|
||||
toLowerCaseAll,
|
||||
buildVarsObject,
|
||||
CASE_MODIFIERS
|
||||
} from '../extension';
|
||||
import { CASE_MODIFIERS } from '@gromlab/create';
|
||||
|
||||
suite('Extension Test Suite', () => {
|
||||
vscode.window.showInformationMessage('Start all tests.');
|
||||
@@ -28,46 +14,9 @@ suite('Extension Test Suite', () => {
|
||||
suite('Template Variable Modifiers', () => {
|
||||
const input = 'my super-name';
|
||||
|
||||
test('toPascalCase', () => {
|
||||
assert.strictEqual(toPascalCase(input), 'MySuperName');
|
||||
});
|
||||
test('toCamelCase', () => {
|
||||
assert.strictEqual(toCamelCase(input), 'mySuperName');
|
||||
});
|
||||
test('toSnakeCase', () => {
|
||||
assert.strictEqual(toSnakeCase(input), 'my_super_name');
|
||||
});
|
||||
test('toKebabCase', () => {
|
||||
assert.strictEqual(toKebabCase(input), 'my-super-name');
|
||||
});
|
||||
test('toScreamingSnakeCase', () => {
|
||||
assert.strictEqual(toScreamingSnakeCase(input), 'MY_SUPER_NAME');
|
||||
});
|
||||
test('toUpperCaseFirst', () => {
|
||||
assert.strictEqual(toUpperCaseFirst(input), 'My super-name');
|
||||
});
|
||||
test('toUpperCaseAll', () => {
|
||||
assert.strictEqual(toUpperCaseAll(input), 'MYSUPERNAME');
|
||||
});
|
||||
test('toLowerCaseAll', () => {
|
||||
assert.strictEqual(toLowerCaseAll(input), 'mysupername');
|
||||
});
|
||||
test('CASE_MODIFIERS map covers all', () => {
|
||||
for (const [mod, fn] of Object.entries(CASE_MODIFIERS)) {
|
||||
assert.strictEqual(typeof fn(input), 'string');
|
||||
}
|
||||
});
|
||||
test('buildVarsObject generates all keys', () => {
|
||||
const vars = buildVarsObject({ name: input });
|
||||
assert.strictEqual(vars['name'], input);
|
||||
assert.strictEqual(vars['name.pascalCase'], 'MySuperName');
|
||||
assert.strictEqual(vars['name.camelCase'], 'mySuperName');
|
||||
assert.strictEqual(vars['name.snakeCase'], 'my_super_name');
|
||||
assert.strictEqual(vars['name.kebabCase'], 'my-super-name');
|
||||
assert.strictEqual(vars['name.screamingSnakeCase'], 'MY_SUPER_NAME');
|
||||
assert.strictEqual(vars['name.upperCase'], 'My super-name');
|
||||
assert.strictEqual(vars['name.lowerCase'], 'my super-name');
|
||||
assert.strictEqual(vars['name.upperCaseAll'], 'MYSUPERNAME');
|
||||
assert.strictEqual(vars['name.lowerCaseAll'], 'mysupername');
|
||||
});
|
||||
});
|
||||
|
||||
56
src/vscode/completion.ts
Normal file
56
src/vscode/completion.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// Регистрация и обработка автодополнения шаблонов
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { collectTemplateVariables, CASE_MODIFIERS } from '@gromlab/create';
|
||||
import { readConfig } from '../core/config';
|
||||
|
||||
function isInTemplatesDir(filePath: string, templatesDir: string): boolean {
|
||||
const rel = path.relative(templatesDir, filePath);
|
||||
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
||||
}
|
||||
|
||||
export function registerTemplateCompletionAndHighlight(context: vscode.ExtensionContext) {
|
||||
const completionProvider = {
|
||||
provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {
|
||||
const config = readConfig();
|
||||
const templatesPath = config.templatesPath || '.templates';
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) return;
|
||||
const templatesDir = path.join(workspaceFolders[0].uri.fsPath, templatesPath);
|
||||
if (!isInTemplatesDir(document.uri.fsPath, templatesDir)) {
|
||||
return undefined;
|
||||
}
|
||||
const line = document.lineAt(position).text;
|
||||
const textBefore = line.slice(0, position.character);
|
||||
const match = /{{\s*([\w]+)?(?:\.([\w]*))?[^}]*$/.exec(textBefore);
|
||||
if (!match) return undefined;
|
||||
const allVars = collectTemplateVariables(templatesDir);
|
||||
const items = [];
|
||||
if (match[2] !== undefined) {
|
||||
for (const mod of Object.keys(CASE_MODIFIERS)) {
|
||||
if (!match[2] || mod.startsWith(match[2])) {
|
||||
const item = new vscode.CompletionItem(mod, vscode.CompletionItemKind.EnumMember);
|
||||
item.insertText = mod;
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const v of allVars) {
|
||||
if (!match[1] || v.startsWith(match[1])) {
|
||||
const item = new vscode.CompletionItem(v, vscode.CompletionItemKind.Variable);
|
||||
item.insertText = v;
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
};
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
'*',
|
||||
completionProvider,
|
||||
'{', '.'
|
||||
)
|
||||
);
|
||||
}
|
||||
108
src/vscode/decorations.ts
Normal file
108
src/vscode/decorations.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
// Декорации и диагностика шаблонов
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { readConfig } from '../core/config';
|
||||
|
||||
const bracketDecoration = vscode.window.createTextEditorDecorationType({
|
||||
color: '#43A047', // зелёный для скобок
|
||||
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
|
||||
fontWeight: 'bold'
|
||||
});
|
||||
const variableDecoration = vscode.window.createTextEditorDecorationType({
|
||||
color: '#FF9800', // оранжевый для имени переменной
|
||||
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
|
||||
fontWeight: 'bold'
|
||||
});
|
||||
const modifierDecoration = vscode.window.createTextEditorDecorationType({
|
||||
color: '#00ACC1', // бирюзовый для модификатора
|
||||
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
|
||||
fontWeight: 'bold'
|
||||
});
|
||||
|
||||
function isInTemplatesDir(filePath: string, templatesDir: string): boolean {
|
||||
const rel = path.relative(templatesDir, filePath);
|
||||
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
||||
}
|
||||
|
||||
function updateTemplateDecorations(editor: vscode.TextEditor) {
|
||||
if (!editor) return;
|
||||
const config = readConfig();
|
||||
const templatesPath = config.templatesPath || 'templates';
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) return;
|
||||
const templatesDir = path.join(workspaceFolders[0].uri.fsPath, templatesPath);
|
||||
if (!isInTemplatesDir(editor.document.uri.fsPath, templatesDir)) return;
|
||||
const brackets: vscode.DecorationOptions[] = [];
|
||||
const variables: vscode.DecorationOptions[] = [];
|
||||
const modifiers: vscode.DecorationOptions[] = [];
|
||||
for (let lineNum = 0; lineNum < editor.document.lineCount; lineNum++) {
|
||||
const line = editor.document.lineAt(lineNum).text;
|
||||
// Ищем все {{variable.modifier}} или {{variable}}
|
||||
const reg = /{{\s*([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_]+))?\s*}}/g;
|
||||
let match;
|
||||
while ((match = reg.exec(line)) !== null) {
|
||||
const start = match.index;
|
||||
const end = start + match[0].length;
|
||||
// Скобки {{ и }}
|
||||
brackets.push({
|
||||
range: new vscode.Range(lineNum, start, lineNum, start + 2)
|
||||
});
|
||||
brackets.push({
|
||||
range: new vscode.Range(lineNum, end - 2, lineNum, end)
|
||||
});
|
||||
// Имя переменной
|
||||
const varStart = start + 2 + line.slice(start + 2).search(/\S/); // после {{
|
||||
variables.push({
|
||||
range: new vscode.Range(lineNum, varStart, lineNum, varStart + match[1].length)
|
||||
});
|
||||
// Модификатор (если есть)
|
||||
if (match[2]) {
|
||||
const modStart = varStart + match[1].length + 1; // +1 за точку
|
||||
modifiers.push({
|
||||
range: new vscode.Range(lineNum, modStart, lineNum, modStart + match[2].length)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
editor.setDecorations(bracketDecoration, brackets);
|
||||
editor.setDecorations(variableDecoration, variables);
|
||||
editor.setDecorations(modifierDecoration, modifiers);
|
||||
}
|
||||
|
||||
export function registerTemplateDecorations(context: vscode.ExtensionContext) {
|
||||
function decorateActiveEditor() {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor) {
|
||||
updateTemplateDecorations(editor);
|
||||
}
|
||||
}
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.onDidChangeActiveTextEditor(editor => {
|
||||
if (editor) decorateActiveEditor();
|
||||
}),
|
||||
vscode.workspace.onDidChangeTextDocument(event => {
|
||||
const editor = vscode.window.visibleTextEditors.find(e => e.document === event.document);
|
||||
if (editor) updateTemplateDecorations(editor);
|
||||
})
|
||||
);
|
||||
// Инициализация при активации
|
||||
setTimeout(() => {
|
||||
vscode.window.visibleTextEditors.forEach(editor => updateTemplateDecorations(editor));
|
||||
}, 300);
|
||||
}
|
||||
|
||||
export function decorateActiveEditor() {
|
||||
// Логика декорирования активного редактора
|
||||
// ...
|
||||
}
|
||||
|
||||
export function clearDiagnosticsForEditor(editor: vscode.TextEditor, templatesDir: string) {
|
||||
// Очистка диагностик для редактора
|
||||
// ...
|
||||
}
|
||||
|
||||
export function clearDiagnosticsForTemplates(context: vscode.ExtensionContext) {
|
||||
// Очистка диагностик для всех шаблонов
|
||||
// ...
|
||||
}
|
||||
65
src/vscode/semanticHighlight.ts
Normal file
65
src/vscode/semanticHighlight.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
// Семантическая подсветка шаблонов
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { readConfig } from '../core/config';
|
||||
|
||||
function isInTemplatesDir(filePath: string, templatesDir: string): boolean {
|
||||
const rel = path.relative(templatesDir, filePath);
|
||||
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
||||
}
|
||||
|
||||
export function registerTemplateSemanticHighlight(context: vscode.ExtensionContext) {
|
||||
const legend = new vscode.SemanticTokensLegend(['bracket', 'variable', 'modifier']);
|
||||
const disposable = vscode.languages.registerDocumentSemanticTokensProvider(
|
||||
{ pattern: '**' }, // теперь на все файлы
|
||||
{
|
||||
provideDocumentSemanticTokens(document: any) {
|
||||
const config = readConfig();
|
||||
const templatesPath = config.templatesPath || 'templates';
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) return;
|
||||
const templatesDir = path.join(workspaceFolders[0].uri.fsPath, templatesPath);
|
||||
// Проверяем, что файл в папке шаблонов
|
||||
if (!isInTemplatesDir(document.uri.fsPath, templatesDir)) {
|
||||
return;
|
||||
}
|
||||
const tokens: number[] = [];
|
||||
for (let lineNum = 0; lineNum < document.lineCount; lineNum++) {
|
||||
const line = document.lineAt(lineNum).text;
|
||||
// Ищем все {{variable.modifier}} или {{variable}} или {{variable.}}
|
||||
const reg = /({{)|(}})|{{\s*([a-zA-Z0-9_]+)(?:\.(\w*))?\s*}}/g;
|
||||
let match;
|
||||
while ((match = reg.exec(line)) !== null) {
|
||||
if (match[1]) {
|
||||
// {{
|
||||
tokens.push(lineNum, match.index, 2, 0, 0); // bracket
|
||||
} else if (match[2]) {
|
||||
// }}
|
||||
tokens.push(lineNum, match.index, 2, 0, 0); // bracket
|
||||
} else if (match[3]) {
|
||||
// variable (имя)
|
||||
const varStart = match.index + 2 + line.slice(match.index + 2).search(/\S/); // после {{
|
||||
tokens.push(lineNum, varStart, match[3].length, 1, 0); // variable
|
||||
if (typeof match[4] === 'string') {
|
||||
// Если есть точка, но модификатор не введён ({{name.}})
|
||||
if (match[4] === '') {
|
||||
// Подсвечиваем только точку как variable
|
||||
const dotStart = varStart + match[3].length;
|
||||
tokens.push(lineNum, dotStart, 1, 1, 0); // variable (точка)
|
||||
} else if (match[4]) {
|
||||
// .modifier
|
||||
const modStart = varStart + match[3].length + 1; // +1 за точку
|
||||
tokens.push(lineNum, modStart, match[4].length, 2, 0); // modifier
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new vscode.SemanticTokens(new Uint32Array(tokens));
|
||||
}
|
||||
},
|
||||
legend
|
||||
);
|
||||
context.subscriptions.push(disposable);
|
||||
return disposable;
|
||||
}
|
||||
139
src/webview/configWebview.ts
Normal file
139
src/webview/configWebview.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
// Webview для конфигурации расширения
|
||||
import * as vscode from 'vscode';
|
||||
import { TemplateFileGeneratorConfig, readConfig, writeConfig } from '../core/config';
|
||||
|
||||
const LOCALIZATION: Record<'ru'|'en', {
|
||||
title: string;
|
||||
templatesPath: string;
|
||||
overwriteFiles: string;
|
||||
inputMode: string;
|
||||
inputBox: string;
|
||||
webview: string;
|
||||
language: string;
|
||||
save: string;
|
||||
russian: string;
|
||||
english: string;
|
||||
}> = {
|
||||
ru: {
|
||||
title: 'Настройки генератора шаблонов',
|
||||
templatesPath: 'Путь к шаблонам:',
|
||||
overwriteFiles: 'Перезаписывать файлы',
|
||||
inputMode: 'Режим ввода:',
|
||||
inputBox: 'InputBox',
|
||||
webview: 'Webview',
|
||||
language: 'Язык:',
|
||||
save: 'Сохранить',
|
||||
russian: 'Русский',
|
||||
english: 'English',
|
||||
},
|
||||
en: {
|
||||
title: 'Template Forge Settings',
|
||||
templatesPath: 'Templates path:',
|
||||
overwriteFiles: 'Overwrite files',
|
||||
inputMode: 'Input mode:',
|
||||
inputBox: 'InputBox',
|
||||
webview: 'Webview',
|
||||
language: 'Language:',
|
||||
save: 'Save',
|
||||
russian: 'Russian',
|
||||
english: 'English',
|
||||
}
|
||||
};
|
||||
|
||||
export async function showConfigWebview(context: vscode.ExtensionContext) {
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'templateFileGeneratorConfig',
|
||||
'Template Forge Settings',
|
||||
vscode.ViewColumn.One,
|
||||
{ enableScripts: true }
|
||||
);
|
||||
let config = readConfig();
|
||||
// Стили теперь лежат в media/styles.css (папка для статики)
|
||||
const stylePath = vscode.Uri.joinPath(context.extensionUri, 'media', 'styles.css');
|
||||
const styleUri = panel.webview.asWebviewUri(stylePath);
|
||||
setHtml((config.language === 'en' ? 'en' : 'ru'));
|
||||
|
||||
function setHtml(language: 'ru'|'en') {
|
||||
panel.webview.html = getHtml(language);
|
||||
}
|
||||
|
||||
function getHtml(language: 'ru'|'en'): string {
|
||||
const dict = LOCALIZATION[language] || LOCALIZATION['ru'];
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="${language}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${panel.webview.cspSource}; script-src 'unsafe-inline';">
|
||||
<title>${dict.title}</title>
|
||||
<link rel="stylesheet" href="${styleUri}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="config-container">
|
||||
<h2>${dict.title}</h2>
|
||||
<form id="configForm">
|
||||
<div class="form-group">
|
||||
<label>${dict.templatesPath}
|
||||
<input name="templatesPath" value="${config.templatesPath}" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" name="overwriteFiles" ${config.overwriteFiles ? 'checked' : ''}/>
|
||||
${dict.overwriteFiles}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${dict.inputMode}
|
||||
<select name="inputMode">
|
||||
<option value="inputBox" ${config.inputMode === 'inputBox' ? 'selected' : ''}>${dict.inputBox}</option>
|
||||
<option value="webview" ${config.inputMode === 'webview' ? 'selected' : ''}>${dict.webview}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${dict.language}
|
||||
<select name="language" id="languageSelect">
|
||||
<option value="ru" ${language === 'ru' ? 'selected' : ''}>${dict.russian}</option>
|
||||
<option value="en" ${language === 'en' ? 'selected' : ''}>${dict.english}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn">${dict.save}</button>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
document.getElementById('configForm').onsubmit = function(e) {
|
||||
e.preventDefault();
|
||||
const data = Object.fromEntries(new FormData(this));
|
||||
data.overwriteFiles = !!this.overwriteFiles.checked;
|
||||
vscode.postMessage({ type: 'save', data });
|
||||
};
|
||||
document.getElementById('languageSelect').onchange = function(e) {
|
||||
vscode.postMessage({ type: 'setLanguage', language: this.value });
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async msg => {
|
||||
if (msg.type === 'save') {
|
||||
await writeConfig(msg.data);
|
||||
vscode.window.setStatusBarMessage('Настройки сохранены!', 3000);
|
||||
panel.dispose();
|
||||
}
|
||||
if (msg.type === 'setLanguage') {
|
||||
// Сохраняем язык в конфиг и перерисовываем webview
|
||||
config.language = msg.language;
|
||||
await writeConfig(config);
|
||||
setHtml(msg.language === 'en' ? 'en' : 'ru');
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
context.subscriptions
|
||||
);
|
||||
}
|
||||
124
src/webview/styles.css
Normal file
124
src/webview/styles.css
Normal file
@@ -0,0 +1,124 @@
|
||||
:root {
|
||||
--bg: #f7f7fa;
|
||||
--panel-bg: #fff;
|
||||
--text: #222;
|
||||
--label: #555;
|
||||
--input-bg: #f0f0f3;
|
||||
--input-border: #d0d0d7;
|
||||
--input-focus: #1976d2;
|
||||
--button-bg: #1976d2;
|
||||
--button-text: #fff;
|
||||
--button-hover: #1565c0;
|
||||
--border-radius: 8px;
|
||||
--shadow: 0 2px 12px rgba(0,0,0,0.07);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #181a1b;
|
||||
--panel-bg: #23272e;
|
||||
--text: #f3f3f3;
|
||||
--label: #b0b0b0;
|
||||
--input-bg: #23272e;
|
||||
--input-border: #33363b;
|
||||
--input-focus: #90caf9;
|
||||
--button-bg: #1976d2;
|
||||
--button-text: #fff;
|
||||
--button-hover: #1565c0;
|
||||
--border-radius: 8px;
|
||||
--shadow: 0 2px 12px rgba(0,0,0,0.25);
|
||||
}
|
||||
}
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: 'Segoe UI', 'Roboto', Arial, sans-serif;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.create-container, .config-container {
|
||||
max-width: 420px;
|
||||
margin: 48px auto;
|
||||
background: var(--panel-bg);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 32px 36px 28px 36px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.head-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.create-container h2, .config-container h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
label {
|
||||
color: var(--label);
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
}
|
||||
input, select {
|
||||
background: var(--input-bg);
|
||||
color: var(--text);
|
||||
border: 1.5px solid var(--input-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 8px 10px;
|
||||
font-size: 1em;
|
||||
transition: border 0.2s, box-shadow 0.2s;
|
||||
outline: none;
|
||||
}
|
||||
input:focus, select:focus {
|
||||
border-color: var(--input-focus);
|
||||
box-shadow: 0 0 0 2px var(--input-focus)33;
|
||||
}
|
||||
button, .btn {
|
||||
margin-top: 10px;
|
||||
background: var(--button-bg);
|
||||
color: var(--button-text);
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 10px 15px;
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, box-shadow 0.2s;
|
||||
box-shadow: 0 1px 4px rgba(25, 118, 210, 0.08);
|
||||
}
|
||||
button:hover, button:focus, .btn:hover, .btn:focus {
|
||||
background: var(--button-hover);
|
||||
}
|
||||
.destination {
|
||||
margin-bottom: 16px;
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
}
|
||||
.lang-select {
|
||||
display: block;
|
||||
}
|
||||
.var-hint {
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.template-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#configForm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
173
src/webview/templateVarsWebview.ts
Normal file
173
src/webview/templateVarsWebview.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
// Webview для выбора шаблона и переменных
|
||||
import * as vscode from 'vscode';
|
||||
import { collectTemplateVariables } from '@gromlab/create';
|
||||
import { I18N_DICTIONARIES } from '../core/i18n';
|
||||
import { writeConfig, readConfig } from '../core/config';
|
||||
|
||||
export async function showTemplateAndVarsWebview(
|
||||
context: vscode.ExtensionContext,
|
||||
templatesMap: Map<string, string>,
|
||||
targetPath: string,
|
||||
initialLanguage: string
|
||||
): Promise<{ template: string, vars: Record<string, string> } | undefined> {
|
||||
let language = initialLanguage;
|
||||
function getDict() {
|
||||
return I18N_DICTIONARIES[language] || I18N_DICTIONARIES['ru'];
|
||||
}
|
||||
const templateNames = Array.from(templatesMap.keys());
|
||||
const stylePath = vscode.Uri.joinPath(context.extensionUri, 'media', 'styles.css');
|
||||
return new Promise((resolve) => {
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'templateVars',
|
||||
getDict().create,
|
||||
vscode.ViewColumn.Active,
|
||||
{ enableScripts: true }
|
||||
);
|
||||
const styleUri = panel.webview.asWebviewUri(stylePath);
|
||||
let currentVars: string[] = [];
|
||||
let currentTemplate = templateNames[0] || '';
|
||||
let disposed = false;
|
||||
|
||||
function getVarsForTemplate(name: string): string[] {
|
||||
const dir = templatesMap.get(name);
|
||||
if (!dir) return [];
|
||||
return Array.from(collectTemplateVariables(dir));
|
||||
}
|
||||
|
||||
function getVarsHtml(vars: string[], values: Record<string, string> = {}) {
|
||||
const dict = getDict();
|
||||
if (!vars.length) return '';
|
||||
return `<h3>${dict.enterVariables}</h3>
|
||||
<div class="var-hint">${dict.varInputHint}</div>
|
||||
<form id="varsForm">
|
||||
${vars.map(v => `
|
||||
<label><input name="${v}" placeholder="{{${v}}}" value="${values[v] || ''}" required /></label><br/><br/>
|
||||
`).join('')}
|
||||
<button type="submit" class="btn">${dict.create}</button>
|
||||
</form>`;
|
||||
}
|
||||
function getTemplatesRadioHtml(templates: string[], selected: string) {
|
||||
const dict = getDict();
|
||||
return `<form id="templateForm">
|
||||
<h3>${dict.chooseTemplate}:</h3>
|
||||
<div class="template-list">
|
||||
${templates.map(t => `
|
||||
<label><input type="radio" name="templateRadio" value="${t}" ${selected === t ? 'checked' : ''}/> ${t}</label>
|
||||
`).join('')}
|
||||
</div>
|
||||
</form>`;
|
||||
}
|
||||
function getLanguageSelectorHtml(selected: string) {
|
||||
return `<label class="lang-select">
|
||||
<select id="languageSelect">
|
||||
<option value="ru" ${selected === 'ru' ? 'selected' : ''}>Русский</option>
|
||||
<option value="en" ${selected === 'en' ? 'selected' : ''}>English</option>
|
||||
</select>
|
||||
</label>`;
|
||||
}
|
||||
function setHtml(templatesHtml: string, varsHtml: string) {
|
||||
const dict = getDict();
|
||||
panel.webview.html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="${language}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${panel.webview.cspSource}; script-src 'unsafe-inline';">
|
||||
<title>${dict.create}</title>
|
||||
<link rel="stylesheet" href="${styleUri}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="create-container">
|
||||
<div class="head-wrap">
|
||||
<h2>${dict.create}</h2>
|
||||
${getLanguageSelectorHtml(language)}
|
||||
</div>
|
||||
<div id="templatesBlock">
|
||||
${templatesHtml}
|
||||
</div>
|
||||
<div id="varsBlock">
|
||||
${varsHtml}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
const vscode = acquireVsCodeApi();
|
||||
function initHandlers() {
|
||||
const templateRadios = document.querySelectorAll('input[name="templateRadio"]');
|
||||
templateRadios.forEach(radio => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
vscode.postMessage({ type: 'selectTemplate', template: e.target.value, language: document.getElementById('languageSelect').value });
|
||||
});
|
||||
});
|
||||
const varsForm = document.getElementById('varsForm');
|
||||
if (varsForm) {
|
||||
varsForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const data = {};
|
||||
Array.from(varsForm.elements).forEach(el => {
|
||||
if (el.name) data[el.name] = el.value;
|
||||
});
|
||||
vscode.postMessage({ type: 'submit', template: document.querySelector('input[name="templateRadio"]:checked')?.value || '', data, language: document.getElementById('languageSelect').value });
|
||||
});
|
||||
}
|
||||
const langSel = document.getElementById('languageSelect');
|
||||
if (langSel) {
|
||||
langSel.addEventListener('change', (e) => {
|
||||
vscode.postMessage({ type: 'setLanguage', language: e.target.value, template: document.querySelector('input[name="templateRadio"]:checked')?.value || '' });
|
||||
});
|
||||
}
|
||||
}
|
||||
window.initHandlers = initHandlers;
|
||||
document.addEventListener('DOMContentLoaded', initHandlers);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
setTimeout(() => {
|
||||
panel.webview.postMessage({ type: 'callInitHandlers' });
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
currentVars = getVarsForTemplate(currentTemplate);
|
||||
setHtml(getTemplatesRadioHtml(templateNames, currentTemplate), getVarsHtml(currentVars));
|
||||
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async message => {
|
||||
if (message.type === 'selectTemplate') {
|
||||
currentTemplate = message.template;
|
||||
if (message.language) language = message.language;
|
||||
if (!currentTemplate) {
|
||||
setHtml(getTemplatesRadioHtml(templateNames, ''), '');
|
||||
return;
|
||||
}
|
||||
currentVars = getVarsForTemplate(currentTemplate);
|
||||
setHtml(getTemplatesRadioHtml(templateNames, currentTemplate), getVarsHtml(currentVars));
|
||||
} else if (message.type === 'setLanguage') {
|
||||
if (message.language) language = message.language;
|
||||
const oldConfig = readConfig();
|
||||
await writeConfig({ ...oldConfig, language });
|
||||
currentTemplate = message.template || templateNames[0] || '';
|
||||
currentVars = getVarsForTemplate(currentTemplate);
|
||||
setHtml(getTemplatesRadioHtml(templateNames, currentTemplate), getVarsHtml(currentVars));
|
||||
} else if (message.type === 'submit') {
|
||||
if (message.language) language = message.language;
|
||||
if (!disposed) {
|
||||
disposed = true;
|
||||
panel.dispose();
|
||||
resolve({ template: message.template, vars: message.data });
|
||||
}
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
context.subscriptions
|
||||
);
|
||||
panel.onDidDispose(() => {
|
||||
if (!disposed) {
|
||||
disposed = true;
|
||||
resolve(undefined);
|
||||
}
|
||||
}, null, context.subscriptions);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user