diff --git a/lib/hterm.js b/lib/hterm.js index 6282e77e..b718c92a 100644 --- a/lib/hterm.js +++ b/lib/hterm.js @@ -86,21 +86,17 @@ lib.wc.substr = function (str, start, optWidth) { const chars = runes(str); let startIndex; let endIndex; - let width = 0; + let width; - for (let i = 0; i < chars.length; i++) { - const codePoint = chars[i].codePointAt(0); - const charWidth = lib.wc.charWidth(codePoint); - if ((width + charWidth) > start) { - startIndex = i; + for (startIndex = 0, width = 0; startIndex < chars.length; startIndex++) { + width += lib.wc.charWidth(chars[startIndex].codePointAt(0)); + if (width > start) { break; } - width += charWidth; } if (optWidth) { - width = 0; - for (endIndex = startIndex; endIndex < chars.length && width < optWidth; endIndex++) { + for (endIndex = startIndex, width = 0; endIndex < chars.length && width < optWidth; endIndex++) { width += lib.wc.charWidth(chars[endIndex].charCodeAt(0)); } @@ -120,28 +116,6 @@ hterm.Keyboard.prototype.onTextInput_ = function (e) { runes(e.data).forEach(this.terminal.onVTKeystroke.bind(this.terminal)); }; -hterm.Terminal.IO.prototype.writeUTF8 = function (string) { - if (this.terminal_.io !== this) { - throw new Error('Attempt to print from inactive IO object.'); - } - - if (!containsNonLatinCodepoints(string)) { - this.terminal_.interpret(string); - return; - } - - runes(string).forEach(rune => { - this.terminal_.getTextAttributes().unicodeNode = containsNonLatinCodepoints(rune); - this.terminal_.interpret(rune); - this.terminal_.getTextAttributes().unicodeNode = false; - }); -}; - -const oldIsDefault = hterm.TextAttributes.prototype.isDefault; -hterm.TextAttributes.prototype.isDefault = function () { - return !this.unicodeNode && oldIsDefault.call(this); -}; - const oldSetFontSize = hterm.Terminal.prototype.setFontSize; hterm.Terminal.prototype.setFontSize = function (px) { oldSetFontSize.call(this, px); @@ -153,7 +127,7 @@ hterm.Terminal.prototype.setFontSize = function (px) { doc.head.appendChild(unicodeNodeStyle); } unicodeNodeStyle.innerHTML = ` - .unicode-node { + .unicode-node:not(.wc-node) { display: inline-block; vertical-align: top; width: ${this.scrollPort_.characterSize.width}px; @@ -161,23 +135,6 @@ hterm.Terminal.prototype.setFontSize = function (px) { `; }; -const oldCreateContainer = hterm.TextAttributes.prototype.createContainer; -hterm.TextAttributes.prototype.createContainer = function (text) { - const container = oldCreateContainer.call(this, text); - if (container.style && runes(text).length === 1 && containsNonLatinCodepoints(text)) { - container.className += ' unicode-node'; - } - return container; -}; - -// Do not match containers when one of them has unicode text (unicode chars need to be alone in their containers) -const oldMatchesContainer = hterm.TextAttributes.prototype.matchesContainer; -hterm.TextAttributes.prototype.matchesContainer = function (obj) { - return oldMatchesContainer.call(this, obj) && - !this.unicodeNode && - !containsNonLatinCodepoints(obj.textContent); -}; - // there's no option to turn off the size overlay hterm.Terminal.prototype.overlaySize = function () {}; @@ -388,6 +345,157 @@ hterm.Terminal.prototype.focusHyperCaret = function () { this.hyperCaret.focus(); }; +// patch for unicode encapsulation +// see comments in original code +hterm.Screen.prototype.insertString = function (str) { + let cursorNode = this.cursorNode_; + let cursorNodeText = cursorNode.textContent; + this.cursorRowNode_.removeAttribute('line-overflow'); + const strWidth = lib.wc.strWidth(str); + this.cursorPosition.column += strWidth; + let offset = this.cursorOffset_; + let reverseOffset = hterm.TextAttributes.nodeWidth(cursorNode) - offset; + if (reverseOffset < 0) { + const ws = lib.f.getWhitespace(-reverseOffset); + if (!(this.textAttributes.underline || + this.textAttributes.strikethrough || + this.textAttributes.background || + this.textAttributes.wcNode || + this.textAttributes.tileData !== null)) { + str = ws + str; + } else if (cursorNode.nodeType === 3 || + !(cursorNode.wcNode || + cursorNode.tileNode || + cursorNode.style.textDecoration || + cursorNode.style.backgroundColor)) { + cursorNode.textContent = (cursorNodeText += ws); + } else { + const wsNode = cursorNode.ownerDocument.createTextNode(ws); + this.cursorRowNode_.insertBefore(wsNode, cursorNode.nextSibling); + this.cursorNode_ = cursorNode = wsNode; + this.cursorOffset_ = offset = -reverseOffset; + cursorNodeText = ws; + } + reverseOffset = 0; + } + this.wrapUnicode(cursorNode); + if (this.textAttributes.matchesContainer(cursorNode)) { + if (reverseOffset === 0) { + cursorNode.textContent = cursorNodeText + str; + } else if (offset === 0) { + cursorNode.textContent = str + cursorNodeText; + } else { + cursorNode.textContent = + hterm.TextAttributes.nodeSubstr(cursorNode, 0, offset) + + str + hterm.TextAttributes.nodeSubstr(cursorNode, offset); + } + this.cursorOffset_ += strWidth; + this.wrapUnicode(cursorNode); + return; + } + if (offset === 0) { + const previousSibling = cursorNode.previousSibling; + if (previousSibling && + this.textAttributes.matchesContainer(previousSibling)) { + previousSibling.textContent += str; + this.cursorNode_ = previousSibling; + this.cursorOffset_ = lib.wc.strWidth(previousSibling.textContent); + this.wrapUnicode(previousSibling); + return; + } + const newNode = this.textAttributes.createContainer(str); + this.cursorRowNode_.insertBefore(newNode, cursorNode); + this.cursorNode_ = newNode; + this.cursorOffset_ = strWidth; + this.wrapUnicode(newNode); + return; + } + if (reverseOffset === 0) { + const nextSibling = cursorNode.nextSibling; + if (nextSibling && + this.textAttributes.matchesContainer(nextSibling)) { + nextSibling.textContent = str + nextSibling.textContent; + this.cursorNode_ = nextSibling; + this.cursorOffset_ = lib.wc.strWidth(str); + this.wrapUnicode(nextSibling); + return; + } + const newNode = this.textAttributes.createContainer(str); + this.cursorRowNode_.insertBefore(newNode, nextSibling); + this.cursorNode_ = newNode; + this.cursorOffset_ = hterm.TextAttributes.nodeWidth(newNode); + this.wrapUnicode(newNode); + return; + } + this.splitNode_(cursorNode, offset); + const newNode = this.textAttributes.createContainer(str); + this.cursorRowNode_.insertBefore(newNode, cursorNode.nextSibling); + this.cursorNode_ = newNode; + this.cursorOffset_ = strWidth; + this.wrapUnicode(newNode); +}; + +// patch for unicode encapsulation +hterm.Screen.prototype.splitNode_ = function (node, offset) { + const afterNode = node.cloneNode(false); + const textContent = node.textContent; + node.textContent = hterm.TextAttributes.nodeSubstr(node, 0, offset); + afterNode.textContent = lib.wc.substr(textContent, offset); + this.wrapUnicode(node); + this.wrapUnicode(afterNode); + if (afterNode.textContent) { + node.parentNode.insertBefore(afterNode, node.nextSibling); + } + if (!node.textContent) { + node.parentNode.removeChild(node); + } +}; + +// encapsulate unicode chars in individual spans +// keep them in the parent node with the rest of the content +// maintains styling and overflow display in divs with a background color +hterm.Screen.prototype.wrapUnicode = function (node) { + const nodeContent = node.textContent; + if (containsNonLatinCodepoints(nodeContent) && !node.className.includes('wc-node')) { + const doc = this.terminal.document_; + if (!node.className.includes('unicode-parent-node')) { + node.className += ' unicode-parent-node'; + } + node.textContent = null; + let strBuffer = ''; + runes(nodeContent).forEach(rune => { + if (containsNonLatinCodepoints(rune)) { + if (strBuffer !== '') { + const stringNode = doc.createTextNode(strBuffer); + node.appendChild(stringNode); + strBuffer = ''; + } + const unicodeNode = doc.createElement('span'); + unicodeNode.className = 'unicode-node'; + unicodeNode.textContent = rune; + node.appendChild(unicodeNode); + } else { + strBuffer += rune; + } + }); + if (strBuffer !== '') { + const stringNode = doc.createTextNode(strBuffer); + node.appendChild(stringNode); + } + } +}; + +// ensure no text node contains unicode +const oldCreateContainer = hterm.TextAttributes.prototype.createContainer; +hterm.TextAttributes.prototype.createContainer = function (text) { + if (this.isDefault() && text && containsNonLatinCodepoints(text)) { + const span = this.document_.createElement('span'); + span.textContent = text; + return span; + } + return oldCreateContainer.call(this, text); +}; + hterm.Screen.prototype.syncSelectionCaret = function () { const p = this.terminal.hyperCaret; const doc = this.terminal.document_;