Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMultiDiffContentPart.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 * as dom from '../../../../../base/browser/dom.js';
7
import { ButtonWithIcon } from '../../../../../base/browser/ui/button/button.js';
8
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
9
import { URI } from '../../../../../base/common/uri.js';
10
import { localize } from '../../../../../nls.js';
11
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
12
import { IChatContentPart } from './chatContentParts.js';
13
import { IChatMultiDiffData } from '../../common/chatService.js';
14
import { ChatTreeItem } from '../chat.js';
15
import { IResourceLabel, ResourceLabels } from '../../../../browser/labels.js';
16
import { WorkbenchList } from '../../../../../platform/list/browser/listService.js';
17
import { IListRenderer, IListVirtualDelegate } from '../../../../../base/browser/ui/list/list.js';
18
import { FileKind } from '../../../../../platform/files/common/files.js';
19
import { createFileIconThemableTreeContainerScope } from '../../../files/browser/views/explorerView.js';
20
import { IThemeService } from '../../../../../platform/theme/common/themeService.js';
21
import { IEditSessionEntryDiff } from '../../common/chatEditingService.js';
22
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../../services/editor/common/editorService.js';
23
import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js';
24
import { MultiDiffEditorItem } from '../../../multiDiffEditor/browser/multiDiffSourceResolverService.js';
25
import { Codicon } from '../../../../../base/common/codicons.js';
26
import { ThemeIcon } from '../../../../../base/common/themables.js';
27
import { IChatRendererContent } from '../../common/chatViewModel.js';
28
import { Emitter, Event } from '../../../../../base/common/event.js';
29
import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js';
30
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
31
import { ActionBar, ActionsOrientation } from '../../../../../base/browser/ui/actionbar/actionbar.js';
32
import { MarshalledId } from '../../../../../base/common/marshallingIds.js';
33
import { ChatContextKeys } from '../../common/chatContextKeys.js';
34
35
const $ = dom.$;
36
37
interface IChatMultiDiffItem {
38
uri: URI;
39
diff?: IEditSessionEntryDiff;
40
}
41
42
const ELEMENT_HEIGHT = 22;
43
const MAX_ITEMS_SHOWN = 6;
44
45
export class ChatMultiDiffContentPart extends Disposable implements IChatContentPart {
46
public readonly domNode: HTMLElement;
47
48
private readonly _onDidChangeHeight = this._register(new Emitter<void>());
49
public readonly onDidChangeHeight = this._onDidChangeHeight.event;
50
51
private list!: WorkbenchList<IChatMultiDiffItem>;
52
private isCollapsed: boolean = false;
53
54
constructor(
55
private readonly content: IChatMultiDiffData,
56
_element: ChatTreeItem,
57
@IInstantiationService private readonly instantiationService: IInstantiationService,
58
@IEditorService private readonly editorService: IEditorService,
59
@IThemeService private readonly themeService: IThemeService,
60
@IMenuService private readonly menuService: IMenuService,
61
@IContextKeyService private readonly contextKeyService: IContextKeyService
62
) {
63
super();
64
65
const headerDomNode = $('.checkpoint-file-changes-summary-header');
66
this.domNode = $('.checkpoint-file-changes-summary', undefined, headerDomNode);
67
this.domNode.tabIndex = 0;
68
69
this._register(this.renderHeader(headerDomNode));
70
this._register(this.renderFilesList(this.domNode));
71
}
72
73
private renderHeader(container: HTMLElement): IDisposable {
74
const fileCount = this.content.multiDiffData.resources.length;
75
76
const viewListButtonContainer = container.appendChild($('.chat-file-changes-label'));
77
const viewListButton = new ButtonWithIcon(viewListButtonContainer, {});
78
viewListButton.label = fileCount === 1
79
? localize('chatMultiDiff.oneFile', 'Changed 1 file')
80
: localize('chatMultiDiff.manyFiles', 'Changed {0} files', fileCount);
81
82
const setExpansionState = () => {
83
viewListButton.icon = this.isCollapsed ? Codicon.chevronRight : Codicon.chevronDown;
84
this.domNode.classList.toggle('chat-file-changes-collapsed', this.isCollapsed);
85
this._onDidChangeHeight.fire();
86
};
87
setExpansionState();
88
89
const disposables = new DisposableStore();
90
disposables.add(viewListButton);
91
disposables.add(viewListButton.onDidClick(() => {
92
this.isCollapsed = !this.isCollapsed;
93
setExpansionState();
94
}));
95
disposables.add(this.renderViewAllFileChangesButton(viewListButton.element));
96
disposables.add(this.renderContributedButtons(viewListButton.element));
97
return toDisposable(() => disposables.dispose());
98
}
99
100
private renderViewAllFileChangesButton(container: HTMLElement): IDisposable {
101
const button = container.appendChild($('.chat-view-changes-icon'));
102
button.classList.add(...ThemeIcon.asClassNameArray(Codicon.diffMultiple));
103
104
return dom.addDisposableListener(button, 'click', (e) => {
105
const source = URI.parse(`multi-diff-editor:${new Date().getMilliseconds().toString() + Math.random().toString()}`);
106
const input = this.instantiationService.createInstance(
107
MultiDiffEditorInput,
108
source,
109
this.content.multiDiffData.title || 'Multi-Diff',
110
this.content.multiDiffData.resources.map(resource => new MultiDiffEditorItem(
111
resource.originalUri,
112
resource.modifiedUri,
113
resource.goToFileUri
114
)),
115
false
116
);
117
const sideBySide = e.altKey;
118
this.editorService.openEditor(input, sideBySide ? SIDE_GROUP : ACTIVE_GROUP);
119
dom.EventHelper.stop(e, true);
120
});
121
}
122
123
private renderContributedButtons(container: HTMLElement): IDisposable {
124
const buttonsContainer = container.appendChild($('.chat-multidiff-contributed-buttons'));
125
const disposables = new DisposableStore();
126
const actionBar = disposables.add(new ActionBar(buttonsContainer, {
127
orientation: ActionsOrientation.HORIZONTAL
128
}));
129
const setupActionBar = () => {
130
actionBar.clear();
131
132
const activeEditorUri = this.editorService.activeEditor?.resource;
133
let marshalledUri: any | undefined = undefined;
134
let contextKeyService: IContextKeyService = this.contextKeyService;
135
if (activeEditorUri) {
136
const { authority } = activeEditorUri;
137
const overlay: [string, unknown][] = [];
138
if (authority) {
139
overlay.push([ChatContextKeys.sessionType.key, authority]);
140
}
141
contextKeyService = this.contextKeyService.createOverlay(overlay);
142
marshalledUri = {
143
...activeEditorUri,
144
$mid: MarshalledId.Uri
145
};
146
}
147
148
const actions = this.menuService.getMenuActions(
149
MenuId.ChatMultiDiffContext,
150
contextKeyService,
151
{ arg: marshalledUri, shouldForwardArgs: true }
152
);
153
const allActions = actions.flatMap(([, actions]) => actions);
154
if (allActions.length > 0) {
155
actionBar.push(allActions, { icon: true, label: false });
156
}
157
};
158
setupActionBar();
159
return disposables;
160
}
161
162
private renderFilesList(container: HTMLElement): IDisposable {
163
const store = new DisposableStore();
164
165
const listContainer = container.appendChild($('.chat-summary-list'));
166
store.add(createFileIconThemableTreeContainerScope(listContainer, this.themeService));
167
const resourceLabels = store.add(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: Event.None }));
168
169
this.list = store.add(this.instantiationService.createInstance(
170
WorkbenchList<IChatMultiDiffItem>,
171
'ChatMultiDiffList',
172
listContainer,
173
new ChatMultiDiffListDelegate(),
174
[this.instantiationService.createInstance(ChatMultiDiffListRenderer, resourceLabels)],
175
{
176
identityProvider: {
177
getId: (element: IChatMultiDiffItem) => element.uri.toString()
178
},
179
setRowLineHeight: true,
180
horizontalScrolling: false,
181
supportDynamicHeights: false,
182
mouseSupport: true,
183
alwaysConsumeMouseWheel: false,
184
accessibilityProvider: {
185
getAriaLabel: (element: IChatMultiDiffItem) => element.uri.path,
186
getWidgetAriaLabel: () => localize('chatMultiDiffList', "File Changes")
187
}
188
}
189
));
190
191
const items: IChatMultiDiffItem[] = [];
192
for (const resource of this.content.multiDiffData.resources) {
193
const uri = resource.modifiedUri || resource.originalUri || resource.goToFileUri;
194
if (!uri) {
195
continue;
196
}
197
198
const item: IChatMultiDiffItem = { uri };
199
200
if (resource.originalUri && resource.modifiedUri) {
201
item.diff = {
202
originalURI: resource.originalUri,
203
modifiedURI: resource.modifiedUri,
204
quitEarly: false,
205
identical: false,
206
added: resource.added || 0,
207
removed: resource.removed || 0
208
};
209
}
210
items.push(item);
211
}
212
213
this.list.splice(0, this.list.length, items);
214
215
const height = Math.min(items.length, MAX_ITEMS_SHOWN) * ELEMENT_HEIGHT;
216
this.list.layout(height);
217
listContainer.style.height = `${height}px`;
218
219
store.add(this.list.onDidOpen((e) => {
220
if (!e.element) {
221
return;
222
}
223
224
if (e.element.diff) {
225
this.editorService.openEditor({
226
original: { resource: e.element.diff.originalURI },
227
modified: { resource: e.element.diff.modifiedURI },
228
options: { preserveFocus: true }
229
});
230
} else {
231
this.editorService.openEditor({
232
resource: e.element.uri,
233
options: { preserveFocus: true }
234
});
235
}
236
}));
237
238
return store;
239
}
240
241
hasSameContent(other: IChatRendererContent): boolean {
242
return other.kind === 'multiDiffData' &&
243
(other as any).multiDiffData?.resources?.length === this.content.multiDiffData.resources.length;
244
}
245
246
addDisposable(disposable: IDisposable): void {
247
this._register(disposable);
248
}
249
}
250
251
class ChatMultiDiffListDelegate implements IListVirtualDelegate<IChatMultiDiffItem> {
252
getHeight(): number {
253
return 22;
254
}
255
256
getTemplateId(): string {
257
return 'chatMultiDiffItem';
258
}
259
}
260
261
interface IChatMultiDiffItemTemplate extends IDisposable {
262
readonly label: IResourceLabel;
263
}
264
265
class ChatMultiDiffListRenderer implements IListRenderer<IChatMultiDiffItem, IChatMultiDiffItemTemplate> {
266
static readonly TEMPLATE_ID = 'chatMultiDiffItem';
267
static readonly CHANGES_SUMMARY_CLASS_NAME = 'insertions-and-deletions';
268
269
readonly templateId: string = ChatMultiDiffListRenderer.TEMPLATE_ID;
270
271
constructor(private labels: ResourceLabels) { }
272
273
renderTemplate(container: HTMLElement): IChatMultiDiffItemTemplate {
274
const label = this.labels.create(container, { supportHighlights: true, supportIcons: true });
275
276
return {
277
label,
278
dispose: () => label.dispose()
279
};
280
}
281
282
renderElement(element: IChatMultiDiffItem, _index: number, templateData: IChatMultiDiffItemTemplate): void {
283
templateData.label.setFile(element.uri, {
284
fileKind: FileKind.FILE,
285
title: element.uri.path
286
});
287
288
const labelElement = templateData.label.element;
289
labelElement.querySelector(`.${ChatMultiDiffListRenderer.CHANGES_SUMMARY_CLASS_NAME}`)?.remove();
290
291
if (element.diff?.added || element.diff?.removed) {
292
const changesSummary = labelElement.appendChild($(`.${ChatMultiDiffListRenderer.CHANGES_SUMMARY_CLASS_NAME}`));
293
294
const addedElement = changesSummary.appendChild($('.insertions'));
295
addedElement.textContent = `+${element.diff.added}`;
296
297
const removedElement = changesSummary.appendChild($('.deletions'));
298
removedElement.textContent = `-${element.diff.removed}`;
299
300
changesSummary.setAttribute('aria-label', localize('chatEditingSession.fileCounts', '{0} lines added, {1} lines removed', element.diff.added, element.diff.removed));
301
}
302
}
303
304
disposeTemplate(templateData: IChatMultiDiffItemTemplate): void {
305
templateData.dispose();
306
}
307
}
308
309