mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge branch 'master' into dependabot/npm_and_yarn/cross-spawn-7.0.6
This commit is contained in:
@@ -167,9 +167,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
|
||||
|
||||
@@ -15,19 +15,22 @@ export interface ITerminalAddon {
|
||||
|
||||
export class LigaturesAddon implements ITerminalAddon , ILigaturesApi {
|
||||
private readonly _fallbackLigatures: string[];
|
||||
private readonly _fontFeatureSettings?: string;
|
||||
|
||||
private _terminal: Terminal | undefined;
|
||||
private _characterJoinerId: number | undefined;
|
||||
|
||||
constructor(options?: Partial<ILigatureOptions>) {
|
||||
// Source: calt set from https://github.com/be5invis/Iosevka?tab=readme-ov-file#ligations
|
||||
this._fallbackLigatures = (options?.fallbackLigatures || [
|
||||
'<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->',
|
||||
'<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=',
|
||||
'<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '-------->',
|
||||
'<~~', '<~', '~>', '~~>', '::', ':::', '==', '!=', '===', '!==',
|
||||
':=', ':-', ':+', '<*', '<*>', '*>', '<|', '<|>', '|>', '+:', '-:', '=:', ':>',
|
||||
'++', '+++', '<!--', '<!---', '<***>'
|
||||
'<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '::', ':::',
|
||||
'<~~', '</', '</>', '/>', '~~>', '==', '!=', '/=', '~=', '<>', '===', '!==', '!===',
|
||||
'<:', ':=', '*=', '*+', '<*', '<*>', '*>', '<|', '<|>', '|>', '+*', '=*', '=:', ':>',
|
||||
'/*', '*/', '+++', '<!--', '<!---'
|
||||
]).sort((a, b) => b.length - a.length);
|
||||
this._fontFeatureSettings = options?.fontFeatureSettings;
|
||||
}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
@@ -36,7 +39,7 @@ export class LigaturesAddon implements ITerminalAddon , ILigaturesApi {
|
||||
}
|
||||
this._terminal = terminal;
|
||||
this._characterJoinerId = enableLigatures(terminal, this._fallbackLigatures);
|
||||
terminal.element.style.fontFeatureSettings = '"liga" on, "calt" on';
|
||||
terminal.element.style.fontFeatureSettings = this._fontFeatureSettings ?? '"calt" on';
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
|
||||
export interface ILigatureOptions {
|
||||
fallbackLigatures: string[];
|
||||
fontFeatureSettings: string;
|
||||
}
|
||||
|
||||
+21
-9
@@ -23,7 +23,10 @@ declare module '@xterm/addon-ligatures' {
|
||||
constructor(options?: Partial<ILigatureOptions>);
|
||||
|
||||
/**
|
||||
* Activates the addon
|
||||
* Activates the addon. Note that if webgl is also being used, that addon
|
||||
* should be reactivated after ligatures is activated in order to apply
|
||||
* {@link ILigatureOptions.fontFeatureSettings} to the texture atlas.
|
||||
*
|
||||
*
|
||||
* @param terminal The terminal the addon is being loaded in.
|
||||
*/
|
||||
@@ -40,19 +43,28 @@ declare module '@xterm/addon-ligatures' {
|
||||
*/
|
||||
export interface ILigatureOptions {
|
||||
/**
|
||||
* Fallback ligatures to use when the font access API is either not supported by the browser or
|
||||
* access is denied. The default set of ligatures is taken from Iosevka's default "calt"
|
||||
* ligation set: https://typeof.net/Iosevka/
|
||||
* Fallback ligatures to use when the font access API is either not
|
||||
* supported by the browser or access is denied. The default set of
|
||||
* ligatures is taken from Iosevka's default "calt" ligation set:
|
||||
* https://typeof.net/Iosevka/
|
||||
*
|
||||
* ```
|
||||
* <-- <--- <<- <- -> ->> --> --->
|
||||
* <== <=== <<= <= => =>> ==> ===> >= >>=
|
||||
* <-> <--> <---> <----> <=> <==> <===> <====> -------->
|
||||
* <~~ <~ ~> ~~> :: ::: == != === !==
|
||||
* := :- :+ <* <*> *> <| <|> |> +: -: =: :>
|
||||
* ++ +++ <!-- <!--- <***>
|
||||
* ```
|
||||
* <-> <--> <---> <----> <=> <==> <===> <====> :: :::
|
||||
* <~~ </ </> /> ~~> == != /= ~= <> === !== !===
|
||||
* <: := *= *+ <* <*> *> <| <|> |> +* =* =: :>
|
||||
* /* <close block comment> +++ <!-- <!---
|
||||
*/
|
||||
fallbackLigatures: string[]
|
||||
|
||||
/**
|
||||
* The CSS `font-feature-settings` value to use for enabling ligatures. This
|
||||
* also supports font variants for example with a value like
|
||||
* `"calt" on, "ss03"`.
|
||||
*
|
||||
* The default value is `"calt" on`.
|
||||
*/
|
||||
fontFeatureSettings: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -238,9 +238,9 @@ export class GlyphRenderer extends Disposable {
|
||||
|
||||
// Get the glyph
|
||||
if (chars && chars.length > 1) {
|
||||
$glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext, false);
|
||||
$glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext, false, this._terminal.element);
|
||||
} else {
|
||||
$glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext, false);
|
||||
$glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext, false, this._terminal.element);
|
||||
}
|
||||
|
||||
$leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
+76
-36
@@ -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';
|
||||
@@ -131,7 +131,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();
|
||||
@@ -240,6 +240,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();
|
||||
@@ -261,7 +262,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);
|
||||
|
||||
@@ -366,14 +367,19 @@ function createTerminal(): void {
|
||||
// Set terminal size again to set the specific dimensions on the demo
|
||||
updateTerminalSize();
|
||||
|
||||
const res = await fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' });
|
||||
const processId = await res.text();
|
||||
pid = processId;
|
||||
socketURL += processId;
|
||||
socket = new WebSocket(socketURL);
|
||||
socket.onopen = runRealTerminal;
|
||||
socket.onclose = runFakeTerminal;
|
||||
socket.onerror = runFakeTerminal;
|
||||
const useRealTerminal = document.getElementById('use-real-terminal');
|
||||
if (useRealTerminal instanceof HTMLInputElement && !useRealTerminal.checked) {
|
||||
runFakeTerminal();
|
||||
} else {
|
||||
const res = await fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' });
|
||||
const processId = await res.text();
|
||||
pid = processId;
|
||||
socketURL += processId;
|
||||
socket = new WebSocket(socketURL);
|
||||
socket.onopen = runRealTerminal;
|
||||
socket.onclose = runFakeTerminal;
|
||||
socket.onerror = runFakeTerminal;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@@ -631,17 +637,25 @@ function initAddons(term: Terminal): void {
|
||||
}
|
||||
return;
|
||||
}
|
||||
function postInitWebgl(): void {
|
||||
setTimeout(() => {
|
||||
setTextureAtlas(addons.webgl.instance.textureAtlas);
|
||||
addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e));
|
||||
addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e));
|
||||
}, 500);
|
||||
}
|
||||
function preDisposeWebgl(): void {
|
||||
if (addons.webgl.instance.textureAtlas) {
|
||||
addons.webgl.instance.textureAtlas.remove();
|
||||
}
|
||||
}
|
||||
if (checkbox.checked) {
|
||||
// HACK: Manually remove addons that cannot be changes
|
||||
addon.instance = new (addon as IDemoAddon<Exclude<AddonType, 'attach'>>).ctor();
|
||||
try {
|
||||
term.loadAddon(addon.instance);
|
||||
if (name === 'webgl') {
|
||||
setTimeout(() => {
|
||||
setTextureAtlas(addons.webgl.instance.textureAtlas);
|
||||
addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e));
|
||||
addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e));
|
||||
}, 0);
|
||||
postInitWebgl();
|
||||
} else if (name === 'unicode11') {
|
||||
term.unicode.activeVersion = '11';
|
||||
} else if (name === 'unicodeGraphemes') {
|
||||
@@ -657,13 +671,24 @@ function initAddons(term: Terminal): void {
|
||||
}
|
||||
} else {
|
||||
if (name === 'webgl') {
|
||||
addons.webgl.instance.textureAtlas.remove();
|
||||
preDisposeWebgl();
|
||||
} else if (name === 'unicode11' || name === 'unicodeGraphemes') {
|
||||
term.unicode.activeVersion = '6';
|
||||
}
|
||||
addon.instance!.dispose();
|
||||
addon.instance = undefined;
|
||||
}
|
||||
if (name === 'ligatures') {
|
||||
// Recreate webgl when ligatures are toggled so texture atlas picks up any font feature
|
||||
// settings changes
|
||||
if (addons.webgl.instance) {
|
||||
preDisposeWebgl();
|
||||
addons.webgl.instance.dispose();
|
||||
addons.webgl.instance = new addons.webgl.ctor();
|
||||
term.loadAddon(addons.webgl.instance);
|
||||
postInitWebgl();
|
||||
}
|
||||
}
|
||||
});
|
||||
const label = document.createElement('label');
|
||||
label.classList.add('addon');
|
||||
@@ -1261,9 +1286,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');
|
||||
@@ -1276,22 +1301,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];
|
||||
@@ -1301,6 +1327,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
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
<div style="display: inline-block; margin-right: 16px;">
|
||||
<dl>
|
||||
<dt>Lifecycle</dt>
|
||||
<dd><label for="use-real-terminal"><input type="checkbox" checked id="use-real-terminal" title="This is used to real vs fake terminals" />Use real terminal</label></dd>
|
||||
<dd><button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button></dd>
|
||||
<dd><button id="create-new-window" title="This is used to test rendering in other windows">Create terminal in new window</button></dd>
|
||||
|
||||
@@ -105,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>
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
@@ -102,6 +103,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._tmpCanvas.remove();
|
||||
for (const page of this.pages) {
|
||||
page.canvas.remove();
|
||||
}
|
||||
@@ -121,7 +123,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
for (let i = 33; i < 126; i++) {
|
||||
queue.enqueue(() => {
|
||||
if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) {
|
||||
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT);
|
||||
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, false, undefined);
|
||||
this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph);
|
||||
}
|
||||
});
|
||||
@@ -241,12 +243,12 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
}
|
||||
}
|
||||
|
||||
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean): IRasterizedGlyph {
|
||||
return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext, restrictToCellHeight);
|
||||
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph {
|
||||
return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext, restrictToCellHeight, domContainer);
|
||||
}
|
||||
|
||||
public getRasterizedGlyph(code: number, bg: number, fg: number, ext: number, restrictToCellHeight: boolean): IRasterizedGlyph {
|
||||
return this._getFromCacheMap(this._cacheMap, code, bg, fg, ext, restrictToCellHeight);
|
||||
public getRasterizedGlyph(code: number, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph {
|
||||
return this._getFromCacheMap(this._cacheMap, code, bg, fg, ext, restrictToCellHeight, domContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,11 +260,12 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
bg: number,
|
||||
fg: number,
|
||||
ext: number,
|
||||
restrictToCellHeight: boolean = false
|
||||
restrictToCellHeight: boolean,
|
||||
domContainer: HTMLElement | undefined
|
||||
): IRasterizedGlyph {
|
||||
$glyph = cacheMap.get(key, bg, fg, ext);
|
||||
if (!$glyph) {
|
||||
$glyph = this._drawToCache(key, bg, fg, ext, restrictToCellHeight);
|
||||
$glyph = this._drawToCache(key, bg, fg, ext, restrictToCellHeight, domContainer);
|
||||
cacheMap.set(key, bg, fg, ext, $glyph);
|
||||
}
|
||||
return $glyph;
|
||||
@@ -422,16 +425,24 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
return this._config.colors.contrastCache;
|
||||
}
|
||||
|
||||
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean = false): IRasterizedGlyph {
|
||||
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph {
|
||||
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
|
||||
|
||||
// Uncomment for debugging
|
||||
// console.log(`draw to cache "${chars}"`, bg, fg, ext);
|
||||
|
||||
// Attach the canvas to the DOM in order to inherit font-feature-settings
|
||||
// from the parent elements. This is necessary for ligatures and variants to
|
||||
// work.
|
||||
if (domContainer && this._tmpCanvas.parentElement !== domContainer) {
|
||||
this._tmpCanvas.style.display = 'none';
|
||||
domContainer.append(this._tmpCanvas);
|
||||
}
|
||||
|
||||
// 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 +783,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 +814,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;
|
||||
|
||||
@@ -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;
|
||||
@@ -107,8 +108,8 @@ export interface ITextureAtlas extends IDisposable {
|
||||
* Clear all glyphs from the texture atlas.
|
||||
*/
|
||||
clearTexture(): void;
|
||||
getRasterizedGlyph(code: number, bg: number, fg: number, ext: number, restrictToCellHeight: boolean): IRasterizedGlyph;
|
||||
getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean): IRasterizedGlyph;
|
||||
getRasterizedGlyph(code: number, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph;
|
||||
getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user