Skip to content

Commit 5eb536c

Browse files
committed
feat: Добавление интерфейса MetadataItem и команды быстрой навигации по метаданным
1 parent fe41908 commit 5eb536c

File tree

5 files changed

+360
-2
lines changed

5 files changed

+360
-2
lines changed

package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@
109109
{
110110
"command": "metadataViewer.clearFilter",
111111
"title": "%1c-metadata-viewer.clearFilter.title%"
112+
},
113+
{
114+
"command": "metadataViewer.quickNavigateMetadata",
115+
"title": "Перейти к объекту метаданных",
116+
"category": "1C"
112117
}
113118
],
114119
"menus": {
@@ -218,6 +223,14 @@
218223
}
219224
}
220225
}
226+
],
227+
"keybindings": [
228+
{
229+
"command": "metadataViewer.quickNavigateMetadata",
230+
"key": "ctrl+alt+m",
231+
"mac": "cmd+alt+m",
232+
"when": "editorTextFocus || editorFocus || viewFocus || true"
233+
}
221234
]
222235
},
223236
"scripts": {
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
import * as vscode from 'vscode';
2+
import { MetadataView } from '../metadataView';
3+
import { MetadataItem } from '../model/metadataItem';
4+
import { TreeItem } from '../ConfigurationFormats/utils';
5+
6+
export class QuickNavigateMetadataCommand {
7+
private metadataView: MetadataView;
8+
9+
constructor(metadataView: MetadataView) {
10+
this.metadataView = metadataView;
11+
}
12+
13+
public async execute() {
14+
// Получаем все метаданные из просмотрщика
15+
const allMetadata = await this.getAllMetadataItems();
16+
17+
if (!allMetadata || allMetadata.length === 0) {
18+
vscode.window.showInformationMessage('Метаданные не найдены. Убедитесь, что открыт проект 1С.');
19+
return;
20+
}
21+
22+
// Создаем интерфейс быстрого выбора
23+
const quickPick = vscode.window.createQuickPick<MetadataQuickPickItem>();
24+
quickPick.placeholder = 'Введите часть названия объекта метаданных (например, "общего назначения")';
25+
quickPick.matchOnDescription = true;
26+
quickPick.matchOnDetail = true;
27+
28+
// Начальный список всех элементов
29+
const allItems = allMetadata.map(item => this.createQuickPickItem(item));
30+
quickPick.items = allItems;
31+
32+
// Обработчик изменения текста поиска
33+
quickPick.onDidChangeValue(() => {
34+
if (quickPick.value.length > 0) {
35+
const searchTerms = quickPick.value.toLowerCase().split(' ');
36+
quickPick.items = allItems.filter(item => {
37+
return this.matchesSearchTerms(item, searchTerms);
38+
});
39+
} else {
40+
quickPick.items = allItems;
41+
}
42+
});
43+
44+
// Обработчик выбора элемента
45+
quickPick.onDidAccept(async () => {
46+
const selectedItem = quickPick.selectedItems[0];
47+
if (selectedItem) {
48+
quickPick.hide();
49+
50+
// Находим элемент метаданных, который был выбран
51+
const metadataItem = selectedItem.metadata;
52+
53+
// Открываем элемент в просмотрщике метаданных
54+
await this.openMetadataItem(metadataItem);
55+
}
56+
});
57+
58+
// Показываем интерфейс быстрого выбора
59+
quickPick.show();
60+
}
61+
62+
private async getAllMetadataItems(): Promise<MetadataItem[]> {
63+
// Получаем все элементы метаданных
64+
const treeItems = await this.getTreeItems();
65+
return this.convertTreeItemsToMetadataItems(treeItems);
66+
}
67+
68+
private async getTreeItems(): Promise<TreeItem[]> {
69+
// Проверяем наличие dataProvider в metadataView
70+
if (!this.metadataView.dataProvider) {
71+
return [];
72+
}
73+
74+
// Получаем корневые элементы через dataProvider
75+
const rootItems = await this.metadataView.dataProvider.getChildren();
76+
if (!rootItems || rootItems.length === 0) {
77+
return [];
78+
}
79+
80+
// Ищем конфигурации (обычно первый элемент)
81+
const configurationsItem = rootItems[0];
82+
if (!configurationsItem) {
83+
return [];
84+
}
85+
86+
// Получаем дочерние элементы конфигураций
87+
const configChildren = await this.metadataView.dataProvider.getChildren(configurationsItem);
88+
return configChildren || [];
89+
}
90+
91+
private convertTreeItemsToMetadataItems(items: TreeItem[], parent?: MetadataItem): MetadataItem[] {
92+
const result: MetadataItem[] = [];
93+
94+
items.forEach(item => {
95+
// Преобразуем TreeItem в MetadataItem
96+
const metadataItem: MetadataItem = {
97+
id: item.id || '',
98+
name: typeof item.label === 'string' ? item.label : item.label?.label || '',
99+
type: item.contextValue || '',
100+
file: item.path,
101+
parent: parent
102+
};
103+
104+
result.push(metadataItem);
105+
106+
// Рекурсивно обрабатываем дочерние элементы
107+
if (item.children && item.children.length > 0) {
108+
const childItems = this.convertTreeItemsToMetadataItems(item.children, metadataItem);
109+
metadataItem.children = childItems;
110+
result.push(...childItems);
111+
}
112+
});
113+
114+
return result;
115+
}
116+
117+
private createQuickPickItem(metadata: MetadataItem): MetadataQuickPickItem {
118+
// Формируем путь для отображения
119+
const path = this.getMetadataPath(metadata);
120+
121+
return {
122+
label: metadata.name,
123+
description: path,
124+
detail: metadata.type,
125+
metadata: metadata
126+
};
127+
}
128+
129+
private getMetadataPath(metadata: MetadataItem): string {
130+
// Формируем путь в виде "Тип\Имя\Подтип"
131+
let current: MetadataItem | undefined = metadata;
132+
const pathParts: string[] = [];
133+
134+
while (current) {
135+
pathParts.unshift(current.name);
136+
current = current.parent;
137+
}
138+
139+
return pathParts.join('\\');
140+
}
141+
142+
private matchesSearchTerms(item: MetadataQuickPickItem, searchTerms: string[]): boolean {
143+
// Собираем все возможные варианты названия
144+
const itemLabel = item.label.toLowerCase();
145+
const itemDescription = item.description?.toLowerCase() || '';
146+
const itemDetail = item.detail?.toLowerCase() || '';
147+
148+
// Разбиваем название метаданных на части (например, "ОбщегоНазначенияБТС" -> "общего", "назначения", "бтс")
149+
const nameParts: string[] = [];
150+
151+
// Разбиваем CamelCase строки
152+
const splitCamelCase = (text: string): string[] => {
153+
// Преобразуем кириллицу из CamelCase в отдельные слова
154+
return text.replace(/([а-яА-Я])(?=[А-Я])/g, '$1 ').toLowerCase().split(' ');
155+
};
156+
157+
// Получаем части имени из различных источников
158+
const labelParts = splitCamelCase(item.label);
159+
nameParts.push(...labelParts);
160+
161+
// Добавляем полное имя метаданных (например "CommonModule.ОбщегоНазначения")
162+
if (itemDescription.includes('\\')) {
163+
const parts = itemDescription.split('\\');
164+
for (const part of parts) {
165+
// Разбиваем каждую часть пути на слова
166+
nameParts.push(...splitCamelCase(part));
167+
168+
// Добавляем также полные пути для поиска
169+
nameParts.push(part);
170+
}
171+
}
172+
173+
// Добавляем тип метаданных (для поиска по типу)
174+
if (itemDetail) {
175+
nameParts.push(itemDetail);
176+
}
177+
178+
// Собираем все строки для поиска
179+
const textToSearch = [
180+
// Полное название элемента
181+
itemLabel,
182+
// Все части для сопоставления с сокращениями
183+
...nameParts,
184+
// Полный путь
185+
itemDescription,
186+
// Тип
187+
itemDetail
188+
].join(' ').toLowerCase();
189+
190+
// Проверяем, содержится ли каждый поисковый термин в строке для поиска
191+
// или соответствует ли он началу какой-либо части названия (для сокращений)
192+
return searchTerms.every(term => {
193+
// Прямое вхождение в какую-либо строку поиска
194+
if (textToSearch.includes(term)) {
195+
return true;
196+
}
197+
198+
// Проверка на сокращения (например "общ" -> "общего")
199+
// Ищем части, которые начинаются с поискового термина
200+
for (const part of nameParts) {
201+
if (part.startsWith(term)) {
202+
return true;
203+
}
204+
}
205+
206+
// Также проверяем совпадение с транслитерацией
207+
// Например, "obsh" -> "общего"
208+
const translitMap: {[key: string]: string} = {
209+
'a': 'а', 'b': 'б', 'v': 'в', 'g': 'г', 'd': 'д', 'e': 'е', 'yo': 'ё', 'zh': 'ж',
210+
'z': 'з', 'i': 'и', 'j': 'й', 'k': 'к', 'l': 'л', 'm': 'м', 'n': 'н', 'o': 'о',
211+
'p': 'п', 'r': 'р', 's': 'с', 't': 'т', 'u': 'у', 'f': 'ф', 'h': 'х', 'ts': 'ц',
212+
'ch': 'ч', 'sh': 'ш', 'sch': 'щ', 'y': 'ы', 'yu': 'ю', 'ya': 'я'
213+
};
214+
215+
// Преобразуем латинский поисковый термин в кириллицу для поиска
216+
let translitTerm = term;
217+
for (const [latin, cyrillic] of Object.entries(translitMap)) {
218+
translitTerm = translitTerm.replace(new RegExp(latin, 'g'), cyrillic);
219+
}
220+
221+
if (translitTerm !== term && textToSearch.includes(translitTerm)) {
222+
return true;
223+
}
224+
225+
return false;
226+
});
227+
}
228+
229+
private async openMetadataItem(metadata: MetadataItem): Promise<void> {
230+
// Если у элемента есть путь к файлу, открываем его
231+
if (metadata.file) {
232+
try {
233+
const document = await vscode.workspace.openTextDocument(metadata.file);
234+
await vscode.window.showTextDocument(document);
235+
} catch (error) {
236+
console.error(`Ошибка при открытии файла ${metadata.file}:`, error);
237+
}
238+
}
239+
240+
// Находим соответствующий TreeItem в дереве
241+
// Используем dataProvider для обхода дерева
242+
const treeItem = await this.findTreeItemById(metadata.id);
243+
244+
if (treeItem) {
245+
// Выполняем команду для открытия соответствующего модуля
246+
// в зависимости от типа элемента метаданных
247+
await this.executeCommandForTreeItem(treeItem);
248+
249+
// Выделяем элемент в дереве метаданных
250+
await this.revealTreeItem(treeItem);
251+
}
252+
}
253+
254+
private async findTreeItemById(id: string): Promise<TreeItem | undefined> {
255+
if (!this.metadataView.dataProvider) {
256+
return undefined;
257+
}
258+
259+
// Получаем корневые элементы
260+
const rootItems = await this.metadataView.dataProvider.getChildren();
261+
if (!rootItems || rootItems.length === 0) {
262+
return undefined;
263+
}
264+
265+
// Ищем элемент по ID с помощью рекурсивной функции
266+
return this.findTreeItemByIdRecursive(rootItems, id);
267+
}
268+
269+
private async findTreeItemByIdRecursive(items: TreeItem[], id: string): Promise<TreeItem | undefined> {
270+
for (const item of items) {
271+
if (item.id === id) {
272+
return item;
273+
}
274+
275+
if (item.children && item.children.length > 0) {
276+
const found = await this.findTreeItemByIdRecursive(item.children, id);
277+
if (found) {
278+
return found;
279+
}
280+
} else if (this.metadataView.dataProvider) {
281+
// Если у элемента нет детей в памяти, попробуем получить их через dataProvider
282+
const children = await this.metadataView.dataProvider.getChildren(item);
283+
if (children && children.length > 0) {
284+
const found = await this.findTreeItemByIdRecursive(children, id);
285+
if (found) {
286+
return found;
287+
}
288+
}
289+
}
290+
}
291+
292+
return undefined;
293+
}
294+
295+
private async executeCommandForTreeItem(item: TreeItem): Promise<void> {
296+
// Выполняем соответствующую команду в зависимости от типа элемента
297+
if (item.command) {
298+
if (typeof item.command === 'string') {
299+
await vscode.commands.executeCommand(item.command);
300+
} else if (item.command.command) {
301+
await vscode.commands.executeCommand(item.command.command, ...(item.command.arguments || []));
302+
}
303+
} else if (item.contextValue) {
304+
// Определяем, какую команду выполнить на основе contextValue
305+
if (item.contextValue.includes('form')) {
306+
await vscode.commands.executeCommand('metadataViewer.openForm', item);
307+
} else if (item.contextValue.includes('object')) {
308+
await vscode.commands.executeCommand('metadataViewer.openObjectModule', item);
309+
} else if (item.contextValue.includes('manager')) {
310+
await vscode.commands.executeCommand('metadataViewer.openManagerModule', item);
311+
} else if (item.contextValue.includes('module')) {
312+
await vscode.commands.executeCommand('metadataViewer.openModule', item);
313+
}
314+
}
315+
}
316+
317+
private async revealTreeItem(item: TreeItem): Promise<void> {
318+
// Прокручиваем дерево к выбранному элементу
319+
await vscode.commands.executeCommand('metadataView.reveal', item);
320+
}
321+
}
322+
323+
// Интерфейс для элементов быстрого выбора
324+
interface MetadataQuickPickItem extends vscode.QuickPickItem {
325+
metadata: MetadataItem;
326+
}

src/extension.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { MetadataView } from './metadataView';
55
import * as fs from 'fs';
66
import { FormPreviewer } from './formPreviewer';
77
import { TreeItem } from './ConfigurationFormats/utils';
8+
import { QuickNavigateMetadataCommand } from './commands/quickNavigateMetadata';
89

910
export function activate(context: vscode.ExtensionContext) {
1011
vscode.commands.registerCommand('metadataViewer.openAppModule', (node: TreeItem) => {
@@ -136,7 +137,17 @@ export function activate(context: vscode.ExtensionContext) {
136137
OpenFile(filePath);
137138
});
138139

139-
new MetadataView(context);
140+
const metadataView = new MetadataView(context);
141+
142+
// Регистрация команды быстрой навигации по метаданным
143+
context.subscriptions.push(
144+
vscode.commands.registerCommand('metadataViewer.quickNavigateMetadata', () => {
145+
const command = new QuickNavigateMetadataCommand(metadataView);
146+
command.execute().catch(error => {
147+
vscode.window.showErrorMessage(`Ошибка выполнения команды: ${error.message}`);
148+
});
149+
})
150+
);
140151
}
141152

142153
function OpenFile(filePath: string) {

src/metadataView.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class MetadataView {
5959
panel: vscode.WebviewPanel | undefined = undefined;
6060
// Фильтр нужен по каждой конфигурации отдельно
6161
subsystemFilter: { id: string; objects: string[] }[] = [];
62-
dataProvider: NodeWithIdTreeDataProvider | null = null;
62+
public dataProvider: NodeWithIdTreeDataProvider | null = null;
6363

6464
constructor(context: vscode.ExtensionContext) {
6565
this.rootPath = (vscode.workspace.workspaceFolders && (vscode.workspace.workspaceFolders.length > 0))

0 commit comments

Comments
 (0)