support to preview simple file creation (#179771)

* support to preview simple file creation

* fix height
This commit is contained in:
Johannes Rieken 2023-04-12 15:49:20 +02:00 committed by GitHub
parent 0e8b1c8e09
commit 381c3ab0d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 180 additions and 47 deletions

View file

@ -137,7 +137,7 @@
display: none;
}
.monaco-editor .interactive-editor.preview .preview {
.monaco-editor .interactive-editor.preview .previewDiff {
display: inherit;
padding: 6px;
border: 1px solid var(--vscode-interactiveEditor-border);
@ -147,6 +147,24 @@
margin: 0 2px 6px 2px;
}
.monaco-editor .interactive-editor.preview .previewCreateTitle {
padding-top: 6px;
}
.monaco-editor .interactive-editor.preview .previewCreate {
display: inherit;
padding: 6px;
border: 1px solid var(--vscode-interactiveEditor-border);
border-radius: 2px;
margin: 0 2px 6px 2px;
}
.monaco-editor .interactive-editor .previewDiff.hidden,
.monaco-editor .interactive-editor .previewCreate.hidden,
.monaco-editor .interactive-editor .previewCreateTitle.hidden {
display: none;
}
/* decoration styles */
.monaco-editor .interactive-editor-lines-deleted-range-inline {

View file

@ -12,7 +12,7 @@ import { IEditorContribution, IEditorDecorationsCollection, ScrollType } from 'v
import { localize } from 'vs/nls';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IInteractiveEditorResponse, IInteractiveEditorService, CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, IInteractiveEditorRequest, IInteractiveEditorSession, IInteractiveEditorSlashCommand, IInteractiveEditorSessionProvider, InteractiveEditorResponseFeedbackKind, IInteractiveEditorEditResponse, 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, CTX_INTERACTIVE_EDITOR_INLNE_DIFF, CTX_INTERACTIVE_EDITOR_HAS_RESPONSE } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
import { IInteractiveEditorResponse, IInteractiveEditorService, CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, IInteractiveEditorRequest, IInteractiveEditorSession, IInteractiveEditorSlashCommand, IInteractiveEditorSessionProvider, InteractiveEditorResponseFeedbackKind, IInteractiveEditorEditResponse, 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, CTX_INTERACTIVE_EDITOR_INLNE_DIFF, CTX_INTERACTIVE_EDITOR_HAS_RESPONSE, IInteractiveEditorBulkEditResponse } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Iterable } from 'vs/base/common/iterator';
import { ICursorStateComputer, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation } from 'vs/editor/common/model';
@ -26,21 +26,23 @@ import { ILogService } from 'vs/platform/log/common/log';
import { StopWatch } from 'vs/base/common/stopwatch';
import { LRUCache } from 'vs/base/common/map';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
import { IBulkEditService, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { IInteractiveSessionWidgetService } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget';
import { IViewsService } from 'vs/workbench/common/views';
import { IInteractiveSessionContributionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionContributionService';
import { InteractiveSessionViewPane } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionSidebar';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { CompletionContext, CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult } from 'vs/editor/common/languages';
import { CompletionContext, CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult, TextEdit } from 'vs/editor/common/languages';
import { LanguageSelector } from 'vs/editor/common/languageSelector';
import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { splitLines } from 'vs/base/common/strings';
import { InteractiveEditorZoneWidget } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { URI } from 'vs/base/common/uri';
import { isEqual } from 'vs/base/common/resources';
import { decodeBase64 } from 'vs/base/common/buffer';
type Exchange = { req: IInteractiveEditorRequest; res: IInteractiveEditorResponse };
@ -152,6 +154,60 @@ class InlineDiffDecorations {
}
}
class UIEditResponse {
readonly localEdits: TextEdit[] = [];
readonly singleCreateFileEdit: { uri: URI; edits: TextEdit[] } | undefined;
readonly workspaceEdits: ResourceEdit[] | undefined;
constructor(uri: URI, readonly raw: IInteractiveEditorBulkEditResponse | IInteractiveEditorEditResponse) {
if (raw.type === 'editorEdit') {
//
this.localEdits = raw.edits;
this.singleCreateFileEdit = undefined;
this.workspaceEdits = undefined;
} else {
//
const edits = ResourceEdit.convert(raw.edits);
let isComplexEdit = false;
for (const edit of edits) {
if (edit instanceof ResourceFileEdit) {
if (!isComplexEdit && edit.newResource && !edit.oldResource) {
// file create
if (this.singleCreateFileEdit) {
isComplexEdit = true;
this.singleCreateFileEdit = undefined;
} else {
this.singleCreateFileEdit = { uri: edit.newResource, edits: [] };
if (edit.options.contentsBase64) {
const newText = decodeBase64(edit.options.contentsBase64).toString();
this.singleCreateFileEdit.edits.push({ range: new Range(1, 1, 1, 1), text: newText });
}
}
}
} else if (edit instanceof ResourceTextEdit) {
//
if (isEqual(edit.resource, uri)) {
this.localEdits.push(edit.textEdit);
} else if (isEqual(this.singleCreateFileEdit?.uri, edit.resource)) {
this.singleCreateFileEdit!.edits.push(edit.textEdit);
} else {
isComplexEdit = true;
}
}
}
if (isComplexEdit) {
this.workspaceEdits = edits;
}
}
}
}
class LastEditorState {
constructor(
@ -159,7 +215,7 @@ class LastEditorState {
readonly modelVersionId: number,
readonly provider: IInteractiveEditorSessionProvider,
readonly session: IInteractiveEditorSession,
readonly response: IInteractiveEditorEditResponse,
readonly response: UIEditResponse,
) { }
}
@ -443,28 +499,29 @@ export class InteractiveEditorController implements IEditorContribution {
continue;
}
if (reply.type === 'bulkEdit') {
this._logService.info('[IE] performaing a BULK EDIT, exiting interactive editor', provider.debugName);
this._bulkEditService.apply(reply.edits, { editor: this._editor, label: localize('ie', "{0}", input), showPreview: true });
// todo@jrieken preview bulk edit?
// todo@jrieken keep interactive editor?
break;
}
this._recorder.addExchange(session, request, reply);
if (reply.type === 'message') {
this._logService.info('[IE] received a MESSAGE, continuing outside editor', provider.debugName);
this._instaService.invokeFunction(showMessageResponse, request.prompt, reply.message.value);
continue;
}
this._ctxLastEditKind.set(reply.edits.length === 1 ? 'simple' : '');
this._recorder.addExchange(session, request, reply);
const editResponse = new UIEditResponse(textModel.uri, reply);
if (editResponse.workspaceEdits) {
this._bulkEditService.apply(editResponse.workspaceEdits, { editor: this._editor, label: localize('ie', "{0}", input), showPreview: true });
// todo@jrieken keep interactive editor?
break;
}
this._ctxLastEditKind.set(editResponse.localEdits.length === 1 ? 'simple' : '');
// inline diff
inlineDiffDecorations.clear();
this._lastEditState = new LastEditorState(textModel, textModel.getAlternativeVersionId(), provider, session, reply);
this._lastEditState = new LastEditorState(textModel, textModel.getAlternativeVersionId(), provider, session, editResponse);
// use whole range from reply
if (reply.wholeRange) {
@ -476,13 +533,16 @@ export class InteractiveEditorController implements IEditorContribution {
if (editMode === 'preview') {
// only preview changes
this._zone.widget.updateToolbar(true);
this._zone.widget.preview(textModel, reply.edits);
if (editResponse.localEdits.length > 0) {
this._zone.widget.showEditsPreview(textModel, editResponse.localEdits);
} else {
this._zone.widget.hideEditsPreview();
}
} else {
// make edits more minimal
const moreMinimalEdits = (await this._editorWorkerService.computeHumanReadableDiff(textModel.uri, reply.edits));
this._logService.trace('[IE] edits from PROVIDER and after making them MORE MINIMAL', provider.debugName, reply.edits, moreMinimalEdits);
const moreMinimalEdits = (await this._editorWorkerService.computeHumanReadableDiff(textModel.uri, editResponse.localEdits));
this._logService.trace('[IE] edits from PROVIDER and after making them MORE MINIMAL', provider.debugName, editResponse.localEdits, moreMinimalEdits);
try {
ignoreModelChanges = true;
@ -499,7 +559,7 @@ export class InteractiveEditorController implements IEditorContribution {
this._editor.pushUndoStop();
this._editor.executeEdits(
'interactive-editor',
(moreMinimalEdits ?? reply.edits).map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)),
(moreMinimalEdits ?? editResponse.localEdits).map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)),
cursorStateComputerAndInlineDiffCollection
);
this._editor.pushUndoStop();
@ -513,7 +573,7 @@ export class InteractiveEditorController implements IEditorContribution {
// line count
const lineSet = new Set<number>();
let addRemoveCount = 0;
for (const edit of moreMinimalEdits ?? reply.edits) {
for (const edit of moreMinimalEdits ?? editResponse.localEdits) {
const len2 = splitLines(edit.text).length - 1;
@ -532,12 +592,20 @@ export class InteractiveEditorController implements IEditorContribution {
}
const linesChanged = addRemoveCount + lineSet.size;
this._zone.widget.updateToolbar(true);
this._zone.widget.updateMessage(linesChanged === 1
? localize('lines.1', "Generated reply and changed 1 line.")
: localize('lines.N', "Generated reply and changed {0} lines.", linesChanged)
);
}
this._zone.widget.updateToolbar(true);
if (editResponse.singleCreateFileEdit) {
this._zone.widget.showCreatePreview(editResponse.singleCreateFileEdit.uri, editResponse.singleCreateFileEdit.edits);
} else {
this._zone.widget.hideCreatePreview();
}
placeholder = reply.placeholder ?? session.placeholder ?? '';
data.rounds += round + '|';
@ -626,15 +694,15 @@ export class InteractiveEditorController implements IEditorContribution {
while (model.getAlternativeVersionId() !== modelVersionId) {
model.undo();
}
this._lastEditState.provider.handleInteractiveEditorResponseFeedback?.(this._lastEditState.session, this._lastEditState.response, InteractiveEditorResponseFeedbackKind.Undone);
return this._lastEditState.response.edits[0].text;
this._lastEditState.provider.handleInteractiveEditorResponseFeedback?.(this._lastEditState.session, this._lastEditState.response.raw, InteractiveEditorResponseFeedbackKind.Undone);
return this._lastEditState.response.localEdits[0].text;
}
}
feedbackLast(helpful: boolean) {
if (this._lastEditState) {
const kind = helpful ? InteractiveEditorResponseFeedbackKind.Helpful : InteractiveEditorResponseFeedbackKind.Unhelpful;
this._lastEditState.provider.handleInteractiveEditorResponseFeedback?.(this._lastEditState.session, this._lastEditState.response, kind);
this._lastEditState.provider.handleInteractiveEditorResponseFeedback?.(this._lastEditState.session, this._lastEditState.response.raw, kind);
this._ctxLastFeedbackKind.set(helpful ? 'helpful' : 'unhelpful');
this._zone.widget.updateMessage('Thank you for your feedback!', undefined, 1250);
}
@ -645,7 +713,7 @@ export class InteractiveEditorController implements IEditorContribution {
const { model, modelVersionId, response } = this._lastEditState;
if (model.getAlternativeVersionId() === modelVersionId) {
model.pushStackElement();
const edits = response.edits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text));
const edits = response.localEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text));
model.pushEditOperations(null, edits, () => null);
model.pushStackElement();
return true;

View file

@ -17,7 +17,7 @@ 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 } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
import { ITextModel } from 'vs/editor/common/model';
import { Dimension, addDisposableListener, getTotalHeight, getTotalWidth, h, reset } from 'vs/base/browser/dom';
import { Emitter, Event } from 'vs/base/common/event';
import { 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';
@ -35,6 +35,8 @@ import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActio
import { 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';
const _commonEditorOptions: IEditorConstructionOptions = {
padding: { top: 3, bottom: 2 },
@ -86,6 +88,7 @@ const _inputEditorOptions: IEditorConstructionOptions = {
const _previewEditorEditorOptions: IDiffEditorConstructionOptions = {
..._commonEditorOptions,
readOnly: true,
wordWrap: 'off',
enableSplitViewResizing: true,
isInEmbeddedEditor: true,
@ -116,7 +119,9 @@ class InteractiveEditorWidget {
]),
]),
h('div.progress@progress'),
h('div.preview@preview'),
h('div.previewDiff@previewDiff'),
h('div.previewCreateTitle.show-file-icons@previewCreateTitle'),
h('div.previewCreate@previewCreate'),
h('div.status@status', [
h('div.actions.hidden@statusToolbar'),
h('div.label@statusLabel'),
@ -126,7 +131,6 @@ class InteractiveEditorWidget {
private readonly _store = new DisposableStore();
private readonly _historyStore = new DisposableStore();
private readonly _previewModel = this._store.add(new MutableDisposable());
readonly inputEditor: ICodeEditor;
private readonly _inputModel: ITextModel;
@ -134,9 +138,14 @@ class InteractiveEditorWidget {
private readonly _progressBar: ProgressBar;
private readonly _previewEditor: EmbeddedDiffEditorWidget;
private readonly _previewDiffEditor: EmbeddedDiffEditorWidget;
private readonly _previewDiffModel = this._store.add(new MutableDisposable());
private readonly _onDidChangeHeight = new Emitter<void>();
private readonly _previewCreateTitle: ResourceLabel;
private readonly _previewCreateEditor: ICodeEditor;
private readonly _previewCreateModel = this._store.add(new MutableDisposable());
private readonly _onDidChangeHeight = new MicrotaskEmitter<void>();
readonly onDidChangeHeight: Event<void> = Event.filter(this._onDidChangeHeight.event, _ => !this._isLayouting);
private _lastDim: Dimension | undefined;
@ -213,10 +222,12 @@ class InteractiveEditorWidget {
this._historyStore.add(statusToolbar);
// preview editor
// preview editors
this._previewDiffEditor = this._store.add(_instantiationService.createInstance(EmbeddedDiffEditorWidget, this._elements.previewDiff, _previewEditorEditorOptions, 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));
this._previewEditor = _instantiationService.createInstance(EmbeddedDiffEditorWidget, this._elements.preview, _previewEditorEditorOptions, parentEditor);
this._store.add(this._previewEditor);
}
dispose(): void {
@ -239,9 +250,13 @@ class InteractiveEditorWidget {
this.inputEditor.layout(new Dimension(innerEditorWidth, this.inputEditor.getContentHeight()));
this._elements.placeholder.style.width = `${innerEditorWidth /* input-padding*/}px`;
const previewDim = new Dimension(dim.width, Math.min(300, Math.max(0, this._previewEditor.getContentHeight())));
this._previewEditor.layout(previewDim);
this._elements.preview.style.height = `${previewDim.height}px`;
const previewDiffDim = new Dimension(dim.width, Math.min(300, Math.max(0, this._previewDiffEditor.getContentHeight())));
this._previewDiffEditor.layout(previewDiffDim);
this._elements.previewDiff.style.height = `${previewDiffDim.height}px`;
const previewCreateDim = new Dimension(dim.width, Math.min(300, Math.max(0, this._previewCreateEditor.getContentHeight())));
this._previewCreateEditor.layout(previewCreateDim);
this._elements.previewCreate.style.height = `${previewCreateDim.height}px`;
}
} finally {
this._isLayouting = false;
@ -251,8 +266,10 @@ class InteractiveEditorWidget {
getHeight(): number {
const base = getTotalHeight(this._elements.progress) + getTotalHeight(this._elements.status);
const editorHeight = this.inputEditor.getContentHeight() + 12 /* padding and border */;
const previewHeight = this._previewEditor.getModel() ? 12 + Math.min(300, Math.max(0, this._previewEditor.getContentHeight())) : 0;
return base + editorHeight + previewHeight + 18 /* padding */ + 8 /*shadow*/;
const previewDiffHeight = this._previewDiffEditor.getModel().modified ? 12 + Math.min(300, Math.max(0, this._previewDiffEditor.getContentHeight())) : 0;
const previewCreateTitleHeight = getTotalHeight(this._elements.previewCreateTitle);
const previewCreateHeight = this._previewCreateEditor.getModel() ? 18 + Math.min(300, Math.max(0, this._previewCreateEditor.getContentHeight())) : 0;
return base + editorHeight + previewDiffHeight + previewCreateTitleHeight + previewCreateHeight + 18 /* padding */ + 8 /*shadow*/;
}
updateProgress(show: boolean) {
@ -381,9 +398,8 @@ class InteractiveEditorWidget {
this._ctxInputEmpty.reset();
reset(this._elements.statusLabel);
this._elements.statusToolbar.classList.add('hidden');
this._previewEditor.setModel(null);
this._previewModel.clear();
this._elements.root.classList.remove('preview');
this.hideCreatePreview();
this.hideEditsPreview();
this._onDidChangeHeight.fire();
}
@ -393,8 +409,9 @@ class InteractiveEditorWidget {
// --- preview
preview(actualModel: ITextModel, edits: TextEdit[]) {
showEditsPreview(actualModel: ITextModel, edits: TextEdit[]) {
this._elements.root.classList.add('preview');
this._elements.previewDiff.classList.remove('hidden');
const pad = 3;
const unionRange = (ranges: IRange[]) => ranges.reduce((p, c) => Range.plusRange(p, c));
@ -417,12 +434,42 @@ class InteractiveEditorWidget {
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);
this._previewModel.value = toDisposable(() => {
this._previewDiffModel.value = toDisposable(() => {
original.dispose();
modified.dispose();
});
this._previewEditor.setModel({ original, modified });
this._previewDiffEditor.setModel({ original, modified });
this._onDidChangeHeight.fire();
}
hideEditsPreview() {
this._elements.root.classList.remove('preview');
this._elements.previewDiff.classList.add('hidden');
this._previewDiffEditor.setModel(null);
this._previewDiffModel.clear();
this._onDidChangeHeight.fire();
}
showCreatePreview(uri: URI, edits: TextEdit[]): void {
this._elements.root.classList.add('preview');
this._elements.previewCreateTitle.classList.remove('hidden');
this._elements.previewCreate.classList.remove('hidden');
this._previewCreateTitle.element.setFile(uri, { fileKind: FileKind.FILE });
const model = this._modelService.createModel('', null, undefined, true);
model.applyEdits(edits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)));
this._previewCreateModel.value = model;
this._previewCreateEditor.setModel(model);
this._onDidChangeHeight.fire();
}
hideCreatePreview() {
this._elements.previewCreateTitle.classList.add('hidden');
this._elements.previewCreate.classList.add('hidden');
this._previewCreateEditor.setModel(null);
this._previewCreateTitle.element.clear();
this._onDidChangeHeight.fire();
}
}