add support for variable size characters

This commit is contained in:
Fabian Jakobs 2011-11-13 19:27:31 +01:00
commit 36ebe6c496
7 changed files with 163 additions and 89 deletions

View file

@ -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")
)
];

View file

@ -0,0 +1,7 @@
Pinyin Simplified
-----------------
反对方山东队但是上
Thai
----
อักษรไทย

View file

@ -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);

View file

@ -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()

View file

@ -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(
"<div class='", clazz, "' style='",
@ -159,7 +156,7 @@ var Marker = function(parentEl) {
// from start of the last line to the selection end
top = this.$getTop(range.end.row, layerConfig);
width = Math.round(range.end.column * layerConfig.characterWidth);
width = layerConfig.textWidth(range.end.row, range.end.column);
stringBuilder.push(
"<div class='", clazz, "' style='",
@ -191,16 +188,15 @@ var Marker = function(parentEl) {
this.drawSingleLineMarker = function(stringBuilder, range, clazz, layerConfig, extraLength, type) {
var padding = type === "background" ? 0 : this.$padding;
var height = layerConfig.lineHeight;
var textWidth = layerConfig.textWidth(range.start.row, range.start.column);
if (type === "background")
var width = layerConfig.width;
else
width = Math.round((range.end.column + (extraLength || 0) - range.start.column) * layerConfig.characterWidth);
width = layerConfig.textWidth(range.start.row, range.end.column + (extraLength || 0)) - textWidth;
var top = this.$getTop(range.start.row, layerConfig);
var left = Math.round(
padding + range.start.column * layerConfig.characterWidth
);
var left = padding + textWidth;
stringBuilder.push(
"<div class='", clazz, "' style='",

View file

@ -346,7 +346,7 @@ var Text = function(parentEl) {
this.$renderToken = function(stringBuilder, screenColumn, token, value) {
var self = this;
var replaceReg = /\t|&|<|( +)|([\v\f \u00a0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000])|[\u1100-\u115F]|[\u11A3-\u11A7]|[\u11FA-\u11FF]|[\u2329-\u232A]|[\u2E80-\u2E99]|[\u2E9B-\u2EF3]|[\u2F00-\u2FD5]|[\u2FF0-\u2FFB]|[\u3000-\u303E]|[\u3041-\u3096]|[\u3099-\u30FF]|[\u3105-\u312D]|[\u3131-\u318E]|[\u3190-\u31BA]|[\u31C0-\u31E3]|[\u31F0-\u321E]|[\u3220-\u3247]|[\u3250-\u32FE]|[\u3300-\u4DBF]|[\u4E00-\uA48C]|[\uA490-\uA4C6]|[\uA960-\uA97C]|[\uAC00-\uD7A3]|[\uD7B0-\uD7C6]|[\uD7CB-\uD7FB]|[\uF900-\uFAFF]|[\uFE10-\uFE19]|[\uFE30-\uFE52]|[\uFE54-\uFE66]|[\uFE68-\uFE6B]|[\uFF01-\uFF60]|[\uFFE0-\uFFE6]/g;
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("&#160;");
@ -361,14 +361,6 @@ var Text = function(parentEl) {
return "&amp;";
} else if (c == "<") {
return "&lt;";
} else if (c == "\u3000") {
// U+3000 is both invisible AND full-width, so must be handled uniquely
var classToUse = self.showInvisibles ? "ace_cjk ace_invisible" : "ace_cjk";
var space = self.showInvisibles ? self.SPACE_CHAR : "";
screenColumn += 1;
return "<span class='" + classToUse + "' style='width:" +
(self.config.characterWidth * 2) +
"px'>" + space + "</span>";
} 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 "&#160;";
}
} else {
screenColumn += 1;
return "<span class='ace_cjk' style='width:" +
(self.config.characterWidth * 2) +
"px'>" + c + "</span>";
}
};
@ -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("&#160;");
} 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 "&amp;";
} else if (c == "<") {
return "&lt;";
} 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 "<span class='ace_invisible'>" + space + "</span>";
} else {
return "&#160;";
}
}
};
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("<span class='", classes, "'>", output, "</span>");
}
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;

View file

@ -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));
};