/* ***** BEGIN LICENSE BLOCK ***** * Distributed under the BSD license: * * Copyright (c) 2010, Ajax.org B.V. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Ajax.org B.V. nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * ***** END LICENSE BLOCK ***** */ define(function(require, exports, module) { "use strict"; var oop = require("../lib/oop"); var dom = require("../lib/dom"); var lang = require("../lib/lang"); var Lines = require("./lines").Lines; var EventEmitter = require("../lib/event_emitter").EventEmitter; var Text = function(parentEl) { this.dom = dom; this.element = this.dom.createElement("div"); this.element.className = "ace_layer ace_text-layer"; parentEl.appendChild(this.element); this.$updateEolChar = this.$updateEolChar.bind(this); this.$lines = new Lines(this.element); }; (function() { oop.implement(this, EventEmitter); this.EOF_CHAR = "\xB6"; this.EOL_CHAR_LF = "\xAC"; this.EOL_CHAR_CRLF = "\xa4"; this.EOL_CHAR = this.EOL_CHAR_LF; this.TAB_CHAR = "\u2014"; //"\u21E5"; this.SPACE_CHAR = "\xB7"; this.$padding = 0; this.MAX_LINE_LENGTH = 10000; this.$updateEolChar = function() { var doc = this.session.doc; var unixMode = doc.getNewLineCharacter() == "\n" && doc.getNewLineMode() != "windows"; var EOL_CHAR = unixMode ? this.EOL_CHAR_LF : this.EOL_CHAR_CRLF; if (this.EOL_CHAR != EOL_CHAR) { this.EOL_CHAR = EOL_CHAR; return true; } }; this.setPadding = function(padding) { this.$padding = padding; this.element.style.margin = "0 " + padding + "px"; }; this.getLineHeight = function() { return this.$fontMetrics.$characterSize.height || 0; }; this.getCharacterWidth = function() { return this.$fontMetrics.$characterSize.width || 0; }; this.$setFontMetrics = function(measure) { this.$fontMetrics = measure; this.$fontMetrics.on("changeCharacterSize", function(e) { this._signal("changeCharacterSize", e); }.bind(this)); this.$pollSizeChanges(); }; this.checkForSizeChanges = function() { this.$fontMetrics.checkForSizeChanges(); }; this.$pollSizeChanges = function() { return this.$pollSizeChangesTimer = this.$fontMetrics.$pollSizeChanges(); }; this.setSession = function(session) { this.session = session; if (session) this.$computeTabString(); }; this.showInvisibles = false; this.setShowInvisibles = function(showInvisibles) { if (this.showInvisibles == showInvisibles) return false; this.showInvisibles = showInvisibles; this.$computeTabString(); return true; }; this.displayIndentGuides = true; this.setDisplayIndentGuides = function(display) { if (this.displayIndentGuides == display) return false; this.displayIndentGuides = display; this.$computeTabString(); return true; }; this.$tabStrings = []; this.onChangeTabSize = this.$computeTabString = function() { var tabSize = this.session.getTabSize(); this.tabSize = tabSize; var tabStr = this.$tabStrings = [0]; for (var i = 1; i < tabSize + 1; i++) { if (this.showInvisibles) { var span = this.dom.createElement("span"); span.className = "ace_invisible ace_invisible_tab"; span.textContent = lang.stringRepeat(this.TAB_CHAR, i); tabStr.push(span); } else { tabStr.push(this.dom.createTextNode(lang.stringRepeat(" ", i), this.element)); } } if (this.displayIndentGuides) { this.$indentGuideRe = /\s\S| \t|\t |\s$/; var className = "ace_indent-guide"; var spaceClass = ""; var tabClass = ""; if (this.showInvisibles) { className += " ace_invisible"; spaceClass = " ace_invisible_space"; tabClass = " ace_invisible_tab"; var spaceContent = lang.stringRepeat(this.SPACE_CHAR, this.tabSize); var tabContent = lang.stringRepeat(this.TAB_CHAR, this.tabSize); } else { var spaceContent = lang.stringRepeat(" ", this.tabSize); var tabContent = spaceContent; } var span = this.dom.createElement("span"); span.className = className + spaceClass; span.textContent = spaceContent; this.$tabStrings[" "] = span; var span = this.dom.createElement("span"); span.className = className + tabClass; span.textContent = tabContent; this.$tabStrings["\t"] = span; } }; this.updateLines = function(config, firstRow, lastRow) { // 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) { return this.update(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)) { first = foldLine.start.row; break; } else { row = foldLine.end.row; } } lineElementsIdx ++; } var heightChanged = false; var row = first; var foldLine = this.session.getNextFoldLine(row); var foldStart = foldLine ? foldLine.start.row : Infinity; while (true) { if (row > foldStart) { row = foldLine.end.row+1; foldLine = this.session.getNextFoldLine(row, foldLine); foldStart = foldLine ? foldLine.start.row :Infinity; } if (row > last) break; var lineElement = lineElements[lineElementsIdx++]; if (lineElement) { this.dom.removeChildren(lineElement); this.$renderLine( lineElement, row, row == foldStart ? foldLine : false ); var height = (config.lineHeight * this.session.getRowLength(row)) + "px"; if (lineElement.style.height != height) { heightChanged = true; lineElement.style.height = height; } } row++; } if (heightChanged) { while (lineElementsIdx < this.$lines.cells.length) { var cell = this.$lines.cells[lineElementsIdx++]; cell.element.style.top = this.$lines.computeLineTop(cell.row, config, this.session) + "px"; } } }; this.scrollLines = function(config) { var oldConfig = this.config; this.config = config; if (this.$lines.pageChanged(oldConfig, config)) return this.update(config); this.$lines.moveContainer(config); var lastRow = config.lastRow; var oldLastRow = oldConfig ? oldConfig.lastRow : -1; if (!oldConfig || oldLastRow < config.firstRow) return this.update(config); if (lastRow < oldConfig.firstRow) return this.update(config); if (!oldConfig || oldConfig.lastRow < config.firstRow) return this.update(config); if (config.lastRow < oldConfig.firstRow) return this.update(config); if (oldConfig.firstRow < config.firstRow) for (var row=this.session.getFoldedRowCount(oldConfig.firstRow, config.firstRow - 1); row>0; row--) this.$lines.shift(); if (oldConfig.lastRow > config.lastRow) for (var row=this.session.getFoldedRowCount(config.lastRow + 1, oldConfig.lastRow); row>0; row--) this.$lines.pop(); if (config.firstRow < oldConfig.firstRow) { this.$lines.unshift(this.$renderLinesFragment(config, config.firstRow, oldConfig.firstRow - 1)); } if (config.lastRow > oldConfig.lastRow) { this.$lines.push(this.$renderLinesFragment(config, oldConfig.lastRow + 1, config.lastRow)); } }; this.$renderLinesFragment = function(config, firstRow, lastRow) { var fragment = []; var row = firstRow; var foldLine = this.session.getNextFoldLine(row); var foldStart = foldLine ? foldLine.start.row : Infinity; while (true) { if (row > foldStart) { row = foldLine.end.row+1; foldLine = this.session.getNextFoldLine(row, foldLine); foldStart = foldLine ? foldLine.start.row : Infinity; } if (row > lastRow) break; var line = this.$lines.createCell(row, config, this.session); var lineEl = line.element; this.dom.removeChildren(lineEl); dom.setStyle(lineEl.style, "height", this.$lines.computeLineHeight(row, config, this.session) + "px"); dom.setStyle(lineEl.style, "top", this.$lines.computeLineTop(row, config, this.session) + "px"); // Get the tokens per line as there might be some lines in between // beeing folded. this.$renderLine(lineEl, row, row == foldStart ? foldLine : false); if (this.$useLineGroups()) { lineEl.className = "ace_line_group"; } else { lineEl.className = "ace_line"; } fragment.push(line); row++; } return fragment; }; this.update = function(config) { this.$lines.moveContainer(config); this.config = config; var firstRow = config.firstRow; var lastRow = config.lastRow; var lines = this.$lines; while (lines.getLength()) lines.pop(); lines.push(this.$renderLinesFragment(config, firstRow, lastRow)); }; this.$textToken = { "text": true, "rparen": true, "lparen": true }; this.$renderToken = function(parent, screenColumn, token, value) { var self = this; var re = /(\t)|( +)|([\x00-\x1f\x80-\xa0\xad\u1680\u180E\u2000-\u200f\u2028\u2029\u202F\u205F\uFEFF\uFFF9-\uFFFC]+)|(\u3000)|([\u1100-\u115F\u11A3-\u11A7\u11FA-\u11FF\u2329-\u232A\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3001-\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]|[\uD800-\uDBFF][\uDC00-\uDFFF])/g; var valueFragment = this.dom.createFragment(this.element); var m; var i = 0; while (m = re.exec(value)) { var tab = m[1]; var simpleSpace = m[2]; var controlCharacter = m[3]; var cjkSpace = m[4]; var cjk = m[5]; if (!self.showInvisibles && simpleSpace) continue; var before = i != m.index ? value.slice(i, m.index) : ""; i = m.index + m[0].length; if (before) { valueFragment.appendChild(this.dom.createTextNode(before, this.element)); } if (tab) { var tabSize = self.session.getScreenTabSize(screenColumn + m.index); valueFragment.appendChild(self.$tabStrings[tabSize].cloneNode(true)); screenColumn += tabSize - 1; } else if (simpleSpace) { if (self.showInvisibles) { var span = this.dom.createElement("span"); span.className = "ace_invisible ace_invisible_space"; span.textContent = lang.stringRepeat(self.SPACE_CHAR, simpleSpace.length); valueFragment.appendChild(span); } else { valueFragment.appendChild(this.com.createTextNode(simpleSpace, this.element)); } } else if (controlCharacter) { var span = this.dom.createElement("span"); span.className = "ace_invisible ace_invisible_space ace_invalid"; span.textContent = lang.stringRepeat(self.SPACE_CHAR, controlCharacter.length); valueFragment.appendChild(span); } else if (cjkSpace) { // U+3000 is both invisible AND full-width, so must be handled uniquely var space = self.showInvisibles ? self.SPACE_CHAR : ""; screenColumn += 1; var span = this.dom.createElement("span"); span.style.width = (self.config.characterWidth * 2) + "px"; span.className = self.showInvisibles ? "ace_cjk ace_invisible ace_invisible_space" : "ace_cjk"; span.textContent = self.showInvisibles ? self.SPACE_CHAR : ""; valueFragment.appendChild(span); } else if (cjk) { screenColumn += 1; var span = this.dom.createElement("span"); span.style.width = (self.config.characterWidth * 2) + "px"; span.className = "ace_cjk"; span.textContent = cjk; valueFragment.appendChild(span); } } valueFragment.appendChild(this.dom.createTextNode(i ? value.slice(i) : value, this.element)); if (!this.$textToken[token.type]) { var classes = "ace_" + token.type.replace(/\./g, " ace_"); var span = this.dom.createElement("span"); if (token.type == "fold") span.style.width = (token.value.length * this.config.characterWidth) + "px"; span.className = classes; span.appendChild(valueFragment); parent.appendChild(span); } else { parent.appendChild(valueFragment); } return screenColumn + value.length; }; this.renderIndentGuide = function(parent, value, max) { var cols = value.search(this.$indentGuideRe); if (cols <= 0 || cols >= max) return value; if (value[0] == " ") { cols -= cols % this.tabSize; var count = cols/this.tabSize; for (var i=0; i= splitChars) { screenColumn = this.$renderToken( lineEl, screenColumn, token, value.substring(0, splitChars - chars) ); value = value.substring(splitChars - chars); chars = splitChars; lineEl = this.$createLineElement(); parent.appendChild(lineEl); lineEl.appendChild(this.dom.createTextNode(lang.stringRepeat("\xa0", splits.indent), this.element)); split ++; screenColumn = 0; splitChars = splits[split] || Number.MAX_VALUE; } if (value.length != 0) { chars += value.length; screenColumn = this.$renderToken( lineEl, screenColumn, token, value ); } } } }; this.$renderSimpleLine = function(parent, tokens) { var screenColumn = 0; var token = tokens[0]; var value = token.value; if (this.displayIndentGuides) value = this.renderIndentGuide(parent, value); if (value) screenColumn = this.$renderToken(parent, screenColumn, token, value); for (var i = 1; i < tokens.length; i++) { token = tokens[i]; value = token.value; if (screenColumn + value.length > this.MAX_LINE_LENGTH) return this.$renderOverflowMessage(parent, screenColumn, token, value); screenColumn = this.$renderToken(parent, screenColumn, token, value); } }; this.$renderOverflowMessage = function(parent, screenColumn, token, value) { this.$renderToken(parent, screenColumn, token, value.slice(0, this.MAX_LINE_LENGTH - screenColumn)); var overflowEl = this.dom.createElement("span"); overflowEl.className = "ace_inline_button ace_keyword ace_toggle_wrap"; overflowEl.style.position = "absolute"; overflowEl.style.right = "0"; overflowEl.textContent = ""; parent.appendChild(overflowEl); }; // row is either first row of foldline or not in fold this.$renderLine = function(parent, row, foldLine) { if (!foldLine && foldLine != false) foldLine = this.session.getFoldLine(row); if (foldLine) var tokens = this.$getFoldLineTokens(row, foldLine); else var tokens = this.session.getTokens(row); var lastLineEl = parent; if (tokens.length) { var splits = this.session.getRowSplitData(row); if (splits && splits.length) { this.$renderWrappedLine(parent, tokens, splits); var lastLineEl = parent.lastChild; } else { var lastLineEl = parent; if (this.$useLineGroups()) { lastLineEl = this.$createLineElement(); parent.appendChild(lastLineEl); } this.$renderSimpleLine(lastLineEl, tokens); } } else if (this.$useLineGroups()) { lastLineEl = this.$createLineElement(); parent.appendChild(lastLineEl); } if (this.showInvisibles && lastLineEl) { if (foldLine) row = foldLine.end.row; var invisibleEl = this.dom.createElement("span"); invisibleEl.className = "ace_invisible ace_invisible_eol"; invisibleEl.textContent = row == this.session.getLength() - 1 ? this.EOF_CHAR : this.EOL_CHAR; lastLineEl.appendChild(invisibleEl); } }; this.$getFoldLineTokens = function(row, foldLine) { var session = this.session; var 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 && idx < tokens.length) { var value = tokens[idx].value; if (value.length + col > to) { renderTokens.push({ type: tokens[idx].type, value: value.substring(0, to - col) }); } else renderTokens.push(tokens[idx]); col += value.length; idx += 1; } } var tokens = session.getTokens(row); foldLine.walk(function(placeholder, row, column, lastColumn, isNewRow) { if (placeholder != null) { renderTokens.push({ type: "fold", value: placeholder }); } else { if (isNewRow) tokens = session.getTokens(row); if (tokens.length) addTokens(tokens, lastColumn, column); } }, foldLine.end.row, this.session.getLine(foldLine.end.row).length); return renderTokens; }; 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() {}; }).call(Text.prototype); exports.Text = Text; });