Skip to content

Commit 192f751

Browse files
authored
Merge branch 'master' into 1674_watch_addons
2 parents f7dbadc + de143b5 commit 192f751

File tree

13 files changed

+655
-120
lines changed

13 files changed

+655
-120
lines changed

β€Ždemo/client.tsβ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ function createTerminal(): void {
102102
e.preventDefault();
103103
const searchOptions = {
104104
regex: (document.getElementById('regex') as HTMLInputElement).checked,
105-
wholeWord: false,
105+
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
106106
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
107107
};
108108
term.findNext(actionElements.findNext.value, searchOptions);
@@ -113,7 +113,7 @@ function createTerminal(): void {
113113
e.preventDefault();
114114
const searchOptions = {
115115
regex: (document.getElementById('regex') as HTMLInputElement).checked,
116-
wholeWord: false,
116+
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
117117
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
118118
};
119119
term.findPrevious(actionElements.findPrevious.value, searchOptions);

β€Ždemo/index.htmlβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ <h3>Actions</h3>
1818
<label>Find previous <input id="find-previous"/></label>
1919
<label>Use regex<input type="checkbox" id="regex"/></label>
2020
<label>Case sensitive<input type="checkbox" id="case-sensitive"/></label>
21+
<label>Whole word<input type="checkbox" id="whole-word"/></label>
2122
</p>
2223
</div>
2324
<div>

β€Žsrc/Buffer.test.tsβ€Ž

Lines changed: 169 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55

66
import { assert } from 'chai';
77
import { ITerminal } from './Types';
8-
import { Buffer, DEFAULT_ATTR } from './Buffer';
8+
import { Buffer, DEFAULT_ATTR, CHAR_DATA_CHAR_INDEX } from './Buffer';
99
import { CircularList } from './common/CircularList';
10-
import { MockTerminal } from './utils/TestUtils.test';
10+
import { MockTerminal, TestTerminal } from './utils/TestUtils.test';
1111
import { BufferLine } from './BufferLine';
1212

1313
const INIT_COLS = 80;
@@ -347,4 +347,171 @@ describe('Buffer', () => {
347347
assert.equal(str3, '😁a');
348348
});
349349
});
350+
describe('stringIndexToBufferIndex', () => {
351+
let terminal: TestTerminal;
352+
353+
beforeEach(() => {
354+
terminal = new TestTerminal({rows: 5, cols: 10});
355+
});
356+
357+
it('multiline ascii', () => {
358+
const input = 'This is ASCII text spanning multiple lines.';
359+
terminal.writeSync(input);
360+
const s = terminal.buffer.iterator(true).next().content;
361+
assert.equal(input, s);
362+
for (let i = 0; i < input.length; ++i) {
363+
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
364+
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
365+
}
366+
});
367+
368+
it('combining e\u0301 in a sentence', () => {
369+
const input = 'Sitting in the cafe\u0301 drinking coffee.';
370+
terminal.writeSync(input);
371+
const s = terminal.buffer.iterator(true).next().content;
372+
assert.equal(input, s);
373+
for (let i = 0; i < 19; ++i) {
374+
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
375+
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
376+
}
377+
// string index 18 & 19 point to combining char e\u0301 ---> same buffer Index
378+
assert.deepEqual(
379+
terminal.buffer.stringIndexToBufferIndex(0, 18),
380+
terminal.buffer.stringIndexToBufferIndex(0, 19));
381+
// after the combining char every string index has an offset of -1
382+
for (let i = 19; i < input.length; ++i) {
383+
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
384+
assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
385+
}
386+
});
387+
388+
it('multiline combining e\u0301', () => {
389+
const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301';
390+
terminal.writeSync(input);
391+
const s = terminal.buffer.iterator(true).next().content;
392+
assert.equal(input, s);
393+
// every buffer cell index contains 2 string indices
394+
for (let i = 0; i < input.length; ++i) {
395+
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
396+
assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
397+
}
398+
});
399+
400+
it('surrogate char in a sentence', () => {
401+
const input = 'The π„ž is a clef widely used in modern notation.';
402+
terminal.writeSync(input);
403+
const s = terminal.buffer.iterator(true).next().content;
404+
assert.equal(input, s);
405+
for (let i = 0; i < 5; ++i) {
406+
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
407+
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
408+
}
409+
// string index 4 & 5 point to surrogate char π„ž ---> same buffer Index
410+
assert.deepEqual(
411+
terminal.buffer.stringIndexToBufferIndex(0, 4),
412+
terminal.buffer.stringIndexToBufferIndex(0, 5));
413+
// after the combining char every string index has an offset of -1
414+
for (let i = 5; i < input.length; ++i) {
415+
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
416+
assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
417+
}
418+
});
419+
420+
it('multiline surrogate char', () => {
421+
const input = 'π„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„ž';
422+
terminal.writeSync(input);
423+
const s = terminal.buffer.iterator(true).next().content;
424+
assert.equal(input, s);
425+
// every buffer cell index contains 2 string indices
426+
for (let i = 0; i < input.length; ++i) {
427+
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
428+
assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
429+
}
430+
});
431+
432+
it('surrogate char with combining', () => {
433+
// eye of Ra with acute accent - string length of 3
434+
const input = 'π“‚€\u0301 - the eye hiroglyph with an acute accent.';
435+
terminal.writeSync(input);
436+
const s = terminal.buffer.iterator(true).next().content;
437+
assert.equal(input, s);
438+
// index 0..2 should map to 0
439+
assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1));
440+
assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2));
441+
for (let i = 2; i < input.length; ++i) {
442+
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
443+
assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex);
444+
}
445+
});
446+
447+
it('multiline surrogate with combining', () => {
448+
const input = 'π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301';
449+
terminal.writeSync(input);
450+
const s = terminal.buffer.iterator(true).next().content;
451+
assert.equal(input, s);
452+
// every buffer cell index contains 3 string indices
453+
for (let i = 0; i < input.length; ++i) {
454+
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
455+
assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex);
456+
}
457+
});
458+
459+
it('fullwidth chars', () => {
460+
const input = 'These οΌ‘οΌ’οΌ“ are some fat numbers.';
461+
terminal.writeSync(input);
462+
const s = terminal.buffer.iterator(true).next().content;
463+
assert.equal(input, s);
464+
for (let i = 0; i < 6; ++i) {
465+
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
466+
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
467+
}
468+
// string index 6, 7, 8 take 2 cells
469+
assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7));
470+
assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8));
471+
// rest of the string has offset of +3
472+
for (let i = 9; i < input.length; ++i) {
473+
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
474+
assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex);
475+
}
476+
});
477+
478+
it('multiline fullwidth chars', () => {
479+
const input = 'οΌ‘οΌ’οΌ“οΌ”οΌ•οΌ–οΌ—οΌ˜οΌ™οΌοΌ‘οΌ’οΌ“οΌ”οΌ•οΌ–οΌ—οΌ˜οΌ™οΌ';
480+
terminal.writeSync(input);
481+
const s = terminal.buffer.iterator(true).next().content;
482+
assert.equal(input, s);
483+
for (let i = 9; i < input.length; ++i) {
484+
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
485+
assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex);
486+
}
487+
});
488+
489+
it('fullwidth combining with emoji - match emoji cell', () => {
490+
const input = 'Lots of οΏ₯\u0301 make me πŸ˜ƒ.';
491+
terminal.writeSync(input);
492+
const s = terminal.buffer.iterator(true).next().content;
493+
assert.equal(input, s);
494+
const stringIndex = s.match(/πŸ˜ƒ/).index;
495+
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex);
496+
assert(terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX], 'πŸ˜ƒ');
497+
});
498+
499+
it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => {
500+
const input = 'aοΌ‘οΌ’οΌ“οΌ”οΌ•οΌ–οΌ—οΌ˜οΌ™οΌοΌ‘οΌ’οΌ“οΌ”οΌ•οΌ–οΌ—οΌ˜οΌ™οΌ';
501+
// the 'a' at the beginning moves all fullwidth chars one to the right
502+
// now the end of the line contains a dangling empty cell since
503+
// the next fullwidth char has to wrap early
504+
// the dangling last cell is wrongly added in the string
505+
// --> fixable after resolving #1685
506+
terminal.writeSync(input);
507+
// TODO: reenable after fix
508+
// const s = terminal.buffer.contents(true).toArray()[0];
509+
// assert.equal(input, s);
510+
for (let i = 10; i < input.length; ++i) {
511+
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i + 1); // TODO: remove +1 after fix
512+
const j = (i - 0) << 1;
513+
assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex);
514+
}
515+
});
516+
});
350517
});

β€Žsrc/Buffer.tsβ€Ž

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { CircularList } from './common/CircularList';
7-
import { CharData, ITerminal, IBuffer, IBufferLine } from './Types';
7+
import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types';
88
import { EventEmitter } from './common/EventEmitter';
99
import { IMarker } from 'xterm';
1010
import { BufferLine } from './BufferLine';
@@ -194,6 +194,36 @@ export class Buffer implements IBuffer {
194194
this.scrollBottom = newRows - 1;
195195
}
196196

197+
/**
198+
* Translates a string index back to a BufferIndex.
199+
* To get the correct buffer position the string must start at `startCol` 0
200+
* (default in translateBufferLineToString).
201+
* The method also works on wrapped line strings given rows were not trimmed.
202+
* The method operates on the CharData string length, there are no
203+
* additional content or boundary checks. Therefore the string and the buffer
204+
* should not be altered in between.
205+
* TODO: respect trim flag after fixing #1685
206+
* @param lineIndex line index the string was retrieved from
207+
* @param stringIndex index within the string
208+
* @param startCol column offset the string was retrieved from
209+
*/
210+
public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): BufferIndex {
211+
while (stringIndex) {
212+
const line = this.lines.get(lineIndex);
213+
if (!line) {
214+
[-1, -1];
215+
}
216+
for (let i = 0; i < line.length; ++i) {
217+
stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length;
218+
if (stringIndex < 0) {
219+
return [lineIndex, i];
220+
}
221+
}
222+
lineIndex++;
223+
}
224+
return [lineIndex, 0];
225+
}
226+
197227
/**
198228
* Translates a buffer line to a string, with optional start and end columns.
199229
* Wide characters will count as two columns in the resulting string. This
@@ -340,6 +370,10 @@ export class Buffer implements IBuffer {
340370
// TODO: This could probably be optimized by relying on sort order and trimming the array using .length
341371
this.markers.splice(this.markers.indexOf(marker), 1);
342372
}
373+
374+
public iterator(trimRight: boolean, startIndex?: number, endIndex?: number): IBufferStringIterator {
375+
return new BufferStringIterator(this, trimRight, startIndex, endIndex);
376+
}
343377
}
344378

345379
export class Marker extends EventEmitter implements IMarker {
@@ -366,3 +400,31 @@ export class Marker extends EventEmitter implements IMarker {
366400
super.dispose();
367401
}
368402
}
403+
404+
export class BufferStringIterator implements IBufferStringIterator {
405+
private _current: number;
406+
407+
constructor (
408+
private _buffer: IBuffer,
409+
private _trimRight: boolean,
410+
private _startIndex: number = 0,
411+
private _endIndex: number = _buffer.lines.length
412+
) {
413+
this._current = this._startIndex;
414+
}
415+
416+
public hasNext(): boolean {
417+
return this._current < this._endIndex;
418+
}
419+
420+
public next(): IBufferStringIteratorResult {
421+
const range = this._buffer.getWrappedRangeForLine(this._current);
422+
let result = '';
423+
for (let i = range.first; i <= range.last; ++i) {
424+
// TODO: always apply trimRight after fixing #1685
425+
result += this._buffer.translateBufferLineToString(i, (this._trimRight) ? i === range.last : false);
426+
}
427+
this._current = range.last + 1;
428+
return {range: range, content: result};
429+
}
430+
}

β€Žsrc/CharWidth.test.tsβ€Ž

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
3+
* @license MIT
4+
*/
5+
6+
import { TestTerminal } from './utils/TestUtils.test';
7+
import { assert } from 'chai';
8+
import { getStringCellWidth } from './CharWidth';
9+
import { IBuffer } from './Types';
10+
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
11+
12+
13+
describe('getStringCellWidth', function(): void {
14+
let terminal: TestTerminal;
15+
16+
beforeEach(() => {
17+
terminal = new TestTerminal({rows: 5, cols: 30});
18+
});
19+
20+
function sumWidths(buffer: IBuffer, start: number, end: number, sentinel: string): number {
21+
let result = 0;
22+
for (let i = start; i < end; ++i) {
23+
const line = buffer.lines.get(i);
24+
for (let j = 0; j < line.length; ++j) { // TODO: change to trimBorder with multiline
25+
const ch = line.get(j);
26+
result += ch[CHAR_DATA_WIDTH_INDEX];
27+
// return on sentinel
28+
if (ch[CHAR_DATA_CHAR_INDEX] === sentinel) {
29+
return result;
30+
}
31+
}
32+
}
33+
return result;
34+
}
35+
36+
it('ASCII chars', function(): void {
37+
const input = 'This is just ASCII text.#';
38+
terminal.writeSync(input);
39+
const s = terminal.buffer.iterator(true).next().content;
40+
assert.equal(input, s);
41+
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
42+
});
43+
it('combining chars', function(): void {
44+
const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301#';
45+
terminal.writeSync(input);
46+
const s = terminal.buffer.iterator(true).next().content;
47+
assert.equal(input, s);
48+
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
49+
});
50+
it('surrogate chars', function(): void {
51+
const input = 'π„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„žπ„ž#';
52+
terminal.writeSync(input);
53+
const s = terminal.buffer.iterator(true).next().content;
54+
assert.equal(input, s);
55+
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
56+
});
57+
it('surrogate combining chars', function(): void {
58+
const input = 'π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301π“‚€\u0301#';
59+
terminal.writeSync(input);
60+
const s = terminal.buffer.iterator(true).next().content;
61+
assert.equal(input, s);
62+
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
63+
});
64+
it('fullwidth chars', function(): void {
65+
const input = 'οΌ‘οΌ’οΌ“οΌ”οΌ•οΌ–οΌ—οΌ˜οΌ™οΌ#';
66+
terminal.writeSync(input);
67+
const s = terminal.buffer.iterator(true).next().content;
68+
assert.equal(input, s);
69+
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
70+
});
71+
it('fullwidth chars offset 1', function(): void {
72+
const input = 'aοΌ‘οΌ’οΌ“οΌ”οΌ•οΌ–οΌ—οΌ˜οΌ™οΌ#';
73+
terminal.writeSync(input);
74+
const s = terminal.buffer.iterator(true).next().content;
75+
assert.equal(input, s);
76+
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
77+
});
78+
// TODO: multiline tests once #1685 is resolved
79+
});

0 commit comments

Comments
Β (0)