diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 6b6adc11da..b9d3472cb5 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -516,6 +516,26 @@ describe('Buffer', () => { assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); } }); + it('should point to correct trimmed .translateBufferLineToString', function(): void { + terminal.writeSync([ + 'abc', // #1 + '12345', // #2 + '¥cafe\u0301¥' // #3 + ].join('\r\n')); + let content = ''; + for (let i = 0; i < 3; ++i) { + const s = terminal.buffer.translateBufferLineToString(i, true); + content += s; + const endIndex = terminal.buffer.stringIndexToBufferIndex(i, s.length - 1, true); + const endChar = terminal.buffer.lines.get(i).get(endIndex[1])[CHAR_DATA_CHAR_INDEX]; + assert.equal(s[s.length - 1], endChar); + // with trim active the next stop after s should be [i + 1, 0] + assert.deepEqual(terminal.buffer.stringIndexToBufferIndex(i, s.length, true), [i + 1, 0]); + } + const lastIndex = terminal.buffer.stringIndexToBufferIndex(0, content.length - 1, true); + const lastChar = terminal.buffer.lines.get(lastIndex[0]).get(lastIndex[1])[CHAR_DATA_CHAR_INDEX]; + assert.equal(content[content.length - 1], lastChar); + }); }); describe('BufferStringIterator', function(): void { it('iterator does not ovrflow buffer limits', function(): void { diff --git a/src/Buffer.ts b/src/Buffer.ts index 10f1d1b2cb..a164a77f7d 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -243,18 +243,23 @@ export class Buffer implements IBuffer { * The method operates on the CharData string length, there are no * additional content or boundary checks. Therefore the string and the buffer * should not be altered in between. - * TODO: respect trim flag after fixing #1685 + * Note: Trimmed cells will be skipped until the next cell with real content. + * Thus it is not possible to get the end of a trimmed string with string.length + * directly. Instead use the last char index and add the char's width to the + * index. * @param lineIndex line index the string was retrieved from * @param stringIndex index within the string * @param startCol column offset the string was retrieved from + * @param trimRight whether to trim spaces from right, defaults to false */ - public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): BufferIndex { + public stringIndexToBufferIndex(lineIndex: number, stringIndex: number, trimRight: boolean = false): BufferIndex { while (stringIndex) { const line = this.lines.get(lineIndex); if (!line) { - [-1, -1]; + return [-1, -1]; } - for (let i = 0; i < line.length; ++i) { + const length = (trimRight) ? line.getTrimmedLength() : line.length; + for (let i = 0; i < length; ++i) { stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length; if (stringIndex < 0) { return [lineIndex, i]; diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7604b01f69..632a0fab33 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -415,6 +415,10 @@ export class InputHandler extends Disposable implements IInputHandler { // autowrap - DECAWM // automatically wraps to the beginning of the next line if (wraparoundMode) { + // nullify leftover cells to the right (happens only for wide chars) + for (let i = buffer.x; i < bufferRow.length; ++i) { + bufferRow.set(i, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + } buffer.x = 0; buffer.y++; if (buffer.y > buffer.scrollBottom) { diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 0ba1294a0e..a1e08348ae 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -5,11 +5,10 @@ import { assert } from 'chai'; import { IMouseZoneManager, IMouseZone } from './ui/Types'; -import { ILinkMatcher, ITerminal, IBufferLine } from './Types'; +import { ILinkMatcher, ITerminal } from './Types'; import { Linkifier } from './Linkifier'; -import { MockBuffer, MockTerminal, TestTerminal } from './ui/TestUtils.test'; -import { CircularList } from './common/CircularList'; -import { BufferLine } from './BufferLine'; +import { getStringCellWidth } from './CharWidth'; +import { TestTerminal } from './ui/TestUtils.test'; class TestLinkifier extends Linkifier { constructor(terminal: ITerminal) { @@ -35,37 +34,20 @@ class TestMouseZoneManager implements IMouseZoneManager { } describe('Linkifier', () => { - let terminal: ITerminal; + let terminal: TestTerminal; let linkifier: TestLinkifier; let mouseZoneManager: TestMouseZoneManager; beforeEach(() => { - terminal = new MockTerminal(); - terminal.cols = 100; - terminal.rows = 10; - terminal.buffer = new MockBuffer(); - (terminal.buffer).setLines(new CircularList(20)); - terminal.buffer.ydisp = 0; + terminal = new TestTerminal({cols: 100, rows: 10}); linkifier = new TestLinkifier(terminal); mouseZoneManager = new TestMouseZoneManager(); + linkifier.attachToDom(mouseZoneManager); }); - function stringToRow(text: string): IBufferLine { - const result = new BufferLine(text.length); - for (let i = 0; i < text.length; i++) { - result.set(i, [0, text.charAt(i), 1, text.charCodeAt(i)]); - } - return result; - } - - function addRow(text: string): void { - terminal.buffer.lines.push(stringToRow(text)); - } - - function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void { - addRow(rowText); + function assertLinkifiesInTerminalRow(terminal: TestTerminal, rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void { + terminal.writeSync(rowText); linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); - terminal.rows = terminal.buffer.lines.length - 1; linkifier.linkifyRows(); // Allow linkify to happen setTimeout(() => { @@ -73,15 +55,15 @@ describe('Linkifier', () => { links.forEach((l, i) => { assert.equal(mouseZoneManager.zones[i].x1, l.x + 1); assert.equal(mouseZoneManager.zones[i].x2, l.x + l.length + 1); - assert.equal(mouseZoneManager.zones[i].y1, terminal.buffer.lines.length); - assert.equal(mouseZoneManager.zones[i].y2, terminal.buffer.lines.length); + assert.equal(mouseZoneManager.zones[i].y1, 1); + assert.equal(mouseZoneManager.zones[i].y2, 1); }); done(); }, 0); } - function assertLinkifiesMultiLineLink(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: MochaDone): void { - addRow(rowText); + function assertLinkifiesInTerminal(terminal: TestTerminal, rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: MochaDone): void { + terminal.writeSync(rowText); linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); linkifier.linkifyRows(); // Allow linkify to happen @@ -114,55 +96,60 @@ describe('Linkifier', () => { describe('link matcher', () => { it('should match a single link', done => { - assertLinkifiesRow('foo', /foo/, [{x: 0, length: 3}], done); + assertLinkifiesInTerminalRow(terminal, 'foo', /foo/, [{x: 0, length: 3}], done); }); it('should match a single link at the start of a text node', done => { - assertLinkifiesRow('foo bar', /foo/, [{x: 0, length: 3}], done); + assertLinkifiesInTerminalRow(terminal, 'foo bar', /foo/, [{x: 0, length: 3}], done); }); it('should match a single link in the middle of a text node', done => { - assertLinkifiesRow('foo bar baz', /bar/, [{x: 4, length: 3}], done); + assertLinkifiesInTerminalRow(terminal, 'foo bar baz', /bar/, [{x: 4, length: 3}], done); }); it('should match a single link at the end of a text node', done => { - assertLinkifiesRow('foo bar', /bar/, [{x: 4, length: 3}], done); + assertLinkifiesInTerminalRow(terminal, 'foo bar', /bar/, [{x: 4, length: 3}], done); }); it('should match a link after a link at the start of a text node', done => { - assertLinkifiesRow('foo bar', /foo|bar/, [{x: 0, length: 3}, {x: 4, length: 3}], done); + assertLinkifiesInTerminalRow(terminal, 'foo bar', /foo|bar/, [{x: 0, length: 3}, {x: 4, length: 3}], done); }); it('should match a link after a link in the middle of a text node', done => { - assertLinkifiesRow('foo bar baz', /bar|baz/, [{x: 4, length: 3}, {x: 8, length: 3}], done); + assertLinkifiesInTerminalRow(terminal, 'foo bar baz', /bar|baz/, [{x: 4, length: 3}, {x: 8, length: 3}], done); }); it('should match a link immediately after a link at the end of a text node', done => { - assertLinkifiesRow('foo barbaz', /bar|baz/, [{x: 4, length: 3}, {x: 7, length: 3}], done); + assertLinkifiesInTerminalRow(terminal, 'foo barbaz', /bar|baz/, [{x: 4, length: 3}, {x: 7, length: 3}], done); }); it('should not duplicate text after a unicode character (wrapped in a span)', done => { // This is a regression test for an issue that came about when using // an oh-my-zsh theme that added the large blue diamond unicode // character (U+1F537) which caused the path to be duplicated. See #642. - assertLinkifiesRow('echo \'🔷foo\'', /foo/, [{x: 8, length: 3}], done); + const charWidth = getStringCellWidth('🔷'); // FIXME: make unicode version dependent + assertLinkifiesInTerminalRow(terminal, 'echo \'🔷foo\'', /foo/, [{x: 6 + charWidth, length: 3}], done); }); describe('multi-line links', () => { + let terminal: TestTerminal; + beforeEach(() => { + terminal = new TestTerminal({cols: 4, rows: 10}); + linkifier = new TestLinkifier(terminal); + mouseZoneManager = new TestMouseZoneManager(); + linkifier.attachToDom(mouseZoneManager); + }); + it('should match links that start on line 1/2 of a wrapped line and end on the last character of line 1/2', done => { - terminal.cols = 4; - assertLinkifiesMultiLineLink('12345', /1234/, [{x1: 0, x2: 4, y1: 0, y2: 0}], done); + assertLinkifiesInTerminal(terminal, '12345', /1234/, [{x1: 0, x2: 4, y1: 0, y2: 0}], done); }); it('should match links that start on line 1/2 of a wrapped line and wrap to line 2/2', done => { - terminal.cols = 4; - assertLinkifiesMultiLineLink('12345', /12345/, [{x1: 0, x2: 1, y1: 0, y2: 1}], done); + assertLinkifiesInTerminal(terminal, '12345', /12345/, [{x1: 0, x2: 1, y1: 0, y2: 1}], done); }); it('should match links that start and end on line 2/2 of a wrapped line', done => { - terminal.cols = 4; - assertLinkifiesMultiLineLink('12345678', /5678/, [{x1: 0, x2: 4, y1: 1, y2: 1}], done); + assertLinkifiesInTerminal(terminal, '12345678', /5678/, [{x1: 0, x2: 4, y1: 1, y2: 1}], done); }); it('should match links that start on line 2/3 of a wrapped line and wrap to line 3/3', done => { - terminal.cols = 4; - assertLinkifiesMultiLineLink('123456789', /56789/, [{x1: 0, x2: 1, y1: 1, y2: 2}], done); + assertLinkifiesInTerminal(terminal, '123456789', /56789/, [{x1: 0, x2: 1, y1: 1, y2: 2}], done); }); }); }); describe('validationCallback', () => { it('should enable link if true', done => { - addRow('test'); + terminal.writeSync('test'); linkifier.registerLinkMatcher(/test/, () => done(), { validationCallback: (url, cb) => { assert.equal(mouseZoneManager.zones.length, 0); @@ -180,7 +167,7 @@ describe('Linkifier', () => { }); it('should validate the uri, not the row', done => { - addRow('abc test abc'); + terminal.writeSync('abc test abc'); linkifier.registerLinkMatcher(/test/, () => done(), { validationCallback: (uri, cb) => { assert.equal(uri, 'test'); @@ -191,7 +178,7 @@ describe('Linkifier', () => { }); it('should disable link if false', done => { - addRow('test'); + terminal.writeSync('test'); linkifier.registerLinkMatcher(/test/, () => assert.fail(), { validationCallback: (url, cb) => { assert.equal(mouseZoneManager.zones.length, 0); @@ -205,7 +192,7 @@ describe('Linkifier', () => { }); it('should trigger for multiple link matches on one row', done => { - addRow('test test'); + terminal.writeSync('test test'); let count = 0; linkifier.registerLinkMatcher(/test/, () => assert.fail(), { validationCallback: (url, cb) => { @@ -243,8 +230,6 @@ describe('Linkifier', () => { describe('unicode handling', () => { let terminal: TestTerminal; - // other than the tests above unicode testing needs the full terminal instance - // to get the special handling of fullwidth, surrogate and combining chars in the input handler beforeEach(() => { terminal = new TestTerminal({cols: 10, rows: 5}); linkifier = new TestLinkifier(terminal); @@ -252,85 +237,68 @@ describe('Linkifier', () => { linkifier.attachToDom(mouseZoneManager); }); - function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: MochaDone): void { - terminal.writeSync(rowText); - linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); - linkifier.linkifyRows(); - // Allow linkify to happen - setTimeout(() => { - assert.equal(mouseZoneManager.zones.length, links.length); - links.forEach((l, i) => { - assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1); - assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1); - assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1); - assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1); - }); - done(); - }, 0); - } - describe('unicode before the match', () => { it('combining - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done); + assertLinkifiesInTerminal(terminal, 'e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done); }); it('combining - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); + assertLinkifiesInTerminal(terminal, 'e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); }); it('surrogate - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done); + assertLinkifiesInTerminal(terminal, '𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done); }); it('surrogate - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); + assertLinkifiesInTerminal(terminal, '𝄞𝄞𝄞 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); }); it('combining surrogate - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done); + assertLinkifiesInTerminal(terminal, '𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done); }); it('combining surrogate - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); + assertLinkifiesInTerminal(terminal, '𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); }); it('fullwidth - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done); + assertLinkifiesInTerminal(terminal, '12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done); }); it('fullwidth - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); + assertLinkifiesInTerminal(terminal, '12 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); }); it('combining fullwidth - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done); + assertLinkifiesInTerminal(terminal, '¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done); }); it('combining fullwidth - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); + assertLinkifiesInTerminal(terminal, '¥\u0301¥\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); }); }); describe('unicode within the match', () => { it('combining - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done); + assertLinkifiesInTerminal(terminal, 'test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done); }); it('combining - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done); + assertLinkifiesInTerminal(terminal, 'testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done); }); it('surrogate - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done); + assertLinkifiesInTerminal(terminal, 'test a𝄞b', /a𝄞b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done); }); it('surrogate - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done); + assertLinkifiesInTerminal(terminal, 'testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done); }); it('combining surrogate - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done); + assertLinkifiesInTerminal(terminal, 'test a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done); }); it('combining surrogate - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done); + assertLinkifiesInTerminal(terminal, 'testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done); }); it('fullwidth - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('test a1b', /a1b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done); + assertLinkifiesInTerminal(terminal, 'test a1b', /a1b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done); }); it('fullwidth - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done); + assertLinkifiesInTerminal(terminal, 'testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done); }); it('combining fullwidth - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done); + assertLinkifiesInTerminal(terminal, 'test a¥\u0301b', /a¥\u0301b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done); }); it('combining fullwidth - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done); + assertLinkifiesInTerminal(terminal, 'testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done); }); }); }); diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 2ec3ed400d..5299d38bb7 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -7,8 +7,7 @@ import { IMouseZoneManager } from './ui/Types'; import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult } from './Types'; import { MouseZone } from './ui/MouseZoneManager'; import { EventEmitter } from './common/EventEmitter'; -import { CHAR_DATA_ATTR_INDEX } from './Buffer'; -import { getStringCellWidth } from './CharWidth'; +import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; /** * The Linkifier applies links to rows shortly after they have been refreshed. @@ -111,7 +110,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { // chars will not match anymore at the viewport borders. const overscanLineLimit = Math.ceil(Linkifier.OVERSCAN_CHAR_LIMIT / this._terminal.cols); const iterator = this._terminal.buffer.iterator( - false, absoluteRowIndexStart, absoluteRowIndexEnd, overscanLineLimit, overscanLineLimit); + true, absoluteRowIndexStart, absoluteRowIndexEnd, overscanLineLimit, overscanLineLimit); while (iterator.hasNext()) { const lineData: IBufferStringIteratorResult = iterator.next(); for (let i = 0; i < this._linkMatchers.length; i++) { @@ -219,9 +218,45 @@ 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 < -1) { + break; + } // get the buffer index as [absolute row, col] for the match - const bufferIndex = this._terminal.buffer.stringIndexToBufferIndex(rowIndex, stringIndex); + const bufferIndex = this._terminal.buffer.stringIndexToBufferIndex(rowIndex, stringIndex, true); + if (bufferIndex[0] < 0) { + break; + } + // calculate buffer index of uri end + // we cannot directly use uri.length here since stringIndexToBufferIndex would + // skip empty cells and stop at the next cell with real content + // instead we fetch the index of the last char in uri and advance to the next cell + const endIndex = this._terminal.buffer.stringIndexToBufferIndex(rowIndex, stringIndex + uri.length - 1, true); + if (endIndex[0] < 0) { + break; + } + + // adjust start index to visible line length + if (bufferIndex[1] >= this._terminal.cols) { + bufferIndex[0]++; + bufferIndex[1] = 0; + } + // advance endIndex to next cell: + // add actual length of the last char to x_offset + // wrap to next buffer line if we overflow + endIndex[1] += this._terminal.buffer.lines.get(endIndex[0]).get(endIndex[1])[CHAR_DATA_WIDTH_INDEX]; + if (endIndex[1] >= this._terminal.buffer.lines.get(endIndex[0]).length) { + endIndex[0]++; + endIndex[1] = 0; + } + // adjust end index to visible line length + if (endIndex[1] > this._terminal.cols) { + endIndex[1] = this._terminal.cols; + } + const visibleLength = (endIndex[0] - bufferIndex[0]) * this._terminal.cols - bufferIndex[1] + endIndex[1]; + if (visibleLength < 1) { + continue; + } const line = this._terminal.buffer.lines.get(bufferIndex[0]); const char = line.get(bufferIndex[1]); @@ -238,11 +273,11 @@ export class Linkifier extends EventEmitter implements ILinkifier { return; } if (isValid) { - this._addLink(bufferIndex[1], bufferIndex[0] - this._terminal.buffer.ydisp, uri, matcher, fg); + this._addLink(bufferIndex[1], bufferIndex[0] - this._terminal.buffer.ydisp, uri, visibleLength, matcher, fg); } }); } else { - this._addLink(bufferIndex[1], bufferIndex[0] - this._terminal.buffer.ydisp, uri, matcher, fg); + this._addLink(bufferIndex[1], bufferIndex[0] - this._terminal.buffer.ydisp, uri, visibleLength, matcher, fg); } } } @@ -255,8 +290,8 @@ export class Linkifier extends EventEmitter implements ILinkifier { * @param matcher The link matcher for the link. * @param fg The link color for hover event. */ - private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher, fg: number): void { - const width = getStringCellWidth(uri); + private _addLink(x: number, y: number, uri: string, length: number, matcher: ILinkMatcher, fg: number): void { + const width = length; const x1 = x % this._terminal.cols; const y1 = y + Math.floor(x / this._terminal.cols); let x2 = (x1 + width) % this._terminal.cols; diff --git a/src/Types.ts b/src/Types.ts index 8ebb28d3e3..ab06f99f8b 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -296,7 +296,7 @@ export interface IBuffer { nextStop(x?: number): number; prevStop(x?: number): number; getBlankLine(attr: number, isWrapped?: boolean): IBufferLine; - stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[]; + stringIndexToBufferIndex(lineIndex: number, stringIndex: number, trimRight?: boolean): number[]; iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator; }