make feedback explicit and stateful (#178007)

This commit is contained in:
Johannes Rieken 2023-03-22 14:30:07 +01:00 committed by GitHub
parent ee036026aa
commit 2f1f07ec2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 106 additions and 33 deletions

View file

@ -28,7 +28,7 @@ export class MainThreadInteractiveEditor implements MainThreadInteractiveEditorS
this._registrations.dispose();
}
async $registerInteractiveEditorProvider(handle: number, debugName: string): Promise<void> {
async $registerInteractiveEditorProvider(handle: number, debugName: string, supportsFeedback: boolean): Promise<void> {
const unreg = this._interactiveEditorService.addProvider({
debugName,
prepareInteractiveEditorSession: async (model, range, token) => {
@ -49,6 +49,9 @@ export class MainThreadInteractiveEditor implements MainThreadInteractiveEditorS
result.edits = reviveWorkspaceEditDto(result.edits, this._uriIdentService);
}
return <IInteractiveEditorResponse | undefined>result;
},
handleInteractiveEditorResponseFeedback: !supportsFeedback ? undefined : async (session, response, kind) => {
this._proxy.$handleFeedback(handle, session.id, response.id, kind);
}
});

View file

@ -195,7 +195,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol));
const extHostProfileContentHandlers = rpcProtocol.set(ExtHostContext.ExtHostProfileContentHandlers, new ExtHostProfileContentHandlers(rpcProtocol));
rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService));
const extHostInteractiveEditor = rpcProtocol.set(ExtHostContext.ExtHostInteractiveEditor, new ExtHostInteractiveEditor(rpcProtocol, extHostDocuments, extHostLogService, extHostCommands));
const extHostInteractiveEditor = rpcProtocol.set(ExtHostContext.ExtHostInteractiveEditor, new ExtHostInteractiveEditor(rpcProtocol, extHostDocuments, extHostLogService));
const extHostInteractiveSession = rpcProtocol.set(ExtHostContext.ExtHostInteractiveSession, new ExtHostInteractiveSession(rpcProtocol, extHostLogService));
// Check that no named customers are missing

View file

@ -26,7 +26,7 @@ import * as languages from 'vs/editor/common/languages';
import { CharacterPair, CommentRule, EnterAction } from 'vs/editor/common/languages/languageConfiguration';
import { EndOfLineSequence } from 'vs/editor/common/model';
import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel';
import { IInteractiveEditorRequest, IInteractiveEditorResponse, IInteractiveEditorSession } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
import { IInteractiveEditorRequest, IInteractiveEditorResponse, IInteractiveEditorSession, InteractiveEditorResponseFeedbackKind } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility';
import { ConfigurationTarget, IConfigurationChange, IConfigurationData, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration';
import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
@ -1072,7 +1072,7 @@ export interface MainThreadInteractiveShape extends IDisposable {
}
export interface MainThreadInteractiveEditorShape extends IDisposable {
$registerInteractiveEditorProvider(handle: number, debugName: string): Promise<void>;
$registerInteractiveEditorProvider(handle: number, debugName: string, supportsFeedback: boolean): Promise<void>;
$unregisterInteractiveEditorProvider(handle: number): Promise<void>;
}
@ -1081,6 +1081,7 @@ export type IInteractiveEditorResponseDto = Dto<IInteractiveEditorResponse>;
export interface ExtHostInteractiveEditorShape {
$prepareInteractiveSession(handle: number, uri: UriComponents, range: ISelection, token: CancellationToken): Promise<IInteractiveEditorSession | undefined>;
$provideResponse(handle: number, session: IInteractiveEditorSession, request: IInteractiveEditorRequest, token: CancellationToken): Promise<IInteractiveEditorResponseDto | undefined>;
$handleFeedback(handle: number, sessionId: number, responseId: number, kind: InteractiveEditorResponseFeedbackKind): void;
$releaseSession(handle: number, sessionId: number): void;
}

View file

@ -4,10 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { toDisposable } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri';
import { ISelection } from 'vs/editor/common/core/selection';
import { IInteractiveEditorSession, IInteractiveEditorRequest } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
import { IInteractiveEditorSession, IInteractiveEditorRequest, InteractiveEditorResponseFeedbackKind } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { ExtHostInteractiveEditorShape, IInteractiveEditorResponseDto, IMainContext, MainContext, MainThreadInteractiveEditorShape } from 'vs/workbench/api/common/extHost.protocol';
@ -15,7 +15,6 @@ import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters';
import { WorkspaceEdit } from 'vs/workbench/api/common/extHostTypes';
import type * as vscode from 'vscode';
import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
class ProviderWrapper {
@ -31,7 +30,7 @@ class ProviderWrapper {
class SessionWrapper {
readonly store = new DisposableStore();
readonly responses: (vscode.InteractiveEditorResponse | vscode.InteractiveEditorMessageResponse)[] = [];
constructor(
readonly session: vscode.InteractiveEditorSession
@ -50,7 +49,6 @@ export class ExtHostInteractiveEditor implements ExtHostInteractiveEditorShape {
mainContext: IMainContext,
private readonly _documents: ExtHostDocuments,
private readonly _logService: ILogService,
private readonly _commands: ExtHostCommands,
) {
this._proxy = mainContext.getProxy(MainContext.MainThreadInteractiveEditor);
}
@ -58,7 +56,7 @@ export class ExtHostInteractiveEditor implements ExtHostInteractiveEditorShape {
registerProvider(extension: Readonly<IRelaxedExtensionDescription>, provider: vscode.InteractiveEditorSessionProvider): vscode.Disposable {
const wrapper = new ProviderWrapper(extension, provider);
this._inputProvider.set(wrapper.handle, wrapper);
this._proxy.$registerInteractiveEditorProvider(wrapper.handle, extension.identifier.value);
this._proxy.$registerInteractiveEditorProvider(wrapper.handle, extension.identifier.value, typeof provider.handleInteractiveEditorResponseFeedback === 'function');
return toDisposable(() => {
this._proxy.$unregisterInteractiveEditorProvider(wrapper.handle);
this._inputProvider.delete(wrapper.handle);
@ -113,15 +111,17 @@ export class ExtHostInteractiveEditor implements ExtHostInteractiveEditorShape {
if (res) {
const id = sessionData.responses.push(res) - 1;
const stub: Partial<IInteractiveEditorResponseDto> = {
wholeRange: typeConvert.Range.from(res.wholeRange),
placeholder: res.placeholder,
commands: res.commands ? res.commands.map(c => this._commands.converter.toInternal(c, sessionData.store)) : undefined,
};
if (ExtHostInteractiveEditor._isMessageResponse(res)) {
return {
...stub,
id,
type: 'message',
message: typeConvert.MarkdownString.from(res.contents),
};
@ -131,6 +131,7 @@ export class ExtHostInteractiveEditor implements ExtHostInteractiveEditorShape {
if (edits instanceof WorkspaceEdit) {
return {
...stub,
id,
type: 'bulkEdit',
edits: typeConvert.WorkspaceEdit.from(edits),
};
@ -138,6 +139,7 @@ export class ExtHostInteractiveEditor implements ExtHostInteractiveEditorShape {
} else if (Array.isArray(edits)) {
return {
...stub,
id,
type: 'editorEdit',
edits: edits.map(typeConvert.TextEdit.from),
};
@ -147,12 +149,20 @@ export class ExtHostInteractiveEditor implements ExtHostInteractiveEditorShape {
return undefined;
}
$handleFeedback(handle: number, sessionId: number, responseId: number, kind: InteractiveEditorResponseFeedbackKind): void {
const entry = this._inputProvider.get(handle);
const sessionData = this._inputSessions.get(sessionId);
const response = sessionData?.responses[responseId];
if (entry && response) {
entry.provider.handleInteractiveEditorResponseFeedback?.(sessionData.session, response, kind === InteractiveEditorResponseFeedbackKind.Helpful ? true : false);
}
}
$releaseSession(handle: number, sessionId: number) {
const sessionData = this._inputSessions.get(sessionId);
const entry = this._inputProvider.get(handle);
if (sessionData && entry) {
entry.provider.releaseInteractiveEditorSession?.(sessionData.session);
sessionData.store.dispose();
}
this._inputSessions.delete(sessionId);
}

View file

@ -15,7 +15,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget';
import { assertType } from 'vs/base/common/types';
import { 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';
import { 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, IInteractiveEditorSessionProvider, InteractiveEditorResponseFeedbackKind } 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';
@ -52,10 +52,9 @@ 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 { Command, CompletionContext, CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult } from 'vs/editor/common/languages';
import { CompletionContext, CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult } from 'vs/editor/common/languages';
import { LanguageSelector } from 'vs/editor/common/languageSelector';
import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
class InteractiveEditorWidget {
@ -503,14 +502,6 @@ export class InteractiveEditorZoneWidget extends ZoneWidget {
}
}
class CommandAction extends Action {
constructor(command: Command, @ICommandService commandService: ICommandService) {
const icon = ThemeIcon.fromString(command.title);
super(command.id, icon ? command.tooltip : command.title, icon ? ThemeIcon.asClassName(icon) : undefined, true, () => commandService.executeCommand(command.id, ...(command.arguments ?? [])));
}
}
class ToggleInlineDiff extends Action {
constructor(private readonly _inlineDiff: InlineDiffDecorations) {
@ -654,6 +645,63 @@ class InlineDiffDecorations {
}
}
class FeedbackToggles {
private readonly _onDidChange = new Emitter<this>();
readonly onDidChange: Event<this> = this._onDidChange.event;
private readonly _helpful: Action;
private readonly _unHelpful: Action;
constructor(provider: IInteractiveEditorSessionProvider, session: IInteractiveEditorSession, response: IInteractiveEditorResponse) {
const supportsFeedback = typeof provider.handleInteractiveEditorResponseFeedback === 'function';
const update = (kind: InteractiveEditorResponseFeedbackKind) => {
if (supportsFeedback) {
provider.handleInteractiveEditorResponseFeedback!(session, response, kind);
if (kind === InteractiveEditorResponseFeedbackKind.Helpful) {
this._helpful.tooltip = localize('thanks', "Thanks for your feedback!");
this._helpful.checked = true;
this._helpful.enabled = false;
this._unHelpful.enabled = false;
} else {
this._unHelpful.tooltip = localize('thanks', "Thanks for your feedback!");
this._unHelpful.checked = true;
this._unHelpful.enabled = false;
this._helpful.enabled = false;
}
this._onDidChange.fire(this);
}
};
this._helpful = new Action('interactiveEditor.helpful', localize('helpful', "Vote Up"), ThemeIcon.asClassName(Codicon.thumbsup), supportsFeedback, () => update(InteractiveEditorResponseFeedbackKind.Helpful));
this._unHelpful = new Action('interactiveEditor.unHelpful', localize('unhelpful', "Vote Down"), ThemeIcon.asClassName(Codicon.thumbsdown), supportsFeedback, () => update(InteractiveEditorResponseFeedbackKind.Unhelpful));
this._helpful.tooltip = this._helpful.label;
this._unHelpful.tooltip = this._unHelpful.label;
}
dispose() {
this._onDidChange.dispose();
this._helpful.dispose();
this._unHelpful.dispose();
}
get actions() {
const result: IAction[] = [];
if (this._helpful.enabled || this._helpful.checked) {
result.push(this._helpful);
}
if (this._unHelpful.enabled || this._unHelpful.checked) {
result.push(this._unHelpful);
}
return result;
}
}
export class InteractiveEditorController implements IEditorContribution {
static ID = 'interactiveEditor';
@ -962,17 +1010,19 @@ export class InteractiveEditorController implements IEditorContribution {
inlineDiffDecorations.update();
const replyActions: Action[] = reply.commands?.map(command => this._instaService.createInstance(CommandAction, command)) ?? [];
const fixedActions: Action[] = [new UndoAction(textModel), new ToggleInlineDiff(inlineDiffDecorations)];
roundStore.add(combinedDisposable(...replyActions, ...fixedActions));
roundStore.add(combinedDisposable(...fixedActions));
const feedback = new FeedbackToggles(provider, session, reply);
roundStore.add(feedback);
roundStore.add(feedback.onDidChange(() => { statusWidget.update({ actions: Separator.join(feedback.actions, fixedActions) }); }));
const editsCount = (moreMinimalEdits ?? reply.edits).length;
statusWidget.update({
message: editsCount === 1 ? localize('edit.1', "Done, made 1 change") : localize('edit.N', "Done, made {0} changes", editsCount),
classes: [],
actions: Separator.join(replyActions, fixedActions),
actions: Separator.join(feedback.actions, fixedActions),
});
if (!InteractiveEditorController._promptHistory.includes(input.value)) {

View file

@ -8,7 +8,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IRange } from 'vs/editor/common/core/range';
import { ISelection } from 'vs/editor/common/core/selection';
import { Command, ProviderResult, TextEdit, WorkspaceEdit } from 'vs/editor/common/languages';
import { ProviderResult, TextEdit, WorkspaceEdit } from 'vs/editor/common/languages';
import { ITextModel } from 'vs/editor/common/model';
import { localize } from 'vs/nls';
import { MenuId } from 'vs/platform/actions/common/actions';
@ -39,27 +39,32 @@ export interface IInteractiveEditorRequest {
export type IInteractiveEditorResponse = IInteractiveEditorEditResponse | IInteractiveEditorBulkEditResponse | IInteractiveEditorMessageResponse;
export interface IInteractiveEditorEditResponse {
id: number;
type: 'editorEdit';
edits: TextEdit[];
placeholder?: string;
wholeRange?: IRange;
commands?: Command[];
}
export interface IInteractiveEditorBulkEditResponse {
id: number;
type: 'bulkEdit';
edits: WorkspaceEdit;
placeholder?: string;
wholeRange?: IRange;
commands?: Command[];
}
export interface IInteractiveEditorMessageResponse {
id: number;
type: 'message';
message: IMarkdownString;
placeholder?: string;
wholeRange?: IRange;
commands?: Command[];
}
export const enum InteractiveEditorResponseFeedbackKind {
Helpful,
Unhelpful
}
export interface IInteractiveEditorSessionProvider {
@ -69,6 +74,8 @@ export interface IInteractiveEditorSessionProvider {
prepareInteractiveEditorSession(model: ITextModel, range: ISelection, token: CancellationToken): ProviderResult<IInteractiveEditorSession>;
provideResponse(item: IInteractiveEditorSession, request: IInteractiveEditorRequest, token: CancellationToken): ProviderResult<IInteractiveEditorResponse>;
handleInteractiveEditorResponseFeedback?(session: IInteractiveEditorSession, response: IInteractiveEditorResponse, kind: InteractiveEditorResponseFeedbackKind): void;
}
export const IInteractiveEditorService = createDecorator<IInteractiveEditorService>('IInteractiveEditorService');

View file

@ -33,7 +33,6 @@ declare module 'vscode' {
edits: TextEdit[] | WorkspaceEdit;
placeholder?: string;
wholeRange?: Range;
commands?: Command[];
}
// todo@API make classes
@ -41,7 +40,6 @@ declare module 'vscode' {
contents: MarkdownString;
placeholder?: string;
wholeRange?: Range;
commands?: Command[];
}
export interface TextDocumentContext {
@ -58,6 +56,10 @@ declare module 'vscode' {
// eslint-disable-next-line local/vscode-dts-provider-naming
releaseInteractiveEditorSession?(session: InteractiveEditorSession): any;
// todo@API use enum instead of boolean
// eslint-disable-next-line local/vscode-dts-provider-naming
handleInteractiveEditorResponseFeedback?(session: InteractiveEditorSession, response: InteractiveEditorResponse | InteractiveEditorMessageResponse, helpful: boolean): void;
}