Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.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
import { addDisposableListener, Dimension } from '../../../../base/browser/dom.js';
6
import * as aria from '../../../../base/browser/ui/aria/aria.js';
7
import { MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
8
import { autorun } from '../../../../base/common/observable.js';
9
import { isEqual } from '../../../../base/common/resources.js';
10
import { assertType } from '../../../../base/common/types.js';
11
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
12
import { StableEditorBottomScrollState } from '../../../../editor/browser/stableEditorScroll.js';
13
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
14
import { Position } from '../../../../editor/common/core/position.js';
15
import { Range } from '../../../../editor/common/core/range.js';
16
import { ScrollType } from '../../../../editor/common/editorCommon.js';
17
import { IOptions, ZoneWidget } from '../../../../editor/contrib/zoneWidget/browser/zoneWidget.js';
18
import { localize } from '../../../../nls.js';
19
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
20
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
21
import { ILogService } from '../../../../platform/log/common/log.js';
22
import { IChatWidgetViewOptions } from '../../chat/browser/chat.js';
23
import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js';
24
import { isResponseVM } from '../../chat/common/chatViewModel.js';
25
import { INotebookEditor } from '../../notebook/browser/notebookBrowser.js';
26
import { ACTION_REGENERATE_RESPONSE, ACTION_REPORT_ISSUE, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_SIDE, MENU_INLINE_CHAT_WIDGET_SECONDARY, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js';
27
import { EditorBasedInlineChatWidget } from './inlineChatWidget.js';
28
29
export class InlineChatZoneWidget extends ZoneWidget {
30
31
private static readonly _options: IOptions = {
32
showFrame: true,
33
frameWidth: 1,
34
// frameColor: 'var(--vscode-inlineChat-border)',
35
isResizeable: true,
36
showArrow: false,
37
isAccessible: true,
38
className: 'inline-chat-widget',
39
keepEditorSelection: true,
40
showInHiddenAreas: true,
41
ordinal: 50000,
42
};
43
44
readonly widget: EditorBasedInlineChatWidget;
45
46
private readonly _scrollUp = this._disposables.add(new ScrollUpState(this.editor));
47
private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>;
48
private _dimension?: Dimension;
49
private notebookEditor?: INotebookEditor;
50
51
constructor(
52
location: IChatWidgetLocationOptions,
53
options: IChatWidgetViewOptions | undefined,
54
editors: { editor: ICodeEditor; notebookEditor?: INotebookEditor },
55
@IInstantiationService private readonly _instaService: IInstantiationService,
56
@ILogService private _logService: ILogService,
57
@IContextKeyService contextKeyService: IContextKeyService,
58
) {
59
super(editors.editor, InlineChatZoneWidget._options);
60
this.notebookEditor = editors.notebookEditor;
61
62
this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService);
63
64
this._disposables.add(toDisposable(() => {
65
this._ctxCursorPosition.reset();
66
}));
67
68
this.widget = this._instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, {
69
statusMenuId: {
70
menu: MENU_INLINE_CHAT_WIDGET_STATUS,
71
options: {
72
buttonConfigProvider: (action, index) => {
73
const isSecondary = index > 0;
74
if (new Set([ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF, ACTION_REPORT_ISSUE]).has(action.id)) {
75
return { isSecondary, showIcon: true, showLabel: false };
76
} else {
77
return { isSecondary };
78
}
79
}
80
}
81
},
82
secondaryMenuId: MENU_INLINE_CHAT_WIDGET_SECONDARY,
83
inZoneWidget: true,
84
chatWidgetViewOptions: {
85
menus: {
86
telemetrySource: 'interactiveEditorWidget-toolbar',
87
inputSideToolbar: MENU_INLINE_CHAT_SIDE
88
},
89
...options,
90
rendererOptions: {
91
renderTextEditsAsSummary: (uri) => {
92
// render when dealing with the current file in the editor
93
return isEqual(uri, editors.editor.getModel()?.uri);
94
},
95
renderDetectedCommandsWithRequest: true,
96
...options?.rendererOptions
97
},
98
}
99
});
100
this._disposables.add(this.widget);
101
102
let revealFn: (() => void) | undefined;
103
this._disposables.add(this.widget.chatWidget.onWillMaybeChangeHeight(() => {
104
if (this.position) {
105
revealFn = this._createZoneAndScrollRestoreFn(this.position);
106
}
107
}));
108
this._disposables.add(this.widget.onDidChangeHeight(() => {
109
if (this.position && !this._usesResizeHeight) {
110
// only relayout when visible
111
revealFn ??= this._createZoneAndScrollRestoreFn(this.position);
112
const height = this._computeHeight();
113
this._relayout(height.linesValue);
114
revealFn?.();
115
revealFn = undefined;
116
}
117
}));
118
119
this.create();
120
121
this._disposables.add(autorun(r => {
122
const isBusy = this.widget.requestInProgress.read(r);
123
this.domNode.firstElementChild?.classList.toggle('busy', isBusy);
124
}));
125
126
this._disposables.add(addDisposableListener(this.domNode, 'click', e => {
127
if (!this.editor.hasWidgetFocus() && !this.widget.hasFocus()) {
128
this.editor.focus();
129
}
130
}, true));
131
132
133
// todo@jrieken listen ONLY when showing
134
const updateCursorIsAboveContextKey = () => {
135
if (!this.position || !this.editor.hasModel()) {
136
this._ctxCursorPosition.reset();
137
} else if (this.position.lineNumber === this.editor.getPosition().lineNumber) {
138
this._ctxCursorPosition.set('above');
139
} else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) {
140
this._ctxCursorPosition.set('below');
141
} else {
142
this._ctxCursorPosition.reset();
143
}
144
};
145
this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey()));
146
this._disposables.add(this.editor.onDidFocusEditorText(e => updateCursorIsAboveContextKey()));
147
updateCursorIsAboveContextKey();
148
}
149
150
protected override _fillContainer(container: HTMLElement): void {
151
152
container.style.setProperty('--vscode-inlineChat-background', 'var(--vscode-editor-background)');
153
154
container.appendChild(this.widget.domNode);
155
}
156
157
protected override _doLayout(heightInPixel: number): void {
158
159
this._updatePadding();
160
161
const info = this.editor.getLayoutInfo();
162
const width = info.contentWidth - info.verticalScrollbarWidth;
163
// width = Math.min(850, width);
164
165
this._dimension = new Dimension(width, heightInPixel);
166
this.widget.layout(this._dimension);
167
}
168
169
private _computeHeight(): { linesValue: number; pixelsValue: number } {
170
const chatContentHeight = this.widget.contentHeight;
171
const editorHeight = this.notebookEditor?.getLayoutInfo().height ?? this.editor.getLayoutInfo().height;
172
173
const contentHeight = this._decoratingElementsHeight() + Math.min(chatContentHeight, Math.max(this.widget.minHeight, editorHeight * 0.42));
174
const heightInLines = contentHeight / this.editor.getOption(EditorOption.lineHeight);
175
return { linesValue: heightInLines, pixelsValue: contentHeight };
176
}
177
178
protected override _getResizeBounds(): { minLines: number; maxLines: number } {
179
const lineHeight = this.editor.getOption(EditorOption.lineHeight);
180
const decoHeight = this._decoratingElementsHeight();
181
182
const minHeightPx = decoHeight + this.widget.minHeight;
183
const maxHeightPx = decoHeight + this.widget.contentHeight;
184
185
return {
186
minLines: minHeightPx / lineHeight,
187
maxLines: maxHeightPx / lineHeight
188
};
189
}
190
191
protected override _onWidth(_widthInPixel: number): void {
192
if (this._dimension) {
193
this._doLayout(this._dimension.height);
194
}
195
}
196
197
override show(position: Position): void {
198
assertType(this.container);
199
200
this._updatePadding();
201
202
const revealZone = this._createZoneAndScrollRestoreFn(position);
203
super.show(position, this._computeHeight().linesValue);
204
this.widget.chatWidget.setVisible(true);
205
this.widget.focus();
206
207
revealZone();
208
this._scrollUp.enable();
209
}
210
211
private _updatePadding() {
212
assertType(this.container);
213
214
const info = this.editor.getLayoutInfo();
215
const marginWithoutIndentation = info.glyphMarginWidth + info.lineNumbersWidth + info.decorationsWidth;
216
this.container.style.paddingLeft = `${marginWithoutIndentation}px`;
217
}
218
219
reveal(position: Position) {
220
const stickyScroll = this.editor.getOption(EditorOption.stickyScroll);
221
const magicValue = stickyScroll.enabled ? stickyScroll.maxLineCount : 0;
222
this.editor.revealLines(position.lineNumber + magicValue, position.lineNumber + magicValue, ScrollType.Immediate);
223
this._scrollUp.reset();
224
this.updatePositionAndHeight(position);
225
}
226
227
override updatePositionAndHeight(position: Position): void {
228
const revealZone = this._createZoneAndScrollRestoreFn(position);
229
super.updatePositionAndHeight(position, !this._usesResizeHeight ? this._computeHeight().linesValue : undefined);
230
revealZone();
231
}
232
233
private _createZoneAndScrollRestoreFn(position: Position): () => void {
234
235
const scrollState = StableEditorBottomScrollState.capture(this.editor);
236
237
const lineNumber = position.lineNumber <= 1 ? 1 : 1 + position.lineNumber;
238
const scrollTop = this.editor.getScrollTop();
239
const lineTop = this.editor.getTopForLineNumber(lineNumber);
240
const zoneTop = lineTop - this._computeHeight().pixelsValue;
241
242
const hasResponse = this.widget.chatWidget.viewModel?.getItems().find(candidate => {
243
return isResponseVM(candidate) && candidate.response.value.length > 0;
244
});
245
246
if (hasResponse && zoneTop < scrollTop || this._scrollUp.didScrollUpOrDown) {
247
// don't reveal the zone if it is already out of view (unless we are still getting ready)
248
// or if an outside scroll-up happened (e.g the user scrolled up/down to see the new content)
249
return this._scrollUp.runIgnored(() => {
250
scrollState.restore(this.editor);
251
});
252
}
253
254
return this._scrollUp.runIgnored(() => {
255
scrollState.restore(this.editor);
256
257
const scrollTop = this.editor.getScrollTop();
258
const lineTop = this.editor.getTopForLineNumber(lineNumber);
259
const zoneTop = lineTop - this._computeHeight().pixelsValue;
260
const editorHeight = this.editor.getLayoutInfo().height;
261
const lineBottom = this.editor.getBottomForLineNumber(lineNumber);
262
263
let newScrollTop = zoneTop;
264
let forceScrollTop = false;
265
266
if (lineBottom >= (scrollTop + editorHeight)) {
267
// revealing the top of the zone would push out the line we are interested in and
268
// therefore we keep the line in the viewport
269
newScrollTop = lineBottom - editorHeight;
270
forceScrollTop = true;
271
}
272
273
if (newScrollTop < scrollTop || forceScrollTop) {
274
this._logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop });
275
this.editor.setScrollTop(newScrollTop, ScrollType.Immediate);
276
}
277
});
278
}
279
280
protected override revealRange(range: Range, isLastLine: boolean): void {
281
// noop
282
}
283
284
override hide(): void {
285
const scrollState = StableEditorBottomScrollState.capture(this.editor);
286
this._scrollUp.disable();
287
this._ctxCursorPosition.reset();
288
this.widget.reset();
289
this.widget.chatWidget.setVisible(false);
290
super.hide();
291
aria.status(localize('inlineChatClosed', 'Closed inline chat widget'));
292
scrollState.restore(this.editor);
293
}
294
}
295
296
class ScrollUpState {
297
298
private _didScrollUpOrDown?: boolean;
299
private _ignoreEvents = false;
300
301
private readonly _listener = new MutableDisposable();
302
303
constructor(private readonly _editor: ICodeEditor) { }
304
305
dispose(): void {
306
this._listener.dispose();
307
}
308
309
reset(): void {
310
this._didScrollUpOrDown = undefined;
311
}
312
313
enable(): void {
314
this._didScrollUpOrDown = undefined;
315
this._listener.value = this._editor.onDidScrollChange(e => {
316
if (!e.scrollTopChanged || this._ignoreEvents) {
317
return;
318
}
319
this._listener.clear();
320
this._didScrollUpOrDown = true;
321
});
322
}
323
324
disable(): void {
325
this._listener.clear();
326
this._didScrollUpOrDown = undefined;
327
}
328
329
runIgnored(callback: () => void): () => void {
330
return () => {
331
this._ignoreEvents = true;
332
try {
333
return callback();
334
} finally {
335
this._ignoreEvents = false;
336
}
337
};
338
}
339
340
get didScrollUpOrDown(): boolean | undefined {
341
return this._didScrollUpOrDown;
342
}
343
344
}
345
346