Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion addons/addon-image/src/SixelHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
const height = this._dec.height;

// partial fix for https://github.com/jerch/xterm-addon-image/issues/37
if (!width || ! height) {
if (!width || !height) {
if (height) {
this._storage.advanceCursor(height);
}
Expand Down
4 changes: 2 additions & 2 deletions addons/addon-ligatures/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"package": "webpack",
"pretest": "npm run build",
"test": "nyc mocha out/**/*.test.js",
"prepublish": "npm run package"
"prepublishOnly": "npm run package"
},
"keywords": [
"font",
Expand All @@ -37,7 +37,7 @@
"devDependencies": {
"@types/lru-cache": "^5.1.0",
"@types/opentype.js": "^0.7.0",
"axios": "^1.15.0",
"axios": "^1.15.2",
"font-finder": "^1.1.0",
"mkdirp": "0.5.5",
"yauzl": "^3.2.1"
Expand Down
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export default tseslint.config(
singleline: { delimiter: 'comma', requireLast: false }
}],
'@stylistic/type-annotation-spacing': 'warn',
'@stylistic/space-unary-ops': 'warn',

'@typescript-eslint/array-type': ['warn', { default: 'array', readonly: 'generic' }],
'@typescript-eslint/consistent-type-assertions': 'warn',
Expand Down
17 changes: 9 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/browser/services/MouseCoordsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class MouseCoordsService implements IMouseCoordsService {

public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined {
return getCoords(
window,
getWindow(element),
event,
element,
colCount,
Expand Down
7 changes: 6 additions & 1 deletion src/browser/services/RenderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class RenderService extends Disposable implements IRenderService {
private _renderDebouncer: IRenderDebouncerWithCallback;
private _pausedResizeTask: DebouncedIdleTask;
private _observerDisposable = this._register(new MutableDisposable());
private _intersectionObserver: IntersectionObserver | undefined;

private _isPaused: boolean = false;
private _needsFullRefresh: boolean = false;
Expand Down Expand Up @@ -127,8 +128,12 @@ export class RenderService extends Disposable implements IRenderService {
// and resume based on terminal visibility if so
if ('IntersectionObserver' in w) {
const observer = new w.IntersectionObserver(e => this._handleIntersectionChange(e[e.length - 1]), { threshold: 0 });
this._observerDisposable.value = toDisposable(() => {
this._intersectionObserver?.disconnect();
this._intersectionObserver = undefined;
});
this._intersectionObserver = observer;
observer.observe(screenElement);
this._observerDisposable.value = toDisposable(() => observer.disconnect());
}
}

Expand Down
36 changes: 36 additions & 0 deletions src/common/Async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,42 @@ export class TimeoutTimer implements IDisposable {
}
}

/**
* Schedules a single runner on the microtask queue. Unlike {@link TimeoutTimer}, a scheduled
* microtask cannot be unqueued; {@link cancel} prevents the runner from executing if it has not
* run yet.
*/
export class MicrotaskTimer implements IDisposable {
private _isScheduled = false;
private _isDisposed = false;

public dispose(): void {
this.cancel();
this._isDisposed = true;
}

public cancel(): void {
this._isScheduled = false;
}

public set(runner: () => void): void {
if (this._isDisposed) {
throw new Error('Calling set on a disposed MicrotaskTimer');
}
if (this._isScheduled) {
return;
}
this._isScheduled = true;
queueMicrotask(() => {
if (!this._isScheduled) {
return;
}
this._isScheduled = false;
runner();
});
}
}

export class IntervalTimer implements IDisposable {
private _disposable: IDisposable | undefined;
private _isDisposed = false;
Expand Down
114 changes: 107 additions & 7 deletions src/common/services/DecorationService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
*/

import { assert } from 'chai';
import { DecorationService } from './DecorationService';
import { DecorationLineCache, DecorationService } from './DecorationService';
import { IMarker } from 'common/Types';
import { Disposable } from 'common/Lifecycle';
import { Emitter } from 'common/Event';
import { MockLogService } from 'common/TestUtils.test';
import { MockLogService, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
import { Buffer } from 'common/buffer/Buffer';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';

function createFakeMarker(line: number): IMarker {
return Object.freeze(new class extends Disposable {
Expand All @@ -19,11 +21,16 @@ function createFakeMarker(line: number): IMarker {
}());
}

function createDecorationService(): DecorationService {
const bufferService = new MockBufferService(80, 24, new MockOptionsService());
return new DecorationService(new MockLogService(), bufferService);
}

const fakeMarker: IMarker = createFakeMarker(1);

describe('DecorationService', () => {
it('should set isDisposed to true after dispose', () => {
const service = new DecorationService(new MockLogService());
const service = createDecorationService();
const decoration = service.registerDecoration({
marker: fakeMarker
});
Expand All @@ -35,7 +42,7 @@ describe('DecorationService', () => {

describe('forEachDecorationAtCell', () => {
it('should find decoration at its marker line', () => {
const service = new DecorationService(new MockLogService());
const service = createDecorationService();
const decoration = service.registerDecoration({
marker: createFakeMarker(5),
width: 10
Expand All @@ -48,7 +55,7 @@ describe('DecorationService', () => {
});

it('should find decoration with height > 1 on subsequent lines', () => {
const service = new DecorationService(new MockLogService());
const service = createDecorationService();
const decoration = service.registerDecoration({
marker: createFakeMarker(5),
width: 10,
Expand All @@ -74,7 +81,7 @@ describe('DecorationService', () => {
});

it('should not find decoration outside its x range', () => {
const service = new DecorationService(new MockLogService());
const service = createDecorationService();
const decoration = service.registerDecoration({
marker: createFakeMarker(5),
x: 5,
Expand All @@ -99,11 +106,35 @@ describe('DecorationService', () => {
service.forEachDecorationAtCell(8, 5, undefined, d => foundAtX8.push(d));
assert.strictEqual(foundAtX8.length, 0);
});

it('should find multi-line decoration when single-line decorations exist on other lines', () => {
const bufferService = new MockBufferService(80, 24, new MockOptionsService());
const serviceWithBuffer = new DecorationService(new MockLogService(), bufferService);
const buffer = bufferService.buffer;
(buffer as Buffer).fillViewportRows();

for (let i = 0; i < 100; i++) {
serviceWithBuffer.registerDecoration({
marker: buffer.addMarker(i),
width: 5
});
}
const multiLine = serviceWithBuffer.registerDecoration({
marker: buffer.addMarker(10),
width: 10,
height: 3
});
assert.ok(multiLine);

const found: typeof multiLine[] = [];
serviceWithBuffer.forEachDecorationAtCell(0, 11, undefined, d => found.push(d));
assert.include(found, multiLine);
});
});

describe('getDecorationsAtCell', () => {
it('should find decoration with height > 1 on subsequent lines', () => {
const service = new DecorationService(new MockLogService());
const service = createDecorationService();
const decoration = service.registerDecoration({
marker: createFakeMarker(5),
width: 10,
Expand All @@ -117,4 +148,73 @@ describe('DecorationService', () => {
assert.strictEqual([...service.getDecorationsAtCell(0, 8)].length, 0);
});
});

describe('DecorationLineCache', () => {
it('should return undefined for lines with no indexed decorations', () => {
const cache = new DecorationLineCache();
assert.isUndefined(cache.getDecorationsOnLine(0));
});
});

describe('line index maintenance', () => {
it('should keep lookups correct after buffer trim', () => {
const bufferService = new MockBufferService(80, 5, new MockOptionsService({ scrollback: 0 }));
const service = new DecorationService(new MockLogService(), bufferService);
const buffer = bufferService.buffer;
(buffer as Buffer).fillViewportRows();

const marker = buffer.addMarker(buffer.lines.length - 1);
const decoration = service.registerDecoration({ marker, width: 10 });
assert.ok(decoration);

buffer.lines.onTrimEmitter.fire(1);

const found: typeof decoration[] = [];
service.forEachDecorationAtCell(0, marker.line, undefined, d => found.push(d));
assert.strictEqual(found.length, 1);
});

it('should remove decoration from line index when marker is trimmed off buffer', () => {
const bufferService = new MockBufferService(80, 5, new MockOptionsService({ scrollback: 0 }));
const service = new DecorationService(new MockLogService(), bufferService);
const buffer = bufferService.buffer;
(buffer as Buffer).fillViewportRows();

const marker = buffer.addMarker(0);
const decoration = service.registerDecoration({ marker, width: 10 });
assert.ok(decoration);

buffer.lines.onTrimEmitter.fire(1);
assert.isTrue(marker.isDisposed);
assert.isTrue(decoration!.isDisposed);

const found: typeof decoration[] = [];
service.forEachDecorationAtCell(0, 0, undefined, d => found.push(d));
assert.strictEqual(found.length, 0);
});

it('should keep multi-line decoration indexed after line insert', async () => {
const bufferService = new MockBufferService(80, 10, new MockOptionsService({ scrollback: 100 }));
const service = new DecorationService(new MockLogService(), bufferService);
const buffer = bufferService.buffer;
(buffer as Buffer).fillViewportRows();

const marker = buffer.addMarker(3);
const decoration = service.registerDecoration({ marker, width: 10, height: 3 });
assert.ok(decoration);

buffer.lines.splice(5, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
await new Promise<void>(resolve => queueMicrotask(resolve));

const foundOnSpan: typeof decoration[] = [];
for (let line = marker.line; line < marker.line + 3; line++) {
service.forEachDecorationAtCell(0, line, undefined, d => foundOnSpan.push(d));
}
assert.include(foundOnSpan, decoration);

const foundOutsideSpan: typeof decoration[] = [];
service.forEachDecorationAtCell(0, marker.line + 3, undefined, d => foundOutsideSpan.push(d));
assert.strictEqual(foundOutsideSpan.length, 0);
});
});
});
Loading
Loading