diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 94e0b13a41086..117c8cf734ef6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -119,6 +119,9 @@ export class InlineChatController implements IEditorContribution { private _messages = this._store.add(new Emitter()); + private readonly _onWillStartSession = this._store.add(new Emitter()); + readonly onWillStartSession = this._onWillStartSession.event; + readonly onDidAcceptInput = Event.filter(this._messages.event, m => m === Message.ACCEPT_INPUT, this._store); readonly onDidCancelInput = Event.filter(this._messages.event, m => m === Message.CANCEL_INPUT || m === Message.CANCEL_SESSION, this._store); @@ -220,6 +223,7 @@ export class InlineChatController implements IEditorContribution { if (options.initialSelection) { this._editor.setSelection(options.initialSelection); } + this._onWillStartSession.fire(); this._currentRun = this._nextState(State.CREATE_SESSION, options); await this._currentRun; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index b4fc93c757d86..abeac899d1ae3 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -65,7 +65,7 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); -const _inputEditorOptions: IEditorConstructionOptions = { +export const _inputEditorOptions: IEditorConstructionOptions = { padding: { top: 2, bottom: 2 }, overviewRulerLanes: 0, glyphMargin: false, diff --git a/src/vs/workbench/contrib/notebook/browser/controller/insertCellActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/insertCellActions.ts index 454b565d71eaf..92ecb50915399 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/insertCellActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/insertCellActions.ts @@ -27,7 +27,26 @@ const INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID = 'notebook.cell.insertMarkdownCellA const INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID = 'notebook.cell.insertMarkdownCellBelow'; const INSERT_MARKDOWN_CELL_AT_TOP_COMMAND_ID = 'notebook.cell.insertMarkdownCellAtTop'; -abstract class InsertCellCommand extends NotebookAction { +export function insertNewCell(accessor: ServicesAccessor, context: INotebookActionContext, kind: CellKind, direction: 'above' | 'below', focusEditor: boolean) { + let newCell: CellViewModel | null = null; + if (context.ui) { + context.notebookEditor.focus(); + } + + const languageService = accessor.get(ILanguageService); + if (context.cell) { + const idx = context.notebookEditor.getCellIndex(context.cell); + newCell = insertCell(languageService, context.notebookEditor, idx, kind, direction, undefined, true); + } else { + const focusRange = context.notebookEditor.getFocus(); + const next = Math.max(focusRange.end - 1, 0); + newCell = insertCell(languageService, context.notebookEditor, next, kind, direction, undefined, true); + } + + return newCell; +} + +export abstract class InsertCellCommand extends NotebookAction { constructor( desc: Readonly, private kind: CellKind, @@ -38,20 +57,7 @@ abstract class InsertCellCommand extends NotebookAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { - let newCell: CellViewModel | null = null; - if (context.ui) { - context.notebookEditor.focus(); - } - - const languageService = accessor.get(ILanguageService); - if (context.cell) { - const idx = context.notebookEditor.getCellIndex(context.cell); - newCell = insertCell(languageService, context.notebookEditor, idx, this.kind, this.direction, undefined, true); - } else { - const focusRange = context.notebookEditor.getFocus(); - const next = Math.max(focusRange.end - 1, 0); - newCell = insertCell(languageService, context.notebookEditor, next, this.kind, this.direction, undefined, true); - } + const newCell = await insertNewCell(accessor, context, this.kind, this.direction, this.focusEditor); if (newCell) { await context.notebookEditor.focusNotebookCell(newCell, this.focusEditor ? 'editor' : 'container'); diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css new file mode 100644 index 0000000000000..7af5203684a1f --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .notebookOverlay .cell-chat-part { + display: none; + color: inherit; + padding: 6px; + border-radius: 6px; + border: 1px solid var(--vscode-inlineChat-border); + background: var(--vscode-inlineChat-background); +} +.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container { + padding: 8px 8px 0px 8px; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body { + display: flex; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .content { + display: flex; + box-sizing: border-box; + outline: 1px solid var(--vscode-inlineChatInput-border); + outline-offset: -1px; + border-radius: 2px; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .content.synthetic-focus { + outline: 1px solid var(--vscode-inlineChatInput-focusBorder); +} + +.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .content .input { + display: flex; + align-items: center; + justify-content: space-between; + padding: 2px 2px 2px 6px; + background-color: var(--vscode-inlineChatInput-background); + cursor: text; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .content .input .monaco-editor-background { + background-color: var(--vscode-inlineChatInput-background); +} + +.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .content .input .editor-placeholder { + position: absolute; + z-index: 1; + color: var(--vscode-inlineChatInput-placeholderForeground); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .content .input .editor-placeholder.hidden { + display: none; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .content .input .editor-container { + vertical-align: middle; +} +.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .toolbar { + display: flex; + flex-direction: column; + align-self: stretch; + padding-right: 4px; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + background: var(--vscode-inlineChatInput-background); +} + +.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .toolbar .actions-container { + display: flex; + flex-direction: row; + gap: 4px; +} + +/* progress */ + +.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .progress { + position: relative; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .progress .monaco-progress-container { + top: 0; +} + +/* status */ + +.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .status { + height: 22px; + margin: 4px; +} + + +.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .status span { + overflow: hidden; + color: var(--vscode-descriptionForeground); + font-size: 11px; + align-self: baseline; + display: flex; +} diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookCellInsertToolbar.css b/src/vs/workbench/contrib/notebook/browser/media/notebookCellInsertToolbar.css index ec646f99b88e7..67625757e7ce4 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebookCellInsertToolbar.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookCellInsertToolbar.css @@ -79,9 +79,10 @@ align-items: center; } -.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .action-item:first-child, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .action-item:first-child { - margin-right: 16px; +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .action-item, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .action-item { + margin-left: 8px; + margin-right: 8px; } .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container span.codicon, diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 36f38ac2bc04e..61b361aac83b7 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -171,6 +171,7 @@ export enum CellLayoutState { export interface CodeCellLayoutInfo { readonly fontInfo: FontInfo | null; + readonly chatHeight: number; readonly editorHeight: number; readonly editorWidth: number; readonly estimatedHasHorizontalScrolling: boolean; @@ -189,6 +190,7 @@ export interface CodeCellLayoutInfo { export interface CodeCellLayoutChangeEvent { readonly source?: string; + readonly chatHeight?: boolean; readonly editorHeight?: boolean; readonly commentHeight?: boolean; readonly outputHeight?: boolean; @@ -200,6 +202,7 @@ export interface CodeCellLayoutChangeEvent { export interface MarkupCellLayoutInfo { readonly fontInfo: FontInfo | null; + readonly chatHeight: number; readonly editorWidth: number; readonly editorHeight: number; readonly statusBarHeight: number; @@ -232,7 +235,7 @@ export interface ICellViewModel extends IGenericCellViewModel { readonly model: NotebookCellTextModel; readonly id: string; readonly textBuffer: IReadonlyTextBuffer; - readonly layoutInfo: { totalHeight: number; bottomToolbarOffset: number; editorWidth: number; editorHeight: number; statusBarHeight: number }; + readonly layoutInfo: { totalHeight: number; bottomToolbarOffset: number; editorWidth: number; editorHeight: number; statusBarHeight: number; chatHeight: number }; readonly onDidChangeLayout: Event; readonly onDidChangeCellStatusBarItems: Event; readonly onCellDecorationsChanged: Event<{ added: INotebookCellDecorationOptions[]; removed: INotebookCellDecorationOptions[] }>; @@ -249,6 +252,7 @@ export interface ICellViewModel extends IGenericCellViewModel { readonly mime: string; cellKind: CellKind; lineNumbers: 'on' | 'off' | 'inherit'; + chatHeight: number; focusMode: CellFocusMode; outputIsHovered: boolean; getText(): string; @@ -588,7 +592,7 @@ export interface INotebookEditor { /** * Reveal cell into viewport. */ - revealInView(cell: ICellViewModel): void; + revealInView(cell: ICellViewModel): Promise; /** * Reveal cell into the top of viewport. @@ -603,7 +607,7 @@ export interface INotebookEditor { /** * Reveal cell into viewport center if cell is currently out of the viewport. */ - revealInCenterIfOutsideViewport(cell: ICellViewModel): void; + revealInCenterIfOutsideViewport(cell: ICellViewModel): Promise; /** * Reveal a line in notebook cell into viewport with minimal scrolling. @@ -798,7 +802,8 @@ export enum CellEditState { export enum CellFocusMode { Container, Editor, - Output + Output, + ChatInput } export enum CursorAtBoundary { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 04aea6e26150b..9ff69e99572ac 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/notebook'; +import 'vs/css!./media/notebookCellChat'; import 'vs/css!./media/notebookCellEditorHint'; import 'vs/css!./media/notebookCellInsertToolbar'; import 'vs/css!./media/notebookCellStatusBar'; @@ -859,6 +860,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } `); + // chat + styleSheets.push(` + .monaco-workbench .notebookOverlay .cell-chat-part { + margin: 0 ${cellRightMargin}px 8px 6px; + } + `); + this._styleElement.textContent = styleSheets.join('\n'); } @@ -2065,7 +2073,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } revealInView(cell: ICellViewModel) { - this._list.revealCell(cell, CellRevealType.Default); + return this._list.revealCell(cell, CellRevealType.Default); } revealInViewAtTop(cell: ICellViewModel) { @@ -2076,8 +2084,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._list.revealCell(cell, CellRevealType.Center); } - revealInCenterIfOutsideViewport(cell: ICellViewModel) { - this._list.revealCell(cell, CellRevealType.CenterIfOutsideViewport); + async revealInCenterIfOutsideViewport(cell: ICellViewModel) { + await this._list.revealCell(cell, CellRevealType.CenterIfOutsideViewport); } revealFirstLineIfOutsideViewport(cell: ICellViewModel) { @@ -2346,7 +2354,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD const firstSelectionPosition = selectionsStartPosition[0]; await this.revealRangeInViewAsync(cell, Range.fromPositions(firstSelectionPosition, firstSelectionPosition)); } else { - this.revealInView(cell); + await this.revealInView(cell); } } @@ -2384,13 +2392,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD if (!options?.skipReveal) { if (typeof options?.focusEditorLine === 'number') { this._cursorNavMode.set(true); - this.revealInView(cell); + await this.revealInView(cell); } else if (options?.revealBehavior === ScrollToRevealBehavior.firstLine) { this.revealFirstLineIfOutsideViewport(cell); } else if (options?.revealBehavior === ScrollToRevealBehavior.fullCell) { - this.revealInView(cell); + await this.revealInView(cell); } else { - this.revealInCenterIfOutsideViewport(cell); + await this.revealInCenterIfOutsideViewport(cell); } } this._list.focusView(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts index e62b28ee4dc78..385d2cbaa3987 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts @@ -78,7 +78,7 @@ export class CellExecutionPart extends CellContentPart { DOM.hide(this._executionOrderLabel); } else { DOM.show(this._executionOrderLabel); - const top = element.layoutInfo.editorHeight - 22 + element.layoutInfo.statusBarHeight; + const top = element.layoutInfo.editorHeight - 22 + element.layoutInfo.statusBarHeight + element.layoutInfo.chatHeight; this._executionOrderLabel.style.top = `${top}px`; } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts index 51b7ba06f55c1..95333b95f4ca0 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts @@ -80,7 +80,8 @@ export class CellFocusIndicator extends CellContentPart { this.bottom.domNode.style.transform = `translateY(${indicatorPostion.bottomIndicatorTop}px)`; this.left.setHeight(indicatorPostion.verticalIndicatorHeight); this.right.setHeight(indicatorPostion.verticalIndicatorHeight); - this.codeFocusIndicator.setHeight(indicatorPostion.verticalIndicatorHeight - this.getIndicatorTopMargin() * 2); + this.codeFocusIndicator.setTop(element.layoutInfo.chatHeight); + this.codeFocusIndicator.setHeight(indicatorPostion.verticalIndicatorHeight - this.getIndicatorTopMargin() * 2 - element.layoutInfo.chatHeight); } else { const cell = element as CodeCellViewModel; const layoutInfo = this.notebookEditor.notebookOptions.getLayoutConfiguration(); @@ -88,7 +89,9 @@ export class CellFocusIndicator extends CellContentPart { const indicatorHeight = cell.layoutInfo.codeIndicatorHeight + cell.layoutInfo.outputIndicatorHeight + cell.layoutInfo.commentHeight; this.left.setHeight(indicatorHeight); this.right.setHeight(indicatorHeight); + this.codeFocusIndicator.setTop(cell.layoutInfo.chatHeight); this.codeFocusIndicator.setHeight(cell.layoutInfo.codeIndicatorHeight); + this.outputFocusIndicator.setTop(cell.layoutInfo.chatHeight); this.outputFocusIndicator.setHeight(Math.max(cell.layoutInfo.outputIndicatorHeight - cell.viewContext.notebookOptions.getLayoutConfiguration().focusIndicatorGap, 0)); this.bottom.domNode.style.transform = `translateY(${cell.layoutInfo.totalHeight - bottomToolbarDimensions.bottomToolbarGap - layoutInfo.cellBottomMargin}px)`; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts index 9c73faa2c6790..2988a3d47bc29 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts @@ -136,7 +136,9 @@ export class CellEditorStatusBar extends CellContentPart { element.focusMode = CellFocusMode.Editor; } else { const currentMode = element.focusMode; - if (currentMode === CellFocusMode.Output && this._notebookEditor.hasWebviewFocus()) { + if (currentMode === CellFocusMode.ChatInput) { + element.focusMode = CellFocusMode.ChatInput; + } else if (currentMode === CellFocusMode.Output && this._notebookEditor.hasWebviewFocus()) { element.focusMode = CellFocusMode.Output; } else { element.focusMode = CellFocusMode.Container; diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts new file mode 100644 index 0000000000000..6f15e73d31778 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts @@ -0,0 +1,501 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancelablePromise, Queue, createCancelablePromise, raceCancellationError } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Codicon } from 'vs/base/common/codicons'; +import { Event } from 'vs/base/common/event'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { MovingAverage } from 'vs/base/common/numbers'; +import { StopWatch } from 'vs/base/common/stopwatch'; +import { assertType } from 'vs/base/common/types'; +import { generateUuid } from 'vs/base/common/uuid'; +import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +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 { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { TextEdit } from 'vs/editor/common/languages'; +import { ICursorStateComputer } from 'vs/editor/common/model'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; +import { localize } from 'vs/nls'; +import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { AsyncProgress } from 'vs/platform/progress/common/progress'; +import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; +import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; +import { IInlineChatSessionService, ReplyResponse, Session, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { ProgressingEditsOptions, asProgressiveEdit, performAsyncTextEdit } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; +import { CTX_INLINE_CHAT_HAS_PROVIDER, EditMode, IInlineChatProgressItem, IInlineChatRequest } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CELL_TITLE_CELL_GROUP_ID, INotebookCellActionContext, NotebookCellAction } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { insertNewCell } from 'vs/workbench/contrib/notebook/browser/controller/insertCellActions'; +import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CellChatWidget, MENU_NOTEBOOK_CELL_CHAT_WIDGET } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatWidget'; +import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NOTEBOOK_EDITOR_EDITABLE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; + +const CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST = new RawContextKey('notebookChatHasActiveRequest', false, localize('notebookChatHasActiveRequest', "Whether the cell chat editor has an active request")); + +interface ICellChatPart { + activeCell: ICellViewModel | undefined; + getWidget(): CellChatWidget; +} + +export class NotebookCellChatController extends Disposable { + private static _cellChatControllers = new WeakMap(); + + static get(cell: ICellViewModel): NotebookCellChatController | undefined { + return NotebookCellChatController._cellChatControllers.get(cell); + } + + private _sessionCtor: CancelablePromise | undefined; + private _activeSession?: Session; + private readonly _ctxHasActiveRequest: IContextKey; + private _isVisible: boolean = false; + private _strategy: EditStrategy = new EditStrategy(); + + private _inlineChatListener: IDisposable | undefined; + + constructor( + private readonly _notebookEditor: INotebookEditorDelegate, + private readonly _chatPart: ICellChatPart, + private readonly _cell: ICellViewModel, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, + @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, + ) { + super(); + + NotebookCellChatController._cellChatControllers.set(this._cell, this); + this._ctxHasActiveRequest = CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST.bindTo(this._contextKeyService); + + this._register(this._cell.onDidChangeEditorAttachState(() => { + const editor = this._getCellEditor(); + this._inlineChatListener?.dispose(); + + if (!editor) { + return; + } + + const inlineChatController = InlineChatController.get(editor); + if (inlineChatController) { + this._inlineChatListener = inlineChatController.onWillStartSession(() => { + this.dismiss(); + }); + } + })); + } + + public override dispose(): void { + if (this._isVisible) { + // detach the chat widget + this._chatPart.getWidget().hide(); + this._sessionCtor?.cancel(); + this._sessionCtor = undefined; + } + + this._inlineChatListener?.dispose(); + this._inlineChatListener = undefined; + this._ctxHasActiveRequest.reset(); + NotebookCellChatController._cellChatControllers.delete(this._cell); + super.dispose(); + } + + layout() { + if (this._isVisible) { + this._chatPart.getWidget().layout(); + } + } + + async show() { + this._isVisible = true; + this._chatPart.getWidget().show(this._cell); + this._sessionCtor = createCancelablePromise(async token => { + if (this._cell.editorAttached) { + const editor = this._getCellEditor(); + if (editor) { + await this._startSession(editor, token); + } + } else { + await Event.toPromise(Event.once(this._cell.onDidChangeEditorAttachState)); + if (token.isCancellationRequested) { + return; + } + + const editor = this._getCellEditor(); + if (editor) { + await this._startSession(editor, token); + } + } + + this._chatPart.getWidget().placeholder = this._activeSession?.session.placeholder ?? localize('default.placeholder', "Ask a question"); + }); + } + + private _getCellEditor() { + const editors = this._notebookEditor.codeEditors.find(editor => editor[0] === this._chatPart.activeCell); + if (!editors || !editors[1].hasModel()) { + return; + } + + const editor = editors[1]; + return editor; + } + + private async _startSession(editor: IActiveCodeEditor, token: CancellationToken) { + if (this._activeSession) { + this._inlineChatSessionService.releaseSession(this._activeSession); + } + + + + const session = await this._inlineChatSessionService.createSession( + editor, + { editMode: EditMode.LivePreview }, + token + ); + + this._activeSession = session; + } + + async acceptInput() { + assertType(this._activeSession); + this._activeSession.addInput(new SessionPrompt(this._getInput())); + + assertType(this._activeSession.lastInput); + + const value = this._activeSession.lastInput.value; + const editors = this._notebookEditor.codeEditors.find(editor => editor[0] === this._chatPart.activeCell); + if (!editors || !editors[1].hasModel()) { + return; + } + + const editor = editors[1]; + + this._ctxHasActiveRequest.set(true); + this._chatPart.getWidget().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 + }; + + 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.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); + 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) { + this._ctxHasActiveRequest.set(false); + this._chatPart.getWidget().updateProgress(false); + return; + } + + const markdownContents = new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false }); + const replyResponse = new ReplyResponse(reply, markdownContents, this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), progressEdits); + for (let i = progressEdits.length; i < replyResponse.allLocalEdits.length; i++) { + await this._makeChanges(editor, replyResponse.allLocalEdits[i], undefined); + } + this._ctxHasActiveRequest.set(false); + this._chatPart.getWidget().updateProgress(false); + } + + async cancelCurrentRequest() { + if (this._activeSession) { + this._inlineChatSessionService.releaseSession(this._activeSession); + } + + this._activeSession = undefined; + } + + async dismiss() { + this._isVisible = false; + this.cancelCurrentRequest(); + this._chatPart.getWidget().hide(); + } + + private _getInput() { + return this._chatPart.getWidget().getInput(); + } + + private async _makeChanges(editor: IActiveCodeEditor, edits: TextEdit[], opts: ProgressingEditsOptions | undefined) { + assertType(this._activeSession); + + const moreMinimalEdits = await this._editorWorkerService.computeMoreMinimalEdits(this._activeSession.textModelN.uri, edits); + // this._log('edits from PROVIDER and after making them MORE MINIMAL', this._activeSession.provider.debugName, edits, moreMinimalEdits); + + if (moreMinimalEdits?.length === 0) { + // nothing left to do + return; + } + + const actualEdits = !opts && moreMinimalEdits ? moreMinimalEdits : edits; + const editOperations = actualEdits.map(TextEdit.asEditOperation); + + try { + // this._ignoreModelContentChanged = true; + this._activeSession.wholeRange.trackEdits(editOperations); + if (opts) { + await this._strategy.makeProgressiveChanges(editor, editOperations, opts); + } else { + await this._strategy.makeChanges(editor, editOperations); + } + // this._ctxDidEdit.set(this._activeSession.hasChangedText); + } finally { + // this._ignoreModelContentChanged = false; + } + } +} + +class EditStrategy { + private _editCount: number = 0; + + async makeProgressiveChanges(editor: IActiveCodeEditor, edits: ISingleEditOperation[], opts: ProgressingEditsOptions): Promise { + // push undo stop before first edit + if (++this._editCount === 1) { + editor.pushUndoStop(); + } + + const durationInSec = opts.duration / 1000; + for (const edit of edits) { + const wordCount = countWords(edit.text ?? ''); + const speed = wordCount / durationInSec; + // console.log({ durationInSec, wordCount, speed: wordCount / durationInSec }); + await performAsyncTextEdit(editor.getModel(), asProgressiveEdit(edit, speed, opts.token)); + } + } + + async makeChanges(editor: IActiveCodeEditor, edits: ISingleEditOperation[]): Promise { + const cursorStateComputerAndInlineDiffCollection: ICursorStateComputer = (undoEdits) => { + let last: Position | null = null; + for (const edit of undoEdits) { + last = !last || last.isBefore(edit.range.getEndPosition()) ? edit.range.getEndPosition() : last; + // this._inlineDiffDecorations.collectEditOperation(edit); + } + return last && [Selection.fromPositions(last)]; + }; + + // push undo stop before first edit + if (++this._editCount === 1) { + editor.pushUndoStop(); + } + editor.executeEdits('inline-chat-live', edits, cursorStateComputerAndInlineDiffCollection); + } +} + +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: 'notebook.cell.chat.start', + title: { + value: localize('notebook.cell.chat.start', "Start Chat"), + original: 'Start Chat' + }, + icon: Codicon.sparkle, + menu: { + id: MenuId.NotebookCellTitle, + group: CELL_TITLE_CELL_GROUP_ID, + order: 0, + when: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_PROVIDER, EditorContextKeys.writable, ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true)) + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + const ctrl = NotebookCellChatController.get(context.cell); + if (!ctrl) { + return; + } + + await ctrl.show(); + } +}); + +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: 'notebook.cell.chat.accept', + title: { + value: localize('notebook.cell.chat.accept', "Make Request"), + original: 'Make Request' + }, + icon: Codicon.send, + keybinding: { + when: CTX_NOTEBOOK_CELL_CHAT_FOCUSED, + weight: KeybindingWeight.EditorCore + 7, + primary: KeyCode.Enter + }, + menu: { + id: MENU_NOTEBOOK_CELL_CHAT_WIDGET, + group: 'main', + order: 1, + when: CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST.negate() + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + const ctrl = NotebookCellChatController.get(context.cell); + if (!ctrl) { + return; + } + + ctrl.acceptInput(); + } +}); + +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: 'notebook.cell.chat.stop', + title: { + value: localize('notebook.cell.chat.stop', "Stop Request"), + original: 'Make Request' + }, + icon: Codicon.debugStop, + menu: { + id: MENU_NOTEBOOK_CELL_CHAT_WIDGET, + group: 'main', + order: 1, + when: CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + const ctrl = NotebookCellChatController.get(context.cell); + if (!ctrl) { + return; + } + + ctrl.cancelCurrentRequest(); + } +}); + +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: 'notebook.cell.chat.close', + title: { + value: localize('notebook.cell.chat.close', "Close Chat"), + original: 'Close Chat' + }, + icon: Codicon.close, + menu: { + id: MENU_NOTEBOOK_CELL_CHAT_WIDGET, + group: 'main', + order: 2 + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + const ctrl = NotebookCellChatController.get(context.cell); + if (!ctrl) { + return; + } + + ctrl.dismiss(); + } +}); + +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: 'notebook.cell.insertCodeCellWithChat', + title: { + value: '$(sparkle) ' + localize('notebookActions.menu.insertCodeCellWithChat', "Generate"), + original: '$(sparkle) Generate', + }, + tooltip: localize('notebookActions.menu.insertCodeCellWithChat.tooltip', "Generate Code Cell with Chat"), + menu: [ + { + id: MenuId.NotebookCellBetween, + group: 'inline', + order: -1, + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + CTX_INLINE_CHAT_HAS_PROVIDER, + ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true) + ) + }, + { + id: MenuId.NotebookCellListTop, + group: 'inline', + order: -1, + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + CTX_INLINE_CHAT_HAS_PROVIDER, + ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true) + ) + }, + ] + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + const newCell = await insertNewCell(accessor, context, CellKind.Code, 'below', true); + + if (!newCell) { + return; + } + await context.notebookEditor.focusNotebookCell(newCell, 'container'); + const ctrl = NotebookCellChatController.get(newCell); + if (!ctrl) { + return; + } + ctrl.show(); + } +}); 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 new file mode 100644 index 0000000000000..a1bd2d808b04a --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatPart.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Lazy } from 'vs/base/common/lazy'; +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 { CellChatWidget } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatWidget'; + +export class CellChatPart extends CellContentPart { + private _controller: NotebookCellChatController | undefined; + + get activeCell() { + return this.currentCell; + } + + private _widget: Lazy; + + constructor( + private readonly _notebookEditor: INotebookEditorDelegate, + partContainer: HTMLElement, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + + this._widget = new Lazy(() => this._instantiationService.createInstance(CellChatWidget, this._notebookEditor, partContainer)); + } + + getWidget() { + return this._widget.value; + } + + override didRenderCell(element: ICellViewModel): void { + this._controller?.dispose(); + this._controller = this._instantiationService.createInstance(NotebookCellChatController, this._notebookEditor, this, element); + + 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(); + } +} + diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatWidget.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatWidget.ts new file mode 100644 index 0000000000000..b0bf8c662d8d7 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatWidget.ts @@ -0,0 +1,174 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $, Dimension, addDisposableListener, append, getTotalWidth, h } from 'vs/base/browser/dom'; +import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { URI } from 'vs/base/common/uri'; +import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { ITextModel } from 'vs/editor/common/model'; +import { IModelService } from 'vs/editor/common/services/model'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; +import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; +import { localize } from 'vs/nls'; +import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { _inputEditorOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { CellFocusMode, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; + +export const CTX_NOTEBOOK_CELL_CHAT_FOCUSED = new RawContextKey('notebookCellChatFocused', false, localize('notebookCellChatFocused', "Whether the cell chat editor is focused")); +export const MENU_NOTEBOOK_CELL_CHAT_WIDGET = MenuId.for('notebookCellChatWidget'); + +export class CellChatWidget extends Disposable { + private static _modelPool: number = 1; + + private readonly _elements = h( + 'div.cell-chat-container@root', + [ + h('div.body', [ + h('div.content@content', [ + h('div.input@input', [ + h('div.editor-placeholder@placeholder'), + h('div.editor-container@editor'), + ]), + h('div.toolbar@editorToolbar'), + ]), + ]), + h('div.progress@progress'), + h('div.status@status') + ] + ); + private readonly _progressBar: ProgressBar; + private readonly _toolbar: MenuWorkbenchToolBar; + + private readonly _inputEditor: IActiveCodeEditor; + private readonly _inputModel: ITextModel; + private readonly _ctxInputEditorFocused: IContextKey; + + private _activeCell: ICellViewModel | undefined; + + set placeholder(value: string) { + this._elements.placeholder.innerText = value; + } + + + constructor( + private readonly _notebookEditor: INotebookEditorDelegate, + private readonly _partContainer: HTMLElement, + @IModelService private readonly _modelService: IModelService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService + ) { + super(); + append(_partContainer, this._elements.root); + this._elements.input.style.height = '24px'; + + const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { + isSimpleWidget: true, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + SnippetController2.ID, + SuggestController.ID + ]) + }; + + this._inputEditor = this._instantiationService.createInstance(CodeEditorWidget, this._elements.editor, { + ..._inputEditorOptions, + ariaLabel: localize('cell-chat-aria-label', "Cell Chat Input"), + }, codeEditorWidgetOptions); + this._register(this._inputEditor); + const uri = URI.from({ scheme: 'vscode', authority: 'inline-chat', path: `/notebook-cell-chat/model${CellChatWidget._modelPool++}.txt` }); + this._inputModel = this._register(this._modelService.getModel(uri) ?? this._modelService.createModel('', null, uri)); + this._inputEditor.setModel(this._inputModel); + + // placeholder + this._elements.placeholder.style.fontSize = `${this._inputEditor.getOption(EditorOption.fontSize)}px`; + this._elements.placeholder.style.lineHeight = `${this._inputEditor.getOption(EditorOption.lineHeight)}px`; + this._register(addDisposableListener(this._elements.placeholder, 'click', () => this._inputEditor.focus())); + + const togglePlaceholder = () => { + const hasText = this._inputModel.getValueLength() > 0; + this._elements.placeholder.classList.toggle('hidden', hasText); + }; + this._store.add(this._inputModel.onDidChangeContent(togglePlaceholder)); + togglePlaceholder(); + + // toolbar + this._toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, MENU_NOTEBOOK_CELL_CHAT_WIDGET, { + telemetrySource: 'interactiveEditorWidget-toolbar', + toolbarOptions: { primaryGroup: 'main' } + })); + + // Create chat response div + const copilotGeneratedCodeSpan = $('span.copilot-generated-code', {}, 'Copilot generated code may be incorrect'); + this._elements.status.appendChild(copilotGeneratedCodeSpan); + + this._register(this._inputEditor.onDidFocusEditorWidget(() => { + if (this._activeCell) { + this._activeCell.focusMode = CellFocusMode.ChatInput; + } + })); + + this._ctxInputEditorFocused = CTX_NOTEBOOK_CELL_CHAT_FOCUSED.bindTo(this._contextKeyService); + this._register(this._inputEditor.onDidFocusEditorWidget(() => { + this._ctxInputEditorFocused.set(true); + })); + this._register(this._inputEditor.onDidBlurEditorWidget(() => { + this._ctxInputEditorFocused.set(false); + })); + + this._progressBar = new ProgressBar(this._elements.progress); + this._register(this._progressBar); + } + + show(element: ICellViewModel) { + this._partContainer.style.display = 'block'; + + this._activeCell = element; + + this._toolbar.context = { + ui: true, + cell: element, + notebookEditor: this._notebookEditor, + $mid: MarshalledId.NotebookCellActionContext + }; + + this.layout(); + this._inputEditor.focus(); + this._activeCell.chatHeight = 82 + 8 /* bottom margin*/; + } + + hide() { + this._partContainer.style.display = 'none'; + if (this._activeCell) { + this._activeCell.chatHeight = 0; + } + } + + getInput() { + return this._inputEditor.getValue(); + } + + updateProgress(show: boolean) { + if (show) { + this._progressBar.infinite(); + } else { + this._progressBar.stop(); + } + } + + layout() { + if (this._activeCell) { + const innerEditorWidth = this._activeCell.layoutInfo.editorWidth - (getTotalWidth(this._elements.editorToolbar) + 8 /* L/R-padding */); + this._inputEditor.layout(new Dimension(innerEditorWidth, this._inputEditor.getContentHeight())); + } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts index 23fc30b123914..29923c35ec26f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts @@ -66,7 +66,7 @@ export interface INotebookCellList { getFocusedElements(): ICellViewModel[]; getSelectedElements(): ICellViewModel[]; scrollToBottom(): void; - revealCell(cell: ICellViewModel, revealType: CellRevealType): void; + revealCell(cell: ICellViewModel, revealType: CellRevealType): Promise; revealCells(range: ICellRange): void; revealRangeInCell(cell: ICellViewModel, range: Selection | Range, revealType: CellRevealRangeType): Promise; revealCellOffsetInCenter(element: ICellViewModel, offset: number): void; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index f6f8aa9245532..cc0be19c3f051 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -25,6 +25,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellPartsCollection } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; +import { CellChatPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatPart'; import { CellComments } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellComments'; import { CellContextKeyPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys'; import { CellDecorations } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellDecorations'; @@ -141,6 +142,7 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen const codeInnerContent = DOM.append(container, $('.cell.code')); const editorPart = DOM.append(codeInnerContent, $('.cell-editor-part')); + const cellChatPart = DOM.append(editorPart, $('.cell-chat-part')); const cellInputCollapsedContainer = DOM.append(codeInnerContent, $('.input-collapse-container')); cellInputCollapsedContainer.style.display = 'none'; const editorContainer = DOM.append(editorPart, $('.cell-editor-container')); @@ -163,6 +165,7 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen const focusIndicatorBottom = new FastDomNode(DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-bottom'))); const cellParts = new CellPartsCollection(DOM.getWindow(rootContainer), [ + templateDisposables.add(scopedInstaService.createInstance(CellChatPart, this.notebookEditor, cellChatPart)), templateDisposables.add(scopedInstaService.createInstance(CellEditorStatusBar, this.notebookEditor, container, editorPart, undefined)), templateDisposables.add(new CellFocusIndicator(this.notebookEditor, titleToolbar, focusIndicatorTop, focusIndicatorLeft, focusIndicatorRight, focusIndicatorBottom)), templateDisposables.add(new FoldedCellHint(this.notebookEditor, DOM.append(container, $('.notebook-folded-hint')))), @@ -257,6 +260,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende // This is also the drag handle const focusIndicatorLeft = new FastDomNode(DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left'))); + const cellChatPart = DOM.append(container, $('.cell-chat-part')); const cellContainer = DOM.append(container, $('.cell.code')); const runButtonContainer = DOM.append(cellContainer, $('.run-button-container')); const cellInputCollapsedContainer = DOM.append(cellContainer, $('.input-collapse-container')); @@ -308,6 +312,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const focusIndicatorPart = templateDisposables.add(new CellFocusIndicator(this.notebookEditor, titleToolbar, focusIndicatorTop, focusIndicatorLeft, focusIndicatorRight, focusIndicatorBottom)); const cellParts = new CellPartsCollection(DOM.getWindow(rootContainer), [ focusIndicatorPart, + templateDisposables.add(scopedInstaService.createInstance(CellChatPart, this.notebookEditor, cellChatPart)), templateDisposables.add(scopedInstaService.createInstance(CellEditorStatusBar, this.notebookEditor, container, editorPart, editor)), templateDisposables.add(scopedInstaService.createInstance(CellProgressBar, editorPart, cellInputCollapsedContainer)), templateDisposables.add(scopedInstaService.createInstance(RunToolbar, this.notebookEditor, contextKeyService, container, runButtonContainer)), diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index c8f720ab04502..dc9d1dd3bb057 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -64,6 +64,20 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod throw new Error('editorHeight is write-only'); } + private _chatHeight = 0; + set chatHeight(height: number) { + if (this._chatHeight === height) { + return; + } + + this._chatHeight = height; + this.layoutChange({ chatHeight: true }, 'CodeCellViewModel#chatHeight'); + } + + get chatHeight() { + return this._chatHeight; + } + private _commentHeight = 0; set commentHeight(height: number) { @@ -163,13 +177,14 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod editorWidth: initialNotebookLayoutInfo ? this.viewContext.notebookOptions.computeCodeCellEditorWidth(initialNotebookLayoutInfo.width) : 0, + chatHeight: 0, statusBarHeight: 0, commentHeight: 0, outputContainerOffset: 0, outputTotalHeight: 0, outputShowMoreContainerHeight: 0, outputShowMoreContainerOffset: 0, - totalHeight: this.computeTotalHeight(17, 0, 0), + totalHeight: this.computeTotalHeight(17, 0, 0, 0), codeIndicatorHeight: 0, outputIndicatorHeight: 0, bottomToolbarOffset: 0, @@ -215,6 +230,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod let editorHeight: number; let totalHeight: number; let hasHorizontalScrolling = false; + const chatHeight = state.chatHeight ? this._chatHeight : this._layoutInfo.chatHeight; if (!state.editorHeight && this._layoutInfo.layoutState === CellLayoutState.FromCache && !state.outputHeight) { // No new editorHeight info - keep cached totalHeight and estimate editorHeight const estimate = this.estimateEditorHeight(state.font?.lineHeight ?? this._layoutInfo.fontInfo?.lineHeight); @@ -225,14 +241,14 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod } else if (state.editorHeight || this._layoutInfo.layoutState === CellLayoutState.Measured) { // Editor has been measured editorHeight = this._editorHeight; - totalHeight = this.computeTotalHeight(this._editorHeight, outputTotalHeight, outputShowMoreContainerHeight); + totalHeight = this.computeTotalHeight(this._editorHeight, outputTotalHeight, outputShowMoreContainerHeight, chatHeight); newState = CellLayoutState.Measured; hasHorizontalScrolling = this._layoutInfo.estimatedHasHorizontalScrolling; } else { const estimate = this.estimateEditorHeight(state.font?.lineHeight ?? this._layoutInfo.fontInfo?.lineHeight); editorHeight = estimate.editorHeight; hasHorizontalScrolling = estimate.hasHorizontalScrolling; - totalHeight = this.computeTotalHeight(editorHeight, outputTotalHeight, outputShowMoreContainerHeight); + totalHeight = this.computeTotalHeight(editorHeight, outputTotalHeight, outputShowMoreContainerHeight, chatHeight); newState = CellLayoutState.Estimated; } @@ -241,6 +257,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod const outputIndicatorHeight = outputTotalHeight + outputShowMoreContainerHeight; const outputContainerOffset = notebookLayoutConfiguration.editorToolbarHeight + notebookLayoutConfiguration.cellTopMargin // CELL_TOP_MARGIN + + chatHeight + editorHeight + statusBarHeight; const outputShowMoreContainerOffset = totalHeight @@ -254,6 +271,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod this._layoutInfo = { fontInfo: state.font ?? this._layoutInfo.fontInfo ?? null, + chatHeight, editorHeight, editorWidth, statusBarHeight, @@ -294,6 +312,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod fontInfo: state.font ?? this._layoutInfo.fontInfo ?? null, editorHeight: this._layoutInfo.editorHeight, editorWidth, + chatHeight: 0, statusBarHeight: 0, commentHeight, outputContainerOffset, @@ -325,6 +344,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod if (totalHeight !== undefined && this._layoutInfo.layoutState !== CellLayoutState.Measured) { this._layoutInfo = { fontInfo: this._layoutInfo.fontInfo, + chatHeight: this._layoutInfo.chatHeight, editorHeight: this._layoutInfo.editorHeight, editorWidth: this._layoutInfo.editorWidth, statusBarHeight: this.layoutInfo.statusBarHeight, @@ -351,7 +371,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod getHeight(lineHeight: number) { if (this._layoutInfo.layoutState === CellLayoutState.Uninitialized) { const estimate = this.estimateEditorHeight(lineHeight); - return this.computeTotalHeight(estimate.editorHeight, 0, 0); + return this.computeTotalHeight(estimate.editorHeight, 0, 0, 0); } else { return this._layoutInfo.totalHeight; } @@ -383,11 +403,12 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod }; } - private computeTotalHeight(editorHeight: number, outputsTotalHeight: number, outputShowMoreContainerHeight: number): number { + private computeTotalHeight(editorHeight: number, outputsTotalHeight: number, outputShowMoreContainerHeight: number, chatHeight: number): number { const layoutConfiguration = this.viewContext.notebookOptions.getLayoutConfiguration(); const { bottomToolbarGap } = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType); return layoutConfiguration.editorToolbarHeight + layoutConfiguration.cellTopMargin + + chatHeight + editorHeight + this.viewContext.notebookOptions.computeEditorStatusbarHeight(this.internalMetadata, this.uri) + this._commentHeight diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts index afbdce70ff834..41fbef9a00797 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts @@ -45,6 +45,17 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM this._updateTotalHeight(this._computeTotalHeight()); } + private _chatHeight = 0; + + set chatHeight(newHeight: number) { + this._chatHeight = newHeight; + this._updateTotalHeight(this._computeTotalHeight()); + } + + get chatHeight() { + return this._chatHeight; + } + private _editorHeight = 0; private _statusBarHeight = 0; set editorHeight(newHeight: number) { @@ -108,6 +119,7 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM const { bottomToolbarGap } = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType); this._layoutInfo = { + chatHeight: 0, editorHeight: 0, previewHeight: 0, fontInfo: initialNotebookLayoutInfo?.fontInfo || null, @@ -199,6 +211,7 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM fontInfo: state.font || this._layoutInfo.fontInfo, editorWidth, previewHeight, + chatHeight: this._chatHeight, editorHeight: this._editorHeight, statusBarHeight: this._statusBarHeight, bottomToolbarOffset: this.viewContext.notebookOptions.computeBottomToolbarOffset(totalHeight, this.viewType), @@ -217,6 +230,7 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM this._layoutInfo = { fontInfo: state.font || this._layoutInfo.fontInfo, editorWidth, + chatHeight: this._chatHeight, editorHeight: this._editorHeight, statusBarHeight: this._statusBarHeight, previewHeight: this._previewHeight, @@ -240,6 +254,7 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM previewHeight: this._layoutInfo.previewHeight, bottomToolbarOffset: this._layoutInfo.bottomToolbarOffset, totalHeight: totalHeight, + chatHeight: this._chatHeight, editorHeight: this._editorHeight, statusBarHeight: this._statusBarHeight, layoutState: CellLayoutState.FromCache, diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts index 257ead51887ef..3e42b3fd7b237 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts @@ -1045,7 +1045,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } } -export type CellViewModel = CodeCellViewModel | MarkupCellViewModel; +export type CellViewModel = (CodeCellViewModel | MarkupCellViewModel) & ICellViewModel; export function createCellViewModel(instantiationService: IInstantiationService, notebookViewModel: NotebookViewModel, cell: NotebookCellTextModel, viewContext: ViewContext) { if (cell.cellKind === CellKind.Code) { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 1303f62ed1d9c..99230c87970c2 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -966,7 +966,8 @@ export const NotebookSetting = { remoteSaving: 'notebook.experimental.remoteSave', gotoSymbolsAllSymbols: 'notebook.gotoSymbols.showAllSymbols', scrollToRevealCell: 'notebook.scrolling.revealNextCellOnExecute', - anchorToFocusedCell: 'notebook.scrolling.experimental.anchorToFocusedCell' + anchorToFocusedCell: 'notebook.scrolling.experimental.anchorToFocusedCell', + cellChat: 'notebook.experimental.cellChat' } as const; export const enum CellStatusbarAlignment {