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

Navigate chat codeblocks with a keyboard shortcuut #182361

Merged
merged 1 commit into from May 14, 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 @@ -20,9 +20,10 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis
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 { IInteractiveSessionWidgetService } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSession';
import { CONTEXT_IN_INTERACTIVE_SESSION } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionContextKeys';
import { IInteractiveSessionCopyAction, IInteractiveSessionService, IInteractiveSessionUserActionEvent, InteractiveSessionCopyKind } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
import { IInteractiveResponseViewModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel';
import { IInteractiveResponseViewModel, isResponseVM } 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';
Expand Down Expand Up @@ -97,7 +98,7 @@ export function registerInteractiveSessionCodeBlockActions() {
return false;
}

const context = getContextFromEditor(editor);
const context = getContextFromEditor(editor, accessor);
if (!context) {
return false;
}
Expand Down Expand Up @@ -155,7 +156,7 @@ export function registerInteractiveSessionCodeBlockActions() {
override async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]) {
let context = args[0];
if (!isCodeBlockActionContext(context)) {
context = getContextFromEditor(editor);
context = getContextFromEditor(editor, accessor);
if (!isCodeBlockActionContext(context)) {
return;
}
Expand Down Expand Up @@ -264,7 +265,7 @@ export function registerInteractiveSessionCodeBlockActions() {
override async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]) {
let context = args[0];
if (!isCodeBlockActionContext(context)) {
context = getContextFromEditor(editor);
context = getContextFromEditor(editor, accessor);
if (!isCodeBlockActionContext(context)) {
return;
}
Expand Down Expand Up @@ -316,7 +317,7 @@ export function registerInteractiveSessionCodeBlockActions() {
override async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]) {
let context = args[0];
if (!isCodeBlockActionContext(context)) {
context = getContextFromEditor(editor);
context = getContextFromEditor(editor, accessor);
if (!isCodeBlockActionContext(context)) {
return;
}
Expand Down Expand Up @@ -357,15 +358,94 @@ export function registerInteractiveSessionCodeBlockActions() {
});
}
});

function navigateCodeBlocks(accessor: ServicesAccessor, reverse?: boolean): void {
const codeEditorService = accessor.get(ICodeEditorService);
const interactiveSessionWidgetService = accessor.get(IInteractiveSessionWidgetService);
const widget = interactiveSessionWidgetService.lastFocusedWidget;
if (!widget) {
return;
}

const editor = codeEditorService.getFocusedCodeEditor();
const editorUri = editor?.getModel()?.uri;
const curCodeBlockInfo = editorUri ? widget.getCodeBlockInfoForEditor(editorUri) : undefined;

const focusResponse = curCodeBlockInfo ?
curCodeBlockInfo.element :
widget.viewModel?.getItems().reverse().find((item): item is IInteractiveResponseViewModel => isResponseVM(item));
if (!focusResponse) {
return;
}

const responseCodeblocks = widget.getCodeBlockInfosForResponse(focusResponse);
const focusIdx = curCodeBlockInfo ?
(curCodeBlockInfo.codeBlockIndex + (reverse ? -1 : 1) + responseCodeblocks.length) % responseCodeblocks.length :
reverse ? responseCodeblocks.length - 1 : 0;

responseCodeblocks[focusIdx]?.focus();
}

registerAction2(class NextCodeBlockAction extends Action2 {
constructor() {
super({
id: 'workbench.action.interactiveSession.nextCodeBlock',
title: {
value: localize('interactive.nextCodeBlock.label', "Next Code Block"),
original: 'Next Code Block'
},
keybinding: {
primary: KeyCode.F9,
weight: KeybindingWeight.WorkbenchContrib,
when: CONTEXT_IN_INTERACTIVE_SESSION,
},
f1: true,
category: INTERACTIVE_SESSION_CATEGORY,
});
}

run(accessor: ServicesAccessor, ...args: any[]) {
navigateCodeBlocks(accessor);
}
});

registerAction2(class PreviousCodeBlockAction extends Action2 {
constructor() {
super({
id: 'workbench.action.interactiveSession.previousCodeBlock',
title: {
value: localize('interactive.previousCodeBlock.label', "Previous Code Block"),
original: 'Previous Code Block'
},
keybinding: {
primary: KeyMod.Shift | KeyCode.F9,
weight: KeybindingWeight.WorkbenchContrib,
when: CONTEXT_IN_INTERACTIVE_SESSION,
},
f1: true,
category: INTERACTIVE_SESSION_CATEGORY,
});
}

run(accessor: ServicesAccessor, ...args: any[]) {
navigateCodeBlocks(accessor, true);
}
});
}

function getContextFromEditor(editor: ICodeEditor): IInteractiveSessionCodeBlockActionContext | undefined {
function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): IInteractiveSessionCodeBlockActionContext | undefined {
const interactiveSessionWidgetService = accessor.get(IInteractiveSessionWidgetService);
const model = editor.getModel();
if (!model) {
return;
}

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

const codeBlockInfo = widget.getCodeBlockInfoForEditor(model.uri);
if (!codeBlockInfo) {
return;
}
Expand Down
Expand Up @@ -5,7 +5,7 @@

import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IInteractiveSlashCommand } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
import { IInteractiveSessionViewModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel';
import { IInteractiveResponseViewModel, IInteractiveSessionViewModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel';
import { Event } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
Expand All @@ -29,6 +29,12 @@ export interface IInteractiveSessionWidgetService {
getWidgetByInputUri(uri: URI): IInteractiveSessionWidget | undefined;
}

export interface IInteractiveSessionCodeBlockInfo {
codeBlockIndex: number;
element: IInteractiveResponseViewModel;
focus(): void;
}

export type IInteractiveSessionWidgetViewContext = { viewId: string } | { resource: boolean };

export interface IInteractiveSessionWidget {
Expand All @@ -42,6 +48,8 @@ export interface IInteractiveSessionWidget {
focusLastMessage(): void;
focusInput(): void;
getSlashCommands(): Promise<IInteractiveSlashCommand[] | undefined>;
getCodeBlockInfoForEditor(uri: URI): IInteractiveSessionCodeBlockInfo | undefined;
getCodeBlockInfosForResponse(response: IInteractiveResponseViewModel): IInteractiveSessionCodeBlockInfo[];
}

export interface IInteractiveSessionViewPane {
Expand Down
Expand Up @@ -16,11 +16,12 @@ import { Codicon } from 'vs/base/common/codicons';
import { Emitter, Event } from 'vs/base/common/event';
import { FuzzyScore } from 'vs/base/common/filters';
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
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 { URI } from 'vs/base/common/uri';
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';
Expand Down Expand Up @@ -49,6 +50,8 @@ 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 { IInteractiveSessionCodeBlockInfo } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSession';
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 @@ -87,6 +90,9 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend
static readonly cursorCharacter = '\u258c';
static readonly ID = 'item';

private readonly codeBlocksByResponseId = new Map<string, IInteractiveSessionCodeBlockInfo[]>();
private readonly codeBlocksByEditorUri = new ResourceMap<IInteractiveSessionCodeBlockInfo>();

private readonly renderer: MarkdownRenderer;

protected readonly _onDidClickFollowup = this._register(new Emitter<IInteractiveSessionReplyFollowup>());
Expand Down Expand Up @@ -152,6 +158,15 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend
return 8;
}

getCodeBlockInfosForResponse(response: IInteractiveResponseViewModel): IInteractiveSessionCodeBlockInfo[] {
const codeBlocks = this.codeBlocksByResponseId.get(response.id);
return codeBlocks ?? [];
}

getCodeBlockInfoForEditor(uri: URI): IInteractiveSessionCodeBlockInfo | undefined {
return this.codeBlocksByEditorUri.get(uri);
}

setVisible(visible: boolean): void {
this._isVisible = visible;
}
Expand Down Expand Up @@ -385,10 +400,13 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend
const usedSlashCommand = slashCommands.find(s => markdown.value.startsWith(`/${s.command} `));
const toRender = usedSlashCommand ? markdown.value.slice(usedSlashCommand.command.length + 2) : markdown.value;
markdown = new MarkdownString(toRender);

const codeblocks: IInteractiveSessionCodeBlockInfo[] = [];
const result = this.renderer.render(markdown, {
fillInIncompleteTokens,
codeBlockRendererSync: (languageId, text) => {
const ref = this.renderCodeBlock({ languageId, text, codeBlockIndex: codeBlockIndex++, element, parentContextKeyService: templateData.contextKeyService }, disposables);
const data = { languageId, text, codeBlockIndex: codeBlockIndex++, element, parentContextKeyService: templateData.contextKeyService };
const ref = this.renderCodeBlock(data, disposables);

// Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping)
// not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render)
Expand All @@ -397,11 +415,28 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend
this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight });
}));

if (isResponseVM(element)) {
const info = {
codeBlockIndex: data.codeBlockIndex,
element,
focus() {
ref.object.focus();
}
};
codeblocks.push(info);
this.codeBlocksByEditorUri.set(ref.object.textModel.uri, info);
disposables.add(toDisposable(() => this.codeBlocksByEditorUri.delete(ref.object.textModel.uri)));
}
disposablesList.push(ref);
return ref.object.element;
}
});

if (isResponseVM(element)) {
this.codeBlocksByResponseId.set(element.id, codeblocks);
disposables.add(toDisposable(() => this.codeBlocksByResponseId.delete(element.id)));
}

if (usedSlashCommand) {
const slashCommandElement = $('span.interactive-slash-command', { title: usedSlashCommand.detail }, `/${usedSlashCommand.command} `);
if (result.element.firstChild?.nodeName.toLowerCase() === 'p') {
Expand Down Expand Up @@ -522,17 +557,10 @@ interface IInteractiveResultCodeBlockPart {
readonly textModel: ITextModel;
layout(width: number): void;
render(data: IInteractiveResultCodeBlockData, width: number): void;
focus(): void;
dispose(): void;
}

export interface IInteractiveSessionCodeBlockInfo {
codeBlockIndex: number;
element: IInteractiveResponseViewModel;
}

// 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;

class CodeBlockPart extends Disposable implements IInteractiveResultCodeBlockPart {
Expand Down Expand Up @@ -620,6 +648,10 @@ class CodeBlockPart extends Disposable implements IInteractiveResultCodeBlockPar
this.editor.setModel(this.textModel);
}

focus(): void {
this.editor.focus();
}

private updatePaddingForLayout() {
// scrollWidth = "the width of the content that needs to be scrolled"
// contentWidth = "the width of the area where content is displayed"
Expand Down Expand Up @@ -669,16 +701,7 @@ class CodeBlockPart extends Disposable implements IInteractiveResultCodeBlockPar

this.layout(width);

if (isResponseVM(data.element) && data.element.providerResponseId) {
codeBlockInfoByModelUri.set(this.textModel.uri, {
element: data.element,
codeBlockIndex: data.codeBlockIndex,
});
} else {
codeBlockInfoByModelUri.delete(this.textModel.uri);
}

this.toolbar.context = <IInteractiveSessionCodeBlockInfo>{
this.toolbar.context = <IInteractiveSessionCodeBlockActionContext>{
code: data.text,
codeBlockIndex: data.codeBlockIndex,
element: data.element,
Expand Down
Expand Up @@ -22,7 +22,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle
import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService';
import { IViewsService } from 'vs/workbench/common/views';
import { clearChatSession } from 'vs/workbench/contrib/interactiveSession/browser/actions/interactiveSessionClear';
import { IInteractiveSessionWidget, IInteractiveSessionWidgetService, IInteractiveSessionWidgetViewContext } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSession';
import { IInteractiveSessionCodeBlockInfo, IInteractiveSessionWidget, IInteractiveSessionWidgetService, IInteractiveSessionWidgetViewContext } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSession';
import { InteractiveSessionInputPart } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionInputPart';
import { IInteractiveSessionRendererDelegate, InteractiveListItemRenderer, InteractiveSessionAccessibilityProvider, InteractiveSessionListDelegate, InteractiveTreeItem } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionListRenderer';
import { InteractiveSessionEditorOptions } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionOptions';
Expand All @@ -31,7 +31,7 @@ import { CONTEXT_INTERACTIVE_REQUEST_IN_PROGRESS, CONTEXT_IN_INTERACTIVE_SESSION
import { IInteractiveSessionContributionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionContributionService';
import { IInteractiveSessionModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel';
import { IInteractiveSessionReplyFollowup, IInteractiveSessionService, IInteractiveSlashCommand } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
import { InteractiveSessionViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel';
import { IInteractiveResponseViewModel, InteractiveSessionViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel';

const $ = dom.$;

Expand Down Expand Up @@ -386,6 +386,14 @@ export class InteractiveSessionWidget extends Disposable implements IInteractive
}
}

getCodeBlockInfosForResponse(response: IInteractiveResponseViewModel): IInteractiveSessionCodeBlockInfo[] {
return this.renderer.getCodeBlockInfosForResponse(response);
}

getCodeBlockInfoForEditor(uri: URI): IInteractiveSessionCodeBlockInfo | undefined {
return this.renderer.getCodeBlockInfoForEditor(uri);
}

focusLastMessage(): void {
if (!this.viewModel) {
return;
Expand Down