Skip to content

Commit

Permalink
Navigate chat codeblocks with a keyboard shortcuut (#182361)
Browse files Browse the repository at this point in the history
Navigate codeblocks with a keyboard shortcuut
  • Loading branch information
roblourens committed May 14, 2023
1 parent 65a3d20 commit 9084e08
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 31 deletions.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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

0 comments on commit 9084e08

Please sign in to comment.