diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index e04a1a754c7..fc623e5924b 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -3053,9 +3053,27 @@ export class Repository { return commits[0]; } - async showCommit(ref: string): Promise { + async showChanges(ref: string): Promise { try { - const result = await this.exec(['show', ref]); + const result = await this.exec(['log', '-p', '-n1', ref, '--']); + return result.stdout.trim(); + } catch (err) { + if (/^fatal: bad revision '.+'/.test(err.stderr || '')) { + err.gitErrorCode = GitErrorCodes.BadRevision; + } + + throw err; + } + } + + async showChangesBetween(ref1: string, ref2: string, path?: string): Promise { + try { + const args = ['log', '-p', `${ref1}..${ref2}`, '--']; + if (path) { + args.push(this.sanitizeRelativePath(path)); + } + + const result = await this.exec(args); return result.stdout.trim(); } catch (err) { if (/^fatal: bad revision '.+'/.test(err.stderr || '')) { diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index f9270d0f2ab..8f440b08c97 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -408,8 +408,8 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec async resolveHistoryItemChatContext(historyItemId: string): Promise { try { - const commitDetails = await this.repository.showCommit(historyItemId); - return commitDetails; + const changes = await this.repository.showChanges(historyItemId); + return changes; } catch (err) { this.logger.error(`[GitHistoryProvider][resolveHistoryItemChatContext] Failed to resolve history item '${historyItemId}': ${err}`); } @@ -417,6 +417,22 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return undefined; } + async resolveHistoryItemChangeRangeChatContext(historyItemId: string, historyItemParentId: string, path: string, token: CancellationToken): Promise { + try { + const changes = await this.repository.showChangesBetween(historyItemParentId, historyItemId, path); + + if (token.isCancellationRequested) { + return undefined; + } + + return `Output of git log -p ${historyItemParentId}..${historyItemId} -- ${path}:\n\n${changes}`; + } catch (err) { + this.logger.error(`[GitHistoryProvider][resolveHistoryItemChangeRangeChatContext] Failed to resolve history item change range '${historyItemId}' for '${path}': ${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 35aa9b99ce2..619a2c70e9f 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1813,8 +1813,12 @@ 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 showChanges(ref: string): Promise { + return await this.run(Operation.Log(false), () => this.repository.showChanges(ref)); + } + + async showChangesBetween(ref1: string, ref2: string, path?: string): Promise { + return await this.run(Operation.Log(false), () => this.repository.showChangesBetween(ref1, ref2, path)); } async getEmptyTree(): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 0e24b06f64f..de23ab0e428 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -204,6 +204,10 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { return this.proxy.$resolveHistoryItemChatContext(this.handle, historyItemId, token ?? CancellationToken.None); } + async resolveHistoryItemChangeRangeChatContext(historyItemId: string, historyItemParentId: string, path: string, token?: CancellationToken): Promise { + return this.proxy.$resolveHistoryItemChangeRangeChatContext(this.handle, historyItemId, historyItemParentId, path, token ?? CancellationToken.None); + } + async resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[], token: CancellationToken): Promise { return this.proxy.$resolveHistoryItemRefsCommonAncestor(this.handle, historyItemRefs, token ?? CancellationToken.None); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 84f618ad7e4..0c4bf617744 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2584,6 +2584,7 @@ export interface ExtHostSCMShape { $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; $resolveHistoryItem(sourceControlHandle: number, historyItemId: string, token: CancellationToken): Promise; $resolveHistoryItemChatContext(sourceControlHandle: number, historyItemId: string, token: CancellationToken): Promise; + $resolveHistoryItemChangeRangeChatContext(sourceControlHandle: number, historyItemId: string, historyItemParentId: string, path: 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 67fcb734572..3988f4adc5e 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -1115,6 +1115,19 @@ export class ExtHostSCM implements ExtHostSCMShape { } } + async $resolveHistoryItemChangeRangeChatContext(sourceControlHandle: number, historyItemId: string, historyItemParentId: string, path: string, token: CancellationToken): Promise { + try { + const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; + const chatContext = await historyProvider?.resolveHistoryItemChangeRangeChatContext?.(historyItemId, historyItemParentId, path, token); + + return chatContext ?? undefined; + } + catch (err) { + this.logService.error('ExtHostSCM#$resolveHistoryItemChangeRangeChatContext', 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/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 7c18f199238..1213829c86f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -78,8 +78,9 @@ import { ChatViewPane } from '../chatViewPane.js'; import { convertBufferToScreenshotVariable } from '../contrib/screenshot.js'; import { clearChatEditor } from './chatClear.js'; import { ISCMService } from '../../../scm/common/scm.js'; -import { ISCMHistoryItemChangeVariableEntry } from '../../common/chatVariableEntries.js'; +import { ISCMHistoryItemChangeRangeVariableEntry, ISCMHistoryItemChangeVariableEntry } from '../../common/chatVariableEntries.js'; import { basename } from '../../../../../base/common/resources.js'; +import { SCMHistoryItemChangeRangeContentProvider, ScmHistoryItemChangeRangeUriFields } from '../../../scm/browser/scmHistoryChatContext.js'; export const CHAT_CATEGORY = localize2('chat.category', 'Chat'); @@ -119,6 +120,13 @@ export interface IChatViewOpenOptions { * A list of source control history item changes to attach to the chat as context. */ attachHistoryItemChanges?: { uri: URI; historyItemId: string }[]; + /** + * A list of source control history item change ranges to attach to the chat as context. + */ + attachHistoryItemChangeRanges?: { + start: { uri: URI; historyItemId: string }; + end: { uri: URI; historyItemId: string }; + }[]; /** * The mode ID or name to open the chat in. */ @@ -267,6 +275,50 @@ abstract class OpenChatGlobalAction extends Action2 { } satisfies ISCMHistoryItemChangeVariableEntry); } } + if (opts?.attachHistoryItemChangeRanges) { + for (const historyItemChangeRange of opts.attachHistoryItemChangeRanges) { + const repository = scmService.getRepository(URI.file(historyItemChangeRange.end.uri.path)); + const historyProvider = repository?.provider.historyProvider.get(); + if (!repository || !historyProvider) { + continue; + } + + const [historyItemStart, historyItemEnd] = await Promise.all([ + historyProvider.resolveHistoryItem(historyItemChangeRange.start.historyItemId), + historyProvider.resolveHistoryItem(historyItemChangeRange.end.historyItemId), + ]); + if (!historyItemStart || !historyItemEnd) { + continue; + } + + const uri = historyItemChangeRange.end.uri.with({ + scheme: SCMHistoryItemChangeRangeContentProvider.scheme, + query: JSON.stringify({ + repositoryId: repository.id, + start: historyItemStart.id, + end: historyItemChangeRange.end.historyItemId + } satisfies ScmHistoryItemChangeRangeUriFields) + }); + + chatWidget.attachmentModel.addContext({ + id: uri.toString(), + name: `${basename(uri)}`, + value: uri, + historyItemChangeStart: { + uri: historyItemChangeRange.start.uri, + historyItem: historyItemStart + }, + historyItemChangeEnd: { + uri: historyItemChangeRange.end.uri, + historyItem: { + ...historyItemEnd, + displayId: historyItemChangeRange.end.historyItemId + } + }, + kind: 'scmHistoryItemChangeRange' + } satisfies ISCMHistoryItemChangeRangeVariableEntry); + } + } let resp: Promise | undefined; diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts index 9da07dc37ed..b54af041321 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts @@ -50,7 +50,7 @@ import { CellUri } from '../../notebook/common/notebookCommon.js'; import { INotebookService } from '../../notebook/common/notebookService.js'; import { getHistoryItemEditorTitle, getHistoryItemHoverContent } from '../../scm/browser/util.js'; import { IChatContentReference } from '../common/chatService.js'; -import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, IPromptFileVariableEntry, IPromptTextVariableEntry, ISCMHistoryItemVariableEntry, OmittedState, PromptFileVariableKind, ChatRequestToolReferenceEntry, ISCMHistoryItemChangeVariableEntry } from '../common/chatVariableEntries.js'; +import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, IPromptFileVariableEntry, IPromptTextVariableEntry, ISCMHistoryItemVariableEntry, OmittedState, PromptFileVariableKind, ChatRequestToolReferenceEntry, ISCMHistoryItemChangeVariableEntry, ISCMHistoryItemChangeRangeVariableEntry } from '../common/chatVariableEntries.js'; import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js'; import { ILanguageModelToolsService, ToolSet } from '../common/languageModelToolsService.js'; import { getCleanPromptName } from '../common/promptSyntax/config/promptFileLocations.js'; @@ -861,6 +861,50 @@ export class SCMHistoryItemChangeAttachmentWidget extends AbstractChatAttachment } } +export class SCMHistoryItemChangeRangeAttachmentWidget extends AbstractChatAttachmentWidget { + constructor( + attachment: ISCMHistoryItemChangeRangeVariableEntry, + currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined, + options: { shouldFocusClearButton: boolean; supportsDeletion: boolean }, + container: HTMLElement, + contextResourceLabels: ResourceLabels, + hoverDelegate: IHoverDelegate, + @ICommandService commandService: ICommandService, + @IOpenerService openerService: IOpenerService, + @IEditorService private readonly editorService: IEditorService, + ) { + super(attachment, options, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService); + + const historyItemStartId = attachment.historyItemChangeStart.historyItem.displayId ?? attachment.historyItemChangeStart.historyItem.id; + const historyItemEndId = attachment.historyItemChangeEnd.historyItem.displayId ?? attachment.historyItemChangeEnd.historyItem.id; + + const nameSuffix = `\u00A0$(${Codicon.gitCommit.id})${historyItemStartId}..${historyItemEndId}`; + this.label.setFile(attachment.value, { fileKind: FileKind.FILE, nameSuffix }); + + this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); + + this.addResourceOpenHandlers(attachment.value, undefined); + this.attachClearButton(); + } + + protected override async openResource(resource: URI, isDirectory: true): Promise; + protected override async openResource(resource: URI, isDirectory: false, range: IRange | undefined): Promise; + protected override async openResource(resource: URI, isDirectory?: boolean, range?: IRange): Promise { + const attachment = this.attachment as ISCMHistoryItemChangeRangeVariableEntry; + const historyItemChangeStart = attachment.historyItemChangeStart; + const historyItemChangeEnd = attachment.historyItemChangeEnd; + + const originalUriTitle = `${basename(historyItemChangeStart.uri.fsPath)} (${historyItemChangeStart.historyItem.displayId ?? historyItemChangeStart.historyItem.id})`; + const modifiedUriTitle = `${basename(historyItemChangeEnd.uri.fsPath)} (${historyItemChangeEnd.historyItem.displayId ?? historyItemChangeEnd.historyItem.id})`; + + await this.editorService.openEditor({ + original: { resource: historyItemChangeStart.uri }, + modified: { resource: historyItemChangeEnd.uri }, + label: `${originalUriTitle} ↔ ${modifiedUriTitle}` + }); + } +} + 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 6c14e4c0895..aefdbc56bf6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -12,9 +12,9 @@ import { URI } from '../../../../../base/common/uri.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ResourceLabels } from '../../../../browser/labels.js'; -import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, OmittedState } from '../../common/chatVariableEntries.js'; +import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, OmittedState } from '../../common/chatVariableEntries.js'; import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../common/chatService.js'; -import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../chatAttachmentWidgets.js'; +import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../chatAttachmentWidgets.js'; export class ChatAttachmentsContentPart extends Disposable { private readonly attachedContextDisposables = this._register(new DisposableStore()); @@ -79,6 +79,8 @@ export class ChatAttachmentsContentPart extends Disposable { widget = this.instantiationService.createInstance(SCMHistoryItemAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); } else if (isSCMHistoryItemChangeVariableEntry(attachment)) { widget = this.instantiationService.createInstance(SCMHistoryItemChangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isSCMHistoryItemChangeRangeVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(SCMHistoryItemChangeRangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); } else { widget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, correspondingContentReference, 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 2927f2805b2..1b9259e7430 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -81,7 +81,7 @@ import { ChatEntitlement, IChatEntitlementService } from '../../../services/chat import { IChatRequestModeInfo } from '../common/chatModel.js'; import { ChatMode, IChatMode, IChatModeService } from '../common/chatModes.js'; import { IChatFollowup } from '../common/chatService.js'; -import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry } from '../common/chatVariableEntries.js'; +import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry } from '../common/chatVariableEntries.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; import { ChatInputHistoryMaxEntries, IChatHistoryEntry, IChatInputState, IChatWidgetHistoryService } from '../common/chatWidgetHistoryService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind, validateChatMode } from '../common/constants.js'; @@ -93,7 +93,7 @@ import { CancelAction, ChatEditingSessionSubmitAction, ChatOpenModelPickerAction import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js'; import { IChatWidget } from './chat.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; -import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from './chatAttachmentWidgets.js'; +import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from './chatAttachmentWidgets.js'; import { IDisposableReference } from './chatContentParts/chatCollections.js'; import { CollapsibleListPool, IChatCollapsibleListItem } from './chatContentParts/chatReferencesContentPart.js'; import { ChatDragAndDrop } from './chatDragAndDrop.js'; @@ -1408,6 +1408,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge attachmentWidget = this.instantiationService.createInstance(SCMHistoryItemAttachmentWidget, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate); } else if (isSCMHistoryItemChangeVariableEntry(attachment)) { attachmentWidget = this.instantiationService.createInstance(SCMHistoryItemChangeAttachmentWidget, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate); + } else if (isSCMHistoryItemChangeRangeVariableEntry(attachment)) { + attachmentWidget = this.instantiationService.createInstance(SCMHistoryItemChangeRangeAttachmentWidget, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate); } else { attachmentWidget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, undefined, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate); } diff --git a/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts index b164909dccc..1a820718a9c 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts @@ -215,12 +215,25 @@ export interface ISCMHistoryItemChangeVariableEntry extends IBaseChatRequestVari readonly historyItem: ISCMHistoryItem; } +export interface ISCMHistoryItemChangeRangeVariableEntry extends IBaseChatRequestVariableEntry { + readonly kind: 'scmHistoryItemChangeRange'; + readonly value: URI; + readonly historyItemChangeStart: { + readonly uri: URI; + readonly historyItem: ISCMHistoryItem; + }; + readonly historyItemChangeEnd: { + readonly uri: URI; + readonly historyItem: ISCMHistoryItem; + }; +} + export type IChatRequestVariableEntry = IGenericChatRequestVariableEntry | IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry | ISymbolVariableEntry | ICommandResultVariableEntry | IDiagnosticVariableEntry | IImageVariableEntry | IChatRequestToolEntry | IChatRequestToolSetEntry | IChatRequestDirectoryEntry | IChatRequestFileEntry | INotebookOutputVariableEntry | IElementVariableEntry | IPromptFileVariableEntry | IPromptTextVariableEntry - | ISCMHistoryItemVariableEntry | ISCMHistoryItemChangeVariableEntry; + | ISCMHistoryItemVariableEntry | ISCMHistoryItemChangeVariableEntry | ISCMHistoryItemChangeRangeVariableEntry; export namespace IChatRequestVariableEntry { @@ -289,6 +302,10 @@ export function isSCMHistoryItemChangeVariableEntry(obj: IChatRequestVariableEnt return obj.kind === 'scmHistoryItemChange'; } +export function isSCMHistoryItemChangeRangeVariableEntry(obj: IChatRequestVariableEntry): obj is ISCMHistoryItemChangeRangeVariableEntry { + return obj.kind === 'scmHistoryItemChangeRange'; +} + export enum PromptFileVariableKind { Instruction = 'vscode.prompt.instructions.root', InstructionReference = `vscode.prompt.instructions`, diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryChatContext.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryChatContext.ts index 580bfdbd665..a41865b6a61 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryChatContext.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryChatContext.ts @@ -64,6 +64,10 @@ export class SCMHistoryItemContextContribution extends Disposable implements IWo this._store.add(textModelResolverService.registerTextModelContentProvider( ScmHistoryItemResolver.scheme, instantiationService.createInstance(SCMHistoryItemContextContentProvider))); + + this._store.add(textModelResolverService.registerTextModelContentProvider( + SCMHistoryItemChangeRangeContentProvider.scheme, + instantiationService.createInstance(SCMHistoryItemChangeRangeContentProvider))); } } @@ -182,6 +186,70 @@ class SCMHistoryItemContextContentProvider implements ITextModelContentProvider } } +export interface ScmHistoryItemChangeRangeUriFields { + readonly repositoryId: string; + readonly start: string; + readonly end: string; +} + +export class SCMHistoryItemChangeRangeContentProvider implements ITextModelContentProvider { + static readonly scheme = 'scm-history-item-change-range'; + constructor( + @IModelService private readonly _modelService: IModelService, + @ISCMService private readonly _scmService: ISCMService + ) { } + + async provideTextContent(resource: URI): Promise { + const uriFields = this._parseUri(resource); + if (!uriFields) { + return null; + } + + const textModel = this._modelService.getModel(resource); + if (textModel) { + return textModel; + } + + const { repositoryId, start, end } = uriFields; + const repository = this._scmService.getRepository(repositoryId); + const historyProvider = repository?.provider.historyProvider.get(); + if (!repository || !historyProvider) { + return null; + } + + const historyItemChangeRangeContext = await historyProvider.resolveHistoryItemChangeRangeChatContext(end, start, resource.path); + if (!historyItemChangeRangeContext) { + return null; + } + + return this._modelService.createModel(historyItemChangeRangeContext, null, resource, false); + } + + private _parseUri(uri: URI): ScmHistoryItemChangeRangeUriFields | undefined { + if (uri.scheme !== SCMHistoryItemChangeRangeContentProvider.scheme) { + return undefined; + } + + let query: ScmHistoryItemChangeRangeUriFields; + try { + query = JSON.parse(uri.query) as ScmHistoryItemChangeRangeUriFields; + } catch (e) { + return undefined; + } + + if (typeof query !== 'object' || query === null) { + return undefined; + } + + const { repositoryId, start, end } = query; + if (typeof repositoryId !== 'string' || typeof start !== 'string' || typeof end !== 'string') { + return undefined; + } + + return { repositoryId, start, end }; + } +} + registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index 007e420e216..77e8b714319 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -27,6 +27,7 @@ export interface ISCMHistoryProvider { provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined, token?: CancellationToken): Promise; resolveHistoryItem(historyItemId: string, token?: CancellationToken): Promise; resolveHistoryItemChatContext(historyItemId: string, token?: CancellationToken): Promise; + resolveHistoryItemChangeRangeChatContext(historyItemId: string, historyItemParentId: string, path: string, token?: CancellationToken): Promise; resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[], token?: CancellationToken): Promise; } diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index b094f4e3854..944cdd3935c 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -32,6 +32,7 @@ declare module 'vscode' { resolveHistoryItem(historyItemId: string, token: CancellationToken): ProviderResult; resolveHistoryItemChatContext(historyItemId: string, token: CancellationToken): ProviderResult; + resolveHistoryItemChangeRangeChatContext(historyItemId: string, historyItemParentId: string, path: string, token: CancellationToken): ProviderResult; resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[], token: CancellationToken): ProviderResult; }