Skip to content

Commit ef9afc3

Browse files
authored
joh/inline progress (microsoft#187705)
* allow to report progress when computing inline chat response, wip still needs better handling in the UI strategies * support for async, sequential progress * for now let extension know if progress/live is supported, for live modes apply edits as they happen
1 parent d568855 commit ef9afc3

File tree

11 files changed

+170
-46
lines changed

11 files changed

+170
-46
lines changed

src/vs/editor/common/languages.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent';
1313
import { IDisposable } from 'vs/base/common/lifecycle';
1414
import { ThemeIcon } from 'vs/base/common/themables';
1515
import { URI, UriComponents } from 'vs/base/common/uri';
16-
import { ISingleEditOperation } from 'vs/editor/common/core/editOperation';
16+
import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation';
1717
import { IPosition, Position } from 'vs/editor/common/core/position';
1818
import { IRange, Range } from 'vs/editor/common/core/range';
1919
import { Selection } from 'vs/editor/common/core/selection';
@@ -1222,6 +1222,13 @@ export interface TextEdit {
12221222
eol?: model.EndOfLineSequence;
12231223
}
12241224

1225+
/** @internal */
1226+
export abstract class TextEdit {
1227+
static asEditOperation(edit: TextEdit): ISingleEditOperation {
1228+
return EditOperation.replace(Range.lift(edit.range), edit.text);
1229+
}
1230+
}
1231+
12251232
/**
12261233
* Interface used to format a model
12271234
*/

src/vs/platform/progress/common/progress.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,31 @@ export class Progress<T> implements IProgress<T> {
111111

112112
static readonly None = Object.freeze<IProgress<unknown>>({ report() { } });
113113

114+
report: (item: T) => void;
115+
114116
private _value?: T;
115117
get value(): T | undefined { return this._value; }
116118

117-
constructor(private callback: (data: T) => void) { }
119+
private _lastTask?: Promise<unknown>;
120+
121+
constructor(private callback: (data: T) => unknown, opts?: { async?: boolean }) {
122+
this.report = opts?.async
123+
? this._reportAsync.bind(this)
124+
: this._reportSync.bind(this);
125+
}
118126

119-
report(item: T) {
127+
private _reportSync(item: T) {
120128
this._value = item;
121129
this.callback(this._value);
122130
}
131+
132+
private _reportAsync(item: T) {
133+
Promise.resolve(this._lastTask).finally(() => {
134+
this._value = item;
135+
const r = this.callback(this._value);
136+
this._lastTask = Promise.resolve(r).finally(() => this._lastTask = undefined);
137+
});
138+
}
123139
}
124140

125141
/**

src/vs/workbench/api/browser/mainThreadInlineChat.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'
99
import { reviveWorkspaceEditDto } from 'vs/workbench/api/browser/mainThreadBulkEdits';
1010
import { ExtHostContext, ExtHostInlineChatShape, MainContext, MainThreadInlineChatShape as MainThreadInlineChatShape } from 'vs/workbench/api/common/extHost.protocol';
1111
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
12+
import { TextEdit } from 'vs/editor/common/languages';
13+
import { IProgress } from 'vs/platform/progress/common/progress';
1214

1315
@extHostNamedCustomer(MainContext.MainThreadInlineChat)
1416
export class MainThreadInlineChat implements MainThreadInlineChatShape {
1517

1618
private readonly _registrations = new DisposableMap<number>();
1719
private readonly _proxy: ExtHostInlineChatShape;
1820

21+
private readonly _progresses = new Map<string, IProgress<any>>();
22+
1923
constructor(
2024
extHostContext: IExtHostContext,
2125
@IInlineChatService private readonly _inlineChatService: IInlineChatService,
@@ -43,12 +47,17 @@ export class MainThreadInlineChat implements MainThreadInlineChatShape {
4347
}
4448
};
4549
},
46-
provideResponse: async (item, request, token) => {
47-
const result = await this._proxy.$provideResponse(handle, item, request, token);
48-
if (result?.type === 'bulkEdit') {
49-
(<IInlineChatBulkEditResponse>result).edits = reviveWorkspaceEditDto(result.edits, this._uriIdentService);
50+
provideResponse: async (item, request, progress, token) => {
51+
this._progresses.set(request.requestId, progress);
52+
try {
53+
const result = await this._proxy.$provideResponse(handle, item, request, token);
54+
if (result?.type === 'bulkEdit') {
55+
(<IInlineChatBulkEditResponse>result).edits = reviveWorkspaceEditDto(result.edits, this._uriIdentService);
56+
}
57+
return <IInlineChatResponse | undefined>result;
58+
} finally {
59+
this._progresses.delete(request.requestId);
5060
}
51-
return <IInlineChatResponse | undefined>result;
5261
},
5362
handleInlineChatResponseFeedback: !supportsFeedback ? undefined : async (session, response, kind) => {
5463
this._proxy.$handleFeedback(handle, session.id, response.id, kind);
@@ -58,6 +67,10 @@ export class MainThreadInlineChat implements MainThreadInlineChatShape {
5867
this._registrations.set(handle, unreg);
5968
}
6069

70+
async $handleProgressChunk(requestId: string, chunk: { message?: string | undefined; edits?: TextEdit[] | undefined }): Promise<void> {
71+
this._progresses.get(requestId)?.report(chunk);
72+
}
73+
6174
async $unregisterInteractiveEditorProvider(handle: number): Promise<void> {
6275
this._registrations.deleteAndDispose(handle);
6376
}

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,7 @@ export interface MainThreadInteractiveShape extends IDisposable {
11211121

11221122
export interface MainThreadInlineChatShape extends IDisposable {
11231123
$registerInteractiveEditorProvider(handle: number, debugName: string, supportsFeedback: boolean): Promise<void>;
1124+
$handleProgressChunk(requestId: string, chunk: { message?: string; edits?: languages.TextEdit[] }): Promise<void>;
11241125
$unregisterInteractiveEditorProvider(handle: number): Promise<void>;
11251126
}
11261127

src/vs/workbench/api/common/extHostInlineChat.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,42 @@ export class ExtHostInteractiveEditor implements ExtHostInlineChatShape {
139139
return;
140140
}
141141

142-
const res = await entry.provider.provideInteractiveEditorResponse({
142+
const apiRequest: vscode.InteractiveEditorRequest = {
143143
session: sessionData.session,
144144
prompt: request.prompt,
145145
selection: typeConvert.Selection.to(request.selection),
146146
wholeRange: typeConvert.Range.to(request.wholeRange),
147147
attempt: request.attempt,
148-
}, token);
148+
live: request.live,
149+
};
150+
151+
152+
let done = false;
153+
const progress: vscode.Progress<{ message?: string; edits?: vscode.TextEdit[] }> = {
154+
report: value => {
155+
if (!request.live) {
156+
throw new Error('Progress reporting is only supported for live sessions');
157+
}
158+
if (done || token.isCancellationRequested) {
159+
return;
160+
}
161+
if (!value.message && !value.edits) {
162+
return;
163+
}
164+
this._proxy.$handleProgressChunk(request.requestId, {
165+
message: value.message,
166+
edits: value.edits?.map(typeConvert.TextEdit.from)
167+
});
168+
}
169+
};
170+
171+
const task = typeof entry.provider.provideInteractiveEditorResponse2 === 'function'
172+
? entry.provider.provideInteractiveEditorResponse2(apiRequest, progress, token)
173+
: entry.provider.provideInteractiveEditorResponse(apiRequest, token);
174+
175+
Promise.resolve(task).finally(() => done = true);
176+
177+
const res = await task;
149178

150179
if (res) {
151180

src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'v
1313
import { StopWatch } from 'vs/base/common/stopwatch';
1414
import { assertType } from 'vs/base/common/types';
1515
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
16-
import { EditOperation } from 'vs/editor/common/core/editOperation';
1716
import { IPosition, Position } from 'vs/editor/common/core/position';
1817
import { IRange, Range } from 'vs/editor/common/core/range';
1918
import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon';
@@ -31,11 +30,14 @@ import { ILogService } from 'vs/platform/log/common/log';
3130
import { EditResponse, EmptyResponse, ErrorResponse, ExpansionState, IInlineChatSessionService, MarkdownResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
3231
import { EditModeStrategy, LivePreviewStrategy, LiveStrategy, PreviewStrategy } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies';
3332
import { InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget';
34-
import { CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_LAST_FEEDBACK, IInlineChatRequest, IInlineChatResponse, INLINE_CHAT_ID, EditMode, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, InlineChatResponseType, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, InlineChateResponseTypes, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_USER_DID_EDIT } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
33+
import { CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_LAST_FEEDBACK, IInlineChatRequest, IInlineChatResponse, INLINE_CHAT_ID, EditMode, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, InlineChatResponseType, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, InlineChateResponseTypes, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_USER_DID_EDIT, IInlineChatProgressItem } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
3534
import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
3635
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
3736
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
3837
import { Lazy } from 'vs/base/common/lazy';
38+
import { Progress } from 'vs/platform/progress/common/progress';
39+
import { generateUuid } from 'vs/base/common/uuid';
40+
import { TextEdit } from 'vs/editor/common/languages';
3941

4042
export const enum State {
4143
CREATE_SESSION = 'CREATE_SESSION',
@@ -465,13 +467,31 @@ export class InlineChatController implements IEditorContribution {
465467

466468
const sw = StopWatch.create();
467469
const request: IInlineChatRequest = {
470+
requestId: generateUuid(),
468471
prompt: this._activeSession.lastInput.value,
469472
attempt: this._activeSession.lastInput.attempt,
470473
selection: this._editor.getSelection(),
471474
wholeRange: this._activeSession.wholeRange.value,
475+
live: this._activeSession.editMode !== EditMode.Preview // TODO@jrieken let extension know what document is used for previewing
472476
};
473477
this._chatAccessibilityService.acceptRequest();
474-
const task = this._activeSession.provider.provideResponse(this._activeSession.session, request, requestCts.token);
478+
479+
const progressEdits: TextEdit[][] = [];
480+
const progress = new Progress<IInlineChatProgressItem>(async data => {
481+
this._log('received chunk', data, request);
482+
if (!request.live) {
483+
throw new Error('Progress in NOT supported in non-live mode');
484+
}
485+
if (data.message) {
486+
this._zone.value.widget.updateToolbar(false);
487+
this._zone.value.widget.updateInfo(data.message);
488+
}
489+
if (data.edits) {
490+
progressEdits.push(data.edits);
491+
await this._makeChanges(progressEdits);
492+
}
493+
}, { async: true });
494+
const task = this._activeSession.provider.provideResponse(this._activeSession.session, request, progress, requestCts.token);
475495
this._log('request started', this._activeSession.provider.debugName, this._activeSession.session, request);
476496

477497
let response: EditResponse | MarkdownResponse | ErrorResponse | EmptyResponse;
@@ -482,10 +502,14 @@ export class InlineChatController implements IEditorContribution {
482502
this._ctxHasActiveRequest.set(true);
483503
reply = await raceCancellationError(Promise.resolve(task), requestCts.token);
484504

485-
if (reply?.type === 'message') {
505+
if (reply?.type === InlineChatResponseType.Message) {
486506
response = new MarkdownResponse(this._activeSession.textModelN.uri, reply);
487507
} else if (reply) {
488-
response = new EditResponse(this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), reply);
508+
const editResponse = new EditResponse(this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), reply, progressEdits);
509+
if (editResponse.allLocalEdits.length > progressEdits.length) {
510+
await this._makeChanges(editResponse.allLocalEdits);
511+
}
512+
response = editResponse;
489513
} else {
490514
response = new EmptyResponse();
491515
}
@@ -531,27 +555,41 @@ export class InlineChatController implements IEditorContribution {
531555
if (!canContinue) {
532556
return State.ACCEPT;
533557
}
534-
const moreMinimalEdits = (await this._editorWorkerService.computeHumanReadableDiff(this._activeSession.textModelN.uri, response.localEdits));
535-
const editOperations = (moreMinimalEdits ?? response.localEdits).map(edit => EditOperation.replace(Range.lift(edit.range), edit.text));
536-
this._log('edits from PROVIDER and after making them MORE MINIMAL', this._activeSession.provider.debugName, response.localEdits, moreMinimalEdits);
558+
}
559+
return State.SHOW_RESPONSE;
560+
}
561+
562+
private async _makeChanges(allEdits: TextEdit[][]) {
563+
assertType(this._activeSession);
564+
assertType(this._strategy);
537565

566+
if (allEdits.length === 0) {
567+
return;
568+
}
569+
570+
// diff-changes from model0 -> modelN+1
571+
for (const edits of allEdits) {
538572
const textModelNplus1 = this._modelService.createModel(createTextBufferFactoryFromSnapshot(this._activeSession.textModelN.createSnapshot()), null, undefined, true);
539-
textModelNplus1.applyEdits(editOperations);
573+
textModelNplus1.applyEdits(edits.map(TextEdit.asEditOperation));
540574
const diff = await this._editorWorkerService.computeDiff(this._activeSession.textModel0.uri, textModelNplus1.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: 5000, computeMoves: false }, 'advanced');
541575
this._activeSession.lastTextModelChanges = diff?.changes ?? [];
542576
textModelNplus1.dispose();
543-
544-
try {
545-
this._ignoreModelContentChanged = true;
546-
this._activeSession.wholeRange.trackEdits(editOperations);
547-
await this._strategy.makeChanges(editOperations);
548-
this._ctxDidEdit.set(this._activeSession.hasChangedText);
549-
} finally {
550-
this._ignoreModelContentChanged = false;
551-
}
552577
}
553578

554-
return State.SHOW_RESPONSE;
579+
// make changes from modelN -> modelN+1
580+
const lastEdits = allEdits[allEdits.length - 1];
581+
const moreMinimalEdits = await this._editorWorkerService.computeHumanReadableDiff(this._activeSession.textModelN.uri, lastEdits);
582+
const editOperations = (moreMinimalEdits ?? lastEdits).map(TextEdit.asEditOperation);
583+
this._log('edits from PROVIDER and after making them MORE MINIMAL', this._activeSession.provider.debugName, lastEdits, moreMinimalEdits);
584+
585+
try {
586+
this._ignoreModelContentChanged = true;
587+
this._activeSession.wholeRange.trackEdits(editOperations);
588+
await this._strategy.makeChanges(editOperations);
589+
this._ctxDidEdit.set(this._activeSession.hasChangedText);
590+
} finally {
591+
this._ignoreModelContentChanged = false;
592+
}
555593
}
556594

557595
private async [State.SHOW_RESPONSE](): Promise<State.WAIT_FOR_INPUT | State.ACCEPT> {

src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -296,19 +296,23 @@ export class MarkdownResponse {
296296

297297
export class EditResponse {
298298

299-
readonly localEdits: TextEdit[] = [];
299+
readonly allLocalEdits: TextEdit[][] = [];
300300
readonly singleCreateFileEdit: { uri: URI; edits: Promise<TextEdit>[] } | undefined;
301301
readonly workspaceEdits: ResourceEdit[] | undefined;
302302
readonly workspaceEditsIncludeLocalEdits: boolean = false;
303303

304304
constructor(
305305
localUri: URI,
306306
readonly modelAltVersionId: number,
307-
readonly raw: IInlineChatBulkEditResponse | IInlineChatEditResponse
307+
readonly raw: IInlineChatBulkEditResponse | IInlineChatEditResponse,
308+
progressEdits: TextEdit[][],
308309
) {
310+
311+
this.allLocalEdits.push(...progressEdits);
312+
309313
if (raw.type === 'editorEdit') {
310314
//
311-
this.localEdits = raw.edits;
315+
this.allLocalEdits.push(raw.edits);
312316
this.singleCreateFileEdit = undefined;
313317
this.workspaceEdits = undefined;
314318

@@ -318,6 +322,7 @@ export class EditResponse {
318322
this.workspaceEdits = edits;
319323

320324
let isComplexEdit = false;
325+
const localEdits: TextEdit[] = [];
321326

322327
for (const edit of edits) {
323328
if (edit instanceof ResourceFileEdit) {
@@ -336,7 +341,7 @@ export class EditResponse {
336341
} else if (edit instanceof ResourceTextEdit) {
337342
//
338343
if (isEqual(edit.resource, localUri)) {
339-
this.localEdits.push(edit.textEdit);
344+
localEdits.push(edit.textEdit);
340345
this.workspaceEditsIncludeLocalEdits = true;
341346

342347
} else if (isEqual(this.singleCreateFileEdit?.uri, edit.resource)) {
@@ -346,7 +351,9 @@ export class EditResponse {
346351
}
347352
}
348353
}
349-
354+
if (localEdits.length > 0) {
355+
this.allLocalEdits.push(localEdits);
356+
}
350357
if (isComplexEdit) {
351358
this.singleCreateFileEdit = undefined;
352359
}

0 commit comments

Comments
 (0)