Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: xtermjs/xterm.js
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 4.18.0
Choose a base ref
...
head repository: xtermjs/xterm.js
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 4.19.0
Choose a head ref

Commits on Feb 28, 2022

  1. update version number

    meganrogge committed Feb 28, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    83cb7c3 View commit details
  2. Merge pull request #3666 from xtermjs/release

    v4.18.0
    meganrogge authored Feb 28, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    f4da8af View commit details

Commits on Mar 2, 2022

  1. Copy the full SHA
    1e89016 View commit details
  2. Fix triple select edge case

    silamon committed Mar 2, 2022
    Copy the full SHA
    78c2f06 View commit details
  3. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    c10a451 View commit details
  4. Copy the full SHA
    4799832 View commit details
  5. update addons

    meganrogge authored Mar 2, 2022
    Copy the full SHA
    ff6fa73 View commit details
  6. Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    f5b6677 View commit details
  7. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    798b805 View commit details
  8. break into two methods

    meganrogge authored Mar 2, 2022
    Copy the full SHA
    7bf12f9 View commit details
  9. Update src/common/buffer/Buffer.ts

    Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
    meganrogge and Tyriar authored Mar 2, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    85dd6a9 View commit details
  10. Update src/common/buffer/Buffer.ts

    Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
    meganrogge and Tyriar authored Mar 2, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    1af22b2 View commit details
  11. use clearAllMarkers

    meganrogge committed Mar 2, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    e75b47c View commit details

Commits on Mar 3, 2022

  1. add to mockbuffer

    meganrogge committed Mar 3, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    82706e7 View commit details
  2. Merge pull request #3671 from meganrogge/master

    during buffer clear, don't dispose of markers on first line
    meganrogge authored Mar 3, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    622a5bb View commit details

Commits on Mar 8, 2022

  1. Copy the full SHA
    2263a15 View commit details
  2. Merge pull request #3674 from coderaiser/feature/lint-using-putout

    Lint using 🐊Putout: part 6
    Tyriar authored Mar 8, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    19367a6 View commit details
  3. add scroll decorations

    meganrogge committed Mar 8, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    a5a9c3b View commit details
  4. get it to work

    meganrogge committed Mar 8, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    4350c7f View commit details

Commits on Mar 9, 2022

  1. clear on dispose

    meganrogge committed Mar 9, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    3a499fc View commit details
  2. clean up

    meganrogge committed Mar 9, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    785a819 View commit details
  3. more cleanup

    meganrogge committed Mar 9, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    d46a685 View commit details
  4. Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    89112fb View commit details
  5. delete unused code

    meganrogge committed Mar 9, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    249e73c View commit details
  6. Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    1948ec8 View commit details
  7. Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    cfd7912 View commit details
  8. call on render

    meganrogge committed Mar 9, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    e797070 View commit details
  9. Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    724bbeb View commit details
  10. Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    89a0cee View commit details
  11. use position sticky

    meganrogge committed Mar 9, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    167a6fd View commit details

Commits on Mar 10, 2022

  1. delete unused import

    meganrogge committed Mar 10, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    bdfc82d View commit details

Commits on Mar 11, 2022

  1. start work

    meganrogge committed Mar 11, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    60c11e8 View commit details
  2. get it to sort of work

    meganrogge committed Mar 11, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    9657442 View commit details
  3. Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    fa4479c View commit details
  4. Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    372b6b6 View commit details
  5. remove unused variable

    meganrogge committed Mar 11, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    da2c59d View commit details
  6. use a class

    meganrogge committed Mar 11, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    453cb00 View commit details
  7. Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    e7bbc41 View commit details
  8. Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    b4de278 View commit details
  9. Update css/xterm.css

    Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
    meganrogge and Tyriar authored Mar 11, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    2f85448 View commit details
  10. re-arrange dom structure

    meganrogge committed Mar 11, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    98a03dd View commit details
  11. Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    a14248d View commit details
  12. insert before

    meganrogge committed Mar 11, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    1250ab6 View commit details

Commits on Mar 12, 2022

  1. scroll -> overviewRuler

    meganrogge committed Mar 12, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    4022277 View commit details
  2. part 1 of massive refactor

    meganrogge committed Mar 12, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    692df15 View commit details
  3. check scrollback in fit addon

    silamon committed Mar 12, 2022
    Copy the full SHA
    8adf947 View commit details
  4. Verified

    This commit was signed with the committer’s verified signature.
    Eugeny Eugene
    Copy the full SHA
    f025c0c View commit details

Commits on Mar 14, 2022

  1. allow setting width

    meganrogge committed Mar 14, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    9e598d9 View commit details
  2. round to the nearest pixel

    meganrogge committed Mar 14, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    3b279e4 View commit details
  3. large refactor, broken

    meganrogge committed Mar 14, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    meganrogge Megan Rogge
    Copy the full SHA
    ced0662 View commit details
Showing with 3,310 additions and 1,438 deletions.
  1. +3 −0 README.md
  2. +4 −1 addons/xterm-addon-fit/src/FitAddon.ts
  3. +1 −1 addons/xterm-addon-ligatures/package.json
  4. +18 −18 addons/xterm-addon-ligatures/src/index.test.ts
  5. +4 −4 addons/xterm-addon-ligatures/src/index.ts
  6. +3 −3 addons/xterm-addon-ligatures/yarn.lock
  7. +1 −1 addons/xterm-addon-search/package.json
  8. +306 −20 addons/xterm-addon-search/src/SearchAddon.ts
  9. +11 −1 addons/xterm-addon-search/src/tsconfig.json
  10. +195 −7 addons/xterm-addon-search/test/SearchAddon.api.ts
  11. +63 −1 addons/xterm-addon-search/typings/xterm-addon-search.d.ts
  12. +7 −0 addons/xterm-addon-search/webpack.config.js
  13. +1 −1 addons/xterm-addon-serialize/package.json
  14. +98 −89 addons/xterm-addon-serialize/src/SerializeAddon.test.ts
  15. +15 −5 addons/xterm-addon-serialize/src/SerializeAddon.ts
  16. +2 −2 addons/xterm-addon-serialize/test/SerializeAddon.api.ts
  17. +1 −1 addons/xterm-addon-web-links/package.json
  18. +11 −3 addons/xterm-addon-web-links/src/WebLinkProvider.ts
  19. +2 −8 addons/xterm-addon-web-links/src/WebLinksAddon.ts
  20. +1 −2 addons/xterm-addon-web-links/test/tsconfig.json
  21. +5 −0 addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts
  22. +1 −1 addons/xterm-addon-webgl/package.json
  23. +7 −95 addons/xterm-addon-webgl/src/GlyphRenderer.ts
  24. +4 −80 addons/xterm-addon-webgl/src/RectangleRenderer.ts
  25. +3 −1 addons/xterm-addon-webgl/src/WebglAddon.ts
  26. +121 −29 addons/xterm-addon-webgl/src/WebglRenderer.ts
  27. +13 −11 addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts
  28. +19 −23 addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts
  29. +2 −2 addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts
  30. +7 −7 addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts
  31. +1 −0 addons/xterm-addon-webgl/src/tsconfig.json
  32. +162 −52 addons/xterm-addon-webgl/test/WebglRenderer.api.ts
  33. +3 −3 bin/publish.js
  34. +14 −4 css/xterm.css
  35. +107 −7 demo/client.ts
  36. +18 −5 demo/index.html
  37. +3 −0 demo/server.js
  38. +3 −3 package.json
  39. +2 −1 src/browser/ColorContrastCache.ts
  40. +1 −1 src/browser/ColorManager.test.ts
  41. +14 −4 src/browser/ColorManager.ts
  42. +12 −7 src/browser/Linkifier2.ts
  43. +22 −3 src/browser/RenderDebouncer.ts
  44. +3 −3 src/browser/Terminal.test.ts
  45. +57 −18 src/browser/Terminal.ts
  46. +63 −6 src/browser/TestUtils.test.ts
  47. +7 −6 src/browser/Types.d.ts
  48. +0 −18 src/browser/Viewport.ts
  49. +131 −0 src/browser/decorations/BufferDecorationRenderer.ts
  50. +88 −0 src/browser/decorations/ColorZoneStore.test.ts
  51. +117 −0 src/browser/decorations/ColorZoneStore.ts
  52. +228 −0 src/browser/decorations/OverviewRulerRenderer.ts
  53. +14 −6 src/browser/input/Mouse.test.ts
  54. +10 −4 src/browser/input/Mouse.ts
  55. +1 −0 src/browser/public/Terminal.ts
  56. +89 −20 src/browser/renderer/BaseRenderLayer.ts
  57. +4 −3 src/browser/renderer/CursorRenderLayer.ts
  58. +76 −7 src/browser/renderer/CustomGlyphs.ts
  59. +4 −3 src/browser/renderer/LinkRenderLayer.ts
  60. +4 −1 src/browser/renderer/Renderer.ts
  61. +15 −0 src/browser/renderer/RendererUtils.ts
  62. +6 −3 src/browser/renderer/SelectionRenderLayer.ts
  63. +17 −3 src/browser/renderer/TextRenderLayer.ts
  64. +1 −1 src/browser/renderer/atlas/CharAtlasUtils.ts
  65. +2 −2 src/browser/renderer/atlas/DynamicCharAtlas.ts
  66. +12 −9 src/browser/renderer/dom/DomRenderer.ts
  67. +29 −8 src/browser/renderer/dom/DomRendererRowFactory.test.ts
  68. +116 −31 src/browser/renderer/dom/DomRendererRowFactory.ts
  69. +6 −0 src/browser/selection/SelectionModel.test.ts
  70. +6 −1 src/browser/selection/SelectionModel.ts
  71. +0 −164 src/browser/services/DecorationService.ts
  72. +1 −0 src/browser/services/MouseService.ts
  73. +21 −7 src/browser/services/RenderService.ts
  74. +6 −0 src/browser/services/SelectionService.test.ts
  75. +23 −5 src/browser/services/SelectionService.ts
  76. +9 −11 src/browser/services/Services.ts
  77. +53 −1 src/{browser → common}/Color.test.ts
  78. +71 −22 src/{browser → common}/Color.ts
  79. +3 −0 src/common/CoreTerminal.ts
  80. +107 −0 src/common/SortedList.test.ts
  81. +88 −0 src/common/SortedList.ts
  82. +14 −1 src/common/TestUtils.test.ts
  83. +6 −0 src/common/Types.d.ts
  84. +22 −13 src/common/buffer/Buffer.ts
  85. +3 −2 src/common/buffer/Types.d.ts
  86. +3 −0 src/common/data/EscapeSequences.ts
  87. +6 −0 src/common/input/Keyboard.test.ts
  88. +24 −1 src/common/input/Keyboard.ts
  89. +5 −0 src/common/input/WriteBuffer.ts
  90. +1 −1 src/common/input/XParseColor.ts
  91. +139 −0 src/common/services/DecorationService.ts
  92. +2 −1 src/common/services/OptionsService.ts
  93. +25 −2 src/common/services/Services.ts
  94. +2 −1 src/tsconfig-library-base.json
  95. +27 −27 test/api/InputHandler.api.ts
  96. +1 −1 test/api/MouseTracking.api.ts
  97. +73 −51 test/api/Terminal.api.ts
  98. +80 −19 typings/xterm.d.ts
  99. +98 −448 yarn.lock
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -189,6 +189,9 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**KubeSail**](https://kubesail.com): The Self-Hosting Company - uses xterm to allow users to exec into kubernetes pods and build github apps
- [**WiTTY**](https://github.com/syssecfsu/witty): Web-based interactive terminal emulator that allows users to easily record, share, and replay console sessions.
- [**libv86 Terminal Forwarding**](https://github.com/hello-smile6/libv86-terminal-forwarding): Peer-to-peer SSH for the web, using WebRTC via [Bugout](https://github.com/chr15m/bugout) for data transfer and [v86](https://github.com/copy/v86) for web-based virtualization.
- [**hack.courses**](https://hack.courses): Interactive Linux and command-line classes using xterm.js to expose a real terminal available for everyone.
- [**Render**](https://render.com): Platform-as-a-service for your apps, websites, and databases using xterm.js to provide a command prompt for user containers and for streaming build and runtime logs.
- [**CloudTTY**](https://github.com/cloudtty/cloudtty): A Friendly Kubernetes CloudShell (Web Terminal).
- [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.
5 changes: 4 additions & 1 deletion addons/xterm-addon-fit/src/FitAddon.ts
Original file line number Diff line number Diff line change
@@ -63,6 +63,9 @@ export class FitAddon implements ITerminalAddon {
return undefined;
}

const scrollbarWidth = this._terminal.options.scrollback === 0 ?
0 : core.viewport.scrollBarWidth;

const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement);
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
@@ -76,7 +79,7 @@ export class FitAddon implements ITerminalAddon {
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
const elementPaddingHor = elementPadding.right + elementPadding.left;
const availableHeight = parentElementHeight - elementPaddingVer;
const availableWidth = parentElementWidth - elementPaddingHor - core.viewport.scrollBarWidth;
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
const geometry = {
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)),
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight))
2 changes: 1 addition & 1 deletion addons/xterm-addon-ligatures/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-ligatures",
"version": "0.5.2",
"version": "0.5.3",
"description": "Add support for programming ligatures to xterm.js",
"author": {
"name": "The xterm.js authors",
36 changes: 18 additions & 18 deletions addons/xterm-addon-ligatures/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -78,7 +78,7 @@ describe('xterm-addon-ligatures', () => {
});

it('handles quoted font names', done => {
term.setOption('fontFamily', '"Fira Code", monospace');
term.options.fontFamily = '"Fira Code", monospace';
assert.deepEqual(term.joiner!(input), []);
onRefresh.callsFake(() => {
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
@@ -87,7 +87,7 @@ describe('xterm-addon-ligatures', () => {
});

it('falls back to later fonts if earlier ones are not present', done => {
term.setOption('fontFamily', 'notinstalled, Fira Code, monospace');
term.options.fontFamily = 'notinstalled, Fira Code, monospace';
assert.deepEqual(term.joiner!(input), []);
onRefresh.callsFake(() => {
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
@@ -98,17 +98,17 @@ describe('xterm-addon-ligatures', () => {
it('uses the current font value', done => {
// The first three calls are all synchronous so that we don't allow time for
// any fonts to load while we're switching things around
term.setOption('fontFamily', 'Fira Code');
term.options.fontFamily = 'Fira Code';
assert.deepEqual(term.joiner!(input), []);
term.setOption('fontFamily', 'notinstalled');
term.options.fontFamily = 'notinstalled';
assert.deepEqual(term.joiner!(input), []);
term.setOption('fontFamily', 'Iosevka');
term.options.fontFamily = 'Iosevka';
assert.deepEqual(term.joiner!(input), []);
onRefresh.callsFake(() => {
assert.deepEqual(term.joiner!(input), [[2, 4]]);

// And switch it back to Fira Code for good measure
term.setOption('fontFamily', 'Fira Code');
term.options.fontFamily = 'Fira Code';

// At this point, we haven't loaded the new font, so the result reverts
// back to empty until that happens
@@ -124,7 +124,7 @@ describe('xterm-addon-ligatures', () => {
it('allows multiple terminal instances that use different fonts', done => {
const onRefresh2 = sinon.stub();
const term2 = new MockTerminal(onRefresh2);
term2.setOption('fontFamily', 'Iosevka');
term2.options.fontFamily = 'Iosevka';
ligatureSupport.enableLigatures(term2 as any);

assert.deepEqual(term.joiner!(input), []);
@@ -140,39 +140,39 @@ describe('xterm-addon-ligatures', () => {
});

it('fails if it finds but cannot load the font', async () => {
term.setOption('fontFamily', 'Nonexistant Font, monospace');
term.options.fontFamily = 'Nonexistant Font, monospace';
assert.deepEqual(term.joiner!(input), []);
await delay(500);
assert.isTrue(onRefresh.notCalled);
assert.throws(() => term.joiner!(input));
});

it('returns nothing if the font is not present on the system', async () => {
term.setOption('fontFamily', 'notinstalled');
term.options.fontFamily = 'notinstalled';
assert.deepEqual(term.joiner!(input), []);
await delay(500);
assert.isTrue(onRefresh.notCalled);
assert.deepEqual(term.joiner!(input), []);
});

it('returns nothing if no specific font is specified', async () => {
term.setOption('fontFamily', 'monospace');
term.options.fontFamily = 'monospace';
assert.deepEqual(term.joiner!(input), []);
await delay(500);
assert.isTrue(onRefresh.notCalled);
assert.deepEqual(term.joiner!(input), []);
});

it('returns nothing if no fonts are provided', async () => {
term.setOption('fontFamily', '');
term.options.fontFamily = '';
assert.deepEqual(term.joiner!(input), []);
await delay(500);
assert.isTrue(onRefresh.notCalled);
assert.deepEqual(term.joiner!(input), []);
});

it('fails when given malformed inputs', async () => {
term.setOption('fontFamily', {} as any);
term.options.fontFamily = {} as any;
assert.deepEqual(term.joiner!(input), []);
await delay(500);
assert.isTrue(onRefresh.notCalled);
@@ -181,7 +181,7 @@ describe('xterm-addon-ligatures', () => {

it('ensures no empty errors are thrown', async () => {
sinon.stub(fontLigatures, 'loadFile').callsFake(async () => { throw undefined; });
term.setOption('fontFamily', 'Iosevka');
term.options.fontFamily = 'Iosevka';
assert.deepEqual(term.joiner!(input), []);
await delay(500);
assert.isTrue(onRefresh.notCalled);
@@ -209,11 +209,11 @@ class MockTerminal {
public deregisterCharacterJoiner(id: number): void {
this.joiner = undefined;
}
public setOption(name: string, value: string | number): void {
this._options[name] = value;
}
public getOption(name: string): string | number {
return this._options[name];
public get options(): { [name: string]: string | number } { return this._options; }
public set options(options: { [name: string]: string | number }) {
for (const key in this._options) {
this._options[key] = options[key];
}
}
}

8 changes: 4 additions & 4 deletions addons/xterm-addon-ligatures/src/index.ts
Original file line number Diff line number Diff line change
@@ -34,7 +34,7 @@ export function enableLigatures(term: Terminal): void {

term.registerCharacterJoiner((text: string): [number, number][] => {
// If the font hasn't been loaded yet, load it and return an empty result
const termFont = term.getOption('fontFamily');
const termFont = term.options.fontFamily;
if (
termFont &&
(loadingState === LoadingState.UNLOADED || currentFontName !== termFont)
@@ -48,20 +48,20 @@ export function enableLigatures(term: Terminal): void {
.then(f => {
// Another request may have come in while we were waiting, so make
// sure our font is still vaild.
if (currentCallFontName === term.getOption('fontFamily')) {
if (currentCallFontName === term.options.fontFamily) {
loadingState = LoadingState.LOADED;
font = f;

// Only refresh things if we actually found a font
if (f) {
term.refresh(0, term.getOption('rows') - 1);
term.refresh(0, term.options.rows! - 1);
}
}
})
.catch(e => {
// Another request may have come in while we were waiting, so make
// sure our font is still vaild.
if (currentCallFontName === term.getOption('fontFamily')) {
if (currentCallFontName === term.options.fontFamily) {
loadingState = LoadingState.FAILED;
font = undefined;
loadError = e;
6 changes: 3 additions & 3 deletions addons/xterm-addon-ligatures/yarn.lock
Original file line number Diff line number Diff line change
@@ -141,9 +141,9 @@ lru-cache@^6.0.0:
yallist "^4.0.0"

minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==

mkdirp@0.5.5:
version "0.5.5"
2 changes: 1 addition & 1 deletion addons/xterm-addon-search/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-search",
"version": "0.8.2",
"version": "0.9.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
326 changes: 306 additions & 20 deletions addons/xterm-addon-search/src/SearchAddon.ts

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion addons/xterm-addon-search/src/tsconfig.json
Original file line number Diff line number Diff line change
@@ -13,10 +13,20 @@
"strict": true,
"types": [
"../../../node_modules/@types/mocha"
]
],
"paths": {
"common/*": [
"../../../src/common/*"
]
}
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../../src/common"
}
]
}
202 changes: 195 additions & 7 deletions addons/xterm-addon-search/test/SearchAddon.api.ts
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@
import { assert } from 'chai';
import { readFile } from 'fs';
import { resolve } from 'path';
import { openTerminal, writeSync, launchBrowser } from '../../../out-test/api/TestUtils';
import { openTerminal, writeSync, launchBrowser, timeout } from '../../../out-test/api/TestUtils';
import { Browser, Page } from 'playwright';

const APP = 'http://127.0.0.1:3001/test';
@@ -16,23 +16,26 @@ let page: Page;
const width = 800;
const height = 600;

describe('Search Tests', function(): void {
before(async function(): Promise<any> {
describe('Search Tests', function (): void {
before(async function (): Promise<any> {
browser = await launchBrowser();
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
await page.goto(APP);
await openTerminal(page);
await page.evaluate(`window.search = new SearchAddon();`);
await page.evaluate(`window.term.loadAddon(window.search);`);
});

after(() => {
browser.close();
});

beforeEach(async () => {
await page.evaluate(`window.term.reset()`);
await page.evaluate(`
window.term.reset()
window.search?.dispose();
window.search = new SearchAddon();
window.term.loadAddon(window.search);
`);
});

it('Simple Search', async () => {
@@ -120,6 +123,176 @@ describe('Search Tests', function(): void {
});
});

describe('onDidChangeResults', async () => {
describe('findNext', () => {
it('should not fire unless the decorations option is set', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc');
assert.strictEqual(await page.evaluate(`window.search.findNext('a')`), true);
assert.strictEqual(await page.evaluate('window.calls.length'), 0);
assert.strictEqual(await page.evaluate(`window.search.findNext('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.strictEqual(await page.evaluate('window.calls.length'), 1);
});
it('should fire with correct event values', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc bc c');
assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
assert.strictEqual(await page.evaluate(`window.search.findNext('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 }
]);
assert.strictEqual(await page.evaluate(`window.search.findNext('d', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 0, resultIndex: -1 }
]);
assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 0, resultIndex: -1 },
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 3, resultIndex: 1 },
{ resultCount: 3, resultIndex: 2 }
]);
});
it('should fire with correct event values (incremental)', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc aabc');
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('ab', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 0, resultIndex: -1 }
]);
});
});
describe('findPrevious', () => {
it('should not fire unless the decorations option is set', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc');
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a')`), true);
assert.strictEqual(await page.evaluate('window.calls.length'), 0);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.strictEqual(await page.evaluate('window.calls.length'), 1);
});
it('should fire with correct event values', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc bc c');
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 }
]);
await timeout(2000);
assert.strictEqual(await page.evaluate(`debugger; window.search.findPrevious('d', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 0, resultIndex: -1 }
]);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 0, resultIndex: -1 },
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 3, resultIndex: 1 },
{ resultCount: 3, resultIndex: 0 }
]);
});
it('should fire with correct event values (incremental)', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc aabc');
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('ab', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 0 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 0, resultIndex: -1 }
]);
});
});
});

describe('Regression tests', () => {
describe('#2444 wrapped line content not being found', () => {
let fixture: string;
@@ -134,7 +307,7 @@ describe('Search Tests', function(): void {
.replace(/\n/g, '\\n\\r');
}
fixture = fixture
.replace(/'/g, '\\\'');
.replace(/'/g, `\\'`);
});
it('should find all occurrences using findNext', async () => {
await writeSync(page, fixture);
@@ -206,6 +379,21 @@ describe('Search Tests', function(): void {
});
});
});
describe('#3834 lines with null characters before search terms', () => {
// This case can be triggered by the prompt when using starship under conpty
it('should find all matches on a line containing null characters', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
// Move cursor forward 1 time to create a null character, as opposed to regular whitespace
await writeSync(page, '\\x1b[CHi Hi');
assert.strictEqual(await page.evaluate(`window.search.findPrevious('h', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 1 }
]);
});
});
});

function makeData(length: number): string {
64 changes: 63 additions & 1 deletion addons/xterm-addon-search/typings/xterm-addon-search.d.ts
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
* @license MIT
*/

import { Terminal, ILinkMatcherOptions, IDisposable, ITerminalAddon } from 'xterm';
import { Terminal, ITerminalAddon, IEvent } from 'xterm';

declare module 'xterm-addon-search' {
/**
@@ -32,6 +32,47 @@ declare module 'xterm-addon-search' {
* `findNext`, not `findPrevious`.
*/
incremental?: boolean;

/**
* When set, will highlight all instances of the word on search and show
* them in the overview ruler if it's enabled.
*/
decorations?: ISearchDecorationOptions;
}

/**
* Options for showing decorations when searching.
*/
interface ISearchDecorationOptions {
/**
* The background color of a match, this must use #RRGGBB format.
*/
matchBackground?: string;

/**
* The border color of a match.
*/
matchBorder?: string;

/**
* The overview ruler color of a match.
*/
matchOverviewRuler: string;

/**
* The background color for the currently active match, this must use #RRGGBB format.
*/
activeMatchBackground?: string;

/**
* The border color of the currently active match.
*/
activeMatchBorder?: string;

/**
* The overview ruler color of the currently active match.
*/
activeMatchColorOverviewRuler: string;
}

/**
@@ -64,5 +105,26 @@ declare module 'xterm-addon-search' {
* @param searchOptions The options for the search.
*/
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean;

/**
* Clears the decorations and selection
*/
public clearDecorations(): void;

/**
* Clears the active result decoration, this decoration is applied on top of the selection so
* removing it will reveal the selection underneath. This is intended to be called on the search
* textarea's `blur` event.
*/
public clearActiveDecoration(): void;

/**
* When decorations are enabled, fires when
* the search results change.
* @returns -1 for resultIndex for a resultCount of 0
* and @returns undefined when the threshold of 1k results
* is exceeded and decorations are disposed of.
*/
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number } | undefined>;
}
}
7 changes: 7 additions & 0 deletions addons/xterm-addon-search/webpack.config.js
Original file line number Diff line number Diff line change
@@ -21,6 +21,13 @@ module.exports = {
}
]
},
resolve: {
modules: ['./node_modules'],
extensions: [ '.js' ],
alias: {
common: path.resolve('../../out/common')
}
},
output: {
filename: mainFile,
path: path.resolve('./lib'),
2 changes: 1 addition & 1 deletion addons/xterm-addon-serialize/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-serialize",
"version": "0.6.1",
"version": "0.7.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
187 changes: 98 additions & 89 deletions addons/xterm-addon-serialize/src/SerializeAddon.test.ts
Original file line number Diff line number Diff line change
@@ -43,7 +43,7 @@ class TestSelectionService {
}
}

describe('xterm-addon-serialize html', () => {
describe('xterm-addon-serialize', () => {
let cm: ColorManager;
let dom: jsdom.JSDOM;
let document: Document;
@@ -83,123 +83,132 @@ describe('xterm-addon-serialize html', () => {
(terminal as any)._core._selectionService = selectionService;
});

it('empty terminal with selection turned off', () => {
const output = serializeAddon.serializeAsHTML();
assert.notEqual(output, '');
assert.equal((output.match(new RegExp('<div><span> {10}</span><\/div>', 'g')) || []).length, 2);
describe('text', () => {
it('restoring cursor styles', async () => {
await writeP(terminal, sgr('32') + '> ' + sgr('0'));
assert.equal(serializeAddon.serialize(), '\u001b[32m> \u001b[0m');
});
});

it('empty terminal with no selection', () => {
const output = serializeAddon.serializeAsHTML({
onlySelection: true
describe('html', () => {
it('empty terminal with selection turned off', () => {
const output = serializeAddon.serializeAsHTML();
assert.notEqual(output, '');
assert.equal((output.match(/<div><span> {10}<\/span><\/div>/g) || []).length, 2);
});

it('empty terminal with no selection', () => {
const output = serializeAddon.serializeAsHTML({
onlySelection: true
});
assert.equal(output, '');
});
assert.equal(output, '');
});

it('basic terminal with selection', async () => {
await writeP(terminal, ' terminal ');
terminal.select(1, 0, 8);
it('basic terminal with selection', async () => {
await writeP(terminal, ' terminal ');
terminal.select(1, 0, 8);

const output = serializeAddon.serializeAsHTML({
onlySelection: true
const output = serializeAddon.serializeAsHTML({
onlySelection: true
});
assert.equal((output.match(/<div><span>terminal<\/span><\/div>/g) || []).length, 1, output);
});
assert.equal((output.match(new RegExp('<div><span>terminal<\/span><\/div>', 'g')) || []).length, 1, output);
});

it('cells with bold styling', async () => {
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');
it('cells with bold styling', async () => {
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');

const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'font-weight: bold;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='font-weight: bold;'>terminal<\/span>/g) || []).length, 1, output);
});

it('cells with italic styling', async () => {
await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' ');
it('cells with italic styling', async () => {
await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' ');

const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'font-style: italic;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='font-style: italic;'>terminal<\/span>/g) || []).length, 1, output);
});

it('cells with inverse styling', async () => {
await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' ');
it('cells with inverse styling', async () => {
await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' ');

const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'color: #000000; background-color: #BFBFBF;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='color: #000000; background-color: #BFBFBF;'>terminal<\/span>/g) || []).length, 1, output);
});

it('cells with underline styling', async () => {
await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' ');
it('cells with underline styling', async () => {
await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' ');

const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'text-decoration: underline;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='text-decoration: underline;'>terminal<\/span>/g) || []).length, 1, output);
});

it('cells with invisible styling', async () => {
await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' ');
it('cells with invisible styling', async () => {
await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' ');

const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'visibility: hidden;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='visibility: hidden;'>terminal<\/span>/g) || []).length, 1, output);
});

it('cells with dim styling', async () => {
await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' ');
it('cells with dim styling', async () => {
await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' ');

const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'opacity: 0.5;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='opacity: 0.5;'>terminal<\/span>/g) || []).length, 1, output);
});

it('cells with strikethrough styling', async () => {
await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' ');
it('cells with strikethrough styling', async () => {
await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' ');

const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'text-decoration: line-through;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='text-decoration: line-through;'>terminal<\/span>/g) || []).length, 1, output);
});

it('cells with combined styling', async () => {
await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' ');
it('cells with combined styling', async () => {
await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' ');

const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'font-weight: bold;\'> <\/span>', 'g')) || []).length, 1, output);
assert.equal((output.match(new RegExp('<span style=\'font-weight: bold; text-decoration: line-through;\'>termi<\/span>', 'g')) || []).length, 1, output);
assert.equal((output.match(new RegExp('<span style=\'text-decoration: line-through;\'>nal<\/span>', 'g')) || []).length, 1, output);
});
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='font-weight: bold;'> <\/span>/g) || []).length, 1, output);
assert.equal((output.match(/<span style='font-weight: bold; text-decoration: line-through;'>termi<\/span>/g) || []).length, 1, output);
assert.equal((output.match(/<span style='text-decoration: line-through;'>nal<\/span>/g) || []).length, 1, output);
});

it('cells with color styling', async () => {
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
it('cells with color styling', async () => {
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');

const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'color: #00ff00;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
});

it('cells with background styling', async () => {
await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' ');
it('cells with background styling', async () => {
await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' ');

const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'background-color: #00ff00;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='background-color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
});

it('empty terminal with default options', async () => {
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('color: #000000; background-color: #ffffff; font-family: courier-new, courier, monospace; font-size: 15px;', 'g')) || []).length, 1, output);
});
it('empty terminal with default options', async () => {
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output);
});

it('empty terminal with custom options', async () => {
terminal.options.fontFamily = 'verdana';
terminal.options.fontSize = 20;
terminal.options.theme = {
foreground: '#ff00ff',
background: '#00ff00'
};
const output = serializeAddon.serializeAsHTML({
includeGlobalBackground: true
});
assert.equal((output.match(new RegExp('color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;', 'g')) || []).length, 1, output);
});
it('empty terminal with custom options', async () => {
terminal.options.fontFamily = 'verdana';
terminal.options.fontSize = 20;
terminal.options.theme = {
foreground: '#ff00ff',
background: '#00ff00'
};
const output = serializeAddon.serializeAsHTML({
includeGlobalBackground: true
});
assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) || []).length, 1, output);
});

it('empty terminal with background included', async () => {
const output = serializeAddon.serializeAsHTML({
includeGlobalBackground: true
it('empty terminal with background included', async () => {
const output = serializeAddon.serializeAsHTML({
includeGlobalBackground: true
});
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output);
});
assert.equal((output.match(new RegExp('color: #ffffff; background-color: #000000; font-family: courier-new, courier, monospace; font-size: 15px;', 'g')) || []).length, 1, output);
});
});
20 changes: 15 additions & 5 deletions addons/xterm-addon-serialize/src/SerializeAddon.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@

import { Terminal, ITerminalAddon, IBuffer, IBufferCell, IBufferRange } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IAttributeData } from 'common/Types';

function constrain(value: number, low: number, high: number): number {
return Math.max(low, Math.min(value, high));
@@ -62,17 +63,17 @@ abstract class BaseSerializeHandler {
protected _serializeString(): string { return ''; }
}

function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean {
function equalFg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
return cell1.getFgColorMode() === cell2.getFgColorMode()
&& cell1.getFgColor() === cell2.getFgColor();
}

function equalBg(cell1: IBufferCell, cell2: IBufferCell): boolean {
function equalBg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
return cell1.getBgColorMode() === cell2.getBgColorMode()
&& cell1.getBgColor() === cell2.getBgColor();
}

function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
return cell1.isInverse() === cell2.isInverse()
&& cell1.isBold() === cell2.isBold()
&& cell1.isUnderline() === cell2.isUnderline()
@@ -229,7 +230,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
this._nullCellCount = 0;
}

private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): number[] {
private _diffStyle(cell: IBufferCell | IAttributeData, oldCell: IBufferCell): number[] {
const sgrSeq: number[] = [];
const fgChanged = !equalFg(cell, oldCell);
const bgChanged = !equalBg(cell, oldCell);
@@ -393,6 +394,15 @@ class StringSerializeHandler extends BaseSerializeHandler {
moveRight(realCursorCol - this._lastCursorCol);
}

// Restore the cursor's current style, see https://github.com/xtermjs/xterm.js/issues/3677
// HACK: Internal API access since it's awkward to expose this in the API and serialize will
// likely be the only consumer
const curAttrData: IAttributeData = (this._terminal as any)._core._inputHandler._curAttrData;
const sgrSeq = this._diffStyle(curAttrData, this._cursorStyle);
if (sgrSeq.length > 0) {
content += `\u001b[${sgrSeq.join(';')}m`;
}

return content;
}
}
@@ -544,7 +554,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
return target;
}

targetLength = targetLength - target.length;
targetLength -= target.length;
if (targetLength > padString.length) {
padString += padString.repeat(targetLength / padString.length);
}
4 changes: 2 additions & 2 deletions addons/xterm-addon-serialize/test/SerializeAddon.api.ts
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ let page: Page;
const width = 800;
const height = 600;

const writeRawSync = (page: any, str: string): Promise<void> => writeSync(page, '\' +' + JSON.stringify(str) + '+ \'');
const writeRawSync = (page: any, str: string): Promise<void> => writeSync(page, `' +` + JSON.stringify(str) + `+ '`);

const testNormalScreenEqual = async (page: any, str: string): Promise<void> => {
await writeRawSync(page, str);
@@ -83,7 +83,7 @@ describe('SerializeAddon', () => {
const buffer3 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);

await page.evaluate(`term.reset();`);
await writeRawSync(page, '1234567890n12345');
await writeRawSync(page, '123456789012345');
const buffer4 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);

assert.throw(() => {
2 changes: 1 addition & 1 deletion addons/xterm-addon-web-links/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-web-links",
"version": "0.5.1",
"version": "0.6.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
14 changes: 11 additions & 3 deletions addons/xterm-addon-web-links/src/WebLinkProvider.ts
Original file line number Diff line number Diff line change
@@ -5,9 +5,10 @@

import { ILinkProvider, ILink, Terminal, IViewportRange } from 'xterm';

interface ILinkProviderOptions {
export interface ILinkProviderOptions {
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
leave?(event: MouseEvent, text: string): void;
urlRegex?: RegExp;
}

export class WebLinkProvider implements ILinkProvider {
@@ -78,10 +79,17 @@ export class LinkComputer {
endY++;
}

let startX = stringIndex + 1;
let startY = startLineIndex + 1;
while (startX > terminal.cols) {
startX -= terminal.cols;
startY++;
}

const range = {
start: {
x: stringIndex + 1,
y: startLineIndex + 1
x: startX,
y: startY
},
end: {
x: endX,
10 changes: 2 additions & 8 deletions addons/xterm-addon-web-links/src/WebLinksAddon.ts
Original file line number Diff line number Diff line change
@@ -3,8 +3,8 @@
* @license MIT
*/

import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable, IViewportRange } from 'xterm';
import { WebLinkProvider } from './WebLinkProvider';
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable } from 'xterm';
import { ILinkProviderOptions, WebLinkProvider } from './WebLinkProvider';

const protocolClause = '(https?:\\/\\/)';
const domainCharacterSet = '[\\da-z\\.-]+';
@@ -40,12 +40,6 @@ function handleLink(event: MouseEvent, uri: string): void {
}
}

interface ILinkProviderOptions {
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
leave?(event: MouseEvent, text: string): void;
urlRegex?: RegExp;
}

export class WebLinksAddon implements ITerminalAddon {
private _linkMatcherId: number | undefined;
private _terminal: Terminal | undefined;
3 changes: 1 addition & 2 deletions addons/xterm-addon-web-links/test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -12,8 +12,7 @@
"strict": true,
"types": [
"../../../node_modules/@types/mocha",
"../../../node_modules/@types/node",
"../../../out-test/api/TestUtils"
"../../../node_modules/@types/node"
]
},
"include": [
Original file line number Diff line number Diff line change
@@ -49,5 +49,10 @@ declare module 'xterm-addon-web-links' {
* happen even when tooltipCallback hasn't fired for the link yet.
*/
leave?(event: MouseEvent, text: string): void;

/**
* A callback to use instead of the default one.
*/
urlRegex?: RegExp;
}
}
2 changes: 1 addition & 1 deletion addons/xterm-addon-webgl/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-webgl",
"version": "0.11.4",
"version": "0.12.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
102 changes: 7 additions & 95 deletions addons/xterm-addon-webgl/src/GlyphRenderer.ts
Original file line number Diff line number Diff line change
@@ -6,14 +6,11 @@
import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_INDICIES_PER_CELL, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_BG_OFFSET } from './RenderModel';
import { fill } from 'common/TypedArrayUtils';
import { slice } from './TypedArray';
import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, Attributes, FgFlags } from 'common/buffer/Constants';
import { NULL_CELL_CODE } from 'common/buffer/Constants';
import { Terminal, IBufferLine } from 'xterm';
import { IColorSet, IColor } from 'browser/Types';
import { IColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { AttributeData } from 'common/buffer/AttributeData';

interface IVertices {
attributes: Float32Array;
@@ -24,7 +21,6 @@ interface IVertices {
* working on the next frame.
*/
attributesBuffers: Float32Array[];
selectionAttributes: Float32Array;
count: number;
}

@@ -91,8 +87,7 @@ export class GlyphRenderer {
attributesBuffers: [
new Float32Array(0),
new Float32Array(0)
],
selectionAttributes: new Float32Array(0)
]
};

constructor(
@@ -187,6 +182,8 @@ export class GlyphRenderer {
if (!this._atlas) {
return;
}

// Get the glyph
if (chars && chars.length > 1) {
rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg);
} else {
@@ -214,91 +211,6 @@ export class GlyphRenderer {
// a_cellpos only changes on resize
}

public updateSelection(model: IRenderModel): void {
const terminal = this._terminal;

this._vertices.selectionAttributes = slice(this._vertices.attributes, 0);

const bg = (this._colors.selectionOpaque.rgba >>> 8) | Attributes.CM_RGB;

if (model.selection.columnSelectMode) {
const startCol = model.selection.startCol;
const width = model.selection.endCol - startCol;
const height = model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow + 1;
for (let y = model.selection.viewportCappedStartRow; y < model.selection.viewportCappedStartRow + height; y++) {
this._updateSelectionRange(startCol, startCol + width, y, model, bg);
}
} else {
// Draw first row
const startCol = model.selection.viewportStartRow === model.selection.viewportCappedStartRow ? model.selection.startCol : 0;
const startRowEndCol = model.selection.viewportCappedStartRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols;
this._updateSelectionRange(startCol, startRowEndCol, model.selection.viewportCappedStartRow, model, bg);

// Draw middle rows
const middleRowsCount = Math.max(model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow - 1, 0);
for (let y = model.selection.viewportCappedStartRow + 1; y <= model.selection.viewportCappedStartRow + middleRowsCount; y++) {
this._updateSelectionRange(0, startRowEndCol, y, model, bg);
}

// Draw final row
if (model.selection.viewportCappedStartRow !== model.selection.viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewportStartRow
const endCol = model.selection.viewportEndRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols;
this._updateSelectionRange(0, endCol, model.selection.viewportCappedEndRow, model, bg);
}
}
}

private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number): void {
const terminal = this._terminal;
const row = y + terminal.buffer.active.viewportY;
let line: IBufferLine | undefined;
for (let x = startCol; x < endCol; x++) {
const offset = (y * this._terminal.cols + x) * RENDER_MODEL_INDICIES_PER_CELL;
const code = model.cells[offset];
let fg = model.cells[offset + RENDER_MODEL_FG_OFFSET];
if (fg & FgFlags.INVERSE) {
const workCell = new AttributeData();
workCell.fg = fg;
workCell.bg = model.cells[offset + RENDER_MODEL_BG_OFFSET];
// Get attributes from fg (excluding inverse) and resolve inverse by pullibng rgb colors
// from bg. This is needed since the inverse fg color should be based on the original bg
// color, not on the selection color
fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK | FgFlags.INVERSE);
switch (workCell.getBgColorMode()) {
case Attributes.CM_P16:
case Attributes.CM_P256:
const c = this._getColorFromAnsiIndex(workCell.getBgColor()).rgba;
fg |= (c >> 8) & Attributes.RED_MASK | (c >> 8) & Attributes.GREEN_MASK | (c >> 8) & Attributes.BLUE_MASK;
case Attributes.CM_RGB:
const arr = AttributeData.toColorRGB(workCell.getBgColor());
fg |= arr[0] << Attributes.RED_SHIFT | arr[1] << Attributes.GREEN_SHIFT | arr[2] << Attributes.BLUE_SHIFT;
case Attributes.CM_DEFAULT:
default:
const c2 = this._colors.background.rgba;
fg |= (c2 >> 8) & Attributes.RED_MASK | (c2 >> 8) & Attributes.GREEN_MASK | (c2 >> 8) & Attributes.BLUE_MASK;
}
fg |= Attributes.CM_RGB;
}
if (code & COMBINED_CHAR_BIT_MASK) {
if (!line) {
line = terminal.buffer.active.getLine(row);
}
const chars = line!.getCell(x)!.getChars();
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg, chars);
} else {
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg);
}
}
}

private _getColorFromAnsiIndex(idx: number): IColor {
if (idx >= this._colors.ansi.length) {
throw new Error('No color found for idx ' + idx);
}
return this._colors.ansi[idx];
}

public clear(force?: boolean): void {
const terminal = this._terminal;
const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL;
@@ -333,7 +245,7 @@ export class GlyphRenderer {
public setColors(): void {
}

public render(renderModel: IRenderModel, isSelectionVisible: boolean): void {
public render(renderModel: IRenderModel): void {
if (!this._atlas) {
return;
}
@@ -357,7 +269,7 @@ export class GlyphRenderer {
let bufferLength = 0;
for (let y = 0; y < renderModel.lineLengths.length; y++) {
const si = y * this._terminal.cols * INDICES_PER_CELL;
const sub = (isSelectionVisible ? this._vertices.selectionAttributes : this._vertices.attributes).subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL);
const sub = this._vertices.attributes.subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL);
activeBuffer.set(sub, bufferLength);
bufferLength += sub.length;
}
84 changes: 4 additions & 80 deletions addons/xterm-addon-webgl/src/RectangleRenderer.ts
Original file line number Diff line number Diff line change
@@ -4,11 +4,11 @@
*/

import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelectionRenderModel } from './Types';
import { fill } from 'common/TypedArrayUtils';
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext } from './Types';
import { Attributes, FgFlags } from 'common/buffer/Constants';
import { Terminal } from 'xterm';
import { IColorSet, IColor } from 'browser/Types';
import { IColor } from 'common/Types';
import { IColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';

@@ -49,7 +49,6 @@ void main() {

interface IVertices {
attributes: Float32Array;
selection: Float32Array;
count: number;
}

@@ -66,12 +65,10 @@ export class RectangleRenderer {
private _attributesBuffer: WebGLBuffer;
private _projectionLocation: WebGLUniformLocation;
private _bgFloat!: Float32Array;
private _selectionFloat!: Float32Array;

private _vertices: IVertices = {
count: 0,
attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY),
selection: new Float32Array(3 * INDICES_PER_RECTANGLE)
attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY)
};

constructor(
@@ -137,11 +134,6 @@ export class RectangleRenderer {
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, this._vertices.attributes, gl.DYNAMIC_DRAW);
gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this._vertices.count);

// Bind selection buffer and draw
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, this._vertices.selection, gl.DYNAMIC_DRAW);
gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, 3);
}

public onResize(): void {
@@ -155,7 +147,6 @@ export class RectangleRenderer {

private _updateCachedColors(): void {
this._bgFloat = this._colorToFloat32Array(this._colors.background);
this._selectionFloat = this._colorToFloat32Array(this._colors.selectionOpaque);
}

private _updateViewportRectangle(): void {
@@ -171,73 +162,6 @@ export class RectangleRenderer {
);
}

public updateSelection(model: ISelectionRenderModel): void {
const terminal = this._terminal;

if (!model.hasSelection) {
fill(this._vertices.selection, 0, 0);
return;
}

if (model.columnSelectMode) {
const startCol = model.startCol;
const width = model.endCol - startCol;
const height = model.viewportCappedEndRow - model.viewportCappedStartRow + 1;
this._addRectangleFloat(
this._vertices.selection,
0,
startCol * this._dimensions.scaledCellWidth,
model.viewportCappedStartRow * this._dimensions.scaledCellHeight,
width * this._dimensions.scaledCellWidth,
height * this._dimensions.scaledCellHeight,
this._selectionFloat
);
fill(this._vertices.selection, 0, INDICES_PER_RECTANGLE);
} else {
// Draw first row
const startCol = model.viewportStartRow === model.viewportCappedStartRow ? model.startCol : 0;
const startRowEndCol = model.viewportCappedStartRow === model.viewportEndRow ? model.endCol : terminal.cols;
this._addRectangleFloat(
this._vertices.selection,
0,
startCol * this._dimensions.scaledCellWidth,
model.viewportCappedStartRow * this._dimensions.scaledCellHeight,
(startRowEndCol - startCol) * this._dimensions.scaledCellWidth,
this._dimensions.scaledCellHeight,
this._selectionFloat
);

// Draw middle rows
const middleRowsCount = Math.max(model.viewportCappedEndRow - model.viewportCappedStartRow - 1, 0);
this._addRectangleFloat(
this._vertices.selection,
INDICES_PER_RECTANGLE,
0,
(model.viewportCappedStartRow + 1) * this._dimensions.scaledCellHeight,
terminal.cols * this._dimensions.scaledCellWidth,
middleRowsCount * this._dimensions.scaledCellHeight,
this._selectionFloat
);

// Draw final row
if (model.viewportCappedStartRow !== model.viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewportStartRow
const endCol = model.viewportEndRow === model.viewportCappedEndRow ? model.endCol : terminal.cols;
this._addRectangleFloat(
this._vertices.selection,
INDICES_PER_RECTANGLE * 2,
0,
model.viewportCappedEndRow * this._dimensions.scaledCellHeight,
endCol * this._dimensions.scaledCellWidth,
this._dimensions.scaledCellHeight,
this._selectionFloat
);
} else {
fill(this._vertices.selection, 0, INDICES_PER_RECTANGLE * 2);
}
}
}

public updateBackgrounds(model: IRenderModel): void {
const terminal = this._terminal;
const vertices = this._vertices;
4 changes: 3 additions & 1 deletion addons/xterm-addon-webgl/src/WebglAddon.ts
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import { ICharacterJoinerService, IRenderService } from 'browser/services/Servic
import { IColorSet } from 'browser/Types';
import { EventEmitter } from 'common/EventEmitter';
import { isSafari } from 'common/Platform';
import { IDecorationService } from 'common/services/Services';

export class WebglAddon implements ITerminalAddon {
private _terminal?: Terminal;
@@ -30,8 +31,9 @@ export class WebglAddon implements ITerminalAddon {
this._terminal = terminal;
const renderService: IRenderService = (terminal as any)._core._renderService;
const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService;
const decorationService: IDecorationService = (terminal as any)._core._decorationService;
const colors: IColorSet = (terminal as any)._core._colorManager.colors;
this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, this._preserveDrawingBuffer);
this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, decorationService, this._preserveDrawingBuffer);
this._renderer.onContextLoss(() => this._onContextLoss.fire());
renderService.setRenderer(this._renderer);
}
150 changes: 121 additions & 29 deletions addons/xterm-addon-webgl/src/WebglRenderer.ts
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import { RectangleRenderer } from './RectangleRenderer';
import { IWebGL2RenderingContext } from './Types';
import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { Disposable } from 'common/Lifecycle';
import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
import { Terminal, IEvent } from 'xterm';
import { IRenderLayer } from './renderLayer/Types';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
@@ -23,6 +23,7 @@ import { addDisposableDomListener } from 'browser/Lifecycle';
import { ICharacterJoinerService } from 'browser/services/Services';
import { CharData, ICellData } from 'common/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { IDecorationService } from 'common/services/Services';

export class WebglRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
@@ -31,6 +32,7 @@ export class WebglRenderer extends Disposable implements IRenderer {

private _model: RenderModel = new RenderModel();
private _workCell: CellData = new CellData();
private _workColors: { fg: number, bg: number } = { fg: 0, bg: 0 };

private _canvas: HTMLCanvasElement;
private _gl: IWebGL2RenderingContext;
@@ -52,6 +54,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _terminal: Terminal,
private _colors: IColorSet,
private readonly _characterJoinerService: ICharacterJoinerService,
private readonly _decorationService: IDecorationService,
preserveDrawingBuffer?: boolean
) {
super();
@@ -164,11 +167,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`;

this._rectangleRenderer.onResize();
if (this._model.selection.hasSelection) {
// Update selection as dimensions have changed
this._rectangleRenderer.updateSelection(this._model.selection);
}

this._glyphRenderer.setDimensions(this.dimensions);
this._glyphRenderer.onResize();

@@ -198,10 +196,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
for (const l of this._renderLayers) {
l.onSelectionChanged(this._terminal, start, end, columnSelectMode);
}

this._updateSelectionModel(start, end, columnSelectMode);

this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
this._requestRedrawViewport();
}

public onCursorMove(): void {
@@ -243,7 +239,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._charAtlas?.clearTexture();
this._model.clear();
this._updateModel(0, this._terminal.rows - 1);
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
this._requestRedrawViewport();
}

public clear(): void {
@@ -289,7 +285,7 @@ export class WebglRenderer extends Disposable implements IRenderer {

// Render
this._rectangleRenderer.render();
this._glyphRenderer.render(this._model, this._model.selection.hasSelection);
this._glyphRenderer.render(this._model);
}

private _updateModel(start: number, end: number): void {
@@ -331,14 +327,17 @@ export class WebglRenderer extends Disposable implements IRenderer {
let code = cell.getCode();
const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;

// Load colors/resolve overrides into work colors
this._loadColorsForCell(x, row);

if (code !== NULL_CELL_CODE) {
this._model.lineLengths[y] = x + 1;
}

// Nothing has changed, no updates needed
if (this._model.cells[i] === code &&
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === cell.bg &&
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === cell.fg) {
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workColors.bg &&
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg) {
continue;
}

@@ -349,10 +348,10 @@ export class WebglRenderer extends Disposable implements IRenderer {

// Cache the results in the model
this._model.cells[i] = code;
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = cell.bg;
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = cell.fg;
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;

this._glyphRenderer.updateCell(x, y, code, cell.bg, cell.fg, chars);
this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, chars);

if (isJoined) {
// Restore work cell
@@ -363,17 +362,110 @@ export class WebglRenderer extends Disposable implements IRenderer {
const j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, NULL_CELL_CHAR);
this._model.cells[j] = NULL_CELL_CODE;
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workCell.bg;
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workCell.fg;
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
}
}
}
}
this._rectangleRenderer.updateBackgrounds(this._model);
if (this._model.selection.hasSelection) {
// Model could be updated but the selection is unchanged
this._glyphRenderer.updateSelection(this._model);
}

/**
* Loads colors for the cell into the work colors object. This resolves overrides/inverse if
* necessary which is why the work cell object is not used.
*/
private _loadColorsForCell(x: number, y: number): void {
this._workColors.bg = this._workCell.bg;
this._workColors.fg = this._workCell.fg;

// Get any foreground/background overrides, this happens on the model to avoid spreading
// override logic throughout the different sub-renderers
let bgOverride: number | undefined;
let fgOverride: number | undefined;

// Apply decorations on the bottom layer
for (const d of this._decorationService.getDecorationsAtCell(x, y, 'bottom')) {
if (d.backgroundColorRGB) {
bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
}
if (d.foregroundColorRGB) {
fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
}
}

// Apply the selection color if needed
if (this._isCellSelected(x, y)) {
bgOverride = this._colors.selectionOpaque.rgba >> 8 & 0xFFFFFF;
if (this._colors.selectionForeground) {
fgOverride = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
}
}

// Apply decorations on the top layer
for (const d of this._decorationService.getDecorationsAtCell(x, y, 'top')) {
if (d.backgroundColorRGB) {
bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
}
if (d.foregroundColorRGB) {
fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
}
}

// Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag
// ahead of time in order to use the correct cache key
if (bgOverride !== undefined) {
// Non-RGB attributes from model + override + force RGB color mode
bgOverride = (this._workCell.bg & ~Attributes.RGB_MASK) | bgOverride | Attributes.CM_RGB;
}
if (fgOverride !== undefined) {
// Non-RGB attributes from model + force disable inverse + override + force RGB color mode
fgOverride = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride | Attributes.CM_RGB;
}

// Handle case where inverse was specified by only one of bgOverride or fgOverride was set,
// resolving the other inverse color and setting the inverse flag if needed.
if (this._workColors.fg & FgFlags.INVERSE) {
if (bgOverride !== undefined && fgOverride === undefined) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this._workColors.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
}
if (bgOverride === undefined && fgOverride !== undefined) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this._workColors.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
}
}

// Use the override if it exists
this._workColors.bg = bgOverride ?? this._workColors.bg;
this._workColors.fg = fgOverride ?? this._workColors.fg;
}

private _isCellSelected(x: number, y: number): boolean {
if (!this._model.selection.hasSelection) {
return false;
}
y -= this._terminal.buffer.active.viewportY;
if (this._model.selection.columnSelectMode) {
if (this._model.selection.startCol <= this._model.selection.endCol) {
return x >= this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
x < this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
}
return x < this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
x >= this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
}
return (y > this._model.selection.viewportStartRow && y < this._model.selection.viewportEndRow) ||
(this._model.selection.viewportStartRow === this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol && x < this._model.selection.endCol) ||
(this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportEndRow && x < this._model.selection.endCol) ||
(this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol);
}

private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
@@ -382,7 +474,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Selection does not exist
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
this._model.clearSelection();
this._rectangleRenderer.updateSelection(this._model.selection);
return;
}

@@ -395,7 +486,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
// No need to draw the selection
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
this._model.clearSelection();
this._rectangleRenderer.updateSelection(this._model.selection);
return;
}

@@ -407,8 +497,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._model.selection.viewportCappedEndRow = viewportCappedEndRow;
this._model.selection.startCol = start[0];
this._model.selection.endCol = end[0];

this._rectangleRenderer.updateSelection(this._model.selection);
}

/**
@@ -440,18 +528,18 @@ export class WebglRenderer extends Disposable implements IRenderer {
// will be floored because since lineHeight can never be lower then 1, there
// is a guarentee that the scaled line height will always be larger than
// scaled char height.
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.getOption('lineHeight'));
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight!);

// Calculate the y coordinate within a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledCharTop = this._terminal.getOption('lineHeight') === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);

// Calculate the scaled cell width, taking the letterSpacing into account.
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.getOption('letterSpacing'));
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing!);

// Calculate the x coordinate with a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledCharLeft = Math.floor(this._terminal.getOption('letterSpacing') / 2);
this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing! / 2);

// Recalculate the canvas dimensions; scaled* define the actual number of
// pixel in the canvas
@@ -482,6 +570,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio;
this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio;
}

private _requestRedrawViewport(): void {
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
}
}

// TODO: Share impl with core
24 changes: 13 additions & 11 deletions addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts
Original file line number Diff line number Diff line change
@@ -6,7 +6,8 @@
import { ICharAtlasConfig } from './Types';
import { Attributes } from 'common/buffer/Constants';
import { Terminal, FontWeight } from 'xterm';
import { IColorSet, IColor } from 'browser/Types';
import { IColorSet } from 'browser/Types';
import { IColor } from 'common/Types';

const NULL_COLOR: IColor = {
css: '',
@@ -22,27 +23,28 @@ export function generateConfig(scaledCellWidth: number, scaledCellHeight: number
cursorAccent: NULL_COLOR,
selectionTransparent: NULL_COLOR,
selectionOpaque: NULL_COLOR,
selectionForeground: NULL_COLOR,
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
// dynamic character atlas.
ansi: colors.ansi.slice(),
contrastCache: colors.contrastCache
};
return {
customGlyphs: terminal.getOption('customGlyphs'),
customGlyphs: terminal.options.customGlyphs!,
devicePixelRatio: window.devicePixelRatio,
letterSpacing: terminal.getOption('letterSpacing'),
lineHeight: terminal.getOption('lineHeight'),
letterSpacing: terminal.options.letterSpacing!,
lineHeight: terminal.options.lineHeight!,
scaledCellWidth,
scaledCellHeight,
scaledCharWidth,
scaledCharHeight,
fontFamily: terminal.getOption('fontFamily'),
fontSize: terminal.getOption('fontSize'),
fontWeight: terminal.getOption('fontWeight') as FontWeight,
fontWeightBold: terminal.getOption('fontWeightBold') as FontWeight,
allowTransparency: terminal.getOption('allowTransparency'),
drawBoldTextInBrightColors: terminal.getOption('drawBoldTextInBrightColors'),
minimumContrastRatio: terminal.getOption('minimumContrastRatio'),
fontFamily: terminal.options.fontFamily!,
fontSize: terminal.options.fontSize!,
fontWeight: terminal.options.fontWeight as FontWeight,
fontWeightBold: terminal.options.fontWeightBold as FontWeight,
allowTransparency: terminal.options.allowTransparency!,
drawBoldTextInBrightColors: terminal.options.drawBoldTextInBrightColors!,
minimumContrastRatio: terminal.options.minimumContrastRatio!,
colors: clonedColors
};
}
42 changes: 19 additions & 23 deletions addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts
Original file line number Diff line number Diff line change
@@ -8,11 +8,12 @@ import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/atlas/Constants';
import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types';
import { DEFAULT_COLOR, Attributes } from 'common/buffer/Constants';
import { throwIfFalsy } from '../WebglUtils';
import { IColor } from 'browser/Types';
import { IColor } from 'common/Types';
import { IDisposable } from 'xterm';
import { AttributeData } from 'common/buffer/AttributeData';
import { channels, rgba } from 'browser/Color';
import { channels, rgba } from 'common/Color';
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
import { excludeFromContrastRatioDemands, isPowerlineGlyph } from 'browser/renderer/RendererUtils';

// For debugging purposes, it can be useful to set this to a really tiny value,
// to verify that LRU eviction works.
@@ -216,8 +217,8 @@ export class WebglCharAtlas implements IDisposable {
}
}

private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string {
const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold);
private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): string {
const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands);
if (minimumContrastCss) {
return minimumContrastCss;
}
@@ -238,7 +239,7 @@ export class WebglCharAtlas implements IDisposable {
const bg = this._config.colors.background.css;
if (bg.length === 9) {
// Remove bg alpha channel if present
return bg.substr(0, 7);
return bg.slice(0, 7);
}
return bg;
}
@@ -281,8 +282,8 @@ export class WebglCharAtlas implements IDisposable {
}
}

private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string | undefined {
if (this._config.minimumContrastRatio === 1) {
private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): string | undefined {
if (this._config.minimumContrastRatio === 1 || excludeFromContrastRatioDemands) {
return undefined;
}

@@ -321,10 +322,15 @@ export class WebglCharAtlas implements IDisposable {
// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
// to draw the glyph to the canvas as well as to restrict the bounding box search to ensure
// giant ligatures (eg. =====>) don't impact overall performance.
const allowedWidth = this._config.scaledCharWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
const allowedWidth = this._config.scaledCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
if (this._tmpCanvas.width < allowedWidth) {
this._tmpCanvas.width = allowedWidth;
}
// Include line height when drawing glyphs
const allowedHeight = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2;
if (this._tmpCanvas.height < allowedHeight) {
this._tmpCanvas.height = allowedHeight;
}
this._tmpCtx.save();

this._workAttributeData.fg = fg;
@@ -370,26 +376,16 @@ export class WebglCharAtlas implements IDisposable {
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
this._tmpCtx.textBaseline = TEXT_BASELINE;

this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold);
const powerLineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0)));

// Apply alpha to dim the character
if (dim) {
this._tmpCtx.globalAlpha = DIM_OPACITY;
}

// Check if the char is a powerline glyph, these will be restricted to a single cell glyph, no
// padding on either side that are allowed for other glyphs since they are designed to be pixel
// perfect but may render with "bad" anti-aliasing
let isPowerlineGlyph = false;
if (chars.length === 1) {
const code = chars.charCodeAt(0);
if (code >= 0xE0A0 && code <= 0xE0D6) {
isPowerlineGlyph = true;
}
}

// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
const padding = isPowerlineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING;
const padding = powerLineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING;

// Draw custom characters if applicable
let drawSuccess = false;
@@ -459,7 +455,7 @@ export class WebglCharAtlas implements IDisposable {
return NULL_RASTERIZED_GLYPH;
}

const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, isPowerlineGlyph, drawSuccess);
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, powerLineGlyph, drawSuccess);
const clippedImageData = this._clipImageData(imageData, this._workBoundingBox);

// Check if there is enough room in the current row and go to next if needed
@@ -494,7 +490,7 @@ export class WebglCharAtlas implements IDisposable {
*/
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean, customGlyph: boolean): IRasterizedGlyph {
boundingBox.top = 0;
const height = restrictedGlyph ? this._config.scaledCharHeight : this._tmpCanvas.height;
const height = restrictedGlyph ? this._config.scaledCellHeight : this._tmpCanvas.height;
const width = restrictedGlyph ? this._config.scaledCharWidth : allowedWidth;
let found = false;
for (let y = 0; y < height; y++) {
4 changes: 2 additions & 2 deletions addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts
Original file line number Diff line number Diff line change
@@ -254,10 +254,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param isBold If we should use the bold fontWeight.
*/
protected _getFont(terminal: Terminal, isBold: boolean, isItalic: boolean): string {
const fontWeight = isBold ? terminal.getOption('fontWeightBold') : terminal.getOption('fontWeight');
const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight;
const fontStyle = isItalic ? 'italic' : '';

return `${fontStyle} ${fontWeight} ${terminal.getOption('fontSize') * window.devicePixelRatio}px ${terminal.getOption('fontFamily')}`;
return `${fontStyle} ${fontWeight} ${terminal.options.fontSize! * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
}
}

14 changes: 7 additions & 7 deletions addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts
Original file line number Diff line number Diff line change
@@ -83,7 +83,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
}

public onOptionsChanged(terminal: Terminal): void {
if (terminal.getOption('cursorBlink')) {
if (terminal.options.cursorBlink) {
if (!this._cursorBlinkStateManager) {
this._cursorBlinkStateManager = new CursorBlinkStateManager(terminal, () => {
this._render(terminal, true);
@@ -140,7 +140,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
this._clearCursor();
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
const cursorStyle = terminal.getOption('cursorStyle');
const cursorStyle = terminal.options.cursorStyle;
if (cursorStyle && cursorStyle !== 'block') {
this._cursorRenderers[cursorStyle](terminal, cursorX, viewportRelativeCursorY, this._cell);
} else {
@@ -150,7 +150,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
this._state.x = cursorX;
this._state.y = viewportRelativeCursorY;
this._state.isFocused = false;
this._state.style = cursorStyle;
this._state.style = cursorStyle!;
this._state.width = this._cell.getWidth();
return;
}
@@ -166,21 +166,21 @@ export class CursorRenderLayer extends BaseRenderLayer {
if (this._state.x === cursorX &&
this._state.y === viewportRelativeCursorY &&
this._state.isFocused === isTerminalFocused(terminal) &&
this._state.style === terminal.getOption('cursorStyle') &&
this._state.style === terminal.options.cursorStyle &&
this._state.width === this._cell.getWidth()) {
return;
}
this._clearCursor();
}

this._ctx.save();
this._cursorRenderers[terminal.getOption('cursorStyle') || 'block'](terminal, cursorX, viewportRelativeCursorY, this._cell);
this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, cursorX, viewportRelativeCursorY, this._cell);
this._ctx.restore();

this._state.x = cursorX;
this._state.y = viewportRelativeCursorY;
this._state.isFocused = false;
this._state.style = terminal.getOption('cursorStyle');
this._state.style = terminal.options.cursorStyle!;
this._state.width = this._cell.getWidth();
}

@@ -205,7 +205,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._fillLeftLineAtCell(x, y, terminal.getOption('cursorWidth'));
this._fillLeftLineAtCell(x, y, terminal.options.cursorWidth!);
this._ctx.restore();
}

1 change: 1 addition & 0 deletions addons/xterm-addon-webgl/src/tsconfig.json
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@
]
},
"strict": true,
"downlevelIteration": true,
"types": [
"../../../node_modules/@types/mocha"
]
214 changes: 162 additions & 52 deletions addons/xterm-addon-webgl/test/WebglRenderer.api.ts

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions bin/publish.js
Original file line number Diff line number Diff line change
@@ -104,11 +104,11 @@ function getNextBetaVersion(packageJson) {
return `${nextStableVersion}-${tag}.1`;
}
const latestPublishedVersion = publishedVersions.sort((a, b) => {
const aVersion = parseInt(a.substr(a.search(/\d+$/)));
const bVersion = parseInt(b.substr(b.search(/\d+$/)));
const aVersion = parseInt(a.slice(a.search(/\d+$/)));
const bVersion = parseInt(b.slice(b.search(/\d+$/)));
return aVersion > bVersion ? -1 : 1;
})[0];
const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/\d+$/)), 10);
const latestTagVersion = parseInt(latestPublishedVersion.slice(latestPublishedVersion.search(/\d+$/)), 10);
return `${nextStableVersion}-${tag}.${latestTagVersion + 1}`;
}

18 changes: 14 additions & 4 deletions css/xterm.css
Original file line number Diff line number Diff line change
@@ -36,6 +36,7 @@
*/

.xterm {
cursor: text;
position: relative;
user-select: none;
-ms-user-select: none;
@@ -124,10 +125,6 @@
line-height: normal;
}

.xterm {
cursor: text;
}

.xterm.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
@@ -178,3 +175,16 @@
z-index: 6;
position: absolute;
}

.xterm-decoration-overview-ruler {
z-index: 7;
position: absolute;
top: 0;
right: 0;
pointer-events: none;
}

.xterm-decoration-top {
z-index: 2;
position: relative;
}
114 changes: 107 additions & 7 deletions demo/client.ts
Original file line number Diff line number Diff line change
@@ -92,8 +92,10 @@ const addons: { [T in AddonType]: IDemoAddon<T>} = {

const terminalContainer = document.getElementById('terminal-container');
const actionElements = {
find: <HTMLInputElement>document.querySelector('#find'),
findNext: <HTMLInputElement>document.querySelector('#find-next'),
findPrevious: <HTMLInputElement>document.querySelector('#find-previous')
findPrevious: <HTMLInputElement>document.querySelector('#find-previous'),
findResults: document.querySelector('#find-results')
};
const paddingElement = <HTMLInputElement>document.getElementById('padding');

@@ -107,7 +109,15 @@ function getSearchOptions(e: KeyboardEvent): ISearchOptions {
regex: (document.getElementById('regex') as HTMLInputElement).checked,
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked,
incremental: e.key !== `Enter`
incremental: e.key !== `Enter`,
decorations: (document.getElementById('highlight-all-matches') as HTMLInputElement).checked ? {
matchBackground: '#232422',
matchBorder: '#555753',
matchOverviewRuler: '#555753',
activeMatchBackground: '#ef2929',
activeMatchBorder: '#ffffff',
activeMatchColorOverviewRuler: '#ef2929'
} : undefined
};
}

@@ -150,7 +160,9 @@ if (document.location.pathname === '/test') {
document.getElementById('htmlserialize').addEventListener('click', htmlSerializeButtonHandler);
document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler);
document.getElementById('load-test').addEventListener('click', loadTest);
document.getElementById('powerline-symbol-test').addEventListener('click', powerlineSymbolTest);
document.getElementById('add-decoration').addEventListener('click', addDecoration);
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
}

function createTerminal(): void {
@@ -202,10 +214,15 @@ function createTerminal(): void {
addDomListener(actionElements.findNext, 'keyup', (e) => {
addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions(e));
});

addDomListener(actionElements.findPrevious, 'keyup', (e) => {
addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions(e));
});
addDomListener(actionElements.findNext, 'blur', (e) => {
addons.search.instance.clearActiveDecoration();
});
addDomListener(actionElements.findPrevious, 'blur', (e) => {
addons.search.instance.clearActiveDecoration();
});

// fit is called within a setTimeout, cols and rows need this.
setTimeout(() => {
@@ -382,9 +399,12 @@ function initAddons(term: TerminalType): void {
if (!addon.canChange) {
checkbox.disabled = true;
}
if(name === 'unicode11' && checkbox.checked) {
if (name === 'unicode11' && checkbox.checked) {
term.unicode.activeVersion = '11';
}
if (name === 'search' && checkbox.checked) {
addon.instance.onDidChangeResults(e => updateFindResults(e));
}
addDomListener(checkbox, 'change', () => {
if (checkbox.checked) {
addon.instance = new addon.ctor();
@@ -395,6 +415,8 @@ function initAddons(term: TerminalType): void {
}, 0);
} else if (name === 'unicode11') {
term.unicode.activeVersion = '11';
} else if (name === 'search') {
addon.instance.onDidChangeResults(e => updateFindResults(e));
}
} else {
if (name === 'webgl') {
@@ -423,6 +445,16 @@ function initAddons(term: TerminalType): void {
container.appendChild(fragment);
}

function updateFindResults(e: { resultIndex: number, resultCount: number } | undefined) {
let content: string;
if (e === undefined) {
content = 'undefined';
} else {
content = `index: ${e.resultIndex}, count: ${e.resultCount}`;
}
actionElements.findResults.textContent = content;
}

function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
element.addEventListener(type, handler);
term._core.register({ dispose: () => element.removeEventListener(type, handler) });
@@ -543,10 +575,78 @@ function loadTest() {
});
}

function powerlineSymbolTest() {
function s(char: string): string {
return `${char} \x1b[7m${char}\x1b[0m `;
}
term.write('\n\n\r');
term.writeln('Standard powerline symbols:');
term.writeln(' 0 1 2 3 4 5 6 7 8 9 A B C D E F');
term.writeln(`0xA_ ${s('\ue0a0')}${s('\ue0a1')}${s('\ue0a2')}`);
term.writeln(`0xB_ ${s('\ue0b0')}${s('\ue0b1')}${s('\ue0b2')}${s('\ue0b3')}`);
term.writeln('');
term.writeln(
`\x1b[7m` +
` inverse \ue0b1 \x1b[0;40m\ue0b0` +
` 0 \ue0b1 \x1b[30;41m\ue0b0\x1b[39m` +
` 1 \ue0b1 \x1b[31;42m\ue0b0\x1b[39m` +
` 2 \ue0b1 \x1b[32;43m\ue0b0\x1b[39m` +
` 3 \ue0b1 \x1b[33;44m\ue0b0\x1b[39m` +
` 4 \ue0b1 \x1b[34;45m\ue0b0\x1b[39m` +
` 5 \ue0b1 \x1b[35;46m\ue0b0\x1b[39m` +
` 6 \ue0b1 \x1b[36;47m\ue0b0\x1b[39m` +
` 7 \ue0b1 \x1b[37;49m\ue0b0\x1b[0m`
);
term.writeln('');
term.writeln(
`\x1b[7m` +
` inverse \ue0b3 \x1b[0;7;40m\ue0b2\x1b[27m` +
` 0 \ue0b3 \x1b[7;30;41m\ue0b2\x1b[27;39m` +
` 1 \ue0b3 \x1b[7;31;42m\ue0b2\x1b[27;39m` +
` 2 \ue0b3 \x1b[7;32;43m\ue0b2\x1b[27;39m` +
` 3 \ue0b3 \x1b[7;33;44m\ue0b2\x1b[27;39m` +
` 4 \ue0b3 \x1b[7;34;45m\ue0b2\x1b[27;39m` +
` 5 \ue0b3 \x1b[7;35;46m\ue0b2\x1b[27;39m` +
` 6 \ue0b3 \x1b[7;36;47m\ue0b2\x1b[27;39m` +
` 7 \ue0b3 \x1b[7;37;49m\ue0b2\x1b[0m`
);
term.writeln('');
term.writeln('Powerline extra symbols:');
term.writeln(' 0 1 2 3 4 5 6 7 8 9 A B C D E F');
term.writeln(`0xA_ ${s('\ue0a3')}`);
term.writeln(`0xB_ ${s('\ue0b4')}${s('\ue0b5')}${s('\ue0b6')}${s('\ue0b7')}${s('\ue0b8')}${s('\ue0b9')}${s('\ue0ba')}${s('\ue0bb')}${s('\ue0bc')}${s('\ue0bd')}${s('\ue0be')}${s('\ue0bf')}`);
term.writeln(`0xC_ ${s('\ue0c0')}${s('\ue0c1')}${s('\ue0c2')}${s('\ue0c3')}${s('\ue0c4')}${s('\ue0c5')}${s('\ue0c6')}${s('\ue0c7')}${s('\ue0c8')}${s('\ue0c9')}${s('\ue0ca')}${s('\ue0cb')}${s('\ue0cc')}${s('\ue0cd')}${s('\ue0be')}${s('\ue0bf')}`);
term.writeln(`0xD_ ${s('\ue0d0')}${s('\ue0d1')}${s('\ue0d2')} ${s('\ue0d4')}`);
term.writeln('');
term.writeln('Sample of nerd fonts icons:');
term.writeln(' nf-linux-apple (\\uF302) \uf302');
term.writeln('nf-mdi-github_face (\\uFbd9) \ufbd9');
}

function addDecoration() {
term.options['overviewRulerWidth'] = 15;
const marker = term.addMarker(1);
const decoration = term.registerDecoration({ marker });
decoration.onRender(() => {
decoration.element.style.backgroundColor = 'red';
const decoration = term.registerDecoration({
marker,
backgroundColor: '#00FF00',
foregroundColor: '#00FE00',
overviewRulerOptions: { color: '#ef292980', position: 'left' }
});
decoration.onRender((e: HTMLElement) => {
e.style.right = '100%';
e.style.backgroundColor = '#ef292980';
});
}

function addOverviewRuler() {
term.options['overviewRulerWidth'] = 15;
term.registerDecoration({marker: term.addMarker(1), overviewRulerOptions: { color: '#ef2929' }});
term.registerDecoration({marker: term.addMarker(3), overviewRulerOptions: { color: '#8ae234' }});
term.registerDecoration({marker: term.addMarker(5), overviewRulerOptions: { color: '#729fcf' }});
term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#ef2929', position: 'left' }});
term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#8ae234', position: 'center' }});
term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#729fcf', position: 'right' }});
term.registerDecoration({marker: term.addMarker(10), overviewRulerOptions: { color: '#8ae234', position: 'center' }});
term.registerDecoration({marker: term.addMarker(10), overviewRulerOptions: { color: '#ffffff80', position: 'full' }});
}

23 changes: 18 additions & 5 deletions demo/index.html
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ <h1 style="color: #2D2E2C">xterm.js: A terminal for the <em style="color: #5DA5D
</div>
<div id="options" class="tabContent">
<h3>Options</h3>
<p>These options can be set in the <code>Terminal</code> constructor or by using the <code>Terminal.setOption</code> function.</p>
<p>These options can be set in the <code>Terminal</code> constructor or by using the <code>Terminal.options</code> property.</p>
<div id="options-container"></div>
</div>
<div id="addons" class="tabContent">
@@ -40,9 +40,11 @@ <h4>SearchAddon</h4>
<div style= "display:flex; flex-direction:column;">
<label>Find next <input id="find-next"/></label>
<label>Find previous <input id="find-previous"/></label>
<div>Results: <span id="find-results"></span></div>
<label><input type="checkbox" id="regex"/>Use regex</label>
<label><input type="checkbox" id="case-sensitive"/>Case sensitive</label>
<label><input type="checkbox" id="whole-word"/>Whole word</label>
<label><input type="checkbox" id="highlight-all-matches" checked/>Highlight All Matches</label>
</div>
<h4>SerializeAddon</h4>
<div>
@@ -65,10 +67,21 @@ <h3>Style</h3>
<div id="test" class="tabContent">
<h3>Test</h3>
<div style="display: inline-block; margin-right: 16px;">
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
<button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button>
<button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button>
<button id="add-decoration" title="Add a decoration to the terminal">Decoration</button>
<dl>
<dt>Lifecycle</dt>
<dd><button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button></dd>

<dt>Performance</dt>
<dd><button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button></dd>

<dt>Styles</dt>
<dd><button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button></dd>
<dd><button id="powerline-symbol-test" title="Write powerline symbol characters to the terminal (\ue0a0+)">Powerline symbol test</button></dd>

<dt>Decorations</dt>
<dd><button id="add-decoration" title="Add a decoration to the terminal">Decoration</button></dd>
<dd><button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button></dd>
</dl>
</div>
</div>
</div>
3 changes: 3 additions & 0 deletions demo/server.js
Original file line number Diff line number Diff line change
@@ -114,6 +114,9 @@ function startServer() {
}
const send = USE_BINARY ? bufferUtf8(ws, 5) : buffer(ws, 5);

// WARNING: This is a naive implementation that will not throttle the flow of data. This means
// it could flood the communication channel and make the terminal unresponsive. Learn more about
// the problem and how to implement flow control at https://xtermjs.org/docs/guides/flowcontrol/
term.on('data', function(data) {
try {
send(data);
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "4.17.0",
"version": "4.19.0",
"main": "lib/xterm.js",
"style": "css/xterm.css",
"types": "typings/xterm.d.ts",
@@ -76,7 +76,7 @@
"mustache": "^4.2.0",
"node-pty": "^0.10.1",
"nyc": "^15.1.0",
"playwright": "^1.16.2",
"playwright": "^1.22.1",
"source-map-loader": "^3.0.0",
"source-map-support": "^0.5.20",
"ts-loader": "^9.1.2",
@@ -85,6 +85,6 @@
"webpack": "^5.61.0",
"webpack-cli": "^4.9.1",
"ws": "^8.2.3",
"xterm-benchmark": "^0.3.0"
"xterm-benchmark": "^0.3.1"
}
}
3 changes: 2 additions & 1 deletion src/browser/ColorContrastCache.ts
Original file line number Diff line number Diff line change
@@ -3,7 +3,8 @@
* @license MIT
*/

import { IColor, IColorContrastCache } from 'browser/Types';
import { IColorContrastCache } from 'browser/Types';
import { IColor } from 'common/Types';

export class ColorContrastCache implements IColorContrastCache {
private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {};
2 changes: 1 addition & 1 deletion src/browser/ColorManager.test.ts
Original file line number Diff line number Diff line change
@@ -34,7 +34,7 @@ describe('ColorManager', () => {
describe('constructor', () => {
it('should fill all colors with values', () => {
for (const key of Object.keys(cm.colors)) {
if (key !== 'ansi' && key !== 'contrastCache') {
if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') {
// A #rrggbb or rgba(...)
assert.ok((cm.colors as any)[key].css.length >= 7);
}
18 changes: 14 additions & 4 deletions src/browser/ColorManager.ts
Original file line number Diff line number Diff line change
@@ -3,11 +3,11 @@
* @license MIT
*/

import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/Types';
import { IColorManager, IColorSet, IColorContrastCache } from 'browser/Types';
import { ITheme } from 'common/services/Services';
import { channels, color, css } from 'browser/Color';
import { channels, color, css } from 'common/Color';
import { ColorContrastCache } from 'browser/ColorContrastCache';
import { ColorIndex } from 'common/Types';
import { ColorIndex, IColor } from 'common/Types';


interface IRestoreColorSet {
@@ -104,6 +104,7 @@ export class ColorManager implements IColorManager {
cursorAccent: DEFAULT_CURSOR_ACCENT,
selectionTransparent: DEFAULT_SELECTION,
selectionOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
selectionForeground: undefined,
ansi: DEFAULT_ANSI_COLORS.slice(),
contrastCache: this._contrastCache
};
@@ -128,6 +129,15 @@ export class ColorManager implements IColorManager {
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true);
this.colors.selectionTransparent = this._parseColor(theme.selection, DEFAULT_SELECTION, true);
this.colors.selectionOpaque = color.blend(this.colors.background, this.colors.selectionTransparent);
const nullColor: IColor = {
css: '',
rgba: 0
};
this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, nullColor) : undefined;
if (this.colors.selectionForeground === nullColor) {
this.colors.selectionForeground = undefined;
}

/**
* If selection color is opaque, blend it with background with 0.3 opacity
* Issue #2737
@@ -185,7 +195,7 @@ export class ColorManager implements IColorManager {
foreground: this.colors.foreground,
background: this.colors.background,
cursor: this.colors.cursor,
ansi: [...this.colors.ansi]
ansi: this.colors.ansi.slice()
};
}

19 changes: 12 additions & 7 deletions src/browser/Linkifier2.ts
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
private _linkProviders: ILinkProvider[] = [];
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
protected _currentLink: ILinkWithState | undefined;
private _mouseDownLink: ILinkWithState | undefined;
private _lastMouseEvent: MouseEvent | undefined;
private _linkCacheDisposables: IDisposable[] = [];
private _lastBufferCell: IBufferCellPosition | undefined;
@@ -61,7 +62,8 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
this._clearCurrentLink();
}));
this.register(addDisposableDomListener(this._element, 'mousemove', this._onMouseMove.bind(this)));
this.register(addDisposableDomListener(this._element, 'click', this._onClick.bind(this)));
this.register(addDisposableDomListener(this._element, 'mousedown', this._handleMouseDown.bind(this)));
this.register(addDisposableDomListener(this._element, 'mouseup', this._handleMouseUp.bind(this)));
}

private _onMouseMove(event: MouseEvent): void {
@@ -129,7 +131,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
let linkProvided = false;

// There is no link cached, so ask for one
this._linkProviders.forEach((linkProvider, i) => {
for (const [i, linkProvider] of this._linkProviders.entries()) {
if (useLineCache) {
const existingReply = this._activeProviderReplies?.get(i);
// If there isn't a reply, the provider hasn't responded yet.
@@ -156,7 +158,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
});
}
});
}
}

private _removeIntersectingLinks(y: number, replies: Map<Number, ILinkWithState[] | undefined>): void {
@@ -222,18 +224,21 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
return linkProvided;
}

private _onClick(event: MouseEvent): void {
private _handleMouseDown(): void {
this._mouseDownLink = this._currentLink;
}

private _handleMouseUp(event: MouseEvent): void {
if (!this._element || !this._mouseService || !this._currentLink) {
return;
}

const position = this._positionFromMouseEvent(event, this._element, this._mouseService);

if (!position) {
return;
}

if (this._linkAtPosition(this._currentLink.link, position)) {
if (this._mouseDownLink === this._currentLink && this._linkAtPosition(this._currentLink.link, position)) {
this._currentLink.link.activate(event, this._currentLink.link.text);
}
}
@@ -303,7 +308,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {

// Add listener for rerendering
if (this._renderService) {
this._linkCacheDisposables.push(this._renderService.onRenderedBufferChange(e => {
this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => {
// 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;
25 changes: 22 additions & 3 deletions src/browser/RenderDebouncer.ts
Original file line number Diff line number Diff line change
@@ -3,16 +3,17 @@
* @license MIT
*/

import { IRenderDebouncer } from 'browser/Types';
import { IRenderDebouncerWithCallback } from 'browser/Types';

/**
* Debounces calls to render terminal rows using animation frames.
*/
export class RenderDebouncer implements IRenderDebouncer {
export class RenderDebouncer implements IRenderDebouncerWithCallback {
private _rowStart: number | undefined;
private _rowEnd: number | undefined;
private _rowCount: number | undefined;
private _animationFrame: number | undefined;
private _refreshCallbacks: FrameRequestCallback[] = [];

constructor(
private _renderCallback: (start: number, end: number) => void
@@ -26,6 +27,14 @@ export class RenderDebouncer implements IRenderDebouncer {
}
}

public addRefreshCallback(callback: FrameRequestCallback): number {
this._refreshCallbacks.push(callback);
if (!this._animationFrame) {
this._animationFrame = window.requestAnimationFrame(() => this._innerRefresh());
}
return this._animationFrame;
}

public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
this._rowCount = rowCount;
// Get the min/max row start/end for the arg values
@@ -43,8 +52,11 @@ export class RenderDebouncer implements IRenderDebouncer {
}

private _innerRefresh(): void {
this._animationFrame = undefined;

// Make sure values are set
if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) {
this._runRefreshCallbacks();
return;
}

@@ -55,9 +67,16 @@ export class RenderDebouncer implements IRenderDebouncer {
// Reset debouncer (this happens before render callback as the render could trigger it again)
this._rowStart = undefined;
this._rowEnd = undefined;
this._animationFrame = undefined;

// Run render callback
this._renderCallback(start, end);
this._runRefreshCallbacks();
}

private _runRefreshCallbacks(): void {
for (const callback of this._refreshCallbacks) {
callback(0);
}
this._refreshCallbacks = [];
}
}
6 changes: 3 additions & 3 deletions src/browser/Terminal.test.ts
Original file line number Diff line number Diff line change
@@ -231,7 +231,7 @@ describe('Terminal', () => {
});
term.paste('foo');
});
it('should sanitize \n chars', done => {
it('should sanitize \\n chars', done => {
term.onData(e => {
assert.equal(e, '\rfoo\rbar\r');
done();
@@ -1332,8 +1332,8 @@ describe('Terminal', () => {
(!(i % 3))
? input[i]
: (i % 3 === 1)
? input.substr(i, 2)
: input.substr(i - 1, 2),
? input.slice(i, i + 2)
: input.slice(i - 1, i + 1),
terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars());
}
});
Loading