From 682afdd61dd66e83cfdbefdf6b8214dbbc65e135 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 14 Apr 2023 17:22:06 -0700 Subject: [PATCH] More codeblock actions and padding tweak (#180017) * Fix chat codeblock padding * Add more codeblock actions --- .../interactiveSessionCodeblockActions.ts | 98 +++++++++++++++++++ .../browser/interactiveSessionListRenderer.ts | 32 +++++- .../common/interactiveSessionService.ts | 10 +- .../common/interactiveSessionServiceImpl.ts | 20 ++++ .../vscode.proposed.interactive.d.ts | 10 +- 5 files changed, 163 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/interactiveSession/browser/actions/interactiveSessionCodeblockActions.ts b/src/vs/workbench/contrib/interactiveSession/browser/actions/interactiveSessionCodeblockActions.ts index 9c52ec2dd8f..1559c0f2269 100644 --- a/src/vs/workbench/contrib/interactiveSession/browser/actions/interactiveSessionCodeblockActions.ts +++ b/src/vs/workbench/contrib/interactiveSession/browser/actions/interactiveSessionCodeblockActions.ts @@ -11,14 +11,18 @@ import { Range } from 'vs/editor/common/core/range'; import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; +import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; import { INTERACTIVE_SESSION_CATEGORY } from 'vs/workbench/contrib/interactiveSession/browser/actions/interactiveSessionActions'; import { IInteractiveSessionCopyAction, IInteractiveSessionService, IInteractiveSessionUserActionEvent, InteractiveSessionCopyKind } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService'; import { IInteractiveResponseViewModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel'; +import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; export interface IInteractiveSessionCodeBlockActionContext { code: string; + languageId: string; codeBlockIndex: number; element: IInteractiveResponseViewModel; } @@ -137,4 +141,98 @@ export function registerInteractiveSessionCodeBlockActions() { } }); + + registerAction2(class InsertIntoNewFileAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.interactiveSession.insertIntoNewFile', + title: { + value: localize('interactive.insertIntoNewFile.label', "Insert Into New File"), + original: 'Insert Into New File' + }, + f1: false, + category: INTERACTIVE_SESSION_CATEGORY, + menu: { + id: MenuId.InteractiveSessionCodeBlock, + } + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]) { + const context = args[0]; + if (!isCodeBlockActionContext(context)) { + return; + } + + const editorService = accessor.get(IEditorService); + const interactiveSessionService = accessor.get(IInteractiveSessionService); + editorService.openEditor({ contents: context.code, languageId: context.languageId, resource: undefined }); + + interactiveSessionService.notifyUserAction({ + providerId: context.element.providerId, + action: { + kind: 'insert', + responseId: context.element.providerResponseId, + codeBlockIndex: context.codeBlockIndex, + totalCharacters: context.code.length, + newFile: true + } + }); + } + }); + + registerAction2(class RunInTerminalAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.interactiveSession.runInTerminal', + title: { + value: localize('interactive.runInTerminal.label', "Run in Terminal"), + original: 'Run in Terminal' + }, + f1: false, + category: INTERACTIVE_SESSION_CATEGORY, + menu: { + id: MenuId.InteractiveSessionCodeBlock, + } + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]) { + const context = args[0]; + if (!isCodeBlockActionContext(context)) { + return; + } + + const interactiveSessionService = accessor.get(IInteractiveSessionService); + const terminalService = accessor.get(ITerminalService); + const editorService = accessor.get(IEditorService); + const terminalEditorService = accessor.get(ITerminalEditorService); + const terminalGroupService = accessor.get(ITerminalGroupService); + + let terminal = await terminalService.getActiveOrCreateInstance(); + + // Why does getActiveOrCreateInstance return a disposed terminal? + terminal = terminal.isDisposed ? await terminalService.createTerminal() : terminal; + + await terminal.focusWhenReady(); + if (terminal.target === TerminalLocation.Editor) { + const existingEditors = editorService.findEditors(terminal.resource); + terminalEditorService.openEditor(terminal, { viewColumn: existingEditors?.[0].groupId }); + } else { + terminalGroupService.showPanel(true); + } + + terminal.sendText(context.code, false); + + interactiveSessionService.notifyUserAction({ + providerId: context.element.providerId, + action: { + kind: 'runInTerminal', + responseId: context.element.providerResponseId, + codeBlockIndex: context.codeBlockIndex, + languageId: context.languageId, + } + }); + } + }); } diff --git a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionListRenderer.ts b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionListRenderer.ts index ed6d43cae9e..ef1121b4400 100644 --- a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionListRenderer.ts +++ b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionListRenderer.ts @@ -19,6 +19,7 @@ import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecyc import { ResourceMap } from 'vs/base/common/map'; import { FileAccess } from 'vs/base/common/network'; import { ThemeIcon } from 'vs/base/common/themables'; +import { withNullAsUndefined } from 'vs/base/common/types'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions'; @@ -520,6 +521,8 @@ export interface IInteractiveResultCodeBlockInfo { export const codeBlockInfosByModelUri = new ResourceMap(); +const defaultCodeblockPadding = 10; + class CodeBlockPart extends Disposable implements IInteractiveResultCodeBlockPart { private readonly _onDidChangeContentHeight = this._register(new Emitter()); public readonly onDidChangeContentHeight = this._onDidChangeContentHeight.event; @@ -531,6 +534,8 @@ class CodeBlockPart extends Disposable implements IInteractiveResultCodeBlockPar public readonly textModel: ITextModel; public readonly element: HTMLElement; + private currentScrollWidth = 0; + constructor( private readonly options: InteractiveSessionEditorOptions, @IInstantiationService instantiationService: IInstantiationService, @@ -558,7 +563,7 @@ class CodeBlockPart extends Disposable implements IInteractiveResultCodeBlockPar scrollBeyondLastLine: false, lineDecorationsWidth: 8, dragAndDrop: false, - padding: { top: 2, bottom: 2 }, + padding: { top: defaultCodeblockPadding, bottom: defaultCodeblockPadding }, mouseWheelZoom: false, scrollbar: { alwaysConsumeMouseWheel: false @@ -583,6 +588,9 @@ class CodeBlockPart extends Disposable implements IInteractiveResultCodeBlockPar this.editor.updateOptions(this.getEditorOptionsFromConfig()); })); + this._register(this.editor.onDidScrollChange(e => { + this.currentScrollWidth = e.scrollWidth; + })); this._register(this.editor.onDidContentSizeChange(e => { if (e.contentHeightChanged) { this._onDidChangeContentHeight.fire(e.contentHeight); @@ -600,6 +608,17 @@ class CodeBlockPart extends Disposable implements IInteractiveResultCodeBlockPar this.editor.setModel(this.textModel); } + private updatePaddingForLayout() { + // scrollWidth = "the width of the content that needs to be scrolled" + // contentWidth = "the width of the area where content is displayed" + const horizontalScrollbarVisible = this.currentScrollWidth > this.editor.getLayoutInfo().contentWidth; + const scrollbarHeight = this.editor.getLayoutInfo().horizontalScrollbarHeight; + const bottomPadding = horizontalScrollbarVisible ? + Math.max(defaultCodeblockPadding - scrollbarHeight, 2) : + defaultCodeblockPadding; + this.editor.updateOptions({ padding: { top: defaultCodeblockPadding, bottom: bottomPadding } }); + } + private getEditorOptionsFromConfig(): IEditorOptions { return { wordWrap: this.options.configuration.resultEditor.wordWrap, @@ -618,6 +637,7 @@ class CodeBlockPart extends Disposable implements IInteractiveResultCodeBlockPar const realContentHeight = this.editor.getContentHeight(); const editorBorder = 2; this.editor.layout({ width: width - editorBorder, height: realContentHeight }); + this.updatePaddingForLayout(); } render(data: IInteractiveResultCodeBlockData, width: number): void { @@ -631,7 +651,9 @@ class CodeBlockPart extends Disposable implements IInteractiveResultCodeBlockPar const text = this.fixCodeText(data.text, data.languageId); this.setText(text); - this.setLanguage(data.languageId); + + const vscodeLanguageId = withNullAsUndefined(this.languageService.getLanguageIdByLanguageName(data.languageId)); + this.setLanguage(vscodeLanguageId); this.layout(width); @@ -649,7 +671,8 @@ class CodeBlockPart extends Disposable implements IInteractiveResultCodeBlockPar this.toolbar.context = { code: data.text, codeBlockIndex: data.codeBlockIndex, - element: data.element + element: data.element, + languageId: vscodeLanguageId }; } @@ -692,8 +715,7 @@ class CodeBlockPart extends Disposable implements IInteractiveResultCodeBlockPar } } - private setLanguage(languageId: string): void { - const vscodeLanguageId = this.languageService.getLanguageIdByLanguageName(languageId); + private setLanguage(vscodeLanguageId: string | undefined): void { this.textModel.setLanguage(vscodeLanguageId ?? PLAINTEXT_LANGUAGE_ID); } } diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionService.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionService.ts index 0cc104c66cc..70c5b035c5d 100644 --- a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionService.ts +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionService.ts @@ -111,6 +111,14 @@ export interface IInteractiveSessionInsertAction { responseId: string; codeBlockIndex: number; totalCharacters: number; + newFile?: boolean; +} + +export interface IInteractiveSessionTerminalAction { + kind: 'runInTerminal'; + responseId: string; + codeBlockIndex: number; + languageId?: string; } export interface IInteractiveSessionCommandAction { @@ -118,7 +126,7 @@ export interface IInteractiveSessionCommandAction { command: IInteractiveSessionResponseCommandFollowup; } -export type InteractiveSessionUserAction = IInteractiveSessionVoteAction | IInteractiveSessionCopyAction | IInteractiveSessionInsertAction | IInteractiveSessionCommandAction; +export type InteractiveSessionUserAction = IInteractiveSessionVoteAction | IInteractiveSessionCopyAction | IInteractiveSessionInsertAction | IInteractiveSessionTerminalAction | IInteractiveSessionCommandAction; export interface IInteractiveSessionUserActionEvent { action: InteractiveSessionUserAction; diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl.ts index 2affbd3a5e7..8f80d7e4ed5 100644 --- a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl.ts @@ -70,10 +70,12 @@ type InteractiveSessionCopyClassification = { type InteractiveSessionInsertEvent = { providerId: string; + newFile: boolean; }; type InteractiveSessionInsertClassification = { providerId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The identifier of the provider that this codeblock response came from.' }; + newFile: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the code was inserted into a new untitled file.' }; owner: 'roblourens'; comment: 'Provides insight into the usage of InteractiveSession features.'; }; @@ -90,6 +92,18 @@ type InteractiveSessionCommandClassification = { comment: 'Provides insight into the usage of InteractiveSession features.'; }; +type InteractiveSessionTerminalEvent = { + providerId: string; + languageId: string; +}; + +type InteractiveSessionTerminalClassification = { + providerId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The identifier of the provider that this codeblock response came from.' }; + languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language of the code that was run in the terminal.' }; + owner: 'roblourens'; + comment: 'Provides insight into the usage of InteractiveSession features.'; +}; + export class InteractiveSessionService extends Disposable implements IInteractiveSessionService { declare _serviceBrand: undefined; @@ -148,6 +162,7 @@ export class InteractiveSessionService extends Disposable implements IInteractiv } else if (action.action.kind === 'insert') { this.telemetryService.publicLog2('interactiveSessionInsert', { providerId: action.providerId, + newFile: !!action.action.newFile }); } else if (action.action.kind === 'command') { const command = CommandsRegistry.getCommand(action.action.command.commandId); @@ -156,6 +171,11 @@ export class InteractiveSessionService extends Disposable implements IInteractiv providerId: action.providerId, commandId }); + } else if (action.action.kind === 'runInTerminal') { + this.telemetryService.publicLog2('interactiveSessionRunInTerminal', { + providerId: action.providerId, + languageId: action.action.languageId ?? '' + }); } this._onDidPerformUserAction.fire(action); diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index 6342f8921ab..9672f01c067 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -185,6 +185,14 @@ declare module 'vscode' { responseId: string; codeBlockIndex: number; totalCharacters: number; + newFile?: boolean; + } + + export interface InteractiveSessionTerminalAction { + kind: 'runInTerminal'; + responseId: string; + codeBlockIndex: number; + languageId?: string; } export interface InteractiveSessionCommandAction { @@ -192,7 +200,7 @@ declare module 'vscode' { command: InteractiveResponseCommand; } - export type InteractiveSessionUserAction = InteractiveSessionVoteAction | InteractiveSessionCopyAction | InteractiveSessionInsertAction | InteractiveSessionCommandAction; + export type InteractiveSessionUserAction = InteractiveSessionVoteAction | InteractiveSessionCopyAction | InteractiveSessionInsertAction | InteractiveSessionTerminalAction | InteractiveSessionCommandAction; export interface InteractiveSessionUserActionEvent { action: InteractiveSessionUserAction;