diff --git a/src/vs/base/browser/ui/button/button.css b/src/vs/base/browser/ui/button/button.css index 14a189bd0fc46..2517cd3571ca9 100644 --- a/src/vs/base/browser/ui/button/button.css +++ b/src/vs/base/browser/ui/button/button.css @@ -101,6 +101,8 @@ border: 1px solid var(--vscode-button-border, transparent); border-left-width: 0 !important; border-radius: 0 2px 2px 0; + display: flex; + align-items: center; } .monaco-button-dropdown > .monaco-button.monaco-text-button { diff --git a/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts b/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts index a1e82f322d539..d51aaf5eb5c49 100644 --- a/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts +++ b/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts @@ -25,7 +25,7 @@ export interface IOptions { className?: string; isAccessible?: boolean; isResizeable?: boolean; - frameColor?: Color; + frameColor?: Color | string; arrowColor?: Color; keepEditorSelection?: boolean; allowUnlimitedHeight?: boolean; @@ -34,7 +34,7 @@ export interface IOptions { } export interface IStyles { - frameColor?: Color | null; + frameColor?: Color | string | null; arrowColor?: Color | null; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css index 5abc0995871ce..97516b7727214 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css @@ -318,7 +318,6 @@ /* create zone */ .monaco-editor .inline-chat-newfile-widget { - padding: 3px 0 6px 0; background-color: var(--vscode-inlineChat-regionHighlight); } @@ -326,7 +325,22 @@ display: flex; align-items: center; justify-content: space-between; - padding: 3px 6px 3px 0; +} + +.monaco-editor .inline-chat-newfile-widget .title .detail { + margin-left: 4px; +} + +.monaco-editor .inline-chat-newfile-widget .buttonbar-widget { + display: flex; + margin-left: auto; + margin-right: 8px; +} + +.monaco-editor .inline-chat-newfile-widget .buttonbar-widget > .monaco-button { + display: inline-flex; + white-space: nowrap; + margin-left: 4px; } /* gutter decoration */ @@ -350,4 +364,3 @@ .monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent:hover { opacity: 1; } - diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 117c8cf734ef6..ed2a46603fe1d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -44,6 +44,7 @@ import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; export const enum State { CREATE_SESSION = 'CREATE_SESSION', @@ -144,6 +145,7 @@ export class InlineChatController implements IEditorContribution { @IKeybindingService private readonly _keybindingService: IKeybindingService, @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, + @IBulkEditService private readonly _bulkEditService: IBulkEditService, ) { this._ctxHasActiveRequest = CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.bindTo(contextKeyService); this._ctxDidEdit = CTX_INLINE_CHAT_DID_EDIT.bindTo(contextKeyService); @@ -270,7 +272,7 @@ export class InlineChatController implements IEditorContribution { this._zone.value.setContainerMargins(); } - if (this._activeSession && this._activeSession.hasChangedText) { + if (this._activeSession && (this._activeSession.hasChangedText || this._activeSession.lastExchange)) { widgetPosition = this._activeSession.wholeRange.value.getStartPosition().delta(-1); } if (this._activeSession) { @@ -688,7 +690,7 @@ export class InlineChatController implements IEditorContribution { if (reply.message) { markdownContents.appendMarkdown(reply.message.value); } - const replyResponse = response = new ReplyResponse(reply, markdownContents, this._activeSession.textModelN.uri, modelAltVersionIdNow, progressEdits); + const replyResponse = response = this._instaService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, modelAltVersionIdNow, progressEdits); for (let i = progressEdits.length; i < replyResponse.allLocalEdits.length; i++) { await this._makeChanges(replyResponse.allLocalEdits[i], undefined); @@ -696,7 +698,7 @@ export class InlineChatController implements IEditorContribution { const a11yMessageResponse = renderMarkdownAsPlaintext(replyResponse.mdContent); - a11yResponse = this._strategy.checkChanges(replyResponse) && a11yVerboseInlineChat + a11yResponse = a11yVerboseInlineChat ? a11yMessageResponse ? localize('editResponseMessage2', "{0}, also review proposed changes in the diff editor.", a11yMessageResponse) : localize('editResponseMessage', "Review proposed changes in the diff editor.") : a11yMessageResponse; } @@ -741,14 +743,10 @@ export class InlineChatController implements IEditorContribution { assertType(this._strategy); const { response } = this._activeSession.lastExchange!; - if (response instanceof ReplyResponse) { - // edit response -> complex... - this._zone.value.widget.updateMarkdownMessage(undefined); - - const canContinue = this._strategy.checkChanges(response); - if (!canContinue) { - return State.CANCEL; - } + if (response instanceof ReplyResponse && response.workspaceEdit) { + // this reply cannot be applied in the normal inline chat UI and needs to be handled off to workspace edit + this._bulkEditService.apply(response.workspaceEdit, { showPreview: true }); + return State.CANCEL; } return State.SHOW_RESPONSE; } @@ -782,7 +780,7 @@ export class InlineChatController implements IEditorContribution { } } - private async [State.SHOW_RESPONSE](): Promise { + private async[State.SHOW_RESPONSE](): Promise { assertType(this._activeSession); assertType(this._strategy); @@ -826,10 +824,6 @@ export class InlineChatController implements IEditorContribution { this._activeSession.lastExpansionState = this._zone.value.widget.expansionState; this._zone.value.widget.updateToolbar(true); - const canContinue = this._strategy.checkChanges(response); - if (!canContinue) { - return State.CANCEL; - } await this._strategy.renderChanges(response); } this._showWidget(false); @@ -837,7 +831,7 @@ export class InlineChatController implements IEditorContribution { return State.WAIT_FOR_INPUT; } - private async [State.PAUSE]() { + private async[State.PAUSE]() { this._ctxDidEdit.reset(); this._ctxUserDidEdit.reset(); @@ -858,7 +852,7 @@ export class InlineChatController implements IEditorContribution { this._activeSession = undefined; } - private async [State.ACCEPT]() { + private async[State.ACCEPT]() { assertType(this._activeSession); assertType(this._strategy); this._sessionStore.clear(); @@ -876,7 +870,7 @@ export class InlineChatController implements IEditorContribution { this[State.PAUSE](); } - private async [State.CANCEL]() { + private async[State.CANCEL]() { assertType(this._activeSession); assertType(this._strategy); this._sessionStore.clear(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts index a4eb23d1431b2..c9b2ce8f92627 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Dimension, getWindow, h, runAtThisOrScheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; -import { MutableDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { assertType } from 'vs/base/common/types'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; @@ -25,17 +25,25 @@ import { IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; import { ILogService } from 'vs/platform/log/common/log'; import { lineRangeAsRange, invertLineRange } from 'vs/workbench/contrib/inlineChat/browser/utils'; import { ResourceLabel } from 'vs/workbench/browser/labels'; -import { URI } from 'vs/base/common/uri'; -import { TextEdit } from 'vs/editor/common/languages'; import { FileKind } from 'vs/platform/files/common/files'; -import { IModelService } from 'vs/editor/common/services/model'; -import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { ILanguageService } from 'vs/editor/common/languages/language'; import { FoldingController } from 'vs/editor/contrib/folding/browser/folding'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { generateUuid } from 'vs/base/common/uuid'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ButtonBar, IButton } from 'vs/base/browser/ui/button/button'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { SaveReason, SideBySideEditor } from 'vs/workbench/common/editor'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IAction, toAction } from 'vs/base/common/actions'; +import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { Codicon } from 'vs/base/common/codicons'; +import { TAB_ACTIVE_MODIFIED_BORDER } from 'vs/workbench/common/theme'; +import { localize } from 'vs/nls'; +import { Event } from 'vs/base/common/event'; export class InlineChatLivePreviewWidget extends ZoneWidget { @@ -308,6 +316,8 @@ export class InlineChatLivePreviewWidget extends ZoneWidget { export class InlineChatFileCreatePreviewWidget extends ZoneWidget { + private static TitleHeight = 35; + private readonly _elements = h('div.inline-chat-newfile-widget@domNode', [ h('div.title@title', [ h('span.name.show-file-icons@name'), @@ -318,21 +328,31 @@ export class InlineChatFileCreatePreviewWidget extends ZoneWidget { private readonly _name: ResourceLabel; private readonly _previewEditor: ICodeEditor; - private readonly _previewModel = new MutableDisposable(); + private readonly _previewStore = new MutableDisposable(); + private readonly _buttonBar: ButtonBarWidget; private _dim: Dimension | undefined; constructor( parentEditor: ICodeEditor, @IInstantiationService instaService: IInstantiationService, - @ILanguageService private readonly _languageService: ILanguageService, - @IModelService private readonly _modelService: IModelService, @IThemeService themeService: IThemeService, - + @ITextModelService private readonly _textModelResolverService: ITextModelService, + @IEditorService private readonly _editorService: IEditorService, ) { - super(parentEditor, { showArrow: false, showFrame: false, isResizeable: false, isAccessible: true, showInHiddenAreas: true, ordinal: 10000 + 2 }); + super(parentEditor, { + showArrow: false, + showFrame: true, + frameColor: colorRegistry.asCssVariable(TAB_ACTIVE_MODIFIED_BORDER), + frameWidth: 1, + isResizeable: true, + isAccessible: true, + showInHiddenAreas: true, + ordinal: 10000 + 2 + }); super.create(); this._name = instaService.createInstance(ResourceLabel, this._elements.name, { supportIcons: true }); + this._elements.detail.appendChild(renderIcon(Codicon.circleFilled)); const contributions = EditorExtensionsRegistry .getEditorContributions() @@ -341,7 +361,6 @@ export class InlineChatFileCreatePreviewWidget extends ZoneWidget { this._previewEditor = instaService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, { scrollBeyondLastLine: false, stickyScroll: { enabled: false }, - readOnly: true, minimap: { enabled: false }, scrollbar: { alwaysConsumeMouseWheel: false, useShadows: true, ignoreHorizontalScrollbarInContentHeight: true, }, }, { isSimpleWidget: true, contributions }, parentEditor); @@ -362,12 +381,16 @@ export class InlineChatFileCreatePreviewWidget extends ZoneWidget { }; doStyle(); this._disposables.add(themeService.onDidColorThemeChange(doStyle)); + + this._buttonBar = instaService.createInstance(ButtonBarWidget); + this._elements.title.appendChild(this._buttonBar.domNode); } override dispose(): void { this._name.dispose(); + this._buttonBar.dispose(); this._previewEditor.dispose(); - this._previewModel.dispose(); + this._previewStore.dispose(); super.dispose(); } @@ -379,25 +402,64 @@ export class InlineChatFileCreatePreviewWidget extends ZoneWidget { throw new Error('Use showFileCreation'); } - showCreation(where: Range, uri: URI, edits: TextEdit[]): void { + async showCreation(where: Position, untitledTextModel: IUntitledTextEditorModel): Promise { + + const store = new DisposableStore(); + this._previewStore.value = store; + + this._name.element.setFile(untitledTextModel.resource, { + fileKind: FileKind.FILE, + fileDecorations: { badges: true, colors: true } + }); + + const actionSave = toAction({ + id: '1', + label: localize('save', "Create"), + run: () => untitledTextModel.save({ reason: SaveReason.EXPLICIT }) + }); + const actionSaveAs = toAction({ + id: '2', + label: localize('saveAs', "Create As"), + run: async () => { + const ids = this._editorService.findEditors(untitledTextModel.resource, { supportSideBySide: SideBySideEditor.ANY }); + await this._editorService.save(ids.slice(), { saveAs: true, reason: SaveReason.EXPLICIT }); + } + }); + + this._buttonBar.update([ + [actionSave, actionSaveAs], + [(toAction({ id: '3', label: localize('discard', "Discard"), run: () => untitledTextModel.revert() }))] + ]); + + store.add(Event.any( + untitledTextModel.onDidRevert, + untitledTextModel.onDidSave, + untitledTextModel.onDidChangeDirty, + untitledTextModel.onWillDispose + )(() => this.hide())); + + await untitledTextModel.resolve(); - this._name.element.setFile(uri, { fileKind: FileKind.FILE }); + const ref = await this._textModelResolverService.createModelReference(untitledTextModel.resource); + store.add(ref); - const langSelection = this._languageService.createByFilepathOrFirstLine(uri, undefined); - const model = this._modelService.createModel('', langSelection, undefined, true); - model.applyEdits(edits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text))); - this._previewModel.value = model; + const model = ref.object.textEditorModel; this._previewEditor.setModel(model); const lineHeight = this.editor.getOption(EditorOption.lineHeight); - this._elements.title.style.height = `${lineHeight}px`; - const maxLines = Math.max(4, Math.floor((this.editor.getLayoutInfo().height / lineHeight) / .33)); + this._elements.title.style.height = `${InlineChatFileCreatePreviewWidget.TitleHeight}px`; + const titleHightInLines = InlineChatFileCreatePreviewWidget.TitleHeight / lineHeight; + + const maxLines = Math.max(4, Math.floor((this.editor.getLayoutInfo().height / lineHeight) * .33)); const lines = Math.min(maxLines, model.getLineCount()); - const lineHeightPadding = (lineHeight / 12) /* padding-top/bottom*/; + super.show(where, titleHightInLines + lines); + } - super.show(where, lines + 1 + lineHeightPadding); + override hide(): void { + this._previewStore.clear(); + super.hide(); } // --- layout @@ -420,8 +482,56 @@ export class InlineChatFileCreatePreviewWidget extends ZoneWidget { const newDim = new Dimension(widthInPixel, heightInPixel); if (!Dimension.equals(this._dim, newDim)) { this._dim = newDim; - const oneLineHeightInPx = this.editor.getOption(EditorOption.lineHeight); - this._previewEditor.layout(this._dim.with(undefined, this._dim.height - oneLineHeightInPx /* title */)); + this._previewEditor.layout(this._dim.with(undefined, this._dim.height - InlineChatFileCreatePreviewWidget.TitleHeight)); } } } + + +class ButtonBarWidget { + + private readonly _domNode = h('div.buttonbar-widget'); + private readonly _buttonBar: ButtonBar; + private readonly _store = new DisposableStore(); + + constructor( + @IContextMenuService private _contextMenuService: IContextMenuService, + ) { + this._buttonBar = new ButtonBar(this.domNode); + + } + + update(allActions: IAction[][]): void { + this._buttonBar.clear(); + let secondary = false; + for (const actions of allActions) { + let btn: IButton; + const [first, ...rest] = actions; + if (!first) { + continue; + } else if (rest.length === 0) { + // single action + btn = this._buttonBar.addButton({ ...defaultButtonStyles, secondary }); + } else { + btn = this._buttonBar.addButtonWithDropdown({ + ...defaultButtonStyles, + addPrimaryActionToDropdown: false, + actions: rest, + contextMenuProvider: this._contextMenuService + }); + } + btn.label = first.label; + this._store.add(btn.onDidClick(() => first.run())); + secondary = true; + } + } + + dispose(): void { + this._buttonBar.dispose(); + this._store.dispose(); + } + + get domNode() { + return this._domNode.root; + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index 98a1f36d34539..388856b8007e5 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Emitter, Event } from 'vs/base/common/event'; import { ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; -import { TextEdit } from 'vs/editor/common/languages'; +import { IWorkspaceTextEdit, TextEdit, WorkspaceEdit } from 'vs/editor/common/languages'; import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; import { EditMode, IInlineChatSessionProvider, IInlineChatSession, IInlineChatBulkEditResponse, IInlineChatEditResponse, IInlineChatMessageResponse, IInlineChatResponse, IInlineChatService, InlineChatResponseType, InlineChateResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IRange, Range } from 'vs/editor/common/core/range'; @@ -23,10 +22,16 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Iterable } from 'vs/base/common/iterator'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { isCancellationError } from 'vs/base/common/errors'; -import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { raceCancellation } from 'vs/base/common/async'; import { LineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ResourceMap } from 'vs/base/common/map'; +import { Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; export type Recording = { when: Date; @@ -282,9 +287,8 @@ export class ErrorResponse { export class ReplyResponse { readonly allLocalEdits: TextEdit[][] = []; - readonly singleCreateFileEdit: { uri: URI; edits: Promise[] } | undefined; - readonly workspaceEdits: ResourceEdit[] | undefined; - readonly workspaceEditsIncludeLocalEdits: boolean = false; + readonly untitledTextModel: IUntitledTextEditorModel | undefined; + readonly workspaceEdit: WorkspaceEdit | undefined; readonly responseType: InlineChateResponseTypes; @@ -294,62 +298,83 @@ export class ReplyResponse { localUri: URI, readonly modelAltVersionId: number, progressEdits: TextEdit[][], + @ITextFileService private readonly _textFileService: ITextFileService, + @ILanguageService private readonly _languageService: ILanguageService, ) { - this.allLocalEdits.push(...progressEdits); + const editsMap = new ResourceMap(); + + editsMap.set(localUri, [...progressEdits]); if (raw.type === InlineChatResponseType.EditorEdit) { // - this.allLocalEdits.push(raw.edits); - this.singleCreateFileEdit = undefined; - this.workspaceEdits = undefined; + editsMap.get(localUri)!.push(raw.edits); + } else if (raw.type === InlineChatResponseType.BulkEdit) { // const edits = ResourceEdit.convert(raw.edits); - this.workspaceEdits = edits; - - let isComplexEdit = false; - const localEdits: TextEdit[] = []; for (const edit of edits) { if (edit instanceof ResourceFileEdit) { - if (!isComplexEdit && edit.newResource && !edit.oldResource) { - // file create - if (this.singleCreateFileEdit) { - isComplexEdit = true; - this.singleCreateFileEdit = undefined; - } else { - this.singleCreateFileEdit = { uri: edit.newResource, edits: [] }; - if (edit.options.contents) { - this.singleCreateFileEdit.edits.push(edit.options.contents.then(x => ({ range: new Range(1, 1, 1, 1), text: x.toString() }))); - } + if (edit.newResource && !edit.oldResource) { + editsMap.set(edit.newResource, []); + if (edit.options.contents) { + console.warn('CONTENT not supported'); } } } else if (edit instanceof ResourceTextEdit) { // - if (isEqual(edit.resource, localUri)) { - localEdits.push(edit.textEdit); - this.workspaceEditsIncludeLocalEdits = true; - - } else if (isEqual(this.singleCreateFileEdit?.uri, edit.resource)) { - this.singleCreateFileEdit!.edits.push(Promise.resolve(edit.textEdit)); + const array = editsMap.get(edit.resource); + if (array) { + array.push([edit.textEdit]); } else { - isComplexEdit = true; + editsMap.set(edit.resource, [[edit.textEdit]]); } } } - if (localEdits.length > 0) { - this.allLocalEdits.push(localEdits); - } - if (isComplexEdit) { - this.singleCreateFileEdit = undefined; + } + + if (editsMap.size === 0) { + this.responseType = InlineChateResponseTypes.OnlyMessages; + } else if (editsMap.size === 1 && editsMap.has(localUri)) { + this.responseType = InlineChateResponseTypes.OnlyEdits; + } else { + this.responseType = InlineChateResponseTypes.Mixed; + } + + let needsWorkspaceEdit = false; + + for (const [uri, edits] of editsMap) { + + needsWorkspaceEdit = needsWorkspaceEdit || (uri.scheme !== Schemas.untitled && !isEqual(uri, localUri)); + + if (uri.scheme === Schemas.untitled && !this.untitledTextModel) { //TODO@jrieken the first untitled model WINS + const langSelection = this._languageService.createByFilepathOrFirstLine(uri, undefined); + const untitledTextModel = this._textFileService.untitled.create({ + associatedResource: uri, + languageId: langSelection.languageId + }); + this.untitledTextModel = untitledTextModel; + + untitledTextModel.resolve().then(async () => { + const model = untitledTextModel.textEditorModel!; + model.applyEdits(edits.flat().map(edit => EditOperation.replace(Range.lift(edit.range), edit.text))); + }); } } - this.responseType = (this.allLocalEdits.length || this.workspaceEdits) - ? mdContent.value ? InlineChateResponseTypes.Mixed : InlineChateResponseTypes.OnlyEdits - : InlineChateResponseTypes.OnlyMessages; + this.allLocalEdits = editsMap.get(localUri) ?? []; + + if (needsWorkspaceEdit) { + const workspaceEdits: IWorkspaceTextEdit[] = []; + for (const [uri, edits] of editsMap) { + for (const edit of edits.flat()) { + workspaceEdits.push({ resource: uri, textEdit: edit, versionId: undefined }); + } + } + this.workspaceEdit = { edits: workspaceEdits }; + } } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 8c2bddd62594b..9e77b97ff6915 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -25,21 +25,19 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { SaveReason } from 'vs/workbench/common/editor'; import { countWords, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { InlineChatFileCreatePreviewWidget, InlineChatLivePreviewWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget'; import { ReplyResponse, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { CTX_INLINE_CHAT_DOCUMENT_CHANGED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; export abstract class EditModeStrategy { abstract dispose(): void; - abstract checkChanges(response: ReplyResponse): boolean; - abstract apply(): Promise; abstract cancel(): Promise; @@ -66,8 +64,6 @@ export class PreviewStrategy extends EditModeStrategy { private readonly _session: Session, private readonly _widget: InlineChatWidget, @IContextKeyService contextKeyService: IContextKeyService, - @IBulkEditService private readonly _bulkEditService: IBulkEditService, - @IInstantiationService private readonly _instaService: IInstantiationService, ) { super(); @@ -84,37 +80,25 @@ export class PreviewStrategy extends EditModeStrategy { this._ctxDocumentChanged.reset(); } - checkChanges(response: ReplyResponse): boolean { - if (!response.workspaceEdits || response.singleCreateFileEdit) { - // preview stategy can handle simple workspace edit (single file create) - return true; - } - this._bulkEditService.apply(response.workspaceEdits, { showPreview: true }); - return false; - } - async apply() { if (!(this._session.lastExchange?.response instanceof ReplyResponse)) { return; } const editResponse = this._session.lastExchange?.response; - if (editResponse.workspaceEdits) { - await this._bulkEditService.apply(editResponse.workspaceEdits); - this._instaService.invokeFunction(showSingleCreateFile, editResponse); - + const { textModelN: modelN } = this._session; - } else if (!editResponse.workspaceEditsIncludeLocalEdits) { - - const { textModelN: modelN } = this._session; - - if (modelN.equalsTextBuffer(this._session.textModel0.getTextBuffer())) { - modelN.pushStackElement(); - for (const edits of editResponse.allLocalEdits) { - modelN.pushEditOperations(null, edits.map(TextEdit.asEditOperation), () => null); - } - modelN.pushStackElement(); + if (modelN.equalsTextBuffer(this._session.textModel0.getTextBuffer())) { + modelN.pushStackElement(); + for (const edits of editResponse.allLocalEdits) { + modelN.pushEditOperations(null, edits.map(TextEdit.asEditOperation), () => null); } + modelN.pushStackElement(); + } + + const { untitledTextModel } = this._session.lastExchange.response; + if (untitledTextModel && !untitledTextModel.isDisposed() && untitledTextModel.isDirty()) { + await untitledTextModel.save({ reason: SaveReason.EXPLICIT }); } } @@ -142,8 +126,8 @@ export class PreviewStrategy extends EditModeStrategy { this._widget.hideEditsPreview(); } - if (response.singleCreateFileEdit) { - this._widget.showCreatePreview(response.singleCreateFileEdit.uri, await Promise.all(response.singleCreateFileEdit.edits)); + if (response.untitledTextModel) { + this._widget.showCreatePreview(response.untitledTextModel); } else { this._widget.hideCreatePreview(); } @@ -237,7 +221,6 @@ export class LiveStrategy extends EditModeStrategy { private readonly _inlineDiffDecorations: InlineDiffDecorations; private readonly _store: DisposableStore = new DisposableStore(); - private _lastResponse?: ReplyResponse; private _editCount: number = 0; constructor( @@ -273,26 +256,16 @@ export class LiveStrategy extends EditModeStrategy { this._inlineDiffDecorations.visible = this._diffEnabled; } - checkChanges(response: ReplyResponse): boolean { - this._lastResponse = response; - if (response.singleCreateFileEdit) { - // preview stategy can handle simple workspace edit (single file create) - return true; - } - if (response.workspaceEdits) { - this._bulkEditService.apply(response.workspaceEdits, { showPreview: true }); - return false; - } - return true; - } - async apply() { if (this._editCount > 0) { this._editor.pushUndoStop(); } - if (this._lastResponse?.workspaceEdits) { - await this._bulkEditService.apply(this._lastResponse.workspaceEdits); - this._instaService.invokeFunction(showSingleCreateFile, this._lastResponse); + if (!(this._session.lastExchange?.response instanceof ReplyResponse)) { + return; + } + const { untitledTextModel } = this._session.lastExchange.response; + if (untitledTextModel && !untitledTextModel.isDisposed() && untitledTextModel.isDirty()) { + await untitledTextModel.save({ reason: SaveReason.EXPLICIT }); } } @@ -348,8 +321,8 @@ export class LiveStrategy extends EditModeStrategy { this._updateSummaryMessage(diff?.changes ?? []); this._inlineDiffDecorations.update(); - if (response.singleCreateFileEdit) { - this._widget.showCreatePreview(response.singleCreateFileEdit.uri, await Promise.all(response.singleCreateFileEdit.edits)); + if (response.untitledTextModel) { + this._widget.showCreatePreview(response.untitledTextModel); } else { this._widget.hideCreatePreview(); } @@ -514,8 +487,8 @@ export class LivePreviewStrategy extends LiveStrategy { await this._updateDiffZones(); - if (response.singleCreateFileEdit) { - this._previewZone.value.showCreation(this._session.wholeRange.value.collapseToStart(), response.singleCreateFileEdit.uri, await Promise.all(response.singleCreateFileEdit.edits)); + if (response.untitledTextModel && !response.untitledTextModel.isDisposed()) { + this._previewZone.value.showCreation(this._session.wholeRange.value.getStartPosition().delta(-1), response.untitledTextModel); } else { this._previewZone.value.hide(); } @@ -526,13 +499,6 @@ export class LivePreviewStrategy extends LiveStrategy { } } -function showSingleCreateFile(accessor: ServicesAccessor, edit: ReplyResponse) { - const editorService = accessor.get(IEditorService); - if (edit.singleCreateFileEdit) { - editorService.openEditor({ resource: edit.singleCreateFileEdit.uri }, SIDE_GROUP); - } -} - export interface AsyncTextEdit { readonly range: IRange; readonly newText: AsyncIterable; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index abeac899d1ae3..74900107cb2fd 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -28,9 +28,9 @@ import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; import { Position } from 'vs/editor/common/core/position'; import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style'; -import { CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult, TextEdit } from 'vs/editor/common/languages'; -import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; -import { ILanguageSelection, ILanguageService } from 'vs/editor/common/languages/language'; +import { CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult } from 'vs/editor/common/languages'; +import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { ILanguageSelection } from 'vs/editor/common/languages/language'; import { ResourceLabel } from 'vs/workbench/browser/labels'; import { FileKind } from 'vs/platform/files/common/files'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; @@ -62,6 +62,8 @@ import { editorForeground, inputBackground, editorBackground } from 'vs/platform import { CodeBlockPart } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { Lazy } from 'vs/base/common/lazy'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; +import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); @@ -183,7 +185,8 @@ export class InlineChatWidget { private readonly _previewCreateTitle: ResourceLabel; private readonly _previewCreateEditor: Lazy; - private readonly _previewCreateModel = this._store.add(new MutableDisposable()); + private readonly _previewCreateDispoable = this._store.add(new MutableDisposable()); + private readonly _onDidChangeHeight = this._store.add(new MicrotaskEmitter()); readonly onDidChangeHeight: Event = Event.filter(this._onDidChangeHeight.event, _ => !this._isLayouting); @@ -207,7 +210,6 @@ export class InlineChatWidget { constructor( private readonly parentEditor: ICodeEditor, @IModelService private readonly _modelService: IModelService, - @ILanguageService private readonly _languageService: ILanguageService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @@ -216,7 +218,8 @@ export class InlineChatWidget { @IConfigurationService private readonly _configurationService: IConfigurationService, @IContextMenuService private readonly _contextMenuService: IContextMenuService, @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService, - @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService + @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, + @ITextModelService private readonly _textModelResolverService: ITextModelService, ) { // input editor logic @@ -721,26 +724,23 @@ export class InlineChatWidget { this._onDidChangeHeight.fire(); } - showCreatePreview(uri: URI, edits: TextEdit[]): void { + async showCreatePreview(model: IUntitledTextEditorModel): Promise { this._elements.previewCreateTitle.classList.remove('hidden'); this._elements.previewCreate.classList.remove('hidden'); - this._previewCreateTitle.element.setFile(uri, { fileKind: FileKind.FILE }); + const ref = await this._textModelResolverService.createModelReference(model.resource); + this._previewCreateDispoable.value = ref; + this._previewCreateTitle.element.setFile(model.resource, { fileKind: FileKind.FILE }); - const langSelection = this._languageService.createByFilepathOrFirstLine(uri, undefined); - const model = this._modelService.createModel('', langSelection, undefined, true); - model.applyEdits(edits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text))); - this._previewCreateModel.value = model; - this._previewCreateEditor.value.setModel(model); + this._previewCreateEditor.value.setModel(ref.object.textEditorModel); this._onDidChangeHeight.fire(); } hideCreatePreview() { this._elements.previewCreateTitle.classList.add('hidden'); this._elements.previewCreate.classList.add('hidden'); - if (this._previewCreateEditor.hasValue) { - this._previewCreateEditor.value.setModel(null); - } + this._previewCreateEditor.rawValue?.setModel(null); + this._previewCreateDispoable.clear(); this._previewCreateTitle.element.clear(); this._onDidChangeHeight.fire(); } 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 6f15e73d31778..a3f686b982325 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 @@ -25,7 +25,7 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { localize } from 'vs/nls'; import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { 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'; @@ -69,6 +69,7 @@ export class NotebookCellChatController extends Disposable { @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, + @IInstantiationService private readonly _instaService: IInstantiationService, ) { super(); @@ -239,7 +240,7 @@ export class NotebookCellChatController extends Disposable { } 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); + const replyResponse = this._instaService.createInstance(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); }