Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add codeblock actions to command palette, and some keybindings #182349

Merged
merged 1 commit into from May 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -3,27 +3,32 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { localize } from 'vs/nls';
import { Codicon } from 'vs/base/common/codicons';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { EditorAction2, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { Range } from 'vs/editor/common/core/range';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { ITextModel } from 'vs/editor/common/model';
import { CopyAction } from 'vs/editor/contrib/clipboard/browser/clipboard';
import { localize } from 'vs/nls';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
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 { codeBlockInfoByModelUri } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionListRenderer';
import { IInteractiveSessionCopyAction, IInteractiveSessionService, IInteractiveSessionUserActionEvent, InteractiveSessionCopyKind } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
import { IInteractiveResponseViewModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel';
import { insertCell } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations';
import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellKind, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon';
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';
import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellKind, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { insertCell } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations';

export interface IInteractiveSessionCodeBlockActionContext {
code: string;
Expand Down Expand Up @@ -80,15 +85,64 @@ export function registerInteractiveSessionCodeBlockActions() {
}
});

registerAction2(class InsertCodeBlockAction extends Action2 {
CopyAction?.addImplementation(50000, 'interactiveSession-codeblock', (accessor) => {
// get active code editor
const editor = accessor.get(ICodeEditorService).getFocusedCodeEditor();
if (!editor) {
return false;
}

const editorModel = editor.getModel();
if (!editorModel) {
return false;
}

const context = getContextFromEditor(editor);
if (!context) {
return false;
}

const noSelection = editor.getSelections()?.length === 1 && editor.getSelection()?.isEmpty();
const copiedText = noSelection ?
editorModel.getValue() :
editor.getSelections()?.reduce((acc, selection) => acc + editorModel.getValueInRange(selection), '') ?? '';
const totalCharacters = editorModel.getValueLength();

// Report copy to extensions
if (context.element.providerResponseId) {
const interactiveSessionService = accessor.get(IInteractiveSessionService);
interactiveSessionService.notifyUserAction({
providerId: context.element.providerId,
action: {
kind: 'copy',
codeBlockIndex: context.codeBlockIndex,
responseId: context.element.providerResponseId,
copyType: InteractiveSessionCopyKind.Action,
copiedText,
copiedCharacters: copiedText.length,
totalCharacters,
}
});
}

// Copy full cell if no selection, otherwise fall back on normal editor implementation
if (noSelection) {
accessor.get(IClipboardService).writeText(context.code);
return true;
}

return false;
});

registerAction2(class InsertCodeBlockAction extends EditorAction2 {
constructor() {
super({
id: 'workbench.action.interactiveSession.insertCodeBlock',
title: {
value: localize('interactive.insertCodeBlock.label', "Insert at Cursor"),
original: 'Insert at Cursor'
},
f1: false,
f1: true,
category: INTERACTIVE_SESSION_CATEGORY,
icon: Codicon.insert,
menu: {
Expand All @@ -98,10 +152,13 @@ export function registerInteractiveSessionCodeBlockActions() {
});
}

async run(accessor: ServicesAccessor, ...args: any[]) {
const context = args[0];
override async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]) {
let context = args[0];
if (!isCodeBlockActionContext(context)) {
return;
context = getContextFromEditor(editor);
if (!isCodeBlockActionContext(context)) {
return;
}
}

const editorService = accessor.get(IEditorService);
Expand Down Expand Up @@ -160,15 +217,14 @@ export function registerInteractiveSessionCodeBlockActions() {
}

private async handleTextEditor(accessor: ServicesAccessor, codeEditor: ICodeEditor, activeModel: ITextModel, context: IInteractiveSessionCodeBlockActionContext) {
this.notifyUserAction(accessor, context);
const bulkEditService = accessor.get(IBulkEditService);

const activeSelection = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1);
await bulkEditService.apply([new ResourceTextEdit(activeModel.uri, {
range: activeSelection,
text: context.code,
})]);

this.notifyUserAction(accessor, context);
}

private notifyUserAction(accessor: ServicesAccessor, context: IInteractiveSessionCodeBlockActionContext) {
Expand All @@ -186,15 +242,15 @@ export function registerInteractiveSessionCodeBlockActions() {

});

registerAction2(class InsertIntoNewFileAction extends Action2 {
registerAction2(class InsertIntoNewFileAction extends EditorAction2 {
constructor() {
super({
id: 'workbench.action.interactiveSession.insertIntoNewFile',
title: {
value: localize('interactive.insertIntoNewFile.label', "Insert Into New File"),
original: 'Insert Into New File'
},
f1: false,
f1: true,
category: INTERACTIVE_SESSION_CATEGORY,
icon: Codicon.newFile,
menu: {
Expand All @@ -205,10 +261,13 @@ export function registerInteractiveSessionCodeBlockActions() {
});
}

async run(accessor: ServicesAccessor, ...args: any[]) {
const context = args[0];
override async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]) {
let context = args[0];
if (!isCodeBlockActionContext(context)) {
return;
context = getContextFromEditor(editor);
if (!isCodeBlockActionContext(context)) {
return;
}
}

const editorService = accessor.get(IEditorService);
Expand All @@ -228,29 +287,39 @@ export function registerInteractiveSessionCodeBlockActions() {
}
});

registerAction2(class RunInTerminalAction extends Action2 {
registerAction2(class RunInTerminalAction extends EditorAction2 {
constructor() {
super({
id: 'workbench.action.interactiveSession.runInTerminal',
title: {
value: localize('interactive.runInTerminal.label', "Run in Terminal"),
original: 'Run in Terminal'
},
f1: false,
f1: true,
category: INTERACTIVE_SESSION_CATEGORY,
icon: Codicon.terminal,
menu: {
id: MenuId.InteractiveSessionCodeBlock,
group: 'navigation',
isHiddenByDefault: true,
},
keybinding: {
primary: KeyMod.WinCtrl | KeyCode.Enter,
win: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Enter
},
weight: KeybindingWeight.EditorContrib
}
});
}

async run(accessor: ServicesAccessor, ...args: any[]) {
const context = args[0];
override async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]) {
let context = args[0];
if (!isCodeBlockActionContext(context)) {
return;
context = getContextFromEditor(editor);
if (!isCodeBlockActionContext(context)) {
return;
}
}

const interactiveSessionService = accessor.get(IInteractiveSessionService);
Expand All @@ -275,7 +344,7 @@ export function registerInteractiveSessionCodeBlockActions() {
terminalGroupService.showPanel(true);
}

terminal.sendText(context.code, false);
terminal.sendText(context.code, false, true);

interactiveSessionService.notifyUserAction(<IInteractiveSessionUserActionEvent>{
providerId: context.element.providerId,
Expand All @@ -289,3 +358,22 @@ export function registerInteractiveSessionCodeBlockActions() {
}
});
}

function getContextFromEditor(editor: ICodeEditor): IInteractiveSessionCodeBlockActionContext | undefined {
const model = editor.getModel();
if (!model) {
return;
}

const codeBlockInfo = codeBlockInfoByModelUri.get(model.uri);
if (!codeBlockInfo) {
return;
}

return {
element: codeBlockInfo.element,
codeBlockIndex: codeBlockInfo.codeBlockIndex,
code: editor.getValue(),
languageId: editor.getModel()!.getLanguageId(),
};
}

This file was deleted.

Expand Up @@ -122,7 +122,6 @@ registerSingleton(IInteractiveSessionContributionService, InteractiveSessionCont
registerSingleton(IInteractiveSessionWidgetService, InteractiveSessionWidgetService, InstantiationType.Delayed);
registerSingleton(IInteractiveSessionWidgetHistoryService, InteractiveSessionWidgetHistoryService, InstantiationType.Delayed);

import 'vs/workbench/contrib/interactiveSession/browser/contrib/interactiveSessionCodeBlockCopy';
import 'vs/workbench/contrib/interactiveSession/browser/contrib/interactiveSessionInputEditorContrib';
import { Schemas } from 'vs/base/common/network';

Expand Up @@ -70,7 +70,7 @@ export class InteractiveSessionEditorInput extends EditorInput {

override async resolve(): Promise<InteractiveSessionEditorModel | null> {
const model = typeof this.sessionId === 'string' ?
this.interactiveSessionService.retrieveSession(this.sessionId) :
this.interactiveSessionService.getOrRestoreSession(this.sessionId) :
this.interactiveSessionService.startSession(this.providerId!, CancellationToken.None);

if (!model) {
Expand Down
Expand Up @@ -49,7 +49,6 @@ import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles';
import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer';
import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard';
import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions';
import { IInteractiveSessionCodeBlockActionContext } from 'vs/workbench/contrib/interactiveSession/browser/actions/interactiveSessionCodeblockActions';
import { InteractiveSessionFollowups } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionFollowups';
import { InteractiveSessionEditorOptions } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionOptions';
import { CONTEXT_RESPONSE_HAS_PROVIDER_ID, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionContextKeys';
Expand Down Expand Up @@ -526,13 +525,13 @@ interface IInteractiveResultCodeBlockPart {
dispose(): void;
}

export interface IInteractiveResultCodeBlockInfo {
providerId: string;
responseId: string;
export interface IInteractiveSessionCodeBlockInfo {
codeBlockIndex: number;
element: IInteractiveResponseViewModel;
}

export const codeBlockInfosByModelUri = new ResourceMap<IInteractiveResultCodeBlockInfo>();
// Enable actions to look this up by editor URI. An alternative would be writing lots of details to element attributes.
export const codeBlockInfoByModelUri = new ResourceMap<IInteractiveSessionCodeBlockInfo>();

const defaultCodeblockPadding = 10;

Expand Down Expand Up @@ -671,17 +670,15 @@ class CodeBlockPart extends Disposable implements IInteractiveResultCodeBlockPar
this.layout(width);

if (isResponseVM(data.element) && data.element.providerResponseId) {
// For telemetry reporting
codeBlockInfosByModelUri.set(this.textModel.uri, {
providerId: data.element.providerId,
responseId: data.element.providerResponseId,
codeBlockIndex: data.codeBlockIndex
codeBlockInfoByModelUri.set(this.textModel.uri, {
element: data.element,
codeBlockIndex: data.codeBlockIndex,
});
} else {
codeBlockInfosByModelUri.delete(this.textModel.uri);
codeBlockInfoByModelUri.delete(this.textModel.uri);
}

this.toolbar.context = <IInteractiveSessionCodeBlockActionContext>{
this.toolbar.context = <IInteractiveSessionCodeBlockInfo>{
code: data.text,
codeBlockIndex: data.codeBlockIndex,
element: data.element,
Expand Down
Expand Up @@ -97,7 +97,7 @@ export class InteractiveSessionViewPane extends ViewPane implements IInteractive
}));
this._widget.render(parent);

const initialModel = this.viewState.sessionId ? this.interactiveSessionService.retrieveSession(this.viewState.sessionId) : undefined;
const initialModel = this.viewState.sessionId ? this.interactiveSessionService.getOrRestoreSession(this.viewState.sessionId) : undefined;
this.updateModel(initialModel);
}

Expand Down
Expand Up @@ -177,7 +177,7 @@ export interface IInteractiveSessionService {
registerSlashCommandProvider(provider: IInteractiveSlashCommandProvider): IDisposable;
getProviderInfos(): IInteractiveProviderInfo[];
startSession(providerId: string, token: CancellationToken): InteractiveSessionModel | undefined;
retrieveSession(sessionId: string): IInteractiveSessionModel | undefined;
getOrRestoreSession(sessionId: string): IInteractiveSessionModel | undefined;

/**
* Returns whether the request was accepted.
Expand Down
Expand Up @@ -286,7 +286,7 @@ export class InteractiveSessionService extends Disposable implements IInteractiv
return model;
}

retrieveSession(sessionId: string): InteractiveSessionModel | undefined {
getOrRestoreSession(sessionId: string): InteractiveSessionModel | undefined {
const model = this._sessionModels.get(sessionId);
if (model) {
return model;
Expand Down