Merge branch 'master' into debugging

This commit is contained in:
Daniel Imms
2018-12-27 14:09:48 -08:00
committed by GitHub
18 changed files with 229 additions and 106 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
--- ---
name: Question name: Question
about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs or https://gitter.im/sourcelair/xterm.js. about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs
--- ---
🛑 The issue tracker is not for questions 🛑 🛑 The issue tracker is not for questions 🛑
If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs or https://gitter.im/sourcelair/xterm.js. If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs.
+20 -39
View File
@@ -2,23 +2,20 @@
[![Build Status](https://dev.azure.com/xtermjs/xterm.js/_apis/build/status/xtermjs.xterm.js)](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3) [![Build Status](https://dev.azure.com/xtermjs/xterm.js/_apis/build/status/xtermjs.xterm.js)](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3)
[![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master) [![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master)
[![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm)
Xterm.js is a terminal front-end component written in JavaScript that works in the browser. Xterm.js is a front-end component written in TypeScript that lets applications bring fully-featured terminals to their users in the browser. It's used by popular projects such as VS Code, Hyper and Theia.
It enables applications to provide fully featured terminals to their users and create great development experiences.
## Features ## Features
- **Text-based application support**: Use xterm.js to work with applications like `bash`, `git` etc.
- **Curses-based application support**: Use xterm.js to work with applications like `vim`, `tmux` etc. - **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim` and `tmux`, this includes support for curses-based apps and mouse event support
- **Mouse events support**: Xterm.js captures mouse events like click and scroll and passes them to the terminal's back-end controlling process - **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer
- **CJK (Chinese, Japanese, Korean) character support**: Xterm.js renders CJK characters seamlessly - **Rich unicode support**: Supports CJK, emojis and IMEs
- **IME support**: Insert international (including CJK) characters using IME input with your keyboard - **Self-contained**: Requires zero dependencies to work
- **Self-contained library**: Xterm.js works on its own. It does not require any external libraries like jQuery or React to work - **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option
- **Modular, event-based API**: Lets you build addons and themes with ease - **And much more**: Links, theming, addons, well documented API, etc.
## What xterm.js is not ## What xterm.js is not
- Xterm.js is not a terminal application that you can download and use on your computer - Xterm.js is not a terminal application that you can download and use on your computer
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output) - Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output)
@@ -30,7 +27,7 @@ First you need to install the module, we ship exclusively through [npm](https://
npm install xterm npm install xterm
``` ```
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself. To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself. Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
```html ```html
<!doctype html> <!doctype html>
@@ -50,33 +47,24 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
</html> </html>
``` ```
Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
### Importing ### Importing
The proposed way to load xterm.js is via the ES6 module syntax. The recommended way to load xterm.js is via the ES6 module syntax:
```javascript ```javascript
import { Terminal } from 'xterm'; import { Terminal } from 'xterm';
``` ```
### API
The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
Note that some APIs are marked *experimental*, these are added so we can experiment with new ideas without committing to support it like a normal semver API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs.
### Addons ### Addons
Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own, by using xterm.js' public API. Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own by using the [public API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts).
To use an addon, just import the JavaScript module and pass it to `Terminal`'s `applyAddon` method: To use an addon, just import the JavaScript module and pass it to `Terminal`'s `applyAddon` method:
```javascript ```javascript
import { Terminal } from xterm; import { Terminal } from 'xterm';
import * as fit from 'xterm/lib/addons/fit/fit'; import * as fit from 'xterm/lib/addons/fit/fit';
Terminal.applyAddon(fit); Terminal.applyAddon(fit);
var xterm = new Terminal(); // Instantiate the terminal var xterm = new Terminal(); // Instantiate the terminal
@@ -87,25 +75,16 @@ You will also need to include the addon's CSS file if it has one in the folder.
#### Importing Addons in TypeScript #### Importing Addons in TypeScript
There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(<any>xterm).fit()`. There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(xterm as any).fit()`. Alternatively, you can import the addon function and enhance the terminal on demand. This has better typing support and is friendly to treeshaking.
Alternatively, you can import addon function and enhance the terminal on demand. This would have better typing support and is friendly to treeshaking. E.g.:
```typescript ```typescript
import { Terminal } from 'xterm'; import { Terminal } from 'xterm';
import { fit } from 'xterm/lib/addons/fit/fit'; import { fit } from 'xterm/lib/addons/fit/fit';
const xterm = new Terminal(); const xterm = new Terminal();
// Fit the terminal when necessary: fit(xterm); // Fit the terminal when necessary
fit(xterm);
``` ```
#### Third party addons
There are also the following third party addons available:
- [xterm-webfont](https://www.npmjs.com/package/xterm-webfont)
## Browser Support ## Browser Support
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support: Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support:
@@ -116,11 +95,13 @@ Since xterm.js is typically implemented as a developer tool, only modern browser
- Safari latest - Safari latest
- IE11 - IE11
Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working. Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers, these are the versions we strive to keep working.
## API ## API
The current full API documentation is available in the [TypeScript declaration file on the repository](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), switch the tag (press `w` when viewing the file) to point at the specific version tag you're using. The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs.
## Real-world uses ## Real-world uses
Xterm.js is used in several world-class applications to provide great terminal experiences. Xterm.js is used in several world-class applications to provide great terminal experiences.
@@ -182,7 +163,7 @@ Do you use xterm.js in your application as well? Please [open a Pull Request](ht
Xterm.js follows a monthly release cycle roughly. Xterm.js follows a monthly release cycle roughly.
The existing releases are available at this GitHub repo's [Releases](https://github.com/sourcelair/xterm.js/releases), while the roadmap is available as [Milestones](https://github.com/sourcelair/xterm.js/milestones). All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), while a rough roadmap is available by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
## Contributing ## Contributing
+3 -3
View File
@@ -113,9 +113,9 @@ function createTerminal(): void {
}); });
addDomListener(actionElements.findPrevious, 'keyup', (e) => { addDomListener(actionElements.findPrevious, 'keyup', (e) => {
const searchOptions = getSearchOptions(); if (e.key === `Enter`) {
searchOptions.incremental = e.key !== `Enter`; term.findPrevious(actionElements.findPrevious.value, getSearchOptions());
term.findPrevious(actionElements.findPrevious.value, searchOptions); }
}); });
// fit is called within a setTimeout, cols and rows need this. // fit is called within a setTimeout, cols and rows need this.
+1 -1
View File
@@ -275,7 +275,7 @@ export class Buffer implements IBuffer {
* @param startCol The column to start at. * @param startCol The column to start at.
* @param endCol The column to end at. * @param endCol The column to end at.
*/ */
public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol: number = null): string { public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol?: number): string {
const line = this.lines.get(lineIndex); const line = this.lines.get(lineIndex);
if (!line) { if (!line) {
return ''; return '';
+5
View File
@@ -325,5 +325,10 @@ describe('BufferLine', function(): void {
chai.expect(line.translateToString(false)).equal(' '); chai.expect(line.translateToString(false)).equal(' ');
chai.expect(line.translateToString(true)).equal(''); chai.expect(line.translateToString(true)).equal('');
}); });
it('should work with endCol=0', () => {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
chai.expect(line.translateToString(true, 0, 0)).equal('');
});
}); });
}); });
+6 -8
View File
@@ -118,13 +118,12 @@ export class BufferLineJSArray implements IBufferLine {
return 0; return 0;
} }
public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = null): string { public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string {
let length = endCol || this.length;
if (trimRight) { if (trimRight) {
length = Math.min(length, this.getTrimmedLength()); endCol = Math.min(endCol, this.getTrimmedLength());
} }
let result = ''; let result = '';
while (startCol < length) { while (startCol < endCol) {
result += this.get(startCol)[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR; result += this.get(startCol)[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR;
startCol += this.get(startCol)[CHAR_DATA_WIDTH_INDEX] || 1; startCol += this.get(startCol)[CHAR_DATA_WIDTH_INDEX] || 1;
} }
@@ -305,13 +304,12 @@ export class BufferLine implements IBufferLine {
return 0; return 0;
} }
public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = null): string { public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string {
let length = endCol || this.length;
if (trimRight) { if (trimRight) {
length = Math.min(length, this.getTrimmedLength()); endCol = Math.min(endCol, this.getTrimmedLength());
} }
let result = ''; let result = '';
while (startCol < length) { while (startCol < endCol) {
const stringData = this._data[startCol * CELL_SIZE + Cell.STRING]; const stringData = this._data[startCol * CELL_SIZE + Cell.STRING];
result += (stringData & IS_COMBINED_BIT_MASK) ? this._combined[startCol] : (stringData) ? String.fromCharCode(stringData) : WHITESPACE_CELL_CHAR; result += (stringData & IS_COMBINED_BIT_MASK) ? this._combined[startCol] : (stringData) ? String.fromCharCode(stringData) : WHITESPACE_CELL_CHAR;
startCol += this._data[startCol * CELL_SIZE + Cell.WIDTH] || 1; startCol += this._data[startCol * CELL_SIZE + Cell.WIDTH] || 1;
+1 -1
View File
@@ -198,7 +198,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
} }
} else { } else {
// Get first row // Get first row
const startRowEndCol = start[1] === end[1] ? end[0] : null; const startRowEndCol = start[1] === end[1] ? end[0] : undefined;
result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol)); result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
// Get middle rows // Get middle rows
+1
View File
@@ -22,6 +22,7 @@ export function toggleFullScreen(term: Terminal, fullscreen: boolean): void {
fn = term.element.classList.add; fn = term.element.classList.add;
} }
fn = fn.bind(term.element.classList);
fn('fullscreen'); fn('fullscreen');
} }
+4 -1
View File
@@ -25,7 +25,10 @@ export interface ISearchOptions {
regex?: boolean; regex?: boolean;
wholeWord?: boolean; wholeWord?: boolean;
caseSensitive?: boolean; caseSensitive?: boolean;
/** Assume caller implements 'search as you type' where findNext gets called when search input changes */ /**
* Use this when you want the selection to expand if it still matches as the
* user types. Note that this only affects findNext.
*/
incremental?: boolean; incremental?: boolean;
} }
+73 -42
View File
@@ -41,6 +41,7 @@ export class SearchHelper implements ISearchHelper {
return false; return false;
} }
let startCol: number = 0;
let startRow = this._terminal._core.buffer.ydisp; let startRow = this._terminal._core.buffer.ydisp;
if (selectionManager.selectionEnd) { if (selectionManager.selectionEnd) {
@@ -48,23 +49,30 @@ export class SearchHelper implements ISearchHelper {
// For incremental search, use existing row // For incremental search, use existing row
if (this._terminal.getSelection().length !== 0) { if (this._terminal.getSelection().length !== 0) {
startRow = incremental ? selectionManager.selectionStart[1] : selectionManager.selectionEnd[1]; startRow = incremental ? selectionManager.selectionStart[1] : selectionManager.selectionEnd[1];
startCol = incremental ? selectionManager.selectionStart[0] : selectionManager.selectionEnd[0];
} }
} }
this._initLinesCache(); this._initLinesCache();
// Search from startRow to end // Search startRow
for (let y = incremental ? startRow : startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { result = this._findInLine(term, startRow, startCol, searchOptions);
result = this._findInLine(term, y, searchOptions);
if (result) { // Search from startRow + 1 to end
break; if (!result) {
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
result = this._findInLine(term, y, 0, searchOptions);
if (result) {
break;
}
} }
} }
// Search from the top to the startRow // Search from the top to the startRow (search the whole startRow again in
// case startCol > 0)
if (!result) { if (!result) {
for (let y = 0; y < startRow; y++) { for (let y = 0; y <= startRow; y++) {
result = this._findInLine(term, y, searchOptions); result = this._findInLine(term, y, 0, searchOptions);
if (result) { if (result) {
break; break;
} }
@@ -84,7 +92,6 @@ export class SearchHelper implements ISearchHelper {
*/ */
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean { public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
const selectionManager = this._terminal._core.selectionManager; const selectionManager = this._terminal._core.selectionManager;
const {incremental} = searchOptions;
let result: ISearchResult; let result: ISearchResult;
if (!term || term.length === 0) { if (!term || term.length === 0) {
@@ -92,29 +99,39 @@ export class SearchHelper implements ISearchHelper {
return false; return false;
} }
const isReverseSearch = true;
let startRow = this._terminal._core.buffer.ydisp; let startRow = this._terminal._core.buffer.ydisp;
let startCol: number = this._terminal._core.buffer.lines.get(startRow).length;
if (selectionManager.selectionStart) { if (selectionManager.selectionStart) {
// Start from the selection start if there is a selection // Start from the selection start if there is a selection
if (this._terminal.getSelection().length !== 0) { if (this._terminal.getSelection().length !== 0) {
startRow = selectionManager.selectionStart[1]; startRow = selectionManager.selectionStart[1];
startCol = selectionManager.selectionStart[0];
} }
} }
this._initLinesCache(); this._initLinesCache();
// Search from startRow to top // Search startRow
for (let y = incremental ? startRow : startRow - 1; y >= 0; y--) { result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch);
result = this._findInLine(term, y, searchOptions);
if (result) { // Search from startRow - 1 to top
break; if (!result) {
for (let y = startRow - 1; y >= 0; y--) {
result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch);
if (result) {
break;
}
} }
} }
// Search from the bottom to startRow // Search from the bottom to startRow (search the whole startRow again in
// case startCol > 0)
if (!result) { if (!result) {
for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) { const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1;
result = this._findInLine(term, y, searchOptions); for (let y = searchFrom; y >= startRow; y--) {
result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch);
if (result) { if (result) {
break; break;
} }
@@ -164,72 +181,86 @@ export class SearchHelper implements ISearchHelper {
* started on an earlier line then it is skipped since it will be properly searched when the terminal line that the * started on an earlier line then it is skipped since it will be properly searched when the terminal line that the
* text starts on is searched. * text starts on is searched.
* @param term The search term. * @param term The search term.
* @param y The line to search. * @param row The line to start the search from.
* @param col The column to start the search from.
* @param searchOptions Search options. * @param searchOptions Search options.
* @return The search result if it was found. * @return The search result if it was found.
*/ */
protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult { protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult {
if (this._terminal._core.buffer.lines.get(y).isWrapped) { if (this._terminal._core.buffer.lines.get(row).isWrapped) {
return; return;
} }
let stringLine = this._linesCache ? this._linesCache[y] : void 0; let stringLine = this._linesCache ? this._linesCache[row] : void 0;
if (stringLine === void 0) { if (stringLine === void 0) {
stringLine = this.translateBufferLineToStringWithWrap(y, true); stringLine = this.translateBufferLineToStringWithWrap(row, true);
if (this._linesCache) { if (this._linesCache) {
this._linesCache[y] = stringLine; this._linesCache[row] = stringLine;
} }
} }
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase(); const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
let searchIndex = -1; const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
let resultIndex = -1;
if (searchOptions.regex) { if (searchOptions.regex) {
const searchRegex = RegExp(searchTerm, 'g'); const searchRegex = RegExp(searchTerm, 'g');
const foundTerm = searchRegex.exec(searchStringLine); let foundTerm: RegExpExecArray;
if (foundTerm && foundTerm[0].length > 0) { if (isReverseSearch) {
searchIndex = searchRegex.lastIndex - foundTerm[0].length; // This loop will get the resultIndex of the _last_ regex match in the range 0..col
term = foundTerm[0]; while (foundTerm = searchRegex.exec(searchStringLine.slice(0, col))) {
resultIndex = searchRegex.lastIndex - foundTerm[0].length;
term = foundTerm[0];
searchRegex.lastIndex -= (term.length - 1);
}
} else {
foundTerm = searchRegex.exec(searchStringLine.slice(col));
if (foundTerm && foundTerm[0].length > 0) {
resultIndex = col + (searchRegex.lastIndex - foundTerm[0].length);
term = foundTerm[0];
}
} }
} else { } else {
searchIndex = searchStringLine.indexOf(searchTerm); if (isReverseSearch) {
resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length);
} else {
resultIndex = searchStringLine.indexOf(searchTerm, col);
}
} }
if (searchIndex >= 0) { if (resultIndex >= 0) {
// Adjust the row number and search index if needed since a "line" of text can span multiple rows // Adjust the row number and search index if needed since a "line" of text can span multiple rows
if (searchIndex >= this._terminal.cols) { if (resultIndex >= this._terminal.cols) {
y += Math.floor(searchIndex / this._terminal.cols); row += Math.floor(resultIndex / this._terminal.cols);
searchIndex = searchIndex % this._terminal.cols; resultIndex = resultIndex % this._terminal.cols;
} }
if (searchOptions.wholeWord && !this._isWholeWord(searchIndex, searchStringLine, term)) { if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
return; return;
} }
const line = this._terminal._core.buffer.lines.get(y); const line = this._terminal._core.buffer.lines.get(row);
for (let i = 0; i < searchIndex; i++) { for (let i = 0; i < resultIndex; i++) {
const charData = line.get(i); const charData = line.get(i);
// Adjust the searchIndex to normalize emoji into single chars // Adjust the searchIndex to normalize emoji into single chars
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/]; const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
if (char.length > 1) { if (char.length > 1) {
searchIndex -= char.length - 1; resultIndex -= char.length - 1;
} }
// Adjust the searchIndex for empty characters following wide unicode // Adjust the searchIndex for empty characters following wide unicode
// chars (eg. CJK) // chars (eg. CJK)
const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/]; const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/];
if (charWidth === 0) { if (charWidth === 0) {
searchIndex++; resultIndex++;
} }
} }
return { return {
term, term,
col: searchIndex, col: resultIndex,
row: y row
}; };
} }
} }
/** /**
* Translates a buffer line to a string, including subsequent lines if they are wraps. * Translates a buffer line to a string, including subsequent lines if they are wraps.
* Wide characters will count as two columns in the resulting string. This * Wide characters will count as two columns in the resulting string. This
+66 -2
View File
@@ -29,8 +29,11 @@ class MockTerminal {
} }
class TestSearchHelper extends SearchHelper { class TestSearchHelper extends SearchHelper {
public findInLine(term: string, y: number, searchOptions?: ISearchOptions): ISearchResult { public findInLine(term: string, rowNumber: number, searchOptions?: ISearchOptions): ISearchResult {
return this._findInLine(term, y, searchOptions); return this._findInLine(term, rowNumber, 0, searchOptions);
}
public findFromIndex(term: string, row: number, col: number, searchOptions?: ISearchOptions, isReverseSearch?: boolean): ISearchResult {
return this._findInLine(term, row, col, searchOptions, isReverseSearch);
} }
} }
@@ -245,5 +248,66 @@ describe('search addon', () => {
expect(hello4).eql(undefined); expect(hello4).eql(undefined);
expect(hello5).eql(undefined); expect(hello5).eql(undefined);
}); });
it('should find multiple matches in line', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('helloooo helloooo\r\naaaAAaaAAA');
term.pushWriteData();
const searchOptions = {
regex: false,
wholeWord: false,
caseSensitive: false
};
const find0 = term.searchHelper.findFromIndex('hello', 0, 0, searchOptions);
const find1 = term.searchHelper.findFromIndex('hello', 0, find0.col + find0.term.length, searchOptions);
const find2 = term.searchHelper.findFromIndex('aaaa', 1, 0, searchOptions);
const find3 = term.searchHelper.findFromIndex('aaaa', 1, find2.col + find2.term.length, searchOptions);
const find4 = term.searchHelper.findFromIndex('aaaa', 1, find3.col + find3.term.length, searchOptions);
expect(find0).eql({col: 0, row: 0, term: 'hello'});
expect(find1).eql({col: 9, row: 0, term: 'hello'});
expect(find2).eql({col: 0, row: 1, term: 'aaaa'});
expect(find3).eql({col: 4, row: 1, term: 'aaaa'});
expect(find4).eql(undefined);
});
it('should find multiple matches in line - reverse search', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('it is what it is');
term.pushWriteData();
const searchOptions = {
regex: false,
wholeWord: false,
caseSensitive: false
};
const isReverseSearch = true;
const find0 = term.searchHelper.findFromIndex('is', 0, 16, searchOptions, isReverseSearch);
const find1 = term.searchHelper.findFromIndex('is', 0, find0.col, searchOptions, isReverseSearch);
const find2 = term.searchHelper.findFromIndex('it', 0, 16, searchOptions, isReverseSearch);
const find3 = term.searchHelper.findFromIndex('it', 0, find2.col, searchOptions, isReverseSearch);
expect(find0).eql({col: 14, row: 0, term: 'is'});
expect(find1).eql({col: 3, row: 0, term: 'is'});
expect(find2).eql({col: 11, row: 0, term: 'it'});
expect(find3).eql({col: 0, row: 0, term: 'it'});
});
it('should find multiple matches in line - reverse search with regex', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('zzzABCzzzzABCABC');
term.pushWriteData();
const searchOptions = {
regex: true,
wholeWord: false,
caseSensitive: true
};
const isReverseSearch = true;
const find0 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, 16, searchOptions, isReverseSearch);
const find1 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find0.col, searchOptions, isReverseSearch);
const find2 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find1.col, searchOptions, isReverseSearch);
const find3 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find2.col, searchOptions, isReverseSearch);
expect(find0).eql({col: 13, row: 0, term: 'ABC'});
expect(find1).eql({col: 10, row: 0, term: 'ABC'});
expect(find2).eql({col: 3, row: 0, term: 'ABC'});
expect(find3).eql(undefined);
});
}); });
}); });
+24
View File
@@ -63,4 +63,28 @@ describe('webLinks addon', () => {
assert.equal(uri, 'http://foo.com/colon:test'); assert.equal(uri, 'http://foo.com/colon:test');
}); });
it('should not allow " character at the end of a URI enclosed with ""', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = '"http://foo.com/"';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/');
});
it('should not allow \' character at the end of a URI enclosed with \'\'', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = '\'http://foo.com/\'';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/');
});
}); });
+1 -1
View File
@@ -14,7 +14,7 @@ const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
const localHostClause = '(localhost)'; const localHostClause = '(localhost)';
const portClause = '(:\\d{1,5})'; const portClause = '(:\\d{1,5})';
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?'; const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:\\s])'; const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:"\'\\s])';
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*'; const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
+3 -2
View File
@@ -7,7 +7,8 @@ import { Terminal } from 'xterm';
import { IWinptyCompatAddonTerminal } from './Interfaces'; import { IWinptyCompatAddonTerminal } from './Interfaces';
const CHAR_DATA_CODE_INDEX = 3; const CHAR_DATA_CODE_INDEX = 3;
const NULL_CELL_CODE = 32; const NULL_CELL_CODE = 0;
const WHITESPACE_CELL_CODE = 32;
export function winptyCompatInit(terminal: Terminal): void { export function winptyCompatInit(terminal: Terminal): void {
const addonTerminal = <IWinptyCompatAddonTerminal>terminal; const addonTerminal = <IWinptyCompatAddonTerminal>terminal;
@@ -32,7 +33,7 @@ export function winptyCompatInit(terminal: Terminal): void {
const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1); const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);
const lastChar = line.get(addonTerminal.cols - 1); const lastChar = line.get(addonTerminal.cols - 1);
if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE) { if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE) {
const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y); const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
nextLine.isWrapped = true; nextLine.isWrapped = true;
} }
+11
View File
@@ -281,5 +281,16 @@ describe('Keyboard', () => {
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }).key, '\x1b[B'); assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }).key, '\x1b[B');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }, { applicationCursorMode: true }).key, '\x1bOB'); assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }, { applicationCursorMode: true }).key, '\x1bOB');
}); });
it('should handle lowercase letters', () => {
assert.equal(testEvaluateKeyboardEvent({ keyCode: 65, key: 'a' }).key, 'a');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 189, key: '-' }).key, '-');
});
it('should handle uppercase letters', () => {
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 65, key: 'A' }).key, 'A');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 49, key: '!' }).key, '!');
});
}); });
}); });
+4
View File
@@ -349,6 +349,10 @@ export function evaluateKeyboardEvent(
if (ev.keyCode === 65) { // cmd + a if (ev.keyCode === 65) { // cmd + a
result.type = KeyboardResultType.SELECT_ALL; result.type = KeyboardResultType.SELECT_ALL;
} }
} else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey &&
ev.keyCode >= 48 && ev.keyCode !== 144 && ev.keyCode !== 145) {
// Include only keys that that result in a character; don't include num lock and scroll lock
result.key = ev.key;
} }
break; break;
} }
+1 -1
View File
@@ -38,7 +38,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure {
} }
public measure(options: ITerminalOptions): void { public measure(options: ITerminalOptions): void {
this._measureElement.style.fontFamily = options.fontFamily; this._measureElement.style.fontFamily = options.fontFamily;
this._measureElement.style.fontSize = `${options.fontSize}px`; this._measureElement.style.fontSize = `${options.fontSize}px`;
const geometry = this._measureElement.getBoundingClientRect(); const geometry = this._measureElement.getBoundingClientRect();
// The element is likely currently display:none, we should retain the // The element is likely currently display:none, we should retain the
+3 -3
View File
@@ -4469,9 +4469,9 @@ nanomatch@^1.2.9:
to-regex "^3.0.1" to-regex "^3.0.1"
natives@^1.1.0: natives@^1.1.0:
version "1.1.4" version "1.1.6"
resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.4.tgz#2f0f224fc9a7dd53407c7667c84cf8dbe773de58" resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.6.tgz#a603b4a498ab77173612b9ea1acdec4d980f00bb"
integrity sha512-Q29yeg9aFKwhLVdkTAejM/HvYG0Y1Am1+HUkFQGn5k2j8GS+v60TVmZh6nujpEAj/qql+wGUrlryO8bF+b1jEg== integrity sha512-6+TDFewD4yxY14ptjKaS63GVdtKiES1pTPyxn9Jb0rBqPMZ7VcCiooEhPNsr+mqHtMGxa/5c/HhcC4uPEUw/nA==
needle@^2.2.1: needle@^2.2.1:
version "2.2.1" version "2.2.1"