diff --git a/bin/test_weblinks.sh b/bin/test_weblinks.sh
index 4488b983..c0635413 100755
--- a/bin/test_weblinks.sh
+++ b/bin/test_weblinks.sh
@@ -13,13 +13,13 @@ echo "aaa http://example.com ¥¥¥ http://example.com aaa"
echo "¥¥¥ http://example.com ¥¥¥ http://example.com aaa"
# full width within url - partial wrong match
-echo "aaa https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문"
+echo "aaa https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 aaa"
# full width within and before - partial wrong match + wrong offsets
-echo "¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문"
+echo "¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥"
# not matching at all
-echo "http://test:password@example.com/some_path"
+echo "http://test:password@example.com/some_path aaa"
# overly long text with urls with final interpunction
echo "Lorem ipsum dolor sit amet, consetetur sadipscing elitr https://ko.wikipedia.org/wiki/위키백과:대문, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat: http://test:password@example.com/some_path."
\ No newline at end of file
diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts
index 094e6a8e..259f1607 100644
--- a/src/browser/renderer/dom/DomRenderer.ts
+++ b/src/browser/renderer/dom/DomRenderer.ts
@@ -38,6 +38,7 @@ export class DomRenderer extends Disposable implements IRenderer {
private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[] = [];
private _selectionContainer: HTMLElement;
+ private _cellToRowElements: Uint16Array[] = [];
public dimensions: IRenderDimensions;
@@ -359,7 +360,10 @@ export class DomRenderer extends Disposable implements IRenderer {
const row = y + this._bufferService.buffer.ydisp;
const lineData = this._bufferService.buffer.lines.get(row);
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
- rowElement.replaceChildren(this._rowFactory.createRow(lineData!, row, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.css.cell.width, this._bufferService.cols));
+ if (!this._cellToRowElements[y] || this._cellToRowElements[y].length !== this._bufferService.cols) {
+ this._cellToRowElements[y] = new Uint16Array(this._bufferService.cols);
+ }
+ rowElement.replaceChildren(this._rowFactory.createRow(lineData!, row, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.css.cell.width, this._bufferService.cols, this._cellToRowElements[y]));
}
}
@@ -376,8 +380,13 @@ export class DomRenderer extends Disposable implements IRenderer {
}
private _setCellUnderline(x: number, x2: number, y: number, y2: number, cols: number, enabled: boolean): void {
- // FIXME: offset calculation is wrong (temp. fixed by adding empty spans for wide chars
- // to fullfill column to element index identity assumption)
+ x = this._cellToRowElements[y][x];
+ x2 = this._cellToRowElements[y2][x2];
+
+ if (x === -1 || x2 === -1) {
+ return;
+ }
+
while (x !== x2 || y !== y2) {
const row = this._rowElements[y];
if (!row) {
diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts
index fbb48536..7d4adf85 100644
--- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts
+++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts
@@ -14,6 +14,8 @@ import { MockCoreService, MockDecorationService, MockOptionsService } from 'comm
import { css } from 'common/Color';
import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService } from 'browser/TestUtils.test';
+const EMPTY_ELEM_MAPPING = new Uint16Array(1000);
+
describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM;
let rowFactory: DomRendererRowFactory;
@@ -35,7 +37,7 @@ describe('DomRendererRowFactory', () => {
describe('createRow', () => {
it('should not create anything for an empty row', () => {
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
''
);
@@ -45,7 +47,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]));
// There should be no element for the following "empty" cell
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '', 0, 0]));
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'語'
);
@@ -53,7 +55,7 @@ describe('DomRendererRowFactory', () => {
it('should add class for cursor and cursor style', () => {
for (const style of ['block', 'bar', 'underline']) {
- const fragment = rowFactory.createRow(lineData, 0, true, style, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, true, style, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
` `
);
@@ -61,7 +63,7 @@ describe('DomRendererRowFactory', () => {
});
it('should add class for cursor blink', () => {
- const fragment = rowFactory.createRow(lineData, 0, true, 'block', 0, true, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, true, 'block', 0, true, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
` `
);
@@ -70,7 +72,7 @@ describe('DomRendererRowFactory', () => {
it('should not render cells that go beyond the terminal\'s columns', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 1);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 1, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -81,7 +83,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.BOLD;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -91,7 +93,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.ITALIC;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -101,7 +103,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.DIM;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -114,7 +116,7 @@ describe('DomRendererRowFactory', () => {
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.SINGLE;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -125,7 +127,7 @@ describe('DomRendererRowFactory', () => {
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.DOUBLE;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -136,7 +138,7 @@ describe('DomRendererRowFactory', () => {
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.CURLY;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -147,7 +149,7 @@ describe('DomRendererRowFactory', () => {
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.DOTTED;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -158,7 +160,7 @@ describe('DomRendererRowFactory', () => {
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.DASHED;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -169,7 +171,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.STRIKETHROUGH;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -182,7 +184,7 @@ describe('DomRendererRowFactory', () => {
cell.fg &= ~Attributes.PCOLOR_MASK;
cell.fg |= i;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
`a`
);
@@ -196,7 +198,7 @@ describe('DomRendererRowFactory', () => {
cell.bg &= ~Attributes.PCOLOR_MASK;
cell.bg |= i;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
`a`
);
@@ -208,7 +210,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= Attributes.CM_P16 | 2 | FgFlags.INVERSE;
cell.bg |= Attributes.CM_P16 | 1;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -219,7 +221,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= FgFlags.INVERSE;
cell.bg |= Attributes.CM_P16 | 1;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -229,7 +231,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg |= Attributes.CM_P16 | 1 | FgFlags.INVERSE;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -242,7 +244,7 @@ describe('DomRendererRowFactory', () => {
cell.fg &= ~Attributes.PCOLOR_MASK;
cell.fg |= i;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
`a`
);
@@ -254,7 +256,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3;
cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -265,7 +267,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3 | FgFlags.INVERSE;
cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -277,7 +279,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
rowFactory.handleSelectionChanged([1, 0], [2, 0], false);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'ab'
);
@@ -285,7 +287,7 @@ describe('DomRendererRowFactory', () => {
it('should force whitespace cells to be rendered above the background', () => {
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
rowFactory.handleSelectionChanged([0, 0], [2, 0], false);
- const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
' a'
);
diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts
index e4722186..b39c62c0 100644
--- a/src/browser/renderer/dom/DomRendererRowFactory.ts
+++ b/src/browser/renderer/dom/DomRendererRowFactory.ts
@@ -32,7 +32,6 @@ export class DomRendererRowFactory {
private _selectionStart: [number, number] | undefined;
private _selectionEnd: [number, number] | undefined;
private _columnSelectMode: boolean = false;
- private _nullSpan: HTMLSpanElement;
constructor(
private readonly _document: Document,
@@ -42,10 +41,7 @@ export class DomRendererRowFactory {
@ICoreService private readonly _coreService: ICoreService,
@IDecorationService private readonly _decorationService: IDecorationService,
@IThemeService private readonly _themeService: IThemeService
- ) {
- this._nullSpan = this._document.createElement('span');
- this._nullSpan.style.width = `0`;
- }
+ ) {}
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
this._selectionStart = start;
@@ -53,7 +49,11 @@ export class DomRendererRowFactory {
this._columnSelectMode = columnSelectMode;
}
- public createRow(lineData: IBufferLine, row: number, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment {
+ public createRow(lineData: IBufferLine, row: number, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number, cellMap: Uint16Array): DocumentFragment {
+ // NOTE: `cellMap` maps cell positions to a span element index in a row.
+ // All positions should be updated, even skipped ones after wide chars or left overs at the end,
+ // otherwise the mouse hover logic might mark the wrong elements as underlined.
+
const fragment = this._document.createDocumentFragment();
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
@@ -71,15 +71,17 @@ export class DomRendererRowFactory {
}
const colors = this._themeService.colors;
+ let elemIndex = -1;
- for (let x = 0; x < lineLength; x++) {
+ let x = 0;
+ for (; x < lineLength; x++) {
lineData.loadCell(x, this._workCell);
let width = this._workCell.getWidth();
// The character to the left is a wide character, drawing is owned by the char at x-1
+ // still have to update cellMap with current element index
if (width === 0) {
- // hack: fix underline bug for wide chars by appending an empty span
- fragment.appendChild(this._nullSpan.cloneNode());
+ cellMap[x] = elemIndex;
continue;
}
@@ -306,9 +308,17 @@ export class DomRendererRowFactory {
}
fragment.appendChild(charElement);
+ cellMap[x] = ++elemIndex;
x = lastCharX;
}
+
+ // since the loop above might exit early not handling all cells,
+ // also set remaining cell positions to last element index
+ if (x < cols - 1) {
+ cellMap.subarray(x + 1).fill(elemIndex);
+ }
+
return fragment;
}