From bcba9bea6e7049abd04e29595ab4686a967d0137 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Sun, 15 Sep 2013 11:09:23 -0700 Subject: [PATCH 1/2] Support variable-width text Updated from 2011 patch by fjakobs: https://github.com/ajaxorg/ace/commit/36ebe6c49632f7a2955ce9933a7432f238a389b7 * removes old double-width CJK handling * replaces monospace assumptions with measurements of actual text * appears to work for Latin, CJK, Thai, Malayalam in ad-hoc testing * does NOT fully handle RTL text issues (Hebrew, Arabic) Conflicts: demo/kitchen-sink/doclist.js lib/ace/edit_session.js lib/ace/layer/text.js lib/ace/virtual_renderer.js --- demo/kitchen-sink/doclist.js | 1 + demo/kitchen-sink/docs/international.md | 7 ++ lib/ace/edit_session.js | 16 +--- lib/ace/layer/cursor.js | 12 ++- lib/ace/layer/marker.js | 13 +-- lib/ace/layer/text.js | 110 +++++++++++++++++++++--- lib/ace/virtual_renderer.js | 40 ++++++++- 7 files changed, 159 insertions(+), 40 deletions(-) create mode 100644 demo/kitchen-sink/docs/international.md diff --git a/demo/kitchen-sink/doclist.js b/demo/kitchen-sink/doclist.js index d003da8e..0b700d19 100644 --- a/demo/kitchen-sink/doclist.js +++ b/demo/kitchen-sink/doclist.js @@ -67,6 +67,7 @@ function makeHuge(txt) { var docs = { "docs/javascript.js": {order: 1, name: "JavaScript"}, + "docs/international.md": "International Text", "docs/latex.tex": {name: "LaTeX", wrapped: true}, "docs/markdown.md": {name: "Markdown", wrapped: true}, diff --git a/demo/kitchen-sink/docs/international.md b/demo/kitchen-sink/docs/international.md new file mode 100644 index 00000000..1170ab12 --- /dev/null +++ b/demo/kitchen-sink/docs/international.md @@ -0,0 +1,7 @@ +Pinyin Simplified +----------------- +反对方山东队但是上 + +Thai +---- +อักษรไทย diff --git a/lib/ace/edit_session.js b/lib/ace/edit_session.js index 9689ea5c..fb066434 100644 --- a/lib/ace/edit_session.js +++ b/lib/ace/edit_session.js @@ -1832,7 +1832,6 @@ var EditSession = function(text, mode) { // "Tokens" var CHAR = 1, - CHAR_EXT = 2, PLACEHOLDER_START = 3, PLACEHOLDER_BODY = 4, PUNCTUATION = 9, @@ -1862,10 +1861,6 @@ var EditSession = function(text, mode) { // Get all the TAB_SPACEs. replace(/12/g, function() { len -= 1; - }). - // Get all the CHAR_EXT/multipleWidth characters. - replace(/2/g, function() { - len -= 1; }); lastDocSplit += len; @@ -1959,7 +1954,7 @@ var EditSession = function(text, mode) { // === ELSE === split = lastSplit + wrapLimit; - // The split is inside of a CHAR or CHAR_EXT token and no space + // The split is inside of a CHAR token and no space // around -> force a split. addSplit(split); } @@ -1993,10 +1988,6 @@ var EditSession = function(text, mode) { arr.push(SPACE); } else if((c > 39 && c < 48) || (c > 57 && c < 64)) { arr.push(PUNCTUATION); - } - // full width characters - else if (c >= 0x1100 && isFullWidth(c)) { - arr.push(CHAR, CHAR_EXT); } else { arr.push(CHAR); } @@ -2028,12 +2019,7 @@ var EditSession = function(text, mode) { if (c == 9) { screenColumn += this.getScreenTabSize(screenColumn); } - // full width characters - else if (c >= 0x1100 && isFullWidth(c)) { - screenColumn += 2; - } else { screenColumn += 1; - } if (screenColumn > maxScreenColumn) { break; } diff --git a/lib/ace/layer/cursor.js b/lib/ace/layer/cursor.js index 4578482a..e6b8e19c 100644 --- a/lib/ace/layer/cursor.js +++ b/lib/ace/layer/cursor.js @@ -170,11 +170,17 @@ var Cursor = function(parentEl) { if (!position) position = this.session.selection.getCursor(); var pos = this.session.documentToScreenPosition(position); - var cursorLeft = this.$padding + pos.column * this.config.characterWidth; + var textWidth = this.config.textWidth(pos.row, pos.column); + var cursorLeft = this.$padding + textWidth; var cursorTop = (pos.row - (onScreen ? this.config.firstRowScreen : 0)) * this.config.lineHeight; + var cursorWidth = (this.config.textWidth(pos.row, pos.column + 1) - textWidth) || this.config.characterWidth; - return {left : cursorLeft, top : cursorTop}; + return { + left : cursorLeft, + top : cursorTop, + width: cursorWidth + }; }; this.update = function(config) { @@ -198,7 +204,7 @@ var Cursor = function(parentEl) { style.left = pixelPos.left + "px"; style.top = pixelPos.top + "px"; - style.width = config.characterWidth + "px"; + style.width = pixelPos.width + "px"; style.height = config.lineHeight + "px"; } while (this.cursors.length > cursorIndex) diff --git a/lib/ace/layer/marker.js b/lib/ace/layer/marker.js index cd1b992d..7238bedf 100644 --- a/lib/ace/layer/marker.js +++ b/lib/ace/layer/marker.js @@ -78,7 +78,7 @@ var Marker = function(parentEl) { range = range.toScreenRange(this.session); if (marker.renderer) { var top = this.$getTop(range.start.row, config); - var left = this.$padding + range.start.column * config.characterWidth; + var left = this.$padding + config.textWidth(range.start.row, range.start.column); marker.renderer(html, range, left, top, config); } else if (marker.type == "fullLine") { this.drawFullLineMarker(html, range, marker.clazz, config); @@ -129,8 +129,10 @@ var Marker = function(parentEl) { // from selection start to the end of the line var padding = this.$padding; var height = config.lineHeight; + var textWidth = config.textWidth(range.start.row, range.start.column); + var left = padding + textWidth; + var width = config.width - textWidth; var top = this.$getTop(range.start.row, config); - var left = padding + range.start.column * config.characterWidth; extraStyle = extraStyle || ""; stringBuilder.push( @@ -143,7 +145,7 @@ var Marker = function(parentEl) { // from start of the last line to the selection end top = this.$getTop(range.end.row, config); - var width = range.end.column * config.characterWidth; + var width = config.textWidth(range.end.row, range.end.column); stringBuilder.push( "
" + space + ""; } else if (b) { return "" + self.SPACE_CHAR + ""; - } else { - screenColumn += 1; - return "" + c + ""; } }; @@ -367,6 +354,101 @@ var Text = function(parentEl) { } return screenColumn + value.length; }; + + this.$measureText = function(tokens, column) { + // build HTML for tokens + var stringBuilder = []; + var len = 0; + var i = 0; + var screenColumn = 0; // TODO + while (len < column && i < tokens.length) { + var token = tokens[i++]; + + var self = this; + var replaceReg = /\t|&|<|( +)|([\v\f \u00a0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000])/g; + var replaceFunc = function(c, a, b, tabIdx, idx4) { + if (c.charCodeAt(0) == 32) { + return new Array(c.length+1).join(" "); + } else if (c == "\t") { + var tabSize = self.session.getScreenTabSize(screenColumn + tabIdx); + screenColumn += tabSize - 1; + return self.$tabStrings[tabSize]; + } else if (c == "&") { + if (useragent.isOldGecko) + return "&"; + else + return "&"; + } else if (c == "<") { + return "<"; + } else if (c.match(/[\v\f \u00a0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000]/)) { + if (self.showInvisibles) { + var space = new Array(c.length+1).join(self.SPACE_CHAR); + return "" + space + ""; + } else { + return " "; + } + } + }; + + var value = token.value; + + // truncate once length is larger than 'column' + len += value.length; + if (len > column) + value = value.substring(0, value.length - (len - column)); + + var output = value.replace(replaceReg, replaceFunc); + + + if (!this.$textToken[token.type]) { + var classes = "ace_" + token.type.replace(/\./g, " ace_"); + stringBuilder.push("", output, ""); + } + else { + stringBuilder.push(output); + } + } + + // render line off screen + var el = document.createElement("div"); + var style = el.style; + style.position = "absolute"; + style.top = "-1000px"; + el.className = "ace_line"; + el.innerHTML = stringBuilder.join(""); + this.element.appendChild(el); + + // measure pixel length + var width = el.offsetWidth; + this.element.removeChild(el); + + return width; + } + + this.textWidth = function(row, column) { + //return this.$characterSize.width * column; + var line = this.session.getTokens(row); + + // cache in tokens object + // this way the cache gets invalidated automatically when the tokens change + if (line.widthCache && line.widthCache[column]) { + // invalidate if font size has changed + if (line.widthCache.rowHeight == this.getLineHeight()) { + return line.widthCache[column]; + } + else + delete line.widthCache; + } + + var width = this.$measureText(line, column); + + if (!line.widthCache) + line.widthCache = { rowHeight: this.getLineHeight() }; + + line.widthCache[column] = width; + + return width; + }; this.renderIndentGuide = function(stringBuilder, value, max) { var cols = value.search(this.$indentGuideRe); diff --git a/lib/ace/virtual_renderer.js b/lib/ace/virtual_renderer.js index 33f3a906..199225ea 100644 --- a/lib/ace/virtual_renderer.js +++ b/lib/ace/virtual_renderer.js @@ -150,6 +150,7 @@ var VirtualRenderer = function(container, theme) { lastRow : 0, lineHeight : 0, characterWidth : 0, + textWidth: function() { return 1; }, minHeight : 1, maxHeight : 1, offset : 0, @@ -997,6 +998,7 @@ var VirtualRenderer = function(container, theme) { lastRow : lastRow, lineHeight : lineHeight, characterWidth : this.characterWidth, + textWidth: this.$textLayer.textWidth.bind(this.$textLayer), minHeight : minHeight, maxHeight : maxHeight, offset : offset, @@ -1389,12 +1391,44 @@ var VirtualRenderer = function(container, theme) { return {row: row, column: col, side: offset - col > 0 ? 1 : -1}; }; + this.$findColumn = function(row, width) { + // binary search to find the screen column + var min = 0; + var max = this.session.getLine(row).length; + + while (true) { + if (max <= 0) + return null; + + // if the range has length one pick the closes characte + if (max-min == 1) { + var wMin = this.$textLayer.textWidth(row, min); + var wMax = this.$textLayer.textWidth(row, max); + + if (Math.abs(wMin-width) < Math.abs(wMax-width)) + return min; + else + return max; + } + else { + // same as Math.floor((max-min)/2) but faster + var pivot = min + ((max - min) >> 1); + var w = this.$textLayer.textWidth(row, pivot); + if (w == width) + return pivot; + else if (w > width) + max = pivot; + else + min = pivot; + } + } + }; + this.screenToTextCoordinates = function(x, y) { var canvasPos = this.scroller.getBoundingClientRect(); - var col = Math.round( - (x + this.scrollLeft - canvasPos.left - this.$padding) / this.characterWidth - ); + var width = x + this.scroller.scrollLeft - canvasPos.left - this.$padding - dom.getPageScrollLeft(); + var col = this.$findColumn(row, width); var row = (y + this.scrollTop - canvasPos.top) / this.lineHeight; From 6d2fbb3ed535e0f8bc792a842094c6b273e55950 Mon Sep 17 00:00:00 2001 From: nightwing Date: Tue, 27 May 2014 04:19:23 +0400 Subject: [PATCH 2/2] fix cursor on lines with tab --- lib/ace/layer/cursor.js | 8 ++----- lib/ace/layer/marker.js | 3 +++ lib/ace/layer/text.js | 44 ++++--------------------------------- lib/ace/virtual_renderer.js | 15 ++++++++----- 4 files changed, 18 insertions(+), 52 deletions(-) diff --git a/lib/ace/layer/cursor.js b/lib/ace/layer/cursor.js index e6b8e19c..2d4a720e 100644 --- a/lib/ace/layer/cursor.js +++ b/lib/ace/layer/cursor.js @@ -170,17 +170,13 @@ var Cursor = function(parentEl) { if (!position) position = this.session.selection.getCursor(); var pos = this.session.documentToScreenPosition(position); - var textWidth = this.config.textWidth(pos.row, pos.column); + var textWidth = this.config.textWidth(pos.row, position.column); var cursorLeft = this.$padding + textWidth; var cursorTop = (pos.row - (onScreen ? this.config.firstRowScreen : 0)) * this.config.lineHeight; var cursorWidth = (this.config.textWidth(pos.row, pos.column + 1) - textWidth) || this.config.characterWidth; - return { - left : cursorLeft, - top : cursorTop, - width: cursorWidth - }; + return {left : cursorLeft, top : cursorTop, width: cursorWidth}; }; this.update = function(config) { diff --git a/lib/ace/layer/marker.js b/lib/ace/layer/marker.js index 7238bedf..af21fcd4 100644 --- a/lib/ace/layer/marker.js +++ b/lib/ace/layer/marker.js @@ -75,7 +75,10 @@ var Marker = function(parentEl) { var range = marker.range.clipRows(config.firstRow, config.lastRow); if (range.isEmpty()) continue; + var docRange = range; range = range.toScreenRange(this.session); + range.start.column = docRange.start.column; + range.end.column = docRange.end.column; if (marker.renderer) { var top = this.$getTop(range.start.row, config); var left = this.$padding + config.textWidth(range.start.row, range.start.column); diff --git a/lib/ace/layer/text.js b/lib/ace/layer/text.js index 6ca804d0..add0dc5f 100644 --- a/lib/ace/layer/text.js +++ b/lib/ace/layer/text.js @@ -346,7 +346,7 @@ var Text = function(parentEl) { var classes = "ace_" + token.type.replace(/\./g, " ace_"); var style = ""; if (token.type == "fold") - style = " style='width:" + (token.value.length * this.config.characterWidth) + "px;' "; + style = " style='width:" + (value.length * this.config.characterWidth) + "px;' "; stringBuilder.push("", output, ""); } else { @@ -360,53 +360,17 @@ var Text = function(parentEl) { var stringBuilder = []; var len = 0; var i = 0; - var screenColumn = 0; // TODO + var screenColumn = 0; while (len < column && i < tokens.length) { var token = tokens[i++]; - - var self = this; - var replaceReg = /\t|&|<|( +)|([\v\f \u00a0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000])/g; - var replaceFunc = function(c, a, b, tabIdx, idx4) { - if (c.charCodeAt(0) == 32) { - return new Array(c.length+1).join(" "); - } else if (c == "\t") { - var tabSize = self.session.getScreenTabSize(screenColumn + tabIdx); - screenColumn += tabSize - 1; - return self.$tabStrings[tabSize]; - } else if (c == "&") { - if (useragent.isOldGecko) - return "&"; - else - return "&"; - } else if (c == "<") { - return "<"; - } else if (c.match(/[\v\f \u00a0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000]/)) { - if (self.showInvisibles) { - var space = new Array(c.length+1).join(self.SPACE_CHAR); - return "" + space + ""; - } else { - return " "; - } - } - }; - var value = token.value; - + // truncate once length is larger than 'column' len += value.length; if (len > column) value = value.substring(0, value.length - (len - column)); - var output = value.replace(replaceReg, replaceFunc); - - - if (!this.$textToken[token.type]) { - var classes = "ace_" + token.type.replace(/\./g, " ace_"); - stringBuilder.push("", output, ""); - } - else { - stringBuilder.push(output); - } + screenColumn = this.$renderToken(stringBuilder, screenColumn, token, value); } // render line off screen diff --git a/lib/ace/virtual_renderer.js b/lib/ace/virtual_renderer.js index 199225ea..1e4746e1 100644 --- a/lib/ace/virtual_renderer.js +++ b/lib/ace/virtual_renderer.js @@ -1413,7 +1413,7 @@ var VirtualRenderer = function(container, theme) { else { // same as Math.floor((max-min)/2) but faster var pivot = min + ((max - min) >> 1); - var w = this.$textLayer.textWidth(row, pivot); + var w = this.$textLayer.textWidth(row, pivot, true); if (w == width) return pivot; else if (w > width) @@ -1426,13 +1426,16 @@ var VirtualRenderer = function(container, theme) { this.screenToTextCoordinates = function(x, y) { var canvasPos = this.scroller.getBoundingClientRect(); - - var width = x + this.scroller.scrollLeft - canvasPos.left - this.$padding - dom.getPageScrollLeft(); - var col = this.$findColumn(row, width); - var row = (y + this.scrollTop - canvasPos.top) / this.lineHeight; - return this.session.screenToDocumentPosition(row, Math.max(col, 0)); + var width = x + this.scroller.scrollLeft - canvasPos.left - this.$padding - dom.getPageScrollLeft(); + + + var pos = this.session.screenToDocumentPosition(row, Math.max(col, 0)); + + var col = this.$findColumn(pos.row, width); + pos.column = col + return pos }; /**