keep the accessible buffer visible until tab/escape are used (#176054)

This commit is contained in:
Megan Rogge 2023-03-03 15:23:11 -06:00 committed by GitHub
parent 8ddd3bcf39
commit ffe596935b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 303 additions and 261 deletions

View file

@ -87,13 +87,13 @@
"vscode-proxy-agent": "^0.12.0",
"vscode-regexpp": "^3.1.0",
"vscode-textmate": "9.0.0",
"xterm": "5.2.0-beta.29",
"xterm": "5.2.0-beta.30",
"xterm-addon-canvas": "0.4.0-beta.7",
"xterm-addon-search": "0.11.0",
"xterm-addon-serialize": "0.9.0",
"xterm-addon-unicode11": "0.5.0",
"xterm-addon-webgl": "0.15.0-beta.7",
"xterm-headless": "5.2.0-beta.29",
"xterm-headless": "5.2.0-beta.30",
"yauzl": "^2.9.2",
"yazl": "^2.4.3"
},

View file

@ -24,13 +24,13 @@
"vscode-proxy-agent": "^0.12.0",
"vscode-regexpp": "^3.1.0",
"vscode-textmate": "9.0.0",
"xterm": "5.2.0-beta.29",
"xterm": "5.2.0-beta.30",
"xterm-addon-canvas": "0.4.0-beta.7",
"xterm-addon-search": "0.11.0",
"xterm-addon-serialize": "0.9.0",
"xterm-addon-unicode11": "0.5.0",
"xterm-addon-webgl": "0.15.0-beta.7",
"xterm-headless": "5.2.0-beta.29",
"xterm-headless": "5.2.0-beta.30",
"yauzl": "^2.9.2",
"yazl": "^2.4.3"
},

View file

@ -11,7 +11,7 @@
"tas-client-umd": "0.1.6",
"vscode-oniguruma": "1.7.0",
"vscode-textmate": "9.0.0",
"xterm": "5.2.0-beta.29",
"xterm": "5.2.0-beta.30",
"xterm-addon-canvas": "0.4.0-beta.7",
"xterm-addon-search": "0.11.0",
"xterm-addon-unicode11": "0.5.0",

View file

@ -88,7 +88,7 @@ xterm-addon-webgl@0.15.0-beta.7:
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.15.0-beta.7.tgz#ab247b499f61e8eebff92e08ec5ca999d87e06af"
integrity sha512-7WCI/D6uFNp3y9TeTsbSo1h7gCy4h/yP2lWn8ZEjCaiGvO11DbKMq17fbiwaR3YmGWXoRKkcLaNIiqxFnjKO4w==
xterm@5.2.0-beta.29:
version "5.2.0-beta.29"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.29.tgz#99764aff5cd8cdb4335f5d59466b134cfcb45e3e"
integrity sha512-zx5RKcQqo78bza4R/m3WtxAJCBAF4U61fy6cxqb1PkqXF9/qdYlySUCVOauMxv+6n6cAxt3EQWwLlgvbvQBbsw==
xterm@5.2.0-beta.30:
version "5.2.0-beta.30"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.30.tgz#6f50796d1652a61b30eeed7fa2bdd9c485a7d8ee"
integrity sha512-l1YBwMnakKXd638oxbzEg9Y1sWqxcrm/q7i5gBuWaK8N7Tq1NvF51FCamxXtfdL4dostgw8WoM+/6KRlL53t6A==

View file

@ -866,15 +866,15 @@ xterm-addon-webgl@0.15.0-beta.7:
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.15.0-beta.7.tgz#ab247b499f61e8eebff92e08ec5ca999d87e06af"
integrity sha512-7WCI/D6uFNp3y9TeTsbSo1h7gCy4h/yP2lWn8ZEjCaiGvO11DbKMq17fbiwaR3YmGWXoRKkcLaNIiqxFnjKO4w==
xterm-headless@5.2.0-beta.29:
version "5.2.0-beta.29"
resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.2.0-beta.29.tgz#dd08312fdb4292c217e685d9e2e8b1957364e298"
integrity sha512-1P4urIeDTkl2C+zGb4WUnKJMACZMPGYHwVXMjkB0WhMISbkt6M34MH9ljxHhnL99dHwlx2Lvi6wvhnpyZucWCg==
xterm-headless@5.2.0-beta.30:
version "5.2.0-beta.30"
resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.2.0-beta.30.tgz#f40b950f744111537a6403d33782669b1149fabb"
integrity sha512-aW6yljrcuu74kxg3w1DG1CZJSz38nKY/HOX3YOOE7cqxlkVXM7lltXZFEiF0xXDR0GHcmnEwnFWqA2rDmdhoDA==
xterm@5.2.0-beta.29:
version "5.2.0-beta.29"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.29.tgz#99764aff5cd8cdb4335f5d59466b134cfcb45e3e"
integrity sha512-zx5RKcQqo78bza4R/m3WtxAJCBAF4U61fy6cxqb1PkqXF9/qdYlySUCVOauMxv+6n6cAxt3EQWwLlgvbvQBbsw==
xterm@5.2.0-beta.30:
version "5.2.0-beta.30"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.30.tgz#6f50796d1652a61b30eeed7fa2bdd9c485a7d8ee"
integrity sha512-l1YBwMnakKXd638oxbzEg9Y1sWqxcrm/q7i5gBuWaK8N7Tq1NvF51FCamxXtfdL4dostgw8WoM+/6KRlL53t6A==
yallist@^4.0.0:
version "4.0.0"

View file

@ -587,18 +587,28 @@
outline-color: var(--vscode-focusBorder);
}
/* Overrides for the a11y buffer as we're hosting monaco within it */
.xterm .xterm-accessible-buffer {
padding: 0 !important;
overflow: initial !important;
overflow-x: initial !important;
}
.xterm .xterm-accessible-buffer:focus-within {
top: 0 !important;
left: 0 !important;
pointer-events: all !important;
}
.xterm > .xterm-accessible-buffer {
.monaco-workbench .accessible-buffer {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
opacity: 0;
/* Reset cursor style as monaco controls it here */
cursor: default;
padding: 0;
overflow: initial;
overflow-x: initial;
}
.monaco-workbench .accessible-buffer div {
white-space: pre-wrap;
}
.monaco-workbench .accessible-buffer.focus-within,
.monaco-workbench .accessible-buffer.active {
pointer-events: all;
opacity: 1;
z-index: 33;
background-color: var(--vscode-terminal-background, --vscode-panel-background);
}

View file

@ -209,27 +209,6 @@
position: relative;
}
.xterm .xterm-accessible-buffer {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
padding: .5em;
opacity: 0;
overflow: scroll;
overflow-x: hidden;
}
.xterm .xterm-accessible-buffer div {
white-space: pre-wrap;
}
.xterm .xterm-accessible-buffer:focus-within {
opacity: 1;
z-index: 33;
top: -5px;
left: 5px;
pointer-events: none;
background-color: var(--vscode-terminal-background, --vscode-panel-background);
.xterm.terminal.hide {
display: none;
}

View file

@ -20,6 +20,7 @@ import { IEditableData } from 'vs/workbench/common/views';
import { ITerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList';
import { ScrollPosition } from 'vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon';
import { ITerminalQuickFixAddon } from 'vs/workbench/contrib/terminal/browser/xterm/quickFixAddon';
import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal';
import { IRegisterContributedProfileArgs, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalBackend, ITerminalConfigHelper, ITerminalFont, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal';
import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn';
import { ISimpleSelectedSuggestion } from 'vs/workbench/services/suggest/browser/simpleSuggestWidget';
@ -587,7 +588,7 @@ export interface ITerminalInstance {
/**
* The xterm.js instance for this terminal.
*/
readonly xterm?: IXtermTerminal;
readonly xterm?: XtermTerminal;
/**
* Returns an array of data events that have fired within the first 10 seconds. If this is
@ -1040,11 +1041,6 @@ export interface IXtermTerminal {
*/
getBufferReverseIterator(): IterableIterator<string>;
/**
* Focuses the accessible buffer, updating its contents
*/
focusAccessibleBuffer(): Promise<void>;
/**
* Refreshes the terminal after it has been moved.
*/

View file

@ -28,7 +28,7 @@ import { IListService } from 'vs/platform/list/browser/listService';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IPickOptions, IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { ITerminalProfile, TerminalExitReason, TerminalLocation, TerminalSettingId, terminalTabFocusContextKey } from 'vs/platform/terminal/common/terminal';
import { ITerminalProfile, TerminalExitReason, TerminalLocation, TerminalSettingId } from 'vs/platform/terminal/common/terminal';
import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands';
import { CLOSE_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands';
@ -60,7 +60,6 @@ import { FileKind } from 'vs/platform/files/common/files';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
import { killTerminalIcon, newTerminalIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons';
import { editorTabFocusContextKey } from 'vs/workbench/browser/parts/editor/tabFocus';
export const switchTerminalActionViewItemSeparator = '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500';
export const switchTerminalShowTabsTitle = localize('showTerminalTabs', "Show Tabs");
@ -380,27 +379,6 @@ export function registerTerminalActions() {
}
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: TerminalCommandId.FocusAccessibleBuffer,
title: { value: localize('workbench.action.terminal.focusAccessibleBuffer', 'Focus Accessible Buffer'), original: 'Focus Accessible Buffer' },
f1: true,
category,
precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated),
keybinding: [
{
primary: KeyMod.Shift | KeyCode.Tab,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.or(terminalTabFocusContextKey, ContextKeyExpr.and(CONTEXT_ACCESSIBILITY_MODE_ENABLED, editorTabFocusContextKey))
}
],
});
}
async run(accessor: ServicesAccessor): Promise<void> {
await accessor.get(ITerminalService).activeInstance?.xterm?.focusAccessibleBuffer();
}
});
registerAction2(class extends Action2 {
constructor() {
super({

View file

@ -116,7 +116,6 @@ function getXtermConstructor(): Promise<typeof XTermTerminal> {
// Localize strings
Terminal.strings.promptLabel = nls.localize('terminal.integrated.a11yPromptLabel', 'Terminal input');
Terminal.strings.tooMuchOutput = nls.localize('terminal.integrated.a11yTooMuchOutput', 'Too much output to announce, navigate to rows manually to read');
Terminal.strings.accessibleBuffer = nls.localize('terminal.integrated.accessibleBuffer', 'Terminal buffer');
resolve(Terminal);
});
return xtermConstructor;

View file

@ -12,7 +12,7 @@ import type { SerializeAddon as SerializeAddonType } from 'xterm-addon-serialize
import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IShellIntegration, TerminalSettingId } from 'vs/platform/terminal/common/terminal';
import { ITerminalFont } from 'vs/workbench/contrib/terminal/common/terminal';
@ -35,17 +35,6 @@ import { Emitter } from 'vs/base/common/event';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { SuggestAddon } from 'vs/workbench/contrib/terminal/browser/xterm/suggestAddon';
import { IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { URI } from 'vs/base/common/uri';
import { ITextModel } from 'vs/editor/common/model';
import { IModelService } from 'vs/editor/common/services/model';
import { StringBuilder } from 'vs/editor/common/core/stringBuilder';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions';
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
import { LinkDetector } from 'vs/editor/contrib/links/browser/links';
import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard';
import { addDisposableListener } from 'vs/base/browser/dom';
const enum RenderConstants {
/**
@ -148,8 +137,6 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II
private _webglAddon?: WebglAddonType;
private _serializeAddon?: SerializeAddonType;
private _accessibleBuffer: AccessibleBuffer | undefined;
private _lastFindResult: { resultIndex: number; resultCount: number } | undefined;
get findResult(): { resultIndex: number; resultCount: number } | undefined { return this._lastFindResult; }
get isStdinDisabled(): boolean { return !!this.raw.options.disableStdin; }
@ -273,10 +260,6 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II
});
}
async focusAccessibleBuffer(): Promise<void> {
this._accessibleBuffer?.focus();
}
async getSelectionAsHtml(command?: ITerminalCommand): Promise<string> {
if (!this._serializeAddon) {
const Addon = await this._getSerializeAddonConstructor();
@ -304,7 +287,6 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II
if (!this._container) {
this.raw.open(container);
}
this._accessibleBuffer = this._instantiationService.createInstance(AccessibleBuffer, this, this._capabilities);
// TODO: Move before open to the DOM renderer doesn't initialize
if (this._shouldLoadWebgl()) {
this._enableWebglRenderer();
@ -754,152 +736,3 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II
this.raw.write(data);
}
}
const enum AccessibleBufferConstants {
Scheme = 'terminal-accessible-buffer'
}
class AccessibleBuffer extends DisposableStore {
private _accessibleBuffer: HTMLElement;
private _bufferEditor: CodeEditorWidget;
private _editorContainer: HTMLElement;
private _commandFinishedDisposable: IDisposable | undefined;
private _refreshSelection: boolean = true;
private _registered: boolean = false;
private _lastContentLength: number = 0;
private _font: ITerminalFont;
constructor(
private readonly _terminal: XtermTerminal,
private readonly _capabilities: ITerminalCapabilityStore,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IModelService private readonly _modelService: IModelService,
@IConfigurationService configurationService: IConfigurationService
) {
super();
const codeEditorWidgetOptions: ICodeEditorWidgetOptions = {
isSimpleWidget: true,
contributions: EditorExtensionsRegistry.getSomeEditorContributions([LinkDetector.ID, SelectionClipboardContributionID])
};
this._font = this._terminal.getFont();
const editorOptions: IEditorConstructionOptions = {
...getSimpleEditorOptions(),
lineDecorationsWidth: 6,
dragAndDrop: true,
cursorWidth: 1,
fontSize: this._font.fontSize,
lineHeight: this._font.charHeight ? this._font.charHeight * this._font.lineHeight : 1,
fontFamily: this._font.fontFamily,
wrappingStrategy: 'advanced',
wrappingIndent: 'none',
padding: { top: 2, bottom: 2 },
quickSuggestions: false,
renderWhitespace: 'none',
dropIntoEditor: { enabled: true },
accessibilitySupport: configurationService.getValue<'auto' | 'off' | 'on'>('editor.accessibilitySupport'),
cursorBlinking: configurationService.getValue('terminal.integrated.cursorBlinking'),
readOnly: true
};
this._accessibleBuffer = this._terminal.raw.element!.querySelector('.xterm-accessible-buffer') as HTMLElement;
// Prevent the accessible buffer letting mouse events to propogate to xterm.js while it's
// visible.
this.add(addDisposableListener(this._accessibleBuffer, 'mousedown', e => e.stopImmediatePropagation()));
this._accessibleBuffer.tabIndex = -1;
this._editorContainer = document.createElement('div');
this._bufferEditor = this._instantiationService.createInstance(CodeEditorWidget, this._editorContainer, editorOptions, codeEditorWidgetOptions);
this.add(configurationService.onDidChangeConfiguration(e => {
if (e.affectedKeys.has(TerminalSettingId.FontFamily)) {
this._font = this._terminal.getFont();
}
}));
}
async focus(): Promise<void> {
await this._updateBufferEditor();
}
private async _updateBufferEditor(): Promise<void> {
const commandDetection = this._capabilities.get(TerminalCapability.CommandDetection);
const fragment = !!commandDetection ? this._getShellIntegrationContent() : this._getAllContent();
const model = await this._getTextModel(URI.from({ scheme: AccessibleBufferConstants.Scheme, fragment }));
if (model) {
this._bufferEditor.setModel(model);
}
if (!this._registered) {
this.add(this._terminal.raw.registerBufferElementProvider({ provideBufferElements: () => this._editorContainer }));
// When this is created, the element isn't yet attached so the dimensions are tiny
this._bufferEditor.layout({ width: this._accessibleBuffer.clientWidth, height: this._accessibleBuffer.clientHeight });
this._registered = true;
}
if (!this._commandFinishedDisposable && commandDetection) {
this._commandFinishedDisposable = commandDetection.onCommandFinished(() => this._refreshSelection = true);
this.add(this._commandFinishedDisposable);
}
if (this._lastContentLength !== fragment.length || this._refreshSelection) {
let lineNumber = 1;
const lineCount = model?.getLineCount();
if (lineCount && model) {
lineNumber = commandDetection ? lineCount - 1 : lineCount > 2 ? lineCount - 2 : 1;
}
this._bufferEditor.setSelection({ startLineNumber: lineNumber, startColumn: 1, endLineNumber: lineNumber, endColumn: 1 });
this._bufferEditor.setScrollTop(this._bufferEditor.getScrollHeight());
this._refreshSelection = false;
this._lastContentLength = fragment.length;
}
// Updates xterm's accessibleBufferActive property
// such that mouse events do not cause the terminal buffer
// to steal the focus
this._accessibleBuffer.focus();
this._bufferEditor.focus();
}
private async _getTextModel(resource: URI): Promise<ITextModel | null> {
const existing = this._modelService.getModel(resource);
if (existing && !existing.isDisposed()) {
return existing;
}
return this._modelService.createModel(resource.fragment, null, resource, false);
}
private _getShellIntegrationContent(): string {
const commands = this._capabilities.get(TerminalCapability.CommandDetection)?.commands;
const sb = new StringBuilder(10000);
if (!commands?.length) {
return this._getAllContent();
}
for (const command of commands) {
sb.appendString(command.command.replace(new RegExp(' ', 'g'), '\xA0'));
if (command.exitCode !== 0) {
sb.appendString(` exited with code ${command.exitCode}`);
}
sb.appendString('\n');
sb.appendString(command.getOutput()?.replace(new RegExp(' ', 'g'), '\xA0') || '');
}
return sb.build();
}
private _getAllContent(): string {
const lines: string[] = [];
let currentLine: string = '';
const buffer = this._terminal.raw.buffer.active;
const end = buffer.length;
for (let i = 0; i < end; i++) {
const line = buffer.getLine(i);
if (!line) {
continue;
}
const isWrapped = buffer.getLine(i + 1)?.isWrapped;
currentLine += line.translateToString(!isWrapped);
if (currentLine && !isWrapped || i === end - 1) {
lines.push(currentLine.replace(new RegExp(' ', 'g'), '\xA0'));
currentLine = '';
}
}
return lines.join('\n');
}
}

View file

@ -6,19 +6,51 @@
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { localize } from 'vs/nls';
import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility';
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { TerminalLocation } from 'vs/platform/terminal/common/terminal';
import { TerminalLocation, terminalTabFocusContextKey } from 'vs/platform/terminal/common/terminal';
import { AccessibilityHelpWidget } from 'vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp';
import { ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal';
import { ITerminalContribution, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ITerminalProcessManager, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal';
import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey';
import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings';
import { AccessibleBufferWidget } from 'vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer';
import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { Terminal } from 'xterm';
import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager';
const category = terminalStrings.actionCategory;
class AccessibleBufferContribution extends DisposableStore implements ITerminalContribution {
static readonly ID: 'terminal.accessible-buffer';
static get(instance: ITerminalInstance): AccessibleBufferContribution | null {
return instance.getContribution<AccessibleBufferContribution>(AccessibleBufferContribution.ID);
}
private _accessibleBufferWidget: AccessibleBufferWidget | undefined;
constructor(
private readonly _instance: ITerminalInstance,
processManager: ITerminalProcessManager,
widgetManager: TerminalWidgetManager,
@IInstantiationService private readonly _instantiationService: IInstantiationService
) {
super();
}
xtermReady(xterm: IXtermTerminal & { raw: Terminal }): void {
this._accessibleBufferWidget = this._instantiationService.createInstance(AccessibleBufferWidget, xterm, this._instance.capabilities);
}
show(): void {
this._accessibleBufferWidget?.show();
}
}
registerTerminalContribution(AccessibleBufferContribution.ID, AccessibleBufferContribution);
registerAction2(class extends Action2 {
constructor() {
super({
@ -55,6 +87,37 @@ registerAction2(class extends Action2 {
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: TerminalCommandId.FocusAccessibleBuffer,
title: { value: localize('workbench.action.terminal.focusAccessibleBuffer', 'Focus Accessible Buffer'), original: 'Focus Accessible Buffer' },
f1: true,
category,
precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated),
keybinding: [
{
primary: KeyMod.Shift | KeyCode.Tab,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(CONTEXT_ACCESSIBILITY_MODE_ENABLED, terminalTabFocusContextKey)
}
],
});
}
async run(accessor: ServicesAccessor): Promise<void> {
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;
}
AccessibleBufferContribution.get(instance)?.show();
}
});
async function revealActiveTerminal(instance: ITerminalInstance, terminalEditorService: ITerminalEditorService, terminalGroupService: ITerminalGroupService): Promise<void> {
if (instance.target === TerminalLocation.Editor) {
await terminalEditorService.revealActiveEditor();

View file

@ -0,0 +1,184 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { KeyCode } from 'vs/base/common/keyCodes';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
import { StringBuilder } from 'vs/editor/common/core/stringBuilder';
import { ITextModel } from 'vs/editor/common/model';
import { IModelService } from 'vs/editor/common/services/model';
import { LinkDetector } from 'vs/editor/contrib/links/browser/links';
import { localize } from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
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 type { Terminal } from 'xterm';
const enum AccessibleBufferConstants {
Scheme = 'terminal-accessible-buffer'
}
export class AccessibleBufferWidget extends DisposableStore {
public static ID: string = AccessibleBufferConstants.Scheme;
private _accessibleBuffer: HTMLElement;
private _bufferEditor: CodeEditorWidget;
private _editorContainer: HTMLElement;
private _commandFinishedDisposable: IDisposable | undefined;
private _refreshSelection: boolean = true;
private _registered: boolean = false;
private _lastContentLength: number = 0;
private _font: ITerminalFont;
private _xtermElement: HTMLElement;
constructor(
private readonly _xterm: IXtermTerminal & { raw: Terminal },
private readonly _capabilities: ITerminalCapabilityStore,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IModelService private readonly _modelService: IModelService,
@IConfigurationService private readonly _configurationService: IConfigurationService
) {
super();
const codeEditorWidgetOptions: ICodeEditorWidgetOptions = {
isSimpleWidget: true,
contributions: EditorExtensionsRegistry.getSomeEditorContributions([LinkDetector.ID, SelectionClipboardContributionID])
};
this._font = _xterm.getFont();
this._xtermElement = _xterm.raw.element!;
const editorOptions: IEditorConstructionOptions = {
...getSimpleEditorOptions(),
lineDecorationsWidth: 6,
dragAndDrop: true,
cursorWidth: 1,
fontSize: this._font.fontSize,
lineHeight: this._font.charHeight ? this._font.charHeight * this._font.lineHeight : 1,
fontFamily: this._font.fontFamily,
wrappingStrategy: 'advanced',
wrappingIndent: 'none',
padding: { top: 2, bottom: 2 },
quickSuggestions: false,
renderWhitespace: 'none',
dropIntoEditor: { enabled: true },
accessibilitySupport: this._configurationService.getValue<'auto' | 'off' | 'on'>('editor.accessibilitySupport'),
cursorBlinking: this._configurationService.getValue('terminal.integrated.cursorBlinking'),
readOnly: true
};
this._accessibleBuffer = document.createElement('div');
this._accessibleBuffer.setAttribute('role', 'document');
this._accessibleBuffer.ariaRoleDescription = localize('terminal.integrated.accessibleBuffer', 'Terminal buffer');
this._accessibleBuffer.classList.add('accessible-buffer');
const elt = _xterm.raw.element;
if (elt) {
elt.insertAdjacentElement('beforebegin', this._accessibleBuffer);
}
this._editorContainer = document.createElement('div');
this._bufferEditor = this._instantiationService.createInstance(CodeEditorWidget, this._editorContainer, editorOptions, codeEditorWidgetOptions);
this.add(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectedKeys.has(TerminalSettingId.FontFamily)) {
this._font = _xterm.getFont();
}
}));
}
private _hide(): void {
this._accessibleBuffer.classList.remove('active');
this._xtermElement.classList.remove('hide');
this._xterm.raw.focus();
}
async show(): Promise<void> {
const commandDetection = this._capabilities.get(TerminalCapability.CommandDetection);
const fragment = !!commandDetection ? this._getShellIntegrationContent() : this._getAllContent();
const model = await this._getTextModel(URI.from({ scheme: AccessibleBufferConstants.Scheme, fragment }));
if (model) {
this._bufferEditor.setModel(model);
}
if (!this._registered) {
this._bufferEditor.layout({ width: this._xtermElement.clientWidth, height: this._xtermElement.clientHeight });
this._bufferEditor.onKeyDown((e) => {
if (e.keyCode === KeyCode.Escape || e.keyCode === KeyCode.Tab) {
this._hide();
}
});
if (commandDetection) {
this._commandFinishedDisposable = commandDetection.onCommandFinished(() => this._refreshSelection = true);
this.add(this._commandFinishedDisposable);
}
this._registered = true;
}
this._accessibleBuffer.tabIndex = -1;
this._accessibleBuffer.classList.add('active');
this._xtermElement.classList.add('hide');
if (this._lastContentLength !== fragment.length || this._refreshSelection) {
let lineNumber = 1;
const lineCount = model?.getLineCount();
if (lineCount && model) {
lineNumber = commandDetection ? lineCount - 1 : lineCount > 2 ? lineCount - 2 : 1;
}
this._bufferEditor.setSelection({ startLineNumber: lineNumber, startColumn: 1, endLineNumber: lineNumber, endColumn: 1 });
this._bufferEditor.setScrollTop(this._bufferEditor.getScrollHeight());
this._refreshSelection = false;
this._lastContentLength = fragment.length;
}
this._accessibleBuffer.replaceChildren(this._editorContainer);
this._bufferEditor.focus();
}
private async _getTextModel(resource: URI): Promise<ITextModel | null> {
const existing = this._modelService.getModel(resource);
if (existing && !existing.isDisposed()) {
return existing;
}
return this._modelService.createModel(resource.fragment, null, resource, false);
}
private _getShellIntegrationContent(): string {
const commands = this._capabilities.get(TerminalCapability.CommandDetection)?.commands;
const sb = new StringBuilder(10000);
if (!commands?.length) {
return this._getAllContent();
}
for (const command of commands) {
sb.appendString(command.command.replace(new RegExp(' ', 'g'), '\xA0'));
if (command.exitCode !== 0) {
sb.appendString(` exited with code ${command.exitCode}`);
}
sb.appendString('\n');
sb.appendString(command.getOutput()?.replace(new RegExp(' ', 'g'), '\xA0') || '');
}
return sb.build();
}
private _getAllContent(): string {
const lines: string[] = [];
let currentLine: string = '';
const buffer = this._xterm?.raw.buffer.active;
if (!buffer) {
return '';
}
const end = buffer.length;
for (let i = 0; i < end; i++) {
const line = buffer.getLine(i);
if (!line) {
continue;
}
const isWrapped = buffer.getLine(i + 1)?.isWrapped;
currentLine += line.translateToString(!isWrapped);
if (currentLine && !isWrapped || i === end - 1) {
lines.push(currentLine.replace(new RegExp(' ', 'g'), '\xA0'));
currentLine = '';
}
}
return lines.join('\n');
}
}

View file

@ -11863,15 +11863,15 @@ xterm-addon-webgl@0.15.0-beta.7:
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.15.0-beta.7.tgz#ab247b499f61e8eebff92e08ec5ca999d87e06af"
integrity sha512-7WCI/D6uFNp3y9TeTsbSo1h7gCy4h/yP2lWn8ZEjCaiGvO11DbKMq17fbiwaR3YmGWXoRKkcLaNIiqxFnjKO4w==
xterm-headless@5.2.0-beta.29:
version "5.2.0-beta.29"
resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.2.0-beta.29.tgz#dd08312fdb4292c217e685d9e2e8b1957364e298"
integrity sha512-1P4urIeDTkl2C+zGb4WUnKJMACZMPGYHwVXMjkB0WhMISbkt6M34MH9ljxHhnL99dHwlx2Lvi6wvhnpyZucWCg==
xterm-headless@5.2.0-beta.30:
version "5.2.0-beta.30"
resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.2.0-beta.30.tgz#f40b950f744111537a6403d33782669b1149fabb"
integrity sha512-aW6yljrcuu74kxg3w1DG1CZJSz38nKY/HOX3YOOE7cqxlkVXM7lltXZFEiF0xXDR0GHcmnEwnFWqA2rDmdhoDA==
xterm@5.2.0-beta.29:
version "5.2.0-beta.29"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.29.tgz#99764aff5cd8cdb4335f5d59466b134cfcb45e3e"
integrity sha512-zx5RKcQqo78bza4R/m3WtxAJCBAF4U61fy6cxqb1PkqXF9/qdYlySUCVOauMxv+6n6cAxt3EQWwLlgvbvQBbsw==
xterm@5.2.0-beta.30:
version "5.2.0-beta.30"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.30.tgz#6f50796d1652a61b30eeed7fa2bdd9c485a7d8ee"
integrity sha512-l1YBwMnakKXd638oxbzEg9Y1sWqxcrm/q7i5gBuWaK8N7Tq1NvF51FCamxXtfdL4dostgw8WoM+/6KRlL53t6A==
y18n@^3.2.1:
version "3.2.2"