Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

On buffer clear, dispose of markers #3625

Closed
wants to merge 16 commits into from
7 changes: 4 additions & 3 deletions addons/xterm-addon-webgl/src/WebglRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ 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 { Terminal, IEvent } from 'xterm';
import { Terminal, IEvent, IBufferDecorationOptions, IDecoration } from 'xterm';
import { IRenderLayer } from './renderLayer/Types';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
import { ITerminal, IColorSet } from 'browser/Types';
Expand All @@ -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 { IBufferService } from 'common/services/Services';

export class WebglRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
Expand Down Expand Up @@ -57,10 +58,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
super();

this._core = (this._terminal as any)._core;

this._renderLayers = [
new LinkRenderLayer(this._core.screenElement!, 2, this._colors, this._core),
new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._colors, this._core, this._onRequestRedraw)
// new DecorationRenderLayer(this._core.screenElement!, 3, this._colors, this._id, this._onRequestRedraw)
];
this.dimensions = {
scaledCharWidth: 0,
Expand Down Expand Up @@ -115,7 +116,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
public get textureAtlas(): HTMLCanvasElement | undefined {
return this._charAtlas?.cacheCanvas;
}

public setColors(colors: IColorSet): void {
this._colors = colors;
// Clear layers and force a full render
Expand Down
5 changes: 5 additions & 0 deletions css/xterm.css
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,8 @@
.xterm-strikethrough {
text-decoration: line-through;
}

.xterm-screen canvas .xterm-decoration {
z-index: 6;
position: absolute;
}
20 changes: 18 additions & 2 deletions src/browser/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import * as Strings from 'browser/LocalizableStrings';
import { SoundService } from 'browser/services/SoundService';
import { MouseZoneManager } from 'browser/MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager';
import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider } from 'xterm';
import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider, IBufferDecorationOptions, IDecoration } from 'xterm';
import { DomRenderer } from 'browser/renderer/dom/DomRenderer';
import { KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IColorEvent, ColorIndex, ColorRequestType } from 'common/Types';
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
Expand All @@ -55,6 +55,7 @@ import { CoreTerminal } from 'common/CoreTerminal';
import { color, rgba } from 'browser/Color';
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
import { toRgbString } from 'common/input/XParseColor';
import { DecorationsService, IDecorationsService } from 'browser/services/DecorationsService';

// Let it work inside Node.js for automated testing purposes.
const document: Document = (typeof window !== 'undefined') ? window.document : null as any;
Expand All @@ -80,6 +81,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
private _charSizeService: ICharSizeService | undefined;
private _mouseService: IMouseService | undefined;
private _renderService: IRenderService | undefined;
private _decorationsService: IDecorationsService | undefined;
private _characterJoinerService: ICharacterJoinerService | undefined;
private _selectionService: ISelectionService | undefined;
private _soundService: ISoundService | undefined;
Expand Down Expand Up @@ -513,6 +515,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.register(this._renderService.onRenderedBufferChange(e => this._onRender.fire(e)));
this.onResize(e => this._renderService!.resize(e.cols, e.rows));

this._decorationsService = this.register(this._instantiationService.createInstance(DecorationsService, this.screenElement));

this._compositionView = document.createElement('div');
this._compositionView.classList.add('composition-view');
this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
Expand Down Expand Up @@ -565,8 +569,12 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.register(this._onScroll.event(ev => {
this.viewport!.syncScrollArea();
this._selectionService!.refresh();
this._decorationsService!.refresh();
}));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => {
this._selectionService!.refresh();
this._decorationsService!.refresh();
}));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService!.refresh()));

this._mouseZoneManager = this._instantiationService.createInstance(MouseZoneManager, this.element, this.screenElement);
this.register(this._mouseZoneManager);
Expand Down Expand Up @@ -998,6 +1006,13 @@ export class Terminal extends CoreTerminal implements ITerminal {
return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset);
}

public registerDecoration(decorationOptions: IBufferDecorationOptions): IDecoration | undefined {
if (!this._decorationsService) {
throw new Error('cannot register a decoration without a decorations service');
}

return this._decorationsService.registerDecoration(decorationOptions);
}
/**
* Gets whether the terminal has an active selection.
*/
Expand Down Expand Up @@ -1293,6 +1308,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
// Don't clear if it's already clear
return;
}
this.buffer.clearMarkers();
this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!);
this.buffer.lines.length = 1;
this.buffer.ydisp = 0;
Expand Down
11 changes: 10 additions & 1 deletion src/browser/TestUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @license MIT
*/

import { IDisposable, IMarker, ISelectionPosition, ILinkProvider } from 'xterm';
import { IDisposable, IMarker, ISelectionPosition, ILinkProvider, IBufferDecorationOptions, IDecoration } from 'xterm';
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { ICharacterJoinerService, ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
Expand Down Expand Up @@ -102,6 +102,9 @@ export class MockTerminal implements ITerminal {
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
throw new Error('Method not implemented.');
}
public registerDecoration(decorationOptions: IBufferDecorationOptions): IDecoration | undefined {
throw new Error('Method not implemented.');
}
public hasSelection(): boolean {
throw new Error('Method not implemented.');
}
Expand Down Expand Up @@ -290,6 +293,9 @@ export class MockRenderer implements IRenderer {
public onDevicePixelRatioChange(): void { }
public clear(): void { }
public renderRows(start: number, end: number): void { }
public registerDecoration(decorationOptions: IBufferDecorationOptions): IDecoration {
throw new Error('Method not implemented.');
}
}

export class MockViewport implements IViewport {
Expand Down Expand Up @@ -419,6 +425,9 @@ export class MockRenderService implements IRenderService {
public dispose(): void {
throw new Error('Method not implemented.');
}
public registerDecoration(decorationOptions: IBufferDecorationOptions): IDecoration {
throw new Error('Method not implemented.');
}
}

export class MockCharacterJoinerService implements ICharacterJoinerService {
Expand Down
3 changes: 2 additions & 1 deletion src/browser/Types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @license MIT
*/

import { IDisposable, IMarker, ISelectionPosition } from 'xterm';
import { IBufferDecorationOptions, IDecoration, IDisposable, IMarker, ISelectionPosition } from 'xterm';
import { IEvent } from 'common/EventEmitter';
import { ICoreTerminal, CharData, ITerminalOptions } from 'common/Types';
import { IMouseService, IRenderService } from './services/Services';
Expand Down Expand Up @@ -61,6 +61,7 @@ export interface IPublicTerminal extends IDisposable {
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
deregisterCharacterJoiner(joinerId: number): void;
addMarker(cursorYOffset: number): IMarker | undefined;
registerDecoration(decorationOptions: IBufferDecorationOptions): IDecoration | undefined;
hasSelection(): boolean;
getSelection(): string;
getSelectionPosition(): ISelectionPosition | undefined;
Expand Down
6 changes: 5 additions & 1 deletion src/browser/public/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @license MIT
*/

import { Terminal as ITerminalApi, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, FontWeight, IModes } from 'xterm';
import { Terminal as ITerminalApi, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, FontWeight, IModes, IBufferDecorationOptions, IDecoration } from 'xterm';
import { ITerminal } from 'browser/Types';
import { Terminal as TerminalCore } from 'browser/Terminal';
import * as Strings from 'browser/LocalizableStrings';
Expand Down Expand Up @@ -171,6 +171,10 @@ export class Terminal implements ITerminalApi {
this._verifyIntegers(cursorYOffset);
return this._core.addMarker(cursorYOffset);
}
public registerDecoration(decorationOptions: IBufferDecorationOptions): IDecoration | undefined {
this._checkProposedApi();
return this._core.registerDecoration(decorationOptions);
}
public addMarker(cursorYOffset: number): IMarker | undefined {
return this.registerMarker(cursorYOffset);
}
Expand Down
5 changes: 3 additions & 2 deletions src/browser/renderer/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import { IRenderLayer, IRenderer, IRenderDimensions, IRequestRedrawEvent } from
import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer';
import { Disposable } from 'common/Lifecycle';
import { IColorSet, ILinkifier, ILinkifier2 } from 'browser/Types';
import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
import { IBufferService, IOptionsService, ICoreService, IInstantiationService } from 'common/services/Services';
import { ICharSizeService } from 'browser/services/Services';
import { IBufferService, IOptionsService, IInstantiationService } from 'common/services/Services';
import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { IBufferDecorationOptions, IDecoration } from 'xterm';

let nextRendererId = 1;

Expand Down
1 change: 1 addition & 0 deletions src/browser/renderer/Types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { IDisposable } from 'common/Types';
import { IColorSet } from 'browser/Types';
import { IEvent } from 'common/EventEmitter';
import { IBufferDecorationOptions, IDecoration } from 'xterm';

export interface IRenderDimensions {
scaledCharWidth: number;
Expand Down
9 changes: 9 additions & 0 deletions src/browser/renderer/dom/DomRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { IOptionsService, IBufferService, IInstantiationService } from 'common/s
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { color } from 'browser/Color';
import { removeElementFromParent } from 'browser/Dom';
import { IBufferDecorationOptions, IDecoration } from 'xterm';

const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
const ROW_CONTAINER_CLASS = 'xterm-rows';
Expand Down Expand Up @@ -151,6 +152,14 @@ export class DomRenderer extends Disposable implements IRenderer {
this._injectCss();
}

public registerDecoration(decorationOptions: IBufferDecorationOptions): IDecoration {
// const decorationLayer = this._renderLayers.find(l => l instanceof DecorationRenderLayer);
// if (decorationLayer instanceof DecorationRenderLayer) {
// return decorationLayer.registerDecoration(decorationOptions);
// }
throw new Error('no decoration layer');
}

private _injectCss(): void {
if (!this._themeStyleElement) {
this._themeStyleElement = document.createElement('style');
Expand Down
148 changes: 148 additions & 0 deletions src/browser/services/DecorationsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
* @license MIT
*/

import { IRenderService } from 'browser/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { createDecorator } from 'common/services/ServiceRegistry';
import { IBufferService } from 'common/services/Services';
import { IDisposable } from 'common/Types';
import { IBufferDecorationOptions, IDecoration, IMarker } from 'xterm';

export interface IDecorationsService extends IDisposable {
registerDecoration(decorationOptions: IBufferDecorationOptions): IDecoration | undefined;
refresh(): void;
dispose(): void;
}

export class DecorationsService extends Disposable implements IDecorationsService {

private _decorations: BufferDecoration[] = [];
private _animationFrame: number | undefined;

constructor(
private readonly _screenElement: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
@IRenderService private readonly _renderService: IRenderService
) {
super();
}

public registerDecoration(decorationOptions: IBufferDecorationOptions): IDecoration | undefined {
if (decorationOptions.marker.isDisposed) {
return undefined;
}
this._resolveDimensions(decorationOptions);
const bufferDecoration = new BufferDecoration(decorationOptions, this._screenElement, this._renderService);
this._decorations.push(bufferDecoration);
return bufferDecoration;
}

public refresh(): void {
if (this._animationFrame) {
return;
}

this._animationFrame = window.requestAnimationFrame(() => this._refresh());
}

private _refresh(): void {
for (const decoration of this._decorations) {
const line = decoration.marker.line - this._bufferService.buffers.active.ydisp;
if (line < 0 || line > this._bufferService.rows) {
decoration.element.style.display = 'none';
} else {
decoration.element.style.top = `${line *this._renderService.dimensions.scaledCellHeight}px`;
decoration.element.style.display = 'block';
}
}
this._animationFrame = undefined;
}

private _resolveDimensions(decorationOptions: IBufferDecorationOptions): void {
if (this._renderService.dimensions.scaledCellWidth) {
decorationOptions.width = decorationOptions.width ? decorationOptions.width * this._renderService.dimensions.scaledCellWidth : this._renderService.dimensions.scaledCellWidth;
} else {
throw new Error('unknown cell width');
}

if (this._renderService.dimensions.scaledCellHeight) {
decorationOptions.height = decorationOptions.height ? decorationOptions.height * this._renderService.dimensions.scaledCellHeight : this._renderService.dimensions.scaledCellHeight;
} else {
throw new Error('unknown cell height');
}
}

public dispose(): void {
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
}
}

export const IDecorationsService = createDecorator<IDecorationsService>('DecorationsService');
class BufferDecoration extends Disposable implements IDecoration {
private static _nextId = 1;
private _marker: IMarker;
private _element: HTMLElement | undefined;
private _id: number = BufferDecoration._nextId++;
public isDisposed: boolean = false;

public get id(): number { return this._id; }
public get element(): HTMLElement { return this._element!; }
public get marker(): IMarker { return this._marker; }

private _onDispose = new EventEmitter<void>();
public get onDispose(): IEvent<void> { return this._onDispose.event; }

private _onRender = new EventEmitter<HTMLElement>();
public get onRender(): IEvent<HTMLElement> { return this._onRender.event; }

constructor(
private readonly _decorationOptions: IBufferDecorationOptions,
private readonly _screenElement: HTMLElement,
private readonly _renderService: IRenderService
) {
super();
this._marker = _decorationOptions.marker;
this._createElement();
this._render();
}

public dispose(): void {
if (this.isDisposed) {
return;
}
this.isDisposed = true;
this._marker.dispose();
// Emit before super.dispose such that dispose listeners get a change to react
this._onDispose.fire();
super.dispose();
}

private _createElement(): void {
this._element = document.createElement('div');
this._element.classList.add('xterm-decoration');
this._element.style.width = `${this._decorationOptions.width}px`;
this._element.style.height = `${this._decorationOptions.height}px`;
this._element.style.top = `${this._marker.line * this._renderService.dimensions.scaledCellHeight}px`;
if (this._decorationOptions.x && this._decorationOptions.x < 0) {
throw new Error(`Cannot create a decoration with a negative x offset: ${this._decorationOptions.x}`);
}
if (this._decorationOptions.anchor === 'right') {
this._element.style.right = this._decorationOptions.x ? `${this._decorationOptions.x * this._renderService.dimensions.scaledCellWidth}px` : '';
} else {
this._element.style.left = this._decorationOptions.x ? `${this._decorationOptions.x * this._renderService.dimensions.scaledCellWidth}px` : '';
}
}

private _render(): void {
if (this._screenElement && this._element) {
this._screenElement.append(this._element);
this._onRender.fire(this._element);
}
}
}
1 change: 1 addition & 0 deletions src/browser/services/RenderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { addDisposableDomListener } from 'browser/Lifecycle';
import { IColorSet, IRenderDebouncer } from 'browser/Types';
import { IOptionsService, IBufferService } from 'common/services/Services';
import { ICharSizeService, IRenderService } from 'browser/services/Services';
import { IDecoration, IBufferDecorationOptions } from 'xterm';

interface ISelectionState {
start: [number, number] | undefined;
Expand Down
Loading