diff --git a/package.json b/package.json index a842f18f4d645..1da790a529a63 100644 --- a/package.json +++ b/package.json @@ -87,13 +87,13 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "8.0.0", - "xterm": "5.2.0-beta.28", + "xterm": "5.2.0-beta.29", "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.28", + "xterm-headless": "5.2.0-beta.29", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/package.json b/remote/package.json index 1eec4929126e6..d39cdb493619c 100644 --- a/remote/package.json +++ b/remote/package.json @@ -24,13 +24,13 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "8.0.0", - "xterm": "5.2.0-beta.28", + "xterm": "5.2.0-beta.29", "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.28", + "xterm-headless": "5.2.0-beta.29", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/web/package.json b/remote/web/package.json index ec717aee479c1..53620ea494a33 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -11,7 +11,7 @@ "tas-client-umd": "0.1.6", "vscode-oniguruma": "1.7.0", "vscode-textmate": "8.0.0", - "xterm": "5.2.0-beta.28", + "xterm": "5.2.0-beta.29", "xterm-addon-canvas": "0.4.0-beta.7", "xterm-addon-search": "0.11.0", "xterm-addon-unicode11": "0.5.0", diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index c5118aaad4b28..79830abf89f9c 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -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.28: - version "5.2.0-beta.28" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.28.tgz#852347e4eaf5aae7d82c90592a42adc9daab2a79" - integrity sha512-aLDxCuqjWHjvnhfWfkxy/y6coNrC+QIhbDe2sdfLPkrxhK6KnYE6qiZD5jXUIQXeq0KmSDcYi/esuKujvobC2A== +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== diff --git a/remote/yarn.lock b/remote/yarn.lock index 29cdd7e80282c..efc0ca00a2622 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -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.28: - version "5.2.0-beta.28" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.2.0-beta.28.tgz#d2c149da51ef138f46268b755c4fdc4202eb771c" - integrity sha512-4XcjBhFwuyjpz2ubESwp75UceySOOKdJszKyyxOQ3/7L937uiVEBBLc8T231XU8lSwWUU7czyNjYyCfpszY4+Q== - -xterm@5.2.0-beta.28: - version "5.2.0-beta.28" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.28.tgz#852347e4eaf5aae7d82c90592a42adc9daab2a79" - integrity sha512-aLDxCuqjWHjvnhfWfkxy/y6coNrC+QIhbDe2sdfLPkrxhK6KnYE6qiZD5jXUIQXeq0KmSDcYi/esuKujvobC2A== +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@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== yallist@^4.0.0: version "4.0.0" diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index dd90c56776c13..56496a8deff31 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -112,8 +112,7 @@ export const enum TerminalSettingId { ShellIntegrationDecorationsEnabled = 'terminal.integrated.shellIntegration.decorationsEnabled', ShellIntegrationCommandHistory = 'terminal.integrated.shellIntegration.history', ShellIntegrationSuggestEnabled = 'terminal.integrated.shellIntegration.suggestEnabled', - SmoothScrolling = 'terminal.integrated.smoothScrolling', - AccessibleBufferContentEditable = 'terminal.integrated.accessibleBufferContentEditable' + SmoothScrolling = 'terminal.integrated.smoothScrolling' } export const enum TerminalLogConstants { diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 75dee393f7320..15d579d37c8c7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -1053,7 +1053,7 @@ export interface IXtermTerminal { /** * Focuses the accessible buffer, updating its contents */ - focusAccessibleBuffer(): void; + focusAccessibleBuffer(): Promise; } export interface IInternalXtermTerminal { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 847fb5887116e..31b34991131e9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -423,7 +423,7 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor): Promise { - accessor.get(ITerminalService).activeInstance?.xterm?.focusAccessibleBuffer(); + await accessor.get(ITerminalService).activeInstance?.xterm?.focusAccessibleBuffer(); } }); registerAction2(class extends Action2 { diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 04185c8406708..d83ddae19c63a 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -38,8 +38,16 @@ 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 { isLinux } from 'vs/base/common/platform'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +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'; const enum RenderConstants { /** @@ -281,7 +289,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II }); } - focusAccessibleBuffer(): void { + async focusAccessibleBuffer(): Promise { this._accessibileBuffer?.focus(); } @@ -312,7 +320,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II if (!this._container) { this.raw.open(container); } - this._accessibileBuffer = this._instantiationService.createInstance(AccessibleBuffer, this.raw, this.getFont(), this._capabilities); + this._accessibileBuffer = this._instantiationService.createInstance(AccessibleBuffer, this, this._capabilities); // TODO: Move before open to the DOM renderer doesn't initialize if (this._shouldLoadWebgl()) { this._enableWebglRenderer(); @@ -763,76 +771,126 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II this.raw.write(data); } } - +const enum ACCESSIBLE_BUFFER { Scheme = 'terminal-accessible-buffer' } class AccessibleBuffer extends DisposableStore { - - private _accessibleBuffer: HTMLElement | undefined; - private _bufferElementFragment: DocumentFragment | undefined; + private _accessibleBuffer: HTMLElement; + private _bufferEditor: CodeEditorWidget; + private _editorContainer: HTMLElement; + private _registered: boolean = false; + private _font: ITerminalFont; constructor( - private readonly _terminal: RawXtermTerminal, - private readonly _font: ITerminalFont, + private readonly _terminal: XtermTerminal, private readonly _capabilities: ITerminalCapabilityStore, - @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - @IConfigurationService private readonly _configurationService: IConfigurationService + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IModelService private readonly _modelService: IModelService, + @IConfigurationService configurationService: IConfigurationService ) { super(); - this.add(this._terminal.registerBufferElementProvider({ provideBufferElements: () => this.focus() })); + 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, + scrollbar: { alwaysConsumeMouseWheel: 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; + 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 { + await this._updateBufferEditor(); + // 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(); } - focus(): DocumentFragment { - if (!this._bufferElementFragment) { - this._bufferElementFragment = document.createDocumentFragment(); + private async _updateBufferEditor(): Promise { + if (!this._registered) { + // Registration is delayed until focus so the capability has time to have been added + this.add(this._terminal.raw.registerBufferElementProvider({ provideBufferElements: () => this._editorContainer })); + this._registered = true; } - this._accessibleBuffer = this._terminal.element?.querySelector('.xterm-accessible-buffer') as HTMLElement || undefined; - if (!this._accessibleBuffer) { - return this._bufferElementFragment; + // 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 }); + const commandDetection = this._capabilities.has(TerminalCapability.CommandDetection); + const fragment = commandDetection ? this._getShellIntegrationContent() : this._getAllContent(); + const model = await this._getTextModel(URI.from({ scheme: ACCESSIBLE_BUFFER.Scheme, fragment })); + if (model) { + this._bufferEditor.setModel(model); } - // see https://github.com/microsoft/vscode/issues/173532 - const accessibleBufferContentEditable = isLinux ? 'on' : this._configurationService.getValue(TerminalSettingId.AccessibleBufferContentEditable); - this._accessibleBuffer.contentEditable = accessibleBufferContentEditable === 'on' || (accessibleBufferContentEditable === 'auto' && !this._accessibilityService.isScreenReaderOptimized()) ? 'true' : 'false'; - // The viewport is undefined when this is focused, so we cannot get the cell height from that. Instead, estimate using the font. - const lineHeight = this._font?.charHeight ? this._font.charHeight * this._font.lineHeight + 'px' : ''; - this._accessibleBuffer.style.lineHeight = lineHeight; + } + + async _getTextModel(resource: URI): Promise { + 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); + let content = localize('terminal.integrated.noContent', "No terminal content available for this session. Run some commands to create content."); if (!commands?.length) { - const noContent = document.createElement('div'); - const noContentLabel = localize('terminal.integrated.noContent', "No terminal content available for this session."); - noContent.textContent = noContentLabel; - this._bufferElementFragment.replaceChildren(noContent); - this._accessibleBuffer.focus(); - return this._bufferElementFragment; - } - let header; - let replaceChildren = true; + return content; + } for (const command of commands) { - header = document.createElement('h2'); - // without this, the text area gets focused when keyboard shortcuts are used - header.tabIndex = -1; - header.textContent = command.command.replace(new RegExp(' ', 'g'), '\xA0'); + sb.appendString(command.command.replace(new RegExp(' ', 'g'), '\xA0')); if (command.exitCode !== 0) { - header.textContent += ` exited with code ${command.exitCode}`; + sb.appendString(` exited with code ${command.exitCode}`); } - const output = document.createElement('div'); - // without this, the text area gets focused when keyboard shortcuts are used - output.tabIndex = -1; - output.textContent = command.getOutput()?.replace(new RegExp(' ', 'g'), '\xA0') || ''; - if (replaceChildren) { - this._bufferElementFragment.replaceChildren(header, output); - replaceChildren = false; - } else { - this._bufferElementFragment.appendChild(header); - this._bufferElementFragment.appendChild(output); + sb.appendString('\n'); + sb.appendString(command.getOutput()?.replace(new RegExp(' ', 'g'), '\xA0') || ''); + } + content = sb.build(); + return content; + } + + 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 (!isWrapped || i === end - 1) { + lines.push(currentLine.replace(new RegExp(' ', 'g'), '\xA0')); + currentLine = ''; } } - this._accessibleBuffer.focus(); - if (this._accessibleBuffer.contentEditable === 'true') { - document.execCommand('selectAll', false, undefined); - document.getSelection()?.collapseToEnd(); - } else if (header) { - // focus the cursor line's header - header.tabIndex = 0; - } - return this._bufferElementFragment; + return lines.join('\n'); } } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index fef77887bdb3a..9a889cabc5425 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -253,7 +253,6 @@ export interface ITerminalConfiguration { }; useWslProfiles: boolean; altClickMovesCursor: boolean; - accessibleBufferContentEditable: 'auto' | 'on' | 'off'; macOptionIsMeta: boolean; macOptionClickForcesSelection: boolean; gpuAcceleration: 'auto' | 'on' | 'canvas' | 'off'; diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index a2dc090c6db2a..a2fa3faf0fc5a 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -579,17 +579,6 @@ const terminalConfiguration: IConfigurationNode = { markdownDescription: localize('terminal.integrated.smoothScrolling', "Controls whether the terminal will scroll using an animation."), type: 'boolean', default: false - }, - [TerminalSettingId.AccessibleBufferContentEditable]: { - markdownDescription: localize('terminal.integrated.accessibleBufferContentEditable', "Controls whether the accessible buffer is marks as a `contenteditable` element. This adds a text cursor to the buffer, allowing selection with the keyboard without a screen reader. Screen reader users will typically want to leave this as `auto` or `off` which will treat the buffer similar to a document. By default, on Linux, this will be set to `on` so that it works when using Orca."), - type: 'string', - enum: ['auto', 'on', 'off'], - enumDescriptions: [ - localize('accessibleBufferContentEditable.auto', "Automatically enable when a screen reader is not detected."), - localize('accessibleBufferContentEditable.on', "Always on."), - localize('accessibleBufferContentEditable.off', "Always off.") - ], - default: 'auto' } } }; diff --git a/yarn.lock b/yarn.lock index 5537568341596..2b3fa64c4ee0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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.28: - version "5.2.0-beta.28" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.2.0-beta.28.tgz#d2c149da51ef138f46268b755c4fdc4202eb771c" - integrity sha512-4XcjBhFwuyjpz2ubESwp75UceySOOKdJszKyyxOQ3/7L937uiVEBBLc8T231XU8lSwWUU7czyNjYyCfpszY4+Q== - -xterm@5.2.0-beta.28: - version "5.2.0-beta.28" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.28.tgz#852347e4eaf5aae7d82c90592a42adc9daab2a79" - integrity sha512-aLDxCuqjWHjvnhfWfkxy/y6coNrC+QIhbDe2sdfLPkrxhK6KnYE6qiZD5jXUIQXeq0KmSDcYi/esuKujvobC2A== +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@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== y18n@^3.2.1: version "3.2.2"