diff --git a/src/vs/workbench/api/browser/mainThreadInteractiveEditor.ts b/src/vs/workbench/api/browser/mainThreadInteractiveEditor.ts index 5cd037ec9c4..95caa4d888f 100644 --- a/src/vs/workbench/api/browser/mainThreadInteractiveEditor.ts +++ b/src/vs/workbench/api/browser/mainThreadInteractiveEditor.ts @@ -28,7 +28,7 @@ export class MainThreadInteractiveEditor implements MainThreadInteractiveEditorS this._registrations.dispose(); } - async $registerInteractiveEditorProvider(handle: number, debugName: string): Promise { + async $registerInteractiveEditorProvider(handle: number, debugName: string, supportsFeedback: boolean): Promise { 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 result; + }, + handleInteractiveEditorResponseFeedback: !supportsFeedback ? undefined : async (session, response, kind) => { + this._proxy.$handleFeedback(handle, session.id, response.id, kind); } }); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index e37e47dd007..fdcc47d5092 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -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 diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 1903e50c643..7a450b9ba77 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -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; + $registerInteractiveEditorProvider(handle: number, debugName: string, supportsFeedback: boolean): Promise; $unregisterInteractiveEditorProvider(handle: number): Promise; } @@ -1081,6 +1081,7 @@ export type IInteractiveEditorResponseDto = Dto; export interface ExtHostInteractiveEditorShape { $prepareInteractiveSession(handle: number, uri: UriComponents, range: ISelection, token: CancellationToken): Promise; $provideResponse(handle: number, session: IInteractiveEditorSession, request: IInteractiveEditorRequest, token: CancellationToken): Promise; + $handleFeedback(handle: number, sessionId: number, responseId: number, kind: InteractiveEditorResponseFeedbackKind): void; $releaseSession(handle: number, sessionId: number): void; } diff --git a/src/vs/workbench/api/common/extHostInteractiveEditor.ts b/src/vs/workbench/api/common/extHostInteractiveEditor.ts index f70905d8e2b..01a35d1bc21 100644 --- a/src/vs/workbench/api/common/extHostInteractiveEditor.ts +++ b/src/vs/workbench/api/common/extHostInteractiveEditor.ts @@ -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, 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 = { 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); } diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts index 7a6eb2d1284..9c6e8b3d310 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts @@ -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(); + readonly onDidChange: Event = 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)) { diff --git a/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts b/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts index 91623d7713b..3677488bc09 100644 --- a/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts @@ -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; provideResponse(item: IInteractiveEditorSession, request: IInteractiveEditorRequest, token: CancellationToken): ProviderResult; + + handleInteractiveEditorResponseFeedback?(session: IInteractiveEditorSession, response: IInteractiveEditorResponse, kind: InteractiveEditorResponseFeedbackKind): void; } export const IInteractiveEditorService = createDecorator('IInteractiveEditorService'); diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index 0b4bc6628f2..1cac3ca7728 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -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; }