diff --git a/demo/kitchen-sink/demo.js b/demo/kitchen-sink/demo.js index 9e3062f5..07bf9383 100644 --- a/demo/kitchen-sink/demo.js +++ b/demo/kitchen-sink/demo.js @@ -242,6 +242,10 @@ var docs = [ new WrappedDoc( "latex", "LaTeX", require("ace/requirejs/text!./docs/latex.tex") + ), + new Doc( + "markdown", "International Text", + require("ace/requirejs/text!./docs/international.md") ) ]; diff --git a/demo/kitchen-sink/docs/international.md b/demo/kitchen-sink/docs/international.md new file mode 100644 index 00000000..92646875 --- /dev/null +++ b/demo/kitchen-sink/docs/international.md @@ -0,0 +1,7 @@ +Pinyin Simplified +----------------- +反对方山东队但是上 + +Thai +---- +อักษรไทย \ No newline at end of file diff --git a/lib/ace/edit_session.js b/lib/ace/edit_session.js index cbdcc1c9..db79a67b 100644 --- a/lib/ace/edit_session.js +++ b/lib/ace/edit_session.js @@ -1150,7 +1150,6 @@ var EditSession = function(text, mode) { // "Tokens" var CHAR = 1, - CHAR_EXT = 2, PLACEHOLDER_START = 3, PLACEHOLDER_BODY = 4, PUNCTUATION = 9, @@ -1177,10 +1176,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; @@ -1267,7 +1262,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); } @@ -1299,10 +1294,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); } @@ -1336,12 +1327,8 @@ 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; - } + screenColumn += 1; + if (screenColumn > maxScreenColumn) { break } @@ -1650,45 +1637,6 @@ var EditSession = function(text, mode) { return screenRows; } - // For every keystroke this gets called once per char in the whole doc!! - // Wouldn't hurt to make it a bit faster for c >= 0x1100 - function isFullWidth(c) { - if (c < 0x1100) - return false; - return c >= 0x1100 && c <= 0x115F || - c >= 0x11A3 && c <= 0x11A7 || - c >= 0x11FA && c <= 0x11FF || - c >= 0x2329 && c <= 0x232A || - c >= 0x2E80 && c <= 0x2E99 || - c >= 0x2E9B && c <= 0x2EF3 || - c >= 0x2F00 && c <= 0x2FD5 || - c >= 0x2FF0 && c <= 0x2FFB || - c >= 0x3000 && c <= 0x303E || - c >= 0x3041 && c <= 0x3096 || - c >= 0x3099 && c <= 0x30FF || - c >= 0x3105 && c <= 0x312D || - c >= 0x3131 && c <= 0x318E || - c >= 0x3190 && c <= 0x31BA || - c >= 0x31C0 && c <= 0x31E3 || - c >= 0x31F0 && c <= 0x321E || - c >= 0x3220 && c <= 0x3247 || - c >= 0x3250 && c <= 0x32FE || - c >= 0x3300 && c <= 0x4DBF || - c >= 0x4E00 && c <= 0xA48C || - c >= 0xA490 && c <= 0xA4C6 || - c >= 0xA960 && c <= 0xA97C || - c >= 0xAC00 && c <= 0xD7A3 || - c >= 0xD7B0 && c <= 0xD7C6 || - c >= 0xD7CB && c <= 0xD7FB || - c >= 0xF900 && c <= 0xFAFF || - c >= 0xFE10 && c <= 0xFE19 || - c >= 0xFE30 && c <= 0xFE52 || - c >= 0xFE54 && c <= 0xFE66 || - c >= 0xFE68 && c <= 0xFE6B || - c >= 0xFF01 && c <= 0xFF60 || - c >= 0xFFE0 && c <= 0xFFE6; - }; - }).call(EditSession.prototype); require("./edit_session/folding").Folding.call(EditSession.prototype); diff --git a/lib/ace/layer/cursor.js b/lib/ace/layer/cursor.js index 8dec094d..68d81cac 100644 --- a/lib/ace/layer/cursor.js +++ b/lib/ace/layer/cursor.js @@ -102,14 +102,15 @@ var Cursor = function(parentEl) { var position = this.session.selection.getCursor(); var pos = this.session.documentToScreenPosition(position); - var cursorLeft = Math.round(this.$padding + - pos.column * this.config.characterWidth); - var cursorTop = (pos.row - (onScreen ? this.config.firstRowScreen : 0)) * - this.config.lineHeight; + var textWidth = this.config.textWidth(pos.row, pos.column); + var cursorLeft = Math.round(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 + top : cursorTop, + width: cursorWidth }; }; @@ -120,7 +121,7 @@ var Cursor = function(parentEl) { this.cursor.style.left = this.pixelPos.left + "px"; this.cursor.style.top = this.pixelPos.top + "px"; - this.cursor.style.width = config.characterWidth + "px"; + this.cursor.style.width = this.pixelPos.width + "px"; this.cursor.style.height = config.lineHeight + "px"; var overwrite = this.session.getOverwrite() diff --git a/lib/ace/layer/marker.js b/lib/ace/layer/marker.js index ce8a5222..8133a8a9 100644 --- a/lib/ace/layer/marker.js +++ b/lib/ace/layer/marker.js @@ -81,9 +81,7 @@ var Marker = function(parentEl) { range = range.toScreenRange(this.session); if (marker.renderer) { var top = this.$getTop(range.start.row, config); - var left = Math.round( - 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 (range.isMultiLine()) { @@ -143,11 +141,10 @@ var Marker = function(parentEl) { // from selection start to the end of the line var padding = type === "background" ? 0 : this.$padding; var height = layerConfig.lineHeight; - var width = Math.round(layerConfig.width - (range.start.column * layerConfig.characterWidth)); + var textWidth = layerConfig.textWidth(range.start.row, range.start.column); + var left = padding + textWidth; + var width = Math.round(layerConfig.width - textWidth); var top = this.$getTop(range.start.row, layerConfig); - var left = Math.round( - padding + range.start.column * layerConfig.characterWidth - ); stringBuilder.push( "
" + space + ""; } 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); @@ -376,11 +368,6 @@ var Text = function(parentEl) { } else { return " "; } - } else { - screenColumn += 1; - return "" + c + ""; } }; @@ -398,6 +385,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, row)[0]; + + // 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.tokens, column); + + if (!line.widthCache) + line.widthCache = { rowHeight: this.getLineHeight() }; + + line.widthCache[column] = width; + + return width; + }; this.$renderLineCore = function(stringBuilder, lastRow, tokens, splits, onlyContents) { var chars = 0; diff --git a/lib/ace/virtual_renderer.js b/lib/ace/virtual_renderer.js index 536096b1..d82c3551 100644 --- a/lib/ace/virtual_renderer.js +++ b/lib/ace/virtual_renderer.js @@ -136,6 +136,7 @@ var VirtualRenderer = function(container, theme) { lastRow : 0, lineHeight : 1, characterWidth : 1, + textWidth: function() { return 1; }, minHeight : 1, maxHeight : 1, offset : 0, @@ -523,6 +524,7 @@ var VirtualRenderer = function(container, theme) { lastRow : lastRow, lineHeight : this.lineHeight, characterWidth : this.characterWidth, + textWidth: this.$textLayer.textWidth.bind(this.$textLayer), minHeight : minHeight, maxHeight : maxHeight, offset : offset, @@ -720,14 +722,48 @@ var VirtualRenderer = function(container, theme) { // todo: handle horizontal scrolling }; + 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 character + 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(pageX, pageY) { var canvasPos = this.scroller.getBoundingClientRect(); - var col = Math.round((pageX + this.scroller.scrollLeft - canvasPos.left - this.$padding - dom.getPageScrollLeft()) - / this.characterWidth); var row = Math.floor((pageY + this.scrollTop - canvasPos.top - dom.getPageScrollTop()) / this.lineHeight); + var width = pageX + this.scroller.scrollLeft - canvasPos.left - this.$padding - dom.getPageScrollLeft(); + var col = this.$findColumn(row, width); + return this.session.screenToDocumentPosition(row, Math.max(col, 0)); };