Path: blob/main/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMultiDiffContentPart.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as dom from '../../../../../base/browser/dom.js';6import { ButtonWithIcon } from '../../../../../base/browser/ui/button/button.js';7import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';8import { URI } from '../../../../../base/common/uri.js';9import { localize } from '../../../../../nls.js';10import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';11import { IChatContentPart } from './chatContentParts.js';12import { IChatMultiDiffData } from '../../common/chatService.js';13import { ChatTreeItem } from '../chat.js';14import { IResourceLabel, ResourceLabels } from '../../../../browser/labels.js';15import { WorkbenchList } from '../../../../../platform/list/browser/listService.js';16import { IListRenderer, IListVirtualDelegate } from '../../../../../base/browser/ui/list/list.js';17import { FileKind } from '../../../../../platform/files/common/files.js';18import { createFileIconThemableTreeContainerScope } from '../../../files/browser/views/explorerView.js';19import { IThemeService } from '../../../../../platform/theme/common/themeService.js';20import { IEditSessionEntryDiff } from '../../common/chatEditingService.js';21import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../../services/editor/common/editorService.js';22import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js';23import { MultiDiffEditorItem } from '../../../multiDiffEditor/browser/multiDiffSourceResolverService.js';24import { Codicon } from '../../../../../base/common/codicons.js';25import { ThemeIcon } from '../../../../../base/common/themables.js';26import { IChatRendererContent } from '../../common/chatViewModel.js';27import { Emitter, Event } from '../../../../../base/common/event.js';28import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js';29import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';30import { ActionBar, ActionsOrientation } from '../../../../../base/browser/ui/actionbar/actionbar.js';31import { MarshalledId } from '../../../../../base/common/marshallingIds.js';32import { ChatContextKeys } from '../../common/chatContextKeys.js';3334const $ = dom.$;3536interface IChatMultiDiffItem {37uri: URI;38diff?: IEditSessionEntryDiff;39}4041const ELEMENT_HEIGHT = 22;42const MAX_ITEMS_SHOWN = 6;4344export class ChatMultiDiffContentPart extends Disposable implements IChatContentPart {45public readonly domNode: HTMLElement;4647private readonly _onDidChangeHeight = this._register(new Emitter<void>());48public readonly onDidChangeHeight = this._onDidChangeHeight.event;4950private list!: WorkbenchList<IChatMultiDiffItem>;51private isCollapsed: boolean = false;5253constructor(54private readonly content: IChatMultiDiffData,55_element: ChatTreeItem,56@IInstantiationService private readonly instantiationService: IInstantiationService,57@IEditorService private readonly editorService: IEditorService,58@IThemeService private readonly themeService: IThemeService,59@IMenuService private readonly menuService: IMenuService,60@IContextKeyService private readonly contextKeyService: IContextKeyService61) {62super();6364const headerDomNode = $('.checkpoint-file-changes-summary-header');65this.domNode = $('.checkpoint-file-changes-summary', undefined, headerDomNode);66this.domNode.tabIndex = 0;6768this._register(this.renderHeader(headerDomNode));69this._register(this.renderFilesList(this.domNode));70}7172private renderHeader(container: HTMLElement): IDisposable {73const fileCount = this.content.multiDiffData.resources.length;7475const viewListButtonContainer = container.appendChild($('.chat-file-changes-label'));76const viewListButton = new ButtonWithIcon(viewListButtonContainer, {});77viewListButton.label = fileCount === 178? localize('chatMultiDiff.oneFile', 'Changed 1 file')79: localize('chatMultiDiff.manyFiles', 'Changed {0} files', fileCount);8081const setExpansionState = () => {82viewListButton.icon = this.isCollapsed ? Codicon.chevronRight : Codicon.chevronDown;83this.domNode.classList.toggle('chat-file-changes-collapsed', this.isCollapsed);84this._onDidChangeHeight.fire();85};86setExpansionState();8788const disposables = new DisposableStore();89disposables.add(viewListButton);90disposables.add(viewListButton.onDidClick(() => {91this.isCollapsed = !this.isCollapsed;92setExpansionState();93}));94disposables.add(this.renderViewAllFileChangesButton(viewListButton.element));95disposables.add(this.renderContributedButtons(viewListButton.element));96return toDisposable(() => disposables.dispose());97}9899private renderViewAllFileChangesButton(container: HTMLElement): IDisposable {100const button = container.appendChild($('.chat-view-changes-icon'));101button.classList.add(...ThemeIcon.asClassNameArray(Codicon.diffMultiple));102103return dom.addDisposableListener(button, 'click', (e) => {104const source = URI.parse(`multi-diff-editor:${new Date().getMilliseconds().toString() + Math.random().toString()}`);105const input = this.instantiationService.createInstance(106MultiDiffEditorInput,107source,108this.content.multiDiffData.title || 'Multi-Diff',109this.content.multiDiffData.resources.map(resource => new MultiDiffEditorItem(110resource.originalUri,111resource.modifiedUri,112resource.goToFileUri113)),114false115);116const sideBySide = e.altKey;117this.editorService.openEditor(input, sideBySide ? SIDE_GROUP : ACTIVE_GROUP);118dom.EventHelper.stop(e, true);119});120}121122private renderContributedButtons(container: HTMLElement): IDisposable {123const buttonsContainer = container.appendChild($('.chat-multidiff-contributed-buttons'));124const disposables = new DisposableStore();125const actionBar = disposables.add(new ActionBar(buttonsContainer, {126orientation: ActionsOrientation.HORIZONTAL127}));128const setupActionBar = () => {129actionBar.clear();130131const activeEditorUri = this.editorService.activeEditor?.resource;132let marshalledUri: any | undefined = undefined;133let contextKeyService: IContextKeyService = this.contextKeyService;134if (activeEditorUri) {135const { authority } = activeEditorUri;136const overlay: [string, unknown][] = [];137if (authority) {138overlay.push([ChatContextKeys.sessionType.key, authority]);139}140contextKeyService = this.contextKeyService.createOverlay(overlay);141marshalledUri = {142...activeEditorUri,143$mid: MarshalledId.Uri144};145}146147const actions = this.menuService.getMenuActions(148MenuId.ChatMultiDiffContext,149contextKeyService,150{ arg: marshalledUri, shouldForwardArgs: true }151);152const allActions = actions.flatMap(([, actions]) => actions);153if (allActions.length > 0) {154actionBar.push(allActions, { icon: true, label: false });155}156};157setupActionBar();158return disposables;159}160161private renderFilesList(container: HTMLElement): IDisposable {162const store = new DisposableStore();163164const listContainer = container.appendChild($('.chat-summary-list'));165store.add(createFileIconThemableTreeContainerScope(listContainer, this.themeService));166const resourceLabels = store.add(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: Event.None }));167168this.list = store.add(this.instantiationService.createInstance(169WorkbenchList<IChatMultiDiffItem>,170'ChatMultiDiffList',171listContainer,172new ChatMultiDiffListDelegate(),173[this.instantiationService.createInstance(ChatMultiDiffListRenderer, resourceLabels)],174{175identityProvider: {176getId: (element: IChatMultiDiffItem) => element.uri.toString()177},178setRowLineHeight: true,179horizontalScrolling: false,180supportDynamicHeights: false,181mouseSupport: true,182alwaysConsumeMouseWheel: false,183accessibilityProvider: {184getAriaLabel: (element: IChatMultiDiffItem) => element.uri.path,185getWidgetAriaLabel: () => localize('chatMultiDiffList', "File Changes")186}187}188));189190const items: IChatMultiDiffItem[] = [];191for (const resource of this.content.multiDiffData.resources) {192const uri = resource.modifiedUri || resource.originalUri || resource.goToFileUri;193if (!uri) {194continue;195}196197const item: IChatMultiDiffItem = { uri };198199if (resource.originalUri && resource.modifiedUri) {200item.diff = {201originalURI: resource.originalUri,202modifiedURI: resource.modifiedUri,203quitEarly: false,204identical: false,205added: resource.added || 0,206removed: resource.removed || 0207};208}209items.push(item);210}211212this.list.splice(0, this.list.length, items);213214const height = Math.min(items.length, MAX_ITEMS_SHOWN) * ELEMENT_HEIGHT;215this.list.layout(height);216listContainer.style.height = `${height}px`;217218store.add(this.list.onDidOpen((e) => {219if (!e.element) {220return;221}222223if (e.element.diff) {224this.editorService.openEditor({225original: { resource: e.element.diff.originalURI },226modified: { resource: e.element.diff.modifiedURI },227options: { preserveFocus: true }228});229} else {230this.editorService.openEditor({231resource: e.element.uri,232options: { preserveFocus: true }233});234}235}));236237return store;238}239240hasSameContent(other: IChatRendererContent): boolean {241return other.kind === 'multiDiffData' &&242(other as any).multiDiffData?.resources?.length === this.content.multiDiffData.resources.length;243}244245addDisposable(disposable: IDisposable): void {246this._register(disposable);247}248}249250class ChatMultiDiffListDelegate implements IListVirtualDelegate<IChatMultiDiffItem> {251getHeight(): number {252return 22;253}254255getTemplateId(): string {256return 'chatMultiDiffItem';257}258}259260interface IChatMultiDiffItemTemplate extends IDisposable {261readonly label: IResourceLabel;262}263264class ChatMultiDiffListRenderer implements IListRenderer<IChatMultiDiffItem, IChatMultiDiffItemTemplate> {265static readonly TEMPLATE_ID = 'chatMultiDiffItem';266static readonly CHANGES_SUMMARY_CLASS_NAME = 'insertions-and-deletions';267268readonly templateId: string = ChatMultiDiffListRenderer.TEMPLATE_ID;269270constructor(private labels: ResourceLabels) { }271272renderTemplate(container: HTMLElement): IChatMultiDiffItemTemplate {273const label = this.labels.create(container, { supportHighlights: true, supportIcons: true });274275return {276label,277dispose: () => label.dispose()278};279}280281renderElement(element: IChatMultiDiffItem, _index: number, templateData: IChatMultiDiffItemTemplate): void {282templateData.label.setFile(element.uri, {283fileKind: FileKind.FILE,284title: element.uri.path285});286287const labelElement = templateData.label.element;288labelElement.querySelector(`.${ChatMultiDiffListRenderer.CHANGES_SUMMARY_CLASS_NAME}`)?.remove();289290if (element.diff?.added || element.diff?.removed) {291const changesSummary = labelElement.appendChild($(`.${ChatMultiDiffListRenderer.CHANGES_SUMMARY_CLASS_NAME}`));292293const addedElement = changesSummary.appendChild($('.insertions'));294addedElement.textContent = `+${element.diff.added}`;295296const removedElement = changesSummary.appendChild($('.deletions'));297removedElement.textContent = `-${element.diff.removed}`;298299changesSummary.setAttribute('aria-label', localize('chatEditingSession.fileCounts', '{0} lines added, {1} lines removed', element.diff.added, element.diff.removed));300}301}302303disposeTemplate(templateData: IChatMultiDiffItemTemplate): void {304templateData.dispose();305}306}307308309