From ecd4f3806f012078b7a5e630ee32372124efe46a Mon Sep 17 00:00:00 2001 From: rebornix Date: Mon, 13 Nov 2023 19:35:17 -0800 Subject: [PATCH 01/12] 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 { From 1d150d7168f7d82f217570184d0da9d80f5cd275 Mon Sep 17 00:00:00 2001 From: rebornix Date: Mon, 13 Nov 2023 19:58:51 -0800 Subject: [PATCH 02/12] Extract strategy --- .../browser/view/cellParts/cellChatWidget.ts | 99 ++++++++++--------- 1 file changed, 55 insertions(+), 44 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellChatWidget.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellChatWidget.ts index b4d347924dce4..a9d44e72a6dc1 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellChatWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellChatWidget.ts @@ -5,7 +5,7 @@ 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 { Queue, raceCancellationError } 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'; @@ -41,7 +41,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati 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 { IInlineChatSessionService, ReplyResponse, Session, SessionPrompt } 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'; @@ -242,6 +242,10 @@ class CellChatWidget extends Disposable { } } + getInput() { + return this._inputEditor.getValue(); + } + updateProgress(show: boolean) { if (show) { this._progressBar.infinite(); @@ -258,6 +262,42 @@ class CellChatWidget extends Disposable { } } +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); + } +} + class NotebookCellChatController extends Disposable { private static _cellChatControllers = new WeakMap(); @@ -268,6 +308,7 @@ class NotebookCellChatController extends Disposable { private _activeSession?: Session; private readonly _ctxHasActiveRequest: IContextKey; private _isVisible: boolean = false; + private _strategy: EditStrategy = new EditStrategy(); constructor( private readonly _notebookEditor: INotebookEditorDelegate, @@ -313,6 +354,8 @@ class NotebookCellChatController extends Disposable { async acceptInput() { assertType(this._activeSession); + this._activeSession.addInput(new SessionPrompt(this._getInput())); + assertType(this._activeSession.lastInput); const value = this._activeSession.lastInput.value; @@ -323,10 +366,6 @@ class NotebookCellChatController extends Disposable { const editor = editors[1]; - if (!this._activeSession) { - return; - } - this._ctxHasActiveRequest.set(true); this._chatPart.getWidget().updateProgress(true); @@ -336,7 +375,7 @@ class NotebookCellChatController extends Disposable { attempt: 0, selection: { selectionStartLineNumber: 1, selectionStartColumn: 1, positionLineNumber: 1, positionColumn: 1 }, wholeRange: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, - live: false + live: true }; const requestCts = new CancellationTokenSource(); @@ -346,7 +385,7 @@ class NotebookCellChatController extends Disposable { const progressiveEditsAvgDuration = new MovingAverage(); const progressiveEditsCts = new CancellationTokenSource(requestCts.token); const progress = new AsyncProgress(async data => { - console.log('received chunk', data, request); + // console.log('received chunk', data, request); if (requestCts.token.isCancellationRequested) { return; @@ -373,7 +412,7 @@ class NotebookCellChatController extends Disposable { }); const task = this._activeSession.provider.provideResponse(this._activeSession.session, request, progress, requestCts.token); - const reply = await task; + 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 @@ -410,11 +449,15 @@ class NotebookCellChatController extends Disposable { this._chatPart.getWidget().hide(); } + private _getInput() { + return this._chatPart.getWidget().getInput(); + } + private async _createSession(editor: IActiveCodeEditor) { const createSessionCts = new CancellationTokenSource(); const session = await this._inlineChatSessionService.createSession( editor, - { editMode: EditMode.Live }, + { editMode: EditMode.LivePreview }, createSessionCts.token ); @@ -441,47 +484,15 @@ class NotebookCellChatController extends Disposable { // this._ignoreModelContentChanged = true; this._activeSession.wholeRange.trackEdits(editOperations); if (opts) { - await this.makeProgressiveChanges(editor, editOperations, opts); + await this._strategy.makeProgressiveChanges(editor, editOperations, opts); } else { - await this.makeChanges(editor, editOperations); + await this._strategy.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 { From 055662fd90b22bb9384c51b26535590e6f5d116f Mon Sep 17 00:00:00 2001 From: rebornix Date: Mon, 13 Nov 2023 21:13:14 -0800 Subject: [PATCH 03/12] reveal cell is async --- .../workbench/contrib/notebook/browser/notebookBrowser.ts | 2 +- .../contrib/notebook/browser/notebookEditorWidget.ts | 8 ++++---- .../notebook/browser/view/notebookRenderingCommon.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 6aa17c1bf83f3..2d3fdb74d796f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -592,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. diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 1f654d7080226..7684dfd8c6b5c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -2066,7 +2066,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) { @@ -2347,7 +2347,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); } } @@ -2385,11 +2385,11 @@ 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); } 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; From ae8d58a41b208d292e55f716cb127d4631aa2b65 Mon Sep 17 00:00:00 2001 From: rebornix Date: Mon, 13 Nov 2023 21:13:41 -0800 Subject: [PATCH 04/12] Extract insertNewCell function --- .../browser/controller/insertCellActions.ts | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) 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'); From 57d450cfc6fdacbf26c06693dac10abb36853c50 Mon Sep 17 00:00:00 2001 From: rebornix Date: Mon, 13 Nov 2023 21:18:05 -0800 Subject: [PATCH 05/12] Support Insert Cell with Chat enabled --- .../media/notebookCellInsertToolbar.css | 7 ++- .../browser/view/cellParts/cellChatWidget.ts | 60 ++++++++++++++++++- 2 files changed, 63 insertions(+), 4 deletions(-) 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/view/cellParts/cellChatWidget.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellChatWidget.ts index a9d44e72a6dc1..200c862ad3037 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellChatWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellChatWidget.ts @@ -46,9 +46,11 @@ import { ProgressingEditsOptions, asProgressiveEdit, performAsyncTextEdit } from 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 { insertNewCell } from 'vs/workbench/contrib/notebook/browser/controller/insertCellActions'; 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'; +import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NOTEBOOK_EDITOR_EDITABLE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; 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")); @@ -113,6 +115,8 @@ export class CellChatPart extends CellContentPart { } override dispose() { + this._controller?.dispose(); + this._controller = undefined; super.dispose(); } } @@ -325,6 +329,10 @@ class NotebookCellChatController extends Disposable { } public override dispose(): void { + if (this._isVisible) { + // detach the chat widget + this._chatPart.getWidget().hide(); + } this._ctxHasActiveRequest.reset(); NotebookCellChatController._cellChatControllers.delete(this._cell); super.dispose(); @@ -614,3 +622,53 @@ registerAction2(class extends NotebookCellAction { 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, 'editor'); + const ctrl = NotebookCellChatController.get(newCell); + if (!ctrl) { + return; + } + ctrl.startSession(); + } +}); From 17f0e23a7ff31dabfc5399ee8df259fd113e0c48 Mon Sep 17 00:00:00 2001 From: rebornix Date: Mon, 13 Nov 2023 21:29:46 -0800 Subject: [PATCH 06/12] part/controller/widget --- .../cellChatController.ts} | 310 +++--------------- .../view/cellParts/chat/cellChatPart.ts | 59 ++++ .../view/cellParts/chat/cellChatWidget.ts | 174 ++++++++++ .../browser/view/renderers/cellRenderer.ts | 2 +- 4 files changed, 279 insertions(+), 266 deletions(-) rename src/vs/workbench/contrib/notebook/browser/view/cellParts/{cellChatWidget.ts => chat/cellChatController.ts} (65%) create mode 100644 src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatPart.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatWidget.ts diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellChatWidget.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts similarity index 65% rename from src/vs/workbench/contrib/notebook/browser/view/cellParts/cellChatWidget.ts rename to src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts index 200c862ad3037..bca6d003803c4 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellChatWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts @@ -3,306 +3,50 @@ * 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, raceCancellationError } 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 { ICursorStateComputer } 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 { 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, SessionPrompt } 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 { insertNewCell } from 'vs/workbench/contrib/notebook/browser/controller/insertCellActions'; -import { CellFocusMode, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellContentPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; +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_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() { - this._controller?.dispose(); - this._controller = undefined; - 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; - } - } - - 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())); - } - } -} - -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); - } +interface ICellChatPart { + activeCell: ICellViewModel | undefined; + getWidget(): CellChatWidget; } -class NotebookCellChatController extends Disposable { +export class NotebookCellChatController extends Disposable { private static _cellChatControllers = new WeakMap(); static get(cell: ICellViewModel): NotebookCellChatController | undefined { @@ -316,7 +60,7 @@ class NotebookCellChatController extends Disposable { constructor( private readonly _notebookEditor: INotebookEditorDelegate, - private readonly _chatPart: CellChatPart, + private readonly _chatPart: ICellChatPart, private readonly _cell: ICellViewModel, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, @@ -503,6 +247,42 @@ class NotebookCellChatController extends Disposable { } } +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( 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..072d55364bc5e --- /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, + _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; + } + } + + 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/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index aaa950f25dc1f..9e524d69101c3 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -25,7 +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 { 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'; From c9b730cb989448e65e3b4cecadab35d4239366dd Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 14 Nov 2023 22:06:55 -0800 Subject: [PATCH 07/12] styling update --- .../contrib/notebook/browser/media/notebookCellChat.css | 9 +++++++++ .../contrib/notebook/browser/notebookBrowser.ts | 2 +- .../contrib/notebook/browser/notebookEditorWidget.ts | 7 +++++++ .../notebook/browser/view/cellParts/cellExecution.ts | 2 +- .../browser/view/cellParts/cellFocusIndicator.ts | 4 +++- .../browser/view/cellParts/chat/cellChatController.ts | 2 +- .../browser/view/cellParts/chat/cellChatWidget.ts | 8 ++++---- .../contrib/notebook/browser/view/cellParts/codeCell.ts | 2 +- .../notebook/browser/view/renderers/cellRenderer.ts | 2 +- .../notebook/browser/viewModel/codeCellViewModel.ts | 2 +- 10 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css index 4ce82533e29e0..80b3cb11605b7 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css @@ -2,6 +2,15 @@ * 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 12px 0px 8px; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 2d3fdb74d796f..b800d7bbffbf8 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -235,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[] }>; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 7684dfd8c6b5c..3d9c07d712064 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -860,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'); } 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..ff94c2f20c882 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,6 +89,7 @@ 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.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/chat/cellChatController.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts index bca6d003803c4..19f7b4faf43b7 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts @@ -444,7 +444,7 @@ registerAction2(class extends NotebookCellAction { if (!newCell) { return; } - await context.notebookEditor.focusNotebookCell(newCell, 'editor'); + await context.notebookEditor.focusNotebookCell(newCell, 'container'); const ctrl = NotebookCellChatController.get(newCell); if (!ctrl) { return; 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 index 072d55364bc5e..b0bf8c662d8d7 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatWidget.ts @@ -63,7 +63,7 @@ export class CellChatWidget extends Disposable { constructor( private readonly _notebookEditor: INotebookEditorDelegate, - _partContainer: HTMLElement, + private readonly _partContainer: HTMLElement, @IModelService private readonly _modelService: IModelService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService @@ -130,7 +130,7 @@ export class CellChatWidget extends Disposable { } show(element: ICellViewModel) { - this._elements.root.style.display = 'block'; + this._partContainer.style.display = 'block'; this._activeCell = element; @@ -143,11 +143,11 @@ export class CellChatWidget extends Disposable { this.layout(); this._inputEditor.focus(); - this._activeCell.chatHeight = 62; + this._activeCell.chatHeight = 82 + 8 /* bottom margin*/; } hide() { - this._elements.root.style.display = 'none'; + this._partContainer.style.display = 'none'; if (this._activeCell) { this._activeCell.chatHeight = 0; } 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 0d3d42e5a2302..31f4337c70ad6 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.viewCell.focusMode === CellFocusMode.ChatInput); + this.templateData.container.classList.toggle('cell-editor-focus', this.viewCell.focusMode === CellFocusMode.Editor); 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 9e524d69101c3..cc0be19c3f051 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -260,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')); @@ -267,7 +268,6 @@ 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')); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index 9fa738031eca5..dc9d1dd3bb057 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -253,7 +253,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod } const statusBarHeight = this.viewContext.notebookOptions.computeEditorStatusbarHeight(this.internalMetadata, this.uri); - const codeIndicatorHeight = chatHeight + editorHeight + statusBarHeight; + const codeIndicatorHeight = editorHeight + statusBarHeight; const outputIndicatorHeight = outputTotalHeight + outputShowMoreContainerHeight; const outputContainerOffset = notebookLayoutConfiguration.editorToolbarHeight + notebookLayoutConfiguration.cellTopMargin // CELL_TOP_MARGIN From 77f41a2558d6efe39ac109f622e63c9bef479188 Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 14 Nov 2023 22:45:28 -0800 Subject: [PATCH 08/12] Update styles --- .../notebook/browser/media/notebookCellChat.css | 2 +- .../contrib/notebook/browser/notebookBrowser.ts | 2 +- .../notebook/browser/notebookEditorWidget.ts | 6 +++--- .../view/cellParts/chat/cellChatController.ts | 15 ++++++++++----- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css index 80b3cb11605b7..7af5203684a1f 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css @@ -12,7 +12,7 @@ background: var(--vscode-inlineChat-background); } .monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container { - padding: 8px 12px 0px 8px; + padding: 8px 8px 0px 8px; } .monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index b800d7bbffbf8..61b361aac83b7 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -607,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. diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 3d9c07d712064..9707e35bb102a 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -2084,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) { @@ -2398,7 +2398,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } else if (options?.revealBehavior === ScrollToRevealBehavior.fullCell) { 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/chat/cellChatController.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts index 19f7b4faf43b7..e558e01a82d47 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts @@ -88,8 +88,15 @@ export class NotebookCellChatController extends Disposable { } } - async startSession() { + async show() { this._isVisible = true; + this._chatPart.getWidget().show(this._cell); + this._chatPart.getWidget().placeholder = this._activeSession?.session.placeholder ?? localize('default.placeholder', "Ask a question"); + + await this.startSession(); + } + + async startSession() { if (this._activeSession) { this._inlineChatSessionService.releaseSession(this._activeSession); } @@ -99,9 +106,7 @@ export class NotebookCellChatController extends Disposable { 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() { @@ -308,7 +313,7 @@ registerAction2(class extends NotebookCellAction { return; } - ctrl.startSession(); + ctrl.show(); } }); @@ -449,6 +454,6 @@ registerAction2(class extends NotebookCellAction { if (!ctrl) { return; } - ctrl.startSession(); + await ctrl.show(); } }); From a9954e0ddf0168625390f5d1873a82e1df3a1ca9 Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 14 Nov 2023 23:04:23 -0800 Subject: [PATCH 09/12] Handle code cell editor async ctor --- .../view/cellParts/chat/cellChatController.ts | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) 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 index e558e01a82d47..e461f2d6b6af2 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Queue, raceCancellationError } from 'vs/base/common/async'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +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'; @@ -53,6 +53,7 @@ export class NotebookCellChatController extends Disposable { return NotebookCellChatController._cellChatControllers.get(cell); } + private _sessionCtor: CancelablePromise | undefined; private _activeSession?: Session; private readonly _ctxHasActiveRequest: IContextKey; private _isVisible: boolean = false; @@ -76,6 +77,8 @@ export class NotebookCellChatController extends Disposable { if (this._isVisible) { // detach the chat widget this._chatPart.getWidget().hide(); + this._sessionCtor?.cancel(); + this._sessionCtor = undefined; } this._ctxHasActiveRequest.reset(); NotebookCellChatController._cellChatControllers.delete(this._cell); @@ -91,12 +94,19 @@ export class NotebookCellChatController extends Disposable { async show() { this._isVisible = true; this._chatPart.getWidget().show(this._cell); - this._chatPart.getWidget().placeholder = this._activeSession?.session.placeholder ?? localize('default.placeholder', "Ask a question"); + this._sessionCtor = createCancelablePromise(async token => { + if (this._cell.editorAttached) { + await this._startSession(token); + } else { + await Event.toPromise(Event.once(this._cell.onDidChangeEditorAttachState)); + await this._startSession(token); + } - await this.startSession(); + this._chatPart.getWidget().placeholder = this._activeSession?.session.placeholder ?? localize('default.placeholder', "Ask a question"); + }); } - async startSession() { + private async _startSession(token: CancellationToken) { if (this._activeSession) { this._inlineChatSessionService.releaseSession(this._activeSession); } @@ -106,7 +116,15 @@ export class NotebookCellChatController extends Disposable { return; } - this._activeSession = await this._createSession(editors[1]); + const editor = editors[1]; + + const session = await this._inlineChatSessionService.createSession( + editor, + { editMode: EditMode.LivePreview }, + token + ); + + this._activeSession = session; } async acceptInput() { @@ -210,19 +228,6 @@ export class NotebookCellChatController extends Disposable { return this._chatPart.getWidget().getInput(); } - private async _createSession(editor: IActiveCodeEditor) { - const createSessionCts = new CancellationTokenSource(); - const session = await this._inlineChatSessionService.createSession( - editor, - { editMode: EditMode.LivePreview }, - createSessionCts.token - ); - - createSessionCts.dispose(); - - return session; - } - private async _makeChanges(editor: IActiveCodeEditor, edits: TextEdit[], opts: ProgressingEditsOptions | undefined) { assertType(this._activeSession); @@ -313,7 +318,7 @@ registerAction2(class extends NotebookCellAction { return; } - ctrl.show(); + await ctrl.show(); } }); @@ -454,6 +459,6 @@ registerAction2(class extends NotebookCellAction { if (!ctrl) { return; } - await ctrl.show(); + ctrl.show(); } }); From 2fd988f0754151c5b09349d7c776a1514fbd1516 Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 14 Nov 2023 23:06:16 -0800 Subject: [PATCH 10/12] early return for cancelled request --- .../browser/view/cellParts/chat/cellChatController.ts | 4 ++++ 1 file changed, 4 insertions(+) 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 index e461f2d6b6af2..d5b53bd15891f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts @@ -99,6 +99,10 @@ export class NotebookCellChatController extends Disposable { await this._startSession(token); } else { await Event.toPromise(Event.once(this._cell.onDidChangeEditorAttachState)); + if (token.isCancellationRequested) { + return; + } + await this._startSession(token); } From afe429c5f5d7412780c51d87cce6cf631203d9d9 Mon Sep 17 00:00:00 2001 From: rebornix Date: Wed, 15 Nov 2023 11:09:41 -0800 Subject: [PATCH 11/12] Listen to inline and dismiss on cell properly --- .../browser/inlineChatController.ts | 4 ++ .../view/cellParts/chat/cellChatController.ts | 49 ++++++++++++++++--- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 4c42fac250f46..31a3dc4bad10f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -118,6 +118,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); @@ -218,6 +221,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/notebook/browser/view/cellParts/chat/cellChatController.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts index d5b53bd15891f..6f15e73d31778 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts @@ -9,7 +9,7 @@ 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 } from 'vs/base/common/lifecycle'; +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'; @@ -29,6 +29,7 @@ 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'; @@ -59,6 +60,8 @@ export class NotebookCellChatController extends Disposable { private _isVisible: boolean = false; private _strategy: EditStrategy = new EditStrategy(); + private _inlineChatListener: IDisposable | undefined; + constructor( private readonly _notebookEditor: INotebookEditorDelegate, private readonly _chatPart: ICellChatPart, @@ -71,6 +74,22 @@ export class NotebookCellChatController extends Disposable { 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 { @@ -80,6 +99,9 @@ export class NotebookCellChatController extends Disposable { this._sessionCtor?.cancel(); this._sessionCtor = undefined; } + + this._inlineChatListener?.dispose(); + this._inlineChatListener = undefined; this._ctxHasActiveRequest.reset(); NotebookCellChatController._cellChatControllers.delete(this._cell); super.dispose(); @@ -96,31 +118,42 @@ export class NotebookCellChatController extends Disposable { this._chatPart.getWidget().show(this._cell); this._sessionCtor = createCancelablePromise(async token => { if (this._cell.editorAttached) { - await this._startSession(token); + const editor = this._getCellEditor(); + if (editor) { + await this._startSession(editor, token); + } } else { await Event.toPromise(Event.once(this._cell.onDidChangeEditorAttachState)); if (token.isCancellationRequested) { return; } - await this._startSession(token); + 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 async _startSession(token: CancellationToken) { - if (this._activeSession) { - this._inlineChatSessionService.releaseSession(this._activeSession); - } - + 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, From defffc14330becd3bc23e320427b2e13164046ce Mon Sep 17 00:00:00 2001 From: rebornix Date: Wed, 15 Nov 2023 15:08:38 -0800 Subject: [PATCH 12/12] Update cell indicator offset --- .../notebook/browser/view/cellParts/cellFocusIndicator.ts | 1 + 1 file changed, 1 insertion(+) 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 ff94c2f20c882..95333b95f4ca0 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts @@ -91,6 +91,7 @@ export class CellFocusIndicator extends CellContentPart { 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)`; }