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
5240 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 { 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/widget/chatWidget.js';
24
import { ChatMode } from '../../chat/common/chatModes.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 _ctxCursorPosition: IContextKey<'above' | 'below' | ''>;
47
private _dimension?: Dimension;
48
private notebookEditor?: INotebookEditor;
49
50
constructor(
51
location: IChatWidgetLocationOptions,
52
options: IChatWidgetViewOptions | undefined,
53
editors: { editor: ICodeEditor; notebookEditor?: INotebookEditor },
54
/** @deprecated should go away with inline2 */
55
clearDelegate: () => Promise<void>,
56
@IInstantiationService private readonly _instaService: IInstantiationService,
57
@ILogService private _logService: ILogService,
58
@IContextKeyService contextKeyService: IContextKeyService,
59
) {
60
super(editors.editor, InlineChatZoneWidget._options);
61
this.notebookEditor = editors.notebookEditor;
62
63
this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService);
64
65
this._disposables.add(toDisposable(() => {
66
this._ctxCursorPosition.reset();
67
}));
68
69
this.widget = this._instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, {
70
statusMenuId: {
71
menu: MENU_INLINE_CHAT_WIDGET_STATUS,
72
options: {
73
buttonConfigProvider: (action, index) => {
74
const isSecondary = index > 0;
75
if (new Set([ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF, ACTION_REPORT_ISSUE]).has(action.id)) {
76
return { isSecondary, showIcon: true, showLabel: false };
77
} else {
78
return { isSecondary };
79
}
80
}
81
}
82
},
83
secondaryMenuId: MENU_INLINE_CHAT_WIDGET_SECONDARY,
84
inZoneWidget: true,
85
chatWidgetViewOptions: {
86
menus: {
87
telemetrySource: 'interactiveEditorWidget-toolbar',
88
inputSideToolbar: MENU_INLINE_CHAT_SIDE
89
},
90
clear: clearDelegate,
91
...options,
92
rendererOptions: {
93
renderTextEditsAsSummary: (uri) => {
94
// render when dealing with the current file in the editor
95
return isEqual(uri, editors.editor.getModel()?.uri);
96
},
97
renderDetectedCommandsWithRequest: true,
98
...options?.rendererOptions
99
},
100
defaultMode: ChatMode.Ask
101
}
102
});
103
this._disposables.add(this.widget);
104
105
let revealFn: (() => void) | undefined;
106
this._disposables.add(this.widget.chatWidget.onWillMaybeChangeHeight(() => {
107
if (this.position) {
108
revealFn = this._createZoneAndScrollRestoreFn(this.position);
109
}
110
}));
111
this._disposables.add(this.widget.onDidChangeHeight(() => {
112
if (this.position && !this._usesResizeHeight) {
113
// only relayout when visible
114
revealFn ??= this._createZoneAndScrollRestoreFn(this.position);
115
const height = this._computeHeight();
116
this._relayout(height.linesValue);
117
revealFn?.();
118
revealFn = undefined;
119
}
120
}));
121
122
this.create();
123
124
this._disposables.add(autorun(r => {
125
const isBusy = this.widget.requestInProgress.read(r);
126
this.domNode.firstElementChild?.classList.toggle('busy', isBusy);
127
}));
128
129
this._disposables.add(addDisposableListener(this.domNode, 'click', e => {
130
if (!this.editor.hasWidgetFocus() && !this.widget.hasFocus()) {
131
this.editor.focus();
132
}
133
}, true));
134
135
136
// todo@jrieken listen ONLY when showing
137
const updateCursorIsAboveContextKey = () => {
138
if (!this.position || !this.editor.hasModel()) {
139
this._ctxCursorPosition.reset();
140
} else if (this.position.lineNumber === this.editor.getPosition().lineNumber) {
141
this._ctxCursorPosition.set('above');
142
} else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) {
143
this._ctxCursorPosition.set('below');
144
} else {
145
this._ctxCursorPosition.reset();
146
}
147
};
148
this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey()));
149
this._disposables.add(this.editor.onDidFocusEditorText(e => updateCursorIsAboveContextKey()));
150
updateCursorIsAboveContextKey();
151
}
152
153
protected override _fillContainer(container: HTMLElement): void {
154
155
container.style.setProperty('--vscode-inlineChat-background', 'var(--vscode-editor-background)');
156
157
container.appendChild(this.widget.domNode);
158
}
159
160
protected override _doLayout(heightInPixel: number): void {
161
162
this._updatePadding();
163
164
const info = this.editor.getLayoutInfo();
165
const width = info.contentWidth - info.verticalScrollbarWidth;
166
// width = Math.min(850, width);
167
168
this._dimension = new Dimension(width, heightInPixel);
169
this.widget.layout(this._dimension);
170
}
171
172
private _computeHeight(): { linesValue: number; pixelsValue: number } {
173
const chatContentHeight = this.widget.contentHeight;
174
const editorHeight = this.notebookEditor?.getLayoutInfo().height ?? this.editor.getLayoutInfo().height;
175
176
const contentHeight = this._decoratingElementsHeight() + Math.min(chatContentHeight, Math.max(this.widget.minHeight, editorHeight * 0.42));
177
const heightInLines = contentHeight / this.editor.getOption(EditorOption.lineHeight);
178
return { linesValue: heightInLines, pixelsValue: contentHeight };
179
}
180
181
protected override _getResizeBounds(): { minLines: number; maxLines: number } {
182
const lineHeight = this.editor.getOption(EditorOption.lineHeight);
183
const decoHeight = this._decoratingElementsHeight();
184
185
const minHeightPx = decoHeight + this.widget.minHeight;
186
const maxHeightPx = decoHeight + this.widget.contentHeight;
187
188
return {
189
minLines: minHeightPx / lineHeight,
190
maxLines: maxHeightPx / lineHeight
191
};
192
}
193
194
protected override _onWidth(_widthInPixel: number): void {
195
if (this._dimension) {
196
this._doLayout(this._dimension.height);
197
}
198
}
199
200
override show(position: Position): void {
201
assertType(this.container);
202
203
this._updatePadding();
204
205
const revealZone = this._createZoneAndScrollRestoreFn(position);
206
super.show(position, this._computeHeight().linesValue);
207
this.widget.chatWidget.setVisible(true);
208
this.widget.focus();
209
210
revealZone();
211
}
212
213
private _updatePadding() {
214
assertType(this.container);
215
216
const info = this.editor.getLayoutInfo();
217
const marginWithoutIndentation = info.glyphMarginWidth + info.lineNumbersWidth + info.decorationsWidth;
218
this.container.style.paddingLeft = `${marginWithoutIndentation}px`;
219
}
220
221
reveal(position: Position) {
222
const stickyScroll = this.editor.getOption(EditorOption.stickyScroll);
223
const magicValue = stickyScroll.enabled ? stickyScroll.maxLineCount : 0;
224
this.editor.revealLines(position.lineNumber + magicValue, position.lineNumber + magicValue, ScrollType.Immediate);
225
this.updatePositionAndHeight(position);
226
}
227
228
override updatePositionAndHeight(position: Position): void {
229
const revealZone = this._createZoneAndScrollRestoreFn(position);
230
super.updatePositionAndHeight(position, !this._usesResizeHeight ? this._computeHeight().linesValue : undefined);
231
revealZone();
232
}
233
234
private _createZoneAndScrollRestoreFn(position: Position): () => void {
235
236
const scrollState = StableEditorBottomScrollState.capture(this.editor);
237
238
const lineNumber = position.lineNumber <= 1 ? 1 : 1 + position.lineNumber;
239
240
return () => {
241
scrollState.restore(this.editor);
242
243
const scrollTop = this.editor.getScrollTop();
244
const lineTop = this.editor.getTopForLineNumber(lineNumber);
245
const zoneTop = lineTop - this._computeHeight().pixelsValue;
246
const editorHeight = this.editor.getLayoutInfo().height;
247
const lineBottom = this.editor.getBottomForLineNumber(lineNumber);
248
249
let newScrollTop = zoneTop;
250
let forceScrollTop = false;
251
252
if (lineBottom >= (scrollTop + editorHeight)) {
253
// revealing the top of the zone would push out the line we are interested in and
254
// therefore we keep the line in the viewport
255
newScrollTop = lineBottom - editorHeight;
256
forceScrollTop = true;
257
}
258
259
if (newScrollTop < scrollTop || forceScrollTop) {
260
this._logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop });
261
this.editor.setScrollTop(newScrollTop, ScrollType.Immediate);
262
}
263
};
264
}
265
266
protected override revealRange(range: Range, isLastLine: boolean): void {
267
// noop
268
}
269
270
override hide(): void {
271
const scrollState = StableEditorBottomScrollState.capture(this.editor);
272
this._ctxCursorPosition.reset();
273
this.widget.chatWidget.setVisible(false);
274
super.hide();
275
aria.status(localize('inlineChatClosed', 'Closed inline chat widget'));
276
scrollState.restore(this.editor);
277
}
278
}
279
280