From 09b53d2a313040b82eb525f00d29e95530e2fd7a Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 2 May 2023 12:57:34 +0200 Subject: [PATCH 1/8] IE - move slash commands into widget, leaner controller, towards a more passive widget --- .../browser/interactiveEditorController.ts | 96 +--------- .../browser/interactiveEditorWidget.ts | 178 +++++++++++++----- 2 files changed, 144 insertions(+), 130 deletions(-) diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts index fe785e64d7925..6fec6e313ad7c 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts @@ -5,7 +5,7 @@ import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { raceCancellationError } from 'vs/base/common/async'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { isCancellationError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; @@ -15,7 +15,7 @@ import { isEqual } from 'vs/base/common/resources'; import { StopWatch } from 'vs/base/common/stopwatch'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./interactiveEditor'; -import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IBulkEditService, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; @@ -23,12 +23,10 @@ import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; import { IEditorContribution, IEditorDecorationsCollection, ScrollType } from 'vs/editor/common/editorCommon'; -import { LanguageSelector } from 'vs/editor/common/languageSelector'; -import { CompletionContext, CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult, TextEdit } from 'vs/editor/common/languages'; +import { TextEdit } from 'vs/editor/common/languages'; import { ICursorStateComputer, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation } from 'vs/editor/common/model'; import { ModelDecorationOptions, createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { IModelService } from 'vs/editor/common/services/model'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; @@ -41,7 +39,7 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { InteractiveEditorDiffWidget } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget'; import { InteractiveEditorZoneWidget } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget'; -import { CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, CTX_INTERACTIVE_EDITOR_INLNE_DIFF, CTX_INTERACTIVE_EDITOR_LAST_EDIT_TYPE as CTX_INTERACTIVE_EDITOR_LAST_EDIT_KIND, CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK as CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK_KIND, IInteractiveEditorBulkEditResponse, IInteractiveEditorEditResponse, IInteractiveEditorRequest, IInteractiveEditorResponse, IInteractiveEditorService, IInteractiveEditorSession, IInteractiveEditorSessionProvider, IInteractiveEditorSlashCommand, INTERACTIVE_EDITOR_ID, EditMode, InteractiveEditorResponseFeedbackKind, CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE, InteractiveEditorResponseType, IInteractiveEditorMessageResponse } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; +import { CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, CTX_INTERACTIVE_EDITOR_INLNE_DIFF, CTX_INTERACTIVE_EDITOR_LAST_EDIT_TYPE as CTX_INTERACTIVE_EDITOR_LAST_EDIT_KIND, CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK as CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK_KIND, IInteractiveEditorBulkEditResponse, IInteractiveEditorEditResponse, IInteractiveEditorRequest, IInteractiveEditorResponse, IInteractiveEditorService, IInteractiveEditorSession, IInteractiveEditorSessionProvider, INTERACTIVE_EDITOR_ID, EditMode, InteractiveEditorResponseFeedbackKind, CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE, InteractiveEditorResponseType, IInteractiveEditorMessageResponse } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; import { IInteractiveSessionWidgetService } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget'; import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; @@ -422,10 +420,7 @@ export class InteractiveEditorController implements IEditorContribution { let value = options?.message ?? ''; let autoSend = options?.autoSend ?? false; - if (session.slashCommands) { - store.add(this._instaService.invokeFunction(installSlashCommandSupport, this._zone.widget.inputEditor as IActiveCodeEditor, session.slashCommands)); - } - + this._zone.widget.updateSlashCommands(session.slashCommands ?? []); this._zone.widget.updateStatus(session.message ?? localize('welcome.1', "AI-generated code may be incorrect.")); // CANCEL when input changes @@ -524,7 +519,7 @@ export class InteractiveEditorController implements IEditorContribution { continue; } - const typeListener = this._zone.widget.inputEditor.onDidChangeModelContent(() => { + const typeListener = this._zone.widget.onDidChangeInput(() => { this.cancelCurrentRequest(); _requestCancelledOnModelContentChanged = true; }); @@ -789,7 +784,9 @@ export class InteractiveEditorController implements IEditorContribution { } const pos = (len + this._historyOffset + (up ? 1 : -1)) % len; const entry = InteractiveEditorController._promptHistory[pos]; - this._zone.widget.populateInputField(entry); + + this._zone.widget.input = entry; + this._zone.widget.selectAll(); this._historyOffset = pos; } @@ -948,81 +945,6 @@ class LivePreviewStrategy extends LiveStrategy { } } -function installSlashCommandSupport(accessor: ServicesAccessor, editor: IActiveCodeEditor, commands: IInteractiveEditorSlashCommand[]) { - - const languageFeaturesService = accessor.get(ILanguageFeaturesService); - - const store = new DisposableStore(); - const selector: LanguageSelector = { scheme: editor.getModel().uri.scheme, pattern: editor.getModel().uri.path, language: editor.getModel().getLanguageId() }; - store.add(languageFeaturesService.completionProvider.register(selector, new class implements CompletionItemProvider { - - _debugDisplayName?: string = 'InteractiveEditorSlashCommandProvider'; - - readonly triggerCharacters?: string[] = ['/']; - - provideCompletionItems(model: ITextModel, position: Position, context: CompletionContext, token: CancellationToken): ProviderResult { - if (position.lineNumber !== 1 && position.column !== 1) { - return undefined; - } - - const suggestions: CompletionItem[] = commands.map(command => { - - const withSlash = `/${command.command}`; - - return { - label: { label: withSlash, description: command.detail }, - insertText: `${withSlash} $0`, - insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, - kind: CompletionItemKind.Text, - range: new Range(1, 1, 1, 1), - }; - }); - - return { suggestions }; - } - })); - - const decorations = editor.createDecorationsCollection(); - - const updateSlashDecorations = () => { - const newDecorations: IModelDeltaDecoration[] = []; - for (const command of commands) { - const withSlash = `/${command.command}`; - const firstLine = editor.getModel().getLineContent(1); - if (firstLine.startsWith(withSlash)) { - newDecorations.push({ - range: new Range(1, 1, 1, withSlash.length + 1), - options: { - description: 'interactive-editor-slash-command', - inlineClassName: 'interactive-editor-slash-command', - } - }); - - // inject detail when otherwise empty - if (firstLine === `/${command.command} `) { - newDecorations.push({ - range: new Range(1, withSlash.length + 1, 1, withSlash.length + 2), - options: { - description: 'interactive-editor-slash-command-detail', - after: { - content: `${command.detail}`, - inlineClassName: 'interactive-editor-slash-command-detail' - } - } - }); - } - break; - } - } - decorations.set(newDecorations); - }; - - store.add(editor.onDidChangeModelContent(updateSlashDecorations)); - updateSlashDecorations(); - - return store; -} - async function showMessageResponse(accessor: ServicesAccessor, query: string, response: string) { const interactiveSessionService = accessor.get(IInteractiveSessionService); const providerId = interactiveSessionService.getProviderInfos()[0]?.id; diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts index 5b69f7ef24b03..3e81db50b8271 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts @@ -14,10 +14,10 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; import { assertType } from 'vs/base/common/types'; -import { CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST, CTX_INTERACTIVE_EDITOR_EMPTY, CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION, CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET, MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, MENU_INTERACTIVE_EDITOR_WIDGET_MARKDOWN_MESSAGE, CTX_INTERACTIVE_EDITOR_MESSAGE_CROP_STATE } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; -import { ITextModel } from 'vs/editor/common/model'; +import { CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST, CTX_INTERACTIVE_EDITOR_EMPTY, CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION, CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET, MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, MENU_INTERACTIVE_EDITOR_WIDGET_MARKDOWN_MESSAGE, CTX_INTERACTIVE_EDITOR_MESSAGE_CROP_STATE, IInteractiveEditorSlashCommand } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; +import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; import { Dimension, addDisposableListener, getTotalHeight, getTotalWidth, h, reset } from 'vs/base/browser/dom'; -import { Event, MicrotaskEmitter } from 'vs/base/common/event'; +import { Emitter, Event, MicrotaskEmitter } from 'vs/base/common/event'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; @@ -31,13 +31,15 @@ import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestCont import { IPosition, Position } from 'vs/editor/common/core/position'; import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style'; import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { TextEdit } from 'vs/editor/common/languages'; +import { CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult, TextEdit } from 'vs/editor/common/languages'; import { EditOperation } 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 { IAction } from 'vs/base/common/actions'; import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { LanguageSelector } from 'vs/editor/common/languageSelector'; const _inputEditorOptions: IEditorConstructionOptions = { padding: { top: 3, bottom: 2 }, @@ -130,9 +132,9 @@ class InteractiveEditorWidget { ); private readonly _store = new DisposableStore(); - private readonly _historyStore = new DisposableStore(); + private readonly _slashCommands = this._store.add(new DisposableStore()); - readonly inputEditor: ICodeEditor; + readonly _inputEditor: ICodeEditor; private readonly _inputModel: ITextModel; private readonly _ctxInputEmpty: IContextKey; private readonly _ctxMessageCropState: IContextKey<'cropped' | 'not_cropped' | 'expanded'>; @@ -149,6 +151,9 @@ class InteractiveEditorWidget { private readonly _onDidChangeHeight = new MicrotaskEmitter(); readonly onDidChangeHeight: Event = Event.filter(this._onDidChangeHeight.event, _ => !this._isLayouting); + private readonly _onDidChangeInput = new Emitter(); + readonly onDidChangeInput: Event = this._onDidChangeInput.event; + private _lastDim: Dimension | undefined; private _isLayouting: boolean = false; @@ -158,6 +163,7 @@ class InteractiveEditorWidget { parentEditor: ICodeEditor, @IModelService private readonly _modelService: IModelService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { @@ -170,12 +176,15 @@ class InteractiveEditorWidget { ]) }; - this.inputEditor = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, _inputEditorOptions, codeEditorWidgetOptions, parentEditor); - this._store.add(this.inputEditor); + this._inputEditor = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, _inputEditorOptions, codeEditorWidgetOptions, parentEditor); + this._store.add(this._inputEditor); const uri = URI.from({ scheme: 'vscode', authority: 'interactive-editor', path: `/interactive-editor/model${InteractiveEditorWidget._modelPool++}.txt` }); this._inputModel = this._modelService.getModel(uri) ?? this._modelService.createModel('', null, uri); - this.inputEditor.setModel(this._inputModel); + this._inputEditor.setModel(this._inputModel); + + + this._store.add(this._inputEditor.onDidChangeModelContent(() => this._onDidChangeInput.fire(this))); // show/hide placeholder depending on text model being empty // content height @@ -189,18 +198,17 @@ class InteractiveEditorWidget { this._elements.placeholder.classList.toggle('hidden', hasText); this._ctxInputEmpty.set(!hasText); - const contentHeight = this.inputEditor.getContentHeight(); + const contentHeight = this._inputEditor.getContentHeight(); if (contentHeight !== currentContentHeight && this._lastDim) { this._lastDim = this._lastDim.with(undefined, contentHeight); - this.inputEditor.layout(this._lastDim); + this._inputEditor.layout(this._lastDim); this._onDidChangeHeight.fire(); } }; this._store.add(this._inputModel.onDidChangeContent(togglePlaceholder)); togglePlaceholder(); - this._store.add(addDisposableListener(this._elements.placeholder, 'click', () => this.inputEditor.focus())); - + this._store.add(addDisposableListener(this._elements.placeholder, 'click', () => this._inputEditor.focus())); const toolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, MENU_INTERACTIVE_EDITOR_WIDGET, { telemetrySource: 'interactiveEditorWidget-toolbar', @@ -220,7 +228,7 @@ class InteractiveEditorWidget { actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => createActionViewItem(this._instantiationService, action, options) }; const statusToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.statusToolbar, MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, workbenchToolbarOptions); - this._historyStore.add(statusToolbar); + this._store.add(statusToolbar); // preview editors this._previewDiffEditor = this._store.add(_instantiationService.createInstance(EmbeddedDiffEditorWidget, this._elements.previewDiff, _previewEditorEditorOptions, { modifiedEditor: codeEditorWidgetOptions, originalEditor: codeEditorWidgetOptions }, parentEditor)); @@ -231,12 +239,11 @@ class InteractiveEditorWidget { this._elements.message.tabIndex = 0; this._elements.statusLabel.tabIndex = 0; const markdownMessageToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.messageActions, MENU_INTERACTIVE_EDITOR_WIDGET_MARKDOWN_MESSAGE, workbenchToolbarOptions); - this._historyStore.add(markdownMessageToolbar); + this._store.add(markdownMessageToolbar); } dispose(): void { this._store.dispose(); - this._historyStore.dispose(); this._ctxInputEmpty.reset(); this._ctxMessageCropState.reset(); } @@ -252,7 +259,7 @@ class InteractiveEditorWidget { dim = new Dimension(innerEditorWidth, dim.height); if (!this._lastDim || !Dimension.equals(this._lastDim, dim)) { this._lastDim = dim; - this.inputEditor.layout(new Dimension(innerEditorWidth, this.inputEditor.getContentHeight())); + this._inputEditor.layout(new Dimension(innerEditorWidth, this._inputEditor.getContentHeight())); this._elements.placeholder.style.width = `${innerEditorWidth /* input-padding*/}px`; const previewDiffDim = new Dimension(dim.width, Math.min(300, Math.max(0, this._previewDiffEditor.getContentHeight()))); @@ -270,7 +277,7 @@ class InteractiveEditorWidget { getHeight(): number { const base = getTotalHeight(this._elements.progress) + getTotalHeight(this._elements.status); - const editorHeight = this.inputEditor.getContentHeight() + 12 /* padding and border */; + const editorHeight = this._inputEditor.getContentHeight() + 12 /* padding and border */; const markdownMessageHeight = getTotalHeight(this._elements.markdownMessage); const previewDiffHeight = this._previewDiffEditor.getModel().modified ? 12 + Math.min(300, Math.max(0, this._previewDiffEditor.getContentHeight())) : 0; const previewCreateTitleHeight = getTotalHeight(this._elements.previewCreateTitle); @@ -286,29 +293,41 @@ class InteractiveEditorWidget { } } + get input(): string { + return this._inputModel.getValue(); + } + + set input(value: string) { + this._inputModel.setValue(value); + } + + selectAll() { + this._inputEditor.setSelection(this._inputModel.getFullModelRange()); + } + getInput(placeholder: string, value: string, token: CancellationToken, requestCancelledOnModelContentChanged: boolean): Promise { - const currentInputEditorPosition = this.inputEditor.getPosition(); - const currentInputEditorValue = this.inputEditor.getValue(); + const currentInputEditorPosition = this._inputEditor.getPosition(); + const currentInputEditorValue = this._inputEditor.getValue(); this._elements.placeholder.innerText = placeholder; - this._elements.placeholder.style.fontSize = `${this.inputEditor.getOption(EditorOption.fontSize)}px`; - this._elements.placeholder.style.lineHeight = `${this.inputEditor.getOption(EditorOption.lineHeight)}px`; + this._elements.placeholder.style.fontSize = `${this._inputEditor.getOption(EditorOption.fontSize)}px`; + this._elements.placeholder.style.lineHeight = `${this._inputEditor.getOption(EditorOption.lineHeight)}px`; this._inputModel.setValue(requestCancelledOnModelContentChanged ? currentInputEditorValue : value); const fullInputModelRange = this._inputModel.getFullModelRange(); if (requestCancelledOnModelContentChanged) { - this.inputEditor.setPosition(currentInputEditorPosition ?? new Position(fullInputModelRange.endLineNumber, fullInputModelRange.endColumn)); + this._inputEditor.setPosition(currentInputEditorPosition ?? new Position(fullInputModelRange.endLineNumber, fullInputModelRange.endColumn)); } else { - this.inputEditor.setSelection(fullInputModelRange); + this._inputEditor.setSelection(fullInputModelRange); } - this.inputEditor.updateOptions({ ariaLabel: localize('aria-label.N', "Interactive Editor Input: {0}", placeholder) }); + this._inputEditor.updateOptions({ ariaLabel: localize('aria-label.N', "Interactive Editor Input: {0}", placeholder) }); const disposeOnDone = new DisposableStore(); - disposeOnDone.add(this.inputEditor.onDidLayoutChange(() => this._onDidChangeHeight.fire())); - disposeOnDone.add(this.inputEditor.onDidContentSizeChange(() => this._onDidChangeHeight.fire())); + disposeOnDone.add(this._inputEditor.onDidLayoutChange(() => this._onDidChangeHeight.fire())); + disposeOnDone.add(this._inputEditor.onDidContentSizeChange(() => this._onDidChangeHeight.fire())); const ctxInnerCursorFirst = CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST.bindTo(this._contextKeyService); const ctxInnerCursorLast = CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST.bindTo(this._contextKeyService); @@ -324,7 +343,7 @@ class InteractiveEditorWidget { }; this.acceptInput = () => { - const newValue = this.inputEditor.getModel()!.getValue(); + const newValue = this._inputEditor.getModel()!.getValue(); if (newValue.trim().length === 0) { // empty or whitespace only this._cancelInput(); @@ -342,24 +361,24 @@ class InteractiveEditorWidget { // (1) inner cursor position (last/first line selected) const updateInnerCursorFirstLast = () => { - if (!this.inputEditor.hasModel()) { + if (!this._inputEditor.hasModel()) { return; } - const { lineNumber } = this.inputEditor.getPosition(); + const { lineNumber } = this._inputEditor.getPosition(); ctxInnerCursorFirst.set(lineNumber === 1); - ctxInnerCursorLast.set(lineNumber === this.inputEditor.getModel().getLineCount()); + ctxInnerCursorLast.set(lineNumber === this._inputEditor.getModel().getLineCount()); }; - disposeOnDone.add(this.inputEditor.onDidChangeCursorPosition(updateInnerCursorFirstLast)); + disposeOnDone.add(this._inputEditor.onDidChangeCursorPosition(updateInnerCursorFirstLast)); updateInnerCursorFirstLast(); // (2) input editor focused or not const updateFocused = () => { - const hasFocus = this.inputEditor.hasWidgetFocus(); + const hasFocus = this._inputEditor.hasWidgetFocus(); ctxInputEditorFocused.set(hasFocus); this._elements.content.classList.toggle('synthetic-focus', hasFocus); }; - disposeOnDone.add(this.inputEditor.onDidFocusEditorWidget(updateFocused)); - disposeOnDone.add(this.inputEditor.onDidBlurEditorWidget(updateFocused)); + disposeOnDone.add(this._inputEditor.onDidFocusEditorWidget(updateFocused)); + disposeOnDone.add(this._inputEditor.onDidBlurEditorWidget(updateFocused)); updateFocused(); this.focus(); @@ -373,11 +392,6 @@ class InteractiveEditorWidget { }); } - populateInputField(value: string) { - this._inputModel.setValue(value.trim()); - this.inputEditor.setSelection(this._inputModel.getFullModelRange()); - } - updateToolbar(show: boolean) { this._elements.statusToolbar.classList.toggle('hidden', !show); this._onDidChangeHeight.fire(); @@ -428,7 +442,7 @@ class InteractiveEditorWidget { } focus() { - this.inputEditor.focus(); + this._inputEditor.focus(); } updateToggleState(expand: boolean) { @@ -504,6 +518,84 @@ class InteractiveEditorWidget { return !this._elements.previewDiff.classList.contains('hidden') || !this._elements.previewCreate.classList.contains('hidden'); } + + // --- slash commands + + updateSlashCommands(commands: IInteractiveEditorSlashCommand[]) { + + this._slashCommands.clear(); + + if (commands.length === 0) { + return; + } + + const selector: LanguageSelector = { scheme: this._inputModel.uri.scheme, pattern: this._inputModel.uri.path, language: this._inputModel.getLanguageId() }; + this._slashCommands.add(this._languageFeaturesService.completionProvider.register(selector, new class implements CompletionItemProvider { + + _debugDisplayName?: string = 'InteractiveEditorSlashCommandProvider'; + + readonly triggerCharacters?: string[] = ['/']; + + provideCompletionItems(_model: ITextModel, position: Position): ProviderResult { + if (position.lineNumber !== 1 && position.column !== 1) { + return undefined; + } + + const suggestions: CompletionItem[] = commands.map(command => { + + const withSlash = `/${command.command}`; + + return { + label: { label: withSlash, description: command.detail }, + insertText: `${withSlash} $0`, + insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, + kind: CompletionItemKind.Text, + range: new Range(1, 1, 1, 1), + }; + }); + + return { suggestions }; + } + })); + + const decorations = this._inputEditor.createDecorationsCollection(); + + const updateSlashDecorations = () => { + const newDecorations: IModelDeltaDecoration[] = []; + for (const command of commands) { + const withSlash = `/${command.command}`; + const firstLine = this._inputModel.getLineContent(1); + if (firstLine.startsWith(withSlash)) { + newDecorations.push({ + range: new Range(1, 1, 1, withSlash.length + 1), + options: { + description: 'interactive-editor-slash-command', + inlineClassName: 'interactive-editor-slash-command', + } + }); + + // inject detail when otherwise empty + if (firstLine === `/${command.command} `) { + newDecorations.push({ + range: new Range(1, withSlash.length + 1, 1, withSlash.length + 2), + options: { + description: 'interactive-editor-slash-command-detail', + after: { + content: `${command.detail}`, + inlineClassName: 'interactive-editor-slash-command-detail' + } + } + }); + } + break; + } + } + decorations.set(newDecorations); + }; + + this._slashCommands.add(this._inputEditor.onDidChangeModelContent(updateSlashDecorations)); + updateSlashDecorations(); + } } export class InteractiveEditorZoneWidget extends ZoneWidget { @@ -598,10 +690,10 @@ export class InteractiveEditorZoneWidget extends ZoneWidget { // todo@jrieken // UGYLY: we need to restore focus because showing the zone removes and adds it and that // means we loose focus for a bit - const hasFocusNow = this.widget.inputEditor.hasWidgetFocus(); + const hasFocusNow = this.widget._inputEditor.hasWidgetFocus(); super.show(where, this._computeHeightInLines()); if (hasFocusNow) { - this.widget.inputEditor.focus(); + this.widget._inputEditor.focus(); } } From 054c80c978176eeb0a0a7205daee62eb427c2ae2 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 2 May 2023 14:41:34 +0200 Subject: [PATCH 2/8] make IE input widget more imperative, move input-promise logic into provider --- .../browser/interactiveEditorController.ts | 39 +++-- .../browser/interactiveEditorWidget.ts | 148 ++++++------------ 2 files changed, 71 insertions(+), 116 deletions(-) diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts index 6fec6e313ad7c..edfce7aae8ee8 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; -import { raceCancellationError } from 'vs/base/common/async'; +import { DeferredPromise, raceCancellationError } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { isCancellationError } from 'vs/base/common/errors'; @@ -299,6 +299,7 @@ export class InteractiveEditorController implements IEditorContribution { private _inlineDiffEnabled: boolean = false; private _currentSession?: Session; + private _currentInputPromise?: DeferredPromise; private _recordings: Recording[] = []; private _ctsSession: CancellationTokenSource = new CancellationTokenSource(); @@ -374,7 +375,7 @@ export class InteractiveEditorController implements IEditorContribution { return; } - const thisSession = this._ctsSession = new CancellationTokenSource(); + this._ctsSession = new CancellationTokenSource(); const textModel = this._editor.getModel(); const selection = this._editor.getSelection(); const session = await provider.prepareInteractiveEditorSession(textModel, selection, this._ctsSession.token); @@ -416,8 +417,6 @@ export class InteractiveEditorController implements IEditorContribution { options: InteractiveEditorController._decoWholeRange }]); - let placeholder = session.placeholder ?? ''; - let value = options?.message ?? ''; let autoSend = options?.autoSend ?? false; this._zone.widget.updateSlashCommands(session.slashCommands ?? []); @@ -484,7 +483,18 @@ export class InteractiveEditorController implements IEditorContribution { this._ctsRequest = new CancellationTokenSource(this._ctsSession.token); this._historyOffset = -1; - const inputPromise = this._zone.getInput(wholeRange.getEndPosition(), placeholder, value, this._ctsRequest.token, _requestCancelledOnModelContentChanged); + this._zone.widget.placeholder = session.placeholder ?? ''; + this._zone.widget.input = options?.message ?? ''; + if (!_requestCancelledOnModelContentChanged) { + this._zone.widget.selectAll(); + } + + this._zone.show(wholeRange.getEndPosition()); + + this._currentInputPromise = new DeferredPromise(); + this._ctsSession.token.onCancellationRequested(() => { this._currentInputPromise?.complete(); }); + + // const inputPromise = this._zone.getInput(wholeRange.getEndPosition(), placeholder, value, this._ctsRequest.token, _requestCancelledOnModelContentChanged); _requestCancelledOnModelContentChanged = false; if (textModel0Changes && editMode === EditMode.LivePreview) { @@ -501,7 +511,12 @@ export class InteractiveEditorController implements IEditorContribution { autoSend = false; this.accept(); } - const input = await inputPromise; + + await this._currentInputPromise.p; + if (this._ctsSession.token.isCancellationRequested) { + break; + } + const input = this._zone.widget.input; if (!input) { continue; @@ -532,7 +547,7 @@ export class InteractiveEditorController implements IEditorContribution { }; const task = provider.provideResponse(session, request, this._ctsRequest.token); this._logService.trace('[IE] request started', provider.debugName, session, request); - value = input; + // this._zone.widget.input = input; let reply: IInteractiveEditorResponse | null | undefined; try { @@ -577,12 +592,12 @@ export class InteractiveEditorController implements IEditorContribution { this._zone.widget.updateStatus(''); this._zone.widget.updateMarkdownMessage(renderedMarkdown.element); const markdownResponse = new MarkdownResponse(textModel.uri, reply); - this._currentSession.addExchange(new SessionExchange(value, markdownResponse)); + this._currentSession.addExchange(new SessionExchange(input, markdownResponse)); continue; } const editResponse = new EditResponse(textModel.uri, reply); - this._currentSession.addExchange(new SessionExchange(value, editResponse)); + this._currentSession.addExchange(new SessionExchange(input, editResponse)); const canContinue = this._strategy.update(editResponse); if (!canContinue) { @@ -677,9 +692,9 @@ export class InteractiveEditorController implements IEditorContribution { this._zone.widget.hideCreatePreview(); } - placeholder = reply.placeholder ?? session.placeholder ?? ''; + this._zone.widget.placeholder = reply.placeholder ?? session.placeholder ?? ''; - } while (!thisSession.token.isCancellationRequested); + } while (!this._ctsSession.token.isCancellationRequested); this._inlineDiffEnabled = inlineDiffDecorations.visible; this._storageService.store(InteractiveEditorController._inlineDiffStorageKey, this._inlineDiffEnabled, StorageScope.PROFILE, StorageTarget.USER); @@ -747,7 +762,7 @@ export class InteractiveEditorController implements IEditorContribution { } accept(): void { - this._zone.widget.acceptInput(); + this._currentInputPromise?.complete(); } cancelCurrentRequest(): void { diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts index 3e81db50b8271..b84900c40ded4 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts @@ -4,16 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./interactiveEditor'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; +import { IActiveCodeEditor, ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IRange, Range } from 'vs/editor/common/core/range'; import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; -import { assertType } from 'vs/base/common/types'; import { CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST, CTX_INTERACTIVE_EDITOR_EMPTY, CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION, CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET, MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, MENU_INTERACTIVE_EDITOR_WIDGET_MARKDOWN_MESSAGE, CTX_INTERACTIVE_EDITOR_MESSAGE_CROP_STATE, IInteractiveEditorSlashCommand } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; import { Dimension, addDisposableListener, getTotalHeight, getTotalWidth, h, reset } from 'vs/base/browser/dom'; @@ -102,8 +100,6 @@ class InteractiveEditorWidget { private static _modelPool: number = 1; - private static _noop = () => { }; - private readonly _elements = h( 'div.interactive-editor@root', [ @@ -134,7 +130,7 @@ class InteractiveEditorWidget { private readonly _store = new DisposableStore(); private readonly _slashCommands = this._store.add(new DisposableStore()); - readonly _inputEditor: ICodeEditor; + readonly _inputEditor: IActiveCodeEditor; private readonly _inputModel: ITextModel; private readonly _ctxInputEmpty: IContextKey; private readonly _ctxMessageCropState: IContextKey<'cropped' | 'not_cropped' | 'expanded'>; @@ -157,8 +153,6 @@ class InteractiveEditorWidget { private _lastDim: Dimension | undefined; private _isLayouting: boolean = false; - public acceptInput: () => void = InteractiveEditorWidget._noop; - private _cancelInput: () => void = InteractiveEditorWidget._noop; constructor( parentEditor: ICodeEditor, @IModelService private readonly _modelService: IModelService, @@ -176,23 +170,55 @@ class InteractiveEditorWidget { ]) }; - this._inputEditor = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, _inputEditorOptions, codeEditorWidgetOptions, parentEditor); + this._inputEditor = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, _inputEditorOptions, codeEditorWidgetOptions, parentEditor); this._store.add(this._inputEditor); + this._store.add(this._inputEditor.onDidChangeModelContent(() => this._onDidChangeInput.fire(this))); + this._store.add(this._inputEditor.onDidLayoutChange(() => this._onDidChangeHeight.fire())); + this._store.add(this._inputEditor.onDidContentSizeChange(() => this._onDidChangeHeight.fire())); const uri = URI.from({ scheme: 'vscode', authority: 'interactive-editor', path: `/interactive-editor/model${InteractiveEditorWidget._modelPool++}.txt` }); this._inputModel = this._modelService.getModel(uri) ?? this._modelService.createModel('', null, uri); this._inputEditor.setModel(this._inputModel); + // --- context keys - this._store.add(this._inputEditor.onDidChangeModelContent(() => this._onDidChangeInput.fire(this))); + this._ctxMessageCropState = CTX_INTERACTIVE_EDITOR_MESSAGE_CROP_STATE.bindTo(this._contextKeyService); + this._ctxInputEmpty = CTX_INTERACTIVE_EDITOR_EMPTY.bindTo(this._contextKeyService); + + const ctxInnerCursorFirst = CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST.bindTo(this._contextKeyService); + const ctxInnerCursorLast = CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST.bindTo(this._contextKeyService); + const ctxInputEditorFocused = CTX_INTERACTIVE_EDITOR_FOCUSED.bindTo(this._contextKeyService); + + // (1) inner cursor position (last/first line selected) + const updateInnerCursorFirstLast = () => { + const { lineNumber } = this._inputEditor.getPosition(); + ctxInnerCursorFirst.set(lineNumber === 1); + ctxInnerCursorLast.set(lineNumber === this._inputModel.getLineCount()); + }; + this._store.add(this._inputEditor.onDidChangeCursorPosition(updateInnerCursorFirstLast)); + updateInnerCursorFirstLast(); + + // (2) input editor focused or not + const updateFocused = () => { + const hasFocus = this._inputEditor.hasWidgetFocus(); + ctxInputEditorFocused.set(hasFocus); + this._elements.content.classList.toggle('synthetic-focus', hasFocus); + }; + this._store.add(this._inputEditor.onDidFocusEditorWidget(updateFocused)); + this._store.add(this._inputEditor.onDidBlurEditorWidget(updateFocused)); + updateFocused(); + + // placeholder + + this._elements.placeholder.style.fontSize = `${this._inputEditor.getOption(EditorOption.fontSize)}px`; + this._elements.placeholder.style.lineHeight = `${this._inputEditor.getOption(EditorOption.lineHeight)}px`; + this._store.add(addDisposableListener(this._elements.placeholder, 'click', () => this._inputEditor.focus())); // show/hide placeholder depending on text model being empty // content height const currentContentHeight = 0; - this._ctxMessageCropState = CTX_INTERACTIVE_EDITOR_MESSAGE_CROP_STATE.bindTo(this._contextKeyService); - this._ctxInputEmpty = CTX_INTERACTIVE_EDITOR_EMPTY.bindTo(this._contextKeyService); const togglePlaceholder = () => { const hasText = this._inputModel.getValueLength() > 0; this._elements.placeholder.classList.toggle('hidden', hasText); @@ -208,7 +234,7 @@ class InteractiveEditorWidget { this._store.add(this._inputModel.onDidChangeContent(togglePlaceholder)); togglePlaceholder(); - this._store.add(addDisposableListener(this._elements.placeholder, 'click', () => this._inputEditor.focus())); + // toolbars const toolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, MENU_INTERACTIVE_EDITOR_WIDGET, { telemetrySource: 'interactiveEditorWidget-toolbar', @@ -299,97 +325,15 @@ class InteractiveEditorWidget { set input(value: string) { this._inputModel.setValue(value); + this._inputEditor.setPosition(this._inputModel.getFullModelRange().getEndPosition()); } selectAll() { this._inputEditor.setSelection(this._inputModel.getFullModelRange()); } - getInput(placeholder: string, value: string, token: CancellationToken, requestCancelledOnModelContentChanged: boolean): Promise { - - const currentInputEditorPosition = this._inputEditor.getPosition(); - const currentInputEditorValue = this._inputEditor.getValue(); - - this._elements.placeholder.innerText = placeholder; - this._elements.placeholder.style.fontSize = `${this._inputEditor.getOption(EditorOption.fontSize)}px`; - this._elements.placeholder.style.lineHeight = `${this._inputEditor.getOption(EditorOption.lineHeight)}px`; - - this._inputModel.setValue(requestCancelledOnModelContentChanged ? currentInputEditorValue : value); - const fullInputModelRange = this._inputModel.getFullModelRange(); - - if (requestCancelledOnModelContentChanged) { - this._inputEditor.setPosition(currentInputEditorPosition ?? new Position(fullInputModelRange.endLineNumber, fullInputModelRange.endColumn)); - } else { - this._inputEditor.setSelection(fullInputModelRange); - } - this._inputEditor.updateOptions({ ariaLabel: localize('aria-label.N', "Interactive Editor Input: {0}", placeholder) }); - - const disposeOnDone = new DisposableStore(); - - disposeOnDone.add(this._inputEditor.onDidLayoutChange(() => this._onDidChangeHeight.fire())); - disposeOnDone.add(this._inputEditor.onDidContentSizeChange(() => this._onDidChangeHeight.fire())); - - const ctxInnerCursorFirst = CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST.bindTo(this._contextKeyService); - const ctxInnerCursorLast = CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST.bindTo(this._contextKeyService); - const ctxInputEditorFocused = CTX_INTERACTIVE_EDITOR_FOCUSED.bindTo(this._contextKeyService); - - return new Promise(resolve => { - - this._cancelInput = () => { - this.acceptInput = InteractiveEditorWidget._noop; - this._cancelInput = InteractiveEditorWidget._noop; - resolve(undefined); - return true; - }; - - this.acceptInput = () => { - const newValue = this._inputEditor.getModel()!.getValue(); - if (newValue.trim().length === 0) { - // empty or whitespace only - this._cancelInput(); - return; - } - - this.acceptInput = InteractiveEditorWidget._noop; - this._cancelInput = InteractiveEditorWidget._noop; - resolve(newValue); - }; - - disposeOnDone.add(token.onCancellationRequested(() => this._cancelInput())); - - // CONTEXT KEYS - - // (1) inner cursor position (last/first line selected) - const updateInnerCursorFirstLast = () => { - if (!this._inputEditor.hasModel()) { - return; - } - const { lineNumber } = this._inputEditor.getPosition(); - ctxInnerCursorFirst.set(lineNumber === 1); - ctxInnerCursorLast.set(lineNumber === this._inputEditor.getModel().getLineCount()); - }; - disposeOnDone.add(this._inputEditor.onDidChangeCursorPosition(updateInnerCursorFirstLast)); - updateInnerCursorFirstLast(); - - // (2) input editor focused or not - const updateFocused = () => { - const hasFocus = this._inputEditor.hasWidgetFocus(); - ctxInputEditorFocused.set(hasFocus); - this._elements.content.classList.toggle('synthetic-focus', hasFocus); - }; - disposeOnDone.add(this._inputEditor.onDidFocusEditorWidget(updateFocused)); - disposeOnDone.add(this._inputEditor.onDidBlurEditorWidget(updateFocused)); - updateFocused(); - - this.focus(); - - }).finally(() => { - disposeOnDone.dispose(); - - ctxInnerCursorFirst.reset(); - ctxInnerCursorLast.reset(); - ctxInputEditorFocused.reset(); - }); + set placeholder(value: string) { + this._elements.placeholder.innerText = value; } updateToolbar(show: boolean) { @@ -676,14 +620,10 @@ export class InteractiveEditorZoneWidget extends ZoneWidget { super._relayout(this._computeHeightInLines()); } - async getInput(where: IPosition, placeholder: string, value: string, token: CancellationToken, requestCancelledOnModelContentChanged: boolean): Promise { - assertType(this.editor.hasModel()); + override show(where: IPosition): void { super.show(where, this._computeHeightInLines()); + this.widget.focus(); this._ctxVisible.set(true); - - const task = this.widget.getInput(placeholder, value, token, requestCancelledOnModelContentChanged); - const result = await task; - return result; } updatePosition(where: IPosition) { From 5a8e4b68dc8447ebe68a7a4fd6492c9f045d187d Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 2 May 2023 15:37:04 +0200 Subject: [PATCH 3/8] enable diff revert commands for livePreview diff --- .../interactiveEditor/browser/interactiveEditorDiffWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget.ts index 459844f3dd8fe..dbb95e7fc59cb 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget.ts @@ -54,7 +54,7 @@ export class InteractiveEditorDiffWidget extends ZoneWidget { this._diffEditor = instantiationService.createInstance(EmbeddedDiffEditorWidget, this._elements.domNode, { scrollbar: { useShadows: false, alwaysConsumeMouseWheel: false }, scrollBeyondLastLine: false, - renderMarginRevertIcon: false, + renderMarginRevertIcon: true, renderOverviewRuler: false, rulers: undefined, overviewRulerBorder: undefined, From da92a4453ba1d599a5971b06d53cf125e4c6a1b3 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 2 May 2023 15:44:47 +0200 Subject: [PATCH 4/8] allow for unlimited height of inline diff zone --- src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts | 8 +++++--- .../browser/interactiveEditorDiffWidget.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts b/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts index 555877f6ac0cc..40d093fda352c 100644 --- a/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts +++ b/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts @@ -28,7 +28,7 @@ export interface IOptions { frameColor?: Color; arrowColor?: Color; keepEditorSelection?: boolean; - + allowUnlimitedHeight?: boolean; ordinal?: number; showInHiddenAreas?: boolean; } @@ -363,8 +363,10 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { const lineHeight = this.editor.getOption(EditorOption.lineHeight); // adjust heightInLines to viewport - const maxHeightInLines = Math.max(12, (this.editor.getLayoutInfo().height / lineHeight) * 0.8); - heightInLines = Math.min(heightInLines, maxHeightInLines); + if (!this.options.allowUnlimitedHeight) { + const maxHeightInLines = Math.max(12, (this.editor.getLayoutInfo().height / lineHeight) * 0.8); + heightInLines = Math.min(heightInLines, maxHeightInLines); + } let arrowHeight = 0; let frameThickness = 0; diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget.ts index dbb95e7fc59cb..7c004d2a07289 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget.ts @@ -42,7 +42,7 @@ export class InteractiveEditorDiffWidget extends ZoneWidget { @IThemeService themeService: IThemeService, @ILogService private readonly _logService: ILogService, ) { - super(editor, { showArrow: false, showFrame: false, isResizeable: false, isAccessible: true, showInHiddenAreas: true, ordinal: 10000 + 1 }); + super(editor, { showArrow: false, showFrame: false, isResizeable: false, isAccessible: true, allowUnlimitedHeight: true, showInHiddenAreas: true, ordinal: 10000 + 1 }); super.create(); this._inlineDiffDecorations = editor.createDecorationsCollection(); From 525886f26824181950a067fa8b91fc386fe27c51 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 2 May 2023 17:31:14 +0200 Subject: [PATCH 5/8] =?UTF-8?q?move=20editMode=20logic=20into=20strategies?= =?UTF-8?q?,=20no=20more=20if-else=20=F0=9F=91=AF=E2=80=8D=E2=99=82?= =?UTF-8?q?=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../browser/interactiveEditorController.ts | 302 ++++++++++-------- .../browser/interactiveEditorWidget.ts | 2 +- 2 files changed, 170 insertions(+), 134 deletions(-) diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts index edfce7aae8ee8..1964b21b10e68 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts @@ -15,9 +15,9 @@ import { isEqual } from 'vs/base/common/resources'; import { StopWatch } from 'vs/base/common/stopwatch'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./interactiveEditor'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IBulkEditService, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; -import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -38,7 +38,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { InteractiveEditorDiffWidget } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget'; -import { InteractiveEditorZoneWidget } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget'; +import { InteractiveEditorWidget, InteractiveEditorZoneWidget } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget'; import { CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, CTX_INTERACTIVE_EDITOR_INLNE_DIFF, CTX_INTERACTIVE_EDITOR_LAST_EDIT_TYPE as CTX_INTERACTIVE_EDITOR_LAST_EDIT_KIND, CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK as CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK_KIND, IInteractiveEditorBulkEditResponse, IInteractiveEditorEditResponse, IInteractiveEditorRequest, IInteractiveEditorResponse, IInteractiveEditorService, IInteractiveEditorSession, IInteractiveEditorSessionProvider, INTERACTIVE_EDITOR_ID, EditMode, InteractiveEditorResponseFeedbackKind, CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE, InteractiveEditorResponseType, IInteractiveEditorMessageResponse } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; import { IInteractiveSessionWidgetService } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget'; import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService'; @@ -270,7 +270,6 @@ export class InteractiveEditorController implements IEditorContribution { return editor.getContribution(INTERACTIVE_EDITOR_ID); } - private static _inlineDiffStorageKey: string = 'interactiveEditor.storage.inlineDiff'; private static _decoBlock = ModelDecorationOptions.register({ description: 'interactive-editor', @@ -289,14 +288,11 @@ export class InteractiveEditorController implements IEditorContribution { private readonly _store = new DisposableStore(); private readonly _zone: InteractiveEditorZoneWidget; private readonly _ctxHasActiveRequest: IContextKey; - private readonly _ctxInlineDiff: IContextKey; private readonly _ctxLastResponseType: IContextKey; private readonly _ctxLastEditKind: IContextKey<'' | 'simple'>; private readonly _ctxLastFeedbackKind: IContextKey<'helpful' | 'unhelpful' | ''>; private _strategy?: EditModeStrategy; - private _lastInlineDecorations?: InlineDiffDecorations; - private _inlineDiffEnabled: boolean = false; private _currentSession?: Session; private _currentInputPromise?: DeferredPromise; @@ -312,7 +308,6 @@ export class InteractiveEditorController implements IEditorContribution { @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, @ILogService private readonly _logService: ILogService, @ITelemetryService private readonly _telemetryService: ITelemetryService, - @IStorageService private readonly _storageService: IStorageService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IModelService private readonly _modelService: IModelService, @ITextModelService private readonly _textModelService: ITextModelService, @@ -321,12 +316,10 @@ export class InteractiveEditorController implements IEditorContribution { ) { this._ctxHasActiveRequest = CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST.bindTo(contextKeyService); - this._ctxInlineDiff = CTX_INTERACTIVE_EDITOR_INLNE_DIFF.bindTo(contextKeyService); this._ctxLastEditKind = CTX_INTERACTIVE_EDITOR_LAST_EDIT_KIND.bindTo(contextKeyService); this._ctxLastResponseType = CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE.bindTo(contextKeyService); this._ctxLastFeedbackKind = CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK_KIND.bindTo(contextKeyService); this._zone = this._store.add(_instaService.createInstance(InteractiveEditorZoneWidget, this._editor)); - } dispose(): void { @@ -360,7 +353,6 @@ export class InteractiveEditorController implements IEditorContribution { // hide/cancel inline completions when invoking IE InlineCompletionsController.get(this._editor)?.hide(); - const editMode = this._getMode(); this._ctsSession.dispose(true); this._cancelNotebookSiblingEditors(); @@ -397,13 +389,22 @@ export class InteractiveEditorController implements IEditorContribution { let textModel0Changes: LineRangeMapping[] | undefined; + const editMode = this._getMode(); this._currentSession = new Session(editMode, textModel0, textModel, provider, session); - this._strategy = this._instaService.createInstance(EditModeStrategy.ctor(editMode), this._currentSession); - this._inlineDiffEnabled = this._storageService.getBoolean(InteractiveEditorController._inlineDiffStorageKey, StorageScope.PROFILE, false); - const inlineDiffDecorations = new InlineDiffDecorations(this._editor, this._inlineDiffEnabled); - this._lastInlineDecorations = inlineDiffDecorations; - this._ctxInlineDiff.set(this._inlineDiffEnabled); + switch (editMode) { + case EditMode.Live: + this._strategy = this._instaService.createInstance(LiveStrategy, this._currentSession, this._editor, this._zone.widget); + break; + case EditMode.LivePreview: + this._strategy = this._instaService.createInstance(LivePreviewStrategy, this._currentSession, this._editor, this._zone.widget, + () => wholeRangeDecoration.getRange(0)!, // TODO@jrieken if it can be null it will be null + ); + break; + case EditMode.Preview: + this._strategy = this._instaService.createInstance(PreviewStrategy, this._currentSession, this._zone.widget); + break; + } const blockDecoration = this._editor.createDecorationsCollection(); const wholeRangeDecoration = this._editor.createDecorationsCollection(); @@ -425,25 +426,23 @@ export class InteractiveEditorController implements IEditorContribution { // CANCEL when input changes this._editor.onDidChangeModel(this.cancelSession, this, store); - if (editMode === EditMode.Live) { + // if (editMode === EditMode.Live) { - // REposition the zone widget whenever the block decoration changes - let lastPost: Position | undefined; - wholeRangeDecoration.onDidChange(e => { - const range = wholeRangeDecoration.getRange(0); - if (range && (!lastPost || !lastPost.equals(range.getEndPosition()))) { - lastPost = range.getEndPosition(); - this._zone.updatePosition(lastPost); - } - }, undefined, store); - } + // // REposition the zone widget whenever the block decoration changes + // let lastPost: Position | undefined; + // wholeRangeDecoration.onDidChange(e => { + // const range = wholeRangeDecoration.getRange(0); + // if (range && (!lastPost || !lastPost.equals(range.getEndPosition()))) { + // lastPost = range.getEndPosition(); + // this._zone.updatePosition(lastPost); + // } + // }, undefined, store); + // } let ignoreModelChanges = false; this._editor.onDidChangeModelContent(e => { if (!ignoreModelChanges) { - // remove inline diff when the model changes - inlineDiffDecorations.clear(); // note when "other" edits happen this._currentSession?.recordExternalEditOccurred(); @@ -459,7 +458,6 @@ export class InteractiveEditorController implements IEditorContribution { }, undefined, store); - const diffZone = this._instaService.createInstance(InteractiveEditorDiffWidget, this._editor, textModel0); let _requestCancelledOnModelContentChanged = false; @@ -497,13 +495,7 @@ export class InteractiveEditorController implements IEditorContribution { // const inputPromise = this._zone.getInput(wholeRange.getEndPosition(), placeholder, value, this._ctsRequest.token, _requestCancelledOnModelContentChanged); _requestCancelledOnModelContentChanged = false; - if (textModel0Changes && editMode === EditMode.LivePreview) { - diffZone.showDiff( - () => wholeRangeDecoration.getRange(0)!, // TODO@jrieken if it can be null it will be null - textModel0Changes - ); - } this._ctxLastFeedbackKind.reset(); // reveal the line after the whole range to ensure that the input box is visible this._editor.revealPosition({ lineNumber: wholeRange.endLineNumber + 1, column: 1 }, ScrollType.Smooth); @@ -599,16 +591,13 @@ export class InteractiveEditorController implements IEditorContribution { const editResponse = new EditResponse(textModel.uri, reply); this._currentSession.addExchange(new SessionExchange(input, editResponse)); - const canContinue = this._strategy.update(editResponse); + const canContinue = this._strategy.checkChanges(editResponse); if (!canContinue) { break; } this._ctxLastEditKind.set(editResponse.localEdits.length === 1 ? 'simple' : ''); - // inline diff - inlineDiffDecorations.clear(); - // use whole range from reply if (reply.wholeRange) { @@ -617,75 +606,23 @@ export class InteractiveEditorController implements IEditorContribution { options: InteractiveEditorController._decoWholeRange }]); } + const moreMinimalEdits = (await this._editorWorkerService.computeHumanReadableDiff(textModel.uri, editResponse.localEdits)); + const editOperations = (moreMinimalEdits ?? editResponse.localEdits).map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)); + this._logService.trace('[IE] edits from PROVIDER and after making them MORE MINIMAL', provider.debugName, editResponse.localEdits, moreMinimalEdits); - if (editMode === EditMode.Preview) { - // only preview changes - if (editResponse.localEdits.length > 0) { - this._zone.widget.showEditsPreview(textModel, editResponse.localEdits); - } else { - this._zone.widget.hideEditsPreview(); - } + const textModelNplus1 = this._modelService.createModel(createTextBufferFactoryFromSnapshot(textModel.createSnapshot()), null, undefined, true); + textModelNplus1.applyEdits(editOperations); + const diff = await this._editorWorkerService.computeDiff(textModel0.uri, textModelNplus1.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: 5000 }, 'advanced'); + textModel0Changes = diff?.changes ?? []; + textModelNplus1.dispose(); - } else { - // make edits more minimal - - const moreMinimalEdits = (await this._editorWorkerService.computeHumanReadableDiff(textModel.uri, editResponse.localEdits)); - const editOperations = (moreMinimalEdits ?? editResponse.localEdits).map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)); - this._logService.trace('[IE] edits from PROVIDER and after making them MORE MINIMAL', provider.debugName, editResponse.localEdits, moreMinimalEdits); - - const textModelNplus1 = this._modelService.createModel(createTextBufferFactoryFromSnapshot(textModel.createSnapshot()), null, undefined, true); - textModelNplus1.applyEdits(editOperations); - const diff = await this._editorWorkerService.computeDiff(textModel0.uri, textModelNplus1.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: 5000 }, 'advanced'); - textModel0Changes = diff?.changes ?? []; - textModelNplus1.dispose(); - - try { - ignoreModelChanges = true; - - 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; - inlineDiffDecorations.collectEditOperation(edit); - } - return last && [Selection.fromPositions(last)]; - }; - - this._editor.pushUndoStop(); - this._editor.executeEdits( - 'interactive-editor', - editOperations, - cursorStateComputerAndInlineDiffCollection - ); - this._editor.pushUndoStop(); - - } finally { - ignoreModelChanges = false; - } - - if (editMode === EditMode.Live) { - inlineDiffDecorations.update(); - } - - // summary message - let linesChanged = 0; - if (textModel0Changes) { - for (const change of textModel0Changes) { - linesChanged += change.changedLineCount; - } - } - let message: string; - if (linesChanged === 0) { - message = localize('lines.0', "Generated reply"); - } else if (linesChanged === 1) { - message = localize('lines.1', "Generated reply and changed 1 line."); - } else { - message = localize('lines.N', "Generated reply and changed {0} lines.", linesChanged); - } - this._zone.widget.updateStatus(message); + try { + ignoreModelChanges = true; + this._strategy.renderChanges(editOperations, textModel0Changes); + } finally { + ignoreModelChanges = false; } - if (editResponse.singleCreateFileEdit) { this._zone.widget.showCreatePreview(editResponse.singleCreateFileEdit.uri, await Promise.all(editResponse.singleCreateFileEdit.edits)); } else { @@ -696,9 +633,6 @@ export class InteractiveEditorController implements IEditorContribution { } while (!this._ctsSession.token.isCancellationRequested); - this._inlineDiffEnabled = inlineDiffDecorations.visible; - this._storageService.store(InteractiveEditorController._inlineDiffStorageKey, this._inlineDiffEnabled, StorageScope.PROFILE, StorageTarget.USER); - this._logService.trace('[IE] session DONE', provider.debugName); this._telemetryService.publicLog2('interactiveEditor/session', this._currentSession.asTelemetryData()); @@ -710,12 +644,8 @@ export class InteractiveEditorController implements IEditorContribution { } // done, cleanup - diffZone.hide(); - diffZone.dispose(); - wholeRangeDecoration.clear(); blockDecoration.clear(); - inlineDiffDecorations.clear(); store.dispose(); session.dispose?.(); @@ -724,7 +654,6 @@ export class InteractiveEditorController implements IEditorContribution { this._ctxLastResponseType.reset(); this._ctxLastFeedbackKind.reset(); this._currentSession = undefined; - this._lastInlineDecorations = undefined; this._zone.hide(); this._editor.focus(); @@ -780,12 +709,7 @@ export class InteractiveEditorController implements IEditorContribution { } toggleInlineDiff(): void { - this._inlineDiffEnabled = !this._inlineDiffEnabled; - this._ctxInlineDiff.set(this._inlineDiffEnabled); - this._storageService.store(InteractiveEditorController._inlineDiffStorageKey, this._inlineDiffEnabled, StorageScope.PROFILE, StorageTarget.USER); - if (this._lastInlineDecorations) { - this._lastInlineDecorations.visible = this._inlineDiffEnabled; - } + this._strategy?.toggleInlineDiff(); } focus(): void { @@ -830,6 +754,7 @@ export class InteractiveEditorController implements IEditorContribution { const strategy = this._strategy; this._strategy = undefined; await strategy?.apply(); + strategy?.dispose(); this._ctsSession.cancel(); return this._currentSession.lastExchange.response; } @@ -839,37 +764,37 @@ export class InteractiveEditorController implements IEditorContribution { const strategy = this._strategy; this._strategy = undefined; await strategy?.cancel(); + strategy?.dispose(); this._ctsSession.cancel(); } } abstract class EditModeStrategy { - static ctor(mode: EditMode) { - switch (mode) { - case EditMode.Live: return LiveStrategy; - case EditMode.LivePreview: return LivePreviewStrategy; - case EditMode.Preview: return PreviewStrategy; - } - } + dispose(): void { } - abstract update(response: EditResponse): boolean; + abstract checkChanges(response: EditResponse): boolean; abstract apply(): Promise; abstract cancel(): Promise; + + abstract renderChanges(edits: ISingleEditOperation[], changes: LineRangeMapping[]): void; + + abstract toggleInlineDiff(): void; } class PreviewStrategy extends EditModeStrategy { constructor( - protected readonly _session: Session, + private readonly _session: Session, + private readonly _widget: InteractiveEditorWidget, @IBulkEditService private readonly _bulkEditService: IBulkEditService, ) { super(); } - update(response: EditResponse): boolean { + checkChanges(response: EditResponse): boolean { if (!response.workspaceEdits || response.singleCreateFileEdit) { // preview stategy can handle simple workspace edit (single file create) return true; @@ -903,19 +828,62 @@ class PreviewStrategy extends EditModeStrategy { async cancel(): Promise { // nothing to do } + + override renderChanges(edits: ISingleEditOperation[], changes: LineRangeMapping[]): void { + const response = this._session.lastExchange?.response; + if (response instanceof EditResponse && response.localEdits.length > 0) { + this._widget.showEditsPreview(this._session.modelN, response.localEdits); + } else { + this._widget.hideEditsPreview(); + } + } + + toggleInlineDiff(): void { } } class LiveStrategy extends EditModeStrategy { + private static _inlineDiffStorageKey: string = 'interactiveEditor.storage.inlineDiff'; + private _inlineDiffEnabled: boolean = false; + + private readonly _inlineDiffDecorations: InlineDiffDecorations; + private readonly _ctxInlineDiff: IContextKey; + constructor( protected readonly _session: Session, + protected readonly _editor: IActiveCodeEditor, + protected readonly _widget: InteractiveEditorWidget, + @IContextKeyService contextKeyService: IContextKeyService, + @IStorageService protected _storageService: IStorageService, @IBulkEditService protected readonly _bulkEditService: IBulkEditService, @IEditorWorkerService protected readonly _editorWorkerService: IEditorWorkerService ) { super(); + this._inlineDiffDecorations = new InlineDiffDecorations(this._editor, this._inlineDiffEnabled); + this._ctxInlineDiff = CTX_INTERACTIVE_EDITOR_INLNE_DIFF.bindTo(contextKeyService); + + this._inlineDiffEnabled = _storageService.getBoolean(LiveStrategy._inlineDiffStorageKey, StorageScope.PROFILE, false); + this._ctxInlineDiff.set(this._inlineDiffEnabled); + this._inlineDiffDecorations.visible = this._inlineDiffEnabled; + } + + override dispose(): void { + this._inlineDiffEnabled = this._inlineDiffDecorations.visible; + this._storageService.store(LiveStrategy._inlineDiffStorageKey, this._inlineDiffEnabled, StorageScope.PROFILE, StorageTarget.USER); + this._inlineDiffDecorations.clear(); + this._ctxInlineDiff.reset(); + + super.dispose(); } - update(response: EditResponse): boolean { + toggleInlineDiff(): void { + this._inlineDiffEnabled = !this._inlineDiffEnabled; + this._ctxInlineDiff.set(this._inlineDiffEnabled); + this._inlineDiffDecorations.visible = this._inlineDiffEnabled; + this._storageService.store(LiveStrategy._inlineDiffStorageKey, this._inlineDiffEnabled, StorageScope.PROFILE, StorageTarget.USER); + } + + checkChanges(response: EditResponse): boolean { if (response.workspaceEdits) { this._bulkEditService.apply(response.workspaceEdits, { showPreview: true }); return false; @@ -938,19 +906,77 @@ class LiveStrategy extends EditModeStrategy { modelN.pushEditOperations(null, operations, () => null); } } + + override renderChanges(edits: ISingleEditOperation[], textModel0Changes: LineRangeMapping[]): void { + + 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)]; + }; + + this._editor.pushUndoStop(); + this._editor.executeEdits('interactive-editor-live', edits, cursorStateComputerAndInlineDiffCollection); + this._editor.pushUndoStop(); + this._inlineDiffDecorations.update(); + this._updateSummaryMessage(textModel0Changes); + } + + protected _updateSummaryMessage(textModel0Changes: LineRangeMapping[]) { + let linesChanged = 0; + if (textModel0Changes) { + for (const change of textModel0Changes) { + linesChanged += change.changedLineCount; + } + } + let message: string; + if (linesChanged === 0) { + message = localize('lines.0', "Generated reply"); + } else if (linesChanged === 1) { + message = localize('lines.1', "Generated reply and changed 1 line."); + } else { + message = localize('lines.N', "Generated reply and changed {0} lines.", linesChanged); + } + this._widget.updateStatus(message); + } } class LivePreviewStrategy extends LiveStrategy { private _lastResponse?: EditResponse; + private readonly _diffZone: InteractiveEditorDiffWidget; + + constructor( + session: Session, + editor: IActiveCodeEditor, + widget: InteractiveEditorWidget, + private _getWholeRange: () => Range, + @IContextKeyService contextKeyService: IContextKeyService, + @IStorageService storageService: IStorageService, + @IBulkEditService bulkEditService: IBulkEditService, + @IEditorWorkerService editorWorkerService: IEditorWorkerService, + @IInstantiationService instaService: IInstantiationService, + ) { + super(session, editor, widget, contextKeyService, storageService, bulkEditService, editorWorkerService); - override update(response: EditResponse): boolean { + this._diffZone = instaService.createInstance(InteractiveEditorDiffWidget, editor, session.model0); + } + + override dispose(): void { + this._diffZone.hide(); + this._diffZone.dispose(); + } + + override checkChanges(response: EditResponse): boolean { this._lastResponse = response; if (response.singleCreateFileEdit) { // preview stategy can handle simple workspace edit (single file create) return true; } - return super.update(response); + return super.checkChanges(response); } override async apply() { @@ -958,6 +984,16 @@ class LivePreviewStrategy extends LiveStrategy { await this._bulkEditService.apply(this._lastResponse.workspaceEdits); } } + + override renderChanges(edits: ISingleEditOperation[], changes: LineRangeMapping[]): void { + + this._editor.pushUndoStop(); + this._editor.executeEdits('interactive-editor-livePreview', edits); + this._editor.pushUndoStop(); + + this._diffZone.showDiff(() => this._getWholeRange(), changes); + this._updateSummaryMessage(changes); + } } async function showMessageResponse(accessor: ServicesAccessor, query: string, response: string) { @@ -967,7 +1003,7 @@ async function showMessageResponse(accessor: ServicesAccessor, query: string, re const interactiveSessionWidgetService = accessor.get(IInteractiveSessionWidgetService); const widget = await interactiveSessionWidgetService.revealViewForProvider(providerId); if (widget && widget.viewModel) { - await interactiveSessionService.addCompleteRequest(widget.viewModel.sessionId, query, { message: response }); + interactiveSessionService.addCompleteRequest(widget.viewModel.sessionId, query, { message: response }); widget.focusLastMessage(); } } diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts index b84900c40ded4..fd320bc1d3a7f 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts @@ -96,7 +96,7 @@ const _previewEditorEditorOptions: IDiffEditorConstructionOptions = { readOnly: true, }; -class InteractiveEditorWidget { +export class InteractiveEditorWidget { private static _modelPool: number = 1; From 7c8af09198966a33dbb359168122051994479127 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 2 May 2023 18:15:51 +0200 Subject: [PATCH 6/8] add missing `super.dispose`-call --- .../interactiveEditor/browser/interactiveEditorController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts index 1964b21b10e68..d7889aeeab5d7 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts @@ -968,6 +968,7 @@ class LivePreviewStrategy extends LiveStrategy { override dispose(): void { this._diffZone.hide(); this._diffZone.dispose(); + super.dispose(); } override checkChanges(response: EditResponse): boolean { From 950c6f0ed3a20e6867111e7b1413dc76f1ac2241 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 3 May 2023 09:01:32 +0200 Subject: [PATCH 7/8] enable/disable apply btn for preview mode --- .../browser/interactiveEditorActions.ts | 4 ++-- .../browser/interactiveEditorController.ts | 19 +++++++++++++++++-- .../common/interactiveEditor.ts | 1 + 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts index 88c6d103c6ca1..4a37696e4c8a4 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts @@ -10,7 +10,7 @@ import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { InteractiveEditorController, InteractiveEditorRunOptions, Recording } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController'; -import { CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, CTX_INTERACTIVE_EDITOR_HAS_PROVIDER, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST, CTX_INTERACTIVE_EDITOR_EMPTY, CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION, CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET, CTX_INTERACTIVE_EDITOR_LAST_EDIT_TYPE, MENU_INTERACTIVE_EDITOR_WIDGET_UNDO, MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK, CTX_INTERACTIVE_EDITOR_INLNE_DIFF, CTX_INTERACTIVE_EDITOR_EDIT_MODE, EditMode, CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE, MENU_INTERACTIVE_EDITOR_WIDGET_MARKDOWN_MESSAGE, CTX_INTERACTIVE_EDITOR_MESSAGE_CROP_STATE } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; +import { CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, CTX_INTERACTIVE_EDITOR_HAS_PROVIDER, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST, CTX_INTERACTIVE_EDITOR_EMPTY, CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION, CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET, CTX_INTERACTIVE_EDITOR_LAST_EDIT_TYPE, MENU_INTERACTIVE_EDITOR_WIDGET_UNDO, MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK, CTX_INTERACTIVE_EDITOR_INLNE_DIFF, CTX_INTERACTIVE_EDITOR_EDIT_MODE, EditMode, CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE, MENU_INTERACTIVE_EDITOR_WIDGET_MARKDOWN_MESSAGE, CTX_INTERACTIVE_EDITOR_MESSAGE_CROP_STATE, CTX_INTERACTIVE_EDITOR_DOCUMENT_CHANGED } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; import { localize } from 'vs/nls'; import { IAction2Options } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -407,7 +407,7 @@ export class ApplyPreviewEdits extends AbstractInteractiveEditorAction { id: 'interactiveEditor.applyEdits', title: localize('applyEdits', 'Apply Changes'), icon: Codicon.check, - precondition: CTX_INTERACTIVE_EDITOR_VISIBLE, + precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE, ContextKeyExpr.or(CTX_INTERACTIVE_EDITOR_DOCUMENT_CHANGED.toNegated(), CTX_INTERACTIVE_EDITOR_EDIT_MODE.notEqualsTo(EditMode.Preview))), keybinding: { weight: KeybindingWeight.EditorContrib + 10, primary: KeyMod.CtrlCmd | KeyCode.Enter diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts index d7889aeeab5d7..f9d6eb8534a06 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts @@ -10,7 +10,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { isCancellationError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { isEqual } from 'vs/base/common/resources'; import { StopWatch } from 'vs/base/common/stopwatch'; import { URI } from 'vs/base/common/uri'; @@ -39,7 +39,7 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { InteractiveEditorDiffWidget } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget'; import { InteractiveEditorWidget, InteractiveEditorZoneWidget } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget'; -import { CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, CTX_INTERACTIVE_EDITOR_INLNE_DIFF, CTX_INTERACTIVE_EDITOR_LAST_EDIT_TYPE as CTX_INTERACTIVE_EDITOR_LAST_EDIT_KIND, CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK as CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK_KIND, IInteractiveEditorBulkEditResponse, IInteractiveEditorEditResponse, IInteractiveEditorRequest, IInteractiveEditorResponse, IInteractiveEditorService, IInteractiveEditorSession, IInteractiveEditorSessionProvider, INTERACTIVE_EDITOR_ID, EditMode, InteractiveEditorResponseFeedbackKind, CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE, InteractiveEditorResponseType, IInteractiveEditorMessageResponse } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; +import { CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, CTX_INTERACTIVE_EDITOR_INLNE_DIFF, CTX_INTERACTIVE_EDITOR_LAST_EDIT_TYPE as CTX_INTERACTIVE_EDITOR_LAST_EDIT_KIND, CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK as CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK_KIND, IInteractiveEditorBulkEditResponse, IInteractiveEditorEditResponse, IInteractiveEditorRequest, IInteractiveEditorResponse, IInteractiveEditorService, IInteractiveEditorSession, IInteractiveEditorSessionProvider, INTERACTIVE_EDITOR_ID, EditMode, InteractiveEditorResponseFeedbackKind, CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE, InteractiveEditorResponseType, IInteractiveEditorMessageResponse, CTX_INTERACTIVE_EDITOR_DOCUMENT_CHANGED } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; import { IInteractiveSessionWidgetService } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget'; import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; @@ -786,12 +786,27 @@ abstract class EditModeStrategy { class PreviewStrategy extends EditModeStrategy { + private readonly _ctxDocumentChanged: IContextKey; + private readonly _listener: IDisposable; + constructor( private readonly _session: Session, private readonly _widget: InteractiveEditorWidget, + @IContextKeyService contextKeyService: IContextKeyService, @IBulkEditService private readonly _bulkEditService: IBulkEditService, ) { super(); + + this._ctxDocumentChanged = CTX_INTERACTIVE_EDITOR_DOCUMENT_CHANGED.bindTo(contextKeyService); + this._listener = Event.debounce(_session.modelN.onDidChangeContent.bind(_session.modelN), () => { }, 350)(_ => { + this._ctxDocumentChanged.set(!_session.modelN.equalsTextBuffer(_session.model0.getTextBuffer())); + }); + } + + override dispose(): void { + this._listener.dispose(); + this._ctxDocumentChanged.reset(); + super.dispose(); } checkChanges(response: EditResponse): boolean { diff --git a/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts b/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts index 6741613f2a81d..3a6ae27e9eb6c 100644 --- a/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts @@ -114,6 +114,7 @@ export const CTX_INTERACTIVE_EDITOR_INLNE_DIFF = new RawContextKey('int export const CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE = new RawContextKey('interactiveEditorLastResponseType', undefined, localize('interactiveEditorResponseType', "What type was the last response of the current interactive editor session")); export const CTX_INTERACTIVE_EDITOR_LAST_EDIT_TYPE = new RawContextKey<'simple' | ''>('interactiveEditorLastEditKind', '', localize('interactiveEditorLastEditKind', "The last kind of edit that was performed")); export const CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK = new RawContextKey<'unhelpful' | 'helpful' | ''>('interactiveEditorLastFeedbackKind', '', localize('interactiveEditorLastFeedbackKind', "The last kind of feedback that was provided")); +export const CTX_INTERACTIVE_EDITOR_DOCUMENT_CHANGED = new RawContextKey('interactiveEditorDocumentChanged', false, localize('interactiveEditorDocumentChanged', "Whether the document has changed concurrently")); export const CTX_INTERACTIVE_EDITOR_EDIT_MODE = new RawContextKey('config.interactiveEditor.editMode', EditMode.Live); // --- menus From b1b7555c6cfd8bfeb6b36cdbf901e10b20b77162 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 3 May 2023 09:44:16 +0200 Subject: [PATCH 8/8] preview mode uses actual change ranges, and view hiding instead of model cropping --- .../browser/interactiveEditorController.ts | 2 +- .../browser/interactiveEditorDiffWidget.ts | 17 ++---- .../browser/interactiveEditorWidget.ts | 58 ++++++++++--------- .../interactiveEditor/browser/utils.ts | 22 +++++++ 4 files changed, 60 insertions(+), 39 deletions(-) create mode 100644 src/vs/workbench/contrib/interactiveEditor/browser/utils.ts diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts index f9d6eb8534a06..ce11e62456a12 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts @@ -847,7 +847,7 @@ class PreviewStrategy extends EditModeStrategy { override renderChanges(edits: ISingleEditOperation[], changes: LineRangeMapping[]): void { const response = this._session.lastExchange?.response; if (response instanceof EditResponse && response.localEdits.length > 0) { - this._widget.showEditsPreview(this._session.modelN, response.localEdits); + this._widget.showEditsPreview(this._session.modelN, edits, changes); } else { this._widget.hideEditsPreview(); } diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget.ts index 7c004d2a07289..570c65ef12f80 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget.ts @@ -23,6 +23,7 @@ import { Position } from 'vs/editor/common/core/position'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { IEditorDecorationsCollection, ScrollType } from 'vs/editor/common/editorCommon'; import { ILogService } from 'vs/platform/log/common/log'; +import { lineRangeAsRange, invertLineRange } from 'vs/workbench/contrib/interactiveEditor/browser/utils'; export class InteractiveEditorDiffWidget extends ZoneWidget { @@ -258,8 +259,8 @@ export class InteractiveEditorDiffWidget extends ZoneWidget { originalLineRange = new LineRange(originalLineRange.startLineNumber, originalLineRange.endLineNumberExclusive + endDelta); } - const originalDiffHidden = invert(originalLineRange, this._textModelv0); - const modifiedDiffHidden = invert(modifiedLineRange, model); + const originalDiffHidden = invertLineRange(originalLineRange, this._textModelv0); + const modifiedDiffHidden = invertLineRange(modifiedLineRange, model); return { originalHidden: originalLineRange, @@ -277,7 +278,7 @@ export class InteractiveEditorDiffWidget extends ZoneWidget { this._logService.debug(`[IE] diff NOTHING to hide for ${editor.getId()} with ${String(editor.getModel()?.uri)}`); return; } - const ranges = lineRanges.map(r => new Range(r.startLineNumber, 1, r.endLineNumberExclusive - 1, 1)); + const ranges = lineRanges.map(lineRangeAsRange); editor.setHiddenAreas(ranges, InteractiveEditorDiffWidget._hideId); this._logService.debug(`[IE] diff HIDING ${ranges} for ${editor.getId()} with ${String(editor.getModel()?.uri)}`); } @@ -304,15 +305,7 @@ export class InteractiveEditorDiffWidget extends ZoneWidget { } } -function invert(range: LineRange, model: ITextModel): LineRange[] { - if (range.isEmpty) { - return []; - } - const result: LineRange[] = []; - result.push(new LineRange(1, range.startLineNumber)); - result.push(new LineRange(range.endLineNumberExclusive, model.getLineCount() + 1)); - return result.filter(r => !r.isEmpty); -} + function isInlineDiffFriendly(mapping: LineRangeMapping): boolean { if (!mapping.modifiedRange.equals(mapping.originalRange)) { diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts index fd320bc1d3a7f..9b7e153399e16 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts @@ -7,7 +7,7 @@ import 'vs/css!./interactiveEditor'; import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IActiveCodeEditor, ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { IRange, Range } from 'vs/editor/common/core/range'; +import { Range } from 'vs/editor/common/core/range'; import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -30,7 +30,7 @@ import { IPosition, Position } from 'vs/editor/common/core/position'; import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style'; import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult, TextEdit } from 'vs/editor/common/languages'; -import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { EditOperation, 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'; @@ -38,6 +38,11 @@ import { IAction } from 'vs/base/common/actions'; import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { LanguageSelector } from 'vs/editor/common/languageSelector'; +import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; +import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; +import { invertLineRange, lineRangeAsRange } from 'vs/workbench/contrib/interactiveEditor/browser/utils'; +import { ScrollType } from 'vs/editor/common/editorCommon'; +import { LineRange } from 'vs/editor/common/core/lineRange'; const _inputEditorOptions: IEditorConstructionOptions = { padding: { top: 3, bottom: 2 }, @@ -397,36 +402,37 @@ export class InteractiveEditorWidget { // --- preview - showEditsPreview(actualModel: ITextModel, edits: TextEdit[]) { + showEditsPreview(textModelv0: ITextModel, edits: ISingleEditOperation[], changes: LineRangeMapping[]) { this._elements.previewDiff.classList.remove('hidden'); - const pad = 3; - const unionRange = (ranges: IRange[]) => ranges.reduce((p, c) => Range.plusRange(p, c)); - - const languageSelection: ILanguageSelection = { languageId: actualModel.getLanguageId(), onDidChange: Event.None }; - const baseModel = this._modelService.createModel(actualModel.getValue(), languageSelection, undefined, true); - - const originalRange = unionRange(edits.map(edit => edit.range)); - const originalRangePadded = baseModel.validateRange(new Range(originalRange.startLineNumber - pad, 1, originalRange.endLineNumber + pad, 1)); - const originalValue = baseModel.getValueInRange(originalRangePadded); - - const undos = baseModel.applyEdits(edits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)), true); - const modifiedRange = unionRange(undos.map(undo => undo.range)); - const modifiedRangePadded = baseModel.validateRange(new Range(modifiedRange.startLineNumber - pad, 1, modifiedRange.endLineNumber + pad, 1)); - const modifiedValue = baseModel.getValueInRange(modifiedRangePadded); - + const languageSelection: ILanguageSelection = { languageId: textModelv0.getLanguageId(), onDidChange: Event.None }; + const modified = this._modelService.createModel(createTextBufferFactoryFromSnapshot(textModelv0.createSnapshot()), languageSelection, undefined, true); + modified.applyEdits(edits, false); + this._previewDiffEditor.setModel({ original: textModelv0, modified }); + + // joined ranges + let originalLineRange = changes[0].originalRange; + let modifiedLineRange = changes[0].modifiedRange; + for (let i = 1; i < changes.length; i++) { + originalLineRange = originalLineRange.join(changes[i].originalRange); + modifiedLineRange = modifiedLineRange.join(changes[i].modifiedRange); + } - baseModel.dispose(); + // apply extra padding + const pad = 3; + const newStartLine = Math.max(1, originalLineRange.startLineNumber - pad); + modifiedLineRange = new LineRange(newStartLine, modifiedLineRange.endLineNumberExclusive); + originalLineRange = new LineRange(newStartLine, originalLineRange.endLineNumberExclusive); - const original = this._modelService.createModel(originalValue, languageSelection, baseModel.uri.with({ scheme: 'vscode', query: 'original' }), true); - const modified = this._modelService.createModel(modifiedValue, languageSelection, baseModel.uri.with({ scheme: 'vscode', query: 'modified' }), true); + modifiedLineRange = new LineRange(modifiedLineRange.startLineNumber, modifiedLineRange.endLineNumberExclusive + pad); + originalLineRange = new LineRange(originalLineRange.startLineNumber, originalLineRange.endLineNumberExclusive + pad); - this._previewDiffModel.value = toDisposable(() => { - original.dispose(); - modified.dispose(); - }); + const hiddenOriginal = invertLineRange(originalLineRange, textModelv0); + const hiddenModified = invertLineRange(modifiedLineRange, modified); + this._previewDiffEditor.getOriginalEditor().setHiddenAreas(hiddenOriginal.map(lineRangeAsRange), 'diff-hidden'); + this._previewDiffEditor.getModifiedEditor().setHiddenAreas(hiddenModified.map(lineRangeAsRange), 'diff-hidden'); + this._previewDiffEditor.revealLine(modifiedLineRange.startLineNumber, ScrollType.Immediate); - this._previewDiffEditor.setModel({ original, modified }); this._onDidChangeHeight.fire(); } diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/utils.ts b/src/vs/workbench/contrib/interactiveEditor/browser/utils.ts new file mode 100644 index 0000000000000..880c7778b1168 --- /dev/null +++ b/src/vs/workbench/contrib/interactiveEditor/browser/utils.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { LineRange } from 'vs/editor/common/core/lineRange'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; + +export function invertLineRange(range: LineRange, model: ITextModel): LineRange[] { + if (range.isEmpty) { + return []; + } + const result: LineRange[] = []; + result.push(new LineRange(1, range.startLineNumber)); + result.push(new LineRange(range.endLineNumberExclusive, model.getLineCount() + 1)); + return result.filter(r => !r.isEmpty); +} + +export function lineRangeAsRange(r: LineRange): Range { + return new Range(r.startLineNumber, 1, r.endLineNumberExclusive - 1, 1); +}