Path: blob/main/extensions/copilot/test/simulation/fixtures/edit-single-line-await-issue-3702/interactiveEditorWidget.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import 'vs/css!./interactiveEditor';6import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';7import { DisposableStore, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';8import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser';9import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions';10import { Range } from 'vs/editor/common/core/range';11import { IEditorContribution, IEditorDecorationsCollection, ScrollType } from 'vs/editor/common/editorCommon';12import { localize } from 'vs/nls';13import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';14import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';15import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget';16import { assertType } from 'vs/base/common/types';17import { IInteractiveEditorResponse, IInteractiveEditorService, CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, 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, IInteractiveEditorRequest, IInteractiveEditorSession, IInteractiveEditorSlashCommand } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';18import { EditOperation } from 'vs/editor/common/core/editOperation';19import { Iterable } from 'vs/base/common/iterator';20import { ICursorStateComputer, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation } from 'vs/editor/common/model';21import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';22import { Dimension, addDisposableListener, getTotalHeight, getTotalWidth, h, reset } from 'vs/base/browser/dom';23import { Emitter, Event } from 'vs/base/common/event';24import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';25import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';26import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';27import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2';28import { IModelService } from 'vs/editor/common/services/model';29import { URI } from 'vs/base/common/uri';30import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';31import { GhostTextController } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextController';32import { MenuWorkbenchToolBar, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar';33import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';34import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController';35import { IPosition, Position } from 'vs/editor/common/core/position';36import { Selection } from 'vs/editor/common/core/selection';37import { raceCancellationError } from 'vs/base/common/async';38import { isCancellationError } from 'vs/base/common/errors';39import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';40import { ILogService } from 'vs/platform/log/common/log';41import { StopWatch } from 'vs/base/common/stopwatch';42import { Action, IAction, Separator } from 'vs/base/common/actions';43import { Codicon } from 'vs/base/common/codicons';44import { ThemeIcon } from 'vs/base/common/themables';45import { LRUCache } from 'vs/base/common/map';46import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';47import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';48import { toErrorMessage } from 'vs/base/common/errorMessage';49import { IInteractiveSessionWidgetService } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget';50import { IViewsService } from 'vs/workbench/common/views';51import { IInteractiveSessionContributionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionContributionService';52import { InteractiveSessionViewPane } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionSidebar';53import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';54import { Command, CompletionContext, CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult } from 'vs/editor/common/languages';55import { LanguageSelector } from 'vs/editor/common/languageSelector';56import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style';57import { ICommandService } from 'vs/platform/commands/common/commands';5859class InteractiveEditorWidget {6061private static _modelPool: number = 1;6263private static _noop = () => { };6465private readonly _elements = h(66'div.interactive-editor@root',67[68h('div.body', [69h('div.content@content', [70h('div.input@input', [71h('div.editor-placeholder@placeholder'),72h('div.editor-container@editor'),73]),74h('div.toolbar@rhsToolbar'),75]),76]),77h('div.progress@progress'),78h('div.status.hidden@status'),79]80);8182private readonly _store = new DisposableStore();83private readonly _historyStore = new DisposableStore();8485readonly inputEditor: ICodeEditor;86private readonly _inputModel: ITextModel;87private readonly _ctxInputEmpty: IContextKey<boolean>;8889private readonly _progressBar: ProgressBar;9091private readonly _onDidChangeHeight = new Emitter<void>();92readonly onDidChangeHeight: Event<void> = Event.filter(this._onDidChangeHeight.event, _ => !this._isLayouting);9394private _editorDim: Dimension | undefined;95private _isLayouting: boolean = false;9697public acceptInput: (preview: boolean) => void = InteractiveEditorWidget._noop;98private _cancelInput: () => void = InteractiveEditorWidget._noop;99100constructor(101parentEditor: ICodeEditor | undefined,102@IModelService private readonly _modelService: IModelService,103@IContextKeyService private readonly _contextKeyService: IContextKeyService,104@IInstantiationService private readonly _instantiationService: IInstantiationService,105) {106107// editor logic108const editorOptions: IEditorConstructionOptions = {109ariaLabel: localize('aria-label', "Interactive Editor Input"),110fontFamily: DEFAULT_FONT_FAMILY,111fontSize: 13,112lineHeight: 20,113padding: { top: 3, bottom: 2 },114wordWrap: 'on',115overviewRulerLanes: 0,116glyphMargin: false,117lineNumbers: 'off',118folding: false,119selectOnLineNumbers: false,120hideCursorInOverviewRuler: true,121selectionHighlight: false,122scrollbar: {123useShadows: false,124vertical: 'hidden',125horizontal: 'auto',126// alwaysConsumeMouseWheel: false127},128lineDecorationsWidth: 0,129overviewRulerBorder: false,130scrollBeyondLastLine: false,131renderLineHighlight: 'none',132fixedOverflowWidgets: true,133dragAndDrop: false,134revealHorizontalRightPadding: 5,135minimap: { enabled: false },136guides: { indentation: false },137cursorWidth: 1,138wrappingStrategy: 'advanced',139wrappingIndent: 'none',140renderWhitespace: 'none',141dropIntoEditor: { enabled: true },142143quickSuggestions: false,144suggest: {145showIcons: false,146showSnippets: false,147}148};149150const codeEditorWidgetOptions: ICodeEditorWidgetOptions = {151isSimpleWidget: true,152contributions: EditorExtensionsRegistry.getSomeEditorContributions([153SnippetController2.ID,154GhostTextController.ID,155SuggestController.ID156])157};158159this.inputEditor = parentEditor160? this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, editorOptions, codeEditorWidgetOptions, parentEditor)161: this._instantiationService.createInstance(CodeEditorWidget, this._elements.editor, editorOptions, codeEditorWidgetOptions);162this._store.add(this.inputEditor);163164const uri = URI.from({ scheme: 'vscode', authority: 'interactive-editor', path: `/interactive-editor/model${InteractiveEditorWidget._modelPool++}.txt` });165this._inputModel = this._modelService.getModel(uri) ?? this._modelService.createModel('', null, uri);166this.inputEditor.setModel(this._inputModel);167168// show/hide placeholder depending on text model being empty169// content height170171const currentContentHeight = 0;172173this._ctxInputEmpty = CTX_INTERACTIVE_EDITOR_EMPTY.bindTo(this._contextKeyService);174const togglePlaceholder = () => {175const hasText = this._inputModel.getValueLength() > 0;176this._elements.placeholder.classList.toggle('hidden', hasText);177this._ctxInputEmpty.set(!hasText);178179const contentHeight = this.inputEditor.getContentHeight();180if (contentHeight !== currentContentHeight && this._editorDim) {181this._editorDim = this._editorDim.with(undefined, contentHeight);182this.inputEditor.layout(this._editorDim);183this._onDidChangeHeight.fire();184}185};186this._store.add(this._inputModel.onDidChangeContent(togglePlaceholder));187togglePlaceholder();188189this._store.add(addDisposableListener(this._elements.placeholder, 'click', () => this.inputEditor.focus()));190191192const toolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.rhsToolbar, MENU_INTERACTIVE_EDITOR_WIDGET, {193telemetrySource: 'interactiveEditorWidget-toolbar',194toolbarOptions: { primaryGroup: 'main' }195});196this._store.add(toolbar);197198this._progressBar = new ProgressBar(this._elements.progress);199this._store.add(this._progressBar);200}201202dispose(): void {203this._store.dispose();204this._historyStore.dispose();205this._ctxInputEmpty.reset();206}207208get domNode(): HTMLElement {209return this._elements.root;210}211212layout(dim: Dimension) {213this._isLayouting = true;214try {215const innerEditorWidth = Math.min(216Number.MAX_SAFE_INTEGER, // TODO@jrieken define max width?217dim.width - (getTotalWidth(this._elements.rhsToolbar) + 12 /* L/R-padding */)218);219const newDim = new Dimension(innerEditorWidth, this.inputEditor.getContentHeight());220if (!this._editorDim || !Dimension.equals(this._editorDim, newDim)) {221this._editorDim = newDim;222this.inputEditor.layout(this._editorDim);223224this._elements.placeholder.style.width = `${innerEditorWidth - 4 /* input-padding*/}px`;225}226} finally {227this._isLayouting = false;228}229}230231getHeight(): number {232const base = getTotalHeight(this._elements.progress) + getTotalHeight(this._elements.status);233const editorHeight = this.inputEditor.getContentHeight() + 6 /* padding and border */;234return base + editorHeight + 12 /* padding */;235}236237updateProgress(show: boolean) {238if (show) {239this._progressBar.infinite();240} else {241this._progressBar.stop();242}243}244245getInput(placeholder: string, value: string, token: CancellationToken): Promise<{ value: string; preview: boolean } | undefined> {246247this._elements.placeholder.innerText = placeholder;248this._elements.placeholder.style.fontSize = `${this.inputEditor.getOption(EditorOption.fontSize)}px`;249this._elements.placeholder.style.lineHeight = `${this.inputEditor.getOption(EditorOption.lineHeight)}px`;250251this._inputModel.setValue(value);252this.inputEditor.setSelection(this._inputModel.getFullModelRange());253254const disposeOnDone = new DisposableStore();255256disposeOnDone.add(this.inputEditor.onDidLayoutChange(() => this._onDidChangeHeight.fire()));257258const ctxInnerCursorFirst = CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST.bindTo(this._contextKeyService);259const ctxInnerCursorLast = CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST.bindTo(this._contextKeyService);260const ctxInputEditorFocused = CTX_INTERACTIVE_EDITOR_FOCUSED.bindTo(this._contextKeyService);261262return new Promise<{ value: string; preview: boolean } | undefined>(resolve => {263264this._cancelInput = () => {265this.acceptInput = InteractiveEditorWidget._noop;266this._cancelInput = InteractiveEditorWidget._noop;267resolve(undefined);268return true;269};270271this.acceptInput = (preview) => {272const newValue = this.inputEditor.getModel()!.getValue();273if (newValue.trim().length === 0) {274// empty or whitespace only275this._cancelInput();276return;277}278279this.acceptInput = InteractiveEditorWidget._noop;280this._cancelInput = InteractiveEditorWidget._noop;281resolve({ value: newValue, preview });282};283284disposeOnDone.add(token.onCancellationRequested(() => this._cancelInput()));285286// CONTEXT KEYS287288// (1) inner cursor position (last/first line selected)289const updateInnerCursorFirstLast = () => {290if (!this.inputEditor.hasModel()) {291return;292}293const { lineNumber } = this.inputEditor.getPosition();294ctxInnerCursorFirst.set(lineNumber === 1);295ctxInnerCursorLast.set(lineNumber === this.inputEditor.getModel().getLineCount());296};297disposeOnDone.add(this.inputEditor.onDidChangeCursorPosition(updateInnerCursorFirstLast));298updateInnerCursorFirstLast();299300// (2) input editor focused or not301const updateFocused = () => {302const hasFocus = this.inputEditor.hasWidgetFocus();303ctxInputEditorFocused.set(hasFocus);304this._elements.content.classList.toggle('synthetic-focus', hasFocus);305};306disposeOnDone.add(this.inputEditor.onDidFocusEditorWidget(updateFocused));307disposeOnDone.add(this.inputEditor.onDidBlurEditorWidget(updateFocused));308updateFocused();309310this.focus();311312}).finally(() => {313disposeOnDone.dispose();314315ctxInnerCursorFirst.reset();316ctxInnerCursorLast.reset();317ctxInputEditorFocused.reset();318});319}320321populateInputField(value: string) {322this._inputModel.setValue(value.trim());323this.inputEditor.setSelection(this._inputModel.getFullModelRange());324}325326createStatusEntry() {327const { root, label, actions } = h('div.status-item@item', [328h('div.label@label'),329h('div.actions@actions'),330]);331332const toolbar = this._instantiationService.createInstance(WorkbenchToolBar, actions, {});333this._historyStore.add(toolbar);334335reset(this._elements.status, root);336this._onDidChangeHeight.fire();337338let oldClasses: string[] = [];339340return {341update: (update: { message?: string; actions?: IAction[]; classes?: string[] }) => {342if (update.message) {343label.innerText = update.message;344this._elements.status.classList.remove('hidden');345}346if (update.actions) {347toolbar.setActions(update.actions);348}349if (update.classes) {350oldClasses.forEach(value => root.classList.remove(value));351oldClasses = update.classes.slice();352root.classList.add(...update.classes);353}354},355updateMessage(message: string) {356label.innerText = message;357},358updateActions(actions: IAction[]) {359toolbar.setActions(actions);360},361updateClasses: (classes: string[]) => {362root.classList.add(...classes);363},364remove: () => {365root.remove();366toolbar.dispose();367this._elements.status.classList.add('hidden');368this._onDidChangeHeight.fire();369}370};371}372373reset() {374this._ctxInputEmpty.reset();375reset(this._elements.status);376}377378focus() {379this.inputEditor.focus();380}381}382383export class InteractiveEditorZoneWidget extends ZoneWidget {384385readonly widget: InteractiveEditorWidget;386387private readonly _ctxVisible: IContextKey<boolean>;388private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>;389390constructor(391editor: ICodeEditor,392@IInstantiationService private readonly _instaService: IInstantiationService,393@IContextKeyService contextKeyService: IContextKeyService,394) {395super(editor, { showFrame: false, showArrow: false, isAccessible: true, className: 'interactive-editor-widget', keepEditorSelection: true });396397this._ctxVisible = CTX_INTERACTIVE_EDITOR_VISIBLE.bindTo(contextKeyService);398this._ctxCursorPosition = CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION.bindTo(contextKeyService);399400this._disposables.add(toDisposable(() => {401this._ctxVisible.reset();402this._ctxCursorPosition.reset();403}));404405this.widget = this._instaService.createInstance(InteractiveEditorWidget, this.editor);406this._disposables.add(this.widget.onDidChangeHeight(() => this._relayout()));407this._disposables.add(this.widget);408this.create();409410411// todo@jrieken listen ONLY when showing412const updateCursorIsAboveContextKey = () => {413if (!this.position || !this.editor.hasModel()) {414this._ctxCursorPosition.reset();415} else if (this.position.lineNumber === this.editor.getPosition().lineNumber) {416this._ctxCursorPosition.set('above');417} else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) {418this._ctxCursorPosition.set('below');419} else {420this._ctxCursorPosition.reset();421}422};423this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey()));424this._disposables.add(this.editor.onDidFocusEditorText(e => updateCursorIsAboveContextKey()));425updateCursorIsAboveContextKey();426}427428protected override _fillContainer(container: HTMLElement): void {429container.appendChild(this.widget.domNode);430}431432protected override _getWidth(info: EditorLayoutInfo): number {433// TODO@jrieken434// makes the zone widget wider than wanted but this aligns435// it with wholeLine decorations that are added above436return info.width;437}438439private _dimension?: Dimension;440441protected override _onWidth(widthInPixel: number): void {442if (this._dimension) {443this._doLayout(this._dimension.height, widthInPixel);444}445}446447protected override _doLayout(heightInPixel: number, widthInPixel: number): void {448449const info = this.editor.getLayoutInfo();450const spaceLeft = info.lineNumbersWidth + info.glyphMarginWidth + info.decorationsWidth;451const spaceRight = info.minimap.minimapWidth + info.verticalScrollbarWidth;452const inputLeftPadding = 4;453const inputRightPadding = 4;454455const width = widthInPixel - (spaceLeft + spaceRight + inputLeftPadding + inputRightPadding);456this._dimension = new Dimension(width, heightInPixel);457this.widget.domNode.style.marginLeft = `${spaceLeft + inputLeftPadding}px`;458this.widget.domNode.style.marginRight = `${spaceRight + inputRightPadding}px`;459this.widget.layout(this._dimension);460}461462private _computeHeightInLines(): number {463const lineHeight = this.editor.getOption(EditorOption.lineHeight);464return this.widget.getHeight() / lineHeight;465}466467protected override _relayout() {468super._relayout(this._computeHeightInLines());469}470471async getInput(where: IPosition, placeholder: string, value: string, token: CancellationToken): Promise<{ value: string; preview: boolean } | undefined> {472assertType(this.editor.hasModel());473super.show(where, this._computeHeightInLines());474this._ctxVisible.set(true);475476const task = this.widget.getInput(placeholder, value, token);477const result = await task;478return result;479}480481updatePosition(where: IPosition) {482// todo@jrieken483// UGYLY: we need to restore focus because showing the zone removes and adds it and that484// means we loose focus for a bit485const hasFocusNow = this.widget.inputEditor.hasWidgetFocus();486super.show(where, this._computeHeightInLines());487if (hasFocusNow) {488this.widget.inputEditor.focus();489}490}491492protected override revealRange(_range: Range, _isLastLine: boolean) {493// disabled494}495496override hide(): void {497this._ctxVisible.reset();498this._ctxCursorPosition.reset();499this.widget.reset();500super.hide();501}502}503504class CommandAction extends Action {505506constructor(command: Command, @ICommandService commandService: ICommandService) {507const icon = ThemeIcon.fromString(command.title);508super(command.id, icon ? command.tooltip : command.title, icon ? ThemeIcon.asClassName(icon) : undefined, true, () => commandService.executeCommand(command.id, ...(command.arguments ?? [])));509}510}511512class ToggleInlineDiff extends Action {513514constructor(private readonly _inlineDiff: InlineDiffDecorations) {515super('diff', localize('toggleInlineDiff', "Toggle Inline Diff"), ThemeIcon.asClassName(Codicon.diff), true);516this.checked = _inlineDiff.visible;517}518519override async run(): Promise<void> {520this._inlineDiff.visible = !this._inlineDiff.visible;521this.checked = this._inlineDiff.visible;522}523}524525class UndoAction extends Action {526527private readonly _myAlternativeVersionId: number;528529constructor(private readonly _model: ITextModel) {530super('undo', localize('undo', "Undo"), ThemeIcon.asClassName(Codicon.discard), false);531this._myAlternativeVersionId = _model.getAlternativeVersionId();532533const update = () => {534this.enabled = this._myAlternativeVersionId === this._model.getAlternativeVersionId();535};536this._store.add(_model.onDidChangeContent(() => update()));537update();538}539540override async run(): Promise<void> {541if (this._myAlternativeVersionId === this._model.getAlternativeVersionId()) {542this._model.undo();543}544}545}546547type Exchange = { req: IInteractiveEditorRequest; res: IInteractiveEditorResponse };548export type Recording = { when: Date; session: IInteractiveEditorSession; value: string; exchanges: Exchange[] };549550class SessionRecorder {551552private readonly _data = new LRUCache<IInteractiveEditorSession, Recording>(3);553554add(session: IInteractiveEditorSession, model: ITextModel) {555this._data.set(session, { when: new Date(), session, value: model.getValue(), exchanges: [] });556}557558addExchange(session: IInteractiveEditorSession, req: IInteractiveEditorRequest, res: IInteractiveEditorResponse) {559this._data.get(session)?.exchanges.push({ req, res });560}561562getAll(): Recording[] {563return [...this._data.values()];564}565}566567type TelemetryData = {568extension: string;569rounds: string;570undos: string;571edits: boolean;572terminalEdits: boolean;573startTime: string;574endTime: string;575};576577type TelemetryDataClassification = {578owner: 'jrieken';579comment: 'Data about an interaction editor session';580extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension providing the data' };581rounds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of request that were made' };582undos: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Requests that have been undone' };583edits: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Did edits happen while the session was active' };584terminalEdits: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Did edits terminal the session' };585startTime: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When the session started' };586endTime: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When the session ended' };587};588589class InlineDiffDecorations {590591private readonly _collection: IEditorDecorationsCollection;592593private _data: { tracking: IModelDeltaDecoration; decorating: IModelDecorationOptions }[] = [];594private _visible: boolean = false;595596constructor(editor: ICodeEditor, visible: boolean = false) {597this._collection = editor.createDecorationsCollection();598this._visible = visible;599}600601get visible() {602return this._visible;603}604605set visible(value: boolean) {606this._visible = value;607this.update();608}609610clear() {611this._collection.clear();612this._data.length = 0;613}614615collectEditOperation(op: IValidEditOperation) {616this._data.push(InlineDiffDecorations._asDecorationData(op));617}618619update() {620this._collection.set(this._data.map(d => {621const res = { ...d.tracking };622if (this._visible) {623res.options = { ...res.options, ...d.decorating };624}625return res;626}));627}628629private static _asDecorationData(edit: IValidEditOperation): { tracking: IModelDeltaDecoration; decorating: IModelDecorationOptions } {630let content = edit.text;631if (content.length > 12) {632content = content.substring(0, 12) + '…';633}634const tracking: IModelDeltaDecoration = {635range: edit.range,636options: {637description: 'interactive-editor-inline-diff',638}639};640641const decorating: IModelDecorationOptions = {642description: 'interactive-editor-inline-diff',643className: 'interactive-editor-lines-inserted-range',644before: {645content,646inlineClassName: 'interactive-editor-lines-deleted-range-inline',647attachedData: edit648}649};650651return { tracking, decorating };652}653}654655export class InteractiveEditorController implements IEditorContribution {656657static ID = 'interactiveEditor';658659static get(editor: ICodeEditor) {660return editor.getContribution<InteractiveEditorController>(InteractiveEditorController.ID);661}662663private static _decoBlock = ModelDecorationOptions.register({664description: 'interactive-editor',665blockClassName: 'interactive-editor-block',666blockDoesNotCollapse: true,667blockPadding: [4, 0, 1, 4]668});669670private static _decoWholeRange = ModelDecorationOptions.register({671description: 'interactive-editor-marker'672});673674private static _promptHistory: string[] = [];675private _historyOffset: number = -1;676677private readonly _store = new DisposableStore();678private readonly _recorder = new SessionRecorder();679private readonly _zone: InteractiveEditorZoneWidget;680private readonly _ctxHasActiveRequest: IContextKey<boolean>;681private _inlineDiffEnabled: boolean = false;682683private _ctsSession: CancellationTokenSource = new CancellationTokenSource();684private _ctsRequest?: CancellationTokenSource;685686constructor(687private readonly _editor: ICodeEditor,688@IInstantiationService private readonly _instaService: IInstantiationService,689@IContextKeyService contextKeyService: IContextKeyService,690@IInteractiveEditorService private readonly _interactiveEditorService: IInteractiveEditorService,691@IBulkEditService private readonly _bulkEditService: IBulkEditService,692@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,693@ILogService private readonly _logService: ILogService,694@ITelemetryService private readonly _telemetryService: ITelemetryService695) {696this._zone = this._store.add(_instaService.createInstance(InteractiveEditorZoneWidget, this._editor));697this._ctxHasActiveRequest = CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST.bindTo(contextKeyService);698}699700dispose(): void {701this._store.dispose();702this._ctsSession.dispose(true);703this._ctsSession.dispose();704}705706getId(): string {707return InteractiveEditorController.ID;708}709710async run(initialRange?: Range): Promise<void> {711712this._ctsSession.dispose(true);713714if (!this._editor.hasModel()) {715return;716}717718const provider = Iterable.first(this._interactiveEditorService.getAllProvider());719if (!provider) {720this._logService.trace('[IE] NO provider found');721return;722}723724const thisSession = this._ctsSession = new CancellationTokenSource();725const textModel = this._editor.getModel();726const selection = this._editor.getSelection();727const session = await provider.prepareInteractiveEditorSession(textModel, selection, this._ctsSession.token);728if (!session) {729this._logService.trace('[IE] NO session', provider.debugName);730return;731}732this._recorder.add(session, textModel);733this._logService.trace('[IE] NEW session', provider.debugName);734735const data: TelemetryData = {736extension: provider.debugName,737startTime: new Date().toISOString(),738endTime: new Date().toISOString(),739edits: false,740terminalEdits: false,741rounds: '',742undos: ''743};744745const statusWidget = this._zone.widget.createStatusEntry();746const inlineDiffDecorations = new InlineDiffDecorations(this._editor, this._inlineDiffEnabled);747748const blockDecoration = this._editor.createDecorationsCollection();749const wholeRangeDecoration = this._editor.createDecorationsCollection();750751if (!initialRange) {752initialRange = session.wholeRange ? Range.lift(session.wholeRange) : selection;753}754if (initialRange.isEmpty()) {755initialRange = new Range(756initialRange.startLineNumber, 1,757initialRange.startLineNumber, textModel.getLineMaxColumn(initialRange.startLineNumber)758);759}760wholeRangeDecoration.set([{761range: initialRange,762options: InteractiveEditorController._decoWholeRange763}]);764765766let placeholder = session.placeholder ?? '';767let value = '';768769const store = new DisposableStore();770771if (session.slashCommands) {772store.add(this._instaService.invokeFunction(installSlashCommandSupport, this._zone.widget.inputEditor as IActiveCodeEditor, session.slashCommands));773}774775// CANCEL when input changes776this._editor.onDidChangeModel(this._ctsSession.cancel, this._ctsSession, store);777778// REposition the zone widget whenever the block decoration changes779let lastPost: Position | undefined;780wholeRangeDecoration.onDidChange(e => {781const range = wholeRangeDecoration.getRange(0);782if (range && (!lastPost || !lastPost.equals(range.getEndPosition()))) {783lastPost = range.getEndPosition();784this._zone.updatePosition(lastPost);785}786}, undefined, store);787788let ignoreModelChanges = false;789this._editor.onDidChangeModelContent(e => {790if (!ignoreModelChanges) {791792// remove inline diff when the model changes793inlineDiffDecorations.clear();794795// note when "other" edits happen796data.edits = true;797798// CANCEL if the document has changed outside the current range799const wholeRange = wholeRangeDecoration.getRange(0);800if (!wholeRange) {801this._ctsSession.cancel();802this._logService.trace('[IE] ABORT wholeRange seems gone/collapsed');803return;804}805for (const change of e.changes) {806if (!Range.areIntersectingOrTouching(wholeRange, change.range)) {807this._ctsSession.cancel();808this._logService.trace('[IE] CANCEL because of model change OUTSIDE range');809data.terminalEdits = true;810break;811}812}813}814815}, undefined, store);816817let round = 0;818const roundStore = new DisposableStore();819store.add(roundStore);820821do {822823round += 1;824825const wholeRange = wholeRangeDecoration.getRange(0);826if (!wholeRange) {827// nuked whole file contents?828this._logService.trace('[IE] ABORT wholeRange seems gone/collapsed');829break;830}831832// visuals: add block decoration833blockDecoration.set([{834range: wholeRange,835options: InteractiveEditorController._decoBlock836}]);837838this._ctsRequest?.dispose(true);839this._ctsRequest = new CancellationTokenSource(this._ctsSession.token);840841this._historyOffset = -1;842this._editor.revealRange(wholeRange, ScrollType.Smooth);843const input = await this._zone.getInput(wholeRange.getEndPosition(), placeholder, value, this._ctsRequest.token);844roundStore.clear();845846if (!input || !input.value) {847continue;848}849850const refer = session.slashCommands?.some(value => value.refer && input.value.startsWith(`/${value.command}`));851if (refer) {852this._logService.info('[IE] seeing refer command, continuing outside editor', provider.debugName);853this._editor.setSelection(wholeRange);854this._instaService.invokeFunction(showMessageResponse, input.value);855continue;856}857858const sw = StopWatch.create();859const request: IInteractiveEditorRequest = {860prompt: input.value,861selection: this._editor.getSelection(),862wholeRange863};864const task = provider.provideResponse(session, request, this._ctsRequest.token);865this._logService.trace('[IE] request started', provider.debugName, session, request);866867let reply: IInteractiveEditorResponse | null | undefined;868try {869this._zone.widget.updateProgress(true);870this._ctxHasActiveRequest.set(true);871reply = await raceCancellationError(Promise.resolve(task), this._ctsRequest.token);872873} catch (e) {874if (!isCancellationError(e)) {875this._logService.error('[IE] ERROR during request', provider.debugName);876this._logService.error(e);877// this._zone.widget.showMessage(toErrorMessage(e));878statusWidget.update({ message: toErrorMessage(e), classes: ['error'], actions: [] });879// statusWidget880continue;881}882} finally {883this._ctxHasActiveRequest.set(false);884this._zone.widget.updateProgress(false);885this._logService.trace('[IE] request took', sw.elapsed(), provider.debugName);886}887888if (this._ctsRequest.token.isCancellationRequested) {889this._logService.trace('[IE] request CANCELED', provider.debugName);890value = input.value;891continue;892}893894if (!reply) {895this._logService.trace('[IE] NO reply or edits', provider.debugName);896value = input.value;897statusWidget.update({ message: localize('empty', "No results, tweak your input and try again."), classes: ['warn'], actions: [] });898continue;899}900901if (reply.type === 'bulkEdit') {902this._logService.info('[IE] performaing a BULK EDIT, exiting interactive editor', provider.debugName);903this._bulkEditService.apply(reply.edits, { editor: this._editor, label: localize('ie', "{0}", input.value), showPreview: true });904// todo@jrieken preview bulk edit?905// todo@jrieken keep interactive editor?906break;907}908909if (reply.type === 'message') {910this._logService.info('[IE] received a MESSAGE, continuing outside editor', provider.debugName);911this._editor.setSelection(reply.wholeRange ?? wholeRange);912this._instaService.invokeFunction(showMessageResponse, request.prompt);913continue;914}915916// make edits more minimal917const moreMinimalEdits = (await this._editorWorkerService.computeMoreMinimalEdits(textModel.uri, reply.edits, true));918this._logService.trace('[IE] edits from PROVIDER and after making them MORE MINIMAL', provider.debugName, reply.edits, moreMinimalEdits);919this._recorder.addExchange(session, request, reply);920921// inline diff922inlineDiffDecorations.clear();923924// use whole range from reply925if (reply.wholeRange) {926wholeRangeDecoration.set([{927range: reply.wholeRange,928options: InteractiveEditorController._decoWholeRange929}]);930}931932try {933ignoreModelChanges = true;934935const cursorStateComputerAndInlineDiffCollection: ICursorStateComputer = (undoEdits) => {936let last: Position | null = null;937for (const edit of undoEdits) {938last = !last || last.isBefore(edit.range.getEndPosition()) ? edit.range.getEndPosition() : last;939inlineDiffDecorations.collectEditOperation(edit);940}941return last && [Selection.fromPositions(last)];942};943944this._editor.pushUndoStop();945this._editor.executeEdits(946'interactive-editor',947(moreMinimalEdits ?? reply.edits).map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)),948cursorStateComputerAndInlineDiffCollection949);950this._editor.pushUndoStop();951952} finally {953ignoreModelChanges = false;954}955956inlineDiffDecorations.update();957958959const replyActions: Action[] = reply.commands?.map(command => this._instaService.createInstance(CommandAction, command)) ?? [];960const fixedActions: Action[] = [new UndoAction(textModel), new ToggleInlineDiff(inlineDiffDecorations)];961roundStore.add(combinedDisposable(...replyActions, ...fixedActions));962963const editsCount = (moreMinimalEdits ?? reply.edits).length;964965statusWidget.update({966message: editsCount === 1 ? localize('edit.1', "Done, made 1 change") : localize('edit.N', "Done, made {0} changes", editsCount),967classes: [],968actions: Separator.join(replyActions, fixedActions),969});970971if (!InteractiveEditorController._promptHistory.includes(input.value)) {972InteractiveEditorController._promptHistory.unshift(input.value);973}974placeholder = reply.placeholder ?? session.placeholder ?? '';975value = '';976data.rounds += round + '|';977978} while (!thisSession.token.isCancellationRequested);979980this._inlineDiffEnabled = inlineDiffDecorations.visible;981982// done, cleanup983wholeRangeDecoration.clear();984blockDecoration.clear();985inlineDiffDecorations.clear();986987store.dispose();988session.dispose?.();989990991this._zone.hide();992this._editor.focus();993994this._logService.trace('[IE] session DONE', provider.debugName);995data.endTime = new Date().toISOString();996997this._telemetryService.publicLog2<TelemetryData, TelemetryDataClassification>('interactiveEditor/session', data);998}9991000accept(preview: boolean): void {1001this._zone.widget.acceptInput(preview);1002}10031004cancelCurrentRequest(): void {1005this._ctsRequest?.cancel();1006}10071008cancelSession() {1009this._ctsSession.cancel();1010}10111012arrowOut(up: boolean): void {1013if (this._zone.position && this._editor.hasModel()) {1014const { column } = this._editor.getPosition();1015const { lineNumber } = this._zone.position;1016const newLine = up ? lineNumber : lineNumber + 1;1017this._editor.setPosition({ lineNumber: newLine, column });1018this._editor.focus();1019}1020}10211022focus(): void {1023this._zone.widget.focus();1024}10251026populateHistory(up: boolean) {1027const len = InteractiveEditorController._promptHistory.length;1028if (len === 0) {1029return;1030}1031const pos = (len + this._historyOffset + (up ? 1 : -1)) % len;1032const entry = InteractiveEditorController._promptHistory[pos];1033this._zone.widget.populateInputField(entry);1034this._historyOffset = pos;1035}10361037recordings() {1038return this._recorder.getAll();1039}1040}10411042function installSlashCommandSupport(accessor: ServicesAccessor, editor: IActiveCodeEditor, commands: IInteractiveEditorSlashCommand[]) {10431044const languageFeaturesService = accessor.get(ILanguageFeaturesService);10451046const store = new DisposableStore();1047const selector: LanguageSelector = { scheme: editor.getModel().uri.scheme, pattern: editor.getModel().uri.path, language: editor.getModel().getLanguageId() };1048store.add(languageFeaturesService.completionProvider.register(selector, new class implements CompletionItemProvider {10491050_debugDisplayName?: string = 'InteractiveEditorSlashCommandProvider';10511052readonly triggerCharacters?: string[] = ['/'];10531054provideCompletionItems(model: ITextModel, position: Position, context: CompletionContext, token: CancellationToken): ProviderResult<CompletionList> {1055if (position.lineNumber !== 1 && position.column !== 1) {1056return undefined;1057}10581059const suggestions: CompletionItem[] = commands.map(command => {10601061const withSlash = `/${command.command}`;10621063return {1064label: withSlash,1065insertText: `${withSlash} $0`,1066insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet,1067kind: CompletionItemKind.Text,1068range: new Range(1, 1, 1, 1),1069detail: command.detail1070};1071});10721073return { suggestions };1074}1075}));10761077const decorations = editor.createDecorationsCollection();10781079const updateSlashDecorations = () => {1080const newDecorations: IModelDeltaDecoration[] = [];1081for (const command of commands) {1082const withSlash = `/${command.command}`;1083const firstLine = editor.getModel().getLineContent(1);1084if (firstLine.startsWith(withSlash)) {1085newDecorations.push({1086range: new Range(1, 1, 1, withSlash.length + 1),1087options: {1088description: 'interactive-editor-slash-command',1089inlineClassName: 'interactive-editor-slash-command',1090}1091});10921093// inject detail when otherwise empty1094if (firstLine === `/${command.command} `) {1095newDecorations.push({1096range: new Range(1, withSlash.length + 1, 1, withSlash.length + 2),1097options: {1098description: 'interactive-editor-slash-command-detail',1099after: {1100content: `${command.detail}`,1101inlineClassName: 'interactive-editor-slash-command-detail'1102}1103}1104});1105}1106break;1107}1108}1109decorations.set(newDecorations);1110};11111112store.add(editor.onDidChangeModelContent(updateSlashDecorations));1113updateSlashDecorations();11141115return store;1116}11171118async function showMessageResponse(accessor: ServicesAccessor, query: string) {111911201121const widgetService = accessor.get(IInteractiveSessionWidgetService);1122const viewsService = accessor.get(IViewsService);1123const interactiveSessionContributionService = accessor.get(IInteractiveSessionContributionService);11241125if (widgetService.lastFocusedWidget && widgetService.lastFocusedWidget.viewId) {1126// option 1 - take the most recent view1127viewsService.openView(widgetService.lastFocusedWidget.viewId, true);1128widgetService.lastFocusedWidget.acceptInput(query);11291130} else {1131// fallback - take the first view that's openable1132for (const { id } of interactiveSessionContributionService.registeredProviders) {1133const viewId = interactiveSessionContributionService.getViewIdForProvider(id);1134const view = await viewsService.openView<InteractiveSessionViewPane>(viewId, true);1135if (view) {1136view.acceptInput(query);1137break;1138}1139}1140}1141}114211431144