Skip to content

Commit

Permalink
Merge pull request #177283 from microsoft/tyriar/177232
Browse files Browse the repository at this point in the history
Make terminal tab hover keyboard accessible
  • Loading branch information
Tyriar authored Mar 20, 2023
2 parents c8189f4 + ca8ab92 commit f7a454f
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 18 deletions.
1 change: 1 addition & 0 deletions src/vs/workbench/contrib/terminal/browser/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ export interface ITerminalGroupService extends ITerminalInstanceHost {
showPanel(focus?: boolean): Promise<void>;
hidePanel(): void;
focusTabs(): void;
focusHover(): void;
showTabs(): void;
updateVisibility(): void;
}
Expand Down
25 changes: 23 additions & 2 deletions src/vs/workbench/contrib/terminal/browser/terminalActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { BrowserFeatures } from 'vs/base/browser/canIUse';
import { Action } from 'vs/base/common/actions';
import { Codicon } from 'vs/base/common/codicons';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Schemas } from 'vs/base/common/network';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { IDisposable } from 'vs/base/common/lifecycle';
Expand Down Expand Up @@ -1711,6 +1711,25 @@ export function registerTerminalActions() {
}
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: TerminalCommandId.FocusHover,
title: terminalStrings.focusHover,
f1: true,
category,
precondition: ContextKeyExpr.or(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.isOpen),
keybinding: {
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyI),
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.or(TerminalContextKeys.tabsFocus, TerminalContextKeys.focus)
}
});
}
async run(accessor: ServicesAccessor) {
accessor.get(ITerminalGroupService).focusHover();
}
});
registerAction2(class extends Action2 {
constructor() {
super({
Expand All @@ -1725,7 +1744,9 @@ export function registerTerminalActions() {
// Weight is higher than work workbench contributions so the keybinding remains
// highest priority when chords are registered afterwards
weight: KeybindingWeight.WorkbenchContrib + 1,
when: TerminalContextKeys.focus
// Disable the keybinding when accessibility mode is enabled as chords include
// important screen reader keybindings such as cmd+k, cmd+i to show the hover
when: ContextKeyExpr.and(TerminalContextKeys.focus, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()),
}]
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe
pane?.terminalTabbedView?.focusTabs();
}

async focusHover(): Promise<void> {
if (this.instances.length === 0) {
return;
}

const pane = this._viewsService.getActiveViewWithId<TerminalViewPane>(TERMINAL_VIEW_ID);
pane?.terminalTabbedView?.focusHover();
}

async focusActiveInstance(): Promise<void> {
return this.showPanel(true);
}
Expand Down
19 changes: 19 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { localize } from 'vs/nls';
import { openContextMenu } from 'vs/workbench/contrib/terminal/browser/terminalContextMenu';
import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys';
import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey';
import { getInstanceHoverInfo } from 'vs/workbench/contrib/terminal/browser/terminalTooltip';
import { IHoverService } from 'vs/workbench/services/hover/browser/hover';

const $ = dom.$;

Expand Down Expand Up @@ -77,6 +79,7 @@ export class TerminalTabbedView extends Disposable {
@IMenuService menuService: IMenuService,
@IStorageService private readonly _storageService: IStorageService,
@IContextKeyService contextKeyService: IContextKeyService,
@IHoverService private readonly _hoverService: IHoverService,
) {
super();

Expand Down Expand Up @@ -479,6 +482,22 @@ export class TerminalTabbedView extends Disposable {
this._focus();
}

focusHover() {
if (this._shouldShowTabs()) {
this._tabList.focusHover();
return;
}
const instance = this._terminalGroupService.activeInstance;
if (!instance) {
return;
}
this._hoverService.showHover({
...getInstanceHoverInfo(instance),
target: this._terminalContainer,
trapFocus: true
}, true);
}

private _focus() {
this._terminalGroupService.activeInstance?.focusWhenReady();
}
Expand Down
33 changes: 18 additions & 15 deletions src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { ITerminalBackend, TerminalCommandId } from 'vs/workbench/contrib/termin
import { TerminalLocation, TerminalSettingId } from 'vs/platform/terminal/common/terminal';
import { Codicon } from 'vs/base/common/codicons';
import { Action } from 'vs/base/common/actions';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { DEFAULT_LABELS_CONTAINER, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
import { IDecorationData, IDecorationsProvider, IDecorationsService } from 'vs/workbench/services/decorations/common/decorations';
import { IHoverAction, IHoverService } from 'vs/workbench/services/hover/browser/hover';
Expand All @@ -45,7 +44,7 @@ import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecy
import { IProcessDetails } from 'vs/platform/terminal/common/terminalProcess';
import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey';
import { getTerminalResourcesFromDragEvent, parseTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri';
import { getShellIntegrationTooltip, getShellProcessTooltip } from 'vs/workbench/contrib/terminal/browser/terminalTooltip';
import { getInstanceHoverInfo } from 'vs/workbench/contrib/terminal/browser/terminalTooltip';
import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles';
import { Event, Emitter } from 'vs/base/common/event';
import { Schemas } from 'vs/base/common/network';
Expand Down Expand Up @@ -80,6 +79,7 @@ export class TerminalTabList extends WorkbenchList<ITerminalInstance> {
@IDecorationsService decorationsService: IDecorationsService,
@IThemeService private readonly _themeService: IThemeService,
@ILifecycleService lifecycleService: ILifecycleService,
@IHoverService private readonly _hoverService: IHoverService,
) {
super('TerminalTabsList', container,
{
Expand Down Expand Up @@ -214,6 +214,19 @@ export class TerminalTabList extends WorkbenchList<ITerminalInstance> {
this.splice(0, this.length, this._terminalGroupService.instances.slice());
}

focusHover(): void {
const instance = this.getSelectedElements()[0];
if (!instance) {
return;
}

this._hoverService.showHover({
...getInstanceHoverInfo(instance),
target: this.getHTMLElement(),
trapFocus: true
}, true);
}

private _updateContextKey() {
this._terminalTabsSingleSelectedContextKey.set(this.getSelectedElements().length === 1);
const instance = this.getFocusedElements();
Expand Down Expand Up @@ -308,19 +321,9 @@ class TerminalTabsRenderer implements IListRenderer<ITerminalInstance, ITerminal
}
}

const hoverInfo = getInstanceHoverInfo(instance);
template.context.hoverActions = hoverInfo.actions;

let statusString = '';
const statuses = instance.statusList.statuses;
template.context.hoverActions = [];
for (const status of statuses) {
statusString += `\n\n---\n\n${status.icon ? `$(${status.icon?.id}) ` : ''}${status.tooltip || status.id}`;
if (status.hoverActions) {
template.context.hoverActions.push(...status.hoverActions);
}
}

const shellIntegrationString = getShellIntegrationTooltip(instance, true);
const shellProcessString = getShellProcessTooltip(instance, true);
const iconId = this._instantiationService.invokeFunction(getIconId, instance);
const hasActionbar = !this.shouldHideActionBar();
let label: string = '';
Expand Down Expand Up @@ -374,7 +377,7 @@ class TerminalTabsRenderer implements IListRenderer<ITerminalInstance, ITerminal
badges: hasText
},
title: {
markdown: new MarkdownString(instance.title + shellProcessString + shellIntegrationString + statusString, { supportThemeIcons: true }),
markdown: hoverInfo.content,
markdownNotSupportedFallback: undefined
},
extraClasses
Expand Down
20 changes: 20 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ import { localize } from 'vs/nls';
import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal';
import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
import { asArray } from 'vs/base/common/arrays';
import { IHoverAction } from 'vs/workbench/services/hover/browser/hover';
import { MarkdownString } from 'vs/base/common/htmlContent';

export function getInstanceHoverInfo(instance: ITerminalInstance): { content: MarkdownString; actions: IHoverAction[] } {
let statusString = '';
const statuses = instance.statusList.statuses;
const actions = [];
for (const status of statuses) {
statusString += `\n\n---\n\n${status.icon ? `$(${status.icon?.id}) ` : ''}${status.tooltip || status.id}`;
if (status.hoverActions) {
actions.push(...status.hoverActions);
}
}

const shellProcessString = getShellProcessTooltip(instance, true);
const shellIntegrationString = getShellIntegrationTooltip(instance, true);
const content = new MarkdownString(instance.title + shellProcessString + shellIntegrationString + statusString, { supportThemeIcons: true });

return { content, actions };
}

export function getShellIntegrationTooltip(instance: ITerminalInstance, markdown: boolean): string {
const shellIntegrationCapabilities: TerminalCapability[] = [];
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/contrib/terminal/common/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,7 @@ export const enum TerminalCommandId {
SelectNextPageSuggestion = 'workbench.action.terminal.selectNextPageSuggestion',
AcceptSelectedSuggestion = 'workbench.action.terminal.acceptSelectedSuggestion',
HideSuggestWidget = 'workbench.action.terminal.hideSuggestWidget',
FocusHover = 'workbench.action.terminal.focusHover',
}

export const DEFAULT_COMMANDS_TO_SKIP_SHELL: string[] = [
Expand Down Expand Up @@ -685,6 +686,7 @@ export const DEFAULT_COMMANDS_TO_SKIP_SHELL: string[] = [
TerminalCommandId.AcceptSelectedSuggestion,
TerminalCommandId.HideSuggestWidget,
TerminalCommandId.ShowTerminalAccessibilityHelp,
TerminalCommandId.FocusHover,
'editor.action.toggleTabFocusMode',
'notifications.hideList',
'notifications.hideToasts',
Expand Down
6 changes: 5 additions & 1 deletion src/vs/workbench/contrib/terminal/common/terminalStrings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,9 @@ export const terminalStrings = {
toggleSizeToContentWidth: {
value: localize('workbench.action.terminal.sizeToContentWidthInstance', "Toggle Size to Content Width"),
original: 'Toggle Size to Content Width'
}
},
focusHover: {
value: localize('workbench.action.terminal.focusHover', "Focus Hover"),
original: 'Focus Hover'
},
};
1 change: 1 addition & 0 deletions src/vs/workbench/test/browser/workbenchTestServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1850,6 +1850,7 @@ export class TestTerminalGroupService implements ITerminalGroupService {
hidePanel(): void { throw new Error('Method not implemented.'); }
focusTabs(): void { throw new Error('Method not implemented.'); }
showTabs(): void { throw new Error('Method not implemented.'); }
focusHover(): void { throw new Error('Method not implemented.'); }
setActiveInstance(instance: ITerminalInstance): void { throw new Error('Method not implemented.'); }
focusActiveInstance(): Promise<void> { throw new Error('Method not implemented.'); }
getInstanceFromResource(resource: URI | undefined): ITerminalInstance | undefined { throw new Error('Method not implemented.'); }
Expand Down

0 comments on commit f7a454f

Please sign in to comment.