mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge branch 'master' into perf3
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
---
|
||||
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 🛑
|
||||
|
||||
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.
|
||||
|
||||
@@ -2,23 +2,20 @@
|
||||
|
||||
[](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3)
|
||||
[](https://coveralls.io/github/xtermjs/xterm.js?branch=master)
|
||||
[](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](https://www.jsdelivr.com/package/npm/xterm)
|
||||
|
||||
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
|
||||
|
||||
It enables applications to provide fully featured terminals to their users and create great development experiences.
|
||||
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.
|
||||
|
||||
## 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.
|
||||
- **Mouse events support**: Xterm.js captures mouse events like click and scroll and passes them to the terminal's back-end controlling process
|
||||
- **CJK (Chinese, Japanese, Korean) character support**: Xterm.js renders CJK characters seamlessly
|
||||
- **IME support**: Insert international (including CJK) characters using IME input with your keyboard
|
||||
- **Self-contained library**: Xterm.js works on its own. It does not require any external libraries like jQuery or React to work
|
||||
- **Modular, event-based API**: Lets you build addons and themes with ease
|
||||
|
||||
- **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
|
||||
- **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer
|
||||
- **Rich unicode support**: Supports CJK, emojis and IMEs
|
||||
- **Self-contained**: Requires zero dependencies to work
|
||||
- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option
|
||||
- **And much more**: Links, theming, addons, well documented API, etc.
|
||||
|
||||
## 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 `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
|
||||
```
|
||||
|
||||
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
|
||||
<!doctype html>
|
||||
@@ -50,33 +47,24 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
|
||||
</html>
|
||||
```
|
||||
|
||||
Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
|
||||
|
||||
### 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
|
||||
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 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:
|
||||
|
||||
```javascript
|
||||
import { Terminal } from xterm;
|
||||
import { Terminal } from 'xterm';
|
||||
import * as fit from 'xterm/lib/addons/fit/fit';
|
||||
|
||||
|
||||
Terminal.applyAddon(fit);
|
||||
|
||||
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
|
||||
|
||||
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()`.
|
||||
|
||||
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.:
|
||||
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.
|
||||
|
||||
```typescript
|
||||
import { Terminal } from 'xterm';
|
||||
import { fit } from 'xterm/lib/addons/fit/fit';
|
||||
const xterm = new Terminal();
|
||||
|
||||
// Fit the terminal when necessary:
|
||||
fit(xterm);
|
||||
fit(xterm); // Fit the terminal when necessary
|
||||
```
|
||||
|
||||
#### Third party addons
|
||||
|
||||
There are also the following third party addons available:
|
||||
|
||||
- [xterm-webfont](https://www.npmjs.com/package/xterm-webfont)
|
||||
|
||||
## 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:
|
||||
@@ -116,11 +95,13 @@ Since xterm.js is typically implemented as a developer tool, only modern browser
|
||||
- Safari latest
|
||||
- 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
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
+17
-19
@@ -14,6 +14,7 @@ import * as fullscreen from '../lib/addons/fullscreen/fullscreen';
|
||||
import * as search from '../lib/addons/search/search';
|
||||
import * as webLinks from '../lib/addons/webLinks/webLinks';
|
||||
import * as winptyCompat from '../lib/addons/winptyCompat/winptyCompat';
|
||||
import { ISearchOptions } from '../lib/addons/search/Interfaces';
|
||||
|
||||
// Pulling in the module's types relies on the <reference> above, it's looks a
|
||||
// little weird here as we're importing "this" module
|
||||
@@ -50,6 +51,14 @@ function setPadding(): void {
|
||||
term.fit();
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
createTerminal();
|
||||
|
||||
const disposeRecreateButtonHandler = () => {
|
||||
@@ -97,26 +106,15 @@ function createTerminal(): void {
|
||||
|
||||
addDomListener(paddingElement, 'change', setPadding);
|
||||
|
||||
addDomListener(actionElements.findNext, 'keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const searchOptions = {
|
||||
regex: (document.getElementById('regex') as HTMLInputElement).checked,
|
||||
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
|
||||
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
|
||||
};
|
||||
term.findNext(actionElements.findNext.value, searchOptions);
|
||||
}
|
||||
addDomListener(actionElements.findNext, 'keyup', (e) => {
|
||||
const searchOptions = getSearchOptions();
|
||||
searchOptions.incremental = e.key !== `Enter`;
|
||||
term.findNext(actionElements.findNext.value, searchOptions);
|
||||
});
|
||||
addDomListener(actionElements.findPrevious, 'keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const searchOptions = {
|
||||
regex: (document.getElementById('regex') as HTMLInputElement).checked,
|
||||
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
|
||||
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
|
||||
};
|
||||
term.findPrevious(actionElements.findPrevious.value, searchOptions);
|
||||
|
||||
addDomListener(actionElements.findPrevious, 'keyup', (e) => {
|
||||
if (e.key === `Enter`) {
|
||||
term.findPrevious(actionElements.findPrevious.value, getSearchOptions());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+34
-6
@@ -355,7 +355,7 @@ describe('Buffer', () => {
|
||||
let terminal: TestTerminal;
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new TestTerminal({rows: 5, cols: 10});
|
||||
terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
|
||||
});
|
||||
|
||||
it('multiline ascii', () => {
|
||||
@@ -508,18 +508,46 @@ describe('Buffer', () => {
|
||||
// the dangling last cell is wrongly added in the string
|
||||
// --> fixable after resolving #1685
|
||||
terminal.writeSync(input);
|
||||
// TODO: reenable after fix
|
||||
// const s = terminal.buffer.contents(true).toArray()[0];
|
||||
// assert.equal(input, s);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
for (let i = 10; i < input.length; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i + 1); // TODO: remove +1 after fix
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
const j = (i - 0) << 1;
|
||||
assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex);
|
||||
}
|
||||
});
|
||||
|
||||
it('test fully wrapped buffer up to last char', () => {
|
||||
const input = Array(6).join('1234567890');
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]);
|
||||
}
|
||||
});
|
||||
|
||||
it('test fully wrapped buffer up to last char with full width odd', () => {
|
||||
const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'
|
||||
+ 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
assert.equal(
|
||||
(!(i % 3))
|
||||
? input[i]
|
||||
: (i % 3 === 1)
|
||||
? input.substr(i, 2)
|
||||
: input.substr(i - 1, 2),
|
||||
terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('BufferStringIterator', function(): void {
|
||||
it('iterator does not ovrflow buffer limits', function(): void {
|
||||
it('iterator does not overflow buffer limits', function(): void {
|
||||
const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
|
||||
const data = [
|
||||
'aaaaaaaaaa',
|
||||
|
||||
+10
-60
@@ -17,9 +17,13 @@ export const CHAR_DATA_WIDTH_INDEX = 2;
|
||||
export const CHAR_DATA_CODE_INDEX = 3;
|
||||
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
|
||||
|
||||
export const NULL_CELL_CHAR = ' ';
|
||||
export const NULL_CELL_CHAR = '';
|
||||
export const NULL_CELL_WIDTH = 1;
|
||||
export const NULL_CELL_CODE = 32;
|
||||
export const NULL_CELL_CODE = 0;
|
||||
|
||||
export const WHITESPACE_CELL_CHAR = ' ';
|
||||
export const WHITESPACE_CELL_WIDTH = 1;
|
||||
export const WHITESPACE_CELL_CODE = 32;
|
||||
|
||||
/**
|
||||
* This class represents a terminal buffer (an internal state of the terminal), where the
|
||||
@@ -248,7 +252,7 @@ export class Buffer implements IBuffer {
|
||||
while (stringIndex) {
|
||||
const line = this.lines.get(lineIndex);
|
||||
if (!line) {
|
||||
[-1, -1];
|
||||
return [-1, -1];
|
||||
}
|
||||
for (let i = 0; i < line.length; ++i) {
|
||||
stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length;
|
||||
@@ -271,65 +275,12 @@ export class Buffer implements IBuffer {
|
||||
* @param startCol The column to start at.
|
||||
* @param endCol The column to end at.
|
||||
*/
|
||||
public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol: number = null): string {
|
||||
// Get full line
|
||||
let lineString = '';
|
||||
public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol?: number): string {
|
||||
const line = this.lines.get(lineIndex);
|
||||
if (!line) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Initialize column and index values. Column values represent the actual
|
||||
// cell column, indexes represent the index in the string. Indexes are
|
||||
// needed here because some chars are 0 characters long (eg. after wide
|
||||
// chars) and some chars are longer than 1 characters long (eg. emojis).
|
||||
let startIndex = startCol;
|
||||
// Only set endCol to the line length when it is null. 0 is a valid column.
|
||||
if (endCol === null) {
|
||||
endCol = line.length;
|
||||
}
|
||||
let endIndex = endCol;
|
||||
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line.get(i);
|
||||
lineString += char[CHAR_DATA_CHAR_INDEX];
|
||||
// Adjust start and end cols for wide characters if they affect their
|
||||
// column indexes
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
if (startCol >= i) {
|
||||
startIndex--;
|
||||
}
|
||||
if (endCol > i) {
|
||||
endIndex--;
|
||||
}
|
||||
} else {
|
||||
// Adjust the columns to take glyphs that are represented by multiple
|
||||
// code points into account.
|
||||
if (char[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
if (startCol > i) {
|
||||
startIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
}
|
||||
if (endCol > i) {
|
||||
endIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the final end col by trimming whitespace on the right of the
|
||||
// line if needed.
|
||||
if (trimRight) {
|
||||
const rightWhitespaceIndex = lineString.search(/\s+$/);
|
||||
if (rightWhitespaceIndex !== -1) {
|
||||
endIndex = Math.min(endIndex, rightWhitespaceIndex);
|
||||
}
|
||||
// Return the empty string if only trimmed whitespace is selected
|
||||
if (endIndex <= startIndex) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return lineString.substring(startIndex, endIndex);
|
||||
return line.translateToString(trimRight, startCol, endCol);
|
||||
}
|
||||
|
||||
public getWrappedRangeForLine(y: number): { first: number, last: number } {
|
||||
@@ -488,8 +439,7 @@ export class BufferStringIterator implements IBufferStringIterator {
|
||||
range.last = Math.min(range.last, this._buffer.lines.length);
|
||||
let result = '';
|
||||
for (let i = range.first; i <= range.last; ++i) {
|
||||
// TODO: always apply trimRight after fixing #1685
|
||||
result += this._buffer.translateBufferLineToString(i, (this._trimRight) ? i === range.last : false);
|
||||
result += this._buffer.translateBufferLineToString(i, this._trimRight);
|
||||
}
|
||||
this._current = range.last + 1;
|
||||
return {range: range, content: result};
|
||||
|
||||
+132
-1
@@ -5,7 +5,7 @@
|
||||
import * as chai from 'chai';
|
||||
import { BufferLine } from './BufferLine';
|
||||
import { CharData, IBufferLine } from './Types';
|
||||
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
|
||||
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './Buffer';
|
||||
|
||||
|
||||
class TestBufferLine extends BufferLine {
|
||||
@@ -200,4 +200,135 @@ describe('BufferLine', function(): void {
|
||||
chai.expect(line.toArray()).eql(Array(7).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
});
|
||||
describe('getTrimLength', function(): void {
|
||||
it('empty line', function(): void {
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
chai.expect(line.getTrimmedLength()).equal(0);
|
||||
});
|
||||
it('ASCII', function(): void {
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.getTrimmedLength()).equal(3);
|
||||
});
|
||||
it('surrogate', function(): void {
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]);
|
||||
chai.expect(line.getTrimmedLength()).equal(3);
|
||||
});
|
||||
it('combining', function(): void {
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
|
||||
chai.expect(line.getTrimmedLength()).equal(3);
|
||||
});
|
||||
it('fullwidth', function(): void {
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, '1', 2, '1'.charCodeAt(0)]);
|
||||
line.set(3, [0, '', 0, undefined]);
|
||||
chai.expect(line.getTrimmedLength()).equal(4); // also counts null cell after fullwidth
|
||||
});
|
||||
});
|
||||
describe('translateToString with and w\'o trimming', function(): void {
|
||||
it('empty line', function(): void {
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
chai.expect(line.translateToString(false)).equal(' ');
|
||||
chai.expect(line.translateToString(true)).equal('');
|
||||
});
|
||||
it('ASCII', function(): void {
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(4, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(5, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.translateToString(false)).equal('a a aa ');
|
||||
chai.expect(line.translateToString(true)).equal('a a aa');
|
||||
chai.expect(line.translateToString(false, 0, 5)).equal('a a a');
|
||||
chai.expect(line.translateToString(false, 0, 4)).equal('a a ');
|
||||
chai.expect(line.translateToString(false, 0, 3)).equal('a a');
|
||||
chai.expect(line.translateToString(true, 0, 5)).equal('a a a');
|
||||
chai.expect(line.translateToString(true, 0, 4)).equal('a a ');
|
||||
chai.expect(line.translateToString(true, 0, 3)).equal('a a');
|
||||
|
||||
});
|
||||
it('surrogate', function(): void {
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]);
|
||||
line.set(4, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]);
|
||||
line.set(5, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]);
|
||||
chai.expect(line.translateToString(false)).equal('a 𝄞 𝄞𝄞 ');
|
||||
chai.expect(line.translateToString(true)).equal('a 𝄞 𝄞𝄞');
|
||||
chai.expect(line.translateToString(false, 0, 5)).equal('a 𝄞 𝄞');
|
||||
chai.expect(line.translateToString(false, 0, 4)).equal('a 𝄞 ');
|
||||
chai.expect(line.translateToString(false, 0, 3)).equal('a 𝄞');
|
||||
chai.expect(line.translateToString(true, 0, 5)).equal('a 𝄞 𝄞');
|
||||
chai.expect(line.translateToString(true, 0, 4)).equal('a 𝄞 ');
|
||||
chai.expect(line.translateToString(true, 0, 3)).equal('a 𝄞');
|
||||
});
|
||||
it('combining', function(): void {
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
|
||||
line.set(4, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
|
||||
line.set(5, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
|
||||
chai.expect(line.translateToString(false)).equal('a e\u0301 e\u0301e\u0301 ');
|
||||
chai.expect(line.translateToString(true)).equal('a e\u0301 e\u0301e\u0301');
|
||||
chai.expect(line.translateToString(false, 0, 5)).equal('a e\u0301 e\u0301');
|
||||
chai.expect(line.translateToString(false, 0, 4)).equal('a e\u0301 ');
|
||||
chai.expect(line.translateToString(false, 0, 3)).equal('a e\u0301');
|
||||
chai.expect(line.translateToString(true, 0, 5)).equal('a e\u0301 e\u0301');
|
||||
chai.expect(line.translateToString(true, 0, 4)).equal('a e\u0301 ');
|
||||
chai.expect(line.translateToString(true, 0, 3)).equal('a e\u0301');
|
||||
});
|
||||
it('fullwidth', function(): void {
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, '1', 2, '1'.charCodeAt(0)]);
|
||||
line.set(3, [0, '', 0, undefined]);
|
||||
line.set(5, [1, '1', 2, '1'.charCodeAt(0)]);
|
||||
line.set(6, [0, '', 0, undefined]);
|
||||
line.set(7, [1, '1', 2, '1'.charCodeAt(0)]);
|
||||
line.set(8, [0, '', 0, undefined]);
|
||||
chai.expect(line.translateToString(false)).equal('a 1 11 ');
|
||||
chai.expect(line.translateToString(true)).equal('a 1 11');
|
||||
chai.expect(line.translateToString(false, 0, 7)).equal('a 1 1');
|
||||
chai.expect(line.translateToString(false, 0, 6)).equal('a 1 1');
|
||||
chai.expect(line.translateToString(false, 0, 5)).equal('a 1 ');
|
||||
chai.expect(line.translateToString(false, 0, 4)).equal('a 1');
|
||||
chai.expect(line.translateToString(false, 0, 3)).equal('a 1');
|
||||
chai.expect(line.translateToString(false, 0, 2)).equal('a ');
|
||||
chai.expect(line.translateToString(true, 0, 7)).equal('a 1 1');
|
||||
chai.expect(line.translateToString(true, 0, 6)).equal('a 1 1');
|
||||
chai.expect(line.translateToString(true, 0, 5)).equal('a 1 ');
|
||||
chai.expect(line.translateToString(true, 0, 4)).equal('a 1');
|
||||
chai.expect(line.translateToString(true, 0, 3)).equal('a 1');
|
||||
chai.expect(line.translateToString(true, 0, 2)).equal('a ');
|
||||
});
|
||||
it('space at end', function(): void {
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
|
||||
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(4, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(5, [1, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(6, [1, ' ', 1, ' '.charCodeAt(0)]);
|
||||
chai.expect(line.translateToString(false)).equal('a a aa ');
|
||||
chai.expect(line.translateToString(true)).equal('a a aa ');
|
||||
});
|
||||
it('should always return some sane value', function(): void {
|
||||
// sanity check - broken line with invalid out of bound null width cells
|
||||
// this can atm happen with deleting/inserting chars in inputhandler by "breaking"
|
||||
// fullwidth pairs --> needs to be fixed after settling BufferLine impl
|
||||
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE], false);
|
||||
chai.expect(line.translateToString(false)).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('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+51
-4
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
import { CharData, IBufferLine } from './Types';
|
||||
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer';
|
||||
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, WHITESPACE_CELL_CHAR } from './Buffer';
|
||||
|
||||
/**
|
||||
* Class representing a terminal line.
|
||||
@@ -107,6 +107,28 @@ export class BufferLineJSArray implements IBufferLine {
|
||||
newLine.copyFrom(this);
|
||||
return newLine;
|
||||
}
|
||||
|
||||
public getTrimmedLength(): number {
|
||||
for (let i = this.length - 1; i >= 0; --i) {
|
||||
const ch = this.get(i);
|
||||
if (ch[CHAR_DATA_CHAR_INDEX] !== '') {
|
||||
return i + ch[CHAR_DATA_WIDTH_INDEX];
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string {
|
||||
if (trimRight) {
|
||||
endCol = Math.min(endCol, this.getTrimmedLength());
|
||||
}
|
||||
let result = '';
|
||||
while (startCol < endCol) {
|
||||
result += this.get(startCol)[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR;
|
||||
startCol += this.get(startCol)[CHAR_DATA_WIDTH_INDEX] || 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/** typed array slots taken by one cell */
|
||||
@@ -119,6 +141,9 @@ const enum Cell {
|
||||
WIDTH = 2
|
||||
}
|
||||
|
||||
/** single vs. combined char distinction */
|
||||
const IS_COMBINED_BIT_MASK = 0x80000000;
|
||||
|
||||
/**
|
||||
* Typed array based bufferline implementation.
|
||||
*/
|
||||
@@ -144,11 +169,11 @@ export class BufferLine implements IBufferLine {
|
||||
const stringData = this._data[index * CELL_SIZE + Cell.STRING];
|
||||
return [
|
||||
this._data[index * CELL_SIZE + Cell.FLAGS],
|
||||
(stringData & 0x80000000)
|
||||
(stringData & IS_COMBINED_BIT_MASK)
|
||||
? this._combined[index]
|
||||
: (stringData) ? String.fromCharCode(stringData) : '',
|
||||
this._data[index * CELL_SIZE + Cell.WIDTH],
|
||||
(stringData & 0x80000000)
|
||||
(stringData & IS_COMBINED_BIT_MASK)
|
||||
? this._combined[index].charCodeAt(this._combined[index].length - 1)
|
||||
: stringData
|
||||
];
|
||||
@@ -158,7 +183,7 @@ export class BufferLine implements IBufferLine {
|
||||
this._data[index * CELL_SIZE + Cell.FLAGS] = value[0];
|
||||
if (value[1].length > 1) {
|
||||
this._combined[index] = value[1];
|
||||
this._data[index * CELL_SIZE + Cell.STRING] = index | 0x80000000;
|
||||
this._data[index * CELL_SIZE + Cell.STRING] = index | IS_COMBINED_BIT_MASK;
|
||||
} else {
|
||||
this._data[index * CELL_SIZE + Cell.STRING] = value[1].charCodeAt(0);
|
||||
}
|
||||
@@ -269,4 +294,26 @@ export class BufferLine implements IBufferLine {
|
||||
newLine.isWrapped = this.isWrapped;
|
||||
return newLine;
|
||||
}
|
||||
|
||||
public getTrimmedLength(): number {
|
||||
for (let i = this.length - 1; i >= 0; --i) {
|
||||
if (this._data[i * CELL_SIZE + Cell.STRING] !== 0) { // 0 ==> ''.charCodeAt(0) ==> NaN ==> 0
|
||||
return i + this._data[i * CELL_SIZE + Cell.WIDTH];
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string {
|
||||
if (trimRight) {
|
||||
endCol = Math.min(endCol, this.getTrimmedLength());
|
||||
}
|
||||
let result = '';
|
||||
while (startCol < endCol) {
|
||||
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;
|
||||
startCol += this._data[startCol * CELL_SIZE + Cell.WIDTH] || 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
+137
-309
File diff suppressed because it is too large
Load Diff
@@ -219,9 +219,17 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
// also correct regex and string search offsets for the next loop run
|
||||
stringIndex = text.indexOf(uri, stringIndex + 1);
|
||||
rex.lastIndex = stringIndex + uri.length;
|
||||
if (stringIndex < 0) {
|
||||
// invalid stringIndex (should not have happened)
|
||||
break;
|
||||
}
|
||||
|
||||
// get the buffer index as [absolute row, col] for the match
|
||||
const bufferIndex = this._terminal.buffer.stringIndexToBufferIndex(rowIndex, stringIndex);
|
||||
if (bufferIndex[0] < 0) {
|
||||
// invalid bufferIndex (should not have happened)
|
||||
break;
|
||||
}
|
||||
|
||||
const line = this._terminal.buffer.lines.get(bufferIndex[0]);
|
||||
const char = line.get(bufferIndex[1]);
|
||||
|
||||
@@ -198,7 +198,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
}
|
||||
} else {
|
||||
// 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));
|
||||
|
||||
// Get middle rows
|
||||
|
||||
@@ -13,7 +13,7 @@ import * as path from 'path';
|
||||
import * as pty from 'node-pty';
|
||||
import { assert } from 'chai';
|
||||
import { Terminal } from './Terminal';
|
||||
import { CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||
import { CHAR_DATA_CHAR_INDEX, WHITESPACE_CELL_CHAR } from './Buffer';
|
||||
import { IViewport } from './Types';
|
||||
|
||||
class TestTerminal extends Terminal {
|
||||
@@ -67,7 +67,7 @@ function terminalToString(term: Terminal): string {
|
||||
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
|
||||
lineText = '';
|
||||
for (let cell = 0; cell < term.cols; ++cell) {
|
||||
lineText += term.buffer.lines.get(line).get(cell)[CHAR_DATA_CHAR_INDEX];
|
||||
lineText += term.buffer.lines.get(line).get(cell)[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR;
|
||||
}
|
||||
// rtrim empty cells as xterm does
|
||||
lineText = lineText.replace(/\s+$/, '');
|
||||
|
||||
+21
-21
@@ -462,7 +462,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS).get(0)[CHAR_DATA_CHAR_INDEX], '');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
||||
@@ -491,7 +491,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(5).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
|
||||
@@ -509,7 +509,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
});
|
||||
@@ -530,9 +530,9 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
// 'a' gets pushed out of buffer
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], '');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 2).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], '');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
||||
@@ -560,7 +560,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
|
||||
@@ -578,7 +578,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
});
|
||||
@@ -776,7 +776,7 @@ describe('term.js addons', () => {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
@@ -787,7 +787,7 @@ describe('term.js addons', () => {
|
||||
term.write(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
@@ -800,7 +800,7 @@ describe('term.js addons', () => {
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
@@ -813,7 +813,7 @@ describe('term.js addons', () => {
|
||||
// auto wraparound mode should cut off the rest of the line
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
@@ -826,7 +826,7 @@ describe('term.js addons', () => {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
@@ -845,8 +845,8 @@ describe('term.js addons', () => {
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
});
|
||||
it('multiple combined é', () => {
|
||||
@@ -928,8 +928,8 @@ describe('term.js addons', () => {
|
||||
}
|
||||
}
|
||||
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
@@ -953,8 +953,8 @@ describe('term.js addons', () => {
|
||||
}
|
||||
}
|
||||
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
||||
@@ -998,8 +998,8 @@ describe('term.js addons', () => {
|
||||
}
|
||||
}
|
||||
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
||||
@@ -1063,7 +1063,7 @@ describe('term.js addons', () => {
|
||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(' '); // fullwidth char got replaced
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(''); // fullwidth char got replaced
|
||||
term.write('b');
|
||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('b');
|
||||
|
||||
@@ -524,6 +524,8 @@ export interface IBufferLine {
|
||||
fill(fillCharData: CharData): void;
|
||||
copyFrom(line: IBufferLine): void;
|
||||
clone(): IBufferLine;
|
||||
getTrimmedLength(): number;
|
||||
translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string;
|
||||
}
|
||||
|
||||
export interface IBufferLineConstructor {
|
||||
|
||||
@@ -22,6 +22,7 @@ export function toggleFullScreen(term: Terminal, fullscreen: boolean): void {
|
||||
fn = term.element.classList.add;
|
||||
}
|
||||
|
||||
fn = fn.bind(term.element.classList);
|
||||
fn('fullscreen');
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,11 @@ export interface ISearchOptions {
|
||||
regex?: boolean;
|
||||
wholeWord?: boolean;
|
||||
caseSensitive?: boolean;
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
export interface ISearchResult {
|
||||
|
||||
@@ -4,16 +4,24 @@
|
||||
*/
|
||||
|
||||
import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces';
|
||||
const nonWordCharacters = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
|
||||
|
||||
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
|
||||
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
|
||||
|
||||
/**
|
||||
* A class that knows how to search the terminal and how to display the results.
|
||||
*/
|
||||
export class SearchHelper implements ISearchHelper {
|
||||
/**
|
||||
* translateBufferLineToStringWithWrap is a fairly expensive call.
|
||||
* We memoize the calls into an array that has a time based ttl.
|
||||
* _linesCache is also invalidated when the terminal cursor moves.
|
||||
*/
|
||||
private _linesCache: string[] = null;
|
||||
private _linesCacheTimeoutId = 0;
|
||||
|
||||
constructor(private _terminal: ISearchAddonTerminal) {
|
||||
// TODO: Search for multiple instances on 1 line
|
||||
// TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted
|
||||
// TODO: Highlight other instances in the viewport
|
||||
this._destroyLinesCache = this._destroyLinesCache.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,32 +32,47 @@ export class SearchHelper implements ISearchHelper {
|
||||
* @return Whether a result was found.
|
||||
*/
|
||||
public findNext(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
const selectionManager = this._terminal._core.selectionManager;
|
||||
const {incremental} = searchOptions;
|
||||
let result: ISearchResult;
|
||||
|
||||
if (!term || term.length === 0) {
|
||||
selectionManager.clearSelection();
|
||||
return false;
|
||||
}
|
||||
|
||||
let result: ISearchResult;
|
||||
|
||||
let startCol: number = 0;
|
||||
let startRow = this._terminal._core.buffer.ydisp;
|
||||
if (this._terminal._core.selectionManager.selectionEnd) {
|
||||
|
||||
if (selectionManager.selectionEnd) {
|
||||
// Start from the selection end if there is a selection
|
||||
// For incremental search, use existing row
|
||||
if (this._terminal.getSelection().length !== 0) {
|
||||
startRow = this._terminal._core.selectionManager.selectionEnd[1];
|
||||
startRow = incremental ? selectionManager.selectionStart[1] : selectionManager.selectionEnd[1];
|
||||
startCol = incremental ? selectionManager.selectionStart[0] : selectionManager.selectionEnd[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Search from ydisp + 1 to end
|
||||
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
|
||||
result = this._findInLine(term, y, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._initLinesCache();
|
||||
|
||||
// Search from the top to the current ydisp
|
||||
// Search startRow
|
||||
result = this._findInLine(term, startRow, startCol, searchOptions);
|
||||
|
||||
// Search from startRow + 1 to end
|
||||
if (!result) {
|
||||
for (let y = 0; y < startRow; y++) {
|
||||
result = this._findInLine(term, y, searchOptions);
|
||||
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 the whole startRow again in
|
||||
// case startCol > 0)
|
||||
if (!result) {
|
||||
for (let y = 0; y <= startRow; y++) {
|
||||
result = this._findInLine(term, y, 0, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
@@ -68,32 +91,47 @@ export class SearchHelper implements ISearchHelper {
|
||||
* @return Whether a result was found.
|
||||
*/
|
||||
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
const selectionManager = this._terminal._core.selectionManager;
|
||||
let result: ISearchResult;
|
||||
|
||||
if (!term || term.length === 0) {
|
||||
selectionManager.clearSelection();
|
||||
return false;
|
||||
}
|
||||
|
||||
let result: ISearchResult;
|
||||
|
||||
const isReverseSearch = true;
|
||||
let startRow = this._terminal._core.buffer.ydisp;
|
||||
if (this._terminal._core.selectionManager.selectionStart) {
|
||||
// Start from the selection end if there is a selection
|
||||
let startCol: number = this._terminal._core.buffer.lines.get(startRow).length;
|
||||
|
||||
if (selectionManager.selectionStart) {
|
||||
// Start from the selection start if there is a selection
|
||||
if (this._terminal.getSelection().length !== 0) {
|
||||
startRow = this._terminal._core.selectionManager.selectionStart[1];
|
||||
startRow = selectionManager.selectionStart[1];
|
||||
startCol = selectionManager.selectionStart[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Search from ydisp + 1 to end
|
||||
for (let y = startRow - 1; y >= 0; y--) {
|
||||
result = this._findInLine(term, y, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._initLinesCache();
|
||||
|
||||
// Search from the top to the current ydisp
|
||||
// Search startRow
|
||||
result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch);
|
||||
|
||||
// Search from startRow - 1 to top
|
||||
if (!result) {
|
||||
for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
|
||||
result = this._findInLine(term, y, searchOptions);
|
||||
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 the whole startRow again in
|
||||
// case startCol > 0)
|
||||
if (!result) {
|
||||
const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1;
|
||||
for (let y = searchFrom; y >= startRow; y--) {
|
||||
result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
@@ -104,6 +142,28 @@ export class SearchHelper implements ISearchHelper {
|
||||
return this._selectResult(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a line cache with a ttl
|
||||
*/
|
||||
private _initLinesCache(): void {
|
||||
if (!this._linesCache) {
|
||||
this._linesCache = new Array(this._terminal._core.buffer.length);
|
||||
this._terminal.on('cursormove', this._destroyLinesCache);
|
||||
}
|
||||
|
||||
window.clearTimeout(this._linesCacheTimeoutId);
|
||||
this._linesCacheTimeoutId = window.setTimeout(() => this._destroyLinesCache(), LINES_CACHE_TIME_TO_LIVE);
|
||||
}
|
||||
|
||||
private _destroyLinesCache(): void {
|
||||
this._linesCache = null;
|
||||
this._terminal.off('cursormove', this._destroyLinesCache);
|
||||
if (this._linesCacheTimeoutId) {
|
||||
window.clearTimeout(this._linesCacheTimeoutId);
|
||||
this._linesCacheTimeoutId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A found substring is a whole word if it doesn't have an alphanumeric character directly adjacent to it.
|
||||
* @param searchIndex starting indext of the potential whole word substring
|
||||
@@ -111,8 +171,8 @@ export class SearchHelper implements ISearchHelper {
|
||||
* @param term the substring that starts at searchIndex
|
||||
*/
|
||||
private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
|
||||
return (((searchIndex === 0) || (nonWordCharacters.indexOf(line[searchIndex - 1]) !== -1)) &&
|
||||
(((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1)));
|
||||
return (((searchIndex === 0) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex - 1]) !== -1)) &&
|
||||
(((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex + term.length]) !== -1)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,65 +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
|
||||
* text starts on is searched.
|
||||
* @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.
|
||||
* @return The search result if it was found.
|
||||
*/
|
||||
protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult {
|
||||
if (this._terminal._core.buffer.lines.get(y).isWrapped) {
|
||||
protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult {
|
||||
if (this._terminal._core.buffer.lines.get(row).isWrapped) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stringLine = this.translateBufferLineToStringWithWrap(y, true);
|
||||
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
|
||||
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
|
||||
let searchIndex = -1;
|
||||
|
||||
if (searchOptions.regex) {
|
||||
const searchRegex = RegExp(searchTerm, 'g');
|
||||
const foundTerm = searchRegex.exec(searchStringLine);
|
||||
if (foundTerm && foundTerm[0].length > 0) {
|
||||
searchIndex = searchRegex.lastIndex - foundTerm[0].length;
|
||||
term = foundTerm[0];
|
||||
let stringLine = this._linesCache ? this._linesCache[row] : void 0;
|
||||
if (stringLine === void 0) {
|
||||
stringLine = this.translateBufferLineToStringWithWrap(row, true);
|
||||
if (this._linesCache) {
|
||||
this._linesCache[row] = stringLine;
|
||||
}
|
||||
} else {
|
||||
searchIndex = searchStringLine.indexOf(searchTerm);
|
||||
}
|
||||
|
||||
if (searchIndex >= 0) {
|
||||
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
|
||||
if (searchIndex >= this._terminal.cols) {
|
||||
y += Math.floor(searchIndex / this._terminal.cols);
|
||||
searchIndex = searchIndex % this._terminal.cols;
|
||||
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
|
||||
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
|
||||
|
||||
let resultIndex = -1;
|
||||
if (searchOptions.regex) {
|
||||
const searchRegex = RegExp(searchTerm, 'g');
|
||||
let foundTerm: RegExpExecArray;
|
||||
if (isReverseSearch) {
|
||||
// This loop will get the resultIndex of the _last_ regex match in the range 0..col
|
||||
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];
|
||||
}
|
||||
}
|
||||
if (searchOptions.wholeWord && !this._isWholeWord(searchIndex, searchStringLine, term)) {
|
||||
} else {
|
||||
if (isReverseSearch) {
|
||||
resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length);
|
||||
} else {
|
||||
resultIndex = searchStringLine.indexOf(searchTerm, col);
|
||||
}
|
||||
}
|
||||
|
||||
if (resultIndex >= 0) {
|
||||
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
|
||||
if (resultIndex >= this._terminal.cols) {
|
||||
row += Math.floor(resultIndex / this._terminal.cols);
|
||||
resultIndex = resultIndex % this._terminal.cols;
|
||||
}
|
||||
if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
|
||||
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);
|
||||
// Adjust the searchIndex to normalize emoji into single chars
|
||||
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
|
||||
if (char.length > 1) {
|
||||
searchIndex -= char.length - 1;
|
||||
resultIndex -= char.length - 1;
|
||||
}
|
||||
// Adjust the searchIndex for empty characters following wide unicode
|
||||
// chars (eg. CJK)
|
||||
const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/];
|
||||
if (charWidth === 0) {
|
||||
searchIndex++;
|
||||
resultIndex++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
term,
|
||||
col: searchIndex,
|
||||
row: y
|
||||
col: resultIndex,
|
||||
row
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -29,8 +29,11 @@ class MockTerminal {
|
||||
}
|
||||
|
||||
class TestSearchHelper extends SearchHelper {
|
||||
public findInLine(term: string, y: number, searchOptions?: ISearchOptions): ISearchResult {
|
||||
return this._findInLine(term, y, searchOptions);
|
||||
public findInLine(term: string, rowNumber: number, searchOptions?: ISearchOptions): ISearchResult {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,8 +121,6 @@ describe('search addon', () => {
|
||||
it('should not select empty lines', () => {
|
||||
search.apply(<any>MockTerminal);
|
||||
const term = new MockTerminal({cols: 20, rows: 3});
|
||||
term.core.write(' ');
|
||||
term.pushWriteData();
|
||||
const line = term.searchHelper.findInLine('^.*$', 0, { regex: true });
|
||||
expect(line).eql(undefined);
|
||||
});
|
||||
@@ -247,5 +248,66 @@ describe('search addon', () => {
|
||||
expect(hello4).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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es5"
|
||||
],
|
||||
"rootDir": ".",
|
||||
|
||||
@@ -63,4 +63,28 @@ describe('webLinks addon', () => {
|
||||
|
||||
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/');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
|
||||
const localHostClause = '(localhost)';
|
||||
const portClause = '(:\\d{1,5})';
|
||||
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
|
||||
const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:\\s])';
|
||||
const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:"\'\\s])';
|
||||
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
|
||||
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
|
||||
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user