Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.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 { $, Dimension, getActiveElement, getTotalHeight, getWindow, h, reset, trackFocus } from '../../../../base/browser/dom.js';
7
import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
8
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
9
import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
10
import { IAction } from '../../../../base/common/actions.js';
11
import { Emitter, Event } from '../../../../base/common/event.js';
12
import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
13
import { constObservable, derived, IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js';
14
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
15
import { AccessibleDiffViewer, IAccessibleDiffViewerModel } from '../../../../editor/browser/widget/diffEditor/components/accessibleDiffViewer.js';
16
import { EditorOption, IComputedEditorOptions } from '../../../../editor/common/config/editorOptions.js';
17
import { LineRange } from '../../../../editor/common/core/ranges/lineRange.js';
18
import { Position } from '../../../../editor/common/core/position.js';
19
import { Range } from '../../../../editor/common/core/range.js';
20
import { Selection } from '../../../../editor/common/core/selection.js';
21
import { DetailedLineRangeMapping, RangeMapping } from '../../../../editor/common/diff/rangeMapping.js';
22
import { ICodeEditorViewState, ScrollType } from '../../../../editor/common/editorCommon.js';
23
import { ITextModel } from '../../../../editor/common/model.js';
24
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
25
import { localize } from '../../../../nls.js';
26
import { IAccessibleViewService } from '../../../../platform/accessibility/browser/accessibleView.js';
27
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
28
import { IWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from '../../../../platform/actions/browser/buttonbar.js';
29
import { createActionViewItem, IMenuEntryActionViewItemOptions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
30
import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
31
import { MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js';
32
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
33
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
34
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
35
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
36
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
37
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
38
import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js';
39
import { asCssVariable, asCssVariableName, editorBackground, inputBackground } from '../../../../platform/theme/common/colorRegistry.js';
40
import { EDITOR_DRAG_AND_DROP_BACKGROUND } from '../../../common/theme.js';
41
import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js';
42
import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js';
43
import { MarkUnhelpfulActionId } from '../../chat/browser/actions/chatTitleActions.js';
44
import { IChatWidgetViewOptions } from '../../chat/browser/chat.js';
45
import { ChatVoteDownButton } from '../../chat/browser/chatListRenderer.js';
46
import { ChatWidget, IChatViewState, IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js';
47
import { chatRequestBackground } from '../../chat/common/chatColors.js';
48
import { ChatContextKeys } from '../../chat/common/chatContextKeys.js';
49
import { IChatModel } from '../../chat/common/chatModel.js';
50
import { ChatAgentVoteDirection, IChatService } from '../../chat/common/chatService.js';
51
import { isResponseVM } from '../../chat/common/chatViewModel.js';
52
import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED, inlineChatBackground, inlineChatForeground } from '../common/inlineChat.js';
53
import { HunkInformation, Session } from './inlineChatSession.js';
54
import './media/inlineChat.css';
55
56
57
export interface InlineChatWidgetViewState {
58
editorViewState: ICodeEditorViewState;
59
input: string;
60
placeholder: string;
61
}
62
63
export interface IInlineChatWidgetConstructionOptions {
64
65
/**
66
* The menu that rendered as button bar, use for accept, discard etc
67
*/
68
statusMenuId: MenuId | { menu: MenuId; options: IWorkbenchButtonBarOptions };
69
70
secondaryMenuId?: MenuId;
71
72
/**
73
* The options for the chat widget
74
*/
75
chatWidgetViewOptions?: IChatWidgetViewOptions;
76
77
inZoneWidget?: boolean;
78
}
79
80
export class InlineChatWidget {
81
82
protected readonly _elements = h(
83
'div.inline-chat@root',
84
[
85
h('div.chat-widget@chatWidget'),
86
h('div.accessibleViewer@accessibleViewer'),
87
h('div.status@status', [
88
h('div.label.info.hidden@infoLabel'),
89
h('div.actions.hidden@toolbar1'),
90
h('div.label.status.hidden@statusLabel'),
91
h('div.actions.secondary.hidden@toolbar2'),
92
]),
93
]
94
);
95
96
protected readonly _store = new DisposableStore();
97
98
private readonly _ctxInputEditorFocused: IContextKey<boolean>;
99
private readonly _ctxResponseFocused: IContextKey<boolean>;
100
101
private readonly _chatWidget: ChatWidget;
102
103
protected readonly _onDidChangeHeight = this._store.add(new Emitter<void>());
104
readonly onDidChangeHeight: Event<void> = Event.filter(this._onDidChangeHeight.event, _ => !this._isLayouting);
105
106
private readonly _requestInProgress = observableValue(this, false);
107
readonly requestInProgress: IObservable<boolean> = this._requestInProgress;
108
109
private _isLayouting: boolean = false;
110
111
readonly scopedContextKeyService: IContextKeyService;
112
113
constructor(
114
location: IChatWidgetLocationOptions,
115
private readonly _options: IInlineChatWidgetConstructionOptions,
116
@IInstantiationService protected readonly _instantiationService: IInstantiationService,
117
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
118
@IKeybindingService private readonly _keybindingService: IKeybindingService,
119
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
120
@IConfigurationService private readonly _configurationService: IConfigurationService,
121
@IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService,
122
@ITextModelService protected readonly _textModelResolverService: ITextModelService,
123
@IChatService private readonly _chatService: IChatService,
124
@IHoverService private readonly _hoverService: IHoverService,
125
) {
126
this.scopedContextKeyService = this._store.add(_contextKeyService.createScoped(this._elements.chatWidget));
127
const scopedInstaService = _instantiationService.createChild(
128
new ServiceCollection([
129
IContextKeyService,
130
this.scopedContextKeyService
131
]),
132
this._store
133
);
134
135
this._chatWidget = scopedInstaService.createInstance(
136
ChatWidget,
137
location,
138
{ isInlineChat: true },
139
{
140
autoScroll: true,
141
defaultElementHeight: 32,
142
renderStyle: 'minimal',
143
renderInputOnTop: false,
144
renderFollowups: true,
145
supportsFileReferences: true,
146
filter: item => {
147
if (!isResponseVM(item) || item.errorDetails) {
148
// show all requests and errors
149
return true;
150
}
151
const emptyResponse = item.response.value.length === 0;
152
if (emptyResponse) {
153
return false;
154
}
155
if (item.response.value.every(item => item.kind === 'textEditGroup' && _options.chatWidgetViewOptions?.rendererOptions?.renderTextEditsAsSummary?.(item.uri))) {
156
return false;
157
}
158
return true;
159
},
160
dndContainer: this._elements.root,
161
..._options.chatWidgetViewOptions
162
},
163
{
164
listForeground: inlineChatForeground,
165
listBackground: inlineChatBackground,
166
overlayBackground: EDITOR_DRAG_AND_DROP_BACKGROUND,
167
inputEditorBackground: inputBackground,
168
resultEditorBackground: editorBackground
169
}
170
);
171
this._elements.root.classList.toggle('in-zone-widget', !!_options.inZoneWidget);
172
this._chatWidget.render(this._elements.chatWidget);
173
this._elements.chatWidget.style.setProperty(asCssVariableName(chatRequestBackground), asCssVariable(inlineChatBackground));
174
this._chatWidget.setVisible(true);
175
this._store.add(this._chatWidget);
176
177
const ctxResponse = ChatContextKeys.isResponse.bindTo(this.scopedContextKeyService);
178
const ctxResponseVote = ChatContextKeys.responseVote.bindTo(this.scopedContextKeyService);
179
const ctxResponseSupportIssues = ChatContextKeys.responseSupportsIssueReporting.bindTo(this.scopedContextKeyService);
180
const ctxResponseError = ChatContextKeys.responseHasError.bindTo(this.scopedContextKeyService);
181
const ctxResponseErrorFiltered = ChatContextKeys.responseIsFiltered.bindTo(this.scopedContextKeyService);
182
183
const viewModelStore = this._store.add(new DisposableStore());
184
this._store.add(this._chatWidget.onDidChangeViewModel(() => {
185
viewModelStore.clear();
186
187
const viewModel = this._chatWidget.viewModel;
188
if (!viewModel) {
189
return;
190
}
191
192
viewModelStore.add(toDisposable(() => {
193
toolbar2.context = undefined;
194
ctxResponse.reset();
195
ctxResponseVote.reset();
196
ctxResponseError.reset();
197
ctxResponseErrorFiltered.reset();
198
ctxResponseSupportIssues.reset();
199
}));
200
201
viewModelStore.add(viewModel.onDidChange(() => {
202
203
this._requestInProgress.set(viewModel.requestInProgress, undefined);
204
205
const last = viewModel.getItems().at(-1);
206
toolbar2.context = last;
207
208
ctxResponse.set(isResponseVM(last));
209
ctxResponseVote.set(isResponseVM(last) ? last.vote === ChatAgentVoteDirection.Down ? 'down' : last.vote === ChatAgentVoteDirection.Up ? 'up' : '' : '');
210
ctxResponseError.set(isResponseVM(last) && last.errorDetails !== undefined);
211
ctxResponseErrorFiltered.set((!!(isResponseVM(last) && last.errorDetails?.responseIsFiltered)));
212
ctxResponseSupportIssues.set(isResponseVM(last) && (last.agent?.metadata.supportIssueReporting ?? false));
213
214
this._onDidChangeHeight.fire();
215
}));
216
this._onDidChangeHeight.fire();
217
}));
218
219
this._store.add(this.chatWidget.onDidChangeContentHeight(() => {
220
this._onDidChangeHeight.fire();
221
}));
222
223
// context keys
224
this._ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(this._contextKeyService);
225
const tracker = this._store.add(trackFocus(this.domNode));
226
this._store.add(tracker.onDidBlur(() => this._ctxResponseFocused.set(false)));
227
this._store.add(tracker.onDidFocus(() => this._ctxResponseFocused.set(true)));
228
229
this._ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(_contextKeyService);
230
this._store.add(this._chatWidget.inputEditor.onDidFocusEditorWidget(() => this._ctxInputEditorFocused.set(true)));
231
this._store.add(this._chatWidget.inputEditor.onDidBlurEditorWidget(() => this._ctxInputEditorFocused.set(false)));
232
233
const statusMenuId = _options.statusMenuId instanceof MenuId ? _options.statusMenuId : _options.statusMenuId.menu;
234
235
// BUTTON bar
236
const statusMenuOptions = _options.statusMenuId instanceof MenuId ? undefined : _options.statusMenuId.options;
237
const statusButtonBar = scopedInstaService.createInstance(MenuWorkbenchButtonBar, this._elements.toolbar1, statusMenuId, {
238
toolbarOptions: { primaryGroup: '0_main' },
239
telemetrySource: _options.chatWidgetViewOptions?.menus?.telemetrySource,
240
menuOptions: { renderShortTitle: true },
241
...statusMenuOptions,
242
});
243
this._store.add(statusButtonBar.onDidChange(() => this._onDidChangeHeight.fire()));
244
this._store.add(statusButtonBar);
245
246
// secondary toolbar
247
const toolbar2 = scopedInstaService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar2, _options.secondaryMenuId ?? MenuId.for(''), {
248
telemetrySource: _options.chatWidgetViewOptions?.menus?.telemetrySource,
249
menuOptions: { renderShortTitle: true, shouldForwardArgs: true },
250
actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => {
251
if (action instanceof MenuItemAction && action.item.id === MarkUnhelpfulActionId) {
252
return scopedInstaService.createInstance(ChatVoteDownButton, action, options as IMenuEntryActionViewItemOptions);
253
}
254
return createActionViewItem(scopedInstaService, action, options);
255
}
256
});
257
this._store.add(toolbar2.onDidChangeMenuItems(() => this._onDidChangeHeight.fire()));
258
this._store.add(toolbar2);
259
260
261
this._store.add(this._configurationService.onDidChangeConfiguration(e => {
262
if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) {
263
this._updateAriaLabel();
264
}
265
}));
266
267
this._elements.root.tabIndex = 0;
268
this._elements.statusLabel.tabIndex = 0;
269
this._updateAriaLabel();
270
271
// this._elements.status
272
this._store.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this._elements.statusLabel, () => {
273
return this._elements.statusLabel.dataset['title'];
274
}));
275
276
this._store.add(this._chatService.onDidPerformUserAction(e => {
277
if (e.sessionId === this._chatWidget.viewModel?.model.sessionId && e.action.kind === 'vote') {
278
this.updateStatus('Thank you for your feedback!', { resetAfter: 1250 });
279
}
280
}));
281
}
282
283
private _updateAriaLabel(): void {
284
285
this._elements.root.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat);
286
287
if (this._accessibilityService.isScreenReaderOptimized()) {
288
let label = defaultAriaLabel;
289
if (this._configurationService.getValue<boolean>(AccessibilityVerbositySettingId.InlineChat)) {
290
const kbLabel = this._keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel();
291
label = kbLabel
292
? localize('inlineChat.accessibilityHelp', "Inline Chat Input, Use {0} for Inline Chat Accessibility Help.", kbLabel)
293
: localize('inlineChat.accessibilityHelpNoKb', "Inline Chat Input, Run the Inline Chat Accessibility Help command for more information.");
294
}
295
this._chatWidget.inputEditor.updateOptions({ ariaLabel: label });
296
}
297
}
298
299
dispose(): void {
300
this._store.dispose();
301
}
302
303
get domNode(): HTMLElement {
304
return this._elements.root;
305
}
306
307
get chatWidget(): ChatWidget {
308
return this._chatWidget;
309
}
310
311
saveState() {
312
this._chatWidget.saveState();
313
}
314
315
layout(widgetDim: Dimension) {
316
const contentHeight = this.contentHeight;
317
this._isLayouting = true;
318
try {
319
this._doLayout(widgetDim);
320
} finally {
321
this._isLayouting = false;
322
323
if (this.contentHeight !== contentHeight) {
324
this._onDidChangeHeight.fire();
325
}
326
}
327
}
328
329
protected _doLayout(dimension: Dimension): void {
330
const extraHeight = this._getExtraHeight();
331
const statusHeight = getTotalHeight(this._elements.status);
332
333
// console.log('ZONE#Widget#layout', { height: dimension.height, extraHeight, progressHeight, followUpsHeight, statusHeight, LIST: dimension.height - progressHeight - followUpsHeight - statusHeight - extraHeight });
334
335
this._elements.root.style.height = `${dimension.height - extraHeight}px`;
336
this._elements.root.style.width = `${dimension.width}px`;
337
338
this._chatWidget.layout(
339
dimension.height - statusHeight - extraHeight,
340
dimension.width
341
);
342
}
343
344
/**
345
* The content height of this widget is the size that would require no scrolling
346
*/
347
get contentHeight(): number {
348
const data = {
349
chatWidgetContentHeight: this._chatWidget.contentHeight,
350
statusHeight: getTotalHeight(this._elements.status),
351
extraHeight: this._getExtraHeight()
352
};
353
const result = data.chatWidgetContentHeight + data.statusHeight + data.extraHeight;
354
return result;
355
}
356
357
get minHeight(): number {
358
// The chat widget is variable height and supports scrolling. It should be
359
// at least "maxWidgetHeight" high and at most the content height.
360
361
let maxWidgetOutputHeight = 100;
362
for (const item of this._chatWidget.viewModel?.getItems() ?? []) {
363
if (isResponseVM(item) && item.response.value.some(r => r.kind === 'textEditGroup' && !r.state?.applied)) {
364
maxWidgetOutputHeight = 270;
365
break;
366
}
367
}
368
369
let value = this.contentHeight;
370
value -= this._chatWidget.contentHeight;
371
value += Math.min(this._chatWidget.input.contentHeight + maxWidgetOutputHeight, this._chatWidget.contentHeight);
372
return value;
373
}
374
375
protected _getExtraHeight(): number {
376
return this._options.inZoneWidget ? 1 : (2 /*border*/ + 4 /*shadow*/);
377
}
378
379
get value(): string {
380
return this._chatWidget.getInput();
381
}
382
383
set value(value: string) {
384
this._chatWidget.setInput(value);
385
}
386
387
selectAll() {
388
this._chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1));
389
}
390
391
set placeholder(value: string) {
392
this._chatWidget.setInputPlaceholder(value);
393
}
394
395
toggleStatus(show: boolean) {
396
this._elements.toolbar1.classList.toggle('hidden', !show);
397
this._elements.toolbar2.classList.toggle('hidden', !show);
398
this._elements.status.classList.toggle('hidden', !show);
399
this._elements.infoLabel.classList.toggle('hidden', !show);
400
this._onDidChangeHeight.fire();
401
}
402
403
updateToolbar(show: boolean) {
404
this._elements.root.classList.toggle('toolbar', show);
405
this._elements.toolbar1.classList.toggle('hidden', !show);
406
this._elements.toolbar2.classList.toggle('hidden', !show);
407
this._elements.status.classList.toggle('actions', show);
408
this._elements.infoLabel.classList.toggle('hidden', show);
409
this._onDidChangeHeight.fire();
410
}
411
412
async getCodeBlockInfo(codeBlockIndex: number): Promise<ITextModel | undefined> {
413
const { viewModel } = this._chatWidget;
414
if (!viewModel) {
415
return undefined;
416
}
417
const items = viewModel.getItems().filter(i => isResponseVM(i));
418
const item = items.at(-1);
419
if (!item) {
420
return;
421
}
422
return viewModel.codeBlockModelCollection.get(viewModel.sessionId, item, codeBlockIndex)?.model;
423
}
424
425
get responseContent(): string | undefined {
426
const requests = this._chatWidget.viewModel?.model.getRequests();
427
return requests?.at(-1)?.response?.response.toString();
428
}
429
430
431
getChatModel(): IChatModel | undefined {
432
return this._chatWidget.viewModel?.model;
433
}
434
435
setChatModel(chatModel: IChatModel, state?: IChatViewState) {
436
this._chatWidget.setModel(chatModel, { ...state, inputValue: undefined });
437
}
438
439
updateInfo(message: string): void {
440
this._elements.infoLabel.classList.toggle('hidden', !message);
441
const renderedMessage = renderLabelWithIcons(message);
442
reset(this._elements.infoLabel, ...renderedMessage);
443
this._onDidChangeHeight.fire();
444
}
445
446
updateStatus(message: string, ops: { classes?: string[]; resetAfter?: number; keepMessage?: boolean; title?: string } = {}) {
447
const isTempMessage = typeof ops.resetAfter === 'number';
448
if (isTempMessage && !this._elements.statusLabel.dataset['state']) {
449
const statusLabel = this._elements.statusLabel.innerText;
450
const title = this._elements.statusLabel.dataset['title'];
451
const classes = Array.from(this._elements.statusLabel.classList.values());
452
setTimeout(() => {
453
this.updateStatus(statusLabel, { classes, keepMessage: true, title });
454
}, ops.resetAfter);
455
}
456
const renderedMessage = renderLabelWithIcons(message);
457
reset(this._elements.statusLabel, ...renderedMessage);
458
this._elements.statusLabel.className = `label status ${(ops.classes ?? []).join(' ')}`;
459
this._elements.statusLabel.classList.toggle('hidden', !message);
460
if (isTempMessage) {
461
this._elements.statusLabel.dataset['state'] = 'temp';
462
} else {
463
delete this._elements.statusLabel.dataset['state'];
464
}
465
466
if (ops.title) {
467
this._elements.statusLabel.dataset['title'] = ops.title;
468
} else {
469
delete this._elements.statusLabel.dataset['title'];
470
}
471
this._onDidChangeHeight.fire();
472
}
473
474
reset() {
475
this._chatWidget.attachmentModel.clear(true);
476
this._chatWidget.saveState();
477
478
reset(this._elements.statusLabel);
479
this._elements.statusLabel.classList.toggle('hidden', true);
480
this._elements.toolbar1.classList.add('hidden');
481
this._elements.toolbar2.classList.add('hidden');
482
this.updateInfo('');
483
484
this._elements.accessibleViewer.classList.toggle('hidden', true);
485
this._onDidChangeHeight.fire();
486
}
487
488
focus() {
489
this._chatWidget.focusInput();
490
}
491
492
hasFocus() {
493
return this.domNode.contains(getActiveElement());
494
}
495
496
}
497
498
const defaultAriaLabel = localize('aria-label', "Inline Chat Input");
499
500
export class EditorBasedInlineChatWidget extends InlineChatWidget {
501
502
private readonly _accessibleViewer = this._store.add(new MutableDisposable<HunkAccessibleDiffViewer>());
503
504
505
constructor(
506
location: IChatWidgetLocationOptions,
507
private readonly _parentEditor: ICodeEditor,
508
options: IInlineChatWidgetConstructionOptions,
509
@IContextKeyService contextKeyService: IContextKeyService,
510
@IKeybindingService keybindingService: IKeybindingService,
511
@IInstantiationService instantiationService: IInstantiationService,
512
@IAccessibilityService accessibilityService: IAccessibilityService,
513
@IConfigurationService configurationService: IConfigurationService,
514
@IAccessibleViewService accessibleViewService: IAccessibleViewService,
515
@ITextModelService textModelResolverService: ITextModelService,
516
@IChatService chatService: IChatService,
517
@IHoverService hoverService: IHoverService,
518
@ILayoutService layoutService: ILayoutService
519
) {
520
const overflowWidgetsNode = layoutService.getContainer(getWindow(_parentEditor.getContainerDomNode())).appendChild($('.inline-chat-overflow.monaco-editor'));
521
super(location, {
522
...options,
523
chatWidgetViewOptions: {
524
...options.chatWidgetViewOptions,
525
editorOverflowWidgetsDomNode: overflowWidgetsNode
526
}
527
}, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, chatService, hoverService);
528
529
this._store.add(toDisposable(() => {
530
overflowWidgetsNode.remove();
531
}));
532
}
533
534
// --- layout
535
536
override get contentHeight(): number {
537
let result = super.contentHeight;
538
539
if (this._accessibleViewer.value) {
540
result += this._accessibleViewer.value.height + 8 /* padding */;
541
}
542
543
return result;
544
}
545
546
protected override _doLayout(dimension: Dimension): void {
547
548
let newHeight = dimension.height;
549
550
if (this._accessibleViewer.value) {
551
this._accessibleViewer.value.width = dimension.width - 12;
552
newHeight -= this._accessibleViewer.value.height + 8;
553
}
554
555
super._doLayout(dimension.with(undefined, newHeight));
556
557
// update/fix the height of the zone which was set to newHeight in super._doLayout
558
this._elements.root.style.height = `${dimension.height - this._getExtraHeight()}px`;
559
}
560
561
override reset() {
562
this._accessibleViewer.clear();
563
super.reset();
564
}
565
566
// --- accessible viewer
567
568
showAccessibleHunk(session: Session, hunkData: HunkInformation): void {
569
570
this._elements.accessibleViewer.classList.remove('hidden');
571
this._accessibleViewer.clear();
572
573
this._accessibleViewer.value = this._instantiationService.createInstance(HunkAccessibleDiffViewer,
574
this._elements.accessibleViewer,
575
session,
576
hunkData,
577
new AccessibleHunk(this._parentEditor, session, hunkData)
578
);
579
580
this._onDidChangeHeight.fire();
581
}
582
}
583
584
class HunkAccessibleDiffViewer extends AccessibleDiffViewer {
585
586
readonly height: number;
587
588
set width(value: number) {
589
this._width2.set(value, undefined);
590
}
591
592
private readonly _width2: ISettableObservable<number>;
593
594
constructor(
595
parentNode: HTMLElement,
596
session: Session,
597
hunk: HunkInformation,
598
models: IAccessibleDiffViewerModel,
599
@IInstantiationService instantiationService: IInstantiationService,
600
) {
601
const width = observableValue('width', 0);
602
const diff = observableValue('diff', HunkAccessibleDiffViewer._asMapping(hunk));
603
const diffs = derived(r => [diff.read(r)]);
604
const lines = Math.min(10, 8 + diff.get().changedLineCount);
605
const height = models.getModifiedOptions().get(EditorOption.lineHeight) * lines;
606
607
super(parentNode, constObservable(true), () => { }, constObservable(false), width, constObservable(height), diffs, models, instantiationService);
608
609
this.height = height;
610
this._width2 = width;
611
612
this._store.add(session.textModelN.onDidChangeContent(() => {
613
diff.set(HunkAccessibleDiffViewer._asMapping(hunk), undefined);
614
}));
615
}
616
617
private static _asMapping(hunk: HunkInformation): DetailedLineRangeMapping {
618
const ranges0 = hunk.getRanges0();
619
const rangesN = hunk.getRangesN();
620
const originalLineRange = LineRange.fromRangeInclusive(ranges0[0]);
621
const modifiedLineRange = LineRange.fromRangeInclusive(rangesN[0]);
622
const innerChanges: RangeMapping[] = [];
623
for (let i = 1; i < ranges0.length; i++) {
624
innerChanges.push(new RangeMapping(ranges0[i], rangesN[i]));
625
}
626
return new DetailedLineRangeMapping(originalLineRange, modifiedLineRange, innerChanges);
627
}
628
629
}
630
631
class AccessibleHunk implements IAccessibleDiffViewerModel {
632
633
constructor(
634
private readonly _editor: ICodeEditor,
635
private readonly _session: Session,
636
private readonly _hunk: HunkInformation
637
) { }
638
639
getOriginalModel(): ITextModel {
640
return this._session.textModel0;
641
}
642
getModifiedModel(): ITextModel {
643
return this._session.textModelN;
644
}
645
getOriginalOptions(): IComputedEditorOptions {
646
return this._editor.getOptions();
647
}
648
getModifiedOptions(): IComputedEditorOptions {
649
return this._editor.getOptions();
650
}
651
originalReveal(range: Range): void {
652
// throw new Error('Method not implemented.');
653
}
654
modifiedReveal(range?: Range | undefined): void {
655
this._editor.revealRangeInCenterIfOutsideViewport(range || this._hunk.getRangesN()[0], ScrollType.Smooth);
656
}
657
modifiedSetSelection(range: Range): void {
658
// this._editor.revealRangeInCenterIfOutsideViewport(range, ScrollType.Smooth);
659
// this._editor.setSelection(range);
660
}
661
modifiedFocus(): void {
662
this._editor.focus();
663
}
664
getModifiedPosition(): Position | undefined {
665
return this._hunk.getRangesN()[0].getStartPosition();
666
}
667
}
668
669