From ecd4f3806f012078b7a5e630ee32372124efe46a Mon Sep 17 00:00:00 2001 From: rebornix Date: Mon, 13 Nov 2023 19:35:17 -0800 Subject: [PATCH] On cell chat --- .../inlineChat/browser/inlineChatWidget.ts | 2 +- .../browser/media/notebookCellChat.css | 94 +++ .../notebook/browser/notebookBrowser.ts | 7 +- .../notebook/browser/notebookEditorWidget.ts | 1 + .../browser/view/cellParts/cellChatWidget.ts | 605 ++++++++++++++++++ .../browser/view/cellParts/cellStatusPart.ts | 4 +- .../browser/view/cellParts/codeCell.ts | 2 +- .../browser/view/renderers/cellRenderer.ts | 5 + .../browser/viewModel/codeCellViewModel.ts | 33 +- .../browser/viewModel/markupCellViewModel.ts | 15 + .../viewModel/notebookViewModelImpl.ts | 2 +- .../contrib/notebook/common/notebookCommon.ts | 3 +- 12 files changed, 761 insertions(+), 12 deletions(-) create mode 100644 src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css create mode 100644 src/vs/workbench/contrib/notebook/browser/view/cellParts/cellChatWidget.ts 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/media/notebookCellChat.css b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css new file mode 100644 index 0000000000000..4ce82533e29e0 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * 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 .cell-chat-container { + padding: 8px 12px 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/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 36f38ac2bc04e..6aa17c1bf83f3 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; @@ -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; @@ -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 e7a948878f35d..1f654d7080226 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'; diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellChatWidget.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellChatWidget.ts new file mode 100644 index 0000000000000..b4d347924dce4 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellChatWidget.ts @@ -0,0 +1,605 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Queue } from 'vs/base/common/async'; +import { 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 { Lazy } from 'vs/base/common/lazy'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { MarshalledId } from 'vs/base/common/marshallingIds'; +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 { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +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, ITextModel } from 'vs/editor/common/model'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; +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, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService, 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 { IInlineChatSessionService, ReplyResponse, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { ProgressingEditsOptions, asProgressiveEdit, performAsyncTextEdit } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; +import { _inputEditorOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +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 { CellFocusMode, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellContentPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; +import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +const CTX_NOTEBOOK_CELL_CHAT_FOCUSED = new RawContextKey('notebookCellChatFocused', false, localize('notebookCellChatFocused', "Whether the cell chat editor is focused")); +const CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST = new RawContextKey('notebookChatHasActiveRequest', false, localize('notebookChatHasActiveRequest', "Whether the cell chat editor has an active request")); +export const MENU_NOTEBOOK_CELL_CHAT_WIDGET = MenuId.for('notebookCellChatWidget'); + +export class CellChatPart extends CellContentPart { + 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 _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._elements.root.style.width = `${element.layoutInfo.editorWidth}px`; + this._controller?.layout(); + } + + override dispose() { + super.dispose(); + } +} + +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, + _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._elements.root.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 = 62; + } + + hide() { + this._elements.root.style.display = 'none'; + if (this._activeCell) { + this._activeCell.chatHeight = 0; + } + } + + 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())); + } + } +} + +class NotebookCellChatController extends Disposable { + private static _cellChatControllers = new WeakMap(); + + static get(cell: ICellViewModel): NotebookCellChatController | undefined { + return NotebookCellChatController._cellChatControllers.get(cell); + } + + private _activeSession?: Session; + private readonly _ctxHasActiveRequest: IContextKey; + private _isVisible: boolean = false; + + constructor( + private readonly _notebookEditor: INotebookEditorDelegate, + private readonly _chatPart: CellChatPart, + 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); + } + + public override dispose(): void { + this._ctxHasActiveRequest.reset(); + NotebookCellChatController._cellChatControllers.delete(this._cell); + super.dispose(); + } + + layout() { + if (this._isVisible) { + this._chatPart.getWidget().layout(); + } + } + + async startSession() { + this._isVisible = true; + if (this._activeSession) { + this._inlineChatSessionService.releaseSession(this._activeSession); + } + + const editors = this._notebookEditor.codeEditors.find(editor => editor[0] === this._chatPart.activeCell); + if (!editors || !editors[1].hasModel()) { + return; + } + + this._chatPart.getWidget().show(this._cell); + this._activeSession = await this._createSession(editors[1]); + this._chatPart.getWidget().placeholder = this._activeSession?.session.placeholder ?? localize('default.placeholder', "Ask a question"); + } + + async acceptInput() { + assertType(this._activeSession); + 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]; + + if (!this._activeSession) { + return; + } + + 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: false + }; + + 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 task; + + 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 async _createSession(editor: IActiveCodeEditor) { + const createSessionCts = new CancellationTokenSource(); + const session = await this._inlineChatSessionService.createSession( + editor, + { editMode: EditMode.Live }, + createSessionCts.token + ); + + createSessionCts.dispose(); + + return session; + } + + 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.makeProgressiveChanges(editor, editOperations, opts); + } else { + await this.makeChanges(editor, editOperations); + } + // this._ctxDidEdit.set(this._activeSession.hasChangedText); + } finally { + // this._ignoreModelContentChanged = false; + } + } + + async makeProgressiveChanges(editor: IActiveCodeEditor, edits: ISingleEditOperation[], opts: ProgressingEditsOptions): Promise { + // push undo stop before first edit + // if (++this._editCount === 1) { + // this._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) { + // this._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; + } + + ctrl.startSession(); + } +}); + +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(); + } +}); 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/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts index 31f4337c70ad6..0d3d42e5a2302 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts @@ -346,7 +346,7 @@ export class CodeCell extends Disposable { this.templateData.editor?.focus(); } - this.templateData.container.classList.toggle('cell-editor-focus', this.viewCell.focusMode === CellFocusMode.Editor); + this.templateData.container.classList.toggle('cell-editor-focus', this.viewCell.focusMode === CellFocusMode.Editor || this.viewCell.focusMode === CellFocusMode.ChatInput); this.templateData.container.classList.toggle('cell-output-focus', this.viewCell.focusMode === CellFocusMode.Output); } 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..aaa950f25dc1f 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/cellChatWidget'; 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')))), @@ -264,6 +267,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const executionOrderLabel = DOM.append(focusIndicatorLeft.domNode, $('div.execution-count-label')); executionOrderLabel.title = localize('cellExecutionOrderCountLabel', 'Execution Order'); const editorPart = DOM.append(cellContainer, $('.cell-editor-part')); + const cellChatPart = DOM.append(editorPart, $('.cell-chat-part')); const editorContainer = DOM.append(editorPart, $('.cell-editor-container')); const cellCommentPartContainer = DOM.append(container, $('.cell-comment-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..9fa738031eca5 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,22 +241,23 @@ 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; } const statusBarHeight = this.viewContext.notebookOptions.computeEditorStatusbarHeight(this.internalMetadata, this.uri); - const codeIndicatorHeight = editorHeight + statusBarHeight; + const codeIndicatorHeight = chatHeight + editorHeight + statusBarHeight; 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 3c5e49d88c65b..c4ee72458c98f 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 {