Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { IAsyncDataSource, ITreeRenderer, ITreeNode, ITreeSorter } from '../../../../../base/browser/ui/tree/tree.js';
7
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
8
import { FuzzyScore, createMatches } from '../../../../../base/common/filters.js';
9
import { IResourceLabel, ResourceLabels } from '../../../../browser/labels.js';
10
import { HighlightedLabel, IHighlight } from '../../../../../base/browser/ui/highlightedlabel/highlightedLabel.js';
11
import { IIdentityProvider, IListVirtualDelegate, IKeyboardNavigationLabelProvider } from '../../../../../base/browser/ui/list/list.js';
12
import { Range } from '../../../../../editor/common/core/range.js';
13
import * as dom from '../../../../../base/browser/dom.js';
14
import { ITextModel } from '../../../../../editor/common/model.js';
15
import { IDisposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
16
import { TextModel } from '../../../../../editor/common/model/textModel.js';
17
import { BulkFileOperations, BulkFileOperation, BulkFileOperationType, BulkTextEdit, BulkCategory } from './bulkEditPreview.js';
18
import { FileKind } from '../../../../../platform/files/common/files.js';
19
import { localize } from '../../../../../nls.js';
20
import { ILabelService } from '../../../../../platform/label/common/label.js';
21
import type { IListAccessibilityProvider } from '../../../../../base/browser/ui/list/listWidget.js';
22
import { IconLabel } from '../../../../../base/browser/ui/iconLabel/iconLabel.js';
23
import { basename } from '../../../../../base/common/resources.js';
24
import { IThemeService } from '../../../../../platform/theme/common/themeService.js';
25
import { ThemeIcon } from '../../../../../base/common/themables.js';
26
import { compare } from '../../../../../base/common/strings.js';
27
import { URI } from '../../../../../base/common/uri.js';
28
import { ResourceFileEdit } from '../../../../../editor/browser/services/bulkEditService.js';
29
import { PLAINTEXT_LANGUAGE_ID } from '../../../../../editor/common/languages/modesRegistry.js';
30
import { SnippetParser } from '../../../../../editor/contrib/snippet/browser/snippetParser.js';
31
import { AriaRole } from '../../../../../base/browser/ui/aria/aria.js';
32
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
33
import * as css from '../../../../../base/browser/cssValue.js';
34
35
// --- VIEW MODEL
36
37
export interface ICheckable {
38
isChecked(): boolean;
39
setChecked(value: boolean): void;
40
}
41
42
export class CategoryElement implements ICheckable {
43
44
constructor(
45
readonly parent: BulkFileOperations,
46
readonly category: BulkCategory
47
) { }
48
49
isChecked(): boolean {
50
const model = this.parent;
51
let checked = true;
52
for (const file of this.category.fileOperations) {
53
for (const edit of file.originalEdits.values()) {
54
checked = checked && model.checked.isChecked(edit);
55
}
56
}
57
return checked;
58
}
59
60
setChecked(value: boolean): void {
61
const model = this.parent;
62
for (const file of this.category.fileOperations) {
63
for (const edit of file.originalEdits.values()) {
64
model.checked.updateChecked(edit, value);
65
}
66
}
67
}
68
}
69
70
export class FileElement implements ICheckable {
71
72
constructor(
73
readonly parent: CategoryElement | BulkFileOperations,
74
readonly edit: BulkFileOperation
75
) { }
76
77
isChecked(): boolean {
78
const model = this.parent instanceof CategoryElement ? this.parent.parent : this.parent;
79
80
let checked = true;
81
82
// only text edit children -> reflect children state
83
if (this.edit.type === BulkFileOperationType.TextEdit) {
84
checked = !this.edit.textEdits.every(edit => !model.checked.isChecked(edit.textEdit));
85
}
86
87
// multiple file edits -> reflect single state
88
for (const edit of this.edit.originalEdits.values()) {
89
if (edit instanceof ResourceFileEdit) {
90
checked = checked && model.checked.isChecked(edit);
91
}
92
}
93
94
// multiple categories and text change -> read all elements
95
if (this.parent instanceof CategoryElement && this.edit.type === BulkFileOperationType.TextEdit) {
96
for (const category of model.categories) {
97
for (const file of category.fileOperations) {
98
if (file.uri.toString() === this.edit.uri.toString()) {
99
for (const edit of file.originalEdits.values()) {
100
if (edit instanceof ResourceFileEdit) {
101
checked = checked && model.checked.isChecked(edit);
102
}
103
}
104
}
105
}
106
}
107
}
108
109
return checked;
110
}
111
112
setChecked(value: boolean): void {
113
const model = this.parent instanceof CategoryElement ? this.parent.parent : this.parent;
114
for (const edit of this.edit.originalEdits.values()) {
115
model.checked.updateChecked(edit, value);
116
}
117
118
// multiple categories and file change -> update all elements
119
if (this.parent instanceof CategoryElement && this.edit.type !== BulkFileOperationType.TextEdit) {
120
for (const category of model.categories) {
121
for (const file of category.fileOperations) {
122
if (file.uri.toString() === this.edit.uri.toString()) {
123
for (const edit of file.originalEdits.values()) {
124
model.checked.updateChecked(edit, value);
125
}
126
}
127
}
128
}
129
}
130
}
131
132
isDisabled(): boolean {
133
if (this.parent instanceof CategoryElement && this.edit.type === BulkFileOperationType.TextEdit) {
134
const model = this.parent.parent;
135
let checked = true;
136
for (const category of model.categories) {
137
for (const file of category.fileOperations) {
138
if (file.uri.toString() === this.edit.uri.toString()) {
139
for (const edit of file.originalEdits.values()) {
140
if (edit instanceof ResourceFileEdit) {
141
checked = checked && model.checked.isChecked(edit);
142
}
143
}
144
}
145
}
146
}
147
return !checked;
148
}
149
return false;
150
}
151
}
152
153
export class TextEditElement implements ICheckable {
154
155
constructor(
156
readonly parent: FileElement,
157
readonly idx: number,
158
readonly edit: BulkTextEdit,
159
readonly prefix: string, readonly selecting: string, readonly inserting: string, readonly suffix: string
160
) { }
161
162
isChecked(): boolean {
163
let model = this.parent.parent;
164
if (model instanceof CategoryElement) {
165
model = model.parent;
166
}
167
return model.checked.isChecked(this.edit.textEdit);
168
}
169
170
setChecked(value: boolean): void {
171
let model = this.parent.parent;
172
if (model instanceof CategoryElement) {
173
model = model.parent;
174
}
175
176
// check/uncheck this element
177
model.checked.updateChecked(this.edit.textEdit, value);
178
179
// make sure parent is checked when this element is checked...
180
if (value) {
181
for (const edit of this.parent.edit.originalEdits.values()) {
182
if (edit instanceof ResourceFileEdit) {
183
(<BulkFileOperations>model).checked.updateChecked(edit, value);
184
}
185
}
186
}
187
}
188
189
isDisabled(): boolean {
190
return this.parent.isDisabled();
191
}
192
}
193
194
export type BulkEditElement = CategoryElement | FileElement | TextEditElement;
195
196
// --- DATA SOURCE
197
198
export class BulkEditDataSource implements IAsyncDataSource<BulkFileOperations, BulkEditElement> {
199
200
public groupByFile: boolean = true;
201
202
constructor(
203
@ITextModelService private readonly _textModelService: ITextModelService,
204
@IInstantiationService private readonly _instantiationService: IInstantiationService,
205
) { }
206
207
hasChildren(element: BulkFileOperations | BulkEditElement): boolean {
208
if (element instanceof FileElement) {
209
return element.edit.textEdits.length > 0;
210
}
211
if (element instanceof TextEditElement) {
212
return false;
213
}
214
return true;
215
}
216
217
async getChildren(element: BulkFileOperations | BulkEditElement): Promise<BulkEditElement[]> {
218
219
// root -> file/text edits
220
if (element instanceof BulkFileOperations) {
221
return this.groupByFile
222
? element.fileOperations.map(op => new FileElement(element, op))
223
: element.categories.map(cat => new CategoryElement(element, cat));
224
}
225
226
// category
227
if (element instanceof CategoryElement) {
228
return Array.from(element.category.fileOperations, op => new FileElement(element, op));
229
}
230
231
// file: text edit
232
if (element instanceof FileElement && element.edit.textEdits.length > 0) {
233
// const previewUri = BulkEditPreviewProvider.asPreviewUri(element.edit.resource);
234
let textModel: ITextModel;
235
let textModelDisposable: IDisposable;
236
try {
237
const ref = await this._textModelService.createModelReference(element.edit.uri);
238
textModel = ref.object.textEditorModel;
239
textModelDisposable = ref;
240
} catch {
241
textModel = this._instantiationService.createInstance(TextModel, '', PLAINTEXT_LANGUAGE_ID, TextModel.DEFAULT_CREATION_OPTIONS, null);
242
textModelDisposable = textModel;
243
}
244
245
const result = element.edit.textEdits.map((edit, idx) => {
246
const range = textModel.validateRange(edit.textEdit.textEdit.range);
247
248
//prefix-math
249
const startTokens = textModel.tokenization.getLineTokens(range.startLineNumber);
250
let prefixLen = 23; // default value for the no tokens/grammar case
251
for (let idx = startTokens.findTokenIndexAtOffset(range.startColumn - 1) - 1; prefixLen < 50 && idx >= 0; idx--) {
252
prefixLen = range.startColumn - startTokens.getStartOffset(idx);
253
}
254
255
//suffix-math
256
const endTokens = textModel.tokenization.getLineTokens(range.endLineNumber);
257
let suffixLen = 0;
258
for (let idx = endTokens.findTokenIndexAtOffset(range.endColumn - 1); suffixLen < 50 && idx < endTokens.getCount(); idx++) {
259
suffixLen += endTokens.getEndOffset(idx) - endTokens.getStartOffset(idx);
260
}
261
262
return new TextEditElement(
263
element,
264
idx,
265
edit,
266
textModel.getValueInRange(new Range(range.startLineNumber, range.startColumn - prefixLen, range.startLineNumber, range.startColumn)),
267
textModel.getValueInRange(range),
268
!edit.textEdit.textEdit.insertAsSnippet ? edit.textEdit.textEdit.text : SnippetParser.asInsertText(edit.textEdit.textEdit.text),
269
textModel.getValueInRange(new Range(range.endLineNumber, range.endColumn, range.endLineNumber, range.endColumn + suffixLen))
270
);
271
});
272
273
textModelDisposable.dispose();
274
return result;
275
}
276
277
return [];
278
}
279
}
280
281
282
export class BulkEditSorter implements ITreeSorter<BulkEditElement> {
283
284
compare(a: BulkEditElement, b: BulkEditElement): number {
285
if (a instanceof FileElement && b instanceof FileElement) {
286
return compareBulkFileOperations(a.edit, b.edit);
287
}
288
289
if (a instanceof TextEditElement && b instanceof TextEditElement) {
290
return Range.compareRangesUsingStarts(a.edit.textEdit.textEdit.range, b.edit.textEdit.textEdit.range);
291
}
292
293
return 0;
294
}
295
}
296
297
export function compareBulkFileOperations(a: BulkFileOperation, b: BulkFileOperation): number {
298
return compare(a.uri.toString(), b.uri.toString());
299
}
300
301
// --- ACCESSI
302
303
export class BulkEditAccessibilityProvider implements IListAccessibilityProvider<BulkEditElement> {
304
305
constructor(@ILabelService private readonly _labelService: ILabelService) { }
306
307
getWidgetAriaLabel(): string {
308
return localize('bulkEdit', "Bulk Edit");
309
}
310
311
getRole(_element: BulkEditElement): AriaRole {
312
return 'checkbox';
313
}
314
315
getAriaLabel(element: BulkEditElement): string | null {
316
if (element instanceof FileElement) {
317
if (element.edit.textEdits.length > 0) {
318
if (element.edit.type & BulkFileOperationType.Rename && element.edit.newUri) {
319
return localize(
320
'aria.renameAndEdit', "Renaming {0} to {1}, also making text edits",
321
this._labelService.getUriLabel(element.edit.uri, { relative: true }), this._labelService.getUriLabel(element.edit.newUri, { relative: true })
322
);
323
324
} else if (element.edit.type & BulkFileOperationType.Create) {
325
return localize(
326
'aria.createAndEdit', "Creating {0}, also making text edits",
327
this._labelService.getUriLabel(element.edit.uri, { relative: true })
328
);
329
330
} else if (element.edit.type & BulkFileOperationType.Delete) {
331
return localize(
332
'aria.deleteAndEdit', "Deleting {0}, also making text edits",
333
this._labelService.getUriLabel(element.edit.uri, { relative: true }),
334
);
335
} else {
336
return localize(
337
'aria.editOnly', "{0}, making text edits",
338
this._labelService.getUriLabel(element.edit.uri, { relative: true }),
339
);
340
}
341
342
} else {
343
if (element.edit.type & BulkFileOperationType.Rename && element.edit.newUri) {
344
return localize(
345
'aria.rename', "Renaming {0} to {1}",
346
this._labelService.getUriLabel(element.edit.uri, { relative: true }), this._labelService.getUriLabel(element.edit.newUri, { relative: true })
347
);
348
349
} else if (element.edit.type & BulkFileOperationType.Create) {
350
return localize(
351
'aria.create', "Creating {0}",
352
this._labelService.getUriLabel(element.edit.uri, { relative: true })
353
);
354
355
} else if (element.edit.type & BulkFileOperationType.Delete) {
356
return localize(
357
'aria.delete', "Deleting {0}",
358
this._labelService.getUriLabel(element.edit.uri, { relative: true }),
359
);
360
}
361
}
362
}
363
364
if (element instanceof TextEditElement) {
365
if (element.selecting.length > 0 && element.inserting.length > 0) {
366
// edit: replace
367
return localize('aria.replace', "line {0}, replacing {1} with {2}", element.edit.textEdit.textEdit.range.startLineNumber, element.selecting, element.inserting);
368
} else if (element.selecting.length > 0 && element.inserting.length === 0) {
369
// edit: delete
370
return localize('aria.del', "line {0}, removing {1}", element.edit.textEdit.textEdit.range.startLineNumber, element.selecting);
371
} else if (element.selecting.length === 0 && element.inserting.length > 0) {
372
// edit: insert
373
return localize('aria.insert', "line {0}, inserting {1}", element.edit.textEdit.textEdit.range.startLineNumber, element.selecting);
374
}
375
}
376
377
return null;
378
}
379
}
380
381
// --- IDENT
382
383
export class BulkEditIdentityProvider implements IIdentityProvider<BulkEditElement> {
384
385
getId(element: BulkEditElement): { toString(): string } {
386
if (element instanceof FileElement) {
387
return element.edit.uri + (element.parent instanceof CategoryElement ? JSON.stringify(element.parent.category.metadata) : '');
388
} else if (element instanceof TextEditElement) {
389
return element.parent.edit.uri.toString() + element.idx;
390
} else {
391
return JSON.stringify(element.category.metadata);
392
}
393
}
394
}
395
396
// --- RENDERER
397
398
class CategoryElementTemplate {
399
400
readonly icon: HTMLDivElement;
401
readonly label: IconLabel;
402
403
constructor(container: HTMLElement) {
404
container.classList.add('category');
405
this.icon = document.createElement('div');
406
container.appendChild(this.icon);
407
this.label = new IconLabel(container);
408
}
409
}
410
411
export class CategoryElementRenderer implements ITreeRenderer<CategoryElement, FuzzyScore, CategoryElementTemplate> {
412
413
static readonly id: string = 'CategoryElementRenderer';
414
415
readonly templateId: string = CategoryElementRenderer.id;
416
417
constructor(@IThemeService private readonly _themeService: IThemeService) { }
418
419
renderTemplate(container: HTMLElement): CategoryElementTemplate {
420
return new CategoryElementTemplate(container);
421
}
422
423
renderElement(node: ITreeNode<CategoryElement, FuzzyScore>, _index: number, template: CategoryElementTemplate): void {
424
425
template.icon.style.setProperty('--background-dark', null);
426
template.icon.style.setProperty('--background-light', null);
427
template.icon.style.color = '';
428
429
const { metadata } = node.element.category;
430
if (ThemeIcon.isThemeIcon(metadata.iconPath)) {
431
// css
432
const className = ThemeIcon.asClassName(metadata.iconPath);
433
template.icon.className = className ? `theme-icon ${className}` : '';
434
template.icon.style.color = metadata.iconPath.color ? this._themeService.getColorTheme().getColor(metadata.iconPath.color.id)?.toString() ?? '' : '';
435
436
437
} else if (URI.isUri(metadata.iconPath)) {
438
// background-image
439
template.icon.className = 'uri-icon';
440
template.icon.style.setProperty('--background-dark', css.asCSSUrl(metadata.iconPath));
441
template.icon.style.setProperty('--background-light', css.asCSSUrl(metadata.iconPath));
442
443
} else if (metadata.iconPath) {
444
// background-image
445
template.icon.className = 'uri-icon';
446
template.icon.style.setProperty('--background-dark', css.asCSSUrl(metadata.iconPath.dark));
447
template.icon.style.setProperty('--background-light', css.asCSSUrl(metadata.iconPath.light));
448
}
449
450
template.label.setLabel(metadata.label, metadata.description, {
451
descriptionMatches: createMatches(node.filterData),
452
});
453
}
454
455
disposeTemplate(template: CategoryElementTemplate): void {
456
template.label.dispose();
457
}
458
}
459
460
class FileElementTemplate {
461
462
private readonly _disposables = new DisposableStore();
463
private readonly _localDisposables = new DisposableStore();
464
465
private readonly _checkbox: HTMLInputElement;
466
private readonly _label: IResourceLabel;
467
private readonly _details: HTMLSpanElement;
468
469
constructor(
470
container: HTMLElement,
471
resourceLabels: ResourceLabels,
472
@ILabelService private readonly _labelService: ILabelService,
473
) {
474
475
this._checkbox = document.createElement('input');
476
this._checkbox.className = 'edit-checkbox';
477
this._checkbox.type = 'checkbox';
478
this._checkbox.setAttribute('role', 'checkbox');
479
container.appendChild(this._checkbox);
480
481
this._label = resourceLabels.create(container, { supportHighlights: true });
482
483
this._details = document.createElement('span');
484
this._details.className = 'details';
485
container.appendChild(this._details);
486
}
487
488
dispose(): void {
489
this._localDisposables.dispose();
490
this._disposables.dispose();
491
this._label.dispose();
492
}
493
494
set(element: FileElement, score: FuzzyScore | undefined) {
495
this._localDisposables.clear();
496
497
this._checkbox.checked = element.isChecked();
498
this._checkbox.disabled = element.isDisabled();
499
this._localDisposables.add(dom.addDisposableListener(this._checkbox, 'change', () => {
500
element.setChecked(this._checkbox.checked);
501
}));
502
503
if (element.edit.type & BulkFileOperationType.Rename && element.edit.newUri) {
504
// rename: oldName → newName
505
this._label.setResource({
506
resource: element.edit.uri,
507
name: localize('rename.label', "{0} → {1}", this._labelService.getUriLabel(element.edit.uri, { relative: true }), this._labelService.getUriLabel(element.edit.newUri, { relative: true })),
508
}, {
509
fileDecorations: { colors: true, badges: false }
510
});
511
512
this._details.innerText = localize('detail.rename', "(renaming)");
513
514
} else {
515
// create, delete, edit: NAME
516
const options = {
517
matches: createMatches(score),
518
fileKind: FileKind.FILE,
519
fileDecorations: { colors: true, badges: false },
520
extraClasses: <string[]>[]
521
};
522
if (element.edit.type & BulkFileOperationType.Create) {
523
this._details.innerText = localize('detail.create', "(creating)");
524
} else if (element.edit.type & BulkFileOperationType.Delete) {
525
this._details.innerText = localize('detail.del', "(deleting)");
526
options.extraClasses.push('delete');
527
} else {
528
this._details.innerText = '';
529
}
530
this._label.setFile(element.edit.uri, options);
531
}
532
}
533
}
534
535
export class FileElementRenderer implements ITreeRenderer<FileElement, FuzzyScore, FileElementTemplate> {
536
537
static readonly id: string = 'FileElementRenderer';
538
539
readonly templateId: string = FileElementRenderer.id;
540
541
constructor(
542
private readonly _resourceLabels: ResourceLabels,
543
@ILabelService private readonly _labelService: ILabelService,
544
) { }
545
546
renderTemplate(container: HTMLElement): FileElementTemplate {
547
return new FileElementTemplate(container, this._resourceLabels, this._labelService);
548
}
549
550
renderElement(node: ITreeNode<FileElement, FuzzyScore>, _index: number, template: FileElementTemplate): void {
551
template.set(node.element, node.filterData);
552
}
553
554
disposeTemplate(template: FileElementTemplate): void {
555
template.dispose();
556
}
557
}
558
559
class TextEditElementTemplate {
560
561
private readonly _disposables = new DisposableStore();
562
private readonly _localDisposables = new DisposableStore();
563
564
private readonly _checkbox: HTMLInputElement;
565
private readonly _icon: HTMLDivElement;
566
private readonly _label: HighlightedLabel;
567
568
constructor(container: HTMLElement, @IThemeService private readonly _themeService: IThemeService) {
569
container.classList.add('textedit');
570
571
this._checkbox = document.createElement('input');
572
this._checkbox.className = 'edit-checkbox';
573
this._checkbox.type = 'checkbox';
574
this._checkbox.setAttribute('role', 'checkbox');
575
container.appendChild(this._checkbox);
576
577
this._icon = document.createElement('div');
578
container.appendChild(this._icon);
579
580
this._label = this._disposables.add(new HighlightedLabel(container));
581
}
582
583
dispose(): void {
584
this._localDisposables.dispose();
585
this._disposables.dispose();
586
}
587
588
set(element: TextEditElement) {
589
this._localDisposables.clear();
590
591
this._localDisposables.add(dom.addDisposableListener(this._checkbox, 'change', e => {
592
element.setChecked(this._checkbox.checked);
593
e.preventDefault();
594
}));
595
if (element.parent.isChecked()) {
596
this._checkbox.checked = element.isChecked();
597
this._checkbox.disabled = element.isDisabled();
598
} else {
599
this._checkbox.checked = element.isChecked();
600
this._checkbox.disabled = element.isDisabled();
601
}
602
603
let value = '';
604
value += element.prefix;
605
value += element.selecting;
606
value += element.inserting;
607
value += element.suffix;
608
609
const selectHighlight: IHighlight = { start: element.prefix.length, end: element.prefix.length + element.selecting.length, extraClasses: ['remove'] };
610
const insertHighlight: IHighlight = { start: selectHighlight.end, end: selectHighlight.end + element.inserting.length, extraClasses: ['insert'] };
611
612
let title: string | undefined;
613
const { metadata } = element.edit.textEdit;
614
if (metadata && metadata.description) {
615
title = localize('title', "{0} - {1}", metadata.label, metadata.description);
616
} else if (metadata) {
617
title = metadata.label;
618
}
619
620
const iconPath = metadata?.iconPath;
621
if (!iconPath) {
622
this._icon.style.display = 'none';
623
} else {
624
this._icon.style.display = 'block';
625
626
this._icon.style.setProperty('--background-dark', null);
627
this._icon.style.setProperty('--background-light', null);
628
629
if (ThemeIcon.isThemeIcon(iconPath)) {
630
// css
631
const className = ThemeIcon.asClassName(iconPath);
632
this._icon.className = className ? `theme-icon ${className}` : '';
633
this._icon.style.color = iconPath.color ? this._themeService.getColorTheme().getColor(iconPath.color.id)?.toString() ?? '' : '';
634
635
636
} else if (URI.isUri(iconPath)) {
637
// background-image
638
this._icon.className = 'uri-icon';
639
this._icon.style.setProperty('--background-dark', css.asCSSUrl(iconPath));
640
this._icon.style.setProperty('--background-light', css.asCSSUrl(iconPath));
641
642
} else {
643
// background-image
644
this._icon.className = 'uri-icon';
645
this._icon.style.setProperty('--background-dark', css.asCSSUrl(iconPath.dark));
646
this._icon.style.setProperty('--background-light', css.asCSSUrl(iconPath.light));
647
}
648
}
649
650
this._label.set(value, [selectHighlight, insertHighlight], title, true);
651
this._icon.title = title || '';
652
}
653
}
654
655
export class TextEditElementRenderer implements ITreeRenderer<TextEditElement, FuzzyScore, TextEditElementTemplate> {
656
657
static readonly id = 'TextEditElementRenderer';
658
659
readonly templateId: string = TextEditElementRenderer.id;
660
661
constructor(@IThemeService private readonly _themeService: IThemeService) { }
662
663
renderTemplate(container: HTMLElement): TextEditElementTemplate {
664
return new TextEditElementTemplate(container, this._themeService);
665
}
666
667
renderElement({ element }: ITreeNode<TextEditElement, FuzzyScore>, _index: number, template: TextEditElementTemplate): void {
668
template.set(element);
669
}
670
671
disposeTemplate(_template: TextEditElementTemplate): void { }
672
}
673
674
export class BulkEditDelegate implements IListVirtualDelegate<BulkEditElement> {
675
676
getHeight(): number {
677
return 23;
678
}
679
680
getTemplateId(element: BulkEditElement): string {
681
682
if (element instanceof FileElement) {
683
return FileElementRenderer.id;
684
} else if (element instanceof TextEditElement) {
685
return TextEditElementRenderer.id;
686
} else {
687
return CategoryElementRenderer.id;
688
}
689
}
690
}
691
692
693
export class BulkEditNaviLabelProvider implements IKeyboardNavigationLabelProvider<BulkEditElement> {
694
695
getKeyboardNavigationLabel(element: BulkEditElement) {
696
if (element instanceof FileElement) {
697
return basename(element.edit.uri);
698
} else if (element instanceof CategoryElement) {
699
return element.category.metadata.label;
700
}
701
return undefined;
702
}
703
}
704
705