Path: blob/main/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.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 { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';7import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js';8import { Button } from '../../../../../base/browser/ui/button/button.js';9import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js';10import { Codicon } from '../../../../../base/common/codicons.js';11import { KeyCode } from '../../../../../base/common/keyCodes.js';12import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';13import { Schemas } from '../../../../../base/common/network.js';14import { basename, dirname } from '../../../../../base/common/resources.js';15import { URI } from '../../../../../base/common/uri.js';16import { ILanguageService } from '../../../../../editor/common/languages/language.js';17import { IModelService } from '../../../../../editor/common/services/model.js';18import { localize } from '../../../../../nls.js';19import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';20import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js';21import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';22import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';23import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';24import { FileKind, IFileService } from '../../../../../platform/files/common/files.js';25import { IHoverService } from '../../../../../platform/hover/browser/hover.js';26import { ILabelService } from '../../../../../platform/label/common/label.js';27import { ResourceLabels } from '../../../../browser/labels.js';28import { ResourceContextKey } from '../../../../common/contextkeys.js';29import { IChatRequestImplicitVariableEntry } from '../../common/chatVariableEntries.js';30import { IChatWidgetService } from '../chat.js';31import { ChatAttachmentModel } from '../chatAttachmentModel.js';3233export class ImplicitContextAttachmentWidget extends Disposable {34public readonly domNode: HTMLElement;3536private readonly renderDisposables = this._register(new DisposableStore());3738constructor(39private readonly attachment: IChatRequestImplicitVariableEntry,40private readonly resourceLabels: ResourceLabels,41private readonly attachmentModel: ChatAttachmentModel,42@IContextKeyService private readonly contextKeyService: IContextKeyService,43@IContextMenuService private readonly contextMenuService: IContextMenuService,44@ILabelService private readonly labelService: ILabelService,45@IMenuService private readonly menuService: IMenuService,46@IFileService private readonly fileService: IFileService,47@ILanguageService private readonly languageService: ILanguageService,48@IModelService private readonly modelService: IModelService,49@IHoverService private readonly hoverService: IHoverService,50@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,51@IConfigurationService private readonly configService: IConfigurationService52) {53super();5455this.domNode = dom.$('.chat-attached-context-attachment.show-file-icons.implicit');56this.render();57}5859private render() {60dom.clearNode(this.domNode);61this.renderDisposables.clear();6263this.domNode.classList.toggle('disabled', !this.attachment.enabled);64const label = this.resourceLabels.create(this.domNode, { supportIcons: true });65const file = URI.isUri(this.attachment.value) ? this.attachment.value : this.attachment.value!.uri;66const range = URI.isUri(this.attachment.value) || !this.attachment.isSelection ? undefined : this.attachment.value!.range;6768const attachmentTypeName = file.scheme === Schemas.vscodeNotebookCell ? localize('cell.lowercase', "cell") : localize('file.lowercase', "file");6970const fileBasename = basename(file);71const fileDirname = dirname(file);72const friendlyName = `${fileBasename} ${fileDirname}`;73const ariaLabel = range ? localize('chat.fileAttachmentWithRange', "Attached {0}, {1}, line {2} to line {3}", attachmentTypeName, friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.fileAttachment', "Attached {0}, {1}", attachmentTypeName, friendlyName);7475const uriLabel = this.labelService.getUriLabel(file, { relative: true });76const currentFile = localize('openEditor', "Current {0} context", attachmentTypeName);77const inactive = localize('enableHint', "Enable current {0} context", attachmentTypeName);78const currentFileHint = this.attachment.enabled || this.attachment.isSelection ? currentFile : inactive;79const title = `${currentFileHint}\n${uriLabel}`;8081label.setFile(file, {82fileKind: FileKind.FILE,83hidePath: true,84range,85title86});87this.domNode.ariaLabel = ariaLabel;88this.domNode.tabIndex = 0;8990const isSuggestedEnabled = this.configService.getValue('chat.implicitContext.suggestedContext');91this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this.domNode, title));929394if (isSuggestedEnabled) {95if (!this.attachment.isSelection) {96const buttonMsg = this.attachment.enabled ? localize('disable', "Disable current {0} context", attachmentTypeName) : '';97const toggleButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: buttonMsg }));98toggleButton.icon = this.attachment.enabled ? Codicon.x : Codicon.plus;99this.renderDisposables.add(toggleButton.onDidClick((e) => {100e.stopPropagation();101e.preventDefault();102if (!this.attachment.enabled) {103this.convertToRegularAttachment();104}105this.attachment.enabled = false;106}));107}108109if (!this.attachment.enabled && this.attachment.isSelection) {110this.domNode.classList.remove('disabled');111}112113this.renderDisposables.add(dom.addDisposableListener(this.domNode, dom.EventType.CLICK, e => {114if (!this.attachment.enabled && !this.attachment.isSelection) {115this.convertToRegularAttachment();116}117}));118119this.renderDisposables.add(dom.addDisposableListener(this.domNode, dom.EventType.KEY_DOWN, e => {120const event = new StandardKeyboardEvent(e);121if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {122if (!this.attachment.enabled && !this.attachment.isSelection) {123e.preventDefault();124e.stopPropagation();125this.convertToRegularAttachment();126}127}128}));129} else {130const buttonMsg = this.attachment.enabled ? localize('disable', "Disable current {0} context", attachmentTypeName) : localize('enable', "Enable current {0} context", attachmentTypeName);131const toggleButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: buttonMsg }));132toggleButton.icon = this.attachment.enabled ? Codicon.eye : Codicon.eyeClosed;133this.renderDisposables.add(toggleButton.onDidClick((e) => {134e.stopPropagation(); // prevent it from triggering the click handler on the parent immediately after rerendering135this.attachment.enabled = !this.attachment.enabled;136}));137}138139// Context menu140const scopedContextKeyService = this.renderDisposables.add(this.contextKeyService.createScoped(this.domNode));141142const resourceContextKey = this.renderDisposables.add(new ResourceContextKey(scopedContextKeyService, this.fileService, this.languageService, this.modelService));143resourceContextKey.set(file);144145this.renderDisposables.add(dom.addDisposableListener(this.domNode, dom.EventType.CONTEXT_MENU, async domEvent => {146const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent);147dom.EventHelper.stop(domEvent, true);148149this.contextMenuService.showContextMenu({150contextKeyService: scopedContextKeyService,151getAnchor: () => event,152getActions: () => {153const menu = this.menuService.getMenuActions(MenuId.ChatInputResourceAttachmentContext, scopedContextKeyService, { arg: file });154return getFlatContextMenuActions(menu);155},156});157}));158}159160private convertToRegularAttachment(): void {161if (!this.attachment.value) {162return;163}164const file = URI.isUri(this.attachment.value) ? this.attachment.value : this.attachment.value.uri;165this.attachmentModel.addFile(file);166this.chatWidgetService.lastFocusedWidget?.focusInput();167}168}169170171