mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge branch 'master' into #4121
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
FROM node:14-buster
|
||||
|
||||
# Configure apt
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install --no-install-recommends apt-utils 2>&1
|
||||
|
||||
# Verify git and process tools are installed
|
||||
RUN apt-get install -y git procps
|
||||
|
||||
# Install yarn
|
||||
RUN apt-get install -y curl apt-transport-https lsb-release \
|
||||
&& curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \
|
||||
&& echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends \
|
||||
yarn
|
||||
|
||||
# Clean up
|
||||
RUN apt-get autoremove -y \
|
||||
&& apt-get clean -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
ENV DEBIAN_FRONTEND=dialog
|
||||
@@ -1,10 +1,20 @@
|
||||
{
|
||||
"name": "xterm.js",
|
||||
"dockerFile": "Dockerfile",
|
||||
"appPort": 3000,
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig",
|
||||
"hbenl.vscode-mocha-test-adapter"
|
||||
]
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:0-18-buster",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {} // yarn
|
||||
},
|
||||
"forwardPorts": [
|
||||
3000
|
||||
],
|
||||
"postCreateCommand": "yarn install",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig",
|
||||
"hbenl.vscode-mocha-test-adapter"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: FAQ
|
||||
url: https://github.com/xtermjs/xterm.js/wiki/FAQ
|
||||
about: See our frequently asked questions before filing a bug report
|
||||
- name: Support / Q&A
|
||||
url: https://github.com/xtermjs/xterm.js/discussions/categories/q-a
|
||||
about: Use GitHub Discussions for community support and general Q&A
|
||||
|
||||
@@ -379,7 +379,17 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
|
||||
}
|
||||
this._ctx.save();
|
||||
this._clipRow(y);
|
||||
|
||||
// Draw the image, use the bitmap if it's available
|
||||
|
||||
// HACK: If the canvas doesn't match, delete the generator. It's not clear how this happens but
|
||||
// something is wrong with either the lifecycle of _bitmapGenerator or the page canvases are
|
||||
// swapped out unexpectedly
|
||||
if (this._bitmapGenerator[glyph.texturePage] && this._charAtlas.pages[glyph.texturePage].canvas !== this._bitmapGenerator[glyph.texturePage]!.canvas) {
|
||||
this._bitmapGenerator[glyph.texturePage]?.bitmap?.close();
|
||||
delete this._bitmapGenerator[glyph.texturePage];
|
||||
}
|
||||
|
||||
if (this._charAtlas.pages[glyph.texturePage].version !== this._bitmapGenerator[glyph.texturePage]?.version) {
|
||||
if (!this._bitmapGenerator[glyph.texturePage]) {
|
||||
this._bitmapGenerator[glyph.texturePage] = new BitmapGenerator(this._charAtlas.pages[glyph.texturePage].canvas);
|
||||
@@ -446,11 +456,12 @@ class BitmapGenerator {
|
||||
public get bitmap(): ImageBitmap | undefined { return this._bitmap; }
|
||||
public version: number = -1;
|
||||
|
||||
constructor(private readonly _canvas: HTMLCanvasElement) {
|
||||
constructor(public readonly canvas: HTMLCanvasElement) {
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
// Clear the bitmap immediately as it's stale
|
||||
this._bitmap?.close();
|
||||
this._bitmap = undefined;
|
||||
// Disable ImageBitmaps on Safari because of https://bugs.webkit.org/show_bug.cgi?id=149990
|
||||
if (isSafari) {
|
||||
@@ -466,9 +477,10 @@ class BitmapGenerator {
|
||||
|
||||
private _generate(): void {
|
||||
if (this._state === BitmapGeneratorState.IDLE) {
|
||||
this._bitmap?.close();
|
||||
this._bitmap = undefined;
|
||||
this._state = BitmapGeneratorState.GENERATING;
|
||||
window.createImageBitmap(this._canvas).then(bitmap => {
|
||||
window.createImageBitmap(this.canvas).then(bitmap => {
|
||||
if (this._state === BitmapGeneratorState.GENERATING_INVALID) {
|
||||
this.refresh();
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
|
||||
import { IColorSet, ITerminal } from 'browser/Types';
|
||||
import { ITerminal } from 'browser/Types';
|
||||
import { CanvasRenderer } from './CanvasRenderer';
|
||||
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { ITerminalAddon, Terminal } from 'xterm';
|
||||
|
||||
@@ -13,6 +13,7 @@ import { IEventEmitter } from 'common/EventEmitter';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { Terminal } from 'xterm';
|
||||
import { toDisposable } from 'common/Lifecycle';
|
||||
import { isFirefox } from 'common/Platform';
|
||||
|
||||
interface ICursorState {
|
||||
x: number;
|
||||
@@ -190,8 +191,9 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
|
||||
private _clearCursor(): void {
|
||||
if (this._state) {
|
||||
// Avoid potential rounding errors when device pixel ratio is less than 1
|
||||
if (this._coreBrowserService.dpr < 1) {
|
||||
// Avoid potential rounding errors when browser is Firefox (#4487) or device pixel ratio is
|
||||
// less than 1
|
||||
if (isFirefox || this._coreBrowserService.dpr < 1) {
|
||||
this._clearAll();
|
||||
} else {
|
||||
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
|
||||
|
||||
@@ -188,12 +188,6 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
nextFillStyle = this._themeService.colors.ansi[cell.getBgColor()].css;
|
||||
}
|
||||
|
||||
// Apply dim to the background, this is relatively slow as the CSS is re-parsed but dim is
|
||||
// rarely used
|
||||
if (nextFillStyle && cell.isDim()) {
|
||||
nextFillStyle = color.multiplyOpacity(css.toColor(nextFillStyle), 0.5).css;
|
||||
}
|
||||
|
||||
// Get any decoration foreground/background overrides, this must be fetched before the early
|
||||
// exist but applied after inverse
|
||||
let isTop = false;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, IDisposable, ITerminalAddon, IBufferRange, IDecoration } from 'xterm';
|
||||
import { Terminal, IDisposable, ITerminalAddon, IDecoration } from 'xterm';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { Disposable, toDisposable, disposeArray } from 'common/Lifecycle';
|
||||
|
||||
export interface ISearchOptions {
|
||||
regex?: boolean;
|
||||
@@ -30,6 +30,10 @@ export interface ISearchPosition {
|
||||
startRow: number;
|
||||
}
|
||||
|
||||
export interface ISearchAddonOptions {
|
||||
highlightLimit: number;
|
||||
}
|
||||
|
||||
export interface ISearchResult {
|
||||
term: string;
|
||||
col: number;
|
||||
@@ -48,15 +52,22 @@ type LineCacheEntry = [
|
||||
lineOffsets: number[]
|
||||
];
|
||||
|
||||
interface IHighlight extends IDisposable {
|
||||
decoration: IDecoration;
|
||||
match: ISearchResult;
|
||||
}
|
||||
|
||||
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?';
|
||||
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
|
||||
const DEFAULT_HIGHLIGHT_LIMIT = 1000;
|
||||
|
||||
export class SearchAddon extends Disposable implements ITerminalAddon {
|
||||
private _terminal: Terminal | undefined;
|
||||
private _cachedSearchTerm: string | undefined;
|
||||
private _selectedDecoration: IDecoration | undefined;
|
||||
private _resultDecorations: Map<number, IDecoration[]> | undefined;
|
||||
private _searchResults: Map<string, ISearchResult> | undefined;
|
||||
private _highlightedLines: Set<number> = new Set();
|
||||
private _highlightDecorations: IHighlight[] = [];
|
||||
private _selectedDecoration: IHighlight | undefined;
|
||||
private _highlightLimit: number;
|
||||
private _onDataDisposable: IDisposable | undefined;
|
||||
private _onResizeDisposable: IDisposable | undefined;
|
||||
private _lastSearchOptions: ISearchOptions | undefined;
|
||||
@@ -71,11 +82,15 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
|
||||
private _cursorMoveListener: IDisposable | undefined;
|
||||
private _resizeListener: IDisposable | undefined;
|
||||
|
||||
private _resultIndex: number | undefined;
|
||||
|
||||
private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number } | undefined>());
|
||||
private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number }>());
|
||||
public readonly onDidChangeResults = this._onDidChangeResults.event;
|
||||
|
||||
constructor(options?: Partial<ISearchAddonOptions>) {
|
||||
super();
|
||||
|
||||
this._highlightLimit = options?.highlightLimit ?? DEFAULT_HIGHLIGHT_LIMIT;
|
||||
}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
this._terminal = terminal;
|
||||
this._onDataDisposable = this.register(this._terminal.onWriteParsed(() => this._updateMatches()));
|
||||
@@ -93,24 +108,18 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
|
||||
}
|
||||
if (this._cachedSearchTerm && this._lastSearchOptions?.decorations) {
|
||||
this._highlightTimeout = setTimeout(() => {
|
||||
this.findPrevious(this._cachedSearchTerm!, { ...this._lastSearchOptions, incremental: true, noScroll: true });
|
||||
this._resultIndex = this._searchResults ? this._searchResults.size - 1 : -1;
|
||||
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults?.size ?? -1 });
|
||||
const term = this._cachedSearchTerm;
|
||||
this._cachedSearchTerm = undefined;
|
||||
this.findPrevious(term!, { ...this._lastSearchOptions, incremental: true, noScroll: true });
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
public clearDecorations(retainCachedSearchTerm?: boolean): void {
|
||||
this._selectedDecoration?.dispose();
|
||||
this._searchResults?.clear();
|
||||
this._resultDecorations?.forEach(decorations => {
|
||||
for (const d of decorations) {
|
||||
d.dispose();
|
||||
}
|
||||
});
|
||||
this._resultDecorations?.clear();
|
||||
this._searchResults = undefined;
|
||||
this._resultDecorations = undefined;
|
||||
this.clearActiveDecoration();
|
||||
disposeArray(this._highlightDecorations);
|
||||
this._highlightDecorations = [];
|
||||
this._highlightedLines.clear();
|
||||
if (!retainCachedSearchTerm) {
|
||||
this._cachedSearchTerm = undefined;
|
||||
}
|
||||
@@ -134,11 +143,16 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
|
||||
}
|
||||
this._lastSearchOptions = searchOptions;
|
||||
if (searchOptions?.decorations) {
|
||||
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
|
||||
if (this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
|
||||
this._highlightAllMatches(term, searchOptions);
|
||||
}
|
||||
}
|
||||
return this._fireResults(term, this._findNextAndSelect(term, searchOptions), searchOptions);
|
||||
|
||||
const found = this._findNextAndSelect(term, searchOptions);
|
||||
this._fireResults(searchOptions);
|
||||
this._cachedSearchTerm = term;
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void {
|
||||
@@ -153,32 +167,30 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
|
||||
|
||||
// new search, clear out the old decorations
|
||||
this.clearDecorations(true);
|
||||
this._searchResults = new Map<string, ISearchResult>();
|
||||
this._resultDecorations = new Map<number, IDecoration[]>();
|
||||
const resultDecorations = this._resultDecorations;
|
||||
|
||||
const searchResultsWithHighlight: ISearchResult[] = [];
|
||||
let prevResult: ISearchResult | undefined = undefined;
|
||||
let result = this._find(term, 0, 0, searchOptions);
|
||||
while (result && !this._searchResults.get(`${result.row}-${result.col}`)) {
|
||||
this._searchResults.set(`${result.row}-${result.col}`, result);
|
||||
while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) {
|
||||
if (searchResultsWithHighlight.length >= this._highlightLimit) {
|
||||
break;
|
||||
}
|
||||
prevResult = result;
|
||||
searchResultsWithHighlight.push(prevResult);
|
||||
result = this._find(
|
||||
term,
|
||||
result.col + result.term.length >= this._terminal.cols ? result.row + 1 : result.row,
|
||||
result.col + result.term.length >= this._terminal.cols ? 0 : result.col + 1,
|
||||
prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row,
|
||||
prevResult.col + prevResult.term.length >= this._terminal.cols ? 0 : prevResult.col + 1,
|
||||
searchOptions
|
||||
);
|
||||
if (this._searchResults.size > 1000) {
|
||||
this.clearDecorations();
|
||||
this._resultIndex = undefined;
|
||||
return;
|
||||
}
|
||||
for (const match of searchResultsWithHighlight) {
|
||||
const decoration = this._createResultDecoration(match, searchOptions.decorations!);
|
||||
if (decoration) {
|
||||
this._highlightedLines.add(decoration.marker.line);
|
||||
this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });
|
||||
}
|
||||
}
|
||||
this._searchResults.forEach(result => {
|
||||
const resultDecoration = this._createResultDecoration(result, searchOptions.decorations!);
|
||||
if (resultDecoration) {
|
||||
const decorationsForLine = resultDecorations.get(resultDecoration.marker.line) || [];
|
||||
decorationsForLine.push(resultDecoration);
|
||||
resultDecorations.set(resultDecoration.marker.line, decorationsForLine);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {
|
||||
@@ -223,26 +235,22 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
|
||||
if (!this._terminal || !term || term.length === 0) {
|
||||
this._terminal?.clearSelection();
|
||||
this.clearDecorations();
|
||||
this._cachedSearchTerm = undefined;
|
||||
this._resultIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._cachedSearchTerm !== term) {
|
||||
this._resultIndex = undefined;
|
||||
this._terminal.clearSelection();
|
||||
}
|
||||
const prevSelectedPos = this._terminal.getSelectionPosition();
|
||||
this._terminal.clearSelection();
|
||||
|
||||
let startCol = 0;
|
||||
let startRow = 0;
|
||||
let currentSelection: IBufferRange | undefined;
|
||||
if (this._terminal.hasSelection()) {
|
||||
const incremental = searchOptions ? searchOptions.incremental : false;
|
||||
// Start from the selection end if there is a selection
|
||||
// For incremental search, use existing row
|
||||
currentSelection = this._terminal.getSelectionPosition()!;
|
||||
startRow = incremental ? currentSelection.start.y : currentSelection.end.y;
|
||||
startCol = incremental ? currentSelection.start.x : currentSelection.end.x;
|
||||
if (prevSelectedPos) {
|
||||
if (this._cachedSearchTerm === term) {
|
||||
startCol = prevSelectedPos.end.x;
|
||||
startRow = prevSelectedPos.end.y;
|
||||
} else {
|
||||
startCol = prevSelectedPos.start.x;
|
||||
startRow = prevSelectedPos.start.y;
|
||||
}
|
||||
}
|
||||
|
||||
this._initLinesCache();
|
||||
@@ -281,24 +289,12 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
|
||||
}
|
||||
|
||||
// If there is only one result, wrap back and return selection if it exists.
|
||||
if (!result && currentSelection) {
|
||||
searchPosition.startRow = currentSelection.start.y;
|
||||
if (!result && prevSelectedPos) {
|
||||
searchPosition.startRow = prevSelectedPos.start.y;
|
||||
searchPosition.startCol = 0;
|
||||
result = this._findInLine(term, searchPosition, searchOptions);
|
||||
}
|
||||
|
||||
if (this._searchResults) {
|
||||
if (this._searchResults.size === 0) {
|
||||
this._resultIndex = -1;
|
||||
} else if (this._resultIndex === undefined) {
|
||||
this._resultIndex = 0;
|
||||
} else {
|
||||
this._resultIndex++;
|
||||
if (this._resultIndex >= this._searchResults.size) {
|
||||
this._resultIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set selection and scroll if a result was found
|
||||
return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll);
|
||||
}
|
||||
@@ -315,75 +311,74 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
|
||||
}
|
||||
this._lastSearchOptions = searchOptions;
|
||||
if (searchOptions?.decorations) {
|
||||
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
|
||||
if (this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
|
||||
this._highlightAllMatches(term, searchOptions);
|
||||
}
|
||||
}
|
||||
return this._fireResults(term, this._findPreviousAndSelect(term, searchOptions), searchOptions);
|
||||
|
||||
const found = this._findPreviousAndSelect(term, searchOptions);
|
||||
this._fireResults(searchOptions);
|
||||
this._cachedSearchTerm = term;
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
private _fireResults(term: string, found: boolean, searchOptions?: ISearchOptions): boolean {
|
||||
private _fireResults(searchOptions?: ISearchOptions): void {
|
||||
if (searchOptions?.decorations) {
|
||||
if (this._resultIndex !== undefined && this._searchResults?.size !== undefined) {
|
||||
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults.size });
|
||||
} else {
|
||||
this._onDidChangeResults.fire(undefined);
|
||||
let resultIndex = -1;
|
||||
if (this._selectedDecoration) {
|
||||
const selectedMatch = this._selectedDecoration.match;
|
||||
for (let i = 0; i < this._highlightDecorations.length; i++) {
|
||||
const match = this._highlightDecorations[i].match;
|
||||
if (match.row === selectedMatch.row && match.col === selectedMatch.col && match.size === selectedMatch.size) {
|
||||
resultIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._onDidChangeResults.fire({ resultIndex, resultCount: this._highlightDecorations.length });
|
||||
}
|
||||
this._cachedSearchTerm = term;
|
||||
return found;
|
||||
}
|
||||
|
||||
private _findPreviousAndSelect(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
let result: ISearchResult | undefined;
|
||||
if (!this._terminal || !term || term.length === 0) {
|
||||
result = undefined;
|
||||
this._terminal?.clearSelection();
|
||||
this.clearDecorations();
|
||||
this._resultIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._cachedSearchTerm !== term) {
|
||||
this._resultIndex = undefined;
|
||||
this._terminal.clearSelection();
|
||||
}
|
||||
const prevSelectedPos = this._terminal.getSelectionPosition();
|
||||
this._terminal.clearSelection();
|
||||
|
||||
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows;
|
||||
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1;
|
||||
let startCol = this._terminal.cols;
|
||||
const isReverseSearch = true;
|
||||
|
||||
const incremental = searchOptions ? searchOptions.incremental : false;
|
||||
let currentSelection: IBufferRange | undefined;
|
||||
if (this._terminal.hasSelection()) {
|
||||
currentSelection = this._terminal.getSelectionPosition()!;
|
||||
// Start from selection start if there is a selection
|
||||
startRow = currentSelection.start.y;
|
||||
startCol = currentSelection.start.x;
|
||||
}
|
||||
|
||||
this._initLinesCache();
|
||||
const searchPosition: ISearchPosition = {
|
||||
startRow,
|
||||
startCol
|
||||
};
|
||||
|
||||
if (incremental) {
|
||||
// Try to expand selection to right first.
|
||||
result = this._findInLine(term, searchPosition, searchOptions, false);
|
||||
const isOldResultHighlighted = result && result.row === startRow && result.col === startCol;
|
||||
if (!isOldResultHighlighted) {
|
||||
// If selection was not able to be expanded to the right, then try reverse search
|
||||
if (currentSelection) {
|
||||
searchPosition.startRow = currentSelection.end.y;
|
||||
searchPosition.startCol = currentSelection.end.x;
|
||||
let result: ISearchResult | undefined;
|
||||
if (prevSelectedPos) {
|
||||
searchPosition.startRow = startRow = prevSelectedPos.start.y;
|
||||
searchPosition.startCol = startCol = prevSelectedPos.start.x;
|
||||
if (this._cachedSearchTerm !== term) {
|
||||
// Try to expand selection to right first.
|
||||
result = this._findInLine(term, searchPosition, searchOptions, false);
|
||||
if (!result) {
|
||||
// If selection was not able to be expanded to the right, then try reverse search
|
||||
searchPosition.startRow = startRow = prevSelectedPos.end.y;
|
||||
searchPosition.startCol = startCol = prevSelectedPos.end.x;
|
||||
}
|
||||
result = this._findInLine(term, searchPosition, searchOptions, true);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
|
||||
}
|
||||
|
||||
@@ -399,8 +394,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
|
||||
}
|
||||
}
|
||||
// If we hit the top and didn't search from the very bottom wrap back down
|
||||
if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows)) {
|
||||
for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows); y >= startRow; y--) {
|
||||
if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows - 1)) {
|
||||
for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows - 1); y >= startRow; y--) {
|
||||
searchPosition.startRow = y;
|
||||
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
|
||||
if (result) {
|
||||
@@ -409,22 +404,6 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
|
||||
}
|
||||
}
|
||||
|
||||
if (this._searchResults) {
|
||||
if (this._searchResults.size === 0) {
|
||||
this._resultIndex = -1;
|
||||
} else if (this._resultIndex === undefined || this._resultIndex < 0) {
|
||||
this._resultIndex = this._searchResults.size - 1;
|
||||
} else {
|
||||
this._resultIndex--;
|
||||
if (this._resultIndex === -1) {
|
||||
this._resultIndex = this._searchResults.size - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is only one result, return true.
|
||||
if (!result && currentSelection) return true;
|
||||
|
||||
// Set selection and scroll if a result was found
|
||||
return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll);
|
||||
}
|
||||
@@ -675,7 +654,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
|
||||
if (options) {
|
||||
const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row);
|
||||
if (marker) {
|
||||
this._selectedDecoration = terminal.registerDecoration({
|
||||
const decoration = terminal.registerDecoration({
|
||||
marker,
|
||||
x: result.col,
|
||||
width: result.size,
|
||||
@@ -685,8 +664,13 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
|
||||
color: options.activeMatchColorOverviewRuler
|
||||
}
|
||||
});
|
||||
this._selectedDecoration?.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true));
|
||||
this._selectedDecoration?.onDispose(() => marker.dispose());
|
||||
if (decoration) {
|
||||
const disposables: IDisposable[] = [];
|
||||
disposables.push(marker);
|
||||
disposables.push(decoration.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true)));
|
||||
disposables.push(decoration.onDispose(() => disposeArray(disposables)));
|
||||
this._selectedDecoration = { decoration, match: result, dispose() { decoration.dispose(); } };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -709,9 +693,6 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
|
||||
* @returns
|
||||
*/
|
||||
private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {
|
||||
if (element.clientWidth <= 0) {
|
||||
return;
|
||||
}
|
||||
if (!element.classList.contains('xterm-find-result-decoration')) {
|
||||
element.classList.add('xterm-find-result-decoration');
|
||||
if (borderColor) {
|
||||
@@ -740,13 +721,17 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
|
||||
x: result.col,
|
||||
width: result.size,
|
||||
backgroundColor: options.matchBackground,
|
||||
overviewRulerOptions: this._resultDecorations?.get(marker.line) ? undefined : {
|
||||
overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {
|
||||
color: options.matchOverviewRuler,
|
||||
position: 'center'
|
||||
}
|
||||
});
|
||||
findResultDecoration?.onRender((e) => this._applyStyles(e, options.matchBorder, false));
|
||||
findResultDecoration?.onDispose(() => marker.dispose());
|
||||
if (findResultDecoration) {
|
||||
const disposables: IDisposable[] = [];
|
||||
disposables.push(marker);
|
||||
disposables.push(findResultDecoration.onRender((e) => this._applyStyles(e, options.matchBorder, false)));
|
||||
disposables.push(findResultDecoration.onDispose(() => disposeArray(disposables)));
|
||||
}
|
||||
return findResultDecoration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ describe('Search Tests', function (): void {
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc aabc');
|
||||
await writeSync(page, 'd abc aabc d');
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 }
|
||||
@@ -201,15 +201,64 @@ describe('Search Tests', function (): void {
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('d', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 0, resultIndex: -1 }
|
||||
]);
|
||||
});
|
||||
it('should fire with more than 1k matches', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
const data = ('a bc'.repeat(10) + '\\n\\r').repeat(150);
|
||||
await writeSync(page, data);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1000, resultIndex: 0 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1000, resultIndex: 0 },
|
||||
{ resultCount: 1000, resultIndex: 1 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('bc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1000, resultIndex: 0 },
|
||||
{ resultCount: 1000, resultIndex: 1 },
|
||||
{ resultCount: 1000, resultIndex: 1 }
|
||||
]);
|
||||
});
|
||||
it('should fire when writing to terminal', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc bc c\\n\\r'.repeat(2));
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('abc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 2, resultIndex: 0 }
|
||||
]);
|
||||
await writeSync(page, 'abc bc c\\n\\r');
|
||||
await timeout(300);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 3, resultIndex: 0 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('findPrevious', () => {
|
||||
it('should not fire unless the decorations option is set', async () => {
|
||||
@@ -233,6 +282,7 @@ describe('Search Tests', function (): void {
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 }
|
||||
]);
|
||||
await page.evaluate(`window.term.clearSelection()`);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
@@ -262,7 +312,7 @@ describe('Search Tests', function (): void {
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc aabc');
|
||||
await writeSync(page, 'd abc aabc d');
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 }
|
||||
@@ -285,15 +335,64 @@ describe('Search Tests', function (): void {
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 0 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('d', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 0, resultIndex: -1 }
|
||||
]);
|
||||
});
|
||||
it('should fire with more than 1k matches', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
const data = ('a bc'.repeat(10) + '\\n\\r').repeat(150);
|
||||
await writeSync(page, data);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1000, resultIndex: -1 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1000, resultIndex: -1 },
|
||||
{ resultCount: 1000, resultIndex: -1 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('bc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1000, resultIndex: -1 },
|
||||
{ resultCount: 1000, resultIndex: -1 },
|
||||
{ resultCount: 1000, resultIndex: -1 }
|
||||
]);
|
||||
});
|
||||
it('should fire when writing to terminal', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc bc c\\n\\r'.repeat(2));
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('abc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
await writeSync(page, 'abc bc c\\n\\r');
|
||||
await timeout(300);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 3, resultIndex: 1 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+20
-4
@@ -75,10 +75,28 @@ declare module 'xterm-addon-search' {
|
||||
activeMatchColorOverviewRuler: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the search addon.
|
||||
*/
|
||||
export interface ISearchAddonOptions {
|
||||
/**
|
||||
* Max number of matches highlighted when decorations are enabled.
|
||||
* Defaults to 1000 highlighted matches
|
||||
*/
|
||||
highlightLimit: number
|
||||
}
|
||||
|
||||
/**
|
||||
* An xterm.js addon that provides search functionality.
|
||||
*/
|
||||
export class SearchAddon implements ITerminalAddon {
|
||||
|
||||
/**
|
||||
* Creates a new search addon.
|
||||
* @param options Options for the search addon.
|
||||
*/
|
||||
constructor(options?: Partial<ISearchAddonOptions>);
|
||||
|
||||
/**
|
||||
* Activates the addon
|
||||
* @param terminal The terminal the addon is being loaded in.
|
||||
@@ -121,10 +139,8 @@ declare module 'xterm-addon-search' {
|
||||
/**
|
||||
* When decorations are enabled, fires when
|
||||
* the search results change.
|
||||
* @returns -1 for resultIndex for a resultCount of 0
|
||||
* and @returns undefined when the threshold of 1k results
|
||||
* is exceeded and decorations are disposed of.
|
||||
* @returns -1 for resultIndex when the threshold of matches is exceeded.
|
||||
*/
|
||||
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number } | undefined>;
|
||||
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number }>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): bo
|
||||
return cell1.isInverse() === cell2.isInverse()
|
||||
&& cell1.isBold() === cell2.isBold()
|
||||
&& cell1.isUnderline() === cell2.isUnderline()
|
||||
&& cell1.isOverline() === cell2.isOverline()
|
||||
&& cell1.isBlink() === cell2.isBlink()
|
||||
&& cell1.isInvisible() === cell2.isInvisible()
|
||||
&& cell1.isItalic() === cell2.isItalic()
|
||||
@@ -264,6 +265,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }
|
||||
if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }
|
||||
if (cell.isUnderline() !== oldCell.isUnderline()) { sgrSeq.push(cell.isUnderline() ? 4 : 24); }
|
||||
if (cell.isOverline() !== oldCell.isOverline()) { sgrSeq.push(cell.isOverline() ? 53 : 55); }
|
||||
if (cell.isBlink() !== oldCell.isBlink()) { sgrSeq.push(cell.isBlink() ? 5 : 25); }
|
||||
if (cell.isInvisible() !== oldCell.isInvisible()) { sgrSeq.push(cell.isInvisible() ? 8 : 28); }
|
||||
if (cell.isItalic() !== oldCell.isItalic()) { sgrSeq.push(cell.isItalic() ? 3 : 23); }
|
||||
@@ -625,7 +627,9 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
|
||||
if (cell.isInverse()) { content.push('color: #000000; background-color: #BFBFBF;'); }
|
||||
if (cell.isBold()) { content.push('font-weight: bold;'); }
|
||||
if (cell.isUnderline()) { content.push('text-decoration: underline;'); }
|
||||
if (cell.isUnderline() && cell.isOverline()) { content.push('text-decoration: overline underline;'); }
|
||||
else if (cell.isUnderline()) { content.push('text-decoration: underline;'); }
|
||||
else if (cell.isOverline()) { content.push('text-decoration: overline;'); }
|
||||
if (cell.isBlink()) { content.push('text-decoration: blink;'); }
|
||||
if (cell.isInvisible()) { content.push('visibility: hidden;'); }
|
||||
if (cell.isItalic()) { content.push('font-style: italic;'); }
|
||||
|
||||
@@ -220,6 +220,18 @@ describe('SerializeAddon', () => {
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with overline', async () => {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(OVERLINED) + line, // Overlined
|
||||
sgr(UNDERLINED) + line, // Overlined, Underlined
|
||||
sgr(NORMAL) + line // Normal
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with color16 and style separately', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
@@ -601,6 +613,7 @@ const BLINK = '5';
|
||||
const INVERSE = '7';
|
||||
const INVISIBLE = '8';
|
||||
const STRIKETHROUGH = '9';
|
||||
const OVERLINED = '53';
|
||||
|
||||
const NO_BOLD = '22';
|
||||
const NO_DIM = '22';
|
||||
@@ -610,3 +623,4 @@ const NO_BLINK = '25';
|
||||
const NO_INVERSE = '27';
|
||||
const NO_INVISIBLE = '28';
|
||||
const NO_STRIKETHROUGH = '29';
|
||||
const NO_OVERLINED = '55';
|
||||
|
||||
@@ -270,7 +270,7 @@ export class RectangleRenderer extends Disposable {
|
||||
$r = (($rgba >> 24) & 0xFF) / 255;
|
||||
$g = (($rgba >> 16) & 0xFF) / 255;
|
||||
$b = (($rgba >> 8 ) & 0xFF) / 255;
|
||||
$a = (!$isDefault && bg & BgFlags.DIM) ? DIM_OPACITY : 1;
|
||||
$a = 1;
|
||||
|
||||
this._addRectangle(vertices.attributes, offset, $x1, $y1, (endX - startX) * this._dimensions.device.cell.width, this._dimensions.device.cell.height, $r, $g, $b, $a);
|
||||
}
|
||||
|
||||
@@ -336,13 +336,16 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
// Tell renderer the frame is beginning
|
||||
// upon a model clear also refresh the full viewport model
|
||||
// (also triggered by an atlas page merge, part of #4480)
|
||||
if (this._glyphRenderer.beginFrame()) {
|
||||
this._clearModel(true);
|
||||
this._updateModel(0, this._terminal.rows - 1);
|
||||
} else {
|
||||
// just update changed lines to draw
|
||||
this._updateModel(start, end);
|
||||
}
|
||||
|
||||
// Update model to reflect what's drawn
|
||||
this._updateModel(start, end);
|
||||
|
||||
// Render
|
||||
this._rectangleRenderer?.render();
|
||||
this._glyphRenderer?.render(this._model);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { IEventEmitter } from 'common/EventEmitter';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { ICoreService, IOptionsService } from 'common/services/Services';
|
||||
import { toDisposable } from 'common/Lifecycle';
|
||||
import { isFirefox } from 'common/Platform';
|
||||
|
||||
interface ICursorState {
|
||||
x: number;
|
||||
@@ -190,8 +191,9 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
|
||||
private _clearCursor(): void {
|
||||
if (this._state) {
|
||||
// Avoid potential rounding errors when device pixel ratio is less than 1
|
||||
if (this._coreBrowserService.dpr < 1) {
|
||||
// Avoid potential rounding errors when browser is Firefox (#4487) or device pixel ratio is
|
||||
// less than 1
|
||||
if (isFirefox || this._coreBrowserService.dpr < 1) {
|
||||
this._clearAll();
|
||||
} else {
|
||||
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
|
||||
|
||||
@@ -364,6 +364,55 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
itWebgl('foreground 16-255 dim', async () => {
|
||||
let data = '';
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
data += `\\x1b[2;38;5;${16 + y * 16 + x}m█\x1b[0m`;
|
||||
}
|
||||
data += '\\r\\n';
|
||||
}
|
||||
await writeSync(page, data);
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||
const r = parseInt(cssColor.slice(1, 3), 16);
|
||||
const g = parseInt(cssColor.slice(3, 5), 16);
|
||||
const b = parseInt(cssColor.slice(5, 7), 16);
|
||||
// It's difficult to assert the exact color due to rounding, just ensure the color differs
|
||||
// to the regular color
|
||||
await pollFor(page, async () => {
|
||||
const c = await getCellColor(x + 1, y + 1);
|
||||
return (
|
||||
(c[0] === 0 || c[0] !== r) &&
|
||||
(c[1] === 0 || c[1] !== g) &&
|
||||
(c[2] === 0 || c[2] !== b)
|
||||
);
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
itWebgl('background 16-255 dim', async () => {
|
||||
let data = '';
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
data += `\\x1b[2;48;5;${16 + y * 16 + x}m \\x1b[0m`;
|
||||
}
|
||||
data += '\\r\\n';
|
||||
}
|
||||
await writeSync(page, data);
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||
const r = parseInt(cssColor.slice(1, 3), 16);
|
||||
const g = parseInt(cssColor.slice(3, 5), 16);
|
||||
const b = parseInt(cssColor.slice(5, 7), 16);
|
||||
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
itWebgl('foreground true color red', async () => {
|
||||
let data = '';
|
||||
for (let y = 0; y < 16; y++) {
|
||||
|
||||
+10
-10
@@ -1,10 +1,10 @@
|
||||
pr:
|
||||
branches:
|
||||
include: ["main", "v5"]
|
||||
include: ["main"]
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
include: ["main", "v5"]
|
||||
include: ["main"]
|
||||
|
||||
jobs:
|
||||
- job: Linux
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: YarnInstaller@3
|
||||
inputs:
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: CacheBeta@1
|
||||
inputs:
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: CacheBeta@1
|
||||
inputs:
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
displayName: Install required packages
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: YarnInstaller@3
|
||||
inputs:
|
||||
@@ -111,11 +111,11 @@ jobs:
|
||||
# Integration tests are too flaky on macOS https://github.com/xtermjs/xterm.js/issues/3590
|
||||
# - job: macOS_IntegrationTests
|
||||
# pool:
|
||||
# vmImage: 'macOS-10.15'
|
||||
# vmImage: 'macOS-11'
|
||||
# steps:
|
||||
# - task: NodeTool@0
|
||||
# inputs:
|
||||
# versionSpec: '14.x'
|
||||
# versionSpec: '18.x'
|
||||
# displayName: 'Install Node.js'
|
||||
# - script: yarn --frozen-lockfile
|
||||
# displayName: 'Install dependencies and build'
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- script: yarn --frozen-lockfile
|
||||
displayName: 'Install dependencies and build'
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: YarnInstaller@3
|
||||
inputs:
|
||||
|
||||
+19
-2
@@ -149,6 +149,7 @@
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm .live-region {
|
||||
@@ -160,7 +161,9 @@
|
||||
}
|
||||
|
||||
.xterm-dim {
|
||||
opacity: 0.5;
|
||||
/* Dim should not apply to background, so the opacity of the foreground color is applied
|
||||
* explicitly in the generated class and reset to 1 here */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xterm-underline-1 { text-decoration: underline; }
|
||||
@@ -169,6 +172,16 @@
|
||||
.xterm-underline-4 { text-decoration: dotted underline; }
|
||||
.xterm-underline-5 { text-decoration: dashed underline; }
|
||||
|
||||
.xterm-overline {
|
||||
text-decoration: overline;
|
||||
}
|
||||
|
||||
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
|
||||
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
|
||||
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
|
||||
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
|
||||
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
|
||||
|
||||
.xterm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
@@ -178,8 +191,12 @@
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.xterm-decoration-overview-ruler {
|
||||
z-index: 7;
|
||||
z-index: 8;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
+22
-8
@@ -132,12 +132,11 @@ function setPadding(): void {
|
||||
addons.fit.instance.fit();
|
||||
}
|
||||
|
||||
function getSearchOptions(e: KeyboardEvent): ISearchOptions {
|
||||
function getSearchOptions(): ISearchOptions {
|
||||
return {
|
||||
regex: (document.getElementById('regex') as HTMLInputElement).checked,
|
||||
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
|
||||
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked,
|
||||
incremental: e.key !== `Enter`,
|
||||
decorations: (document.getElementById('highlight-all-matches') as HTMLInputElement).checked ? {
|
||||
matchBackground: '#232422',
|
||||
matchBorder: '#555753',
|
||||
@@ -303,11 +302,23 @@ function createTerminal(): void {
|
||||
|
||||
addDomListener(paddingElement, 'change', setPadding);
|
||||
|
||||
addDomListener(actionElements.findNext, 'keyup', (e) => {
|
||||
addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions(e));
|
||||
addDomListener(actionElements.findNext, 'keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions());
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
addDomListener(actionElements.findPrevious, 'keyup', (e) => {
|
||||
addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions(e));
|
||||
addDomListener(actionElements.findNext, 'input', (e) => {
|
||||
addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions());
|
||||
});
|
||||
addDomListener(actionElements.findPrevious, 'keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions());
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
addDomListener(actionElements.findPrevious, 'input', (e) => {
|
||||
addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions());
|
||||
});
|
||||
addDomListener(actionElements.findNext, 'blur', (e) => {
|
||||
addons.search.instance.clearActiveDecoration();
|
||||
@@ -965,7 +976,9 @@ function sgrTest(): void {
|
||||
{ ps: 45, name: 'Background Magenta' },
|
||||
{ ps: 46, name: 'Background Cyan' },
|
||||
{ ps: 47, name: 'Background White' },
|
||||
{ ps: 49, name: 'Background default' }
|
||||
{ ps: 49, name: 'Background default' },
|
||||
{ ps: 53, name: 'Overlined' },
|
||||
{ ps: 55, name: 'Not overlined' }
|
||||
];
|
||||
const maxNameLength = entries.reduce<number>((p, c) => Math.max(c.name.length, p), 0);
|
||||
for (const e of entries) {
|
||||
@@ -977,7 +990,8 @@ function sgrTest(): void {
|
||||
}
|
||||
const comboEntries: { ps: number[] }[] = [
|
||||
{ ps: [1, 2, 3, 4, 5, 6, 7, 9] },
|
||||
{ ps: [2, 41] }
|
||||
{ ps: [2, 41] },
|
||||
{ ps: [4, 53] }
|
||||
];
|
||||
term.write('\n\n\r');
|
||||
term.writeln(`Combinations`);
|
||||
|
||||
+12
-15
@@ -111,35 +111,32 @@ function startServer() {
|
||||
}
|
||||
// binary message buffering
|
||||
function bufferUtf8(socket, timeout, maxSize) {
|
||||
const dataBuffer = new Uint8Array(maxSize);
|
||||
let sender = null;
|
||||
const chunks = [];
|
||||
let length = 0;
|
||||
let sender = null;
|
||||
return (data) => {
|
||||
function flush() {
|
||||
socket.send(Buffer.from(dataBuffer.buffer, 0, length));
|
||||
chunks.push(data);
|
||||
length += data.length;
|
||||
if (length > maxSize || userInput) {
|
||||
userInput = false;
|
||||
socket.send(Buffer.concat(chunks));
|
||||
chunks.length = 0;
|
||||
length = 0;
|
||||
if (sender) {
|
||||
clearTimeout(sender);
|
||||
sender = null;
|
||||
}
|
||||
}
|
||||
if (length + data.length > maxSize) {
|
||||
flush();
|
||||
}
|
||||
dataBuffer.set(data, length);
|
||||
length += data.length;
|
||||
if (length > maxSize || userInput) {
|
||||
userInput = false;
|
||||
flush();
|
||||
} else if (!sender) {
|
||||
sender = setTimeout(() => {
|
||||
socket.send(Buffer.concat(chunks));
|
||||
chunks.length = 0;
|
||||
length = 0;
|
||||
sender = null;
|
||||
flush();
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
}
|
||||
const send = (USE_BINARY ? bufferUtf8 : buffer)(ws, 5, 262144);
|
||||
const send = (USE_BINARY ? bufferUtf8 : buffer)(ws, 3, 262144);
|
||||
|
||||
// WARNING: This is a naive implementation that will not throttle the flow of data. This means
|
||||
// it could flood the communication channel and make the terminal unresponsive. Learn more about
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user