diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts similarity index 77% rename from src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatActions.ts rename to src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts index 575a5ddedeb..8d6325c6b26 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts @@ -6,7 +6,6 @@ import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { ILanguageService } from 'vs/editor/common/languages/language'; import { localize, localize2 } from 'vs/nls'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; @@ -15,11 +14,9 @@ import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkey import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { insertCell } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; +import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS, NotebookChatController } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController'; import { INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, getEditorFromArgsOrActivePane } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; -import { insertNewCell } from 'vs/workbench/contrib/notebook/browser/controller/insertCellActions'; -import { CellEditState, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS, NotebookCellChatController } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController'; +import { CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; @@ -46,12 +43,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const ctrl = NotebookCellChatController.get(context.cell); - if (!ctrl) { - return; - } - - ctrl.acceptInput(); + NotebookChatController.get(context.notebookEditor)?.acceptInput(); } }); @@ -115,9 +107,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const editor = context.notebookEditor; - const activeCell = context.cell; - await editor.focusNotebookCell(activeCell, 'editor'); + await NotebookChatController.get(context.notebookEditor)?.focusNext(); } }); @@ -146,14 +136,8 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const activeCell = context.cell; - // Navigate to cell chat widget if it exists - const controller = NotebookCellChatController.get(activeCell); - if (controller && controller.isWidgetVisible()) { - controller.focusWidget(); - return; - } - + const index = context.notebookEditor.getCellIndex(context.cell); + await NotebookChatController.get(context.notebookEditor)?.focusNearestWidget(index, 'above'); } }); @@ -182,29 +166,8 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const editor = context.notebookEditor; - const activeCell = context.cell; - - const idx = editor.getCellIndex(activeCell); - if (typeof idx !== 'number') { - return; - } - - if (idx >= editor.getLength() - 1) { - // last one - return; - } - - const targetCell = editor.cellAt(idx + 1); - - if (targetCell) { - // Navigate to cell chat widget if it exists - const controller = NotebookCellChatController.get(targetCell); - if (controller && controller.isWidgetVisible()) { - controller.focusWidget(); - return; - } - } + const index = context.notebookEditor.getCellIndex(context.cell); + await NotebookChatController.get(context.notebookEditor)?.focusNearestWidget(index, 'below'); } }); @@ -225,12 +188,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const ctrl = NotebookCellChatController.get(context.cell); - if (!ctrl) { - return; - } - - ctrl.cancelCurrentRequest(false); + NotebookChatController.get(context.notebookEditor)?.cancelCurrentRequest(false); } }); @@ -250,12 +208,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const ctrl = NotebookCellChatController.get(context.cell); - if (!ctrl) { - return; - } - - ctrl.dismiss(false); + NotebookChatController.get(context.notebookEditor)?.dismiss(); } }); @@ -285,12 +238,7 @@ registerAction2(class extends NotebookAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const ctrl = NotebookCellChatController.get(context.cell); - if (!ctrl) { - return; - } - - ctrl.acceptSession(); + NotebookChatController.get(context.notebookEditor)?.acceptSession(); } }); @@ -315,15 +263,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const ctrl = NotebookCellChatController.get(context.cell); - if (!ctrl) { - return; - } - - // todo discard - ctrl.dismiss(true); - // focus on the cell editor container - context.notebookEditor.focusNotebookCell(context.cell, 'container'); + NotebookChatController.get(context.notebookEditor)?.discard(); } }); @@ -343,12 +283,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const ctrl = NotebookCellChatController.get(context.cell); - if (!ctrl) { - return; - } - - ctrl.feedbackLast(InlineChatResponseFeedbackKind.Helpful); + NotebookChatController.get(context.notebookEditor)?.feedbackLast(InlineChatResponseFeedbackKind.Helpful); } }); @@ -368,12 +303,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const ctrl = NotebookCellChatController.get(context.cell); - if (!ctrl) { - return; - } - - ctrl.feedbackLast(InlineChatResponseFeedbackKind.Unhelpful); + NotebookChatController.get(context.notebookEditor)?.feedbackLast(InlineChatResponseFeedbackKind.Unhelpful); } }); @@ -393,12 +323,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const ctrl = NotebookCellChatController.get(context.cell); - if (!ctrl) { - return; - } - - ctrl.feedbackLast(InlineChatResponseFeedbackKind.Bug); + NotebookChatController.get(context.notebookEditor)?.feedbackLast(InlineChatResponseFeedbackKind.Bug); } }); @@ -458,7 +383,22 @@ registerAction2(class extends NotebookAction { override getEditorContextFromArgsOrActive(accessor: ServicesAccessor, ...args: any[]): IInsertCellWithChatArgs | undefined { const [firstArg] = args; if (!firstArg) { - return undefined; + const notebookEditor = getEditorFromArgsOrActivePane(accessor); + if (!notebookEditor) { + return undefined; + } + + const activeCell = notebookEditor.getActiveCell(); + if (!activeCell) { + return undefined; + } + + return { + cell: activeCell, + notebookEditor, + input: undefined, + autoSend: undefined + }; } if (typeof firstArg !== 'object' || typeof firstArg.index !== 'number') { @@ -481,33 +421,9 @@ registerAction2(class extends NotebookAction { } async runWithContext(accessor: ServicesAccessor, context: IInsertCellWithChatArgs) { - let newCell: ICellViewModel | null = null; - if (!context.cell) { - // insert at the top - const languageService = accessor.get(ILanguageService); - newCell = insertCell(languageService, context.notebookEditor, 0, CellKind.Code, 'above', undefined, true); - } else { - newCell = insertNewCell(accessor, context, CellKind.Code, 'below', true); - } - - if (!newCell) { - return; - } - - await context.notebookEditor.focusNotebookCell(newCell, 'container'); - const ctrl = NotebookCellChatController.get(newCell); - if (!ctrl) { - return; - } - - context.notebookEditor.getCellsInRange().forEach(cell => { - const cellCtrl = NotebookCellChatController.get(cell); - if (cellCtrl) { - cellCtrl.dismiss(false); - } - }); - - ctrl.show(context.input, context.autoSend); + const index = Math.max(0, context.cell ? context.notebookEditor.getCellIndex(context.cell) + 1 : 0); + context.notebookEditor.focusContainer(); + NotebookChatController.get(context.notebookEditor)?.run(index, context.input, context.autoSend); } }); @@ -537,26 +453,8 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const languageService = accessor.get(ILanguageService); - const newCell = insertCell(languageService, context.notebookEditor, 0, CellKind.Code, 'above', undefined, true); - - if (!newCell) { - return; - } - await context.notebookEditor.focusNotebookCell(newCell, 'container'); - const ctrl = NotebookCellChatController.get(newCell); - if (!ctrl) { - return; - } - - context.notebookEditor.getCellsInRange().forEach(cell => { - const cellCtrl = NotebookCellChatController.get(cell); - if (cellCtrl) { - cellCtrl.dismiss(false); - } - }); - - ctrl.show(); + context.notebookEditor.focusContainer(); + NotebookChatController.get(context.notebookEditor)?.run(0, '', false); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts similarity index 54% rename from src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts rename to src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts index 0e05e8cae14..86aadb52b8d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -3,25 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Dimension, WindowIntervalTimer } from 'vs/base/browser/dom'; -import { CancelablePromise, Queue, createCancelablePromise, raceCancellationError } from 'vs/base/common/async'; +import { Dimension, WindowIntervalTimer, getWindow, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; +import { CancelablePromise, Queue, createCancelablePromise, disposableTimeout, raceCancellationError } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; import { MovingAverage } from 'vs/base/common/numbers'; import { StopWatch } from 'vs/base/common/stopwatch'; import { assertType } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { TextEdit } from 'vs/editor/common/languages'; -import { ICursorStateComputer } from 'vs/editor/common/model'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ICursorStateComputer, ITextModel } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; +import { IModelService } from 'vs/editor/common/services/model'; import { localize } from 'vs/nls'; -import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { MenuId } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -29,16 +33,21 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { AsyncProgress } from 'vs/platform/progress/common/progress'; import { SaveReason } from 'vs/workbench/common/editor'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; -import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; +import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingService'; import { EmptyResponse, ErrorResponse, ReplyResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; import { ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; -import { asProgressiveEdit, performAsyncTextEdit } from 'vs/workbench/contrib/inlineChat/browser/utils'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; -import { CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, CTX_INLINE_CHAT_VISIBLE, EditMode, IInlineChatProgressItem, IInlineChatRequest, InlineChatResponseFeedbackKind, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingService'; +import { asProgressiveEdit, performAsyncTextEdit } from 'vs/workbench/contrib/inlineChat/browser/utils'; +import { CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, EditMode, IInlineChatProgressItem, IInlineChatRequest, InlineChatResponseFeedbackKind, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { insertCell, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; +import { INotebookEditor, INotebookEditorContribution, INotebookViewZone, ScrollToRevealBehavior } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; +import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + + +import 'vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions'; export const CTX_NOTEBOOK_CELL_CHAT_FOCUSED = new RawContextKey('notebookCellChatFocused', false, localize('notebookCellChatFocused', "Whether the cell chat editor is focused")); export const CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST = new RawContextKey('notebookChatHasActiveRequest', false, localize('notebookChatHasActiveRequest', "Whether the cell chat editor has an active request")); @@ -48,219 +57,374 @@ export const MENU_CELL_CHAT_WIDGET_STATUS = MenuId.for('cellChatWidget.status'); export const MENU_CELL_CHAT_WIDGET_FEEDBACK = MenuId.for('cellChatWidget.feedback'); export const MENU_CELL_CHAT_WIDGET_TOOLBAR = MenuId.for('cellChatWidget.toolbar'); -interface ICellChatPart { - activeCell: ICellViewModel | undefined; -} +const WIDGET_MARGIN_BOTTOM = 16; -export class NotebookCellChatController extends Disposable { - private static _cellChatControllers = new WeakMap(); +class NotebookChatWidget extends Disposable implements INotebookViewZone { + private _afterModelPosition: number; - static get(cell: ICellViewModel): NotebookCellChatController | undefined { - return NotebookCellChatController._cellChatControllers.get(cell); + set afterModelPosition(afterModelPosition: number) { + this._afterModelPosition = afterModelPosition; } - private _sessionCtor: CancelablePromise | undefined; - private _activeSession?: Session; - private readonly _ctxHasActiveRequest: IContextKey; - private _isVisible: boolean = false; - private _strategy: EditStrategy | undefined; + get afterModelPosition(): number { + return this._afterModelPosition; + } + + private _heightInPx: number; + + set heightInPx(heightInPx: number) { + this._heightInPx = heightInPx; + } + + get heightInPx(): number { + return this._heightInPx; + } + + private _editingCell: CellViewModel | null = null; - private _inlineChatListener: IDisposable | undefined; - private _widget: InlineChatWidget | undefined; - private _toolbar: MenuWorkbenchToolBar | undefined; - private readonly _ctxVisible: IContextKey; - private readonly _ctxCellWidgetFocused: IContextKey; - private readonly _ctxLastResponseType: IContextKey; - private _widgetDisposableStore: DisposableStore = this._register(new DisposableStore()); constructor( - private readonly _notebookEditor: INotebookEditorDelegate, - private readonly _chatPart: ICellChatPart, - private readonly _cell: ICellViewModel, - private readonly _partContainer: HTMLElement, - @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, - @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, - @ICommandService private readonly _commandService: ICommandService, - @IInlineChatSavingService private readonly _inlineChatSavingService: IInlineChatSavingService, + private readonly _notebookEditor: INotebookEditor, + readonly id: string, + readonly domNode: HTMLElement, + readonly widgetContainer: HTMLElement, + readonly inlineChatWidget: InlineChatWidget, + readonly parentEditor: CodeEditorWidget, + afterModelPosition: number, + heightInPx: number, + private readonly _languageService: ILanguageService, ) { super(); - NotebookCellChatController._cellChatControllers.set(this._cell, this); - this._ctxHasActiveRequest = CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST.bindTo(this._contextKeyService); - this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(_contextKeyService); - this._ctxCellWidgetFocused = CTX_NOTEBOOK_CELL_CHAT_FOCUSED.bindTo(this._contextKeyService); - this._ctxLastResponseType = CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.bindTo(this._contextKeyService); + this._afterModelPosition = afterModelPosition; + this._heightInPx = heightInPx; - this._register(this._cell.onDidChangeEditorAttachState(() => { - const editor = this._getCellEditor(); - this._inlineChatListener?.dispose(); - - if (!editor) { - return; - } - - if (!this._widget && this._isVisible) { - this._initialize(editor); - } - - const inlineChatController = InlineChatController.get(editor); - if (inlineChatController) { - this._inlineChatListener = inlineChatController.onWillStartSession(() => { - this.dismiss(false); - }); - } + this._register(inlineChatWidget.onDidChangeHeight(() => { + this.heightInPx = inlineChatWidget.getHeight() + WIDGET_MARGIN_BOTTOM; + this._notebookEditor.changeViewZones(accessor => { + accessor.layoutZone(id); + }); + this._layoutWidget(inlineChatWidget, widgetContainer); })); + + this._layoutWidget(inlineChatWidget, widgetContainer); } - private _initialize(editor: IActiveCodeEditor) { - this._widget = this._instantiationService.createInstance(InlineChatWidget, editor, { - menuId: MENU_CELL_CHAT_INPUT, - widgetMenuId: MENU_CELL_CHAT_WIDGET, - statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS, - feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK + focus() { + this.inlineChatWidget.focus(); + } + + async getEditingCellEditor() { + if (this._editingCell) { + await this._notebookEditor.focusNotebookCell(this._editingCell, 'editor'); + return this._notebookEditor.activeCodeEditor; + } + + if (!this._notebookEditor.hasModel()) { + return undefined; + } + + this._editingCell = insertCell(this._languageService, this._notebookEditor, this._afterModelPosition, CellKind.Code, 'above'); + + if (!this._editingCell) { + return undefined; + } + + await this._notebookEditor.focusNotebookCell(this._editingCell, 'editor', { revealBehavior: ScrollToRevealBehavior.firstLine }); + return this._notebookEditor.activeCodeEditor; + } + + async discardChange() { + if (this._notebookEditor.hasModel() && this._editingCell) { + // remove the cell from the notebook + runDeleteAction(this._notebookEditor, this._editingCell); + } + } + + private _layoutWidget(inlineChatWidget: InlineChatWidget, widgetContainer: HTMLElement) { + const layoutConfiguration = this._notebookEditor.notebookOptions.getLayoutConfiguration(); + const rightMargin = layoutConfiguration.cellRightMargin; + const leftMargin = this._notebookEditor.notebookOptions.getCellEditorContainerLeftMargin(); + const maxWidth = !inlineChatWidget.showsAnyPreview() ? 640 : Number.MAX_SAFE_INTEGER; + const width = Math.min(maxWidth, this._notebookEditor.getLayoutInfo().width - leftMargin - rightMargin); + + inlineChatWidget.layout(new Dimension(width, 80 + WIDGET_MARGIN_BOTTOM)); + inlineChatWidget.domNode.style.width = `${width}px`; + widgetContainer.style.left = `${leftMargin}px`; + } + + override dispose() { + this._notebookEditor.changeViewZones(accessor => { + accessor.removeZone(this.id); }); - - this._widgetDisposableStore.add(this._widget.onDidChangeHeight(() => { - this._updateHeight(); - })); - - this._widgetDisposableStore.add(this._notebookExecutionStateService.onDidChangeExecution(e => { - if (e.notebook.toString() !== this._notebookEditor.textModel?.uri.toString()) { - return; - } - - if (e.type === NotebookExecutionType.cell && e.affectsCell(this._cell.uri) && e.changed === undefined /** complete */) { - // check if execution is successfull - const { lastRunSuccess } = this._cell.internalMetadata; - if (lastRunSuccess) { - this._strategy?.createSnapshot(); - } - } - })); - - - this._partContainer.appendChild(this._widget.domNode); - } - - public override dispose(): void { - if (this._isVisible) { - // detach the chat widget - this._widget?.reset(); - this._sessionCtor?.cancel(); - this._sessionCtor = undefined; - } - - try { - if (this._widget) { - this._partContainer.removeChild(this._widget.domNode); - } - - } catch (_ex) { - // might not be attached - } - - // dismiss since we can't restore the widget properly now - this.dismiss(false); - this._widget?.dispose(); - this._inlineChatListener?.dispose(); - this._toolbar?.dispose(); - this._inlineChatListener = undefined; - this._ctxHasActiveRequest.reset(); - this._ctxVisible.reset(); - NotebookCellChatController._cellChatControllers.delete(this._cell); + this.domNode.remove(); super.dispose(); } +} - isWidgetVisible() { - return this._isVisible; +export class NotebookChatController extends Disposable implements INotebookEditorContribution { + static id: string = 'workbench.notebook.chatController'; + static counter: number = 0; + + public static get(editor: INotebookEditor): NotebookChatController | null { + return editor.getContribution(NotebookChatController.id); + } + private _strategy: EditStrategy | undefined; + private _sessionCtor: CancelablePromise | undefined; + private _activeSession?: Session; + private readonly _ctxHasActiveRequest: IContextKey; + private readonly _ctxCellWidgetFocused: IContextKey; + private readonly _ctxLastResponseType: IContextKey; + private _widget: NotebookChatWidget | undefined; + constructor( + private readonly _notebookEditor: INotebookEditor, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ICommandService private readonly _commandService: ICommandService, + @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, + @IInlineChatSavingService private readonly _inlineChatSavingService: IInlineChatSavingService, + @IModelService private readonly _modelService: IModelService, + @ILanguageService private readonly _languageService: ILanguageService, + + ) { + super(); + this._ctxHasActiveRequest = CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST.bindTo(this._contextKeyService); + this._ctxCellWidgetFocused = CTX_NOTEBOOK_CELL_CHAT_FOCUSED.bindTo(this._contextKeyService); + this._ctxLastResponseType = CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.bindTo(this._contextKeyService); } - layout() { - if (this._isVisible && this._widget) { - const width = this._notebookEditor.getLayoutInfo().width - (/** margin */ 16 + 6) - (/** padding */ 6 * 2); - const height = this._widget.getHeight(); - this._widget.layout(new Dimension(width, height)); - } - } + run(index: number, input: string | undefined, autoSend: boolean | undefined): void { + if (this._widget) { + if (this._widget.afterModelPosition === index) { + // this._chatZone + // chatZone focus + } else { + const window = getWindow(this._widget.domNode); + this._widget.dispose(); + this._widget = undefined; - private _updateHeight() { - const surrounding = 6 * 2 /** padding */ + 6 /** cell chat widget margin bottom */ + 2 /** border */; - const heightWithPadding = this._isVisible && this._widget - ? (this._widget.getHeight() - 8 /** shadow */ - 18 /** padding */ - 6 /** widget's internal margin top */ + surrounding) - : 0; + scheduleAtNextAnimationFrame(window, () => { + this._createWidget(index, input, autoSend); + }); + } - if (this._cell.chatHeight === heightWithPadding) { return; } - this._cell.chatHeight = heightWithPadding; - this._partContainer.style.height = `${heightWithPadding - surrounding}px`; + this._createWidget(index, input, autoSend); + // TODO: reveal widget to the center if it's out of the viewport } - async show(input?: string, autoSend?: boolean) { - this._isVisible = true; - if (!this._widget) { - const editor = this._getCellEditor(); - if (editor) { - this._initialize(editor); + private _createWidget(index: number, input: string | undefined, autoSend: boolean | undefined) { + const viewZoneContainer = document.createElement('div'); + viewZoneContainer.classList.add('monaco-editor'); + const widgetContainer = document.createElement('div'); + widgetContainer.style.position = 'absolute'; + viewZoneContainer.appendChild(widgetContainer); + + const fakeParentEditorElement = document.createElement('div'); + + const fakeParentEditor = this._instantiationService.createInstance( + CodeEditorWidget, + fakeParentEditorElement, + { + }, + { isSimpleWidget: true } + ); + + const inputBoxPath = `/notebook-chat-input0-${NotebookChatController.counter++}`; + const inputUri = URI.from({ scheme: Schemas.untitled, path: inputBoxPath }); + const result: ITextModel = this._modelService.createModel('', null, inputUri, false); + fakeParentEditor.setModel(result); + + const inlineChatWidget = this._instantiationService.createInstance( + InlineChatWidget, + fakeParentEditor, + { + menuId: MENU_CELL_CHAT_INPUT, + widgetMenuId: MENU_CELL_CHAT_WIDGET, + statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS, + feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK } - } + ); + inlineChatWidget.placeholder = localize('default.placeholder', "Ask a question"); + inlineChatWidget.updateInfo(localize('welcome.1', "AI-generated code may be incorrect")); + widgetContainer.appendChild(inlineChatWidget.domNode); - this._partContainer.style.display = 'flex'; - this._widget?.focus(); - this._widget?.updateInfo(localize('welcome.1', "AI-generated code may be incorrect")); - this._ctxVisible.set(true); - this._ctxCellWidgetFocused.set(true); - this._updateHeight(); + this._notebookEditor.changeViewZones(accessor => { + const id = accessor.addZone({ + afterModelPosition: index, + heightInPx: 80 + WIDGET_MARGIN_BOTTOM, + domNode: viewZoneContainer + }); - this._sessionCtor = createCancelablePromise(async token => { - if (this._cell.editorAttached) { - const editor = this._getCellEditor(); - if (editor) { - await this._startSession(editor, token); + this._widget = new NotebookChatWidget( + this._notebookEditor, + id, + viewZoneContainer, + widgetContainer, + inlineChatWidget, + fakeParentEditor, + index, + 80 + WIDGET_MARGIN_BOTTOM, + this._languageService + ); + + disposableTimeout(() => { + this._ctxCellWidgetFocused.set(true); + this._widget?.focus(); + }, 0, this._store); + + this._sessionCtor = createCancelablePromise(async token => { + + if (fakeParentEditor.hasModel()) { + this._startSession(fakeParentEditor, token); + + if (this._widget) { + this._widget.inlineChatWidget.placeholder = this._activeSession?.session.placeholder ?? localize('default.placeholder', "Ask a question"); + this._widget.inlineChatWidget.updateInfo(this._activeSession?.session.message ?? localize('welcome.1', "AI-generated code may be incorrect")); + this._widget.focus(); + } + + if (this._widget && input) { + this._widget.inlineChatWidget.value = input; + + if (autoSend) { + this.acceptInput(); + } + } } - } else { - await Event.toPromise(Event.once(this._cell.onDidChangeEditorAttachState)); - if (token.isCancellationRequested) { - return; - } - - const editor = this._getCellEditor(); - if (editor) { - await this._startSession(editor, token); - } - } - - if (this._widget) { - this._widget.placeholder = this._activeSession?.session.placeholder ?? localize('default.placeholder', "Ask a question"); - this._widget.updateInfo(this._activeSession?.session.message ?? localize('welcome.1', "AI-generated code may be incorrect")); - this._widget.focus(); - } - - if (this._widget && input) { - this._widget.value = input; - - if (autoSend) { - this.acceptInput(); - } - } + }); }); } - async focusWidget() { - this._widget?.focus(); - } + async acceptInput() { + assertType(this._activeSession); + assertType(this._widget); + this._activeSession.addInput(new SessionPrompt(this._widget.inlineChatWidget.value)); - private _getCellEditor() { - const editors = this._notebookEditor.codeEditors.find(editor => editor[0] === this._chatPart.activeCell); - if (!editors || !editors[1].hasModel()) { + assertType(this._activeSession.lastInput); + const value = this._activeSession.lastInput.value; + const editor = this._widget.parentEditor; + const model = editor.getModel(); + + if (!editor.hasModel() || !model) { return; } - const editor = editors[1]; - return editor; + this._ctxHasActiveRequest.set(true); + this._widget?.inlineChatWidget.updateProgress(true); + + const request: IInlineChatRequest = { + requestId: generateUuid(), + prompt: value, + attempt: 0, + selection: { selectionStartLineNumber: 1, selectionStartColumn: 1, positionLineNumber: 1, positionColumn: 1 }, + wholeRange: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, + live: true, + previewDocument: model.uri, + withIntentDetection: true, // TODO: don't hard code but allow in corresponding UI to run without intent detection? + }; + + //TODO: update progress in a newly inserted cell below the widget instead of the fake editor + + const requestCts = new CancellationTokenSource(); + const progressEdits: TextEdit[][] = []; + const progressiveEditsQueue = new Queue(); + const progressiveEditsClock = StopWatch.create(); + const progressiveEditsAvgDuration = new MovingAverage(); + const progressiveEditsCts = new CancellationTokenSource(requestCts.token); + const progress = new AsyncProgress(async data => { + // console.log('received chunk', data, request); + + if (requestCts.token.isCancellationRequested) { + return; + } + + if (data.message) { + this._widget?.inlineChatWidget.updateToolbar(false); + this._widget?.inlineChatWidget.updateInfo(data.message); + } + + if (data.edits?.length) { + if (!request.live) { + throw new Error('Progress in NOT supported in non-live mode'); + } + progressEdits.push(data.edits); + progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed()); + progressiveEditsClock.reset(); + + progressiveEditsQueue.queue(async () => { + // making changes goes into a queue because otherwise the async-progress time will + // influence the time it takes to receive the changes and progressive typing will + // become infinitely fast + await this._makeChanges(data.edits!, data.editsShouldBeInstant + ? undefined + : { duration: progressiveEditsAvgDuration.value, token: progressiveEditsCts.token } + ); + }); + } + }); + + const task = this._activeSession.provider.provideResponse(this._activeSession.session, request, progress, requestCts.token); + let response: ReplyResponse | ErrorResponse | EmptyResponse; + + try { + this._widget?.inlineChatWidget.updateChatMessage(undefined); + this._widget?.inlineChatWidget.updateFollowUps(undefined); + this._widget?.inlineChatWidget.updateProgress(true); + this._widget?.inlineChatWidget.updateInfo(!this._activeSession.lastExchange ? localize('thinking', "Thinking\u2026") : ''); + this._ctxHasActiveRequest.set(true); + + const reply = await raceCancellationError(Promise.resolve(task), requestCts.token); + if (progressiveEditsQueue.size > 0) { + // we must wait for all edits that came in via progress to complete + await Event.toPromise(progressiveEditsQueue.onDrained); + } + await progress.drain(); + + if (!reply) { + response = new EmptyResponse(); + } else { + const markdownContents = new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false }); + const replyResponse = response = this._instantiationService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), progressEdits, request.requestId); + for (let i = progressEdits.length; i < replyResponse.allLocalEdits.length; i++) { + await this._makeChanges(replyResponse.allLocalEdits[i], undefined); + } + + if (this._activeSession?.provider.provideFollowups) { + const followupCts = new CancellationTokenSource(); + const followups = await this._activeSession.provider.provideFollowups(this._activeSession.session, replyResponse.raw, followupCts.token); + if (followups && this._widget) { + const widget = this._widget; + widget.inlineChatWidget.updateFollowUps(followups, async followup => { + if (followup.kind === 'reply') { + widget.inlineChatWidget.value = followup.message; + this.acceptInput(); + } else { + await this.acceptSession(); + this._commandService.executeCommand(followup.commandId, ...(followup.args ?? [])); + } + }); + } + } + } + } catch (e) { + response = new ErrorResponse(e); + } finally { + this._ctxHasActiveRequest.set(false); + this._widget?.inlineChatWidget.updateProgress(false); + this._widget?.inlineChatWidget.updateInfo(''); + this._widget?.inlineChatWidget.updateToolbar(true); + } + + this._ctxHasActiveRequest.set(false); + this._widget?.inlineChatWidget.updateProgress(false); + this._widget?.inlineChatWidget.updateInfo(''); + this._widget?.inlineChatWidget.updateToolbar(true); + + this._activeSession.addExchange(new SessionExchange(this._activeSession.lastInput, response)); + this._ctxLastResponseType.set(response instanceof ReplyResponse ? response.raw.type : undefined); } private async _startSession(editor: IActiveCodeEditor, token: CancellationToken) { @@ -282,184 +446,18 @@ export class NotebookCellChatController extends Disposable { this._strategy = new EditStrategy(session); } - async acceptInput() { + private async _makeChanges(edits: TextEdit[], opts: ProgressingEditsOptions | undefined) { assertType(this._activeSession); + assertType(this._strategy); assertType(this._widget); - this._activeSession.addInput(new SessionPrompt(this._widget.value)); - assertType(this._activeSession.lastInput); + const editor = await this._widget.getEditingCellEditor(); - const value = this._activeSession.lastInput.value; - const editors = this._notebookEditor.codeEditors.find(editor => editor[0] === this._chatPart.activeCell); - if (!editors || !editors[1].hasModel()) { + if (!editor || !editor.hasModel()) { return; } - const editor = editors[1]; - - this._ctxHasActiveRequest.set(true); - this._widget?.updateProgress(true); - - const request: IInlineChatRequest = { - requestId: generateUuid(), - prompt: value, - attempt: 0, - selection: { selectionStartLineNumber: 1, selectionStartColumn: 1, positionLineNumber: 1, positionColumn: 1 }, - wholeRange: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, - live: true, - previewDocument: editor.getModel().uri, - withIntentDetection: true, // TODO: don't hard code but allow in corresponding UI to run without intent detection? - }; - - const requestCts = new CancellationTokenSource(); - const progressEdits: TextEdit[][] = []; - const progressiveEditsQueue = new Queue(); - const progressiveEditsClock = StopWatch.create(); - const progressiveEditsAvgDuration = new MovingAverage(); - const progressiveEditsCts = new CancellationTokenSource(requestCts.token); - const progress = new AsyncProgress(async data => { - // console.log('received chunk', data, request); - - if (requestCts.token.isCancellationRequested) { - return; - } - - if (data.message) { - this._widget?.updateToolbar(false); - this._widget?.updateInfo(data.message); - } - - if (data.edits?.length) { - if (!request.live) { - throw new Error('Progress in NOT supported in non-live mode'); - } - progressEdits.push(data.edits); - progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed()); - progressiveEditsClock.reset(); - - progressiveEditsQueue.queue(async () => { - // making changes goes into a queue because otherwise the async-progress time will - // influence the time it takes to receive the changes and progressive typing will - // become infinitely fast - await this._makeChanges(editor, data.edits!, data.editsShouldBeInstant - ? undefined - : { duration: progressiveEditsAvgDuration.value, token: progressiveEditsCts.token } - ); - }); - } - }); - - const task = this._activeSession.provider.provideResponse(this._activeSession.session, request, progress, requestCts.token); - let response: ReplyResponse | ErrorResponse | EmptyResponse; - - try { - this._widget?.updateChatMessage(undefined); - this._widget?.updateFollowUps(undefined); - this._widget?.updateProgress(true); - this._widget?.updateInfo(!this._activeSession.lastExchange ? localize('thinking', "Thinking\u2026") : ''); - this._ctxHasActiveRequest.set(true); - - const reply = await raceCancellationError(Promise.resolve(task), requestCts.token); - if (progressiveEditsQueue.size > 0) { - // we must wait for all edits that came in via progress to complete - await Event.toPromise(progressiveEditsQueue.onDrained); - } - await progress.drain(); - - if (!reply) { - response = new EmptyResponse(); - } else { - const markdownContents = new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false }); - const replyResponse = response = this._instantiationService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), progressEdits, request.requestId); - for (let i = progressEdits.length; i < replyResponse.allLocalEdits.length; i++) { - await this._makeChanges(editor, replyResponse.allLocalEdits[i], undefined); - } - - if (this._activeSession?.provider.provideFollowups) { - const followupCts = new CancellationTokenSource(); - const followups = await this._activeSession.provider.provideFollowups(this._activeSession.session, replyResponse.raw, followupCts.token); - if (followups && this._widget) { - const widget = this._widget; - widget.updateFollowUps(followups, async followup => { - if (followup.kind === 'reply') { - widget.value = followup.message; - this.acceptInput(); - } else { - await this.acceptSession(); - this._commandService.executeCommand(followup.commandId, ...(followup.args ?? [])); - } - }); - } - } - } - } catch (e) { - response = new ErrorResponse(e); - } finally { - this._ctxHasActiveRequest.set(false); - this._widget?.updateProgress(false); - this._widget?.updateInfo(''); - this._widget?.updateToolbar(true); - } - - this._ctxHasActiveRequest.set(false); - this._widget?.updateProgress(false); - this._widget?.updateInfo(''); - this._widget?.updateToolbar(true); - - this._activeSession.addExchange(new SessionExchange(this._activeSession.lastInput, response)); - this._ctxLastResponseType.set(response instanceof ReplyResponse ? response.raw.type : undefined); - } - - async cancelCurrentRequest(discard: boolean) { - if (discard) { - this._strategy?.cancel(); - } - - if (this._activeSession) { - this._inlineChatSessionService.releaseSession(this._activeSession); - } - - this._activeSession = undefined; - } - - async acceptSession() { - assertType(this._activeSession); - assertType(this._strategy); - - const editor = this._getCellEditor(); - assertType(editor); - - try { - await this._strategy.apply(editor); - } catch (_err) { } - - this._inlineChatSessionService.releaseSession(this._activeSession); - this.dismiss(false); - } - - async dismiss(discard: boolean) { - this._isVisible = false; - this._partContainer.style.display = 'none'; - this.cancelCurrentRequest(discard); - this._ctxCellWidgetFocused.set(false); - this._ctxVisible.set(false); - this._ctxLastResponseType.reset(); - this._widget?.reset(); - this._updateHeight(); - } - - async feedbackLast(kind: InlineChatResponseFeedbackKind) { - if (this._activeSession?.lastExchange && this._activeSession.lastExchange.response instanceof ReplyResponse) { - this._activeSession.provider.handleInlineChatResponseFeedback?.(this._activeSession.session, this._activeSession.lastExchange.response.raw, kind); - this._widget?.updateStatus('Thank you for your feedback!', { resetAfter: 1250 }); - } - } - - private async _makeChanges(editor: IActiveCodeEditor, edits: TextEdit[], opts: ProgressingEditsOptions | undefined) { - assertType(this._activeSession); - assertType(this._strategy); - - const moreMinimalEdits = await this._editorWorkerService.computeMoreMinimalEdits(this._activeSession.textModelN.uri, edits); + const moreMinimalEdits = await this._editorWorkerService.computeMoreMinimalEdits(editor.getModel().uri, edits); // this._log('edits from PROVIDER and after making them MORE MINIMAL', this._activeSession.provider.debugName, edits, moreMinimalEdits); if (moreMinimalEdits?.length === 0) { @@ -484,9 +482,91 @@ export class NotebookCellChatController extends Disposable { // this._ignoreModelContentChanged = false; } } + + async acceptSession() { + assertType(this._activeSession); + assertType(this._strategy); + + const editor = this._widget?.parentEditor; + if (!editor?.hasModel()) { + return; + } + + try { + await this._strategy.apply(editor); + } catch (_err) { } + + this._inlineChatSessionService.releaseSession(this._activeSession); + this.dismiss(); + } + + async focusNext() { + if (!this._widget) { + return; + } + + const index = this._widget.afterModelPosition; + const cell = this._notebookEditor.cellAt(index); + if (!cell) { + return; + } + + await this._notebookEditor.focusNotebookCell(cell, 'editor'); + } + + focusNearestWidget(index: number, direction: 'above' | 'below') { + switch (direction) { + case 'above': + if (this._widget?.afterModelPosition === index) { + this._widget.focus(); + } + break; + case 'below': + if (this._widget?.afterModelPosition === index + 1) { + this._widget.focus(); + } + break; + default: + break; + } + } + + + async cancelCurrentRequest(discard: boolean) { + if (discard) { + this._strategy?.cancel(); + } + + if (this._activeSession) { + this._inlineChatSessionService.releaseSession(this._activeSession); + } + + this._activeSession = undefined; + } + + discard() { + this._strategy?.cancel(); + this._widget?.discardChange(); + } + + async feedbackLast(kind: InlineChatResponseFeedbackKind) { + if (this._activeSession?.lastExchange && this._activeSession.lastExchange.response instanceof ReplyResponse) { + this._activeSession.provider.handleInlineChatResponseFeedback?.(this._activeSession.session, this._activeSession.lastExchange.response.raw, kind); + this._widget?.inlineChatWidget.updateStatus('Thank you for your feedback!', { resetAfter: 1250 }); + } + } + + + dismiss() { + this._ctxCellWidgetFocused.set(false); + this._sessionCtor?.cancel(); + this._sessionCtor = undefined; + this._widget?.dispose(); + this._widget = undefined; + } } -class EditStrategy { +export class EditStrategy { private _editCount: number = 0; constructor( @@ -558,3 +638,7 @@ class EditStrategy { } } } + + +registerNotebookContribution(NotebookChatController.id, NotebookChatController); + diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatPart.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatPart.ts index 461dc85c664..059f5ab7b14 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatPart.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatPart.ts @@ -3,54 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellContentPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; -import { NotebookCellChatController } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController'; - -import 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatActions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export class CellChatPart extends CellContentPart { - private _controller: NotebookCellChatController | undefined; + // private _controller: NotebookCellChatController | undefined; get activeCell() { return this.currentCell; } constructor( - private readonly _notebookEditor: INotebookEditorDelegate, - private readonly _partContainer: HTMLElement, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IConfigurationService private readonly _configurationService: IConfigurationService, + _notebookEditor: INotebookEditorDelegate, + _partContainer: HTMLElement, ) { super(); } override didRenderCell(element: ICellViewModel): void { - this._controller?.dispose(); - const enabled = this._configurationService.getValue(NotebookSetting.cellChat); - if (enabled) { - this._controller = this._instantiationService.createInstance(NotebookCellChatController, this._notebookEditor, this, element, this._partContainer); - } - super.didRenderCell(element); } override unrenderCell(element: ICellViewModel): void { - this._controller?.dispose(); - this._controller = undefined; super.unrenderCell(element); } override updateInternalLayoutNow(element: ICellViewModel): void { - this._controller?.layout(); } override dispose() { - this._controller?.dispose(); - this._controller = undefined; super.dispose(); } }