Merge branch 'master' into dev_wlw

This commit is contained in:
Daniel Imms
2022-03-31 09:00:09 -07:00
committed by GitHub
44 changed files with 1195 additions and 473 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-ligatures",
"version": "0.5.2",
"version": "0.5.3",
"description": "Add support for programming ligatures to xterm.js",
"author": {
"name": "The xterm.js authors",
+3 -3
View File
@@ -141,9 +141,9 @@ lru-cache@^6.0.0:
yallist "^4.0.0"
minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mkdirp@0.5.5:
version "0.5.5"
+273 -15
View File
@@ -3,13 +3,24 @@
* @license MIT
*/
import { Terminal, IBufferLine, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm';
import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition, IDecoration } from 'xterm';
import { EventEmitter } from 'common/EventEmitter';
export interface ISearchOptions {
regex?: boolean;
wholeWord?: boolean;
caseSensitive?: boolean;
incremental?: boolean;
decorations?: ISearchDecorationOptions;
}
interface ISearchDecorationOptions {
matchBackground?: string;
matchBorder?: string;
matchOverviewRuler: string;
activeMatchBackground?: string;
activeMatchBorder?: string;
activeMatchColorOverviewRuler: string;
}
export interface ISearchPosition {
@@ -40,7 +51,14 @@ const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
export class SearchAddon implements ITerminalAddon {
private _terminal: Terminal | undefined;
private _dataChanged: boolean = false;
private _cachedSearchTerm: string | undefined;
private _selectedDecoration: IDecoration | undefined;
private _resultDecorations: Map<number, IDecoration[]> | undefined;
private _searchResults: Map<string, ISearchResult> | undefined;
private _onDataDisposable: IDisposable | undefined;
private _lastSearchOptions: ISearchOptions | undefined;
private _highlightTimeout: number | undefined;
/**
* translateBufferLineToStringWithWrap is a fairly expensive call.
* We memoize the calls into an array that has a time based ttl.
@@ -51,11 +69,46 @@ export class SearchAddon implements ITerminalAddon {
private _cursorMoveListener: IDisposable | undefined;
private _resizeListener: IDisposable | undefined;
private _resultIndex: number | undefined;
private readonly _onDidChangeResults = new EventEmitter<{resultIndex: number, resultCount: number} | undefined>();
public readonly onDidChangeResults = this._onDidChangeResults.event;
public activate(terminal: Terminal): void {
this._terminal = terminal;
this._onDataDisposable = this._terminal.onData(() => {
this._dataChanged = true;
if (this._highlightTimeout) {
window.clearTimeout(this._highlightTimeout);
}
if (this._cachedSearchTerm && this._lastSearchOptions?.decorations) {
this._highlightTimeout = setTimeout(() => {
this.findPrevious(this._cachedSearchTerm!, { ...this._lastSearchOptions, incremental: true });
}, 200);
}
});
}
public dispose(): void { }
public dispose(): void {
this.clearDecorations();
this._onDataDisposable?.dispose();
}
public clearDecorations(): void {
this._selectedDecoration?.dispose();
this._searchResults?.clear();
this._resultDecorations?.forEach(decorations => {
for (const d of decorations) {
d.dispose();
}
});
this._resultDecorations?.clear();
this._cachedSearchTerm = undefined;
this._searchResults = undefined;
this._resultDecorations = undefined;
this._dataChanged = true;
this._resultIndex = undefined;
}
/**
* Find the next instance of the term, then scroll to and select it. If it
@@ -68,12 +121,111 @@ export class SearchAddon implements ITerminalAddon {
if (!this._terminal) {
throw new Error('Cannot use addon until it has been loaded');
}
this._lastSearchOptions = searchOptions;
if (searchOptions?.decorations) {
this._highlightAllMatches(term, searchOptions);
}
const next = this._findNextAndSelect(term, searchOptions);
if (searchOptions?.decorations) {
if (next && this._resultIndex !== undefined && this._searchResults?.size) {
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults.size });
} else {
this._onDidChangeResults.fire(undefined);
}
}
return next;
}
private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void {
if (!this._terminal) {
throw new Error('Cannot use addon until it has been loaded');
}
if (!term || term.length === 0) {
this._terminal.clearSelection();
this.clearDecorations();
return;
}
searchOptions = searchOptions || {};
if (term === this._cachedSearchTerm && !this._dataChanged) {
return;
}
// new search, clear out the old decorations
this.clearDecorations();
this._searchResults = new Map<string, ISearchResult>();
this._resultDecorations = new Map<number, IDecoration[]>();
const resultDecorations = this._resultDecorations;
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);
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,
searchOptions
);
}
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);
}
});
if (this._dataChanged) {
this._dataChanged = false;
}
if (this._searchResults.size > 0) {
this._cachedSearchTerm = term;
}
}
private _find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {
if (!this._terminal || !term || term.length === 0) {
this._terminal?.clearSelection();
this.clearDecorations();
return undefined;
}
if (startCol > this._terminal.cols) {
throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`);
}
let result: ISearchResult | undefined = undefined;
this._initLinesCache();
const searchPosition: ISearchPosition = {
startRow,
startCol
};
// Search startRow
result = this._findInLine(term, searchPosition, searchOptions);
// Search from startRow + 1 to end
if (!result) {
for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
searchPosition.startRow = y;
searchPosition.startCol = 0;
// If the current line is wrapped line, increase index of column to ignore the previous scan
// Otherwise, reset beginning column index to zero with set new unwrapped line index
result = this._findInLine(term, searchPosition, searchOptions);
if (result) {
break;
}
}
}
return result;
}
private _findNextAndSelect(term: string, searchOptions?: ISearchOptions): boolean {
if (!this._terminal || !term || term.length === 0) {
this._terminal?.clearSelection();
this.clearDecorations();
return false;
}
let startCol = 0;
let startRow = 0;
let currentSelection: ISelectionPosition | undefined;
@@ -95,7 +247,6 @@ export class SearchAddon implements ITerminalAddon {
// Search startRow
let result = this._findInLine(term, searchPosition, searchOptions);
// Search from startRow + 1 to end
if (!result) {
@@ -129,10 +280,20 @@ export class SearchAddon implements ITerminalAddon {
result = this._findInLine(term, searchPosition, searchOptions);
}
// Set selection and scroll if a result was found
return this._selectResult(result);
}
if (this._searchResults) {
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);
}
/**
* Find the previous instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
@@ -144,16 +305,37 @@ export class SearchAddon implements ITerminalAddon {
if (!this._terminal) {
throw new Error('Cannot use addon until it has been loaded');
}
this._lastSearchOptions = searchOptions;
if (searchOptions?.decorations) {
this._highlightAllMatches(term, searchOptions);
}
const previous = this._findPreviousAndSelect(term, searchOptions);
if (searchOptions?.decorations) {
if (previous && this._resultIndex !== undefined && this._searchResults?.size) {
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults.size });
} else {
this._onDidChangeResults.fire(undefined);
}
}
return previous;
}
if (!term || term.length === 0) {
this._terminal.clearSelection();
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();
return false;
}
const isReverseSearch = true;
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows;
let startCol = this._terminal.cols;
let result: ISearchResult | undefined;
const isReverseSearch = true;
const incremental = searchOptions ? searchOptions.incremental : false;
let currentSelection: ISelectionPosition | undefined;
if (this._terminal.hasSelection()) {
@@ -207,11 +389,22 @@ export class SearchAddon implements ITerminalAddon {
}
}
if (this._searchResults) {
if (this._resultIndex === undefined) {
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);
return this._selectResult(result, searchOptions?.decorations);
}
/**
@@ -446,15 +639,32 @@ export class SearchAddon implements ITerminalAddon {
/**
* Selects and scrolls to a result.
* @param result The result to select.
* @return Whethera result was selected.
* @return Whether a result was selected.
*/
private _selectResult(result: ISearchResult | undefined): boolean {
private _selectResult(result: ISearchResult | undefined, decorations?: ISearchDecorationOptions): boolean {
const terminal = this._terminal!;
this._selectedDecoration?.dispose();
if (!result) {
terminal.clearSelection();
return false;
}
terminal.select(result.col, result.row, result.size);
if (decorations?.activeMatchColorOverviewRuler) {
const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row);
if (marker) {
this._selectedDecoration = terminal.registerDecoration({
marker,
x: result.col,
width: result.size,
overviewRulerOptions: {
color: decorations.activeMatchColorOverviewRuler
}
});
this._selectedDecoration?.onRender((e) => this._applyStyles(e, decorations.activeMatchBackground, decorations.activeMatchBorder, result));
this._selectedDecoration?.onDispose(() => marker.dispose());
}
}
// If it is not in the viewport then we scroll else it just gets selected
if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) {
let scroll = result.row - terminal.buffer.active.viewportY;
@@ -463,4 +673,52 @@ export class SearchAddon implements ITerminalAddon {
}
return true;
}
/**
* Applies styles to the decoration when it is rendered
* @param element the decoration's element
* @param backgroundColor the background color to apply
* @param borderColor the border color to apply
* @param result the search result associated with the decoration
* @returns
*/
private _applyStyles(element: HTMLElement, backgroundColor: string | undefined, borderColor: string | undefined, result: ISearchResult): void {
if (element.clientWidth <= 0) {
return;
}
if (!element.classList.contains('xterm-find-result-decoration')) {
element.classList.add('xterm-find-result-decoration');
if (backgroundColor) {
element.style.backgroundColor = backgroundColor;
}
if (borderColor) {
element.style.outline = `1px solid ${borderColor}`;
}
}
}
/**
* Creates a decoration for the result and applies styles
* @param result the search result for which to create the decoration
* @param color the color to use for the decoration
* @returns the {@link IDecoration} or undefined if the marker has already been disposed of
*/
private _createResultDecoration(result: ISearchResult, decorations: ISearchDecorationOptions): IDecoration | undefined {
const terminal = this._terminal!;
const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row);
if (!marker || !decorations?.matchOverviewRuler) {
return undefined;
}
const findResultDecoration = terminal.registerDecoration({
marker,
x: result.col,
width: result.size,
overviewRulerOptions: this._resultDecorations?.get(marker.line) && !this._dataChanged ? undefined : {
color: decorations.matchOverviewRuler, position: 'center'
}
});
findResultDecoration?.onRender((e) => this._applyStyles(e, decorations.matchBackground, decorations.matchBorder, result));
findResultDecoration?.onDispose(() => marker.dispose());
return findResultDecoration;
}
}
+11 -1
View File
@@ -13,10 +13,20 @@
"strict": true,
"types": [
"../../../node_modules/@types/mocha"
]
],
"paths": {
"common/*": [
"../../../src/common/*"
]
}
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../../src/common"
}
]
}
@@ -134,7 +134,7 @@ describe('Search Tests', function(): void {
.replace(/\n/g, '\\n\\r');
}
fixture = fixture
.replace(/'/g, '\\\'');
.replace(/'/g, `\\'`);
});
it('should find all occurrences using findNext', async () => {
await writeSync(page, fixture);
+54 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { Terminal, ILinkMatcherOptions, IDisposable, ITerminalAddon } from 'xterm';
import { Terminal, ITerminalAddon, IEvent } from 'xterm';
declare module 'xterm-addon-search' {
/**
@@ -32,6 +32,47 @@ declare module 'xterm-addon-search' {
* `findNext`, not `findPrevious`.
*/
incremental?: boolean;
/**
* When set, will highlight all instances of the word on search and show
* them in the overview ruler if it's enabled.
*/
decorations?: ISearchDecorationOptions;
}
/**
* Options for showing decorations when searching.
*/
interface ISearchDecorationOptions {
/**
* The background color of a match.
*/
matchBackground?: string;
/**
* The border color of a match
*/
matchBorder?: string;
/**
* The overview ruler color of a match.
*/
matchOverviewRuler: string;
/**
* The background color for the currently active match.
*/
activeMatchBackground?: string;
/**
* The border color of the currently active match.
*/
activeMatchBorder?: string;
/**
* The overview ruler color of the currently active match.
*/
activeMatchColorOverviewRuler: string;
}
/**
@@ -64,5 +105,17 @@ declare module 'xterm-addon-search' {
* @param searchOptions The options for the search.
*/
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean;
/**
* Clears the decorations and selection
*/
public clearDecorations(): void;
/**
* When decorations are enabled, fires when
* the search results or the selected result changes,
* returning undefined if there are no matches.
*/
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number } | undefined>;
}
}
@@ -21,6 +21,13 @@ module.exports = {
}
]
},
resolve: {
modules: ['./node_modules'],
extensions: [ '.js' ],
alias: {
common: path.resolve('../../out/common')
}
},
output: {
filename: mainFile,
path: path.resolve('./lib'),
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-serialize",
"version": "0.6.1",
"version": "0.6.2",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -43,7 +43,7 @@ class TestSelectionService {
}
}
describe('xterm-addon-serialize html', () => {
describe('xterm-addon-serialize', () => {
let cm: ColorManager;
let dom: jsdom.JSDOM;
let document: Document;
@@ -83,123 +83,132 @@ describe('xterm-addon-serialize html', () => {
(terminal as any)._core._selectionService = selectionService;
});
it('empty terminal with selection turned off', () => {
const output = serializeAddon.serializeAsHTML();
assert.notEqual(output, '');
assert.equal((output.match(new RegExp('<div><span> {10}</span><\/div>', 'g')) || []).length, 2);
});
it('empty terminal with no selection', () => {
const output = serializeAddon.serializeAsHTML({
onlySelection: true
describe('text', () => {
it('restoring cursor styles', async () => {
await writeP(terminal, sgr('32') + '> ' + sgr('0'));
assert.equal(serializeAddon.serialize(), '\u001b[32m> \u001b[0m');
});
assert.equal(output, '');
});
it('basic terminal with selection', async () => {
await writeP(terminal, ' terminal ');
terminal.select(1, 0, 8);
const output = serializeAddon.serializeAsHTML({
onlySelection: true
describe('html', () => {
it('empty terminal with selection turned off', () => {
const output = serializeAddon.serializeAsHTML();
assert.notEqual(output, '');
assert.equal((output.match(/<div><span> {10}<\/span><\/div>/g) || []).length, 2);
});
assert.equal((output.match(new RegExp('<div><span>terminal<\/span><\/div>', 'g')) || []).length, 1, output);
});
it('cells with bold styling', async () => {
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'font-weight: bold;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with italic styling', async () => {
await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'font-style: italic;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with inverse styling', async () => {
await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'color: #000000; background-color: #BFBFBF;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with underline styling', async () => {
await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'text-decoration: underline;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with invisible styling', async () => {
await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'visibility: hidden;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with dim styling', async () => {
await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'opacity: 0.5;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with strikethrough styling', async () => {
await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'text-decoration: line-through;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with combined styling', async () => {
await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'font-weight: bold;\'> <\/span>', 'g')) || []).length, 1, output);
assert.equal((output.match(new RegExp('<span style=\'font-weight: bold; text-decoration: line-through;\'>termi<\/span>', 'g')) || []).length, 1, output);
assert.equal((output.match(new RegExp('<span style=\'text-decoration: line-through;\'>nal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with color styling', async () => {
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'color: #00ff00;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with background styling', async () => {
await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'background-color: #00ff00;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('empty terminal with default options', async () => {
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('color: #000000; background-color: #ffffff; font-family: courier-new, courier, monospace; font-size: 15px;', 'g')) || []).length, 1, output);
});
it('empty terminal with custom options', async () => {
terminal.options.fontFamily = 'verdana';
terminal.options.fontSize = 20;
terminal.options.theme = {
foreground: '#ff00ff',
background: '#00ff00'
};
const output = serializeAddon.serializeAsHTML({
includeGlobalBackground: true
it('empty terminal with no selection', () => {
const output = serializeAddon.serializeAsHTML({
onlySelection: true
});
assert.equal(output, '');
});
assert.equal((output.match(new RegExp('color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;', 'g')) || []).length, 1, output);
});
it('empty terminal with background included', async () => {
const output = serializeAddon.serializeAsHTML({
includeGlobalBackground: true
it('basic terminal with selection', async () => {
await writeP(terminal, ' terminal ');
terminal.select(1, 0, 8);
const output = serializeAddon.serializeAsHTML({
onlySelection: true
});
assert.equal((output.match(/<div><span>terminal<\/span><\/div>/g) || []).length, 1, output);
});
it('cells with bold styling', async () => {
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='font-weight: bold;'>terminal<\/span>/g) || []).length, 1, output);
});
it('cells with italic styling', async () => {
await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='font-style: italic;'>terminal<\/span>/g) || []).length, 1, output);
});
it('cells with inverse styling', async () => {
await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='color: #000000; background-color: #BFBFBF;'>terminal<\/span>/g) || []).length, 1, output);
});
it('cells with underline styling', async () => {
await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='text-decoration: underline;'>terminal<\/span>/g) || []).length, 1, output);
});
it('cells with invisible styling', async () => {
await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='visibility: hidden;'>terminal<\/span>/g) || []).length, 1, output);
});
it('cells with dim styling', async () => {
await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='opacity: 0.5;'>terminal<\/span>/g) || []).length, 1, output);
});
it('cells with strikethrough styling', async () => {
await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='text-decoration: line-through;'>terminal<\/span>/g) || []).length, 1, output);
});
it('cells with combined styling', async () => {
await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='font-weight: bold;'> <\/span>/g) || []).length, 1, output);
assert.equal((output.match(/<span style='font-weight: bold; text-decoration: line-through;'>termi<\/span>/g) || []).length, 1, output);
assert.equal((output.match(/<span style='text-decoration: line-through;'>nal<\/span>/g) || []).length, 1, output);
});
it('cells with color styling', async () => {
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
});
it('cells with background styling', async () => {
await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='background-color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
});
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);
});
it('empty terminal with custom options', async () => {
terminal.options.fontFamily = 'verdana';
terminal.options.fontSize = 20;
terminal.options.theme = {
foreground: '#ff00ff',
background: '#00ff00'
};
const output = serializeAddon.serializeAsHTML({
includeGlobalBackground: true
});
assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) || []).length, 1, output);
});
it('empty terminal with background included', async () => {
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(new RegExp('color: #ffffff; background-color: #000000; font-family: courier-new, courier, monospace; font-size: 15px;', 'g')) || []).length, 1, output);
});
});
@@ -7,6 +7,7 @@
import { Terminal, ITerminalAddon, IBuffer, IBufferCell, IBufferRange } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IAttributeData } from 'common/Types';
function constrain(value: number, low: number, high: number): number {
return Math.max(low, Math.min(value, high));
@@ -62,17 +63,17 @@ abstract class BaseSerializeHandler {
protected _serializeString(): string { return ''; }
}
function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean {
function equalFg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
return cell1.getFgColorMode() === cell2.getFgColorMode()
&& cell1.getFgColor() === cell2.getFgColor();
}
function equalBg(cell1: IBufferCell, cell2: IBufferCell): boolean {
function equalBg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
return cell1.getBgColorMode() === cell2.getBgColorMode()
&& cell1.getBgColor() === cell2.getBgColor();
}
function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
return cell1.isInverse() === cell2.isInverse()
&& cell1.isBold() === cell2.isBold()
&& cell1.isUnderline() === cell2.isUnderline()
@@ -229,7 +230,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
this._nullCellCount = 0;
}
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): number[] {
private _diffStyle(cell: IBufferCell | IAttributeData, oldCell: IBufferCell): number[] {
const sgrSeq: number[] = [];
const fgChanged = !equalFg(cell, oldCell);
const bgChanged = !equalBg(cell, oldCell);
@@ -393,6 +394,15 @@ class StringSerializeHandler extends BaseSerializeHandler {
moveRight(realCursorCol - this._lastCursorCol);
}
// Restore the cursor's current style, see https://github.com/xtermjs/xterm.js/issues/3677
// HACK: Internal API access since it's awkward to expose this in the API and serialize will
// likely be the only consumer
const curAttrData: IAttributeData = (this._terminal as any)._core._inputHandler._curAttrData;
const sgrSeq = this._diffStyle(curAttrData, this._cursorStyle);
if (sgrSeq.length > 0) {
content += `\u001b[${sgrSeq.join(';')}m`;
}
return content;
}
}
@@ -544,7 +554,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
return target;
}
targetLength = targetLength - target.length;
targetLength -= target.length;
if (targetLength > padString.length) {
padString += padString.repeat(targetLength / padString.length);
}
@@ -14,7 +14,7 @@ let page: Page;
const width = 800;
const height = 600;
const writeRawSync = (page: any, str: string): Promise<void> => writeSync(page, '\' +' + JSON.stringify(str) + '+ \'');
const writeRawSync = (page: any, str: string): Promise<void> => writeSync(page, `' +` + JSON.stringify(str) + `+ '`);
const testNormalScreenEqual = async (page: any, str: string): Promise<void> => {
await writeRawSync(page, str);
@@ -5,9 +5,10 @@
import { ILinkProvider, ILink, Terminal, IViewportRange } from 'xterm';
interface ILinkProviderOptions {
export interface ILinkProviderOptions {
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
leave?(event: MouseEvent, text: string): void;
urlRegex?: RegExp;
}
export class WebLinkProvider implements ILinkProvider {
@@ -3,8 +3,8 @@
* @license MIT
*/
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable, IViewportRange } from 'xterm';
import { WebLinkProvider } from './WebLinkProvider';
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable } from 'xterm';
import { ILinkProviderOptions, WebLinkProvider } from './WebLinkProvider';
const protocolClause = '(https?:\\/\\/)';
const domainCharacterSet = '[\\da-z\\.-]+';
@@ -40,12 +40,6 @@ function handleLink(event: MouseEvent, uri: string): void {
}
}
interface ILinkProviderOptions {
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
leave?(event: MouseEvent, text: string): void;
urlRegex?: RegExp;
}
export class WebLinksAddon implements ITerminalAddon {
private _linkMatcherId: number | undefined;
private _terminal: Terminal | undefined;
@@ -49,5 +49,10 @@ declare module 'xterm-addon-web-links' {
* happen even when tooltipCallback hasn't fired for the link yet.
*/
leave?(event: MouseEvent, text: string): void;
/**
* A callback to use instead of the default one.
*/
urlRegex?: RegExp;
}
}
@@ -238,7 +238,7 @@ export class WebglCharAtlas implements IDisposable {
const bg = this._config.colors.background.css;
if (bg.length === 9) {
// Remove bg alpha channel if present
return bg.substr(0, 7);
return bg.slice(0, 7);
}
return bg;
}
@@ -260,9 +260,9 @@ describe('WebGL Renderer Integration Tests', async () => {
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.substr(1, 2), 16);
const g = parseInt(cssColor.substr(3, 2), 16);
const b = parseInt(cssColor.substr(5, 2), 16);
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]);
}
}
@@ -280,9 +280,9 @@ describe('WebGL Renderer Integration Tests', async () => {
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.substr(1, 2), 16);
const g = parseInt(cssColor.substr(3, 2), 16);
const b = parseInt(cssColor.substr(5, 2), 16);
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]);
}
}
@@ -300,9 +300,9 @@ describe('WebGL Renderer Integration Tests', async () => {
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.substr(1, 2), 16);
const g = parseInt(cssColor.substr(3, 2), 16);
const b = parseInt(cssColor.substr(5, 2), 16);
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]);
}
}
@@ -320,9 +320,9 @@ describe('WebGL Renderer Integration Tests', async () => {
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.substr(1, 2), 16);
const g = parseInt(cssColor.substr(3, 2), 16);
const b = parseInt(cssColor.substr(5, 2), 16);
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]);
}
}
@@ -356,9 +356,9 @@ describe('WebGL Renderer Integration Tests', async () => {
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.substr(1, 2), 16);
const g = parseInt(cssColor.substr(3, 2), 16);
const b = parseInt(cssColor.substr(5, 2), 16);
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]);
}
}
+3 -3
View File
@@ -104,11 +104,11 @@ function getNextBetaVersion(packageJson) {
return `${nextStableVersion}-${tag}.1`;
}
const latestPublishedVersion = publishedVersions.sort((a, b) => {
const aVersion = parseInt(a.substr(a.search(/\d+$/)));
const bVersion = parseInt(b.substr(b.search(/\d+$/)));
const aVersion = parseInt(a.slice(a.search(/\d+$/)));
const bVersion = parseInt(b.slice(b.search(/\d+$/)));
return aVersion > bVersion ? -1 : 1;
})[0];
const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/\d+$/)), 10);
const latestTagVersion = parseInt(latestPublishedVersion.slice(latestPublishedVersion.search(/\d+$/)), 10);
return `${nextStableVersion}-${tag}.${latestTagVersion + 1}`;
}
+7
View File
@@ -178,3 +178,10 @@
z-index: 6;
position: absolute;
}
.xterm-decoration-overview-ruler {
z-index: 7;
position: absolute;
top: 0;
right: 0;
}
+27 -5
View File
@@ -92,6 +92,7 @@ const addons: { [T in AddonType]: IDemoAddon<T>} = {
const terminalContainer = document.getElementById('terminal-container');
const actionElements = {
find: <HTMLInputElement>document.querySelector('#find'),
findNext: <HTMLInputElement>document.querySelector('#find-next'),
findPrevious: <HTMLInputElement>document.querySelector('#find-previous')
};
@@ -107,7 +108,15 @@ function getSearchOptions(e: KeyboardEvent): ISearchOptions {
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`
incremental: e.key !== `Enter`,
decorations: (document.getElementById('highlight-all-matches') as HTMLInputElement).checked ? {
matchBackground: '#55575380',
matchBorder: '#555753',
matchOverviewRuler: '#555753',
activeMatchBackground: '#ef292980',
activeMatchBorder: '#ef2929',
activeMatchColorOverviewRuler: '#ef2929'
} : undefined
};
}
@@ -151,6 +160,7 @@ if (document.location.pathname === '/test') {
document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler);
document.getElementById('load-test').addEventListener('click', loadTest);
document.getElementById('add-decoration').addEventListener('click', addDecoration);
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
}
function createTerminal(): void {
@@ -544,9 +554,21 @@ function loadTest() {
}
function addDecoration() {
term.options['overviewRulerWidth'] = 15;
const marker = term.addMarker(1);
const decoration = term.registerDecoration({ marker });
decoration.onRender(() => {
decoration.element.style.backgroundColor = 'red';
});
const decoration = term.registerDecoration({ marker, overviewRulerOptions: { color: '#ef2929'} });
decoration.onRender((e) => e.style.backgroundColor = '#ef2929');
}
function addOverviewRuler() {
term.options['overviewRulerWidth'] = 15;
term.registerDecoration({marker: term.addMarker(1), overviewRulerOptions: { color: '#ef2929' }});
term.registerDecoration({marker: term.addMarker(3), overviewRulerOptions: { color: '#8ae234' }});
term.registerDecoration({marker: term.addMarker(5), overviewRulerOptions: { color: '#729fcf' }});
term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#ef2929', position: 'left' }});
term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#8ae234', position: 'center' }});
term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#729fcf', position: 'right' }});
term.registerDecoration({marker: term.addMarker(10), overviewRulerOptions: { color: '#8ae234', position: 'center' }});
term.registerDecoration({marker: term.addMarker(10), overviewRulerOptions: { color: '#ffffff80', position: 'full' }});
}
+2
View File
@@ -43,6 +43,7 @@
<label><input type="checkbox" id="regex"/>Use regex</label>
<label><input type="checkbox" id="case-sensitive"/>Case sensitive</label>
<label><input type="checkbox" id="whole-word"/>Whole word</label>
<label><input type="checkbox" id="highlight-all-matches"/>Highlight All Matches</label>
</div>
<h4>SerializeAddon</h4>
<div>
@@ -69,6 +70,7 @@
<button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button>
<button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button>
<button id="add-decoration" title="Add a decoration to the terminal">Decoration</button>
<button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button>
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More