mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-19 14:22:19 +01:00
SCM - add the capability to attach a history item as chat context (#249073)
* Saving my work * Got the variable widget working * Refactored the variable widget * Clean up the attachment widget * Content provider stub * Add proposed API for chat context * Add method to get the complete details of a commit
This commit is contained in:
Vendored
+1
@@ -395,6 +395,7 @@ export interface GitExtension {
|
||||
|
||||
export const enum GitErrorCodes {
|
||||
BadConfigFile = 'BadConfigFile',
|
||||
BadRevision = 'BadRevision',
|
||||
AuthenticationFailed = 'AuthenticationFailed',
|
||||
NoUserNameConfigured = 'NoUserNameConfigured',
|
||||
NoUserEmailConfigured = 'NoUserEmailConfigured',
|
||||
|
||||
@@ -2927,6 +2927,19 @@ export class Repository {
|
||||
return commits[0];
|
||||
}
|
||||
|
||||
async showCommit(ref: string): Promise<string> {
|
||||
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<string[]> {
|
||||
const result = await this.exec(['rev-list', `${ref1}..${ref2}`]);
|
||||
if (result.stderr) {
|
||||
|
||||
@@ -344,6 +344,17 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
|
||||
return historyItemChanges;
|
||||
}
|
||||
|
||||
async resolveHistoryItemChatContext(historyItemId: string): Promise<string | undefined> {
|
||||
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<string | undefined> {
|
||||
try {
|
||||
if (historyItemRefs.length === 0) {
|
||||
|
||||
@@ -1705,6 +1705,10 @@ export class Repository implements Disposable {
|
||||
return await this.repository.getCommit(ref);
|
||||
}
|
||||
|
||||
async showCommit(ref: string): Promise<string> {
|
||||
return await this.run(Operation.Show, () => this.repository.showCommit(ref));
|
||||
}
|
||||
|
||||
async getEmptyTree(): Promise<string> {
|
||||
if (!this._EMPTY_TREE) {
|
||||
const result = await this.repository.exec(['hash-object', '-t', 'tree', '/dev/null']);
|
||||
|
||||
@@ -195,6 +195,10 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider {
|
||||
|
||||
constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { }
|
||||
|
||||
async resolveHistoryItemChatContext(historyItemId: string): Promise<string | undefined> {
|
||||
return this.proxy.$resolveHistoryItemChatContext(this.handle, historyItemId, CancellationToken.None);
|
||||
}
|
||||
|
||||
async resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[]): Promise<string | undefined> {
|
||||
return this.proxy.$resolveHistoryItemRefsCommonAncestor(this.handle, historyItemRefs, CancellationToken.None);
|
||||
}
|
||||
|
||||
@@ -2563,6 +2563,7 @@ export interface ExtHostSCMShape {
|
||||
$provideHistoryItemRefs(sourceControlHandle: number, historyItemRefs: string[] | undefined, token: CancellationToken): Promise<SCMHistoryItemRefDto[] | undefined>;
|
||||
$provideHistoryItems(sourceControlHandle: number, options: any, token: CancellationToken): Promise<SCMHistoryItemDto[] | undefined>;
|
||||
$provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise<SCMHistoryItemChangeDto[] | undefined>;
|
||||
$resolveHistoryItemChatContext(sourceControlHandle: number, historyItemId: string, token: CancellationToken): Promise<string | undefined>;
|
||||
$resolveHistoryItemRefsCommonAncestor(sourceControlHandle: number, historyItemRefs: string[], token: CancellationToken): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1046,6 +1046,19 @@ export class ExtHostSCM implements ExtHostSCMShape {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
async $resolveHistoryItemChatContext(sourceControlHandle: number, historyItemId: string, token: CancellationToken): Promise<string | undefined> {
|
||||
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<string | undefined> {
|
||||
try {
|
||||
const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider;
|
||||
|
||||
@@ -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<void> {
|
||||
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);
|
||||
|
||||
+4
-2
@@ -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<string>('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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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<IResolvedMultiDiffSource> {
|
||||
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<readonly MultiDiffEditorItem[]>(
|
||||
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<IResolvedMultiDiffSource> {
|
||||
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<readonly MultiDiffEditorItem[]>(
|
||||
historyItemChanges.map(change => new MultiDiffEditorItem(change.originalUri, change.modifiedUri, change.uri)));
|
||||
|
||||
return { resources };
|
||||
}
|
||||
}
|
||||
|
||||
class ScmResolvedMultiDiffSource implements IResolvedMultiDiffSource {
|
||||
|
||||
@@ -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<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
|
||||
id: 'scm',
|
||||
order: 5,
|
||||
|
||||
@@ -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> | 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<ITextModel | null> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface ISCMHistoryProvider {
|
||||
provideHistoryItemRefs(historyItemsRefs?: string[]): Promise<ISCMHistoryItemRef[] | undefined>;
|
||||
provideHistoryItems(options: ISCMHistoryOptions): Promise<ISCMHistoryItem[] | undefined>;
|
||||
provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise<ISCMHistoryItemChange[] | undefined>;
|
||||
resolveHistoryItemChatContext(historyItemId: string): Promise<string | undefined>;
|
||||
resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[]): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ declare module 'vscode' {
|
||||
provideHistoryItems(options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult<SourceControlHistoryItem[]>;
|
||||
provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult<SourceControlHistoryItemChange[]>;
|
||||
|
||||
resolveHistoryItemChatContext(historyItemId: string, token: CancellationToken): ProviderResult<string>;
|
||||
resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[], token: CancellationToken): ProviderResult<string>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user