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

enable chat code block actions in accessible view #206849

Merged
merged 7 commits into from Mar 7, 2024
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 @@ -22,6 +22,8 @@ export const accessibleViewVerbosityEnabled = new RawContextKey<boolean>('access
export const accessibleViewGoToSymbolSupported = new RawContextKey<boolean>('accessibleViewGoToSymbolSupported', false, true);
export const accessibleViewOnLastLine = new RawContextKey<boolean>('accessibleViewOnLastLine', false, true);
export const accessibleViewCurrentProviderId = new RawContextKey<string>('accessibleViewCurrentProviderId', undefined, undefined);
export const accessibleViewInCodeBlock = new RawContextKey<boolean>('accessibleViewInCodeBlock', undefined, undefined);
export const accessibleViewContainsCodeBlocks = new RawContextKey<boolean>('accessibleViewContainsCodeBlocks', undefined, undefined);

/**
* Miscellaneous settings tagged with accessibility and implemented in the accessibility contrib but
Expand Down
113 changes: 106 additions & 7 deletions src/vs/workbench/contrib/accessibility/browser/accessibleView.ts
Expand Up @@ -40,8 +40,9 @@ import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, AccessibleViewProviderId, accessibilityHelpIsShown, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, AccessibleViewProviderId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands';
import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart';
import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions';

const enum DIMENSIONS {
Expand Down Expand Up @@ -91,6 +92,7 @@ export interface IAccessibleViewService {
* @param verbositySettingKey The setting key for the verbosity of the feature
*/
getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null;
getCodeBlockContext(): ICodeBlockActionContext | undefined;
}

export const enum AccessibleViewType {
Expand Down Expand Up @@ -127,6 +129,13 @@ export interface IAccessibleViewOptions {
id?: AccessibleViewProviderId;
}

interface ICodeBlock {
startLine: number;
endLine: number;
code: string;
languageId?: string;
}

export class AccessibleView extends Disposable {
private _editorWidget: CodeEditorWidget;

Expand All @@ -137,6 +146,9 @@ export class AccessibleView extends Disposable {
private _accessibleViewVerbosityEnabled: IContextKey<boolean>;
private _accessibleViewGoToSymbolSupported: IContextKey<boolean>;
private _accessibleViewCurrentProviderId: IContextKey<string>;
private _accessibleViewInCodeBlock: IContextKey<boolean>;
private _accessibleViewContainsCodeBlocks: IContextKey<boolean>;
private _codeBlocks?: ICodeBlock[];

get editorWidget() { return this._editorWidget; }
private _container: HTMLElement;
Expand Down Expand Up @@ -169,6 +181,8 @@ export class AccessibleView extends Disposable {
this._accessibleViewVerbosityEnabled = accessibleViewVerbosityEnabled.bindTo(this._contextKeyService);
this._accessibleViewGoToSymbolSupported = accessibleViewGoToSymbolSupported.bindTo(this._contextKeyService);
this._accessibleViewCurrentProviderId = accessibleViewCurrentProviderId.bindTo(this._contextKeyService);
this._accessibleViewInCodeBlock = accessibleViewInCodeBlock.bindTo(this._contextKeyService);
this._accessibleViewContainsCodeBlocks = accessibleViewContainsCodeBlocks.bindTo(this._contextKeyService);
this._onLastLine = accessibleViewOnLastLine.bindTo(this._contextKeyService);

this._container = document.createElement('div');
Expand Down Expand Up @@ -229,6 +243,13 @@ export class AccessibleView extends Disposable {
this._register(this._editorWidget.onDidChangeCursorPosition(() => {
this._onLastLine.set(this._editorWidget.getPosition()?.lineNumber === this._editorWidget.getModel()?.getLineCount());
}));
this._register(this._editorWidget.onDidChangeCursorPosition(() => {
const cursorPosition = this._editorWidget.getPosition()?.lineNumber;
if (this._codeBlocks && cursorPosition !== undefined) {
const inCodeBlock = this._codeBlocks.find(c => c.startLine <= cursorPosition && c.endLine >= cursorPosition) !== undefined;
this._accessibleViewInCodeBlock.set(inCodeBlock);
}
}));
}

private _resetContextKeys(): void {
Expand All @@ -254,6 +275,18 @@ export class AccessibleView extends Disposable {
}
}

getCodeBlockContext(): ICodeBlockActionContext | undefined {
const position = this._editorWidget.getPosition();
if (!this._codeBlocks?.length || !position) {
return;
}
const codeBlockIndex = this._codeBlocks?.findIndex(c => c.startLine <= position?.lineNumber && c.endLine >= position?.lineNumber);
const codeBlock = codeBlockIndex !== undefined && codeBlockIndex > -1 ? this._codeBlocks[codeBlockIndex] : undefined;
if (!codeBlock || codeBlockIndex === undefined) {
return;
}
return { code: codeBlock.code, languageId: codeBlock.languageId, codeBlockIndex, element: undefined };
}

showLastProvider(id: AccessibleViewProviderId): void {
if (!this._lastProvider || this._lastProvider.options.id !== id) {
Expand Down Expand Up @@ -328,6 +361,35 @@ export class AccessibleView extends Disposable {
this._instantiationService.createInstance(AccessibleViewSymbolQuickPick, this).show(this._currentProvider);
}

calculateCodeBlocks(markdown: string): void {
if (!this._currentProvider || this._currentProvider.options.id !== AccessibleViewProviderId.Chat) {
return;
}
if (this._currentProvider.options.language && this._currentProvider.options.language !== 'markdown') {
// Symbols haven't been provided and we cannot parse this language
return;
}
const lines = markdown.split('\n');
this._codeBlocks = [];
let inBlock = false;
let startLine = 0;

lines.forEach((line, i) => {
let languageId: string | undefined;
if (!inBlock && line.startsWith('```')) {
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
inBlock = true;
startLine = i + 1;
languageId = line.substring(3).trim();
} else if (inBlock && line.startsWith('```')) {
inBlock = false;
const endLine = i;
const code = lines.slice(startLine, endLine).join('\n');
this._codeBlocks?.push({ startLine, endLine, code, languageId });
}
});
this._accessibleViewContainsCodeBlocks.set(this._codeBlocks.length > 0);
}

getSymbols(): IAccessibleViewSymbol[] | undefined {
if (!this._currentProvider || !this._currentContent) {
return;
Expand Down Expand Up @@ -430,11 +492,8 @@ export class AccessibleView extends Disposable {
}

private _render(provider: IAccessibleContentProvider, container: HTMLElement, showAccessibleViewHelp?: boolean): IDisposable {
if (!showAccessibleViewHelp) {
// don't overwrite the current provider
this._currentProvider = provider;
this._accessibleViewCurrentProviderId.set(provider.id);
}
this._currentProvider = provider;
this._accessibleViewCurrentProviderId.set(provider.id);
const value = this._configurationService.getValue(provider.verbositySettingKey);
const readMoreLink = provider.options.readMoreUrl ? localize("openDoc", "\n\nOpen a browser window with more information related to accessibility (H).") : '';
let disableHelpHint = '';
Expand All @@ -459,7 +518,11 @@ export class AccessibleView extends Disposable {
}
const verbose = this._configurationService.getValue(provider.verbositySettingKey);
const exitThisDialogHint = verbose && !provider.options.position ? localize('exit', '\n\nExit this dialog (Escape).') : '';
this._currentContent = message + provider.provideContent() + readMoreLink + disableHelpHint + exitThisDialogHint;
const newContent = message + provider.provideContent() + readMoreLink + disableHelpHint + exitThisDialogHint;
if (newContent && newContent !== this._currentContent && provider.options.type !== AccessibleViewType.Help && !provider.options.language || provider.options.language === 'markdown') {
this.calculateCodeBlocks(newContent);
}
this._currentContent = newContent;
this._updateContextKeys(provider, true);
const widgetIsFocused = this._editorWidget.hasTextFocus() || this._editorWidget.hasWidgetFocus();
this._getTextModel(URI.from({ path: `accessible-view-${provider.verbositySettingKey}`, scheme: 'accessible-view', fragment: this._currentContent })).then((model) => {
Expand Down Expand Up @@ -618,6 +681,7 @@ export class AccessibleView extends Disposable {
const navigationHint = this._getNavigationHint();
const goToSymbolHint = this._getGoToSymbolHint(providerHasSymbols);
const toolbarHint = localize('toolbar', "Navigate to the toolbar (Shift+Tab)).");
const chatHints = this._getChatHints();

let hint = localize('intro', "In the accessible view, you can:\n");
if (navigationHint) {
Expand All @@ -629,6 +693,37 @@ export class AccessibleView extends Disposable {
if (toolbarHint) {
hint += ' - ' + toolbarHint + '\n';
}
if (chatHints) {
hint += chatHints;
}
return hint;
}

private _getChatHints(): string | undefined {
if (this._currentProvider?.id !== AccessibleViewProviderId.Chat) {
return;
}
let hint = '';
const insertAtCursorKb = this._keybindingService.lookupKeybinding('workbench.action.chat.insertCodeBlock')?.getAriaLabel();
const insertIntoNewFileKb = this._keybindingService.lookupKeybinding('workbench.action.chat.insertIntoNewFile')?.getAriaLabel();
const runInTerminalKb = this._keybindingService.lookupKeybinding('workbench.action.chat.runInTerminal')?.getAriaLabel();

if (insertAtCursorKb) {
hint += localize('insertAtCursor', " - Insert the code block at the cursor ({0}).\n", insertAtCursorKb);
} else {
hint += localize('insertAtCursorNoKb', " - Insert the code block at the cursor by configuring a keybinding for the Chat: Insert Code Block command.\n");
}
if (insertIntoNewFileKb) {
hint += localize('insertIntoNewFile', " - Insert the code block into a new file ({0}).\n", insertIntoNewFileKb);
} else {
hint += localize('insertIntoNewFileNoKb', " - Insert the code block into a new file by configuring a keybinding for the Chat: Insert at Cursor command.\n");
}
if (runInTerminalKb) {
hint += localize('runInTerminal', " - Run the code block in the terminal ({0}).\n", runInTerminalKb);
} else {
hint += localize('runInTerminalNoKb', " - Run the coe block in the terminal by configuring a keybinding for the Chat: Insert into Terminal command.\n");
}

return hint;
}

Expand Down Expand Up @@ -734,6 +829,9 @@ export class AccessibleViewService extends Disposable implements IAccessibleView
editorWidget?.revealLine(position.lineNumber);
}
}
getCodeBlockContext(): ICodeBlockActionContext | undefined {
return this._accessibleView?.getCodeBlockContext();
}
}

class AccessibleViewSymbolQuickPick {
Expand Down Expand Up @@ -775,6 +873,7 @@ export interface IAccessibleViewSymbol extends IPickerQuickAccessItem {
markdownToParse?: string;
firstListItem?: string;
lineNumber?: number;
endLineNumber?: number;
}

function shouldHide(event: KeyboardEvent, keybindingService: IKeybindingService, configurationService: IConfigurationService): boolean {
Expand Down
Expand Up @@ -24,6 +24,8 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { TerminalLocation } from 'vs/platform/terminal/common/terminal';
import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor';
import { accessibleViewInCodeBlock } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart';
Expand Down Expand Up @@ -147,21 +149,24 @@ export function registerChatCodeBlockActions() {

// Report copy to extensions
const chatService = accessor.get(IChatService);
chatService.notifyUserAction({
providerId: context.element.providerId,
agentId: context.element.agent?.id,
sessionId: context.element.sessionId,
requestId: context.element.requestId,
result: context.element.result,
action: {
kind: 'copy',
codeBlockIndex: context.codeBlockIndex,
copyKind: ChatCopyKind.Action,
copiedText,
copiedCharacters: copiedText.length,
totalCharacters,
}
});
const element = context.element as IChatResponseViewModel | undefined;
if (element) {
chatService.notifyUserAction({
providerId: element.providerId,
agentId: element.agent?.id,
sessionId: element.sessionId,
requestId: element.requestId,
result: element.result,
action: {
kind: 'copy',
codeBlockIndex: context.codeBlockIndex,
copyKind: ChatCopyKind.Action,
copiedText,
copiedCharacters: copiedText.length,
totalCharacters,
}
});
}

// Copy full cell if no selection, otherwise fall back on normal editor implementation
if (noSelection) {
Expand All @@ -187,7 +192,7 @@ export function registerChatCodeBlockActions() {
when: CONTEXT_IN_CHAT_SESSION,
},
keybinding: {
when: ContextKeyExpr.and(CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_CHAT_INPUT.negate()),
when: ContextKeyExpr.or(ContextKeyExpr.and(CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_CHAT_INPUT.negate()), accessibleViewInCodeBlock),
primary: KeyMod.CtrlCmd | KeyCode.Enter,
mac: { primary: KeyMod.WinCtrl | KeyCode.Enter },
weight: KeybindingWeight.WorkbenchContrib
Expand Down Expand Up @@ -431,7 +436,7 @@ export function registerChatCodeBlockActions() {
primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Enter
},
weight: KeybindingWeight.EditorContrib,
when: CONTEXT_IN_CHAT_SESSION,
when: ContextKeyExpr.or(CONTEXT_IN_CHAT_SESSION, accessibleViewInCodeBlock),
}]
});
}
Expand Down Expand Up @@ -557,8 +562,13 @@ export function registerChatCodeBlockActions() {
});
}

function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): IChatCodeBlockActionContext | undefined {
function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): ICodeBlockActionContext | undefined {
const chatWidgetService = accessor.get(IChatWidgetService);
const accessibleViewService = accessor.get(IAccessibleViewService);
const accessibleViewCodeBlock = accessibleViewService.getCodeBlockContext();
if (accessibleViewCodeBlock) {
return accessibleViewCodeBlock;
}
const model = editor.getModel();
if (!model) {
return;
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/contrib/chat/browser/codeBlockPart.ts
Expand Up @@ -101,7 +101,7 @@ export function parseLocalFileData(text: string) {

export interface ICodeBlockActionContext {
code: string;
languageId: string;
languageId?: string;
codeBlockIndex: number;
element: unknown;
}
Expand Down