Merge branch 'master' into progress-addon

This commit is contained in:
Jörg Breitbart
2025-01-07 16:49:09 +01:00
25 changed files with 248 additions and 112 deletions
+2 -2
View File
@@ -170,9 +170,9 @@ jobs:
strategy:
matrix:
node-version: [18] # just one as integration tests are about testing in browser
runs-on: [ubuntu] # macos is flaky
runs-on: [ubuntu-22.04] # macos is flaky
browser: [chromium, firefox, webkit]
runs-on: ${{ matrix.runs-on }}-latest
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}.x
+7 -3
View File
@@ -499,12 +499,16 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
const [stringLine, offsets] = cache;
const offset = this._bufferColsToStringOffset(row, col);
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
let searchTerm = term;
let searchStringLine = stringLine;
if (!searchOptions.regex) {
searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
}
let resultIndex = -1;
if (searchOptions.regex) {
const searchRegex = RegExp(searchTerm, 'g');
const searchRegex = RegExp(searchTerm, searchOptions.caseSensitive ? 'g' : 'gi');
let foundTerm: RegExpExecArray | null;
if (isReverseSearch) {
// This loop will get the resultIndex of the _last_ regex match in the range 0..offset
@@ -222,7 +222,7 @@ describe('SerializeAddon', () => {
it('empty terminal with default options', async () => {
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output);
assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: monospace; font-size: 15px;/g) || []).length, 1, output);
});
it('empty terminal with custom options', async () => {
@@ -242,7 +242,7 @@ describe('SerializeAddon', () => {
const output = serializeAddon.serializeAsHTML({
includeGlobalBackground: true
});
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output);
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: monospace; font-size: 15px;/g) || []).length, 1, output);
});
it('cells with custom color styling', async () => {
+45 -17
View File
@@ -33,6 +33,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _charAtlasDisposable = this._register(new MutableDisposable());
private _charAtlas: ITextureAtlas | undefined;
private _devicePixelRatio: number;
private _deviceMaxTextureSize: number;
private _observerDisposable = this._register(new MutableDisposable());
private _model: RenderModel = new RenderModel();
@@ -102,6 +103,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
throw new Error('WebGL2 not supported ' + this._gl);
}
this._deviceMaxTextureSize = this._gl.getParameter(this._gl.MAX_TEXTURE_SIZE);
this._register(addDisposableListener(this._canvas, 'webglcontextlost', (e) => {
console.log('webglcontextlost event received');
// Prevent the default behavior in order to enable WebGL context restoration.
@@ -272,7 +275,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
this.dimensions.device.cell.height,
this.dimensions.device.char.width,
this.dimensions.device.char.height,
this._coreBrowserService.dpr
this._coreBrowserService.dpr,
this._deviceMaxTextureSize
);
if (this._charAtlas !== atlas) {
this._onChangeTextureAtlas.fire(atlas.pages[0].canvas);
@@ -354,7 +358,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
private _updateCursorBlink(): void {
if (this._terminal.options.cursorBlink) {
if (this._coreService.decPrivateModes.cursorBlink ?? this._terminal.options.cursorBlink) {
this._cursorBlinkStateManager.value = new CursorBlinkStateManager(() => {
this._requestRedrawCursor();
}, this._coreBrowserService);
@@ -377,8 +381,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
let line: IBufferLine;
let joinedRanges: [number, number][];
let isJoined: boolean;
let skipJoinedCheckUntilX: number = 0;
let isValidJoinRange: boolean = true;
let lastCharX: number;
let range: [number, number];
let isCursorRow: boolean;
let chars: string;
let code: number;
let width: number;
@@ -387,6 +394,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
let j: number;
start = clamp(start, terminal.rows - 1, 0);
end = clamp(end, terminal.rows - 1, 0);
const cursorStyle = this._coreService.decPrivateModes.cursorStyle ?? terminal.options.cursorStyle ?? 'block';
const cursorY = this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY;
const viewportRelativeCursorY = cursorY - terminal.buffer.ydisp;
@@ -404,6 +412,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
row = y + terminal.buffer.ydisp;
line = terminal.buffer.lines.get(row)!;
this._model.lineLengths[y] = 0;
isCursorRow = cursorY === row;
skipJoinedCheckUntilX = 0;
joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
for (x = 0; x < terminal.cols; x++) {
lastBg = this._cellColorResolver.result.bg;
@@ -415,25 +425,43 @@ export class WebglRenderer extends Disposable implements IRenderer {
// If true, indicates that the current character(s) to draw were joined.
isJoined = false;
// Indicates whether this cell is part of a joined range that should be ignored as it cannot
// be rendered entirely, like the selection state differs across the range.
isValidJoinRange = (x >= skipJoinedCheckUntilX);
lastCharX = x;
// Process any joined character ranges as needed. Because of how the
// ranges are produced, we know that they are valid for the characters
// and attributes of our input.
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
isJoined = true;
if (joinedRanges.length > 0 && x === joinedRanges[0][0] && isValidJoinRange) {
range = joinedRanges.shift()!;
// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly.
cell = new JoinedCellData(
cell,
line!.translateToString(true, range[0], range[1]),
range[1] - range[0]
);
// If the ligature's selection state is not consistent, don't join it. This helps the
// selection render correctly regardless whether they should be joined.
const firstSelectionState = this._model.selection.isCellSelected(this._terminal, range[0], row);
for (i = range[0] + 1; i < range[1]; i++) {
isValidJoinRange &&= (firstSelectionState === this._model.selection.isCellSelected(this._terminal, i, row));
}
// Similarly, if the cursor is in the ligature, don't join it.
isValidJoinRange &&= !isCursorRow || cursorX < range[0] || cursorX >= range[1];
if (!isValidJoinRange) {
skipJoinedCheckUntilX = range[1];
} else {
isJoined = true;
// Skip over the cells occupied by this range in the loop
lastCharX = range[1] - 1;
// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly.
cell = new JoinedCellData(
cell,
line!.translateToString(true, range[0], range[1]),
range[1] - range[0]
);
// Skip over the cells occupied by this range in the loop
lastCharX = range[1] - 1;
}
}
chars = cell.getChars();
@@ -450,8 +478,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
x: cursorX,
y: viewportRelativeCursorY,
width: cell.getWidth(),
style: this._coreBrowserService.isFocused ?
(terminal.options.cursorStyle || 'block') : terminal.options.cursorInactiveStyle,
style: this._coreBrowserService.isFocused ? cursorStyle : terminal.options.cursorInactiveStyle,
cursorWidth: terminal.options.cursorWidth,
dpr: this._devicePixelRatio
};
@@ -459,9 +486,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
if (x >= cursorX && x <= lastCursorX &&
((this._coreBrowserService.isFocused &&
(terminal.options.cursorStyle || 'block') === 'block') ||
cursorStyle === 'block') ||
(this._coreBrowserService.isFocused === false &&
terminal.options.cursorInactiveStyle === 'block'))) {
terminal.options.cursorInactiveStyle === 'block'))
) {
this._cellColorResolver.result.fg =
Attributes.CM_RGB | (this._themeService.colors.cursorAccent.rgba >> 8 & Attributes.RGB_MASK);
this._cellColorResolver.result.bg =
@@ -94,7 +94,8 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) {
return;
}
this._charAtlas = acquireTextureAtlas(terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr);
this._charAtlas = acquireTextureAtlas(terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr, 2048);
this._charAtlas.warmUp();
}
+38 -22
View File
@@ -16,7 +16,7 @@ if ('WebAssembly' in window) {
ImageAddon = imageAddon.ImageAddon;
}
import { Terminal, ITerminalOptions, type IDisposable } from '@xterm/xterm';
import { Terminal, ITerminalOptions, type IDisposable, type ITheme } from '@xterm/xterm';
import { AttachAddon } from '@xterm/addon-attach';
import { ClipboardAddon } from '@xterm/addon-clipboard';
import { FitAddon } from '@xterm/addon-fit';
@@ -136,7 +136,7 @@ const xtermjsTheme = {
brightCyan: '#72F0FF',
white: '#F8F8F8',
brightWhite: '#FFFFFF'
};
} satisfies ITheme;
function setPadding(): void {
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
addons.fit.instance.fit();
@@ -246,6 +246,7 @@ if (document.location.pathname === '/test') {
document.getElementById('add-decoration').addEventListener('click', addDecoration);
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
document.getElementById('decoration-stress-test').addEventListener('click', decorationStressTest);
document.getElementById('ligatures-test').addEventListener('click', ligaturesTest);
document.getElementById('weblinks-test').addEventListener('click', testWeblinks);
document.getElementById('bce').addEventListener('click', coloredErase);
addVtButtons();
@@ -268,7 +269,7 @@ function createTerminal(): void {
backend: 'conpty',
buildNumber: 22621
} : undefined,
fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"',
fontFamily: '"Fira Code", monospace, "Powerline Extra Symbols"',
theme: xtermjsTheme
} as ITerminalOptions);
@@ -1275,9 +1276,9 @@ function addVtButtons(): void {
const element = document.createElement('button');
element.textContent = name;
writeCsi.split('');
const prefix = writeCsi.length === 2 ? writeCsi[0] : '';
const suffix = writeCsi[writeCsi.length - 1];
const writeCsiSplit = writeCsi.split('|');
const prefix = writeCsiSplit.length === 2 ? writeCsiSplit[0] : '';
const suffix = writeCsiSplit[writeCsiSplit.length - 1];
element.addEventListener(`click`, () => term.write(csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`)));
const desc = document.createElement('span');
@@ -1290,22 +1291,23 @@ function addVtButtons(): void {
}
const vtFragment = document.createDocumentFragment();
const buttonSpecs: { [key: string]: { label: string, description: string, paramCount?: number }} = {
A: { label: 'CUU ↑', description: 'Cursor Up Ps Times' },
B: { label: 'CUD ↓', description: 'Cursor Down Ps Times' },
C: { label: 'CUF →', description: 'Cursor Forward Ps Times' },
D: { label: 'CUB ←', description: 'Cursor Backward Ps Times' },
E: { label: 'CNL', description: 'Cursor Next Line Ps Times' },
F: { label: 'CPL', description: 'Cursor Preceding Line Ps Times' },
G: { label: 'CHA', description: 'Cursor Character Absolute' },
H: { label: 'CUP', description: 'Cursor Position [row;column]', paramCount: 2 },
I: { label: 'CHT', description: 'Cursor Forward Tabulation Ps tab stops' },
J: { label: 'ED', description: 'Erase in Display' },
'?J': { label: 'DECSED', description: 'Erase in Display' },
K: { label: 'EL', description: 'Erase in Line' },
'?K': { label: 'DECSEL', description: 'Erase in Line' },
L: { label: 'IL', description: 'Insert Ps Line(s)' },
M: { label: 'DL', description: 'Delete Ps Line(s)' },
P: { label: 'DCH', description: 'Delete Ps Character(s)' }
A: { label: 'CUU ↑', description: 'Cursor Up Ps Times' },
B: { label: 'CUD ↓', description: 'Cursor Down Ps Times' },
C: { label: 'CUF →', description: 'Cursor Forward Ps Times' },
D: { label: 'CUB ←', description: 'Cursor Backward Ps Times' },
E: { label: 'CNL', description: 'Cursor Next Line Ps Times' },
F: { label: 'CPL', description: 'Cursor Preceding Line Ps Times' },
G: { label: 'CHA', description: 'Cursor Character Absolute' },
H: { label: 'CUP', description: 'Cursor Position [row;column]', paramCount: 2 },
I: { label: 'CHT', description: 'Cursor Forward Tabulation Ps tab stops' },
J: { label: 'ED', description: 'Erase in Display' },
'?|J': { label: 'DECSED', description: 'Erase in Display' },
K: { label: 'EL', description: 'Erase in Line' },
'?|K': { label: 'DECSEL', description: 'Erase in Line' },
L: { label: 'IL', description: 'Insert Ps Line(s)' },
M: { label: 'DL', description: 'Delete Ps Line(s)' },
P: { label: 'DCH', description: 'Delete Ps Character(s)' },
' q': { label: 'DECSCUSR', description: 'Set Cursor Style', paramCount: 1 }
};
for (const s of Object.keys(buttonSpecs)) {
const spec = buttonSpecs[s];
@@ -1315,6 +1317,20 @@ function addVtButtons(): void {
document.querySelector('#vt-container').appendChild(vtFragment);
}
function ligaturesTest(): void {
term.write([
'',
'-<< -< -<- <-- <--- <<- <- -> ->> --> ---> ->- >- >>-',
'=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=',
'<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __',
'<~~ </ </> /> ~~> == != /= ~= <> === !== !=== =/= =!=',
'<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>',
'(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -| <!-- <!---',
'==== ===== ====== ======= ======== =========',
'---- ----- ------ ------- -------- ---------'
].join('\r\n'));
}
function testWeblinks(): void {
const linkExamples = `
aaa http://example.com aaa http://example.com aaa
+3
View File
@@ -106,6 +106,9 @@
<dd><button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button></dd>
<dd><button id="decoration-stress-test" title="Toggle between adding and removing a decoration to each line">Stress Test</button></dd>
<dt>Ligatures Addon</dt>
<dd><button id="ligatures-test" title="Write common ligatures sequences">Common ligatures</button></dd>
<dt>Weblinks Addon</dt>
<dd><button id="weblinks-test" title="Various url conditions from demo data, hover&click to test">Test URLs</button></dd>
+4 -2
View File
@@ -400,7 +400,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
}
// If the terminal is already opened
if (this.element?.ownerDocument.defaultView && this._coreBrowserService) {
if (this.element?.ownerDocument.defaultView && this._coreBrowserService && this.element?.isConnected) {
// Adjust the window if needed
if (this.element.ownerDocument.defaultView !== this._coreBrowserService.window) {
this._coreBrowserService.window = this.element.ownerDocument.defaultView;
@@ -437,7 +437,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
this.screenElement.appendChild(this._helperContainer);
fragment.appendChild(this.screenElement);
this.textarea = this._document.createElement('textarea');
const textarea = this.textarea = this._document.createElement('textarea');
this.textarea.classList.add('xterm-helper-textarea');
this.textarea.setAttribute('aria-label', Strings.promptLabel.get());
if (!Browser.isChromeOS) {
@@ -449,6 +449,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
this.textarea.setAttribute('autocapitalize', 'off');
this.textarea.setAttribute('spellcheck', 'false');
this.textarea.tabIndex = 0;
this._register(this.optionsService.onSpecificOptionChange('disableStdin', () => textarea.readOnly = this.optionsService.rawOptions.disableStdin));
this.textarea.readOnly = this.optionsService.rawOptions.disableStdin;
// Register the core browser service before the generic textarea handlers are registered so it
// handles them first. Otherwise the renderers may use the wrong focus state.
+8 -3
View File
@@ -13,7 +13,7 @@ import { ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/se
import { ILinkifier2, ILinkifierEvent, ITerminal, ReadonlyColorSet } from 'browser/Types';
import { color } from 'common/Color';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IBufferService, IInstantiationService, IOptionsService } from 'common/services/Services';
import { IBufferService, ICoreService, IInstantiationService, IOptionsService } from 'common/services/Services';
import { Emitter } from 'vs/base/common/event';
@@ -59,6 +59,7 @@ export class DomRenderer extends Disposable implements IRenderer {
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IOptionsService private readonly _optionsService: IOptionsService,
@IBufferService private readonly _bufferService: IBufferService,
@ICoreService private readonly _coreService: ICoreService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
@IThemeService private readonly _themeService: IThemeService
) {
@@ -161,6 +162,10 @@ export class DomRenderer extends Disposable implements IRenderer {
// Base CSS
let styles =
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` +
// Disabling pointer events circumvents a browser behavior that prevents `click` events from
// being delivered if the target element is replaced during the click. This happened due to
// refresh() being called during the mousedown handler to start a selection.
` pointer-events: none;` +
` color: ${colors.foreground.css};` +
` font-family: ${this._optionsService.rawOptions.fontFamily};` +
` font-size: ${this._optionsService.rawOptions.fontSize}px;` +
@@ -437,8 +442,8 @@ export class DomRenderer extends Disposable implements IRenderer {
const buffer = this._bufferService.buffer;
const cursorAbsoluteY = buffer.ybase + buffer.y;
const cursorX = Math.min(buffer.x, this._bufferService.cols - 1);
const cursorBlink = this._optionsService.rawOptions.cursorBlink;
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
const cursorBlink = this._coreService.decPrivateModes.cursorBlink ?? this._optionsService.rawOptions.cursorBlink;
const cursorStyle = this._coreService.decPrivateModes.cursorStyle ?? this._optionsService.rawOptions.cursorStyle;
const cursorInactiveStyle = this._optionsService.rawOptions.cursorInactiveStyle;
for (let y = start; y <= end; y++) {
@@ -84,6 +84,7 @@ export class DomRendererRowFactory {
let charElement: HTMLSpanElement | undefined;
let cellAmount = 0;
let text = '';
let i = 0;
let oldBg = 0;
let oldFg = 0;
let oldExt = 0;
@@ -91,6 +92,7 @@ export class DomRendererRowFactory {
let oldSpacing = 0;
let oldIsInSelection: boolean = false;
let spacing = 0;
let skipJoinedCheckUntilX = 0;
const classes: string[] = [];
const hasHover = linkStart !== -1 && linkEnd !== -1;
@@ -106,29 +108,46 @@ export class DomRendererRowFactory {
// If true, indicates that the current character(s) to draw were joined.
let isJoined = false;
// Indicates whether this cell is part of a joined range that should be ignored as it cannot
// be rendered entirely, like the selection state differs across the range.
let isValidJoinRange = (x >= skipJoinedCheckUntilX);
let lastCharX = x;
// Process any joined character ranges as needed. Because of how the
// ranges are produced, we know that they are valid for the characters
// and attributes of our input.
let cell = this._workCell;
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
isJoined = true;
if (joinedRanges.length > 0 && x === joinedRanges[0][0] && isValidJoinRange) {
const range = joinedRanges.shift()!;
// If the ligature's selection state is not consistent, don't join it. This helps the
// selection render correctly regardless whether they should be joined.
const firstSelectionState = this._isCellInSelection(range[0], row);
for (i = range[0] + 1; i < range[1]; i++) {
isValidJoinRange &&= (firstSelectionState === this._isCellInSelection(i, row));
}
// Similarly, if the cursor is in the ligature, don't join it.
isValidJoinRange &&= !isCursorRow || cursorX < range[0] || cursorX >= range[1];
if (!isValidJoinRange) {
skipJoinedCheckUntilX = range[1];
} else {
isJoined = true;
// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly
cell = new JoinedCellData(
this._workCell,
lineData.translateToString(true, range[0], range[1]),
range[1] - range[0]
);
// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly
cell = new JoinedCellData(
this._workCell,
lineData.translateToString(true, range[0], range[1]),
range[1] - range[0]
);
// Skip over the cells occupied by this range in the loop
lastCharX = range[1] - 1;
// Skip over the cells occupied by this range in the loop
lastCharX = range[1] - 1;
// Recalculate width
width = cell.getWidth();
// Recalculate width
width = cell.getWidth();
}
}
const isInSelection = this._isCellInSelection(x, row);
@@ -178,6 +197,7 @@ export class DomRendererRowFactory {
&& !isCursorCell
&& !isJoined
&& !isDecorated
&& isValidJoinRange
) {
// no span alterations, thus only account chars skipping all code below
if (cell.isInvisible()) {
@@ -435,7 +455,7 @@ export class DomRendererRowFactory {
}
// exclude conditions for cell merging - never merge these
if (!isCursorCell && !isJoined && !isDecorated) {
if (!isCursorCell && !isJoined && !isDecorated && isValidJoinRange) {
cellAmount++;
} else {
charElement.textContent = text;
@@ -31,9 +31,10 @@ export function acquireTextureAtlas(
deviceCellHeight: number,
deviceCharWidth: number,
deviceCharHeight: number,
devicePixelRatio: number
devicePixelRatio: number,
deviceMaxTextureSize: number
): ITextureAtlas {
const newConfig = generateConfig(deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, options, colors, devicePixelRatio);
const newConfig = generateConfig(deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, options, colors, devicePixelRatio, deviceMaxTextureSize);
// Check to see if the terminal already owns this config
for (let i = 0; i < charAtlasCache.length; i++) {
@@ -9,7 +9,7 @@ import { ITerminalOptions } from '@xterm/xterm';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { NULL_COLOR } from 'common/Color';
export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, options: Required<ITerminalOptions>, colors: ReadonlyColorSet, devicePixelRatio: number): ICharAtlasConfig {
export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, options: Required<ITerminalOptions>, colors: ReadonlyColorSet, devicePixelRatio: number, deviceMaxTextureSize: number): ICharAtlasConfig {
// null out some fields that don't matter
const clonedColors: IColorSet = {
foreground: colors.foreground,
@@ -34,6 +34,7 @@ export function generateConfig(deviceCellWidth: number, deviceCellHeight: number
return {
customGlyphs: options.customGlyphs,
devicePixelRatio,
deviceMaxTextureSize,
letterSpacing: options.letterSpacing,
lineHeight: options.lineHeight,
deviceCellWidth: deviceCellWidth,
+24 -1
View File
@@ -66,6 +66,7 @@ export class TextureAtlas implements ITextureAtlas {
// The set of atlas pages that can be written to
private _activePages: AtlasPage[] = [];
private _overflowSizePage: AtlasPage | undefined;
private _tmpCanvas: HTMLCanvasElement;
// A temporary context that glyphs are drawn to before being transfered to the atlas.
@@ -431,7 +432,7 @@ export class TextureAtlas implements ITextureAtlas {
// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
// to draw the glyph to the canvas as well as to restrict the bounding box search to ensure
// giant ligatures (eg. =====>) don't impact overall performance.
const allowedWidth = Math.min(this._config.deviceCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2, this._textureSize);
const allowedWidth = Math.min(this._config.deviceCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2, this._config.deviceMaxTextureSize);
if (this._tmpCanvas.width < allowedWidth) {
this._tmpCanvas.width = allowedWidth;
}
@@ -772,6 +773,27 @@ export class TextureAtlas implements ITextureAtlas {
}
}
// Create a new page for oversized glyphs as they come up
if (rasterizedGlyph.size.x > this._textureSize) {
if (!this._overflowSizePage) {
this._overflowSizePage = new AtlasPage(this._document, this._config.deviceMaxTextureSize);
this.pages.push(this._overflowSizePage);
// Request the model to be cleared to refresh all texture pages.
this._requestClearModel = true;
this._onAddTextureAtlasCanvas.fire(this._overflowSizePage.canvas);
}
activePage = this._overflowSizePage;
activeRow = this._overflowSizePage.currentRow;
// Move to next row if necessary
if (activeRow.x + rasterizedGlyph.size.x >= activePage.canvas.width) {
activeRow.x = 0;
activeRow.y += activeRow.height;
activeRow.height = 0;
}
break;
}
// Create a new page if too much vertical space would be wasted or there is not enough room
// left in the page. The previous active row will become fixed in the process as it now has a
// fixed height
@@ -782,6 +804,7 @@ export class TextureAtlas implements ITextureAtlas {
if (activePage.currentRow.y + activePage.currentRow.height + rasterizedGlyph.size.y >= activePage.canvas.height) {
// Find the first page with room to create the new row on
let candidatePage: AtlasPage | undefined;
for (const p of this._activePages) {
if (p.currentRow.y + p.currentRow.height + rasterizedGlyph.size.y < p.canvas.height) {
candidatePage = p;
+1
View File
@@ -11,6 +11,7 @@ import type { Event } from 'vs/base/common/event';
export interface ICharAtlasConfig {
customGlyphs: boolean;
devicePixelRatio: number;
deviceMaxTextureSize: number;
letterSpacing: number;
lineHeight: number;
fontSize: number;
+2 -2
View File
@@ -82,8 +82,8 @@ export class ThemeService extends Disposable implements IThemeService {
const colors = this._colors;
colors.foreground = parseColor(theme.foreground, DEFAULT_FOREGROUND);
colors.background = parseColor(theme.background, DEFAULT_BACKGROUND);
colors.cursor = parseColor(theme.cursor, DEFAULT_CURSOR);
colors.cursorAccent = parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT);
colors.cursor = color.blend(colors.background, parseColor(theme.cursor, DEFAULT_CURSOR));
colors.cursorAccent = color.blend(colors.background, parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT));
colors.selectionBackgroundTransparent = parseColor(theme.selectionBackground, DEFAULT_SELECTION);
colors.selectionBackgroundOpaque = color.blend(colors.background, colors.selectionBackgroundTransparent);
colors.selectionInactiveBackgroundTransparent = parseColor(theme.selectionInactiveBackground, colors.selectionBackgroundTransparent);
+14 -14
View File
@@ -202,38 +202,38 @@ describe('InputHandler', () => {
describe('setCursorStyle', () => {
it('should call Terminal.setOption with correct params', () => {
inputHandler.setCursorStyle(Params.fromArray([0]));
assert.equal(optionsService.options['cursorStyle'], 'block');
assert.equal(optionsService.options['cursorBlink'], true);
assert.equal(coreService.decPrivateModes.cursorStyle, undefined);
assert.equal(coreService.decPrivateModes.cursorBlink, undefined);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([1]));
assert.equal(optionsService.options['cursorStyle'], 'block');
assert.equal(optionsService.options['cursorBlink'], true);
assert.equal(coreService.decPrivateModes.cursorStyle, 'block');
assert.equal(coreService.decPrivateModes.cursorBlink, true);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([2]));
assert.equal(optionsService.options['cursorStyle'], 'block');
assert.equal(optionsService.options['cursorBlink'], false);
assert.equal(coreService.decPrivateModes.cursorStyle, 'block');
assert.equal(coreService.decPrivateModes.cursorBlink, false);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([3]));
assert.equal(optionsService.options['cursorStyle'], 'underline');
assert.equal(optionsService.options['cursorBlink'], true);
assert.equal(coreService.decPrivateModes.cursorStyle, 'underline');
assert.equal(coreService.decPrivateModes.cursorBlink, true);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([4]));
assert.equal(optionsService.options['cursorStyle'], 'underline');
assert.equal(optionsService.options['cursorBlink'], false);
assert.equal(coreService.decPrivateModes.cursorStyle, 'underline');
assert.equal(coreService.decPrivateModes.cursorBlink, false);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([5]));
assert.equal(optionsService.options['cursorStyle'], 'bar');
assert.equal(optionsService.options['cursorBlink'], true);
assert.equal(coreService.decPrivateModes.cursorStyle, 'bar');
assert.equal(coreService.decPrivateModes.cursorBlink, true);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([6]));
assert.equal(optionsService.options['cursorStyle'], 'bar');
assert.equal(optionsService.options['cursorBlink'], false);
assert.equal(coreService.decPrivateModes.cursorStyle, 'bar');
assert.equal(coreService.decPrivateModes.cursorBlink, false);
});
});
describe('setMode', () => {
+29 -23
View File
@@ -2714,7 +2714,7 @@ export class InputHandler extends Disposable implements IInputHandler {
/**
* CSI Ps SP q Set cursor style (DECSCUSR, VT520).
* Ps = 0 -> blinking block.
* Ps = 0 -> reset to option.
* Ps = 1 -> blinking block (default).
* Ps = 2 -> steady block.
* Ps = 3 -> blinking underline.
@@ -2724,31 +2724,37 @@ export class InputHandler extends Disposable implements IInputHandler {
*
* @vt: #Y CSI DECSCUSR "Set Cursor Style" "CSI Ps SP q" "Set cursor style."
* Supported cursor styles:
* - empty, 0 or 1: steady block
* - 2: blink block
* - 3: steady underline
* - 4: blink underline
* - 5: steady bar
* - 6: blink bar
* - 0: reset to option
* - empty, 1: blinking block
* - 2: steady block
* - 3: blinking underline
* - 4: steady underline
* - 5: blinking bar
* - 6: steady bar
*/
public setCursorStyle(params: IParams): boolean {
const param = params.params[0] || 1;
switch (param) {
case 1:
case 2:
this._optionsService.options.cursorStyle = 'block';
break;
case 3:
case 4:
this._optionsService.options.cursorStyle = 'underline';
break;
case 5:
case 6:
this._optionsService.options.cursorStyle = 'bar';
break;
const param = params.length === 0 ? 1 : params.params[0];
if (param === 0) {
this._coreService.decPrivateModes.cursorStyle = undefined;
this._coreService.decPrivateModes.cursorBlink = undefined;
} else {
switch (param) {
case 1:
case 2:
this._coreService.decPrivateModes.cursorStyle = 'block';
break;
case 3:
case 4:
this._coreService.decPrivateModes.cursorStyle = 'underline';
break;
case 5:
case 6:
this._coreService.decPrivateModes.cursorStyle = 'bar';
break;
}
const isBlinking = param % 2 === 1;
this._coreService.decPrivateModes.cursorBlink = isBlinking;
}
const isBlinking = param % 2 === 1;
this._optionsService.options.cursorBlink = isBlinking;
return true;
}
+2
View File
@@ -89,6 +89,8 @@ export class MockCoreService implements ICoreService {
applicationCursorKeys: false,
applicationKeypad: false,
bracketedPasteMode: false,
cursorBlink: undefined,
cursorStyle: undefined,
origin: false,
reverseWraparound: false,
sendFocus: false,
+2
View File
@@ -268,6 +268,8 @@ export interface IDecPrivateModes {
applicationCursorKeys: boolean;
applicationKeypad: boolean;
bracketedPasteMode: boolean;
cursorBlink: boolean | undefined;
cursorStyle: CursorStyle | undefined;
origin: boolean;
reverseWraparound: boolean;
sendFocus: boolean;
+2
View File
@@ -17,6 +17,8 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({
applicationCursorKeys: false,
applicationKeypad: false,
bracketedPasteMode: false,
cursorBlink: undefined,
cursorStyle: undefined,
origin: false,
reverseWraparound: false,
sendFocus: false,
+1 -1
View File
@@ -67,7 +67,7 @@ export class InstantiationService implements IInstantiationService {
for (const dependency of serviceDependencies) {
const service = this._services.get(dependency.id);
if (!service) {
throw new Error(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id}.`);
throw new Error(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id._id}.`);
}
serviceArgs.push(service);
}
+1 -1
View File
@@ -21,7 +21,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
documentOverride: null,
fastScrollModifier: 'alt',
fastScrollSensitivity: 5,
fontFamily: 'courier-new, courier, monospace',
fontFamily: 'monospace',
fontSize: 15,
fontWeight: 'normal',
fontWeightBold: 'bold',
+1 -1
View File
@@ -33,7 +33,7 @@ export function createDecorator<T>(id: string): IServiceIdentifier<T> {
storeServiceDependency(decorator, target, index);
};
decorator.toString = () => id;
decorator._id = id;
serviceRegistry.set(id, decorator);
return decorator;
+1
View File
@@ -124,6 +124,7 @@ export interface ICharsetService {
export interface IServiceIdentifier<T> {
(...args: any[]): void;
type: T;
_id: string;
}
export interface IBrandedService {
+18
View File
@@ -1248,6 +1248,24 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
await ctx.value.proxy.scrollLines(-2);
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]);
});
test('#5241 cursor with alpha should blend color with background color', async () => {
const theme: ITheme = {
cursor: '#FF000080'
};
await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
await ctx.value.proxy.focus();
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [128, 0, 0, 255]);
});
test('#5241 cursorAccent with alpha should blend color with background color', async () => {
const theme: ITheme = {
cursorAccent: '#FF000080'
};
await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
await ctx.value.proxy.focus();
await ctx.value.proxy.write('■');
await ctx.value.proxy.write('\x1b[1D');
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [128, 0, 0, 255]);
});
});
}