Skip to content

Commit ece0e45

Browse files
authored
add go to command for terminal accessible buffer (#177110)
1 parent 7b151c9 commit ece0e45

File tree

4 files changed

+111
-10
lines changed

4 files changed

+111
-10
lines changed

src/vs/workbench/contrib/terminal/common/terminal.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ export const enum TerminalCommandId {
489489
OpenWebLink = 'workbench.action.terminal.openUrlLink',
490490
RunRecentCommand = 'workbench.action.terminal.runRecentCommand',
491491
FocusAccessibleBuffer = 'workbench.action.terminal.focusAccessibleBuffer',
492+
NavigateAccessibleBuffer = 'workbench.action.terminal.navigateAccessibleBuffer',
492493
CopyLastCommandOutput = 'workbench.action.terminal.copyLastCommandOutput',
493494
GoToRecentDirectory = 'workbench.action.terminal.goToRecentDirectory',
494495
CopyAndClearSelection = 'workbench.action.terminal.copyAndClearSelection',

src/vs/workbench/contrib/terminal/common/terminalContextKey.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const enum TerminalContextKeyStrings {
1414
HasFixedWidth = 'terminalHasFixedWidth',
1515
ProcessSupported = 'terminalProcessSupported',
1616
Focus = 'terminalFocus',
17+
AccessibleBufferFocus = 'terminalAccessibleBufferFocus',
1718
EditorFocus = 'terminalEditorFocus',
1819
TabsFocus = 'terminalTabsFocus',
1920
WebExtensionContributedProfile = 'terminalWebExtensionContributedProfile',
@@ -42,6 +43,9 @@ export namespace TerminalContextKeys {
4243
/** Whether the terminal is focused. */
4344
export const focus = new RawContextKey<boolean>(TerminalContextKeyStrings.Focus, false, localize('terminalFocusContextKey', "Whether the terminal is focused."));
4445

46+
/** Whether the accessible buffer is focused. */
47+
export const accessibleBufferFocus = new RawContextKey<boolean>(TerminalContextKeyStrings.AccessibleBufferFocus, false, localize('terminalAccessibleBufferFocusContextKey', "Whether the terminal accessible buffer is focused."));
48+
4549
/** Whether a terminal in the editor area is focused. */
4650
export const editorFocus = new RawContextKey<boolean>(TerminalContextKeyStrings.EditorFocus, false, localize('terminalEditorFocusContextKey', "Whether a terminal in the editor area is focused."));
4751

src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/brow
2222
import { DisposableStore } from 'vs/base/common/lifecycle';
2323
import { Terminal } from 'xterm';
2424
import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager';
25+
import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
2526

2627
const category = terminalStrings.actionCategory;
2728

@@ -42,11 +43,15 @@ class AccessibleBufferContribution extends DisposableStore implements ITerminalC
4243
}
4344
layout(xterm: IXtermTerminal & { raw: Terminal }): void {
4445
if (!this._accessibleBufferWidget) {
45-
this._accessibleBufferWidget = this._instantiationService.createInstance(AccessibleBufferWidget, this._instance.instanceId, xterm);
46+
this._accessibleBufferWidget = this._instantiationService.createInstance(AccessibleBufferWidget, this._instance, xterm);
4647
}
4748
}
48-
show(): void {
49-
this._accessibleBufferWidget?.show();
49+
async show(): Promise<void> {
50+
await this._accessibleBufferWidget?.show();
51+
}
52+
53+
async createCommandQuickPick(): Promise<IQuickPick<IQuickPickItem> | undefined> {
54+
return this._accessibleBufferWidget?.createQuickPick();
5055
}
5156
}
5257
registerTerminalContribution(AccessibleBufferContribution.ID, AccessibleBufferContribution);
@@ -115,7 +120,39 @@ registerAction2(class extends Action2 {
115120
if (!instance) {
116121
return;
117122
}
118-
AccessibleBufferContribution.get(instance)?.show();
123+
await AccessibleBufferContribution.get(instance)?.show();
124+
}
125+
});
126+
127+
registerAction2(class extends Action2 {
128+
constructor() {
129+
super({
130+
id: TerminalCommandId.NavigateAccessibleBuffer,
131+
title: { value: localize('workbench.action.terminal.navigateAccessibleBuffer', 'Navigate Accessible Buffer'), original: 'Navigate Accessible Buffer' },
132+
f1: true,
133+
category,
134+
precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated),
135+
keybinding: [
136+
{
137+
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyO,
138+
weight: KeybindingWeight.WorkbenchContrib + 2,
139+
when: ContextKeyExpr.or(TerminalContextKeys.accessibleBufferFocus, ContextKeyExpr.and(CONTEXT_ACCESSIBILITY_MODE_ENABLED, TerminalContextKeys.focus))
140+
}
141+
],
142+
});
143+
}
144+
async run(accessor: ServicesAccessor): Promise<void> {
145+
const terminalService = accessor.get(ITerminalService);
146+
const terminalGroupService = accessor.get(ITerminalGroupService);
147+
const terminalEditorService = accessor.get(ITerminalEditorService);
148+
149+
const instance = await terminalService.getActiveOrCreateInstance();
150+
await revealActiveTerminal(instance, terminalEditorService, terminalGroupService);
151+
if (!instance) {
152+
return;
153+
}
154+
const quickPick = await AccessibleBufferContribution.get(instance)?.createCommandQuickPick();
155+
quickPick?.show();
119156
}
120157
});
121158

src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer.ts

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { KeyCode } from 'vs/base/common/keyCodes';
77
import { DisposableStore } from 'vs/base/common/lifecycle';
88
import { URI } from 'vs/base/common/uri';
9+
import * as dom from 'vs/base/browser/dom';
910
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
1011
import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
1112
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
@@ -19,8 +20,13 @@ import { TerminalSettingId } from 'vs/platform/terminal/common/terminal';
1920
import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard';
2021
import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions';
2122
import { ITerminalFont } from 'vs/workbench/contrib/terminal/common/terminal';
22-
import { IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal';
23+
import { ITerminalInstance, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal';
2324
import type { Terminal } from 'xterm';
25+
import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
26+
import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
27+
import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService';
28+
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
29+
import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey';
2430

2531
const enum Constants {
2632
Scheme = 'terminal-accessible-buffer',
@@ -35,15 +41,22 @@ export class AccessibleBufferWidget extends DisposableStore {
3541
private _editorContainer: HTMLElement;
3642
private _font: ITerminalFont;
3743
private _xtermElement: HTMLElement;
44+
private readonly _focusedContextKey: IContextKey<boolean>;
45+
private readonly _focusTracker: dom.IFocusTracker;
3846

3947
constructor(
40-
private readonly _instanceId: number,
48+
private readonly _instance: ITerminalInstance,
4149
private readonly _xterm: IXtermTerminal & { raw: Terminal },
4250
@IInstantiationService private readonly _instantiationService: IInstantiationService,
4351
@IModelService private readonly _modelService: IModelService,
44-
@IConfigurationService private readonly _configurationService: IConfigurationService
52+
@IConfigurationService private readonly _configurationService: IConfigurationService,
53+
@IQuickInputService private readonly _quickInputService: IQuickInputService,
54+
@IAudioCueService private readonly _audioCueService: IAudioCueService,
55+
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
56+
4557
) {
4658
super();
59+
this._focusedContextKey = TerminalContextKeys.accessibleBufferFocus.bindTo(this._contextKeyService);
4760
const codeEditorWidgetOptions: ICodeEditorWidgetOptions = {
4861
isSimpleWidget: true,
4962
contributions: EditorExtensionsRegistry.getSomeEditorContributions([LinkDetector.ID, SelectionClipboardContributionID])
@@ -76,6 +89,9 @@ export class AccessibleBufferWidget extends DisposableStore {
7689
this._editorContainer = document.createElement('div');
7790
this._accessibleBuffer.tabIndex = -1;
7891
this._bufferEditor = this._instantiationService.createInstance(CodeEditorWidget, this._editorContainer, editorOptions, codeEditorWidgetOptions);
92+
this._focusTracker = this.add(dom.trackFocus(this._editorContainer));
93+
this.add(this._focusTracker.onDidFocus(() => this._focusedContextKey.set(true)));
94+
this.add(this._focusTracker.onDidBlur(() => this._focusedContextKey.reset()));
7995
this._accessibleBuffer.replaceChildren(this._editorContainer);
8096
this._xtermElement.insertAdjacentElement('beforebegin', this._accessibleBuffer);
8197
this._bufferEditor.layout({ width: this._xtermElement.clientWidth, height: this._xtermElement.clientHeight });
@@ -95,18 +111,18 @@ export class AccessibleBufferWidget extends DisposableStore {
95111
}
96112
}));
97113
this.add(this._xterm.raw.onWriteParsed(async () => {
98-
if (this._accessibleBuffer.classList.contains(Constants.Active)) {
114+
if (this._focusedContextKey.get()) {
99115
await this._updateEditor(true);
100116
}
101117
}));
102-
this._updateEditor();
103118
this.add(this._bufferEditor.onDidFocusEditorText(async () => {
104119
// if the editor is focused via tab, we need to update the model
105120
// and show it
106121
await this._updateEditor();
107122
this._accessibleBuffer.classList.add(Constants.Active);
108123
this._xtermElement.classList.add(Constants.Hide);
109124
}));
125+
this._updateEditor();
110126
}
111127

112128
private _hide(): void {
@@ -122,7 +138,7 @@ export class AccessibleBufferWidget extends DisposableStore {
122138
const lineNumber = lineCount + 1;
123139
model.pushEditOperations(null, [{ range: { startLineNumber: lineNumber, endLineNumber: lineNumber, startColumn: 1, endColumn: 1 }, text: await this._getContent(lineNumber - 1) }], () => []);
124140
} else {
125-
model = await this._getTextModel(URI.from({ scheme: `${Constants.Scheme}-${this._instanceId}`, fragment: await this._getContent() }));
141+
model = await this._getTextModel(URI.from({ scheme: `${Constants.Scheme}-${this._instance.instanceId}`, fragment: await this._getContent() }));
126142
}
127143
if (!model) {
128144
throw new Error('Could not create accessible buffer editor model');
@@ -145,6 +161,49 @@ export class AccessibleBufferWidget extends DisposableStore {
145161
this._bufferEditor.setScrollTop(this._bufferEditor.getScrollHeight());
146162
}
147163

164+
async createQuickPick(): Promise<IQuickPick<IQuickPickItem> | undefined> {
165+
if (!this._focusedContextKey.get()) {
166+
await this.show();
167+
}
168+
const commands = this._instance.capabilities.get(TerminalCapability.CommandDetection)?.commands;
169+
if (!commands?.length) {
170+
return;
171+
}
172+
const quickPickItems: IQuickPickItem[] = [];
173+
for (const command of commands) {
174+
const line = command.marker?.line;
175+
if (!line) {
176+
continue;
177+
}
178+
quickPickItems.push(
179+
{
180+
label: localize('terminal.integrated.symbolQuickPick.labelNoExitCode', '{0}', command.command),
181+
meta: JSON.stringify({ line: line + 1, exitCode: command.exitCode })
182+
});
183+
}
184+
const quickPick = this._quickInputService.createQuickPick<IQuickPickItem>();
185+
quickPick.onDidAccept(() => {
186+
const item = quickPick.activeItems[0];
187+
const model = this._bufferEditor.getModel();
188+
if (!model || !item.meta) {
189+
return;
190+
}
191+
quickPick.hide();
192+
const data: { line: number; exitCode: number } = JSON.parse(item.meta);
193+
this._bufferEditor.setSelection({ startLineNumber: data.line, startColumn: 1, endLineNumber: data.line, endColumn: 1 });
194+
this._bufferEditor.revealLine(data.line);
195+
return;
196+
});
197+
quickPick.onDidChangeActive(() => {
198+
const data = quickPick.activeItems?.[0]?.meta;
199+
if (data && JSON.parse(data).exitCode) {
200+
this._audioCueService.playAudioCue(AudioCue.error, true);
201+
}
202+
});
203+
quickPick.items = quickPickItems.reverse();
204+
return quickPick;
205+
}
206+
148207
async show(): Promise<void> {
149208
await this._updateEditor();
150209
this._accessibleBuffer.tabIndex = -1;

0 commit comments

Comments
 (0)