Skip to content

Commit 6ed1f42

Browse files
authored
Merge branch 'master' into osc52
2 parents 3cedfbe + 826c057 commit 6ed1f42

27 files changed

+162
-62
lines changed

.devcontainer/devcontainer.json

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
{
22
"name": "xterm.js",
3-
"image": "mcr.microsoft.com/devcontainers/typescript-node:0-18-buster",
3+
"image": "mcr.microsoft.com/devcontainers/typescript-node:18-bookworm",
44
"features": {
5-
"ghcr.io/devcontainers/features/node:1": {} // yarn
5+
"ghcr.io/devcontainers/features/node:1": {
6+
"version": 18
7+
} // yarn
68
},
79
"forwardPorts": [
810
3000
911
],
10-
"postCreateCommand": "yarn install",
12+
"postCreateCommand": "yarn install && yarn setup",
1113
"customizations": {
1214
"vscode": {
1315
"extensions": [

.nvmrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
16
1+
18

.vscode/launch.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
"runtimeExecutable": "npm",
6262
"runtimeArgs": ["start"],
6363
"stopOnEntry": true,
64-
"runtimeVersion": "16",
64+
"runtimeVersion": "18",
6565
"serverReadyAction": {
6666
"action": "openExternally",
6767
"pattern": "App listening to (http://.*?:[0-9]+)"

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
223223
- [**Cloudtutor.io**](https://cloudtutor.io): innovative online learning platform that offers users access to an interactive lab.
224224
- [**Helix Editor Playground**](https://github.com/tomgroenwoldt/helix-editor-playground): Online playground for the terminal based helix editor.
225225
- [**Coder**](https://github.com/coder/coder): Self-Hosted Remote Development Environments
226+
- [**Wave Terminal**](https://waveterm.dev): An open-source, ai-native, terminal built for seamless workflows.
226227
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
227228

228229
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.

addons/addon-attach/webpack.config.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ module.exports = {
2525
filename: mainFile,
2626
path: path.resolve('./lib'),
2727
library: addonName,
28-
libraryTarget: 'umd'
28+
libraryTarget: 'umd',
29+
// Force usage of globalThis instead of global / self. (This is cross-env compatible)
30+
globalObject: 'globalThis',
2931
},
3032
mode: 'production'
3133
};

addons/addon-canvas/webpack.config.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ module.exports = {
3333
filename: mainFile,
3434
path: path.resolve('./lib'),
3535
library: addonName,
36-
libraryTarget: 'umd'
36+
libraryTarget: 'umd',
37+
// Force usage of globalThis instead of global / self. (This is cross-env compatible)
38+
globalObject: 'globalThis',
3739
},
3840
mode: 'production'
3941
};

addons/addon-fit/webpack.config.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ module.exports = {
2525
filename: mainFile,
2626
path: path.resolve('./lib'),
2727
library: addonName,
28-
libraryTarget: 'umd'
28+
libraryTarget: 'umd',
29+
// Force usage of globalThis instead of global / self. (This is cross-env compatible)
30+
globalObject: 'globalThis',
2931
},
3032
mode: 'production'
3133
};

addons/addon-image/webpack.config.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ const addon = {
3333
filename: mainFile,
3434
path: path.resolve('./lib'),
3535
library: addonName,
36-
libraryTarget: 'umd'
36+
libraryTarget: 'umd',
37+
// Force usage of globalThis instead of global / self. (This is cross-env compatible)
38+
globalObject: 'globalThis',
3739
},
3840
mode: 'production'
3941
};

addons/addon-ligatures/webpack.config.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ module.exports = {
2525
filename: mainFile,
2626
path: path.resolve('./lib'),
2727
library: addonName,
28-
libraryTarget: 'umd'
28+
libraryTarget: 'umd',
29+
// Force usage of globalThis instead of global / self. (This is cross-env compatible)
30+
globalObject: 'globalThis',
2931
},
3032
mode: 'production',
3133
externals: {

addons/addon-ligatures/yarn.lock

+3-3
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ fd-slicer@~1.1.0:
8686
pend "~1.2.0"
8787

8888
follow-redirects@^1.15.0:
89-
version "1.15.3"
90-
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
91-
integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
89+
version "1.15.6"
90+
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
91+
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
9292

9393
font-finder@^1.0.3:
9494
version "1.0.4"

addons/addon-search/webpack.config.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ module.exports = {
3232
filename: mainFile,
3333
path: path.resolve('./lib'),
3434
library: addonName,
35-
libraryTarget: 'umd'
35+
libraryTarget: 'umd',
36+
// Force usage of globalThis instead of global / self. (This is cross-env compatible)
37+
globalObject: 'globalThis',
3638
},
3739
mode: 'production'
3840
};

addons/addon-serialize/src/SerializeAddon.test.ts

+10
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,16 @@ describe('SerializeAddon', () => {
138138
assert.equal((output.match(/<div><span>terminal<\/span><\/div>/g) || []).length, 1, output);
139139
});
140140

141+
it('basic terminal with html unsafe chars', async () => {
142+
await writeP(terminal, ' <a>&pi; ');
143+
terminal.select(1, 0, 7);
144+
145+
const output = serializeAddon.serializeAsHTML({
146+
onlySelection: true
147+
});
148+
assert.equal((output.match(/<div><span>&lt;a>&amp;pi;<\/span><\/div>/g) || []).length, 1, output);
149+
});
150+
141151
it('cells with bold styling', async () => {
142152
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');
143153

addons/addon-serialize/src/SerializeAddon.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ function constrain(value: number, low: number, high: number): number {
1414
return Math.max(low, Math.min(value, high));
1515
}
1616

17+
function escapeHTMLChar(c: string): string {
18+
switch (c) {
19+
case '&': return '&amp;';
20+
case '<': return '&lt;';
21+
}
22+
return c;
23+
}
24+
1725
// TODO: Refine this template class later
1826
abstract class BaseSerializeHandler {
1927
constructor(
@@ -669,7 +677,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
669677
if (isEmptyCell) {
670678
this._currentRow += ' ';
671679
} else {
672-
this._currentRow += cell.getChars();
680+
this._currentRow += escapeHTMLChar(cell.getChars());
673681
}
674682
}
675683

addons/addon-serialize/webpack.config.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ module.exports = {
3434
path: path.resolve('./lib'),
3535
library: addonName,
3636
libraryTarget: 'umd',
37-
globalObject: 'this'
37+
// Force usage of globalThis instead of global / self. (This is cross-env compatible)
38+
globalObject: 'globalThis',
3839
},
3940
mode: 'production'
4041
};

addons/addon-unicode-graphemes/webpack.config.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ module.exports = {
3232
filename: mainFile,
3333
path: path.resolve('./lib'),
3434
library: addonName,
35-
libraryTarget: 'umd'
35+
libraryTarget: 'umd',
36+
// Force usage of globalThis instead of global / self. (This is cross-env compatible)
37+
globalObject: 'globalThis',
3638
},
3739
mode: 'production'
3840
};

addons/addon-unicode11/webpack.config.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ module.exports = {
3333
path: path.resolve('./lib'),
3434
library: addonName,
3535
libraryTarget: 'umd',
36-
globalObject: 'this'
36+
// Force usage of globalThis instead of global / self. (This is cross-env compatible)
37+
globalObject: 'globalThis',
3738
},
3839
mode: 'production'
3940
};

addons/addon-web-links/webpack.config.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ module.exports = {
2525
filename: mainFile,
2626
path: path.resolve('./lib'),
2727
library: addonName,
28-
libraryTarget: 'umd'
28+
libraryTarget: 'umd',
29+
// Force usage of globalThis instead of global / self. (This is cross-env compatible)
30+
globalObject: 'globalThis',
2931
},
3032
mode: 'production'
3133
};

addons/addon-webgl/webpack.config.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ module.exports = {
3333
filename: mainFile,
3434
path: path.resolve('./lib'),
3535
library: addonName,
36-
libraryTarget: 'umd'
36+
libraryTarget: 'umd',
37+
// Force usage of globalThis instead of global / self. (This is cross-env compatible)
38+
globalObject: 'globalThis',
3739
},
3840
mode: 'production'
3941
};

src/browser/Viewport.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export class Viewport extends Disposable implements IViewport {
5151
target: -1
5252
};
5353

54+
private _ensureTimeout: number;
55+
5456
private readonly _onRequestScrollLines = this.register(new EventEmitter<{ amount: number, suppressScrollEvent: boolean }>());
5557
public readonly onRequestScrollLines = this._onRequestScrollLines.event;
5658

@@ -83,7 +85,7 @@ export class Viewport extends Disposable implements IViewport {
8385
this.register(this._optionsService.onSpecificOptionChange('scrollback', () => this.syncScrollArea()));
8486

8587
// Perform this async to ensure the ICharSizeService is ready.
86-
setTimeout(() => this.syncScrollArea());
88+
this._ensureTimeout = window.setTimeout(() => this.syncScrollArea());
8789
}
8890

8991
private _handleThemeChange(colors: ReadonlyColorSet): void {
@@ -405,4 +407,8 @@ export class Viewport extends Disposable implements IViewport {
405407
this._viewportElement.scrollTop += deltaY;
406408
return this._bubbleScroll(ev, deltaY);
407409
}
410+
411+
public dispose(): void {
412+
clearTimeout(this._ensureTimeout);
413+
}
408414
}

src/browser/renderer/dom/DomRenderer.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -343,18 +343,16 @@ export class DomRenderer extends Disposable implements IRenderer {
343343
}
344344

345345
this._selectionRenderModel.update(this._terminal, start, end, columnSelectMode);
346+
if (!this._selectionRenderModel.hasSelection) {
347+
return;
348+
}
346349

347350
// Translate from buffer position to viewport position
348351
const viewportStartRow = this._selectionRenderModel.viewportStartRow;
349352
const viewportEndRow = this._selectionRenderModel.viewportEndRow;
350353
const viewportCappedStartRow = this._selectionRenderModel.viewportCappedStartRow;
351354
const viewportCappedEndRow = this._selectionRenderModel.viewportCappedEndRow;
352355

353-
// No need to draw the selection
354-
if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) {
355-
return;
356-
}
357-
358356
// Create the selections
359357
const documentFragment = this._document.createDocumentFragment();
360358

src/browser/services/CoreBrowserService.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export class CoreBrowserService extends Disposable implements ICoreBrowserServic
1313

1414
private _isFocused = false;
1515
private _cachedIsFocused: boolean | undefined = undefined;
16-
private _screenDprMonitor = new ScreenDprMonitor(this._window);
16+
private _screenDprMonitor = this.register(new ScreenDprMonitor(this._window));
1717

1818
private readonly _onDprChange = this.register(new EventEmitter<number>());
1919
public readonly onDprChange = this._onDprChange.event;
@@ -31,8 +31,12 @@ export class CoreBrowserService extends Disposable implements ICoreBrowserServic
3131
this.register(this.onWindowChange(w => this._screenDprMonitor.setWindow(w)));
3232
this.register(forwardEvent(this._screenDprMonitor.onDprChange, this._onDprChange));
3333

34-
this._textarea.addEventListener('focus', () => this._isFocused = true);
35-
this._textarea.addEventListener('blur', () => this._isFocused = false);
34+
this.register(
35+
addDisposableDomListener(this._textarea, 'focus', () => (this._isFocused = true))
36+
);
37+
this.register(
38+
addDisposableDomListener(this._textarea, 'blur', () => (this._isFocused = false))
39+
);
3640
}
3741

3842
public get window(): Window & typeof globalThis {

src/common/InputHandler.test.ts

+31-2
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ import { Attributes, BgFlags, UnderlineStyle } from 'common/buffer/Constants';
1212
import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData';
1313
import { Params } from 'common/parser/Params';
1414
import { MockCoreService, MockBufferService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test';
15-
import { IBufferService, ICoreService } from 'common/services/Services';
15+
import { IBufferService, ICoreService, type IOscLinkService } from 'common/services/Services';
1616
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
1717
import { clone } from 'common/Clone';
1818
import { BufferService } from 'common/services/BufferService';
1919
import { CoreService } from 'common/services/CoreService';
20+
import { OscLinkService } from 'common/services/OscLinkService';
2021

2122

2223
function getCursor(bufferService: IBufferService): number[] {
@@ -59,15 +60,17 @@ describe('InputHandler', () => {
5960
let bufferService: IBufferService;
6061
let coreService: ICoreService;
6162
let optionsService: MockOptionsService;
63+
let oscLinkService: IOscLinkService;
6264
let inputHandler: TestInputHandler;
6365

6466
beforeEach(() => {
6567
optionsService = new MockOptionsService();
6668
bufferService = new BufferService(optionsService);
6769
bufferService.resize(80, 30);
6870
coreService = new CoreService(bufferService, new MockLogService(), optionsService);
71+
oscLinkService = new OscLinkService(bufferService);
6972

70-
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
73+
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, oscLinkService, new MockCoreMouseService(), new MockUnicodeService());
7174
});
7275

7376
describe('SL/SR/DECIC/DECDC', () => {
@@ -1982,6 +1985,32 @@ describe('InputHandler', () => {
19821985
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: 0, color: [170, 187, 204] }, { type: ColorRequestType.SET, index: 123, color: [0, 17, 34] }]]);
19831986
stack.length = 0;
19841987
});
1988+
it('8: hyperlink with id', async () => {
1989+
await inputHandler.parseP('\x1b]8;id=100;http://localhost:3000\x07');
1990+
assert.notStrictEqual(inputHandler.curAttrData.extended.urlId, 0);
1991+
assert.deepStrictEqual(
1992+
oscLinkService.getLinkData(inputHandler.curAttrData.extended.urlId),
1993+
{
1994+
id: '100',
1995+
uri: 'http://localhost:3000'
1996+
}
1997+
);
1998+
await inputHandler.parseP('\x1b]8;;\x07');
1999+
assert.strictEqual(inputHandler.curAttrData.extended.urlId, 0);
2000+
});
2001+
it('8: hyperlink with semi-colon', async () => {
2002+
await inputHandler.parseP('\x1b]8;;http://localhost:3000;abc=def\x07');
2003+
assert.notStrictEqual(inputHandler.curAttrData.extended.urlId, 0);
2004+
assert.deepStrictEqual(
2005+
oscLinkService.getLinkData(inputHandler.curAttrData.extended.urlId),
2006+
{
2007+
id: undefined,
2008+
uri: 'http://localhost:3000;abc=def'
2009+
}
2010+
);
2011+
await inputHandler.parseP('\x1b]8;;\x07');
2012+
assert.strictEqual(inputHandler.curAttrData.extended.urlId, 0);
2013+
});
19852014
it('104: restore events', async () => {
19862015
const stack: IColorEvent[] = [];
19872016
inputHandler.onColor(ev => stack.push(ev));

src/common/InputHandler.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -2972,14 +2972,18 @@ export class InputHandler extends Disposable implements IInputHandler {
29722972
* feedback. Use `OSC 8 ; ; BEL` to finish the current hyperlink.
29732973
*/
29742974
public setHyperlink(data: string): boolean {
2975-
const args = data.split(';');
2976-
if (args.length < 2) {
2977-
return false;
2975+
// Arg parsing is special cases to support unencoded semi-colons in the URIs (#4944)
2976+
const idx = data.indexOf(';');
2977+
if (idx === -1) {
2978+
// malformed sequence, just return as handled
2979+
return true;
29782980
}
2979-
if (args[1]) {
2980-
return this._createHyperlink(args[0], args[1]);
2981+
const id = data.slice(0, idx).trim();
2982+
const uri = data.slice(idx + 1);
2983+
if (uri) {
2984+
return this._createHyperlink(id, uri);
29812985
}
2982-
if (args[0].trim()) {
2986+
if (id.trim()) {
29832987
return false;
29842988
}
29852989
return this._finishHyperlink();

test/playwright/SharedRendererTests.ts

+14
Original file line numberDiff line numberDiff line change
@@ -1234,6 +1234,20 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
12341234
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, rows), [0, 0, 0, 255]);
12351235
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, rows, CellColorPosition.FIRST), [0, 0, 255, 255]);
12361236
});
1237+
test('#4917 The selection should not be displayed if it is not within the scope of the viewport.', async () => {
1238+
const theme: ITheme = {
1239+
selectionBackground: '#FF0000'
1240+
};
1241+
await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
1242+
for (let index = 0; index < 160; index++) {
1243+
await ctx.value.proxy.writeln(``);
1244+
}
1245+
await ctx.value.proxy.scrollToBottom();
1246+
const rows = await ctx.value.proxy.buffer.active.length;
1247+
await ctx.value.proxy.selectLines(rows - 1, rows - 1);
1248+
await ctx.value.proxy.scrollLines(-2);
1249+
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]);
1250+
});
12371251
});
12381252
}
12391253

0 commit comments

Comments
 (0)