mirror of
https://github.com/atom/atom.git
synced 2026-01-24 14:28:14 -05:00
566 lines
20 KiB
JavaScript
566 lines
20 KiB
JavaScript
/* 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 <fabian AT ajax DOT org>
|
|
* Julian Viereck <julian DOT viereck AT gmail DOT com>
|
|
* Mihai Sucan <mihai.sucan@gmail.com>
|
|
*
|
|
* 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 = "¶";
|
|
this.EOL_CHAR = "¬";
|
|
this.TAB_CHAR = "→";
|
|
this.SPACE_CHAR = "·";
|
|
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("<span class='ace_invisible'>"
|
|
+ this.TAB_CHAR
|
|
+ new Array(i).join(" ")
|
|
+ "</span>");
|
|
} 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("<div class='ace_line_group'>")
|
|
|
|
// 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("</div>"); // 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 "<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);
|
|
return "<span class='ace_invisible'>" + space + "</span>";
|
|
} else {
|
|
return " ";
|
|
}
|
|
} else {
|
|
screenColumn += 1;
|
|
return "<span class='ace_cjk' style='width:" +
|
|
(self.config.characterWidth * 2) +
|
|
"px'>" + c + "</span>";
|
|
}
|
|
};
|
|
|
|
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);
|
|
}
|
|
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("<div class='ace_line' style='height:",
|
|
this.config.lineHeight, "px",
|
|
"'>"
|
|
);
|
|
}
|
|
|
|
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("</div>",
|
|
"<div class='ace_line' style='height:",
|
|
this.config.lineHeight, "px",
|
|
"'>"
|
|
);
|
|
}
|
|
|
|
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("<span class='ace_invisible'>" + this.EOL_CHAR + "</span>");
|
|
else
|
|
stringBuilder.push("<span class='ace_invisible'>" + this.EOF_CHAR + "</span>");
|
|
}
|
|
if (!onlyContents)
|
|
stringBuilder.push("</div>");
|
|
};
|
|
|
|
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;
|
|
|
|
});
|