diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 3e8c92349cc..7ebdfdf00e8 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -395,6 +395,7 @@ export interface GitExtension { export const enum GitErrorCodes { BadConfigFile = 'BadConfigFile', + BadRevision = 'BadRevision', AuthenticationFailed = 'AuthenticationFailed', NoUserNameConfigured = 'NoUserNameConfigured', NoUserEmailConfigured = 'NoUserEmailConfigured', diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index f6b487e8b93..d9e998f7757 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -2927,6 +2927,19 @@ export class Repository { return commits[0]; } + async showCommit(ref: string): Promise { + try { + const result = await this.exec(['show', ref]); + return result.stdout.trim(); + } catch (err) { + if (/^fatal: bad revision '.+'/.test(err.stderr || '')) { + err.gitErrorCode = GitErrorCodes.BadRevision; + } + + throw err; + } + } + async revList(ref1: string, ref2: string): Promise { const result = await this.exec(['rev-list', `${ref1}..${ref2}`]); if (result.stderr) { diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 886510b0031..19f9dc52fda 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -344,6 +344,17 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return historyItemChanges; } + async resolveHistoryItemChatContext(historyItemId: string): Promise { + try { + const commitDetails = await this.repository.showCommit(historyItemId); + return commitDetails; + } catch (err) { + this.logger.error(`[GitHistoryProvider][resolveHistoryItemChatContext] Failed to resolve history item '${historyItemId}': ${err}`); + } + + return undefined; + } + async resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[]): Promise { try { if (historyItemRefs.length === 0) { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index f2c8146a407..3b8b445d644 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1705,6 +1705,10 @@ export class Repository implements Disposable { return await this.repository.getCommit(ref); } + async showCommit(ref: string): Promise { + return await this.run(Operation.Show, () => this.repository.showCommit(ref)); + } + async getEmptyTree(): Promise { if (!this._EMPTY_TREE) { const result = await this.repository.exec(['hash-object', '-t', 'tree', '/dev/null']); diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 3ac52e3e71d..9b27080d0cc 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -195,6 +195,10 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { } + async resolveHistoryItemChatContext(historyItemId: string): Promise { + return this.proxy.$resolveHistoryItemChatContext(this.handle, historyItemId, CancellationToken.None); + } + async resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[]): Promise { return this.proxy.$resolveHistoryItemRefsCommonAncestor(this.handle, historyItemRefs, CancellationToken.None); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 0d175b9d310..8e8837478da 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2563,6 +2563,7 @@ export interface ExtHostSCMShape { $provideHistoryItemRefs(sourceControlHandle: number, historyItemRefs: string[] | undefined, token: CancellationToken): Promise; $provideHistoryItems(sourceControlHandle: number, options: any, token: CancellationToken): Promise; $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; + $resolveHistoryItemChatContext(sourceControlHandle: number, historyItemId: string, token: CancellationToken): Promise; $resolveHistoryItemRefsCommonAncestor(sourceControlHandle: number, historyItemRefs: string[], token: CancellationToken): Promise; } diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 487f91504eb..eb77029e5f4 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -1046,6 +1046,19 @@ export class ExtHostSCM implements ExtHostSCMShape { return Promise.resolve(undefined); } + async $resolveHistoryItemChatContext(sourceControlHandle: number, historyItemId: string, token: CancellationToken): Promise { + try { + const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; + const chatContext = await historyProvider?.resolveHistoryItemChatContext(historyItemId, token); + + return chatContext ?? undefined; + } + catch (err) { + this.logService.error('ExtHostSCM#$resolveHistoryItemChatContext', err); + return undefined; + } + } + async $resolveHistoryItemRefsCommonAncestor(sourceControlHandle: number, historyItemRefs: string[], token: CancellationToken): Promise { try { const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts index e1dafd98d1f..deb814d3f6f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts @@ -25,7 +25,7 @@ import { IOpenerService, OpenInternalOptions } from '../../../../platform/opener import { IThemeService, FolderThemeIcon } from '../../../../platform/theme/common/themeService.js'; import { IResourceLabel, ResourceLabels, IFileLabelOptions } from '../../../browser/labels.js'; import { revealInSideBarCommand } from '../../files/browser/fileActions.contribution.js'; -import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, OmittedState } from '../common/chatModel.js'; +import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, ISCMHistoryItemVariableEntry, OmittedState } from '../common/chatModel.js'; import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js'; import { chatAttachmentResourceContextKey } from './chatContentParts/chatAttachmentsContentPart.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; @@ -51,7 +51,6 @@ import { IContextMenuService } from '../../../../platform/contextview/browser/co import { ResourceContextKey } from '../../../common/contextkeys.js'; import { Location, SymbolKind } from '../../../../editor/common/languages.js'; - abstract class AbstractChatAttachmentWidget extends Disposable { public readonly element: HTMLElement; public readonly label: IResourceLabel; @@ -597,6 +596,47 @@ export class ElementChatAttachmentWidget extends AbstractChatAttachmentWidget { } } +export class SCMHistoryItemAttachmentWidget extends AbstractChatAttachmentWidget { + constructor( + attachment: ISCMHistoryItemVariableEntry, + currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined, + options: { shouldFocusClearButton: boolean; supportsDeletion: boolean }, + container: HTMLElement, + contextResourceLabels: ResourceLabels, + hoverDelegate: IHoverDelegate, + @ICommandService commandService: ICommandService, + @IOpenerService openerService: IOpenerService + ) { + super(attachment, options, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService); + + this.label.setLabel(attachment.name, undefined); + + this.element.style.cursor = 'pointer'; + this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); + + this._store.add(dom.addDisposableListener(this.element, dom.EventType.CLICK, (e: MouseEvent) => { + dom.EventHelper.stop(e, true); + this._openAttachment(attachment); + })); + + this._store.add(dom.addDisposableListener(this.element, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { + dom.EventHelper.stop(e, true); + this._openAttachment(attachment); + } + })); + + this.attachClearButton(); + } + + private async _openAttachment(attachment: ISCMHistoryItemVariableEntry): Promise { + await this.commandService.executeCommand('_workbench.openMultiDiffEditor', { + title: attachment.title, multiDiffSourceUri: attachment.value + }); + } +} + export function hookUpResourceAttachmentDragAndContextMenu(accessor: ServicesAccessor, widget: HTMLElement, resource: URI): IDisposable { const contextKeyService = accessor.get(IContextKeyService); const instantiationService = accessor.get(IInstantiationService); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index 2a1e1a95361..89bae566457 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -14,9 +14,9 @@ import { localize } from '../../../../../nls.js'; import { RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ResourceLabels } from '../../../../browser/labels.js'; -import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry } from '../../common/chatModel.js'; +import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isSCMHistoryItemVariableEntry } from '../../common/chatModel.js'; import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../common/chatService.js'; -import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget } from '../chatAttachmentWidgets.js'; +import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, SCMHistoryItemAttachmentWidget } from '../chatAttachmentWidgets.js'; export const chatAttachmentResourceContextKey = new RawContextKey('chatAttachmentResource', undefined, { type: 'URI', description: localize('resource', "The full value of the chat attachment resource, including scheme and path") }); @@ -62,6 +62,8 @@ export class ChatAttachmentsContentPart extends Disposable { widget = this.instantiationService.createInstance(PasteAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); } else if (resource && isNotebookOutputVariableEntry(attachment)) { widget = this.instantiationService.createInstance(NotebookCellOutputChatAttachmentWidget, resource, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isSCMHistoryItemVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(SCMHistoryItemAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); } else { widget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 59dd28bc75a..aac5c735f64 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -71,7 +71,7 @@ import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEd import { IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IChatEditingSession } from '../common/chatEditingService.js'; -import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry } from '../common/chatModel.js'; +import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isSCMHistoryItemVariableEntry } from '../common/chatModel.js'; import { IChatFollowup } from '../common/chatService.js'; import { IChatVariablesService } from '../common/chatVariables.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; @@ -84,7 +84,7 @@ import { PromptInstructionsAttachmentsCollectionWidget } from './attachments/pro import { IChatWidget } from './chat.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; import { toChatVariable } from './chatAttachmentModel/chatPromptAttachmentsCollection.js'; -import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget } from './chatAttachmentWidgets.js'; +import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, SCMHistoryItemAttachmentWidget } from './chatAttachmentWidgets.js'; import { IDisposableReference } from './chatContentParts/chatCollections.js'; import { CollapsibleListPool, IChatCollapsibleListItem } from './chatContentParts/chatReferencesContentPart.js'; import { ChatDragAndDrop } from './chatDragAndDrop.js'; @@ -1240,6 +1240,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge attachmentWidget = this.instantiationService.createInstance(ElementChatAttachmentWidget, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate); } else if (isPasteVariableEntry(attachment)) { attachmentWidget = this.instantiationService.createInstance(PasteAttachmentWidget, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate); + } else if (isSCMHistoryItemVariableEntry(attachment)) { + attachmentWidget = this.instantiationService.createInstance(SCMHistoryItemAttachmentWidget, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate); } else { attachmentWidget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate); } diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index a713a2fb4c0..22f40b434cf 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -203,10 +203,16 @@ export interface IPromptFileVariableEntry extends IBaseChatRequestVariableEntry readonly kind: 'promptFile'; } +export interface ISCMHistoryItemVariableEntry extends IBaseChatRequestVariableEntry { + readonly kind: 'scmHistoryItem'; + readonly title: string; + readonly value: URI; +} + export type IChatRequestVariableEntry = IGenericChatRequestVariableEntry | IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry | ISymbolVariableEntry | ICommandResultVariableEntry | IDiagnosticVariableEntry | IImageVariableEntry | IChatRequestToolEntry | IChatRequestDirectoryEntry | IChatRequestFileEntry | INotebookOutputVariableEntry | IElementVariableEntry - | IPromptFileVariableEntry; + | IPromptFileVariableEntry | ISCMHistoryItemVariableEntry; export function isImplicitVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestImplicitVariableEntry { return obj.kind === 'implicit'; @@ -244,6 +250,10 @@ export function isChatRequestVariableEntry(obj: unknown): obj is IChatRequestVar typeof entry.name === 'string'; } +export function isSCMHistoryItemVariableEntry(obj: IChatRequestVariableEntry): obj is ISCMHistoryItemVariableEntry { + return obj.kind === 'scmHistoryItem'; +} + export interface IChatRequestVariableData { variables: IChatRequestVariableEntry[]; } diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts index 73903ffaf42..3d2e0c2a4ea 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts @@ -15,7 +15,7 @@ import { IInstantiationService, ServicesAccessor } from '../../../../platform/in import { IActivityService, ProgressBadge } from '../../../services/activity/common/activity.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ISCMHistoryItem } from '../../scm/common/history.js'; -import { ISCMRepository, ISCMResourceGroup, ISCMService } from '../../scm/common/scm.js'; +import { ISCMProvider, ISCMRepository, ISCMResourceGroup, ISCMService } from '../../scm/common/scm.js'; import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from './multiDiffSourceResolverService.js'; export class ScmMultiDiffSourceResolver implements IMultiDiffSourceResolver { @@ -92,42 +92,24 @@ interface ScmHistoryItemUriFields { } export class ScmHistoryItemResolver implements IMultiDiffSourceResolver { - private static readonly _scheme = 'scm-history-item'; + static readonly scheme = 'scm-history-item'; - public static getMultiDiffSourceUri(repositoryId: string, historyItem: ISCMHistoryItem): URI { + public static getMultiDiffSourceUri(provider: ISCMProvider, historyItem: ISCMHistoryItem): URI { const historyItemParentId = historyItem.parentIds.length > 0 ? historyItem.parentIds[0] : undefined; return URI.from({ - scheme: ScmHistoryItemResolver._scheme, + scheme: ScmHistoryItemResolver.scheme, + path: provider.rootUri?.fsPath, query: JSON.stringify({ - repositoryId, + repositoryId: provider.id, historyItemId: historyItem.id, historyItemParentId } satisfies ScmHistoryItemUriFields) }, true); } - constructor(@ISCMService private readonly _scmService: ISCMService) { } - - canHandleUri(uri: URI): boolean { - return this._parseUri(uri) !== undefined; - } - - async resolveDiffSource(uri: URI): Promise { - const { repositoryId, historyItemId, historyItemParentId } = this._parseUri(uri)!; - - const repository = this._scmService.getRepository(repositoryId); - const historyProvider = repository?.provider.historyProvider.get(); - const historyItemChanges = await historyProvider?.provideHistoryItemChanges(historyItemId, historyItemParentId) ?? []; - - const resources = ValueWithChangeEvent.const( - historyItemChanges.map(change => new MultiDiffEditorItem(change.originalUri, change.modifiedUri, change.uri))); - - return { resources }; - } - - private _parseUri(uri: URI): ScmHistoryItemUriFields | undefined { - if (uri.scheme !== ScmHistoryItemResolver._scheme) { + public static parseUri(uri: URI): ScmHistoryItemUriFields | undefined { + if (uri.scheme !== ScmHistoryItemResolver.scheme) { return undefined; } @@ -150,6 +132,25 @@ export class ScmHistoryItemResolver implements IMultiDiffSourceResolver { return { repositoryId, historyItemId, historyItemParentId }; } + + constructor(@ISCMService private readonly _scmService: ISCMService) { } + + canHandleUri(uri: URI): boolean { + return ScmHistoryItemResolver.parseUri(uri) !== undefined; + } + + async resolveDiffSource(uri: URI): Promise { + const { repositoryId, historyItemId, historyItemParentId } = ScmHistoryItemResolver.parseUri(uri)!; + + const repository = this._scmService.getRepository(repositoryId); + const historyProvider = repository?.provider.historyProvider.get(); + const historyItemChanges = await historyProvider?.provideHistoryItemChanges(historyItemId, historyItemParentId) ?? []; + + const resources = ValueWithChangeEvent.const( + historyItemChanges.map(change => new MultiDiffEditorItem(change.originalUri, change.modifiedUri, change.uri))); + + return { resources }; + } } class ScmResolvedMultiDiffSource implements IResolvedMultiDiffSource { diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 22199e56e6d..8cb2bb41254 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -45,6 +45,7 @@ import { RemoteNameContext } from '../../../common/contextkeys.js'; import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { SCMAccessibilityHelp } from './scmAccessibilityHelp.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; +import { SCMHistoryItemContextContribution } from './scmHistoryChatContext.js'; ModesRegistry.registerLanguage({ id: 'scminput', @@ -164,6 +165,12 @@ registerWorkbenchContribution2( WorkbenchPhase.AfterRestored ); +registerWorkbenchContribution2( + SCMHistoryItemContextContribution.ID, + SCMHistoryItemContextContribution, + WorkbenchPhase.AfterRestored +); + Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ id: 'scm', order: 5, diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryChatContext.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryChatContext.ts new file mode 100644 index 00000000000..47c7d1f4f54 --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryChatContext.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { coalesce } from '../../../../base/common/arrays.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { URI } from '../../../../base/common/uri.js'; +import { ITextModel } from '../../../../editor/common/model.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { localize } from '../../../../nls.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { IChatWidget } from '../../chat/browser/chat.js'; +import { IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPickService } from '../../chat/browser/chatContextPickService.js'; +import { ISCMHistoryItemVariableEntry } from '../../chat/common/chatModel.js'; +import { ScmHistoryItemResolver } from '../../multiDiffEditor/browser/scmMultiDiffSourceResolver.js'; +import { ISCMService, ISCMViewService } from '../common/scm.js'; +import { getHistoryItemEditorTitle } from './util.js'; + +export class SCMHistoryItemContextContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.chat.scmHistoryItemContextContribution'; + + constructor( + @IChatContextPickService contextPickService: IChatContextPickService, + @IInstantiationService instantiationService: IInstantiationService, + @ITextModelService textModelResolverService: ITextModelService + ) { + super(); + this._store.add(contextPickService.registerChatContextItem( + instantiationService.createInstance(SCMHistoryItemContext))); + + this._store.add(textModelResolverService.registerTextModelContentProvider( + ScmHistoryItemResolver.scheme, + instantiationService.createInstance(SCMHistoryItemContextContentProvider))); + } +} + +class SCMHistoryItemContext implements IChatContextPickerItem { + readonly type = 'pickerPick'; + readonly label = localize('chatContext.scmHistoryItems', 'Source Control History Items...'); + readonly icon = Codicon.gitCommit; + + constructor( + @ISCMViewService private readonly _scmViewService: ISCMViewService + ) { } + + isEnabled(_widget: IChatWidget): Promise | boolean { + const activeRepository = this._scmViewService.activeRepository.get(); + return activeRepository?.provider.historyProvider.get() !== undefined; + } + + asPicker(_widget: IChatWidget) { + return { + placeholder: localize('chatContext.scmHistoryItems.placeholder', 'Select a source control history item'), + picks: async (_query: string) => { + const activeRepository = this._scmViewService.activeRepository.get(); + const historyProvider = activeRepository?.provider.historyProvider.get(); + if (!activeRepository || !historyProvider) { + return []; + } + + const historyItemRefs = coalesce([ + historyProvider.historyItemRef.get(), + historyProvider.historyItemRemoteRef.get(), + historyProvider.historyItemBaseRef.get(), + ]).map(ref => ref.id); + + const historyItems = await historyProvider.provideHistoryItems({ historyItemRefs, limit: 100 }) ?? []; + + return historyItems.map(historyItem => ({ + iconClass: ThemeIcon.asClassName(Codicon.gitCommit), + label: historyItem.subject, + description: historyItem.displayId ?? historyItem.id, + asAttachment: () => { + const historyItemTitle = getHistoryItemEditorTitle(historyItem); + const multiDiffSourceUri = ScmHistoryItemResolver.getMultiDiffSourceUri(activeRepository.provider, historyItem); + const attachmentName = `$(${Codicon.repo.id})\u00A0${activeRepository.provider.name}\u00A0$(${Codicon.gitCommit.id})\u00A0${historyItem.displayId ?? historyItem.id}`; + + return { + id: historyItem.id, + name: attachmentName, + value: multiDiffSourceUri, + title: historyItemTitle, + kind: 'scmHistoryItem' + } satisfies ISCMHistoryItemVariableEntry; + } + }) satisfies IChatContextPickerPickItem); + } + }; + } +} + +class SCMHistoryItemContextContentProvider implements ITextModelContentProvider { + constructor( + @IModelService private readonly _modelService: IModelService, + @ISCMService private readonly _scmService: ISCMService + ) { } + + async provideTextContent(resource: URI): Promise { + const uriFields = ScmHistoryItemResolver.parseUri(resource); + if (!uriFields) { + return null; + } + + const textModel = this._modelService.getModel(resource); + if (textModel) { + return textModel; + } + + const { repositoryId, historyItemId } = uriFields; + const repository = this._scmService.getRepository(repositoryId); + const historyProvider = repository?.provider.historyProvider.get(); + if (!repository || !historyProvider) { + return null; + } + + const historyItemContext = await historyProvider.resolveHistoryItemChatContext(historyItemId); + if (!historyItemContext) { + return null; + } + + return this._modelService.createModel(historyItemContext, null, resource, false); + } +} diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 3e0f25163ea..5f8eefab77b 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -286,7 +286,7 @@ registerAction2(class extends Action2 { getHistoryItemEditorTitle(historyItem) : localize('historyItemChangesEditorTitle', "All Changes ({0} ↔ {1})", historyItemLast.displayId ?? historyItemLast.id, historyItem.displayId ?? historyItem.id); - const multiDiffSourceUri = ScmHistoryItemResolver.getMultiDiffSourceUri(provider.id, historyItem); + const multiDiffSourceUri = ScmHistoryItemResolver.getMultiDiffSourceUri(provider, historyItem); commandService.executeCommand('_workbench.openMultiDiffEditor', { title, multiDiffSourceUri }); } }); diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index c362c8ab3bd..0a48cc4043b 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -24,6 +24,7 @@ export interface ISCMHistoryProvider { provideHistoryItemRefs(historyItemsRefs?: string[]): Promise; provideHistoryItems(options: ISCMHistoryOptions): Promise; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise; + resolveHistoryItemChatContext(historyItemId: string): Promise; resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[]): Promise; } diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index 9eedd16d767..10276173374 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -30,6 +30,7 @@ declare module 'vscode' { provideHistoryItems(options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult; + resolveHistoryItemChatContext(historyItemId: string, token: CancellationToken): ProviderResult; resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[], token: CancellationToken): ProviderResult; }