diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 5381e1be66c3b..92e05e7cafe61 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -489,6 +489,7 @@ export const enum TerminalCommandId { OpenWebLink = 'workbench.action.terminal.openUrlLink', RunRecentCommand = 'workbench.action.terminal.runRecentCommand', FocusAccessibleBuffer = 'workbench.action.terminal.focusAccessibleBuffer', + NavigateAccessibleBuffer = 'workbench.action.terminal.navigateAccessibleBuffer', CopyLastCommandOutput = 'workbench.action.terminal.copyLastCommandOutput', GoToRecentDirectory = 'workbench.action.terminal.goToRecentDirectory', CopyAndClearSelection = 'workbench.action.terminal.copyAndClearSelection', diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index 3dd2f02093311..a3eb8693d018b 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -14,6 +14,7 @@ export const enum TerminalContextKeyStrings { HasFixedWidth = 'terminalHasFixedWidth', ProcessSupported = 'terminalProcessSupported', Focus = 'terminalFocus', + AccessibleBufferFocus = 'terminalAccessibleBufferFocus', EditorFocus = 'terminalEditorFocus', TabsFocus = 'terminalTabsFocus', WebExtensionContributedProfile = 'terminalWebExtensionContributedProfile', @@ -42,6 +43,9 @@ export namespace TerminalContextKeys { /** Whether the terminal is focused. */ export const focus = new RawContextKey(TerminalContextKeyStrings.Focus, false, localize('terminalFocusContextKey', "Whether the terminal is focused.")); + /** Whether the accessible buffer is focused. */ + export const accessibleBufferFocus = new RawContextKey(TerminalContextKeyStrings.AccessibleBufferFocus, false, localize('terminalAccessibleBufferFocusContextKey', "Whether the terminal accessible buffer is focused.")); + /** Whether a terminal in the editor area is focused. */ export const editorFocus = new RawContextKey(TerminalContextKeyStrings.EditorFocus, false, localize('terminalEditorFocusContextKey', "Whether a terminal in the editor area is focused.")); diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts index 5d1f3ef03fafa..d68142dfdc9d3 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts @@ -22,6 +22,7 @@ import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/brow import { DisposableStore } from 'vs/base/common/lifecycle'; import { Terminal } from 'xterm'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; +import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; const category = terminalStrings.actionCategory; @@ -42,11 +43,15 @@ class AccessibleBufferContribution extends DisposableStore implements ITerminalC } layout(xterm: IXtermTerminal & { raw: Terminal }): void { if (!this._accessibleBufferWidget) { - this._accessibleBufferWidget = this._instantiationService.createInstance(AccessibleBufferWidget, this._instance.instanceId, xterm); + this._accessibleBufferWidget = this._instantiationService.createInstance(AccessibleBufferWidget, this._instance, xterm); } } - show(): void { - this._accessibleBufferWidget?.show(); + async show(): Promise { + await this._accessibleBufferWidget?.show(); + } + + async createCommandQuickPick(): Promise | undefined> { + return this._accessibleBufferWidget?.createQuickPick(); } } registerTerminalContribution(AccessibleBufferContribution.ID, AccessibleBufferContribution); @@ -115,7 +120,39 @@ registerAction2(class extends Action2 { if (!instance) { return; } - AccessibleBufferContribution.get(instance)?.show(); + await AccessibleBufferContribution.get(instance)?.show(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: TerminalCommandId.NavigateAccessibleBuffer, + title: { value: localize('workbench.action.terminal.navigateAccessibleBuffer', 'Navigate Accessible Buffer'), original: 'Navigate Accessible Buffer' }, + f1: true, + category, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + keybinding: [ + { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyO, + weight: KeybindingWeight.WorkbenchContrib + 2, + when: ContextKeyExpr.or(TerminalContextKeys.accessibleBufferFocus, ContextKeyExpr.and(CONTEXT_ACCESSIBILITY_MODE_ENABLED, TerminalContextKeys.focus)) + } + ], + }); + } + async run(accessor: ServicesAccessor): Promise { + const terminalService = accessor.get(ITerminalService); + const terminalGroupService = accessor.get(ITerminalGroupService); + const terminalEditorService = accessor.get(ITerminalEditorService); + + const instance = await terminalService.getActiveOrCreateInstance(); + await revealActiveTerminal(instance, terminalEditorService, terminalGroupService); + if (!instance) { + return; + } + const quickPick = await AccessibleBufferContribution.get(instance)?.createCommandQuickPick(); + quickPick?.show(); } }); diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer.ts index 6e756793a2ddc..afa4a59a5c018 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer.ts @@ -6,6 +6,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import * as dom from 'vs/base/browser/dom'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; @@ -19,8 +20,13 @@ import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { ITerminalFont } from 'vs/workbench/contrib/terminal/common/terminal'; -import { IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalInstance, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import type { Terminal } from 'xterm'; +import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; const enum Constants { Scheme = 'terminal-accessible-buffer', @@ -35,15 +41,22 @@ export class AccessibleBufferWidget extends DisposableStore { private _editorContainer: HTMLElement; private _font: ITerminalFont; private _xtermElement: HTMLElement; + private readonly _focusedContextKey: IContextKey; + private readonly _focusTracker: dom.IFocusTracker; constructor( - private readonly _instanceId: number, + private readonly _instance: ITerminalInstance, private readonly _xterm: IXtermTerminal & { raw: Terminal }, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IModelService private readonly _modelService: IModelService, - @IConfigurationService private readonly _configurationService: IConfigurationService + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @IAudioCueService private readonly _audioCueService: IAudioCueService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + ) { super(); + this._focusedContextKey = TerminalContextKeys.accessibleBufferFocus.bindTo(this._contextKeyService); const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { isSimpleWidget: true, contributions: EditorExtensionsRegistry.getSomeEditorContributions([LinkDetector.ID, SelectionClipboardContributionID]) @@ -76,6 +89,9 @@ export class AccessibleBufferWidget extends DisposableStore { this._editorContainer = document.createElement('div'); this._accessibleBuffer.tabIndex = -1; this._bufferEditor = this._instantiationService.createInstance(CodeEditorWidget, this._editorContainer, editorOptions, codeEditorWidgetOptions); + this._focusTracker = this.add(dom.trackFocus(this._editorContainer)); + this.add(this._focusTracker.onDidFocus(() => this._focusedContextKey.set(true))); + this.add(this._focusTracker.onDidBlur(() => this._focusedContextKey.reset())); this._accessibleBuffer.replaceChildren(this._editorContainer); this._xtermElement.insertAdjacentElement('beforebegin', this._accessibleBuffer); this._bufferEditor.layout({ width: this._xtermElement.clientWidth, height: this._xtermElement.clientHeight }); @@ -95,11 +111,10 @@ export class AccessibleBufferWidget extends DisposableStore { } })); this.add(this._xterm.raw.onWriteParsed(async () => { - if (this._accessibleBuffer.classList.contains(Constants.Active)) { + if (this._focusedContextKey.get()) { await this._updateEditor(true); } })); - this._updateEditor(); this.add(this._bufferEditor.onDidFocusEditorText(async () => { // if the editor is focused via tab, we need to update the model // and show it @@ -107,6 +122,7 @@ export class AccessibleBufferWidget extends DisposableStore { this._accessibleBuffer.classList.add(Constants.Active); this._xtermElement.classList.add(Constants.Hide); })); + this._updateEditor(); } private _hide(): void { @@ -122,7 +138,7 @@ export class AccessibleBufferWidget extends DisposableStore { const lineNumber = lineCount + 1; model.pushEditOperations(null, [{ range: { startLineNumber: lineNumber, endLineNumber: lineNumber, startColumn: 1, endColumn: 1 }, text: await this._getContent(lineNumber - 1) }], () => []); } else { - model = await this._getTextModel(URI.from({ scheme: `${Constants.Scheme}-${this._instanceId}`, fragment: await this._getContent() })); + model = await this._getTextModel(URI.from({ scheme: `${Constants.Scheme}-${this._instance.instanceId}`, fragment: await this._getContent() })); } if (!model) { throw new Error('Could not create accessible buffer editor model'); @@ -145,6 +161,49 @@ export class AccessibleBufferWidget extends DisposableStore { this._bufferEditor.setScrollTop(this._bufferEditor.getScrollHeight()); } + async createQuickPick(): Promise | undefined> { + if (!this._focusedContextKey.get()) { + await this.show(); + } + const commands = this._instance.capabilities.get(TerminalCapability.CommandDetection)?.commands; + if (!commands?.length) { + return; + } + const quickPickItems: IQuickPickItem[] = []; + for (const command of commands) { + const line = command.marker?.line; + if (!line) { + continue; + } + quickPickItems.push( + { + label: localize('terminal.integrated.symbolQuickPick.labelNoExitCode', '{0}', command.command), + meta: JSON.stringify({ line: line + 1, exitCode: command.exitCode }) + }); + } + const quickPick = this._quickInputService.createQuickPick(); + quickPick.onDidAccept(() => { + const item = quickPick.activeItems[0]; + const model = this._bufferEditor.getModel(); + if (!model || !item.meta) { + return; + } + quickPick.hide(); + const data: { line: number; exitCode: number } = JSON.parse(item.meta); + this._bufferEditor.setSelection({ startLineNumber: data.line, startColumn: 1, endLineNumber: data.line, endColumn: 1 }); + this._bufferEditor.revealLine(data.line); + return; + }); + quickPick.onDidChangeActive(() => { + const data = quickPick.activeItems?.[0]?.meta; + if (data && JSON.parse(data).exitCode) { + this._audioCueService.playAudioCue(AudioCue.error, true); + } + }); + quickPick.items = quickPickItems.reverse(); + return quickPick; + } + async show(): Promise { await this._updateEditor(); this._accessibleBuffer.tabIndex = -1;