Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../../../../base/browser/dom.js';
7
import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';
8
import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js';
9
import { Button } from '../../../../../base/browser/ui/button/button.js';
10
import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js';
11
import { Codicon } from '../../../../../base/common/codicons.js';
12
import { KeyCode } from '../../../../../base/common/keyCodes.js';
13
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
14
import { Schemas } from '../../../../../base/common/network.js';
15
import { basename, dirname } from '../../../../../base/common/resources.js';
16
import { URI } from '../../../../../base/common/uri.js';
17
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
18
import { IModelService } from '../../../../../editor/common/services/model.js';
19
import { localize } from '../../../../../nls.js';
20
import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';
21
import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js';
22
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
23
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
24
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
25
import { FileKind, IFileService } from '../../../../../platform/files/common/files.js';
26
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
27
import { ILabelService } from '../../../../../platform/label/common/label.js';
28
import { ResourceLabels } from '../../../../browser/labels.js';
29
import { ResourceContextKey } from '../../../../common/contextkeys.js';
30
import { IChatRequestImplicitVariableEntry } from '../../common/chatVariableEntries.js';
31
import { IChatWidgetService } from '../chat.js';
32
import { ChatAttachmentModel } from '../chatAttachmentModel.js';
33
34
export class ImplicitContextAttachmentWidget extends Disposable {
35
public readonly domNode: HTMLElement;
36
37
private readonly renderDisposables = this._register(new DisposableStore());
38
39
constructor(
40
private readonly attachment: IChatRequestImplicitVariableEntry,
41
private readonly resourceLabels: ResourceLabels,
42
private readonly attachmentModel: ChatAttachmentModel,
43
@IContextKeyService private readonly contextKeyService: IContextKeyService,
44
@IContextMenuService private readonly contextMenuService: IContextMenuService,
45
@ILabelService private readonly labelService: ILabelService,
46
@IMenuService private readonly menuService: IMenuService,
47
@IFileService private readonly fileService: IFileService,
48
@ILanguageService private readonly languageService: ILanguageService,
49
@IModelService private readonly modelService: IModelService,
50
@IHoverService private readonly hoverService: IHoverService,
51
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
52
@IConfigurationService private readonly configService: IConfigurationService
53
) {
54
super();
55
56
this.domNode = dom.$('.chat-attached-context-attachment.show-file-icons.implicit');
57
this.render();
58
}
59
60
private render() {
61
dom.clearNode(this.domNode);
62
this.renderDisposables.clear();
63
64
this.domNode.classList.toggle('disabled', !this.attachment.enabled);
65
const label = this.resourceLabels.create(this.domNode, { supportIcons: true });
66
const file = URI.isUri(this.attachment.value) ? this.attachment.value : this.attachment.value!.uri;
67
const range = URI.isUri(this.attachment.value) || !this.attachment.isSelection ? undefined : this.attachment.value!.range;
68
69
const attachmentTypeName = file.scheme === Schemas.vscodeNotebookCell ? localize('cell.lowercase', "cell") : localize('file.lowercase', "file");
70
71
const fileBasename = basename(file);
72
const fileDirname = dirname(file);
73
const friendlyName = `${fileBasename} ${fileDirname}`;
74
const 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);
75
76
const uriLabel = this.labelService.getUriLabel(file, { relative: true });
77
const currentFile = localize('openEditor', "Current {0} context", attachmentTypeName);
78
const inactive = localize('enableHint', "Enable current {0} context", attachmentTypeName);
79
const currentFileHint = this.attachment.enabled || this.attachment.isSelection ? currentFile : inactive;
80
const title = `${currentFileHint}\n${uriLabel}`;
81
82
label.setFile(file, {
83
fileKind: FileKind.FILE,
84
hidePath: true,
85
range,
86
title
87
});
88
this.domNode.ariaLabel = ariaLabel;
89
this.domNode.tabIndex = 0;
90
91
const isSuggestedEnabled = this.configService.getValue('chat.implicitContext.suggestedContext');
92
this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this.domNode, title));
93
94
95
if (isSuggestedEnabled) {
96
if (!this.attachment.isSelection) {
97
const buttonMsg = this.attachment.enabled ? localize('disable', "Disable current {0} context", attachmentTypeName) : '';
98
const toggleButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: buttonMsg }));
99
toggleButton.icon = this.attachment.enabled ? Codicon.x : Codicon.plus;
100
this.renderDisposables.add(toggleButton.onDidClick((e) => {
101
e.stopPropagation();
102
e.preventDefault();
103
if (!this.attachment.enabled) {
104
this.convertToRegularAttachment();
105
}
106
this.attachment.enabled = false;
107
}));
108
}
109
110
if (!this.attachment.enabled && this.attachment.isSelection) {
111
this.domNode.classList.remove('disabled');
112
}
113
114
this.renderDisposables.add(dom.addDisposableListener(this.domNode, dom.EventType.CLICK, e => {
115
if (!this.attachment.enabled && !this.attachment.isSelection) {
116
this.convertToRegularAttachment();
117
}
118
}));
119
120
this.renderDisposables.add(dom.addDisposableListener(this.domNode, dom.EventType.KEY_DOWN, e => {
121
const event = new StandardKeyboardEvent(e);
122
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
123
if (!this.attachment.enabled && !this.attachment.isSelection) {
124
e.preventDefault();
125
e.stopPropagation();
126
this.convertToRegularAttachment();
127
}
128
}
129
}));
130
} else {
131
const buttonMsg = this.attachment.enabled ? localize('disable', "Disable current {0} context", attachmentTypeName) : localize('enable', "Enable current {0} context", attachmentTypeName);
132
const toggleButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: buttonMsg }));
133
toggleButton.icon = this.attachment.enabled ? Codicon.eye : Codicon.eyeClosed;
134
this.renderDisposables.add(toggleButton.onDidClick((e) => {
135
e.stopPropagation(); // prevent it from triggering the click handler on the parent immediately after rerendering
136
this.attachment.enabled = !this.attachment.enabled;
137
}));
138
}
139
140
// Context menu
141
const scopedContextKeyService = this.renderDisposables.add(this.contextKeyService.createScoped(this.domNode));
142
143
const resourceContextKey = this.renderDisposables.add(new ResourceContextKey(scopedContextKeyService, this.fileService, this.languageService, this.modelService));
144
resourceContextKey.set(file);
145
146
this.renderDisposables.add(dom.addDisposableListener(this.domNode, dom.EventType.CONTEXT_MENU, async domEvent => {
147
const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent);
148
dom.EventHelper.stop(domEvent, true);
149
150
this.contextMenuService.showContextMenu({
151
contextKeyService: scopedContextKeyService,
152
getAnchor: () => event,
153
getActions: () => {
154
const menu = this.menuService.getMenuActions(MenuId.ChatInputResourceAttachmentContext, scopedContextKeyService, { arg: file });
155
return getFlatContextMenuActions(menu);
156
},
157
});
158
}));
159
}
160
161
private convertToRegularAttachment(): void {
162
if (!this.attachment.value) {
163
return;
164
}
165
const file = URI.isUri(this.attachment.value) ? this.attachment.value : this.attachment.value.uri;
166
this.attachmentModel.addFile(file);
167
this.chatWidgetService.lastFocusedWidget?.focusInput();
168
}
169
}
170
171