-
Notifications
You must be signed in to change notification settings - Fork 31.8k
/
Copy pathpromptInstructionsCollectionWidget.ts
200 lines (169 loc) · 6.35 KB
/
promptInstructionsCollectionWidget.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from '../../../../../../base/common/uri.js';
import { Emitter } from '../../../../../../base/common/event.js';
import { ResourceLabels } from '../../../../../browser/labels.js';
import { Disposable } from '../../../../../../base/common/lifecycle.js';
import { ILogService } from '../../../../../../platform/log/common/log.js';
import { InstructionsAttachmentWidget } from './promptInstructionsWidget.js';
import { PROMPT_LANGUAGE_ID } from '../../../common/promptSyntax/constants.js';
import { IModelService } from '../../../../../../editor/common/services/model.js';
import { ILanguageService } from '../../../../../../editor/common/languages/language.js';
import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
import { ChatPromptAttachmentsCollection } from '../../chatAttachmentModel/chatPromptAttachmentsCollection.js';
/**
* Widget for a collection of prompt instructions attachments.
* See {@linkcode InstructionsAttachmentWidget}.
*/
export class PromptInstructionsAttachmentsCollectionWidget extends Disposable {
/**
* List of child instruction attachment widgets.
*/
private children: InstructionsAttachmentWidget[] = [];
/**
* Event that fires when number of attachments change
*
* See {@linkcode onAttachmentsCountChange}.
*/
private _onAttachmentsCountChange = this._register(new Emitter<void>());
/**
* Subscribe to the `onAttachmentsCountChange` event.
* @param callback Function to invoke when number of attachments change.
*/
public onAttachmentsCountChange(callback: () => unknown): this {
this._register(this._onAttachmentsCountChange.event(callback));
return this;
}
/**
* The parent DOM node this widget was rendered into.
*/
private parentNode: HTMLElement | undefined;
/**
* Get all `URI`s of all valid references, including all
* the possible references nested inside the children.
*/
public get references(): readonly URI[] {
return this.model.references;
}
/**
* Get the list of all prompt instruction attachment variables, including all
* nested child references of each attachment explicitly attached by user.
*/
public get chatAttachments() {
return this.model.chatAttachments;
}
/**
* Check if child widget list is empty (no attachments present).
*/
public get empty(): boolean {
return this.children.length === 0;
}
/**
* Check if any of the attachments is a prompt file.
*/
public get hasPromptFile(): boolean {
return this.references.some((uri) => {
const model = this.modelService.getModel(uri);
const languageId = model ? model.getLanguageId() : this.languageService.guessLanguageIdByFilepathOrFirstLine(uri);
return languageId === PROMPT_LANGUAGE_ID;
});
}
constructor(
private readonly model: ChatPromptAttachmentsCollection,
private readonly resourceLabels: ResourceLabels,
@IInstantiationService private readonly initService: IInstantiationService,
@ILanguageService private readonly languageService: ILanguageService,
@IModelService private readonly modelService: IModelService,
@ILogService private readonly logService: ILogService,
) {
super();
this.render = this.render.bind(this);
// when a new attachment model is added, create a new child widget for it
this._register(this.model.onAdd((attachment) => {
const widget = this.initService.createInstance(
InstructionsAttachmentWidget,
attachment,
this.resourceLabels,
);
// handle the child widget disposal event, removing it from the list
widget.onDispose(this.handleAttachmentDispose.bind(this, widget));
// register the new child widget
this.children.push(widget);
// if parent node is present - append the widget to it, otherwise wait
// until the `render` method will be called
if (this.parentNode) {
this.parentNode.appendChild(widget.domNode);
}
// fire the event to notify about the change in the number of attachments
this._onAttachmentsCountChange.fire();
}));
}
/**
* Handle child widget disposal.
* @param widget The child widget that was disposed.
*/
public handleAttachmentDispose(widget: InstructionsAttachmentWidget): this {
// common prefix for all log messages
const logPrefix = `[onChildDispose] Widget for instructions attachment '${widget.uri.path}'`;
// flag to check if the widget was found in the children list
let widgetExists = false;
// filter out disposed child widget from the list
this.children = this.children.filter((child) => {
if (child === widget) {
// because we filter out all objects here it might be ok to have multiple of them, but
// it also highlights a potential issue in our logic somewhere else, so trace a warning here
if (widgetExists) {
this.logService.warn(
`${logPrefix} is present in the children references list multiple times.`,
);
}
widgetExists = true;
return false;
}
return true;
});
// no widget was found in the children list, while it might be ok it also
// highlights a potential issue in our logic, so trace a warning here
if (!widgetExists) {
this.logService.warn(
`${logPrefix} was disposed, but was not found in the child references.`,
);
}
if (!this.parentNode) {
this.logService.warn(
`${logPrefix} no parent node reference found.`,
);
}
// remove the child widget root node from the DOM
this.parentNode?.removeChild(widget.domNode);
// fire the event to notify about the change in the number of attachments
this._onAttachmentsCountChange.fire();
return this;
}
/**
* Render attachments into the provided `parentNode`.
*
* Note! this method assumes that the provided `parentNode` is cleared by the caller.
*/
public render(
parentNode: HTMLElement,
): this {
this.parentNode = parentNode;
for (const widget of this.children) {
this.parentNode.appendChild(widget.domNode);
}
return this;
}
/**
* Dispose of the widget, including all the child
* widget instances.
*/
public override dispose(): void {
for (const child of this.children) {
child.dispose();
}
super.dispose();
}
}