Compare commits

..
Author SHA1 Message Date
Megan Rogge 2a91c0643f Revert "pull AccessibleBuffer out of AccessibilityManager (#4400)"
This reverts commit 6daadc4c55.
2023-02-06 07:14:01 -06:00
Megan Rogge 6daadc4c55 pull AccessibleBuffer out of AccessibilityManager (#4400) 2023-02-03 18:59:30 +00:00
Daniel Imms c05d0f650c Merge pull request #4398 from Tyriar/tyriar/a11y_remove
Remove accessibility tree code
2023-02-02 11:47:17 -08:00
Daniel Imms d5b5f1a9a4 Clean up 2023-02-02 06:24:18 -08:00
Daniel Imms dfcd674f8c Fix const -> let in LocalizableStrings
This needs to be settable outside, so const gives the wrong impression
2023-02-02 06:18:09 -08:00
Daniel Imms a7e0a9c534 Remove old accessibility tree code 2023-02-02 06:17:32 -08:00
Daniel Imms 5839accbf1 Use is prefix for boolean prop/getter 2023-02-02 06:12:04 -08:00
Megan Rogge 01c9a98e8e update accessibilityBufferActive on focusOut (#4395) 2023-02-01 20:57:52 +00:00
Daniel Imms bcfed71e1d Merge pull request #4393 from Tyriar/createinstance
Improve createInstance types
2023-02-01 12:38:30 -08:00
Daniel Imms 046d4b8314 Improve createInstance types 2023-02-01 08:42:51 -08:00
Megan Rogge a9da61e605 accessibilityBuffer: textarea -> div & prevent mouse click from exiting (#4389) 2023-01-31 18:25:25 -06:00
Daniel Imms 24c7be210c Merge pull request #4390 from Tyriar/webgl_exception
Handle glyph/rect renderer safer
2023-01-31 11:25:41 -08:00
Daniel Imms 9c815250b6 Handle glyph/rect renderer safer
See microsoft/vscode#172782
2023-01-31 11:14:33 -08:00
Megan Rogge 377f88796f append accessibility tree as child, not as adjacent elt to terminal (#4383) 2023-01-24 19:40:46 +00:00
Megan Rogge ed6c849501 attach and detach accessibility tree root element (#4382) 2023-01-24 09:53:10 -06:00
Megan Rogge 5a21948b4a make accessibility buffer readonly (#4381) 2023-01-23 19:03:19 +00:00
Megan Rogge b24bda92ad use textbox instead of div for accessibility buffer (#4377) 2023-01-20 17:36:25 +00:00
Megan Rogge 9895691e0e use content editable (#4376) 2023-01-19 21:30:58 +00:00
Megan Rogge 77870b06b9 fix #4374 (#4375) 2023-01-19 16:49:53 +00:00
Daniel Imms 41dc93a008 Prototype improved a11y buffer view (#4340) 2023-01-18 23:00:21 +00:00
Daniel Imms ba9f2e0f11 Merge pull request #4364 from Tyriar/dom_remove
Replace dom helper with HTMLElement.remove()
2023-01-05 09:22:55 -08:00
Daniel Imms b3fb8db6b3 Replace dom helper with HTMLElement.remove() 2023-01-05 09:14:16 -08:00
Daniel Imms 91946dcd9b Merge pull request #4363 from xtermjs/dependabot/npm_and_yarn/json5-2.2.3
Bump json5 from 2.1.2 to 2.2.3
2023-01-05 08:23:59 -08:00
dependabot[bot] 41c0e26292 Bump json5 from 2.1.2 to 2.2.3
Bumps [json5](https://github.com/json5/json5) from 2.1.2 to 2.2.3.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.1.2...v2.2.3)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-05 16:15:34 +00:00
Daniel Imms fe89817667 Merge pull request #4361 from xtermjs/circularlist-cleanup
Removed unused imports in CircularList
2022-12-28 07:19:26 -08:00
Eugene 7ee7aa2719 removed unused imports in CircularList 2022-12-28 15:41:58 +01:00
Daniel Imms b05c26c6fd Merge pull request #4358 from Tyriar/4357
Request links after viewport resize
2022-12-27 06:50:53 -08:00
Daniel Imms 0adbedf98d Request links after viewport resize
This clears the underline when hovering the link but this is an edge
case that isn't so important. As it's difficult to do outside of the
demo.

Fixes #4357
2022-12-27 06:41:58 -08:00
Daniel Imms 89ead0b4b7 Merge pull request #4356 from Coding-Kiwi/Coding-Kiwi-patch-1
Add graSSHopper to Real-world uses
2022-12-27 03:56:47 -08:00
Erik 349510065b Add graSSHopper to Real-world uses 2022-12-26 20:48:44 +01:00
Daniel Imms fa8a0e7f24 Merge pull request #4355 from Tyriar/4354
Clear canvas selection state after a resize
2022-12-24 08:10:44 -08:00
Daniel Imms 0c4e9c6b27 Merge pull request #4347 from silamon/deprecatedubuntu
Switch to Ubuntu 20.04
2022-12-24 08:06:14 -08:00
Daniel Imms de2b62eeac Merge pull request #4352 from Tyriar/4096
Include right part of wide char in underline gap
2022-12-24 08:05:41 -08:00
Daniel Imms 65b2af10f8 Clear canvas selection state after a resize
Fixes #4354
2022-12-24 08:02:41 -08:00
Daniel Imms 1a20fbb25f Merge pull request #4353 from Tyriar/4057
Force canvas renderer selection redraw on theme change
2022-12-24 08:02:06 -08:00
Daniel Imms d51ba2578b Force canvas renderer selection redraw on theme change
Fixes #4057
2022-12-24 07:46:51 -08:00
Daniel Imms 56734874ac Include right part of wide char in underline gap
Fixes #4096
2022-12-24 07:30:26 -08:00
Daniel Imms 72f1d1048a Merge pull request #4350 from Tyriar/dotted
Improve rendering of dotted underlines
2022-12-23 14:16:15 -08:00
Daniel Imms d17df65fad Improve rendering of dotted underlines
Fixes #4060
2022-12-23 14:04:12 -08:00
Simon Lamon c2504fa669 Ubuntu 18.04 deprecation 2022-12-23 17:37:26 +01:00
Daniel Imms 5a2bf6d039 Merge pull request #4346 from Tyriar/4314_2
Clamp start and end in render service to ensure they're valid
2022-12-21 17:03:15 -08:00
Daniel Imms 568e4c6935 Clamp start and end in render service to ensure they're valid
Fixes #4314
2022-12-21 16:55:03 -08:00
Daniel Imms 8b1c5c4ffb Revert "Fix NPE in webgl renderer"
This reverts commit ea7571c2b9.
2022-12-21 16:51:45 -08:00
Daniel Imms d34bb5ed79 Merge pull request #4343 from Tyriar/bump_other_addons
Bump other addons for v5.1 release
2022-12-21 08:28:46 -08:00
Daniel Imms ace4c3663f Bump other addons for v5.1 release
Fixes #4336
2022-12-21 08:27:01 -08:00
Daniel Imms 75075ebb70 Merge pull request #4342 from Tyriar/4341
Fix link event listener leak
2022-12-20 15:15:22 -08:00
Daniel Imms 7b8ad8c0b4 Improve comment 2022-12-20 15:01:30 -08:00
Daniel Imms f28c21eb77 Fix link event listener leak
The bug here is that _clearCurrentLink was being called only on the
line(s) that changed, but _askForLink was then being triggered
regardless. This caused more onRenderedViewportChange listeners to be
registered and for the thread blocking to get exponentially worse.

Fixes #4341
2022-12-20 14:58:35 -08:00
Daniel Imms 3142460db5 Merge pull request #4335 from jerch/cleanup_buffer
Cleanup Buffer.ts
2022-12-19 17:01:26 -08:00
Daniel Imms b97b1b6093 Merge pull request #4337 from Tyriar/whitespace_bump
Make package.json changes to trigger release
2022-12-19 12:55:19 -08:00
Daniel Imms 303db85763 Make package.json changes to trigger release
Fixes #4336
2022-12-19 12:55:01 -08:00
Jörg Breitbart 49ec7199e9 remover iterator and stringIndexToBufferIndex from buffer 2022-12-19 19:50:18 +01:00
33 changed files with 225 additions and 653 deletions
+1
View File
@@ -214,6 +214,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**web3os**](https://web3os.sh): A decentralized operating system for the next web
- [**Cratecode**](https://cratecode.com): Learn to program for free through interactive online lessons. Cratecode uses xterm.js to give users access to their own Linux environment.
- [**Super Terminal**](https://github.com/bugwheels94/super-terminal): It is a http based terminal for developers who dont like repetition and save time.
- [**graSSHopper**](https://grasshopper.coding.kiwi): A simple SSH client with file explorer, history and many more features.
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-canvas",
"version": "0.2.0",
"version": "0.3.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -3,7 +3,6 @@
* @license MIT
*/
import { removeElementFromParent } from 'browser/Dom';
import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache';
import { TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
@@ -69,10 +68,12 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
this.register(this._themeService.onChangeColors(e => {
this._refreshCharAtlas(e);
this.reset();
// Trigger selection changed as it's handled separately to regular rendering
this.handleSelectionChanged(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
}));
this.register(toDisposable(() => {
removeElementFromParent(this._canvas);
this._canvas.remove();
this._charAtlas?.dispose();
}));
}
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
import { IColorSet, ITerminal } from 'browser/Types';
import { CanvasRenderer } from './CanvasRenderer';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
@@ -7,7 +7,7 @@ import { removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache'
import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver';
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, ISelectionService, IThemeService } from 'browser/services/Services';
import { ILinkifier2 } from 'browser/Types';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
import { Disposable, toDisposable } from 'common/Lifecycle';
@@ -5,7 +5,6 @@
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { Terminal } from 'xterm';
@@ -48,6 +47,7 @@ export class SelectionRenderLayer extends BaseRenderLayer {
// On resize use the base render layer's cached selection values since resize clears _state
// inside reset.
if (this._selectionModel.selectionStart && this._selectionModel.selectionEnd) {
this._clearState();
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-fit",
"version": "0.6.0",
"version": "0.7.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-search",
"version": "0.10.0",
"version": "0.11.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-unicode11",
"version": "0.4.0",
"version": "0.5.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -23,4 +23,5 @@
"peerDependencies": {
"xterm": "^5.0.0"
}
}
+1
View File
@@ -24,4 +24,5 @@
"peerDependencies": {
"xterm": "^5.0.0"
}
}
+19 -18
View File
@@ -40,8 +40,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _canvas: HTMLCanvasElement;
private _gl: IWebGL2RenderingContext;
private _rectangleRenderer: RectangleRenderer;
private _glyphRenderer: GlyphRenderer;
private _rectangleRenderer?: RectangleRenderer;
private _glyphRenderer?: GlyphRenderer;
public readonly dimensions: IRenderDimensions;
@@ -181,10 +181,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._core.screenElement!.style.width = `${this.dimensions.css.canvas.width}px`;
this._core.screenElement!.style.height = `${this.dimensions.css.canvas.height}px`;
this._rectangleRenderer.setDimensions(this.dimensions);
this._rectangleRenderer.handleResize();
this._glyphRenderer.setDimensions(this.dimensions);
this._glyphRenderer.handleResize();
this._rectangleRenderer?.setDimensions(this.dimensions);
this._rectangleRenderer?.handleResize();
this._glyphRenderer?.setDimensions(this.dimensions);
this._glyphRenderer?.handleResize();
this._refreshCharAtlas();
@@ -279,7 +279,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
this._charAtlas = atlas;
this._charAtlas.warmUp();
this._glyphRenderer.setAtlas(this._charAtlas);
this._glyphRenderer?.setAtlas(this._charAtlas);
}
/**
@@ -290,7 +290,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _clearModel(clearGlyphRenderer: boolean): void {
this._model.clear();
if (clearGlyphRenderer) {
this._glyphRenderer.clear();
this._glyphRenderer?.clear();
}
}
@@ -331,6 +331,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
l.handleGridChanged(this._terminal, start, end);
}
if (!this._glyphRenderer || !this._rectangleRenderer) {
return;
}
// Tell renderer the frame is beginning
if (this._glyphRenderer.beginFrame()) {
this._clearModel(true);
@@ -340,8 +344,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._updateModel(start, end);
// Render
this._rectangleRenderer.render();
this._glyphRenderer.render(this._model);
this._rectangleRenderer?.render();
this._glyphRenderer?.render(this._model);
}
private _updateModel(start: number, end: number): void {
@@ -352,7 +356,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
let lastBg: number;
let y: number;
let row: number;
let line: IBufferLine | undefined;
let line: IBufferLine;
let joinedRanges: [number, number][];
let isJoined: boolean;
let lastCharX: number;
@@ -365,10 +369,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
for (y = start; y <= end; y++) {
row = y + terminal.buffer.ydisp;
line = terminal.buffer.lines.get(row);
if (!line) {
break;
}
line = terminal.buffer.lines.get(row)!;
this._model.lineLengths[y] = 0;
joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
for (x = 0; x < terminal.cols; x++) {
@@ -432,7 +433,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
this._glyphRenderer.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg);
this._glyphRenderer!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg);
if (isJoined) {
// Restore work cell
@@ -441,7 +442,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Null out non-first cells
for (x++; x < lastCharX; x++) {
j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0);
this._glyphRenderer!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0);
this._model.cells[j] = NULL_CELL_CODE;
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
@@ -450,7 +451,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
}
}
this._rectangleRenderer.updateBackgrounds(this._model);
this._rectangleRenderer!.updateBackgrounds(this._model);
}
/**
+2 -2
View File
@@ -82,7 +82,7 @@ jobs:
- job: Linux_IntegrationTests
pool:
vmImage: 'ubuntu-18.04'
vmImage: 'ubuntu-20.04'
steps:
- script: |
# source: https://github.com/microsoft/playwright/issues/1041
@@ -151,7 +151,7 @@ jobs:
- Windows_IntegrationTests
condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['FORCE_RELEASE'], 'true')))
pool:
vmImage: 'ubuntu-18.04'
vmImage: 'ubuntu-20.04'
steps:
- task: NodeTool@0
inputs:
+20
View File
@@ -146,10 +146,30 @@
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 10;
color: transparent;
}
.xterm .xterm-accessibility-buffer {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
padding: .5em;
background: #000;
color: #fff;
opacity: 0;
overflow: scroll;
overflow-x: hidden;
}
.xterm .xterm-accessibility-buffer:focus {
opacity: 1;
z-index: 20;
}
.xterm .live-region {
position: absolute;
left: -9999px;
+75 -180
View File
@@ -4,35 +4,24 @@
*/
import * as Strings from 'browser/LocalizableStrings';
import { ITerminal, IRenderDebouncer } from 'browser/Types';
import { IBuffer } from 'common/buffer/Types';
import { ITerminal, IRenderDebouncer, ReadonlyColorSet } from 'browser/Types';
import { isMac } from 'common/Platform';
import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
import { IRenderService } from 'browser/services/Services';
import { removeElementFromParent } from 'browser/Dom';
import { IRenderService, IThemeService } from 'browser/services/Services';
import { IOptionsService } from 'common/services/Services';
import { ITerminalOptions } from 'xterm';
const MAX_ROWS_TO_READ = 20;
const enum BoundaryPosition {
TOP,
BOTTOM
}
export class AccessibilityManager extends Disposable {
private _accessibilityTreeRoot: HTMLElement;
private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[];
private _accessibilityContainer: HTMLElement;
private _accessiblityBuffer: HTMLElement;
private _liveRegion: HTMLElement;
private _liveRegionLineCount: number = 0;
private _renderRowsDebouncer: IRenderDebouncer;
private _screenDprMonitor: ScreenDprMonitor;
private _topBoundaryFocusListener: (e: FocusEvent) => void;
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
private _liveRegionDebouncer: IRenderDebouncer;
/**
* This queue has a character pushed to it for keys that are pressed, if the
@@ -47,47 +36,48 @@ export class AccessibilityManager extends Disposable {
private _charsToAnnounce: string = '';
private _isAccessibilityBufferActive: boolean = false;
public get isAccessibilityBufferActive(): boolean { return this._isAccessibilityBufferActive; }
constructor(
private readonly _terminal: ITerminal,
private readonly _renderService: IRenderService
@IOptionsService optionsService: IOptionsService,
@IRenderService private readonly _renderService: IRenderService,
@IThemeService themeService: IThemeService
) {
super();
this._accessibilityTreeRoot = document.createElement('div');
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
this._accessibilityTreeRoot.tabIndex = 0;
this._rowContainer = document.createElement('div');
this._rowContainer.setAttribute('role', 'list');
this._rowContainer.classList.add('xterm-accessibility-tree');
this._rowElements = [];
for (let i = 0; i < this._terminal.rows; i++) {
this._rowElements[i] = this._createAccessibilityTreeNode();
this._rowContainer.appendChild(this._rowElements[i]);
}
this._topBoundaryFocusListener = e => this._handleBoundaryFocus(e, BoundaryPosition.TOP);
this._bottomBoundaryFocusListener = e => this._handleBoundaryFocus(e, BoundaryPosition.BOTTOM);
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
this._refreshRowsDimensions();
this._accessibilityTreeRoot.appendChild(this._rowContainer);
this._renderRowsDebouncer = new TimeBasedDebouncer(this._renderRows.bind(this));
this._refreshRows();
this._accessibilityContainer = document.createElement('div');
this._accessibilityContainer.classList.add('xterm-accessibility');
this._liveRegion = document.createElement('div');
this._liveRegion.classList.add('live-region');
this._liveRegion.setAttribute('aria-live', 'assertive');
this._accessibilityTreeRoot.appendChild(this._liveRegion);
this._accessibilityContainer.appendChild(this._liveRegion);
this._liveRegionDebouncer = this.register(new TimeBasedDebouncer(this._announceCharacters.bind(this)));
if (!this._terminal.element) {
throw new Error('Cannot enable accessibility before Terminal.open');
}
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityContainer);
this.register(this._renderRowsDebouncer);
this.register(this._terminal.onResize(e => this._handleResize(e.rows)));
this._accessiblityBuffer = document.createElement('div');
this._accessiblityBuffer.setAttribute('role', 'document');
this._accessiblityBuffer.ariaRoleDescription = Strings.accessibilityBuffer;
this._accessiblityBuffer.tabIndex = 0;
this._accessibilityContainer.appendChild(this._accessiblityBuffer);
this._accessiblityBuffer.classList.add('xterm-accessibility-buffer');
this.register(addDisposableDomListener(this._accessiblityBuffer, 'keydown', (ev: KeyboardEvent) => {
if (ev.key === 'Tab') {
this._isAccessibilityBufferActive = false;
}}
));
this.register(addDisposableDomListener(this._accessiblityBuffer, 'focus',() => this._refreshAccessibilityBuffer()));
this.register(addDisposableDomListener(this._accessiblityBuffer, 'focusout',() => {
this._isAccessibilityBufferActive = false;
}));
this.register(this._liveRegionDebouncer);
this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end)));
this.register(this._terminal.onScroll(() => this._refreshRows()));
// Line feed is an issue as the prompt won't be read out after a command is run
@@ -96,108 +86,18 @@ export class AccessibilityManager extends Disposable {
this.register(this._terminal.onA11yTab(spaceCount => this._handleTab(spaceCount)));
this.register(this._terminal.onKey(e => this._handleKey(e.key)));
this.register(this._terminal.onBlur(() => this._clearLiveRegion()));
this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions()));
this._screenDprMonitor = new ScreenDprMonitor(window);
this.register(this._screenDprMonitor);
this._screenDprMonitor.setListener(() => this._refreshRowsDimensions());
// This shouldn't be needed on modern browsers but is present in case the
// media query that drives the ScreenDprMonitor isn't supported
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
this._handleColorChange(themeService.colors);
this.register(themeService.onChangeColors(e => this._handleColorChange(e)));
this._handleFontOptionChange(optionsService.options);
this.register(optionsService.onMultipleOptionChange(['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight'], () => this._handleFontOptionChange(optionsService.options)));
this.register(toDisposable(() => {
removeElementFromParent(this._accessibilityTreeRoot);
this._rowElements.length = 0;
this._accessiblityBuffer.remove();
this._accessibilityContainer.remove();
}));
}
private _handleBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
const boundaryElement = e.target as HTMLElement;
const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2];
// Don't scroll if the buffer top has reached the end in that direction
const posInSet = boundaryElement.getAttribute('aria-posinset');
const lastRowPos = position === BoundaryPosition.TOP ? '1' : `${this._terminal.buffer.lines.length}`;
if (posInSet === lastRowPos) {
return;
}
// Don't scroll when the last focused item was not the second row (focus is going the other
// direction)
if (e.relatedTarget !== beforeBoundaryElement) {
return;
}
// Remove old boundary element from array
let topBoundaryElement: HTMLElement;
let bottomBoundaryElement: HTMLElement;
if (position === BoundaryPosition.TOP) {
topBoundaryElement = boundaryElement;
bottomBoundaryElement = this._rowElements.pop()!;
this._rowContainer.removeChild(bottomBoundaryElement);
} else {
topBoundaryElement = this._rowElements.shift()!;
bottomBoundaryElement = boundaryElement;
this._rowContainer.removeChild(topBoundaryElement);
}
// Remove listeners from old boundary elements
topBoundaryElement.removeEventListener('focus', this._topBoundaryFocusListener);
bottomBoundaryElement.removeEventListener('focus', this._bottomBoundaryFocusListener);
// Add new element to array/DOM
if (position === BoundaryPosition.TOP) {
const newElement = this._createAccessibilityTreeNode();
this._rowElements.unshift(newElement);
this._rowContainer.insertAdjacentElement('afterbegin', newElement);
} else {
const newElement = this._createAccessibilityTreeNode();
this._rowElements.push(newElement);
this._rowContainer.appendChild(newElement);
}
// Add listeners to new boundary elements
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
// Scroll up
this._terminal.scrollLines(position === BoundaryPosition.TOP ? -1 : 1);
// Focus new boundary before element
this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2].focus();
// Prevent the standard behavior
e.preventDefault();
e.stopImmediatePropagation();
}
private _handleResize(rows: number): void {
// Remove bottom boundary listener
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
// Grow rows as required
for (let i = this._rowContainer.children.length; i < this._terminal.rows; i++) {
this._rowElements[i] = this._createAccessibilityTreeNode();
this._rowContainer.appendChild(this._rowElements[i]);
}
// Shrink rows as required
while (this._rowElements.length > rows) {
this._rowContainer.removeChild(this._rowElements.pop()!);
}
// Add bottom boundary listener
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
this._refreshRowsDimensions();
}
private _createAccessibilityTreeNode(): HTMLElement {
const element = document.createElement('div');
element.setAttribute('role', 'listitem');
element.tabIndex = -1;
this._refreshRowDimensions(element);
return element;
}
private _handleTab(spaceCount: number): void {
for (let i = 0; i < spaceCount; i++) {
this._handleChar(' ');
@@ -227,7 +127,7 @@ export class AccessibilityManager extends Disposable {
if (isMac) {
if (this._liveRegion.textContent && this._liveRegion.textContent.length > 0 && !this._liveRegion.parentNode) {
setTimeout(() => {
this._accessibilityTreeRoot.appendChild(this._liveRegion);
this._accessibilityContainer.appendChild(this._liveRegion);
}, 0);
}
}
@@ -240,7 +140,7 @@ export class AccessibilityManager extends Disposable {
// Only detach/attach on mac as otherwise messages can go unaccounced
if (isMac) {
removeElementFromParent(this._liveRegion);
this._liveRegion.remove();
}
}
@@ -253,40 +153,7 @@ export class AccessibilityManager extends Disposable {
}
private _refreshRows(start?: number, end?: number): void {
this._renderRowsDebouncer.refresh(start, end, this._terminal.rows);
}
private _renderRows(start: number, end: number): void {
const buffer: IBuffer = this._terminal.buffer;
const setSize = buffer.lines.length.toString();
for (let i = start; i <= end; i++) {
const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true);
const posInSet = (buffer.ydisp + i + 1).toString();
const element = this._rowElements[i];
if (element) {
if (lineData.length === 0) {
element.innerText = '\u00a0';
} else {
element.textContent = lineData;
}
element.setAttribute('aria-posinset', posInSet);
element.setAttribute('aria-setsize', setSize);
}
}
this._announceCharacters();
}
private _refreshRowsDimensions(): void {
if (!this._renderService.dimensions.css.cell.height) {
return;
}
this._accessibilityTreeRoot.style.width = `${this._renderService.dimensions.css.canvas.width}px`;
if (this._rowElements.length !== this._terminal.rows) {
this._handleResize(this._terminal.rows);
}
for (let i = 0; i < this._terminal.rows; i++) {
this._refreshRowDimensions(this._rowElements[i]);
}
this._liveRegionDebouncer.refresh(start, end, this._terminal.rows);
}
private _refreshRowDimensions(element: HTMLElement): void {
@@ -300,4 +167,32 @@ export class AccessibilityManager extends Disposable {
this._liveRegion.textContent += this._charsToAnnounce;
this._charsToAnnounce = '';
}
private _refreshAccessibilityBuffer(): void {
if (!this._terminal.viewport) {
return;
}
this._isAccessibilityBufferActive = true;
const { bufferElements } = this._terminal.viewport.getBufferElements(0);
for (const element of bufferElements) {
if (element.textContent) {
element.textContent = element.textContent.replace(new RegExp(' ', 'g'), '\xA0');
}
}
this._accessiblityBuffer.replaceChildren(...bufferElements);
this._accessiblityBuffer.scrollTop = this._accessiblityBuffer.scrollHeight;
}
private _handleColorChange(colorSet: ReadonlyColorSet): void {
this._accessiblityBuffer.style.backgroundColor = colorSet.background.css;
this._accessiblityBuffer.style.color = colorSet.foreground.css;
}
private _handleFontOptionChange(options: Required<ITerminalOptions>): void {
this._accessiblityBuffer.style.fontFamily = options.fontFamily;
this._accessiblityBuffer.style.fontSize = `${options.fontSize}px`;
this._accessiblityBuffer.style.lineHeight = `${options.lineHeight * this._renderService.dimensions.css.cell.height}px`;
this._accessiblityBuffer.style.letterSpacing = `${options.letterSpacing}px`;
}
}
-41
View File
@@ -1,41 +0,0 @@
/**
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
* @license MIT
*/
import jsdom = require('jsdom');
import { removeElementFromParent } from 'browser/Dom';
import { strictEqual, doesNotThrow } from 'assert';
describe('Dom', () => {
const dom = new jsdom.JSDOM();
const document = dom.window.document;
describe('removeElementFromParent', () => {
it('should remove single child', () => {
const e = document.createElement('div');
document.body.appendChild(e);
strictEqual(e.parentElement, document.body);
removeElementFromParent(e);
strictEqual(e.parentElement, null);
});
it('should remove multiple elements', () => {
const e1 = document.createElement('div');
const e2 = document.createElement('div');
document.body.appendChild(e1);
document.body.appendChild(e2);
strictEqual(e1.parentElement, document.body);
strictEqual(e2.parentElement, document.body);
removeElementFromParent(e1, e2);
strictEqual(e1.parentElement, null);
strictEqual(e2.parentElement, null);
});
it('should not throw on undefined', () => {
const e = document.createElement('div');
document.body.appendChild(e);
strictEqual(e.parentElement, document.body);
doesNotThrow(() => removeElementFromParent(undefined, e));
strictEqual(e.parentElement, null);
});
});
});
-10
View File
@@ -1,10 +0,0 @@
/**
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
* @license MIT
*/
export function removeElementFromParent(...elements: (HTMLElement | undefined)[]): void {
for (const e of elements) {
e?.parentElement?.removeChild(e);
}
}
+24 -9
View File
@@ -23,6 +23,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
private _linkCacheDisposables: IDisposable[] = [];
private _lastBufferCell: IBufferCellPosition | undefined;
private _isMouseOut: boolean = true;
private _wasResized: boolean = false;
private _activeProviderReplies: Map<Number, ILinkWithState[] | undefined> | undefined;
private _activeLine: number = -1;
@@ -39,6 +40,11 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
this.register(toDisposable(() => {
this._lastMouseEvent = undefined;
}));
// Listen to resize to catch the case where it's resized and the cursor is out of the viewport.
this.register(this._bufferService.onResize(() => {
this._clearCurrentLink();
this._wasResized = true;
}));
}
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
@@ -105,9 +111,10 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
private _handleHover(position: IBufferCellPosition): void {
// TODO: This currently does not cache link provider results across wrapped lines, activeLine should be something like `activeRange: {startY, endY}`
// Check if we need to clear the link
if (this._activeLine !== position.y) {
if (this._activeLine !== position.y || this._wasResized) {
this._clearCurrentLink();
this._askForLink(position, false);
this._wasResized = false;
return;
}
@@ -309,19 +316,27 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
});
// Add listener for rerendering
// Listen to viewport changes to re-render the link under the cursor (only when the line the
// link is on changes)
if (this._renderService) {
this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => {
// Sanity check, this shouldn't happen in practice as this listener would be disposed
if (!this._currentLink) {
return;
}
// When start is 0 a scroll most likely occurred, make sure links above the fold also get
// cleared.
const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp;
const oldEvent = this._currentLink ? this._lastMouseEvent : undefined;
this._clearCurrentLink(start, e.end + 1 + this._bufferService.buffer.ydisp);
if (oldEvent && this._element) {
// re-eval previously active link after changes
const position = this._positionFromMouseEvent(oldEvent, this._element, this._mouseService!);
if (position) {
this._askForLink(position, false);
const end = this._bufferService.buffer.ydisp + 1 + e.end;
// Only clear the link if the viewport change happened on this line
if (this._currentLink.link.range.start.y >= start && this._currentLink.link.range.end.y <= end) {
this._clearCurrentLink(start, end);
if (this._lastMouseEvent && this._element) {
// re-eval previously active link after changes
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!);
if (position) {
this._askForLink(position, false);
}
}
}
}));
+3
View File
@@ -10,3 +10,6 @@ export let promptLabel = 'Terminal input';
// eslint-disable-next-line prefer-const
export let tooMuchOutput = 'Too much output to announce, navigate to rows manually to read';
// eslint-disable-next-line prefer-const
export let accessibilityBuffer = 'Accessibility buffer';
-235
View File
@@ -1041,241 +1041,6 @@ describe('Terminal', () => {
});
});
describe('Buffer.stringIndexToBufferIndex', () => {
let terminal: TestTerminal;
beforeEach(() => {
terminal = new TestTerminal({ rows: 5, cols: 10, scrollback: 5 });
});
it('multiline ascii', async () => {
const input = 'This is ASCII text spanning multiple lines.';
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
}
});
it('combining e\u0301 in a sentence', async () => {
const input = 'Sitting in the cafe\u0301 drinking coffee.';
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < 19; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
}
// string index 18 & 19 point to combining char e\u0301 ---> same buffer Index
assert.deepEqual(
terminal.buffer.stringIndexToBufferIndex(0, 18),
terminal.buffer.stringIndexToBufferIndex(0, 19));
// after the combining char every string index has an offset of -1
for (let i = 19; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
}
});
it('multiline combining e\u0301', async () => {
const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301';
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// every buffer cell index contains 2 string indices
for (let i = 0; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
}
});
it('surrogate char in a sentence', async () => {
const input = 'The 𝄞 is a clef widely used in modern notation.';
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < 5; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
}
// string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index
assert.deepEqual(
terminal.buffer.stringIndexToBufferIndex(0, 4),
terminal.buffer.stringIndexToBufferIndex(0, 5));
// after the combining char every string index has an offset of -1
for (let i = 5; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
}
});
it('multiline surrogate char', async () => {
const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞';
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// every buffer cell index contains 2 string indices
for (let i = 0; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
}
});
it('surrogate char with combining', async () => {
// eye of Ra with acute accent - string length of 3
const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.';
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// index 0..2 should map to 0
assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1));
assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2));
for (let i = 2; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex);
}
});
it('multiline surrogate with combining', async () => {
const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301';
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// every buffer cell index contains 3 string indices
for (let i = 0; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex);
}
});
it('fullwidth chars', async () => {
const input = 'These 123 are some fat numbers.';
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < 6; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
}
// string index 6, 7, 8 take 2 cells
assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7));
assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8));
// rest of the string has offset of +3
for (let i = 9; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex);
}
});
it('multiline fullwidth chars', async () => {
const input = '12345678901234567890';
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 9; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex);
}
});
it('fullwidth combining with emoji - match emoji cell', async () => {
const input = 'Lots of ¥\u0301 make me 😃.';
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
const stringIndex = s.match(/😃/)!.index!;
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex);
assert(terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars(), '😃');
});
it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', async () => {
const input = 'a12345678901234567890';
// the 'a' at the beginning moves all fullwidth chars one to the right
// now the end of the line contains a dangling empty cell since
// the next fullwidth char has to wrap early
// the dangling last cell is wrongly added in the string
// --> fixable after resolving #1685
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 10; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true);
const j = (i - 0) << 1;
assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex);
}
});
it('test fully wrapped buffer up to last char', async () => {
const input = Array(6).join('1234567890');
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true);
assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars());
}
});
it('test fully wrapped buffer up to last char with full width odd', async () => {
const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'
+ 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301';
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true);
assert.equal(
(!(i % 3))
? input[i]
: (i % 3 === 1)
? input.slice(i, i + 2)
: input.slice(i - 1, i + 1),
terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars());
}
});
it('should handle \t in lines correctly', async () => {
const input = '\thttps://google.de';
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(s, Array(terminal.optionsService.options.tabStopWidth + 1).join(' ') + 'https://google.de');
});
});
describe('BufferStringIterator', function (): void {
it('iterator does not overflow buffer limits', async () => {
const terminal = new TestTerminal({ rows: 5, cols: 10, scrollback: 5 });
const data = [
'aaaaaaaaaa',
'aaaaaaaaa\n',
'aaaaaaaaaa',
'aaaaaaaaa\n',
'aaaaaaaaaa',
'aaaaaaaaaa',
'aaaaaaaaaa',
'aaaaaaaaa\n',
'aaaaaaaaaa',
'aaaaaaaaaa'
];
await terminal.writeP(data.join(''));
// brute force test with insane values
assert.doesNotThrow(() => {
for (let overscan = 0; overscan < 20; ++overscan) {
for (let start = -10; start < 20; ++start) {
for (let end = -10; end < 20; ++end) {
const it = terminal.buffer.iterator(false, start, end, overscan, overscan);
while (it.hasNext()) {
it.next();
}
}
}
}
});
});
});
describe('Windows Mode', () => {
it('should mark lines as wrapped when the line ends in a non-null character after a LF', async () => {
const data = [
+5 -3
View File
@@ -267,7 +267,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
private _handleScreenReaderModeOptionChange(value: boolean): void {
if (value) {
if (!this._accessibilityManager && this._renderService) {
this._accessibilityManager = new AccessibilityManager(this, this._renderService);
this._accessibilityManager = this._instantiationService.createInstance(AccessibilityManager, this);
}
} else {
this._accessibilityManager?.dispose();
@@ -419,7 +419,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.element.dir = 'ltr'; // xterm.css assumes LTR
this.element.classList.add('terminal');
this.element.classList.add('xterm');
this.element.setAttribute('tabindex', '0');
parent.appendChild(this.element);
// Performance: Use a document fragment to build the terminal
@@ -553,7 +552,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
if (this.options.screenReaderMode) {
// Note that this must be done *after* the renderer is created in order to
// ensure the correct order of the dprchange event
this._accessibilityManager = new AccessibilityManager(this, this._renderService);
this._accessibilityManager = this._instantiationService.createInstance(AccessibilityManager, this);
}
this.register(this.optionsService.onSpecificOptionChange('screenReaderMode', e => this._handleScreenReaderModeOptionChange(e)));
@@ -768,6 +767,9 @@ export class Terminal extends CoreTerminal implements ITerminal {
*/
this.register(addDisposableDomListener(el, 'mousedown', (ev: MouseEvent) => {
ev.preventDefault();
if (this._accessibilityManager?.isAccessibilityBufferActive) {
return;
}
this.focus();
// Don't send the mouse button to the pty if mouse events are disabled or
+7 -7
View File
@@ -8,7 +8,7 @@ import { IEvent, EventEmitter } from 'common/EventEmitter';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, ICompositionHelper, CharacterJoinerHandler, IBufferRange, ReadonlyColorSet } from 'browser/Types';
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions, ColorIndex } from 'common/Types';
import { Buffer } from 'common/buffer/Buffer';
import * as Browser from 'common/Platform';
@@ -140,6 +140,9 @@ export class MockTerminal implements ITerminal {
public write(data: string): void {
throw new Error('Method not implemented.');
}
public getBufferElements(startLine: number, endLine?: number | undefined): { bufferElements: HTMLElement[], cursorElement?: HTMLElement | undefined } {
throw new Error('Method not implemented.');
}
public bracketedPasteMode!: boolean;
public renderer!: IRenderer;
public linkifier2!: ILinkifier2;
@@ -240,12 +243,6 @@ export class MockBuffer implements IBuffer {
public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {
return Buffer.prototype.getBlankLine.apply(this, arguments as any);
}
public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[] {
return Buffer.prototype.stringIndexToBufferIndex.apply(this, arguments as any);
}
public iterator(trimRight: boolean, startIndex?: number, endIndex?: number): IBufferStringIterator {
return Buffer.prototype.iterator.apply(this, arguments as any);
}
public getNullCell(attr?: IAttributeData): ICellData {
throw new Error('Method not implemented.');
}
@@ -316,6 +313,9 @@ export class MockViewport implements IViewport {
public getLinesScrolled(ev: WheelEvent): number {
throw new Error('Method not implemented.');
}
public getBufferElements(startLine: number, endLine?: number | undefined): { bufferElements: HTMLElement[], cursorElement?: HTMLElement | undefined } {
throw new Error('Method not implemented.');
}
}
export class MockCompositionHelper implements ICompositionHelper {
+1
View File
@@ -144,6 +144,7 @@ export interface IViewport extends IDisposable {
scrollBarWidth: number;
syncScrollArea(immediate?: boolean): void;
getLinesScrolled(ev: WheelEvent): number;
getBufferElements(startLine: number, endLine?: number): { bufferElements: HTMLElement[], cursorElement?: HTMLElement };
handleWheel(ev: WheelEvent): boolean;
handleTouchStart(ev: TouchEvent): void;
handleTouchMove(ev: TouchEvent): boolean;
+27
View File
@@ -279,6 +279,33 @@ export class Viewport extends Disposable implements IViewport {
return amount;
}
public getBufferElements(startLine: number, endLine?: number): { bufferElements: HTMLElement[], cursorElement?: HTMLElement } {
let currentLine: string = '';
let cursorElement: HTMLElement | undefined;
const bufferElements: HTMLElement[] = [];
const end = endLine ?? this._bufferService.buffer.lines.length;
const lines = this._bufferService.buffer.lines;
for (let i = startLine; i < end; i++) {
const line = lines.get(i);
if (!line) {
continue;
}
const isWrapped = lines.get(i + 1)?.isWrapped;
currentLine += line.translateToString(!isWrapped);
if (!isWrapped || i === lines.length - 1) {
const div = document.createElement('div');
div.textContent = currentLine;
bufferElements.push(div);
if (currentLine.length > 0) {
cursorElement = div;
}
currentLine = '';
}
}
return { bufferElements, cursorElement };
}
/**
* Gets the number of pixels scrolled by the mouse event taking into account what type of delta
* is being used.
+4 -2
View File
@@ -3,7 +3,6 @@
* @license MIT
*/
import { removeElementFromParent } from 'browser/Dom';
import { BOLD_CLASS, CURSOR_BLINK_CLASS, CURSOR_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory, ITALIC_CLASS } from 'browser/renderer/dom/DomRendererRowFactory';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
@@ -87,7 +86,10 @@ export class DomRenderer extends Disposable implements IRenderer {
// Outside influences such as React unmounts may manipulate the DOM before our disposal.
// https://github.com/xtermjs/xterm.js/issues/2960
removeElementFromParent(this._rowContainer, this._selectionContainer, this._themeStyleElement, this._dimensionsStyleElement);
this._rowContainer.remove();
this._selectionContainer.remove();
this._themeStyleElement.remove();
this._dimensionsStyleElement.remove();
}));
}
+2 -2
View File
@@ -587,7 +587,7 @@ export class TextureAtlas implements ITextureAtlas {
);
break;
case UnderlineStyle.DOTTED:
this._tmpCtx.setLineDash([this._config.devicePixelRatio * 2, this._config.devicePixelRatio]);
this._tmpCtx.setLineDash([Math.round(lineWidth), Math.round(lineWidth)]);
this._tmpCtx.moveTo(xChLeft, yTop);
this._tmpCtx.lineTo(xChRight, yTop);
break;
@@ -627,7 +627,7 @@ export class TextureAtlas implements ITextureAtlas {
// outline around the whole glyph, as well as additional pixels in the glyph at the top
// which would increase GPU memory demands
const clipRegion = new Path2D();
clipRegion.rect(xLeft, yTop - Math.ceil(lineWidth / 2), this._config.deviceCellWidth, yBot - yTop + Math.ceil(lineWidth / 2));
clipRegion.rect(xLeft, yTop - Math.ceil(lineWidth / 2), this._config.deviceCellWidth * chWidth, yBot - yTop + Math.ceil(lineWidth / 2));
this._tmpCtx.clip(clipRegion);
this._tmpCtx.lineWidth = this._config.devicePixelRatio * 3;
this._tmpCtx.strokeStyle = backgroundColor.css;
+8
View File
@@ -152,6 +152,14 @@ export class RenderService extends Disposable implements IRenderService {
if (!this._renderer) {
return;
}
// Since this is debounced, a resize event could have happened between the time a refresh was
// requested and when this triggers. Clamp the values of start and end to ensure they're valid
// given the current viewport state.
start = Math.min(start, this._rowCount - 1);
end = Math.min(end, this._rowCount - 1);
// Render
this._renderer.renderRows(start, end);
// Update selection if needed
+1 -1
View File
@@ -4,7 +4,7 @@
*/
import { ICircularList } from 'common/Types';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { EventEmitter } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
export interface IInsertEvent {
+2 -100
View File
@@ -4,7 +4,7 @@
*/
import { CircularList, IInsertEvent } from 'common/CircularList';
import { IBuffer, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from 'common/buffer/Types';
import { IBuffer } from 'common/buffer/Types';
import { IBufferLine, ICellData, IAttributeData, ICharset } from 'common/Types';
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
@@ -14,7 +14,7 @@ import { Marker } from 'common/buffer/Marker';
import { IOptionsService, IBufferService } from 'common/services/Services';
import { DEFAULT_CHARSET } from 'common/data/Charsets';
import { ExtendedAttrs } from 'common/buffer/AttributeData';
import { DebouncedIdleTask, IdleTaskQueue } from 'common/TaskQueue';
import { IdleTaskQueue } from 'common/TaskQueue';
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
@@ -510,43 +510,6 @@ export class Buffer implements IBuffer {
}
}
// private _reflowSmallerGetLinesNeeded()
/**
* Translates a string index back to a BufferIndex.
* To get the correct buffer position the string must start at `startCol` 0
* (default in translateBufferLineToString).
* The method also works on wrapped line strings given rows were not trimmed.
* The method operates on the CharData string length, there are no
* additional content or boundary checks. Therefore the string and the buffer
* should not be altered in between.
* TODO: respect trim flag after fixing #1685
* @param lineIndex line index the string was retrieved from
* @param stringIndex index within the string
* @param trimRight Whether to trim whitespace to the right.
*/
public stringIndexToBufferIndex(lineIndex: number, stringIndex: number, trimRight: boolean = false): BufferIndex {
while (stringIndex) {
const line = this.lines.get(lineIndex);
if (!line) {
return [-1, -1];
}
const length = (trimRight) ? line.getTrimmedLength() : line.length;
for (let i = 0; i < length; ++i) {
if (line.get(i)[CHAR_DATA_WIDTH_INDEX]) {
// empty cells report a string length of 0, but get replaced
// with a whitespace in translateToString, thus replace with 1
stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length || 1;
}
if (stringIndex < 0) {
return [lineIndex, i];
}
}
lineIndex++;
}
return [lineIndex, 0];
}
/**
* Translates a buffer line to a string, with optional start and end columns.
* Wide characters will count as two columns in the resulting string. This
@@ -684,65 +647,4 @@ export class Buffer implements IBuffer {
this.markers.splice(this.markers.indexOf(marker), 1);
}
}
public iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator {
return new BufferStringIterator(this, trimRight, startIndex, endIndex, startOverscan, endOverscan);
}
}
/**
* Iterator to get unwrapped content strings from the buffer.
* The iterator returns at least the string data between the borders
* `startIndex` and `endIndex` (exclusive) and will expand the lines
* by `startOverscan` to the top and by `endOverscan` to the bottom,
* if no new line was found in between.
* It will never read/return string data beyond `startIndex - startOverscan`
* or `endIndex + endOverscan`. Therefore the first and last line might be truncated.
* It is possible to always get the full string for the first and last line as well
* by setting the overscan values to the actual buffer length. This not recommended
* since it might return the whole buffer within a single string in a worst case scenario.
*/
export class BufferStringIterator implements IBufferStringIterator {
private _current: number;
constructor(
private _buffer: IBuffer,
private _trimRight: boolean,
private _startIndex: number = 0,
private _endIndex: number = _buffer.lines.length,
private _startOverscan: number = 0,
private _endOverscan: number = 0
) {
if (this._startIndex < 0) {
this._startIndex = 0;
}
if (this._endIndex > this._buffer.lines.length) {
this._endIndex = this._buffer.lines.length;
}
this._current = this._startIndex;
}
public hasNext(): boolean {
return this._current < this._endIndex;
}
public next(): IBufferStringIteratorResult {
const range = this._buffer.getWrappedRangeForLine(this._current);
// limit search window to overscan value at both borders
if (range.first < this._startIndex - this._startOverscan) {
range.first = this._startIndex - this._startOverscan;
}
if (range.last > this._endIndex + this._endOverscan) {
range.last = this._endIndex + this._endOverscan;
}
// limit to current buffer length
range.first = Math.max(range.first, 0);
range.last = Math.min(range.last, this._buffer.lines.length);
let content = '';
for (let i = range.first; i <= range.last; ++i) {
content += this._buffer.translateBufferLineToString(i, this._trimRight);
}
this._current = range.last + 1;
return { range, content };
}
}
-12
View File
@@ -9,16 +9,6 @@ import { IEvent } from 'common/EventEmitter';
// BufferIndex denotes a position in the buffer: [rowIndex, colIndex]
export type BufferIndex = [number, number];
export interface IBufferStringIteratorResult {
range: { first: number, last: number };
content: string;
}
export interface IBufferStringIterator {
hasNext(): boolean;
next(): IBufferStringIteratorResult;
}
export interface IBuffer {
readonly lines: ICircularList<IBufferLine>;
ydisp: number;
@@ -40,8 +30,6 @@ export interface IBuffer {
nextStop(x?: number): number;
prevStop(x?: number): number;
getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine;
stringIndexToBufferIndex(lineIndex: number, stringIndex: number, trimRight?: boolean): number[];
iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator;
getNullCell(attr?: IAttributeData): ICellData;
getWhitespaceCell(attr?: IAttributeData): ICellData;
addMarker(y: number): IMarker;
+3 -11
View File
@@ -132,17 +132,9 @@ export interface IBrandedService {
serviceBrand: undefined;
}
type GetLeadingNonServiceArgs<Args> =
Args extends [...IBrandedService[]] ? []
: Args extends [infer A1, ...IBrandedService[]] ? [A1]
: Args extends [infer A1, infer A2, ...IBrandedService[]] ? [A1, A2]
: Args extends [infer A1, infer A2, infer A3, ...IBrandedService[]] ? [A1, A2, A3]
: Args extends [infer A1, infer A2, infer A3, infer A4, ...IBrandedService[]] ? [A1, A2, A3, A4]
: Args extends [infer A1, infer A2, infer A3, infer A4, infer A5, ...IBrandedService[]] ? [A1, A2, A3, A4, A5]
: Args extends [infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, ...IBrandedService[]] ? [A1, A2, A3, A4, A5, A6]
: Args extends [infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, infer A7, ...IBrandedService[]] ? [A1, A2, A3, A4, A5, A6, A7]
: Args extends [infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, infer A7, infer A8, ...IBrandedService[]] ? [A1, A2, A3, A4, A5, A6, A7, A8]
: never;
type GetLeadingNonServiceArgs<TArgs extends any[]> = TArgs extends [] ? []
: TArgs extends [...infer TFirst, infer TLast] ? TLast extends IBrandedService ? GetLeadingNonServiceArgs<TFirst> : TArgs
: never;
export const IInstantiationService = createDecorator<IInstantiationService>('InstantiationService');
export interface IInstantiationService {
+6 -1
View File
@@ -498,6 +498,11 @@ declare module 'xterm' {
* being printed to the terminal when `screenReaderMode` is enabled.
*/
tooMuchOutput: string;
/**
* The aria label for the accessibility buffer
*/
accessibilityBuffer: string;
}
/**
@@ -1179,7 +1184,7 @@ declare module 'xterm' {
* @param range The buffer range of the link.
*/
leave?(event: MouseEvent, text: string, range: IBufferRange): void;
/**
* Whether to receive non-HTTP URLs from LinkProvider. When false, any usage of non-HTTP URLs
* will be ignored. Enabling this option without proper protection in `activate` function
+3 -10
View File
@@ -2650,11 +2650,9 @@ json-stable-stringify-without-jsonify@^1.0.1:
integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
json5@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.2.tgz#43ef1f0af9835dd624751a6b7fa48874fb2d608e"
integrity sha512-MoUOQ4WdiN3yxhm7NEVJSJrieAo5hNSLQ5sj05OTRHPL9HOBy8u4Bu88jsC1jvqAdN+E1bJmsUcZH+1HQxliqQ==
dependencies:
minimist "^1.2.5"
version "2.2.3"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
kind-of@^6.0.2:
version "6.0.3"
@@ -2853,11 +2851,6 @@ minimatch@^3.0.4:
dependencies:
brace-expansion "^1.1.7"
minimist@^1.2.5:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mocha@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.1.0.tgz#dbf1114b7c3f9d0ca5de3133906aea3dfc89ef7a"