Merge branch 'master' into cursor/headless-clear-markers-ccc0
This commit is contained in:
@@ -158,6 +158,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._isAttached = this._core.screenElement!.isConnected;
|
||||
|
||||
this._register(toDisposable(() => {
|
||||
clearTimeout(this._contextRestorationTimeout);
|
||||
this._contextRestorationTimeout = undefined;
|
||||
for (const l of this._renderLayers) {
|
||||
l.dispose();
|
||||
}
|
||||
|
||||
@@ -563,8 +563,9 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
try {
|
||||
this._onWillOpen.fire(this.element);
|
||||
} catch (e) {
|
||||
this._logService.error('onWillOpen handler threw an exception', e);
|
||||
}
|
||||
catch { /* fails to load addon for some reason */ }
|
||||
if (!this._renderService.hasRenderer()) {
|
||||
this._renderService.setRenderer(this._createRenderer());
|
||||
}
|
||||
|
||||
@@ -43,6 +43,20 @@ describe('Linkifier2', () => {
|
||||
},
|
||||
activate: () => { }
|
||||
};
|
||||
const multiLineLink: ILink = {
|
||||
text: 'foo',
|
||||
range: {
|
||||
start: {
|
||||
x: 2,
|
||||
y: 1
|
||||
},
|
||||
end: {
|
||||
x: 4,
|
||||
y: 2
|
||||
}
|
||||
},
|
||||
activate: () => { }
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const dom = new jsdom.JSDOM();
|
||||
@@ -86,4 +100,30 @@ describe('Linkifier2', () => {
|
||||
linkifier.linkLeave({ classList: { add: () => { } } } as any, link, {} as any);
|
||||
});
|
||||
|
||||
it('onShowLinkUnderline event range is correct for wrapped links', done => {
|
||||
linkifier.onShowLinkUnderline(e => {
|
||||
assert.equal(multiLineLink.range.start.x - 1, e.x1);
|
||||
assert.equal(multiLineLink.range.start.y - 1, e.y1);
|
||||
assert.equal(multiLineLink.range.end.x, e.x2);
|
||||
assert.equal(multiLineLink.range.end.y - 1, e.y2);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
linkifier.linkHover({ classList: { add: () => { } } } as any, multiLineLink, {} as any);
|
||||
});
|
||||
|
||||
it('onHideLinkUnderline event range is correct for wrapped links', done => {
|
||||
linkifier.onHideLinkUnderline(e => {
|
||||
assert.equal(multiLineLink.range.start.x - 1, e.x1);
|
||||
assert.equal(multiLineLink.range.start.y - 1, e.y1);
|
||||
assert.equal(multiLineLink.range.end.x, e.x2);
|
||||
assert.equal(multiLineLink.range.end.y - 1, e.y2);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
linkifier.linkLeave({ classList: { add: () => { } } } as any, multiLineLink, {} as any);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { OscLinkProvider } from 'browser/OscLinkProvider';
|
||||
import { ILink } from 'browser/Types';
|
||||
import { createCellData, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
|
||||
import { IBufferService, IOscLinkService } from 'common/services/Services';
|
||||
import { IBufferLine, IOscLinkData } from 'common/Types';
|
||||
|
||||
class TestOscLinkService implements IOscLinkService {
|
||||
public serviceBrand: any;
|
||||
public registerLink(_linkData: IOscLinkData): number { return 0; }
|
||||
public addLineToLink(_linkId: number, _y: number): void { }
|
||||
public getLinkData(linkId: number): IOscLinkData | undefined {
|
||||
return { uri: `https://example.com/${linkId}` };
|
||||
}
|
||||
}
|
||||
|
||||
function setText(line: IBufferLine | undefined, x: number, text: string): void {
|
||||
if (!line) {
|
||||
throw new Error('Missing buffer line');
|
||||
}
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
line.setCell(x + i, createCellData(0, text[i], 1));
|
||||
}
|
||||
}
|
||||
|
||||
function setUrl(line: IBufferLine | undefined, x: number, text: string, linkId: number): void {
|
||||
if (!line) {
|
||||
throw new Error('Missing buffer line');
|
||||
}
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const cell = createCellData(0, text[i], 1);
|
||||
cell.extended.urlId = linkId;
|
||||
cell.updateExtended();
|
||||
line.setCell(x + i, cell);
|
||||
}
|
||||
}
|
||||
|
||||
function getLinks(provider: OscLinkProvider, y: number): Promise<ILink[]> {
|
||||
return new Promise(resolve => provider.provideLinks(y, links => resolve(links ?? [])));
|
||||
}
|
||||
|
||||
describe('OscLinkProvider', () => {
|
||||
let bufferService: IBufferService;
|
||||
let provider: OscLinkProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
const optionsService = new MockOptionsService();
|
||||
bufferService = new MockBufferService(5, 5, optionsService);
|
||||
provider = new OscLinkProvider(bufferService, optionsService, new TestOscLinkService());
|
||||
});
|
||||
|
||||
it('expands a wrapped link range backward to the previous line', async () => {
|
||||
const line1 = bufferService.buffer.lines.get(0);
|
||||
const line2 = bufferService.buffer.lines.get(1);
|
||||
setText(line1, 0, 'aa');
|
||||
setUrl(line1, 2, 'bbb', 1);
|
||||
setUrl(line2, 0, 'cccc', 1);
|
||||
setText(line2, 4, 'x');
|
||||
line2!.isWrapped = true;
|
||||
|
||||
const links = await getLinks(provider, 2);
|
||||
assert.lengthOf(links, 1);
|
||||
assert.deepEqual(links[0].range, {
|
||||
start: { x: 3, y: 1 },
|
||||
end: { x: 4, y: 2 }
|
||||
});
|
||||
});
|
||||
|
||||
it('expands a wrapped link range forward when a link ends at line boundary', async () => {
|
||||
const line1 = bufferService.buffer.lines.get(0);
|
||||
const line2 = bufferService.buffer.lines.get(1);
|
||||
setUrl(line1, 0, 'aaaaa', 1);
|
||||
setUrl(line2, 0, 'bb', 1);
|
||||
setText(line2, 2, 'ccc');
|
||||
line2!.isWrapped = true;
|
||||
|
||||
const links = await getLinks(provider, 1);
|
||||
assert.lengthOf(links, 1);
|
||||
assert.deepEqual(links[0].range, {
|
||||
start: { x: 1, y: 1 },
|
||||
end: { x: 2, y: 2 }
|
||||
});
|
||||
});
|
||||
|
||||
it('does not merge wrapped links with different url ids', async () => {
|
||||
const line1 = bufferService.buffer.lines.get(0);
|
||||
const line2 = bufferService.buffer.lines.get(1);
|
||||
setUrl(line1, 0, 'aaaaa', 1);
|
||||
setUrl(line2, 0, 'bbb', 2);
|
||||
setText(line2, 3, 'cc');
|
||||
line2!.isWrapped = true;
|
||||
|
||||
const links = await getLinks(provider, 1);
|
||||
assert.lengthOf(links, 1);
|
||||
assert.deepEqual(links[0].range, {
|
||||
start: { x: 1, y: 1 },
|
||||
end: { x: 5, y: 1 }
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@
|
||||
import { IBufferRange, ILink } from 'browser/Types';
|
||||
import { ILinkProvider } from 'browser/services/Services';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
|
||||
|
||||
export class OscLinkProvider implements ILinkProvider {
|
||||
@@ -57,19 +58,8 @@ export class OscLinkProvider implements ILinkProvider {
|
||||
if (finishLink || (currentStart !== -1 && x === lineLength - 1)) {
|
||||
const text = this._oscLinkService.getLinkData(currentLinkId)?.uri;
|
||||
if (text) {
|
||||
// These ranges are 1-based
|
||||
const range: IBufferRange = {
|
||||
start: {
|
||||
x: currentStart + 1,
|
||||
y
|
||||
},
|
||||
end: {
|
||||
// Offset end x if it's a link that ends on the last cell in the line
|
||||
x: x + (!finishLink && x === lineLength - 1 ? 1 : 0),
|
||||
y
|
||||
}
|
||||
};
|
||||
|
||||
const endX = x + (!finishLink && x === lineLength - 1 ? 1 : 0);
|
||||
const range = this._getRangeWithLineWrap(y, currentStart, endX, currentLinkId);
|
||||
let ignoreLink = false;
|
||||
if (!linkHandler?.allowNonHttpProtocols) {
|
||||
try {
|
||||
@@ -111,6 +101,82 @@ export class OscLinkProvider implements ILinkProvider {
|
||||
// id
|
||||
callback(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a single-line OSC 8 range to a contiguous wrapped range for the same link id.
|
||||
*/
|
||||
private _getRangeWithLineWrap(y: number, startX: number, endX: number, linkId: number): IBufferRange {
|
||||
let startY = y;
|
||||
let finalStartX = startX;
|
||||
let endY = y;
|
||||
let finalEndX = endX;
|
||||
|
||||
// Expand upward only when this segment starts at column 0 and the current line is wrapped.
|
||||
while (finalStartX === 0) {
|
||||
const currentLine = this._bufferService.buffer.lines.get(startY - 1);
|
||||
if (!currentLine?.isWrapped) {
|
||||
break;
|
||||
}
|
||||
const previousLine = this._bufferService.buffer.lines.get(startY - 2);
|
||||
if (!previousLine) {
|
||||
break;
|
||||
}
|
||||
const previousLineLength = previousLine.getTrimmedLength();
|
||||
if (previousLineLength === 0 || !this._hasUrlId(previousLine, previousLineLength - 1, linkId)) {
|
||||
break;
|
||||
}
|
||||
let previousStartX = previousLineLength - 1;
|
||||
while (previousStartX > 0 && this._hasUrlId(previousLine, previousStartX - 1, linkId)) {
|
||||
previousStartX--;
|
||||
}
|
||||
startY--;
|
||||
finalStartX = previousStartX;
|
||||
}
|
||||
|
||||
// Expand downward only when this segment reaches trimmed EOL and the next line is wrapped.
|
||||
while (true) {
|
||||
const currentLine = this._bufferService.buffer.lines.get(endY - 1);
|
||||
if (!currentLine) {
|
||||
break;
|
||||
}
|
||||
const currentLineLength = currentLine.getTrimmedLength();
|
||||
if (finalEndX !== currentLineLength) {
|
||||
break;
|
||||
}
|
||||
const nextLine = this._bufferService.buffer.lines.get(endY);
|
||||
if (!nextLine?.isWrapped) {
|
||||
break;
|
||||
}
|
||||
const nextLineLength = nextLine.getTrimmedLength();
|
||||
if (nextLineLength === 0 || !this._hasUrlId(nextLine, 0, linkId)) {
|
||||
break;
|
||||
}
|
||||
let nextEndX = 1;
|
||||
while (nextEndX < nextLineLength && this._hasUrlId(nextLine, nextEndX, linkId)) {
|
||||
nextEndX++;
|
||||
}
|
||||
endY++;
|
||||
finalEndX = nextEndX;
|
||||
}
|
||||
|
||||
// IBufferRange uses 1-based coordinates.
|
||||
return {
|
||||
start: {
|
||||
x: finalStartX + 1,
|
||||
y: startY
|
||||
},
|
||||
end: {
|
||||
x: finalEndX,
|
||||
y: endY
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _hasUrlId(line: IBufferLine, x: number, linkId: number): boolean {
|
||||
const cell = this._workCell;
|
||||
line.loadCell(x, cell);
|
||||
return !!cell.hasExtendedAttrs() && cell.extended.urlId === linkId;
|
||||
}
|
||||
}
|
||||
|
||||
function defaultActivate(e: MouseEvent, uri: string): void {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { moveToCellSequence } from 'browser/input/MoveToCell';
|
||||
import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
||||
import { ICoreBrowserService, IMouseCoordsService, IRenderService, ISelectionService } from 'browser/services/Services';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
import * as Browser from 'common/Platform';
|
||||
import { IBufferLine, ICellData, IDisposable } from 'common/Types';
|
||||
import { getRangeLength } from 'common/buffer/BufferRange';
|
||||
@@ -102,7 +102,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
|
||||
private _mouseMoveListener: EventListener;
|
||||
private _mouseUpListener: EventListener;
|
||||
private _trimListener: IDisposable;
|
||||
private readonly _trimListener = this._register(new MutableDisposable<IDisposable>());
|
||||
private _workCell: CellData = new CellData();
|
||||
|
||||
private _mouseDownTimeStamp: number = 0;
|
||||
@@ -140,7 +140,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
this.clearSelection();
|
||||
}
|
||||
});
|
||||
this._trimListener = this._bufferService.buffer.lines.onTrim(amount => this._handleTrim(amount));
|
||||
this._trimListener.value = this._bufferService.buffer.lines.onTrim(amount => this._handleTrim(amount));
|
||||
this._register(this._bufferService.buffers.onBufferActivate(e => this._handleBufferActivate(e)));
|
||||
|
||||
this.enable();
|
||||
@@ -769,8 +769,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
// reverseIndex) and delete in a splice is only ever used when the same
|
||||
// number of elements was just added. Given this is could actually be
|
||||
// beneficial to leave the selection as is for these cases.
|
||||
this._trimListener.dispose();
|
||||
this._trimListener = e.activeBuffer.lines.onTrim(amount => this._handleTrim(amount));
|
||||
this._trimListener.value = e.activeBuffer.lines.onTrim(amount => this._handleTrim(amount));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { clone } from 'common/Clone';
|
||||
|
||||
describe('clone', () => {
|
||||
it('should clone simple objects', () => {
|
||||
const test = {
|
||||
a: 1,
|
||||
b: 2
|
||||
};
|
||||
|
||||
assert.deepEqual(clone(test), { a: 1, b: 2 });
|
||||
});
|
||||
|
||||
it('should clone nested objects', () => {
|
||||
const test = {
|
||||
bar: {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: {
|
||||
foo: 'bar'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
assert.deepEqual(clone(test), {
|
||||
bar: {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: {
|
||||
foo: 'bar'
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should clone array values', () => {
|
||||
const test = {
|
||||
a: [1, 2, 3],
|
||||
b: [1, null, 'test', { foo: 'bar' }]
|
||||
};
|
||||
|
||||
assert.deepEqual(clone(test), {
|
||||
a: [1, 2, 3],
|
||||
b: [1, null, 'test', { foo: 'bar' }]
|
||||
});
|
||||
});
|
||||
|
||||
it('should stop mutation from occuring on the original object', () => {
|
||||
const test = {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: {
|
||||
foo: 'bar'
|
||||
}
|
||||
};
|
||||
|
||||
const cloned = clone(test);
|
||||
|
||||
test.a = 5;
|
||||
test.c.foo = 'barbaz';
|
||||
|
||||
assert.deepEqual(cloned, {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: {
|
||||
foo: 'bar'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should clone to a maximum depth of 5 by default', () => {
|
||||
const test = {
|
||||
a: {
|
||||
b: {
|
||||
c: {
|
||||
d: {
|
||||
e: {
|
||||
f: 'foo'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cloned = clone(test);
|
||||
|
||||
test.a.b.c.d.e.f = 'bar';
|
||||
|
||||
// The values at a greater depth then 5 should not be cloned
|
||||
assert.equal((cloned as any).a.b.c.d.e.f, 'bar');
|
||||
});
|
||||
|
||||
it('should allow an optional maximum depth to be set', () => {
|
||||
const test = {
|
||||
a: {
|
||||
b: {
|
||||
c: 'foo'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cloned = clone(test, 2);
|
||||
|
||||
test.a.b.c = 'bar';
|
||||
|
||||
// The values at a greater depth then 2 should not be cloned
|
||||
assert.equal((cloned as any).a.b.c, 'bar');
|
||||
});
|
||||
|
||||
it('should not throw when cloning a recursive reference', () => {
|
||||
const test = {
|
||||
a: {
|
||||
b: {
|
||||
c: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test.a.b.c = test;
|
||||
|
||||
assert.doesNotThrow(() => clone(test));
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/*
|
||||
* A simple utility for cloning values
|
||||
*/
|
||||
export function clone<T>(val: T, depth: number = 5): T {
|
||||
if (typeof val !== 'object') {
|
||||
return val;
|
||||
}
|
||||
|
||||
// If we're cloning an array, use an array as the base, otherwise use an object
|
||||
const clonedObject: any = Array.isArray(val) ? [] : {};
|
||||
|
||||
for (const key in val) {
|
||||
// Recursively clone eack item unless we're at the maximum depth
|
||||
clonedObject[key] = depth <= 1 ? val[key] : (val[key] && clone(val[key], depth - 1));
|
||||
}
|
||||
|
||||
return clonedObject as T;
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import { Params } from 'common/parser/Params';
|
||||
import { MockCoreService, MockBufferService, MockOptionsService, MockLogService, MockMouseStateService, MockCharsetService, MockUnicodeService, MockOscLinkService, extendedAttributes } from 'common/TestUtils.test';
|
||||
import { IBufferService, ICoreService, type IOscLinkService } from 'common/services/Services';
|
||||
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
|
||||
import { clone } from 'common/Clone';
|
||||
import { BufferService } from 'common/services/BufferService';
|
||||
import { CoreService } from 'common/services/CoreService';
|
||||
import { OscLinkService } from 'common/services/OscLinkService';
|
||||
@@ -227,32 +226,32 @@ describe('InputHandler', () => {
|
||||
assert.equal(coreService.decPrivateModes.cursorStyle, undefined);
|
||||
assert.equal(coreService.decPrivateModes.cursorBlink, undefined);
|
||||
|
||||
optionsService.options = clone(DEFAULT_OPTIONS);
|
||||
optionsService.options = structuredClone(DEFAULT_OPTIONS);
|
||||
inputHandler.setCursorStyle(Params.fromArray([1]));
|
||||
assert.equal(coreService.decPrivateModes.cursorStyle, 'block');
|
||||
assert.equal(coreService.decPrivateModes.cursorBlink, true);
|
||||
|
||||
optionsService.options = clone(DEFAULT_OPTIONS);
|
||||
optionsService.options = structuredClone(DEFAULT_OPTIONS);
|
||||
inputHandler.setCursorStyle(Params.fromArray([2]));
|
||||
assert.equal(coreService.decPrivateModes.cursorStyle, 'block');
|
||||
assert.equal(coreService.decPrivateModes.cursorBlink, false);
|
||||
|
||||
optionsService.options = clone(DEFAULT_OPTIONS);
|
||||
optionsService.options = structuredClone(DEFAULT_OPTIONS);
|
||||
inputHandler.setCursorStyle(Params.fromArray([3]));
|
||||
assert.equal(coreService.decPrivateModes.cursorStyle, 'underline');
|
||||
assert.equal(coreService.decPrivateModes.cursorBlink, true);
|
||||
|
||||
optionsService.options = clone(DEFAULT_OPTIONS);
|
||||
optionsService.options = structuredClone(DEFAULT_OPTIONS);
|
||||
inputHandler.setCursorStyle(Params.fromArray([4]));
|
||||
assert.equal(coreService.decPrivateModes.cursorStyle, 'underline');
|
||||
assert.equal(coreService.decPrivateModes.cursorBlink, false);
|
||||
|
||||
optionsService.options = clone(DEFAULT_OPTIONS);
|
||||
optionsService.options = structuredClone(DEFAULT_OPTIONS);
|
||||
inputHandler.setCursorStyle(Params.fromArray([5]));
|
||||
assert.equal(coreService.decPrivateModes.cursorStyle, 'bar');
|
||||
assert.equal(coreService.decPrivateModes.cursorBlink, true);
|
||||
|
||||
optionsService.options = clone(DEFAULT_OPTIONS);
|
||||
optionsService.options = structuredClone(DEFAULT_OPTIONS);
|
||||
inputHandler.setCursorStyle(Params.fromArray([6]));
|
||||
assert.equal(coreService.decPrivateModes.cursorStyle, 'bar');
|
||||
assert.equal(coreService.decPrivateModes.cursorBlink, false);
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IMouseStateService, ICharsetService, UnicodeCharProperties, UnicodeCharWidth, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService, type IBufferResizeEvent } from 'common/services/Services';
|
||||
import { UnicodeService } from 'common/services/UnicodeService';
|
||||
import { clone } from 'common/Clone';
|
||||
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
|
||||
import { IBufferSet, IBuffer } from 'common/buffer/Types';
|
||||
import { BufferSet } from 'common/buffer/BufferSet';
|
||||
@@ -157,7 +156,7 @@ export class MockLogService implements ILogService {
|
||||
|
||||
export class MockOptionsService implements IOptionsService {
|
||||
public serviceBrand: any;
|
||||
public readonly rawOptions: Required<ITerminalOptions> = clone(DEFAULT_OPTIONS);
|
||||
public readonly rawOptions: Required<ITerminalOptions> = structuredClone(DEFAULT_OPTIONS);
|
||||
public options: Required<ITerminalOptions> = this.rawOptions;
|
||||
public onOptionChange: IEvent<keyof ITerminalOptions> = new Emitter<keyof ITerminalOptions>().event;
|
||||
constructor(testOptions?: Partial<ITerminalOptions>) {
|
||||
|
||||
@@ -125,5 +125,20 @@ describe('WriteBuffer', () => {
|
||||
assert.deepEqual(stack, []);
|
||||
assert.deepEqual(cbStack, []);
|
||||
});
|
||||
it('flushSync fires onWriteParsed', () => {
|
||||
let parsed = 0;
|
||||
wb.onWriteParsed(() => parsed++);
|
||||
wb.write('a');
|
||||
wb.write('b');
|
||||
assert.equal(parsed, 0);
|
||||
wb.flushSync();
|
||||
assert.equal(parsed, 1);
|
||||
});
|
||||
it('flushSync with no pending writes does not fire onWriteParsed', () => {
|
||||
let parsed = 0;
|
||||
wb.onWriteParsed(() => parsed++);
|
||||
wb.flushSync();
|
||||
assert.equal(parsed, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,7 +71,9 @@ export class WriteBuffer extends Disposable {
|
||||
|
||||
// Process all pending chunks synchronously
|
||||
let chunk: string | Uint8Array | undefined;
|
||||
let didProcess = false;
|
||||
while (chunk = this._writeBuffer.shift()) {
|
||||
didProcess = true;
|
||||
this._action(chunk);
|
||||
const cb = this._callbacks.shift();
|
||||
if (cb) cb();
|
||||
@@ -84,6 +86,9 @@ export class WriteBuffer extends Disposable {
|
||||
this._callbacks.length = 0;
|
||||
|
||||
this._isSyncWriting = false;
|
||||
if (didProcess) {
|
||||
this._onWriteParsed.fire();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { clone } from 'common/Clone';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IDecPrivateModes, IKittyKeyboardState, IModes } from 'common/Types';
|
||||
import { IBufferService, ICoreService, ILogService, IOptionsService } from 'common/services/Services';
|
||||
@@ -61,14 +60,14 @@ export class CoreService extends Disposable implements ICoreService {
|
||||
) {
|
||||
super();
|
||||
this.isCursorInitialized = _optionsService.rawOptions.showCursorImmediately ?? false;
|
||||
this.modes = clone(DEFAULT_MODES);
|
||||
this.decPrivateModes = clone(DEFAULT_DEC_PRIVATE_MODES);
|
||||
this.modes = structuredClone(DEFAULT_MODES);
|
||||
this.decPrivateModes = structuredClone(DEFAULT_DEC_PRIVATE_MODES);
|
||||
this.kittyKeyboard = DEFAULT_KITTY_KEYBOARD_STATE();
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.modes = clone(DEFAULT_MODES);
|
||||
this.decPrivateModes = clone(DEFAULT_DEC_PRIVATE_MODES);
|
||||
this.modes = structuredClone(DEFAULT_MODES);
|
||||
this.decPrivateModes = structuredClone(DEFAULT_DEC_PRIVATE_MODES);
|
||||
this.kittyKeyboard = DEFAULT_KITTY_KEYBOARD_STATE();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user