/* vim:ts=4:sts=4:sw=4: * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Ajax.org Code Editor (ACE). * * The Initial Developer of the Original Code is * Ajax.org B.V. * Portions created by the Initial Developer are Copyright (C) 2010 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Fabian Jakobs * Julian Viereck * Mihai Sucan * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ define(function(require, exports, module) { var oop = require("pilot/oop"); var dom = require("pilot/dom"); var lang = require("pilot/lang"); var useragent = require("pilot/useragent"); var EventEmitter = require("pilot/event_emitter").EventEmitter; var Text = function(parentEl) { this.element = dom.createElement("div"); this.element.className = "ace_layer ace_text-layer"; this.element.style.width = "auto"; parentEl.appendChild(this.element); this.$characterSize = this.$measureSizes() || {width: 0, height: 0}; this.$pollSizeChanges(); }; (function() { oop.implement(this, EventEmitter); this.EOF_CHAR = "\xB6"; //"¶"; this.EOL_CHAR = "\xAC"; //"¬"; this.TAB_CHAR = "\u2192"; //"→"; this.SPACE_CHAR = "\xB7"; //"·"; this.$padding = 0; this.setPadding = function(padding) { this.$padding = padding; this.element.style.padding = "0 " + padding + "px"; }; this.getLineHeight = function() { return this.$characterSize.height || 1; }; this.getCharacterWidth = function() { return this.$characterSize.width || 1; }; this.checkForSizeChanges = function() { var size = this.$measureSizes(); if (size && (this.$characterSize.width !== size.width || this.$characterSize.height !== size.height)) { this.$characterSize = size; this._dispatchEvent("changeCharaterSize", {data: size}); } }; this.$pollSizeChanges = function() { var self = this; this.$pollSizeChangesTimer = setInterval(function() { self.checkForSizeChanges(); }, 500); }; this.$fontStyles = { fontFamily : 1, fontSize : 1, fontWeight : 1, fontStyle : 1, lineHeight : 1 }; this.$measureSizes = function() { var n = 1000; if (!this.$measureNode) { var measureNode = this.$measureNode = dom.createElement("div"); var style = measureNode.style; style.width = style.height = "auto"; style.left = style.top = (-n * 40) + "px"; style.visibility = "hidden"; style.position = "absolute"; style.overflow = "visible"; style.whiteSpace = "nowrap"; // in FF 3.6 monospace fonts can have a fixed sub pixel width. // that's why we have to measure many characters // Note: characterWidth can be a float! measureNode.innerHTML = lang.stringRepeat("Xy", n); if (document.body) { document.body.appendChild(measureNode); } else { var container = this.element.parentNode; while (!dom.hasCssClass(container, "ace_editor")) container = container.parentNode; container.appendChild(measureNode); } } var style = this.$measureNode.style; var computedStyle = dom.computedStyle(this.element); for (var prop in this.$fontStyles) style[prop] = computedStyle[prop]; var size = { height: this.$measureNode.offsetHeight, width: this.$measureNode.offsetWidth / (n * 2) }; // Size and width can be null if the editor is not visible or // detached from the document if (size.width == 0 && size.height == 0) return null; return size; }; this.setSession = function(session) { this.session = session; }; this.showInvisibles = false; this.setShowInvisibles = function(showInvisibles) { if (this.showInvisibles == showInvisibles) return false; this.showInvisibles = showInvisibles; return true; }; this.$tabStrings = []; this.$computeTabString = function() { var tabSize = this.session.getTabSize(); var tabStr = this.$tabStrings = [0]; for (var i = 1; i < tabSize + 1; i++) { if (this.showInvisibles) { tabStr.push("" + this.TAB_CHAR + new Array(i).join(" ") + ""); } else { tabStr.push(new Array(i+1).join(" ")); } } }; this.updateLines = function(config, firstRow, lastRow) { this.$computeTabString(); // Due to wrap line changes there can be new lines if e.g. // the line to updated wrapped in the meantime. if (this.config.lastRow != config.lastRow || this.config.firstRow != config.firstRow) { this.scrollLines(config); } this.config = config; var first = Math.max(firstRow, config.firstRow); var last = Math.min(lastRow, config.lastRow); var lineElements = this.element.childNodes; var lineElementsIdx = 0; for (var row = config.firstRow; row < first; row++) { var foldLine = this.session.getFoldLine(row); if (foldLine) { if (foldLine.containsRow(first)) { break; } else { row = foldLine.end.row; } } lineElementsIdx ++; } for (var i=first; i<=last; i++) { var lineElement = lineElements[lineElementsIdx++]; if (!lineElement) continue; var html = []; var tokens = this.session.getTokens(i, i); this.$renderLine(html, i, tokens[0].tokens, !this.$useLineGroups()); lineElement = dom.setInnerHtml(lineElement, html.join("")); i = this.session.getRowFoldEnd(i); } }; this.scrollLines = function(config) { this.$computeTabString(); var oldConfig = this.config; this.config = config; if (!oldConfig || oldConfig.lastRow < config.firstRow) return this.update(config); if (config.lastRow < oldConfig.firstRow) return this.update(config); var el = this.element; if (oldConfig.firstRow < config.firstRow) for (var row=this.session.getFoldedRowCount(oldConfig.firstRow, config.firstRow - 1); row>0; row--) el.removeChild(el.firstChild); if (oldConfig.lastRow > config.lastRow) for (var row=this.session.getFoldedRowCount(config.lastRow + 1, oldConfig.lastRow); row>0; row--) el.removeChild(el.lastChild); if (config.firstRow < oldConfig.firstRow) { var fragment = this.$renderLinesFragment(config, config.firstRow, oldConfig.firstRow - 1); if (el.firstChild) el.insertBefore(fragment, el.firstChild); else el.appendChild(fragment); } if (config.lastRow > oldConfig.lastRow) { var fragment = this.$renderLinesFragment(config, oldConfig.lastRow + 1, config.lastRow); el.appendChild(fragment); } }; this.$renderLinesFragment = function(config, firstRow, lastRow) { var fragment = document.createDocumentFragment(), row = firstRow, fold = this.session.getNextFold(row), foldStart = fold ?fold.start.row :Infinity; while (true) { if(row > foldStart) { row = fold.end.row+1; fold = this.session.getNextFold(row); foldStart = fold ?fold.start.row :Infinity; } if(row > lastRow) break; var container = dom.createElement("div"); var html = []; // Get the tokens per line as there might be some lines in between // beeing folded. // OPTIMIZE: If there is a long block of unfolded lines, just make // this call once for that big block of unfolded lines. var tokens = this.session.getTokens(row, row); if (tokens.length == 1) this.$renderLine(html, row, tokens[0].tokens, false); // don't use setInnerHtml since we are working with an empty DIV container.innerHTML = html.join(""); if (this.$useLineGroups()) { container.className = 'ace_line_group'; fragment.appendChild(container); } else { var lines = container.childNodes while(lines.length) fragment.appendChild(lines[0]); } row++; } return fragment; }; this.update = function(config) { this.$computeTabString(); this.config = config; var html = []; var firstRow = config.firstRow, lastRow = config.lastRow; var row = firstRow, fold = this.session.getNextFold(row), foldStart = fold ?fold.start.row :Infinity; while (true) { if(row > foldStart) { row = fold.end.row+1; fold = this.session.getNextFold(row); foldStart = fold ?fold.start.row :Infinity; } if(row > lastRow) break; if (this.$useLineGroups()) html.push("
") // Get the tokens per line as there might be some lines in between // beeing folded. // OPTIMIZE: If there is a long block of unfolded lines, just make // this call once for that big block of unfolded lines. var tokens = this.session.getTokens(row, row); if (tokens.length == 1) this.$renderLine(html, row, tokens[0].tokens, false); if (this.$useLineGroups()) html.push("
"); // end the line group row++; } this.element = dom.setInnerHtml(this.element, html.join("")); }; this.$textToken = { "text": true, "rparen": true, "lparen": true }; 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 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 == "\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 "" + 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); return "" + space + ""; } else { return " "; } } else { screenColumn += 1; return "" + c + ""; } }; 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); } return screenColumn + value.length; }; this.$renderLineCore = function(stringBuilder, lastRow, tokens, splits, onlyContents) { var chars = 0; var split = 0; var splitChars; var characterWidth = this.config.characterWidth; var screenColumn = 0; var self = this; if (!splits || splits.length == 0) splitChars = Number.MAX_VALUE; else splitChars = splits[0]; if (!onlyContents) { stringBuilder.push("
" ); } for (var i = 0; i < tokens.length; i++) { var token = tokens[i]; var value = token.value; if (chars + value.length < splitChars) { screenColumn = self.$renderToken( stringBuilder, screenColumn, token, value ); chars += value.length; } else { while (chars + value.length >= splitChars) { screenColumn = self.$renderToken( stringBuilder, screenColumn, token, value.substring(0, splitChars - chars) ); value = value.substring(splitChars - chars); chars = splitChars; if (!onlyContents) { stringBuilder.push("
", "
" ); } split ++; screenColumn = 0; splitChars = splits[split] || Number.MAX_VALUE; } if (value.length != 0) { chars += value.length; screenColumn = self.$renderToken( stringBuilder, screenColumn, token, value ); } } } if (this.showInvisibles) { if (lastRow !== this.session.getLength() - 1) stringBuilder.push("" + this.EOL_CHAR + ""); else stringBuilder.push("" + this.EOF_CHAR + ""); } if (!onlyContents) stringBuilder.push("
"); }; this.$renderLine = function(stringBuilder, row, tokens, onlyContents) { // Check if the line to render is folded or not. If not, things are // simple, otherwise, we need to fake some things... if (!this.session.isRowFolded(row)) { var splits = this.session.getRowSplitData(row); this.$renderLineCore(stringBuilder, row, tokens, splits, onlyContents); } else { this.$renderFoldLine(stringBuilder, row, tokens, onlyContents); } }; this.$renderFoldLine = function(stringBuilder, row, tokens, onlyContents) { var session = this.session, foldLine = session.getFoldLine(row), renderTokens = []; function addTokens(tokens, from, to) { var idx = 0, col = 0; while ((col + tokens[idx].value.length) < from) { col += tokens[idx].value.length; idx++; if (idx == tokens.length) { return; } } if (col != from) { var value = tokens[idx].value.substring(from - col); // Check if the token value is longer then the from...to spacing. if (value.length > (to - from)) { value = value.substring(0, to - from); } renderTokens.push({ type: tokens[idx].type, value: value }); col = from + value.length; idx += 1; } while (col < to) { var value = tokens[idx].value; if (value.length + col > to) { value = value.substring(0, to - col); } renderTokens.push({ type: tokens[idx].type, value: value }); col += value.length; idx += 1; } } foldLine.walk(function(placeholder, row, column, lastColumn, isNewRow) { if (placeholder) { renderTokens.push({ type: "fold", value: placeholder }); } else { if (isNewRow) { tokens = this.session.getTokens(row, row)[0].tokens; } if (tokens.length != 0) { addTokens(tokens, lastColumn, column); } } }.bind(this), foldLine.end.row, this.session.getLine(foldLine.end.row).length); // TODO: Build a fake splits array! var splits = this.session.$useWrapMode?this.session.$wrapData[row]:null; this.$renderLineCore(stringBuilder, row, renderTokens, splits, onlyContents); }; this.$useLineGroups = function() { // For the updateLines function to work correctly, it's important that the // child nodes of this.element correspond on a 1-to-1 basis to rows in the // document (as distinct from lines on the screen). For sessions that are // wrapped, this means we need to add a layer to the node hierarchy (tagged // with the class name ace_line_group). return this.session.getUseWrapMode(); }; this.destroy = function() { clearInterval(this.$pollSizeChangesTimer); if (this.$measureNode) this.$measureNode.parentNode.removeChild(this.$measureNode); delete this.$measureNode; }; }).call(Text.prototype); exports.Text = Text; });