diff --git a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts index 9fea174a584d0..553470a8494bc 100644 --- a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts +++ b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts @@ -7,7 +7,7 @@ import * as objects from 'vs/base/common/objects'; import { ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; -import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; +import { DiffEditorWidget, IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditorWidget'; import { ConfigurationChangedEvent, IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -75,6 +75,7 @@ export class EmbeddedDiffEditorWidget extends DiffEditorWidget { constructor( domElement: HTMLElement, options: Readonly, + codeEditorWidgetOptions: IDiffCodeEditorWidgetOptions, parentEditor: ICodeEditor, @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService instantiationService: IInstantiationService, @@ -85,7 +86,7 @@ export class EmbeddedDiffEditorWidget extends DiffEditorWidget { @IClipboardService clipboardService: IClipboardService, @IEditorProgressService editorProgressService: IEditorProgressService, ) { - super(domElement, parentEditor.getRawOptions(), {}, clipboardService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService, contextMenuService, editorProgressService); + super(domElement, parentEditor.getRawOptions(), codeEditorWidgetOptions, clipboardService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService, contextMenuService, editorProgressService); this._parentEditor = parentEditor; this._overwriteOptions = options; diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditor.css b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditor.css index 7c337cec8bb2a..fcf47491fa6c1 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditor.css +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditor.css @@ -187,3 +187,9 @@ .monaco-editor .interactive-editor-slash-command-detail { opacity: 0.5; } + +/* diff zone */ + +.monaco-editor .interactive-editor-diff-widget { + padding: 6px 0; +} diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts index 0baa817e53325..2adf2d7650ace 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts @@ -7,7 +7,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; -import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { InteractiveEditorController, 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_HAS_RESPONSE, CTX_INTERACTIVE_EDITOR_EDIT_MODE } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; @@ -21,6 +21,7 @@ import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/commo import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; import { ILogService } from 'vs/platform/log/common/log'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; export class StartSessionAction extends EditorAction2 { @@ -62,6 +63,13 @@ abstract class AbstractInteractiveEditorAction extends EditorAction2 { } const ctrl = InteractiveEditorController.get(editor); if (!ctrl) { + for (const diffEditor of accessor.get(ICodeEditorService).listDiffEditors()) { + if (diffEditor.getOriginalEditor() === editor || diffEditor.getModifiedEditor() === editor) { + if (diffEditor instanceof EmbeddedDiffEditorWidget) { + this.runEditorCommand(accessor, diffEditor.getParentEditor(), ..._args); + } + } + } return; } this.runInteractiveEditorCommand(accessor, ctrl, editor, ..._args); @@ -124,25 +132,6 @@ export class StopRequestAction extends AbstractInteractiveEditorAction { } } -export class CancelSessionAction extends AbstractInteractiveEditorAction { - - constructor() { - super({ - id: 'interactiveEditor.cancel', - title: localize('cancel', 'Cancel'), - precondition: CTX_INTERACTIVE_EDITOR_VISIBLE, - keybinding: { - weight: KeybindingWeight.EditorContrib - 1, - primary: KeyCode.Escape - } - }); - } - - runInteractiveEditorCommand(_accessor: ServicesAccessor, ctrl: InteractiveEditorController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.cancelSession(); - } -} - export class ArrowOutUpAction extends AbstractInteractiveEditorAction { constructor() { super({ @@ -380,7 +369,7 @@ export class ToggleInlineDiff extends AbstractInteractiveEditorAction { id: MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, when: CTX_INTERACTIVE_EDITOR_EDIT_MODE.isEqualTo('direct'), group: '0_main', - order: 1 + order: 10 } }); } @@ -397,10 +386,14 @@ export class ApplyPreviewEdits extends AbstractInteractiveEditorAction { id: 'interactiveEditor.applyEdits', title: localize('applyEdits', 'Apply Changes'), icon: Codicon.check, - precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE, CTX_INTERACTIVE_EDITOR_EDIT_MODE.isEqualTo('preview')), + precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE), + keybinding: { + weight: KeybindingWeight.EditorContrib + 10, + primary: KeyMod.CtrlCmd | KeyCode.Enter + }, menu: { id: MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, - when: CTX_INTERACTIVE_EDITOR_EDIT_MODE.isEqualTo('preview'), + // when: CTX_INTERACTIVE_EDITOR_EDIT_MODE.isEqualTo('preview'), group: '0_main', order: 0 } @@ -415,7 +408,6 @@ export class ApplyPreviewEdits extends AbstractInteractiveEditorAction { logService.warn('FAILED to apply changes, no edit response'); return; } - ctrl.cancelSession(); if (edit.singleCreateFileEdit) { editorService.openEditor({ resource: edit.singleCreateFileEdit.uri }, SIDE_GROUP); } @@ -423,6 +415,32 @@ export class ApplyPreviewEdits extends AbstractInteractiveEditorAction { } } +export class CancelSessionAction extends AbstractInteractiveEditorAction { + + constructor() { + super({ + id: 'interactiveEditor.cancel', + title: localize('discard', 'Discard Changes'), + icon: Codicon.clearAll, + precondition: CTX_INTERACTIVE_EDITOR_VISIBLE, + keybinding: { + weight: KeybindingWeight.EditorContrib - 1, + primary: KeyCode.Escape + }, + menu: { + id: MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, + // when: CTX_INTERACTIVE_EDITOR_EDIT_MODE.isEqualTo('preview'), + group: '0_main', + order: 1 + } + }); + } + + runInteractiveEditorCommand(_accessor: ServicesAccessor, ctrl: InteractiveEditorController, _editor: ICodeEditor, ..._args: any[]): void { + ctrl.cancelSession(); + } +} + export class CopyRecordings extends AbstractInteractiveEditorAction { constructor() { diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts index ec2e2f55a4e81..5c3f815bd7d38 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts @@ -42,6 +42,9 @@ import { InteractiveEditorZoneWidget } from 'vs/workbench/contrib/interactiveEdi import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { URI } from 'vs/base/common/uri'; import { isEqual } from 'vs/base/common/resources'; +import { InteractiveEditorDiffWidget } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget'; +import { IModelService } from 'vs/editor/common/services/model'; +import { Event } from 'vs/base/common/event'; type Exchange = { req: IInteractiveEditorRequest; res: IInteractiveEditorResponse }; @@ -220,7 +223,7 @@ class LastEditorState { ) { } } -type EditMode = 'preview' | 'direct'; +type EditMode = 'live' | 'livePreview' | 'preview'; export class InteractiveEditorController implements IEditorContribution { @@ -256,6 +259,7 @@ export class InteractiveEditorController implements IEditorContribution { private readonly _ctxLastFeedbackKind: IContextKey<'helpful' | 'unhelpful' | ''>; private _lastEditState?: LastEditorState; + private _strategy?: EditModeStrategy; private _lastInlineDecorations?: InlineDiffDecorations; private _inlineDiffEnabled: boolean = false; @@ -266,12 +270,12 @@ export class InteractiveEditorController implements IEditorContribution { private readonly _editor: ICodeEditor, @IInstantiationService private readonly _instaService: IInstantiationService, @IInteractiveEditorService private readonly _interactiveEditorService: IInteractiveEditorService, - @IBulkEditService private readonly _bulkEditService: IBulkEditService, @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, @IContextKeyService contextKeyService: IContextKeyService, ) { @@ -293,9 +297,13 @@ export class InteractiveEditorController implements IEditorContribution { return InteractiveEditorController.ID; } + private _getMode(): EditMode { + return this._configurationService.getValue('interactiveEditor.editMode'); + } + async run(initialRange?: Range): Promise { - const editMode: EditMode = this._configurationService.getValue('interactiveEditor.editMode'); + const editMode = this._getMode(); this._ctsSession.dispose(true); @@ -330,6 +338,11 @@ export class InteractiveEditorController implements IEditorContribution { undos: '' }; + this._strategy = this._instaService.createInstance( + editMode === 'preview' ? PreviewStrategy : editMode === 'livePreview' ? LivePreviewStrategy : LiveStrategy, + textModel, textModel.getAlternativeVersionId() + ); + this._inlineDiffEnabled = this._storageService.getBoolean(InteractiveEditorController._inlineDiffStorageKey, StorageScope.PROFILE, false); const inlineDiffDecorations = new InlineDiffDecorations(this._editor, this._inlineDiffEnabled); this._lastInlineDecorations = inlineDiffDecorations; @@ -365,7 +378,7 @@ export class InteractiveEditorController implements IEditorContribution { this._zone.widget.updateMessage(session.message ?? localize('welcome.1', "AI-generated code may be incorrect.")); // CANCEL when input changes - this._editor.onDidChangeModel(this._ctsSession.cancel, this._ctsSession, store); + this._editor.onDidChangeModel(this.cancelSession, this, store); // REposition the zone widget whenever the block decoration changes let lastPost: Position | undefined; @@ -410,6 +423,10 @@ export class InteractiveEditorController implements IEditorContribution { const roundStore = new DisposableStore(); store.add(roundStore); + const diffBaseModel = this._modelService.createModel(textModel.getValue(), { languageId: textModel.getLanguageId(), onDidChange: Event.None }, undefined, true); + store.add(diffBaseModel); + const diffZone = this._instaService.createInstance(InteractiveEditorDiffWidget, this._editor, diffBaseModel); + do { round += 1; @@ -421,6 +438,14 @@ export class InteractiveEditorController implements IEditorContribution { break; } + if (round > 1 && editMode === 'livePreview') { + if (!textModel.equalsTextBuffer(diffBaseModel.getTextBuffer())) { + diffZone.show(wholeRangeDecoration.getRange(0)!); + } else { + diffZone.hide(); + } + } + // visuals: add block decoration blockDecoration.set([{ range: wholeRange, @@ -511,9 +536,8 @@ export class InteractiveEditorController implements IEditorContribution { const editResponse = new EditResponse(textModel.uri, reply); - if (editResponse.workspaceEdits && (!editResponse.singleCreateFileEdit || editMode === 'direct')) { - this._bulkEditService.apply(editResponse.workspaceEdits, { editor: this._editor, label: localize('ie', "{0}", input), showPreview: true }); - // todo@jrieken keep interactive editor? + const canContinue = this._strategy.update(editResponse); + if (!canContinue) { break; } @@ -569,7 +593,9 @@ export class InteractiveEditorController implements IEditorContribution { ignoreModelChanges = false; } - inlineDiffDecorations.update(); + if (editMode === 'live') { + inlineDiffDecorations.update(); + } // line count const lineSet = new Set(); @@ -616,6 +642,9 @@ export class InteractiveEditorController implements IEditorContribution { this._inlineDiffEnabled = inlineDiffDecorations.visible; this._storageService.store(InteractiveEditorController._inlineDiffStorageKey, this._inlineDiffEnabled, StorageScope.PROFILE, StorageTarget.USER); + diffZone.hide(); + diffZone.dispose(); + // done, cleanup wholeRangeDecoration.clear(); blockDecoration.clear(); @@ -647,10 +676,6 @@ export class InteractiveEditorController implements IEditorContribution { this._ctsRequest?.cancel(); } - cancelSession() { - this._ctsSession.cancel(); - } - arrowOut(up: boolean): void { if (this._zone.position && this._editor.hasModel()) { const { column } = this._editor.getPosition(); @@ -713,22 +738,120 @@ export class InteractiveEditorController implements IEditorContribution { if (!this._lastEditState) { return undefined; } + const { response } = this._lastEditState; + await this._strategy?.apply(); + this._ctsSession.cancel(); + return response; + } + + cancelSession() { + this._ctsSession.cancel(); + this._strategy?.cancel(); + } +} + +abstract class EditModeStrategy { + + abstract update(response: EditResponse): boolean; + + abstract apply(): Promise; - const { model, modelVersionId, response } = this._lastEditState; + abstract cancel(): void; +} + +class PreviewStrategy extends EditModeStrategy { + + private _lastResponse?: EditResponse; + + constructor( + private readonly _model: ITextModel, + private readonly _versionId: number, + @IBulkEditService private readonly _bulkEditService: IBulkEditService, + ) { + super(); + } + + update(response: EditResponse): boolean { + this._lastResponse = response; + if (!response.workspaceEdits || response.singleCreateFileEdit) { + // preview stategy can handle simple workspace edit (single file create) + return true; + } + this._bulkEditService.apply(response.workspaceEdits, { showPreview: true }); + return false; + } + + async apply() { + + const response = this._lastResponse; + if (!response) { + return; + } if (response.workspaceEdits) { await this._bulkEditService.apply(response.workspaceEdits); } else if (!response.workspaceEditsIncludeLocalEdits) { - if (model.getAlternativeVersionId() === modelVersionId) { - model.pushStackElement(); + if (this._model.getAlternativeVersionId() === this._versionId) { + this._model.pushStackElement(); const edits = response.localEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)); - model.pushEditOperations(null, edits, () => null); - model.pushStackElement(); + this._model.pushEditOperations(null, edits, () => null); + this._model.pushStackElement(); } } + } - return response; + cancel(): void { + // nothing to do + } +} + +class LiveStrategy extends EditModeStrategy { + + constructor( + protected readonly _model: ITextModel, + protected readonly _versionId: number, + @IBulkEditService protected readonly _bulkEditService: IBulkEditService, + ) { + super(); + } + + update(response: EditResponse): boolean { + if (response.workspaceEdits) { + this._bulkEditService.apply(response.workspaceEdits, { showPreview: true }); + return false; + } + return true; + } + + async apply() { + // nothing to do + } + + cancel(): void { + while (this._model.getAlternativeVersionId() !== this._versionId) { + this._model.undo(); + } + } +} + +class LivePreviewStrategy extends LiveStrategy { + + private _lastResponse?: EditResponse; + + override update(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); + } + + override async apply() { + if (this._lastResponse?.workspaceEdits) { + await this._bulkEditService.apply(this._lastResponse.workspaceEdits); + } } } diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget.ts new file mode 100644 index 0000000000000..cafa4a5eedce7 --- /dev/null +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorDiffWidget.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Dimension, h } from 'vs/base/browser/dom'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { assertType } from 'vs/base/common/types'; +import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Position } from 'vs/editor/common/core/position'; +import { IRange } from 'vs/editor/common/core/range'; +import { ScrollType } from 'vs/editor/common/editorCommon'; +import { ITextModel } from 'vs/editor/common/model'; +import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; +import * as editorColorRegistry from 'vs/editor/common/core/editorColorRegistry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { interactiveEditorDiffInserted, interactiveEditorDiffRemoved, interactiveEditorRegionHighlight } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; + +export class InteractiveEditorDiffWidget extends ZoneWidget { + + private static readonly _hideId = 'overlayDiff'; + + private readonly _elements = h('div.interactive-editor-diff-widget@domNode'); + + private readonly _diffEditor: IDiffEditor; + private readonly _sessionStore = this._disposables.add(new DisposableStore()); + private _dim: Dimension | undefined; + + constructor( + editor: ICodeEditor, + private readonly _originalModel: ITextModel, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + ) { + super(editor, { showArrow: false, showFrame: false, isResizeable: false, isAccessible: true }); + super.create(); + + this._diffEditor = instantiationService.createInstance(EmbeddedDiffEditorWidget, this._elements.domNode, { + scrollbar: { useShadows: false, alwaysConsumeMouseWheel: false }, + renderMarginRevertIcon: false, + diffCodeLens: false, + scrollBeyondLastLine: false, + stickyScroll: { enabled: false } + }, { + originalEditor: { contributions: [] }, + modifiedEditor: { contributions: [] } + }, editor); + this._disposables.add(this._diffEditor); + + const doStyle = () => { + const theme = themeService.getColorTheme(); + const overrides: [target: string, source: string][] = [ + [colorRegistry.editorBackground, interactiveEditorRegionHighlight], + [editorColorRegistry.editorGutter, interactiveEditorRegionHighlight], + [colorRegistry.diffInsertedLine, interactiveEditorDiffInserted], + [colorRegistry.diffInserted, interactiveEditorDiffInserted], + [colorRegistry.diffRemovedLine, interactiveEditorDiffRemoved], + [colorRegistry.diffRemoved, interactiveEditorDiffRemoved], + ]; + + for (const [target, source] of overrides) { + const value = theme.getColor(source); + if (value) { + this._elements.domNode.style.setProperty(colorRegistry.asCssVariableName(target), String(value)); + } + } + }; + doStyle(); + this._disposables.add(themeService.onDidColorThemeChange(doStyle)); + + } + + protected override _fillContainer(container: HTMLElement): void { + container.appendChild(this._elements.domNode); + } + + override show(range: IRange): void { + assertType(this.editor.hasModel()); + this._sessionStore.clear(); + + this.editor.setHiddenAreas([range], InteractiveEditorDiffWidget._hideId); + const modified = this.editor.getModel(); + const lineHeightDiff = Math.max(1, Math.abs(modified.getLineCount() - this._originalModel.getLineCount()) + (range.endLineNumber - range.startLineNumber)); + const lineHeightPadding = (this.editor.getOption(EditorOption.lineHeight) / 12) /* padding-top/bottom*/; + + this._diffEditor.setModel({ original: this._originalModel, modified }); + this._diffEditor.revealRange(range, ScrollType.Immediate); + this._diffEditor.getModifiedEditor().setHiddenAreas(invertRange(range, modified), InteractiveEditorDiffWidget._hideId); + + const updateHiddenAreasOriginal = () => { + // todo@jrieken this needs work when both are equal + const changes = this._diffEditor.getLineChanges(); + if (!changes) { + return; + } + let startLine = Number.MAX_VALUE; + let endLine = 0; + for (const change of changes) { + startLine = Math.min(startLine, change.originalStartLineNumber, change.modifiedStartLineNumber); + endLine = Math.max(endLine, change.originalEndLineNumber || change.originalStartLineNumber, change.modifiedEndLineNumber || change.modifiedStartLineNumber); + } + const combinedRange = this._originalModel.validateRange({ startLineNumber: startLine, startColumn: 1, endLineNumber: endLine, endColumn: Number.MAX_VALUE }); + + const hiddenRangesOriginal = invertRange(combinedRange, this._originalModel); + this._diffEditor.getOriginalEditor().setHiddenAreas(hiddenRangesOriginal, InteractiveEditorDiffWidget._hideId); + + const hiddenRangesModified = invertRange(combinedRange, modified); + this._diffEditor.getModifiedEditor().setHiddenAreas(hiddenRangesModified, InteractiveEditorDiffWidget._hideId); + }; + this._diffEditor.onDidUpdateDiff(updateHiddenAreasOriginal, undefined, this._sessionStore); + updateHiddenAreasOriginal(); + + super.show(new Position(range.endLineNumber, 1), lineHeightDiff + lineHeightPadding); + } + + override hide(): void { + this.editor.setHiddenAreas([], InteractiveEditorDiffWidget._hideId); + this._diffEditor.getOriginalEditor().setHiddenAreas([], InteractiveEditorDiffWidget._hideId); + this._diffEditor.getModifiedEditor().setHiddenAreas([], InteractiveEditorDiffWidget._hideId); + this._diffEditor.setModel(null); + super.hide(); + } + + protected override _onWidth(widthInPixel: number): void { + if (this._dim) { + this._doLayout(this._dim.height, widthInPixel); + } + } + + protected override _doLayout(heightInPixel: number, widthInPixel: number): void { + const newDim = new Dimension(widthInPixel, heightInPixel); + if (Dimension.equals(this._dim, newDim)) { + return; + } + this._dim = newDim; + this._diffEditor.layout(this._dim.with(undefined, this._dim.height - 12 /* padding */)); + } +} + +function invertRange(range: IRange, model: ITextModel): IRange[] { + const result: IRange[] = []; + if (range.startLineNumber > 1) { + result.push({ startLineNumber: 1, startColumn: 1, endLineNumber: range.startLineNumber - 1, endColumn: 1 }); + } + if (range.endLineNumber < model.getLineCount()) { + result.push({ startLineNumber: range.endLineNumber + 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: 1 }); + } + return result; +} diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts index 6ebd59cb02c6d..6a3779bbf8f96 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts @@ -38,7 +38,7 @@ import { ILanguageSelection } from 'vs/editor/common/languages/language'; import { ResourceLabel } from 'vs/workbench/browser/labels'; import { FileKind } from 'vs/platform/files/common/files'; -const _commonEditorOptions: IEditorConstructionOptions = { +const _inputEditorOptions: IEditorConstructionOptions = { padding: { top: 3, bottom: 2 }, overviewRulerLanes: 0, glyphMargin: false, @@ -68,16 +68,7 @@ const _commonEditorOptions: IEditorConstructionOptions = { showIcons: false, showSnippets: false, showStatusBar: false, - } -}; - -const _inputEditorOptions: IEditorConstructionOptions = { - ..._commonEditorOptions, - lineNumbers: 'off', - selectOnLineNumbers: false, - lineDecorationsWidth: 0, - renderWhitespace: 'none', - cursorWidth: 1, + }, wordWrap: 'on', ariaLabel: localize('aria-label', "Interactive Editor Input"), fontFamily: DEFAULT_FONT_FAMILY, @@ -86,17 +77,15 @@ const _inputEditorOptions: IEditorConstructionOptions = { }; const _previewEditorEditorOptions: IDiffEditorConstructionOptions = { - ..._commonEditorOptions, - readOnly: true, - wordWrap: 'off', - enableSplitViewResizing: true, - isInEmbeddedEditor: true, - renderOverviewRuler: false, - ignoreTrimWhitespace: false, - renderSideBySide: true, + scrollbar: { useShadows: false, alwaysConsumeMouseWheel: false }, + renderMarginRevertIcon: false, + diffCodeLens: false, + scrollBeyondLastLine: false, + stickyScroll: { enabled: false }, originalAriaLabel: localize('modified', 'Modified'), modifiedAriaLabel: localize('original', 'Original'), diffAlgorithm: 'smart', + readOnly: true, }; class InteractiveEditorWidget { @@ -118,9 +107,9 @@ class InteractiveEditorWidget { ]), ]), h('div.progress@progress'), - h('div.previewDiff@previewDiff'), + h('div.previewDiff.hidden@previewDiff'), h('div.previewCreateTitle.show-file-icons@previewCreateTitle'), - h('div.previewCreate@previewCreate'), + h('div.previewCreate.hidden@previewCreate'), h('div.status@status', [ h('div.actions.hidden@statusToolbar'), h('div.label@statusLabel'), @@ -222,7 +211,7 @@ class InteractiveEditorWidget { this._historyStore.add(statusToolbar); // preview editors - this._previewDiffEditor = this._store.add(_instantiationService.createInstance(EmbeddedDiffEditorWidget, this._elements.previewDiff, _previewEditorEditorOptions, parentEditor)); + this._previewDiffEditor = this._store.add(_instantiationService.createInstance(EmbeddedDiffEditorWidget, this._elements.previewDiff, _previewEditorEditorOptions, { modifiedEditor: codeEditorWidgetOptions, originalEditor: codeEditorWidgetOptions }, parentEditor)); this._previewCreateTitle = this._store.add(_instantiationService.createInstance(ResourceLabel, this._elements.previewCreateTitle, { supportIcons: true })); this._previewCreateEditor = this._store.add(_instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, parentEditor)); diff --git a/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts b/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts index 1da1384f1c938..0aacbece7e730 100644 --- a/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts @@ -14,10 +14,10 @@ import { ITextModel } from 'vs/editor/common/model'; import { localize } from 'vs/nls'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; -import { editorHoverHighlight, editorWidgetBorder, focusBorder, inputBackground, inputPlaceholderForeground, registerColor, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; +import { diffInserted, diffRemoved, editorHoverHighlight, editorWidgetBorder, focusBorder, inputBackground, inputPlaceholderForeground, registerColor, transparent, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; export interface IInteractiveEditorSlashCommand { command: string; @@ -105,9 +105,7 @@ export const CTX_INTERACTIVE_EDITOR_HAS_RESPONSE = new RawContextKey('i export const CTX_INTERACTIVE_EDITOR_INLNE_DIFF = new RawContextKey('interactiveEditorInlineDiff', false, localize('interactiveEditorInlineDiff', "Whether interactive editor show inline diffs for changes")); 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_VALUE_INTERACTIVE_EDITOR_EDIT_MODE_YOLO = ContextKeyExpr.equals('config.interactiveEditor.editMode', 'direct'); -export const CTX_INTERACTIVE_EDITOR_EDIT_MODE = new RawContextKey<'direct' | 'preview'>('config.interactiveEditor.editMode', 'direct'); +export const CTX_INTERACTIVE_EDITOR_EDIT_MODE = new RawContextKey<'live' | 'livePreview' | 'preview'>('config.interactiveEditor.editMode', 'live'); // --- menus @@ -119,19 +117,22 @@ MenuRegistry.appendMenuItem(MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, { title: localize('undo', "Undo..."), icon: Codicon.discard, group: '0_main', - order: 0, + order: 2, when: CTX_INTERACTIVE_EDITOR_EDIT_MODE.isEqualTo('direct') }); // --- colors -registerColor('interactiveEditor.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, localize('interactiveEditor.border', "Border color of the interactive editor widget")); -registerColor('interactiveEditor.shadow', { dark: widgetShadow, light: widgetShadow, hcDark: widgetShadow, hcLight: widgetShadow }, localize('interactiveEditor.shadow', "Shadow color of the interactive editor widget")); -registerColor('interactiveEditor.regionHighlight', { dark: editorHoverHighlight, light: editorHoverHighlight, hcDark: editorHoverHighlight, hcLight: editorHoverHighlight }, localize('interactiveEditor.regionHighlight', "Background highlighting of the current interactive region. Must be transparent."), true); -registerColor('interactiveEditorInput.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, localize('interactiveEditorInput.border', "Border color of the interactive editor input")); -registerColor('interactiveEditorInput.focusBorder', { dark: focusBorder, light: focusBorder, hcDark: focusBorder, hcLight: focusBorder }, localize('interactiveEditorInput.focusBorder', "Border color of the interactive editor input when focused")); -registerColor('interactiveEditorInput.placeholderForeground', { dark: inputPlaceholderForeground, light: inputPlaceholderForeground, hcDark: inputPlaceholderForeground, hcLight: inputPlaceholderForeground }, localize('interactiveEditorInput.placeholderForeground', "Foreground color of the interactive editor input placeholder")); -registerColor('interactiveEditorInput.background', { dark: inputBackground, light: inputBackground, hcDark: inputBackground, hcLight: inputBackground }, localize('interactiveEditorInput.background', "Background color of the interactive editor input")); +export const interactiveEditorBorder = registerColor('interactiveEditor.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, localize('interactiveEditor.border', "Border color of the interactive editor widget")); +export const interactiveEditorShadow = registerColor('interactiveEditor.shadow', { dark: widgetShadow, light: widgetShadow, hcDark: widgetShadow, hcLight: widgetShadow }, localize('interactiveEditor.shadow', "Shadow color of the interactive editor widget")); +export const interactiveEditorRegionHighlight = registerColor('interactiveEditor.regionHighlight', { dark: editorHoverHighlight, light: editorHoverHighlight, hcDark: editorHoverHighlight, hcLight: editorHoverHighlight }, localize('interactiveEditor.regionHighlight', "Background highlighting of the current interactive region. Must be transparent."), true); +export const interactiveEditorInputBorder = registerColor('interactiveEditorInput.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, localize('interactiveEditorInput.border', "Border color of the interactive editor input")); +export const interactiveEditorInputFocusBorder = registerColor('interactiveEditorInput.focusBorder', { dark: focusBorder, light: focusBorder, hcDark: focusBorder, hcLight: focusBorder }, localize('interactiveEditorInput.focusBorder', "Border color of the interactive editor input when focused")); +export const interactiveEditorInputPlaceholderForeground = registerColor('interactiveEditorInput.placeholderForeground', { dark: inputPlaceholderForeground, light: inputPlaceholderForeground, hcDark: inputPlaceholderForeground, hcLight: inputPlaceholderForeground }, localize('interactiveEditorInput.placeholderForeground', "Foreground color of the interactive editor input placeholder")); +export const interactiveEditorInputBackground = registerColor('interactiveEditorInput.background', { dark: inputBackground, light: inputBackground, hcDark: inputBackground, hcLight: inputBackground }, localize('interactiveEditorInput.background', "Background color of the interactive editor input")); + +export const interactiveEditorDiffInserted = registerColor('interactiveEditorDiff.inserted', { dark: transparent(diffInserted, .5), light: transparent(diffInserted, .5), hcDark: transparent(diffInserted, .5), hcLight: transparent(diffInserted, .5) }, localize('interactiveEditorDiff.inserted', "Background color of inserted text in the interactive editor input")); +export const interactiveEditorDiffRemoved = registerColor('interactiveEditorDiff.removed', { dark: transparent(diffRemoved, .5), light: transparent(diffRemoved, .5), hcDark: transparent(diffRemoved, .5), hcLight: transparent(diffRemoved, .5) }, localize('interactiveEditorDiff.removed', "Background color of removed text in the interactive editor input")); // settings @@ -140,9 +141,9 @@ Registry.as(Extensions.Configuration).registerConfigurat properties: { 'interactiveEditor.editMode': { description: localize('editMode', "Configure if changes crafted in the interactive editor are applied directly or previewed first"), - default: 'direct', + default: 'live', type: 'string', - enum: ['preview', 'direct'] + enum: ['live', 'livePreview', 'preview'] } } }); diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 56fdbf0a4accd..cd294eafb7d23 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -412,7 +412,7 @@ class DirtyDiffWidget extends PeekViewWidget { stickyScroll: { enabled: false } }; - this.diffEditor = this.instantiationService.createInstance(EmbeddedDiffEditorWidget, container, options, this.editor); + this.diffEditor = this.instantiationService.createInstance(EmbeddedDiffEditorWidget, container, options, {}, this.editor); this._disposables.add(this.diffEditor); } diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index d9d0e4c3f28a0..b9c103d96c989 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -1044,6 +1044,7 @@ class DiffContentProvider extends Disposable implements IPeekOutputRenderer { EmbeddedDiffEditorWidget, this.container, diffEditorOptions, + {}, this.editor, ) : this.instantiationService.createInstance( DiffEditorWidget,