Merge branch 'master' into fix/memory-leak

This commit is contained in:
Daniel Imms
2024-04-21 08:06:05 -07:00
committed by GitHub
63 changed files with 512 additions and 154 deletions
+3
View File
@@ -72,6 +72,9 @@ export class MockTerminal implements ITerminal {
public focus(): void {
throw new Error('Method not implemented.');
}
public input(data: string, wasUserInput: boolean = true): void {
throw new Error('Method not implemented.');
}
public resize(columns: number, rows: number): void {
throw new Error('Method not implemented.');
}
+15 -2
View File
@@ -36,6 +36,8 @@ export class Viewport extends Disposable implements IViewport {
private _activeBuffer: IBuffer;
private _renderDimensions: IRenderDimensions;
private _smoothScrollAnimationFrame: number = 0;
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
// quick fix and could have a more robust solution in place that reset the value when needed.
@@ -49,6 +51,8 @@ export class Viewport extends Disposable implements IViewport {
target: -1
};
private _ensureTimeout: number;
private readonly _onRequestScrollLines = this.register(new EventEmitter<{ amount: number, suppressScrollEvent: boolean }>());
public readonly onRequestScrollLines = this._onRequestScrollLines.event;
@@ -81,7 +85,7 @@ export class Viewport extends Disposable implements IViewport {
this.register(this._optionsService.onSpecificOptionChange('scrollback', () => this.syncScrollArea()));
// Perform this async to ensure the ICharSizeService is ready.
setTimeout(() => this.syncScrollArea());
this._ensureTimeout = window.setTimeout(() => this.syncScrollArea());
}
private _handleThemeChange(colors: ReadonlyColorSet): void {
@@ -211,7 +215,12 @@ export class Viewport extends Disposable implements IViewport {
// Continue or finish smooth scroll
if (percent < 1) {
this._coreBrowserService.window.requestAnimationFrame(() => this._smoothScroll());
if (!this._smoothScrollAnimationFrame) {
this._smoothScrollAnimationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
this._smoothScrollAnimationFrame = 0;
this._smoothScroll();
});
}
} else {
this._clearSmoothScrollState();
}
@@ -398,4 +407,8 @@ export class Viewport extends Disposable implements IViewport {
this._viewportElement.scrollTop += deltaY;
return this._bubbleScroll(ev, deltaY);
}
public dispose(): void {
clearTimeout(this._ensureTimeout);
}
}
+3 -2
View File
@@ -159,8 +159,9 @@ export class CompositionHelper {
// otherwise input characters can be duplicated. (Issue #3191)
currentCompositionPosition.start += this._dataAlreadySent.length;
if (this._isComposing) {
// Use the end position to get the string if a new composition has started.
input = this._textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end);
// Use the start position of the new composition to get the string
// if a new composition has started.
input = this._textarea.value.substring(currentCompositionPosition.start, this._compositionPosition.start);
} else {
// Don't use the end position here in order to pick up any characters after the
// composition has finished, for example when typing a non-composition character
+9 -4
View File
@@ -20,6 +20,8 @@ import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOption
*/
const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows'];
let $value = 0;
export class Terminal extends Disposable implements ITerminalApi {
private _core: ITerminal;
private _addonManager: AddonManager;
@@ -138,6 +140,9 @@ export class Terminal extends Disposable implements ITerminalApi {
public focus(): void {
this._core.focus();
}
public input(data: string, wasUserInput: boolean = true): void {
this._core.input(data, wasUserInput);
}
public resize(columns: number, rows: number): void {
this._verifyIntegers(columns, rows);
this._core.resize(columns, rows);
@@ -246,16 +251,16 @@ export class Terminal extends Disposable implements ITerminalApi {
}
private _verifyIntegers(...values: number[]): void {
for (const value of values) {
if (value === Infinity || isNaN(value) || value % 1 !== 0) {
for ($value of values) {
if ($value === Infinity || isNaN($value) || $value % 1 !== 0) {
throw new Error('This API only accepts integers');
}
}
}
private _verifyPositiveIntegers(...values: number[]): void {
for (const value of values) {
if (value && (value === Infinity || isNaN(value) || value % 1 !== 0 || value < 0)) {
for ($value of values) {
if ($value && ($value === Infinity || isNaN($value) || $value % 1 !== 0 || $value < 0)) {
throw new Error('This API only accepts positive integers');
}
}
+27 -10
View File
@@ -183,14 +183,23 @@ export class DomRenderer extends Disposable implements IRenderer {
` font-style: italic;` +
`}`;
// Blink animation
const blinkAnimationUnderlineId = `blink_underline_${this._terminalClass}`;
const blinkAnimationBarId = `blink_bar_${this._terminalClass}`;
const blinkAnimationBlockId = `blink_block_${this._terminalClass}`;
styles +=
`@keyframes blink_box_shadow` + `_` + this._terminalClass + ` {` +
`@keyframes ${blinkAnimationUnderlineId} {` +
` 50% {` +
` border-bottom-style: hidden;` +
` }` +
`}`;
styles +=
`@keyframes blink_block` + `_` + this._terminalClass + ` {` +
`@keyframes ${blinkAnimationBarId} {` +
` 50% {` +
` box-shadow: none;` +
` }` +
`}`;
styles +=
`@keyframes ${blinkAnimationBlockId} {` +
` 0% {` +
` background-color: ${colors.cursor.css};` +
` color: ${colors.cursorAccent.css};` +
@@ -202,13 +211,23 @@ export class DomRenderer extends Disposable implements IRenderer {
`}`;
// Cursor
styles +=
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}:not(.${RowCss.CURSOR_STYLE_BLOCK_CLASS}) {` +
` animation: blink_box_shadow` + `_` + this._terminalClass + ` 1s step-end infinite;` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_UNDERLINE_CLASS} {` +
` animation: ${blinkAnimationUnderlineId} 1s step-end infinite;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BAR_CLASS} {` +
` animation: ${blinkAnimationBarId} 1s step-end infinite;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
` animation: blink_block` + `_` + this._terminalClass + ` 1s step-end infinite;` +
` animation: ${blinkAnimationBlockId} 1s step-end infinite;` +
`}` +
// !important helps fix an issue where the cursor will not render on top of the selection,
// however it's very hard to fix this issue and retain the blink animation without the use of
// !important. So this edge case fails when cursor blink is on.
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
` background-color: ${colors.cursor.css};` +
` color: ${colors.cursorAccent.css};` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS}:not(.${RowCss.CURSOR_BLINK_CLASS}) {` +
` background-color: ${colors.cursor.css} !important;` +
` color: ${colors.cursorAccent.css} !important;` +
`}` +
@@ -324,6 +343,9 @@ export class DomRenderer extends Disposable implements IRenderer {
}
this._selectionRenderModel.update(this._terminal, start, end, columnSelectMode);
if (!this._selectionRenderModel.hasSelection) {
return;
}
// Translate from buffer position to viewport position
const viewportStartRow = this._selectionRenderModel.viewportStartRow;
@@ -331,11 +353,6 @@ export class DomRenderer extends Disposable implements IRenderer {
const viewportCappedStartRow = this._selectionRenderModel.viewportCappedStartRow;
const viewportCappedEndRow = this._selectionRenderModel.viewportCappedEndRow;
// No need to draw the selection
if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) {
return;
}
// Create the selections
const documentFragment = this._document.createDocumentFragment();
+11 -4
View File
@@ -134,9 +134,14 @@ export class WidthCache implements IDisposable {
public get(c: string, bold: boolean | number, italic: boolean | number): number {
let cp = 0;
if (!bold && !italic && c.length === 1 && (cp = c.charCodeAt(0)) < WidthCacheSettings.FLAT_SIZE) {
return this._flat[cp] !== WidthCacheSettings.FLAT_UNSET
? this._flat[cp]
: (this._flat[cp] = this._measure(c, 0));
if (this._flat[cp] !== WidthCacheSettings.FLAT_UNSET) {
return this._flat[cp];
}
const width = this._measure(c, 0);
if (width > 0) {
this._flat[cp] = width;
}
return width;
}
let key = c;
if (bold) key += 'B';
@@ -147,7 +152,9 @@ export class WidthCache implements IDisposable {
if (bold) variant |= FontVariant.BOLD;
if (italic) variant |= FontVariant.ITALIC;
width = this._measure(c, variant);
this._holey!.set(key, width);
if (width > 0) {
this._holey!.set(key, width);
}
}
return width;
}
@@ -92,7 +92,7 @@ export class CellColorResolver {
$bg = this._themeService.colors.ansi[this.result.fg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$bg = (this.result.fg & Attributes.RGB_MASK) << 8 | 0xFF;
$bg = ((this.result.fg & Attributes.RGB_MASK) << 8) | 0xFF;
break;
case Attributes.CM_DEFAULT:
default:
@@ -105,7 +105,7 @@ export class CellColorResolver {
$bg = this._themeService.colors.ansi[this.result.bg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$bg = this.result.bg & Attributes.RGB_MASK << 8 | 0xFF;
$bg = ((this.result.bg & Attributes.RGB_MASK) << 8) | 0xFF;
break;
// No need to consider default bg color here as it's not possible
}
@@ -143,7 +143,7 @@ export class CellColorResolver {
$fg = this._themeService.colors.ansi[this.result.bg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$fg = this.result.bg & Attributes.RGB_MASK << 8 | 0xFF;
$fg = ((this.result.bg & Attributes.RGB_MASK) << 8) | 0xFF;
break;
// No need to consider default bg color here as it's not possible
}
@@ -154,7 +154,7 @@ export class CellColorResolver {
$fg = this._themeService.colors.ansi[this.result.fg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$fg = (this.result.fg & Attributes.RGB_MASK) << 8 | 0xFF;
$fg = ((this.result.fg & Attributes.RGB_MASK) << 8) | 0xFF;
break;
case Attributes.CM_DEFAULT:
default:
@@ -23,10 +23,43 @@ export function isRestrictedPowerlineGlyph(codepoint: number): boolean {
return 0xE0B0 <= codepoint && codepoint <= 0xE0B7;
}
function isNerdFontGlyph(codepoint: number): boolean {
return 0xE000 <= codepoint && codepoint <= 0xF8FF;
}
function isBoxOrBlockGlyph(codepoint: number): boolean {
return 0x2500 <= codepoint && codepoint <= 0x259F;
}
export function isEmoji(codepoint: number): boolean {
return (
codepoint >= 0x1F600 && codepoint <= 0x1F64F || // Emoticons
codepoint >= 0x1F300 && codepoint <= 0x1F5FF || // Misc Symbols and Pictographs
codepoint >= 0x1F680 && codepoint <= 0x1F6FF || // Transport and Map
codepoint >= 0x2600 && codepoint <= 0x26FF || // Misc symbols
codepoint >= 0x2700 && codepoint <= 0x27BF || // Dingbats
codepoint >= 0xFE00 && codepoint <= 0xFE0F || // Variation Selectors
codepoint >= 0x1F900 && codepoint <= 0x1F9FF || // Supplemental Symbols and Pictographs
codepoint >= 0x1F1E6 && codepoint <= 0x1F1FF
);
}
export function allowRescaling(codepoint: number | undefined, width: number, glyphSizeX: number, deviceCellWidth: number): boolean {
return (
// Is single cell width
width === 1 &&
// Glyph exceeds cell bounds, add 50% to avoid hurting readability by rescaling glyphs that
// barely overlap
glyphSizeX > Math.ceil(deviceCellWidth * 1.5) &&
// Never rescale ascii
codepoint !== undefined && codepoint > 0xFF &&
// Never rescale emoji
!isEmoji(codepoint) &&
// Never rescale powerline or nerd fonts
!isPowerlineGlyph(codepoint) && !isNerdFontGlyph(codepoint)
);
}
export function treatGlyphAsBackgroundColor(codepoint: number): boolean {
return isPowerlineGlyph(codepoint) || isBoxOrBlockGlyph(codepoint);
}
+3 -2
View File
@@ -87,7 +87,8 @@ export class RenderService extends Disposable implements IRenderService {
'fontSize',
'fontWeight',
'fontWeightBold',
'minimumContrastRatio'
'minimumContrastRatio',
'rescaleOverlappingGlyphs'
], () => {
this.clear();
this.handleResize(bufferService.cols, bufferService.rows);
@@ -247,7 +248,7 @@ export class RenderService extends Disposable implements IRenderService {
return;
}
if (this._isPaused) {
this._pausedResizeTask.set(() => this._renderer.value!.handleResize(cols, rows));
this._pausedResizeTask.set(() => this._renderer.value?.handleResize(cols, rows));
} else {
this._renderer.value.handleResize(cols, rows);
}
+5 -2
View File
@@ -3,7 +3,6 @@
* @license MIT
*/
import { isNode } from 'common/Platform';
import { IColor, IColorRGB } from 'common/Types';
let $r = 0;
@@ -117,9 +116,10 @@ export namespace color {
* '#rrggbbaa').
*/
export namespace css {
// Attempt to set get the shared canvas context
let $ctx: CanvasRenderingContext2D | undefined;
let $litmusColor: CanvasGradient | undefined;
if (!isNode) {
try {
// This is guaranteed to run in the first window, so document should be correct
const canvas = document.createElement('canvas');
canvas.width = 1;
@@ -133,6 +133,9 @@ export namespace css {
$litmusColor = $ctx.createLinearGradient(0, 0, 1, 1);
}
}
catch {
// noop
}
/**
* Converts a css string to an IColor, this should handle all valid CSS color strings and will
+4
View File
@@ -168,6 +168,10 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this._writeBuffer.writeSync(data, maxSubsequentCalls);
}
public input(data: string, wasUserInput: boolean = true): void {
this.coreService.triggerDataEvent(data, wasUserInput);
}
public resize(x: number, y: number): void {
if (isNaN(x) || isNaN(y)) {
return;
+1 -1
View File
@@ -2979,7 +2979,7 @@ export class InputHandler extends Disposable implements IInputHandler {
if (args[1]) {
return this._createHyperlink(args[0], args[1]);
}
if (args[0]) {
if (args[0].trim()) {
return false;
}
return this._finishHyperlink();
+1 -1
View File
@@ -14,7 +14,7 @@ interface INavigator {
declare const navigator: INavigator;
declare const process: unknown;
export const isNode = (typeof process !== 'undefined') ? true : false;
export const isNode = (typeof process !== 'undefined' && 'title' in (process as any)) ? true : false;
const userAgent = (isNode) ? 'node' : navigator.userAgent;
const platform = (isNode) ? 'node' : navigator.platform;
+86 -10
View File
@@ -3,16 +3,27 @@
* @license MIT
*/
import { IdleTaskQueue } from 'common/TaskQueue';
// Work variables to avoid garbage collection.
let i = 0;
/**
* A generic list that is maintained in sorted order and allows values with duplicate keys. This
* list is based on binary search and as such locating a key will take O(log n) amortized, this
* includes the by key iterator.
* A generic list that is maintained in sorted order and allows values with duplicate keys. Deferred
* batch insertion and deletion is used to significantly reduce the time it takes to insert and
* delete a large amount of items in succession. This list is based on binary search and as such
* locating a key will take O(log n) amortized, this includes the by key iterator.
*/
export class SortedList<T> {
private readonly _array: T[] = [];
private _array: T[] = [];
private readonly _insertedValues: T[] = [];
private readonly _flushInsertedTask = new IdleTaskQueue();
private _isFlushingInserted = false;
private readonly _deletedIndices: number[] = [];
private readonly _flushDeletedTask = new IdleTaskQueue();
private _isFlushingDeleted = false;
constructor(
private readonly _getKey: (value: T) => number
@@ -21,18 +32,50 @@ export class SortedList<T> {
public clear(): void {
this._array.length = 0;
this._insertedValues.length = 0;
this._flushInsertedTask.clear();
this._isFlushingInserted = false;
this._deletedIndices.length = 0;
this._flushDeletedTask.clear();
this._isFlushingDeleted = false;
}
public insert(value: T): void {
if (this._array.length === 0) {
this._array.push(value);
return;
this._flushCleanupDeleted();
if (this._insertedValues.length === 0) {
this._flushInsertedTask.enqueue(() => this._flushInserted());
}
this._insertedValues.push(value);
}
private _flushInserted(): void {
const sortedAddedValues = this._insertedValues.sort((a, b) => this._getKey(a) - this._getKey(b));
let sortedAddedValuesIndex = 0;
let arrayIndex = 0;
const newArray = new Array(this._array.length + this._insertedValues.length);
for (let newArrayIndex = 0; newArrayIndex < newArray.length; newArrayIndex++) {
if (arrayIndex >= this._array.length || this._getKey(sortedAddedValues[sortedAddedValuesIndex]) <= this._getKey(this._array[arrayIndex])) {
newArray[newArrayIndex] = sortedAddedValues[sortedAddedValuesIndex];
sortedAddedValuesIndex++;
} else {
newArray[newArrayIndex] = this._array[arrayIndex++];
}
}
this._array = newArray;
this._insertedValues.length = 0;
}
private _flushCleanupInserted(): void {
if (!this._isFlushingInserted && this._insertedValues.length > 0) {
this._flushInsertedTask.flush();
}
i = this._search(this._getKey(value));
this._array.splice(i, 0, value);
}
public delete(value: T): boolean {
this._flushCleanupInserted();
if (this._array.length === 0) {
return false;
}
@@ -49,14 +92,43 @@ export class SortedList<T> {
}
do {
if (this._array[i] === value) {
this._array.splice(i, 1);
if (this._deletedIndices.length === 0) {
this._flushDeletedTask.enqueue(() => this._flushDeleted());
}
this._deletedIndices.push(i);
return true;
}
} while (++i < this._array.length && this._getKey(this._array[i]) === key);
return false;
}
private _flushDeleted(): void {
this._isFlushingDeleted = true;
const sortedDeletedIndices = this._deletedIndices.sort((a, b) => a - b);
let sortedDeletedIndicesIndex = 0;
const newArray = new Array(this._array.length - sortedDeletedIndices.length);
let newArrayIndex = 0;
for (let i = 0; i < this._array.length; i++) {
if (sortedDeletedIndices[sortedDeletedIndicesIndex] === i) {
sortedDeletedIndicesIndex++;
} else {
newArray[newArrayIndex++] = this._array[i];
}
}
this._array = newArray;
this._deletedIndices.length = 0;
this._isFlushingDeleted = false;
}
private _flushCleanupDeleted(): void {
if (!this._isFlushingDeleted && this._deletedIndices.length > 0) {
this._flushDeletedTask.flush();
}
}
public *getKeyIterator(key: number): IterableIterator<T> {
this._flushCleanupInserted();
this._flushCleanupDeleted();
if (this._array.length === 0) {
return;
}
@@ -73,6 +145,8 @@ export class SortedList<T> {
}
public forEachByKey(key: number, callback: (value: T) => void): void {
this._flushCleanupInserted();
this._flushCleanupDeleted();
if (this._array.length === 0) {
return;
}
@@ -89,6 +163,8 @@ export class SortedList<T> {
}
public values(): IterableIterator<T> {
this._flushCleanupInserted();
this._flushCleanupDeleted();
// Duplicate the array to avoid issues when _array changes while iterating
return [...this._array].values();
}
+2 -1
View File
@@ -45,7 +45,8 @@ export class DecorationService extends Disposable implements IDecorationService
const decoration = new Decoration(options);
if (decoration) {
const markerDispose = decoration.marker.onDispose(() => decoration.dispose());
decoration.onDispose(() => {
const listener = decoration.onDispose(() => {
listener.dispose();
if (decoration) {
if (this._decorations.delete(decoration)) {
this._onDecorationRemoved.fire(decoration);
+1
View File
@@ -44,6 +44,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
allowTransparency: false,
tabStopWidth: 8,
theme: {},
rescaleOverlappingGlyphs: false,
rightClickSelectsWord: isMac,
windowOptions: {},
windowsMode: false,
+1
View File
@@ -234,6 +234,7 @@ export interface ITerminalOptions {
macOptionIsMeta?: boolean;
macOptionClickForcesSelection?: boolean;
minimumContrastRatio?: number;
rescaleOverlappingGlyphs?: boolean;
rightClickSelectsWord?: boolean;
rows?: number;
screenReaderMode?: boolean;
+4
View File
@@ -81,6 +81,10 @@ export class Terminal extends CoreTerminal {
this._onBell.fire();
}
public input(data: string, wasUserInput: boolean = true): void {
this.coreService.triggerDataEvent(data, wasUserInput);
}
/**
* Resizes the terminal.
*
+4
View File
@@ -80,6 +80,7 @@ export class Terminal extends Disposable implements ITerminalApi {
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }
public get onScroll(): IEvent<number> { return this._core.onScroll; }
public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }
public get onWriteParsed(): IEvent<void> { return this._core.onWriteParsed; }
public get parser(): IParser {
this._checkProposedApi();
@@ -134,6 +135,9 @@ export class Terminal extends Disposable implements ITerminalApi {
this._publicOptions[propName] = options[propName];
}
}
public input(data: string, wasUserInput: boolean = true): void {
this._core.input(data, wasUserInput);
}
public resize(columns: number, rows: number): void {
this._verifyIntegers(columns, rows);
this._core.resize(columns, rows);