Skip to content

Commit cbf65ca

Browse files
authored
Merge pull request microsoft#257906 from microsoft/rebornix/strict-herring
Support contributed file changes/diff part
2 parents 8f58c42 + 9b64bd6 commit cbf65ca

File tree

8 files changed

+350
-4
lines changed

8 files changed

+350
-4
lines changed

src/vs/workbench/api/common/extHost.api.impl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1857,6 +1857,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
18571857
ChatResponseExtensionsPart: extHostTypes.ChatResponseExtensionsPart,
18581858
ChatResponsePullRequestPart: extHostTypes.ChatResponsePullRequestPart,
18591859
ChatPrepareToolInvocationPart: extHostTypes.ChatPrepareToolInvocationPart,
1860+
ChatResponseMultiDiffPart: extHostTypes.ChatResponseMultiDiffPart,
18601861
ChatResponseReferencePartStatusKind: extHostTypes.ChatResponseReferencePartStatusKind,
18611862
ChatRequestTurn: extHostTypes.ChatRequestTurn,
18621863
ChatRequestTurn2: extHostTypes.ChatRequestTurn,

src/vs/workbench/api/common/extHostTypeConverters.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { IViewBadge } from '../../common/views.js';
4141
import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js';
4242
import { IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js';
4343
import { IChatRequestVariableEntry, isImageVariableEntry } from '../../contrib/chat/common/chatVariableEntries.js';
44-
import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatPrepareToolInvocationPart, IChatProgressMessage, IChatPullRequestContent, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js';
44+
import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatMultiDiffData, IChatPrepareToolInvocationPart, IChatProgressMessage, IChatPullRequestContent, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js';
4545
import { IToolResult, IToolResultInputOutputDetails, IToolResultOutputDetails, ToolDataSource } from '../../contrib/chat/common/languageModelToolsService.js';
4646
import * as chatProvider from '../../contrib/chat/common/languageModels.js';
4747
import { IChatMessageDataPart, IChatResponseDataPart, IChatResponsePromptTsxPart, IChatResponseTextPart } from '../../contrib/chat/common/languageModels.js';
@@ -2633,6 +2633,30 @@ export namespace ChatResponseFilesPart {
26332633
}
26342634
}
26352635

2636+
export namespace ChatResponseMultiDiffPart {
2637+
export function from(part: vscode.ChatResponseMultiDiffPart): IChatMultiDiffData {
2638+
return {
2639+
kind: 'multiDiffData',
2640+
multiDiffData: {
2641+
title: part.title,
2642+
resources: part.value.map(entry => ({
2643+
originalUri: entry.originalUri,
2644+
modifiedUri: entry.modifiedUri,
2645+
goToFileUri: entry.goToFileUri
2646+
}))
2647+
}
2648+
};
2649+
}
2650+
export function to(part: Dto<IChatMultiDiffData>): vscode.ChatResponseMultiDiffPart {
2651+
const resources = part.multiDiffData.resources.map(resource => ({
2652+
originalUri: resource.originalUri ? URI.revive(resource.originalUri) : undefined,
2653+
modifiedUri: resource.modifiedUri ? URI.revive(resource.modifiedUri) : undefined,
2654+
goToFileUri: resource.goToFileUri ? URI.revive(resource.goToFileUri) : undefined
2655+
}));
2656+
return new types.ChatResponseMultiDiffPart(resources, part.multiDiffData.title);
2657+
}
2658+
}
2659+
26362660
export namespace ChatResponseAnchorPart {
26372661
export function from(part: vscode.ChatResponseAnchorPart): Dto<IChatContentInlineReference> {
26382662
// Work around type-narrowing confusion between vscode.Uri and URI
@@ -2977,6 +3001,8 @@ export namespace ChatResponsePart {
29773001
return ChatResponseProgressPart.from(part);
29783002
} else if (part instanceof types.ChatResponseFileTreePart) {
29793003
return ChatResponseFilesPart.from(part);
3004+
} else if (part instanceof types.ChatResponseMultiDiffPart) {
3005+
return ChatResponseMultiDiffPart.from(part);
29803006
} else if (part instanceof types.ChatResponseCommandButtonPart) {
29813007
return ChatResponseCommandButtonPart.from(part, commandsConverter, commandDisposables);
29823008
} else if (part instanceof types.ChatResponseTextEditPart) {

src/vs/workbench/api/common/extHostTypes.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4609,6 +4609,15 @@ export class ChatResponseFileTreePart {
46094609
}
46104610
}
46114611

4612+
export class ChatResponseMultiDiffPart {
4613+
value: vscode.ChatResponseDiffEntry[];
4614+
title: string;
4615+
constructor(value: vscode.ChatResponseDiffEntry[], title: string) {
4616+
this.value = value;
4617+
this.title = title;
4618+
}
4619+
}
4620+
46124621
export class ChatResponseAnchorPart implements vscode.ChatResponseAnchorPart {
46134622
value: vscode.Uri | vscode.Location;
46144623
title?: string;
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
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 { IEditorService } from '../../../../services/editor/common/editorService.js';
23+
import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
24+
import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js';
25+
import { MultiDiffEditorItem } from '../../../multiDiffEditor/browser/multiDiffSourceResolverService.js';
26+
import { Codicon } from '../../../../../base/common/codicons.js';
27+
import { ThemeIcon } from '../../../../../base/common/themables.js';
28+
import { IChatRendererContent } from '../../common/chatViewModel.js';
29+
import { Emitter, Event } from '../../../../../base/common/event.js';
30+
31+
const $ = dom.$;
32+
33+
interface IChatMultiDiffItem {
34+
uri: URI;
35+
diff?: IEditSessionEntryDiff;
36+
}
37+
38+
const ELEMENT_HEIGHT = 22;
39+
const MAX_ITEMS_SHOWN = 6;
40+
41+
export class ChatMultiDiffContentPart extends Disposable implements IChatContentPart {
42+
public readonly domNode: HTMLElement;
43+
44+
45+
private readonly _onDidChangeHeight = this._register(new Emitter<void>());
46+
public readonly onDidChangeHeight = this._onDidChangeHeight.event;
47+
48+
private list!: WorkbenchList<IChatMultiDiffItem>;
49+
private isCollapsed: boolean = true;
50+
51+
constructor(
52+
private readonly content: IChatMultiDiffData,
53+
element: ChatTreeItem,
54+
@IInstantiationService private readonly instantiationService: IInstantiationService,
55+
@IEditorService private readonly editorService: IEditorService,
56+
@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,
57+
@IThemeService private readonly themeService: IThemeService
58+
) {
59+
super();
60+
61+
const headerDomNode = $('.checkpoint-file-changes-summary-header');
62+
this.domNode = $('.checkpoint-file-changes-summary', undefined, headerDomNode);
63+
this.domNode.tabIndex = 0;
64+
65+
this._register(this.renderHeader(headerDomNode));
66+
this._register(this.renderFilesList(this.domNode));
67+
}
68+
69+
private renderHeader(container: HTMLElement): IDisposable {
70+
const fileCount = this.content.multiDiffData.resources.length;
71+
72+
const viewListButtonContainer = container.appendChild($('.chat-file-changes-label'));
73+
const viewListButton = new ButtonWithIcon(viewListButtonContainer, {});
74+
viewListButton.label = fileCount === 1
75+
? localize('chatMultiDiff.oneFile', 'Changed 1 file')
76+
: localize('chatMultiDiff.manyFiles', 'Changed {0} files', fileCount);
77+
78+
const setExpansionState = () => {
79+
viewListButton.icon = this.isCollapsed ? Codicon.chevronRight : Codicon.chevronDown;
80+
this.domNode.classList.toggle('chat-file-changes-collapsed', this.isCollapsed);
81+
this._onDidChangeHeight.fire();
82+
};
83+
setExpansionState();
84+
85+
const disposables = new DisposableStore();
86+
disposables.add(viewListButton);
87+
disposables.add(viewListButton.onDidClick(() => {
88+
this.isCollapsed = !this.isCollapsed;
89+
setExpansionState();
90+
}));
91+
disposables.add(this.renderViewAllFileChangesButton(viewListButton.element));
92+
return toDisposable(() => disposables.dispose());
93+
}
94+
95+
private renderViewAllFileChangesButton(container: HTMLElement): IDisposable {
96+
const button = container.appendChild($('.chat-view-changes-icon'));
97+
button.classList.add(...ThemeIcon.asClassNameArray(Codicon.diffMultiple));
98+
99+
return dom.addDisposableListener(button, 'click', (e) => {
100+
const source = URI.parse(`multi-diff-editor:${new Date().getMilliseconds().toString() + Math.random().toString()}`);
101+
const input = this.instantiationService.createInstance(
102+
MultiDiffEditorInput,
103+
source,
104+
this.content.multiDiffData.title || 'Multi-Diff',
105+
this.content.multiDiffData.resources.map(resource => new MultiDiffEditorItem(
106+
resource.originalUri,
107+
resource.modifiedUri,
108+
resource.goToFileUri
109+
)),
110+
false
111+
);
112+
this.editorGroupsService.activeGroup.openEditor(input);
113+
dom.EventHelper.stop(e, true);
114+
});
115+
}
116+
117+
private renderFilesList(container: HTMLElement): IDisposable {
118+
const store = new DisposableStore();
119+
120+
const listContainer = container.appendChild($('.chat-summary-list'));
121+
store.add(createFileIconThemableTreeContainerScope(listContainer, this.themeService));
122+
const resourceLabels = store.add(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: Event.None }));
123+
124+
this.list = store.add(this.instantiationService.createInstance(
125+
WorkbenchList<IChatMultiDiffItem>,
126+
'ChatMultiDiffList',
127+
listContainer,
128+
new ChatMultiDiffListDelegate(),
129+
[this.instantiationService.createInstance(ChatMultiDiffListRenderer, resourceLabels)],
130+
{
131+
identityProvider: {
132+
getId: (element: IChatMultiDiffItem) => element.uri.toString()
133+
},
134+
setRowLineHeight: true,
135+
horizontalScrolling: false,
136+
supportDynamicHeights: false,
137+
mouseSupport: true,
138+
accessibilityProvider: {
139+
getAriaLabel: (element: IChatMultiDiffItem) => element.uri.path,
140+
getWidgetAriaLabel: () => localize('chatMultiDiffList', "File Changes")
141+
}
142+
}
143+
));
144+
145+
const items: IChatMultiDiffItem[] = [];
146+
for (const resource of this.content.multiDiffData.resources) {
147+
const uri = resource.modifiedUri || resource.originalUri || resource.goToFileUri;
148+
if (!uri) {
149+
continue;
150+
}
151+
152+
const item: IChatMultiDiffItem = { uri };
153+
154+
if (resource.originalUri && resource.modifiedUri) {
155+
item.diff = {
156+
originalURI: resource.originalUri,
157+
modifiedURI: resource.modifiedUri,
158+
quitEarly: false,
159+
identical: false,
160+
added: 0,
161+
removed: 0
162+
};
163+
}
164+
165+
items.push(item);
166+
}
167+
168+
this.list.splice(0, this.list.length, items);
169+
170+
const height = Math.min(items.length, MAX_ITEMS_SHOWN) * ELEMENT_HEIGHT;
171+
this.list.layout(height);
172+
listContainer.style.height = `${height}px`;
173+
174+
store.add(this.list.onDidOpen((e) => {
175+
if (!e.element) {
176+
return;
177+
}
178+
179+
if (e.element.diff) {
180+
this.editorService.openEditor({
181+
original: { resource: e.element.diff.originalURI },
182+
modified: { resource: e.element.diff.modifiedURI },
183+
options: { preserveFocus: true }
184+
});
185+
} else {
186+
this.editorService.openEditor({
187+
resource: e.element.uri,
188+
options: { preserveFocus: true }
189+
});
190+
}
191+
}));
192+
193+
return store;
194+
}
195+
196+
hasSameContent(other: IChatRendererContent): boolean {
197+
return other.kind === 'multiDiffData' &&
198+
(other as any).multiDiffData?.resources?.length === this.content.multiDiffData.resources.length;
199+
}
200+
201+
addDisposable(disposable: IDisposable): void {
202+
this._register(disposable);
203+
}
204+
}
205+
206+
class ChatMultiDiffListDelegate implements IListVirtualDelegate<IChatMultiDiffItem> {
207+
getHeight(): number {
208+
return 22;
209+
}
210+
211+
getTemplateId(): string {
212+
return 'chatMultiDiffItem';
213+
}
214+
}
215+
216+
interface IChatMultiDiffItemTemplate extends IDisposable {
217+
readonly label: IResourceLabel;
218+
}
219+
220+
class ChatMultiDiffListRenderer implements IListRenderer<IChatMultiDiffItem, IChatMultiDiffItemTemplate> {
221+
static readonly TEMPLATE_ID = 'chatMultiDiffItem';
222+
static readonly CHANGES_SUMMARY_CLASS_NAME = 'insertions-and-deletions';
223+
224+
readonly templateId: string = ChatMultiDiffListRenderer.TEMPLATE_ID;
225+
226+
constructor(private labels: ResourceLabels) { }
227+
228+
renderTemplate(container: HTMLElement): IChatMultiDiffItemTemplate {
229+
const label = this.labels.create(container, { supportHighlights: true, supportIcons: true });
230+
return { label, dispose: () => label.dispose() };
231+
}
232+
233+
renderElement(element: IChatMultiDiffItem, _index: number, templateData: IChatMultiDiffItemTemplate): void {
234+
templateData.label.setFile(element.uri, {
235+
fileKind: FileKind.FILE,
236+
title: element.uri.path
237+
});
238+
}
239+
240+
disposeTemplate(templateData: IChatMultiDiffItemTemplate): void {
241+
templateData.dispose();
242+
}
243+
}

src/vs/workbench/contrib/chat/browser/chatListRenderer.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import { IChatAgentMetadata } from '../common/chatAgents.js';
4949
import { ChatContextKeys } from '../common/chatContextKeys.js';
5050
import { IChatTextEditGroup } from '../common/chatModel.js';
5151
import { chatSubcommandLeader } from '../common/chatParserTypes.js';
52-
import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatErrorLevel, IChatChangesSummary, IChatConfirmation, IChatContentReference, IChatElicitationRequest, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatPullRequestContent, IChatTask, IChatTaskSerialized, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop } from '../common/chatService.js';
52+
import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatErrorLevel, IChatChangesSummary, IChatConfirmation, IChatContentReference, IChatElicitationRequest, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatPullRequestContent, IChatMultiDiffData, IChatTask, IChatTaskSerialized, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop } from '../common/chatService.js';
5353
import { IChatChangesSummaryPart, IChatCodeCitations, IChatErrorDetailsPart, IChatReferences, IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatWorkingProgress, isRequestVM, isResponseVM } from '../common/chatViewModel.js';
5454
import { getNWords } from '../common/chatWordCounter.js';
5555
import { CodeBlockModelCollection } from '../common/codeBlockModelCollection.js';
@@ -72,6 +72,7 @@ import { ChatCollapsibleListContentPart, ChatUsedReferencesListContentPart, Coll
7272
import { ChatTaskContentPart } from './chatContentParts/chatTaskContentPart.js';
7373
import { ChatTextEditContentPart, DiffEditorPool } from './chatContentParts/chatTextEditContentPart.js';
7474
import { ChatTreeContentPart, TreePool } from './chatContentParts/chatTreeContentPart.js';
75+
import { ChatMultiDiffContentPart } from './chatContentParts/chatMultiDiffContentPart.js';
7576
import { ChatErrorContentPart } from './chatContentParts/chatErrorContentPart.js';
7677
import { ChatToolInvocationPart } from './chatContentParts/toolInvocationParts/chatToolInvocationPart.js';
7778
import { ChatMarkdownDecorationsRenderer } from './chatMarkdownDecorationsRenderer.js';
@@ -1104,6 +1105,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
11041105
try {
11051106
if (content.kind === 'treeData') {
11061107
return this.renderTreeData(content, templateData, context);
1108+
} else if (content.kind === 'multiDiffData') {
1109+
return this.renderMultiDiffData(content, templateData, context);
11071110
} else if (content.kind === 'progressMessage') {
11081111
return this.instantiationService.createInstance(ChatProgressContentPart, content, this.renderer, context, undefined, undefined, undefined);
11091112
} else if (content.kind === 'progressTask' || content.kind === 'progressTaskSerialized') {
@@ -1218,6 +1221,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
12181221
return treePart;
12191222
}
12201223

1224+
private renderMultiDiffData(content: IChatMultiDiffData, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart {
1225+
const multiDiffPart = this.instantiationService.createInstance(ChatMultiDiffContentPart, content, context.element);
1226+
multiDiffPart.addDisposable(multiDiffPart.onDidChangeHeight(() => {
1227+
this.updateItemHeight(templateData);
1228+
}));
1229+
return multiDiffPart;
1230+
}
1231+
12211232
private renderContentReferencesListData(references: IChatReferences, labelOverride: string | undefined, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): ChatCollapsibleListContentPart {
12221233
const referencesPart = this.instantiationService.createInstance(ChatUsedReferencesListContentPart, references.references, labelOverride, context, this._contentReferencesListPool, { expandedWhenEmptyResponse: checkModeOption(this.delegate.currentChatMode(), this.rendererOptions.referencesExpandedWhenEmptyResponse) });
12231234
referencesPart.addDisposable(referencesPart.onDidChangeHeight(() => {

0 commit comments

Comments
 (0)