640 lines
20 KiB
JavaScript
640 lines
20 KiB
JavaScript
/* ***** 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 Services 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>
|
|
*
|
|
* 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 event = require("pilot/event");
|
|
var GutterLayer = require("ace/layer/gutter").Gutter;
|
|
var MarkerLayer = require("ace/layer/marker").Marker;
|
|
var TextLayer = require("ace/layer/text").Text;
|
|
var CursorLayer = require("ace/layer/cursor").Cursor;
|
|
var ScrollBar = require("ace/scrollbar").ScrollBar;
|
|
var RenderLoop = require("ace/renderloop").RenderLoop;
|
|
var EventEmitter = require("pilot/event_emitter").EventEmitter;
|
|
var editorCss = require("text!ace/css/editor.css");
|
|
|
|
// import CSS once
|
|
dom.importCssString(editorCss);
|
|
|
|
var VirtualRenderer = function(container, theme) {
|
|
this.container = container;
|
|
dom.addCssClass(this.container, "ace_editor");
|
|
|
|
this.setTheme(theme);
|
|
|
|
this.$gutter = document.createElement("div");
|
|
this.$gutter.className = "ace_gutter";
|
|
this.container.appendChild(this.$gutter);
|
|
|
|
this.scroller = document.createElement("div");
|
|
this.scroller.className = "ace_scroller";
|
|
this.container.appendChild(this.scroller);
|
|
|
|
this.content = document.createElement("div");
|
|
this.content.style.position = "absolute";
|
|
this.scroller.appendChild(this.content);
|
|
|
|
this.$gutterLayer = new GutterLayer(this.$gutter);
|
|
this.$markerLayer = new MarkerLayer(this.content);
|
|
|
|
var textLayer = this.$textLayer = new TextLayer(this.content);
|
|
this.canvas = textLayer.element;
|
|
|
|
this.characterWidth = textLayer.getCharacterWidth();
|
|
this.lineHeight = textLayer.getLineHeight();
|
|
|
|
this.$cursorLayer = new CursorLayer(this.content);
|
|
|
|
this.layers = [ this.$markerLayer, textLayer, this.$cursorLayer ];
|
|
|
|
this.scrollBar = new ScrollBar(container);
|
|
this.scrollBar.addEventListener("scroll", this.onScroll.bind(this));
|
|
|
|
this.scrollTop = 0;
|
|
|
|
this.cursorPos = {
|
|
row : 0,
|
|
column : 0
|
|
};
|
|
|
|
var self = this;
|
|
this.$textLayer.addEventListener("changeCharaterSize", function() {
|
|
self.characterWidth = textLayer.getCharacterWidth();
|
|
self.lineHeight = textLayer.getLineHeight();
|
|
|
|
self.$loop.schedule(self.CHANGE_FULL);
|
|
});
|
|
event.addListener(this.$gutter, "click", this.$onGutterClick.bind(this));
|
|
event.addListener(this.$gutter, "dblclick", this.$onGutterClick.bind(this));
|
|
|
|
this.$size = {
|
|
width: 0,
|
|
height: 0,
|
|
scrollerHeight: 0,
|
|
scrollerWidth: 0
|
|
};
|
|
|
|
this.$loop = new RenderLoop(this.$renderChanges.bind(this));
|
|
this.$loop.schedule(this.CHANGE_FULL);
|
|
|
|
this.$updatePrintMargin();
|
|
this.setPadding(4);
|
|
};
|
|
|
|
(function() {
|
|
|
|
this.showGutter = true;
|
|
|
|
this.CHANGE_CURSOR = 1;
|
|
this.CHANGE_MARKER = 2;
|
|
this.CHANGE_GUTTER = 4;
|
|
this.CHANGE_SCROLL = 8;
|
|
this.CHANGE_LINES = 16;
|
|
this.CHANGE_TEXT = 32;
|
|
this.CHANGE_SIZE = 64;
|
|
this.CHANGE_FULL = 128;
|
|
|
|
oop.implement(this, EventEmitter);
|
|
|
|
this.setDocument = function(doc) {
|
|
this.lines = doc.lines;
|
|
this.doc = doc;
|
|
this.$cursorLayer.setDocument(doc);
|
|
this.$markerLayer.setDocument(doc);
|
|
this.$textLayer.setDocument(doc);
|
|
|
|
this.$loop.schedule(this.CHANGE_FULL);
|
|
};
|
|
|
|
/**
|
|
* Triggers partial update of the text layer
|
|
*/
|
|
this.updateLines = function(firstRow, lastRow) {
|
|
if (lastRow === undefined)
|
|
lastRow = Infinity;
|
|
|
|
if (!this.$changedLines) {
|
|
this.$changedLines = {
|
|
firstRow: firstRow,
|
|
lastRow: lastRow
|
|
};
|
|
}
|
|
else {
|
|
if (this.$changedLines.firstRow > firstRow)
|
|
this.$changedLines.firstRow = firstRow;
|
|
|
|
if (this.$changedLines.lastRow < lastRow)
|
|
this.$changedLines.lastRow = lastRow;
|
|
}
|
|
|
|
this.$loop.schedule(this.CHANGE_LINES);
|
|
};
|
|
|
|
/**
|
|
* Triggers full update of the text layer
|
|
*/
|
|
this.updateText = function() {
|
|
this.$loop.schedule(this.CHANGE_TEXT);
|
|
};
|
|
|
|
/**
|
|
* Triggers a full update of all layers
|
|
*/
|
|
this.updateFull = function() {
|
|
this.$loop.schedule(this.CHANGE_FULL);
|
|
};
|
|
|
|
/**
|
|
* Triggers resize of the editor
|
|
*/
|
|
this.onResize = function() {
|
|
var changes = this.CHANGE_SIZE;
|
|
|
|
var height = dom.getInnerHeight(this.container);
|
|
if (this.$size.height != height) {
|
|
this.$size.height = height;
|
|
|
|
this.scroller.style.height = height + "px";
|
|
this.scrollBar.setHeight(height);
|
|
|
|
if (this.doc) {
|
|
this.scrollToY(this.getScrollTop());
|
|
changes = changes | this.CHANGE_FULL;
|
|
}
|
|
}
|
|
|
|
var width = dom.getInnerWidth(this.container);
|
|
if (this.$size.width != width) {
|
|
this.$size.width = width;
|
|
|
|
var gutterWidth = this.showGutter ? this.$gutter.offsetWidth : 0;
|
|
this.scroller.style.left = gutterWidth + "px";
|
|
this.scroller.style.width = Math.max(0, width - gutterWidth - this.scrollBar.getWidth()) + "px";
|
|
}
|
|
|
|
this.$size.scrollerWidth = this.scroller.clientWidth;
|
|
this.$size.scrollerHeight = this.scroller.clientHeight;
|
|
this.$loop.schedule(changes);
|
|
};
|
|
|
|
this.setTokenizer = function(tokenizer) {
|
|
this.$tokenizer = tokenizer;
|
|
this.$textLayer.setTokenizer(tokenizer);
|
|
this.$loop.schedule(this.CHANGE_TEXT);
|
|
};
|
|
|
|
this.$onGutterClick = function(e) {
|
|
var pageX = event.getDocumentX(e);
|
|
var pageY = event.getDocumentY(e);
|
|
|
|
this._dispatchEvent("gutter" + e.type, {
|
|
row: this.screenToTextCoordinates(pageX, pageY).row,
|
|
htmlEvent: e
|
|
});
|
|
};
|
|
|
|
this.$showInvisibles = true;
|
|
this.setShowInvisibles = function(showInvisibles) {
|
|
this.$showInvisibles = showInvisibles;
|
|
this.$textLayer.setShowInvisibles(showInvisibles);
|
|
|
|
this.$loop.schedule(this.CHANGE_TEXT);
|
|
};
|
|
|
|
this.getShowInvisibles = function() {
|
|
return this.$showInvisibles;
|
|
};
|
|
|
|
this.$showPrintMargin = true;
|
|
this.setShowPrintMargin = function(showPrintMargin) {
|
|
this.$showPrintMargin = showPrintMargin;
|
|
this.$updatePrintMargin();
|
|
};
|
|
|
|
this.getShowPrintMargin = function() {
|
|
return this.$showPrintMargin;
|
|
};
|
|
|
|
this.$printMarginColumn = 80;
|
|
this.setPrintMarginColumn = function(showPrintMargin) {
|
|
this.$printMarginColumn = showPrintMargin;
|
|
this.$updatePrintMargin();
|
|
};
|
|
|
|
this.getPrintMarginColumn = function() {
|
|
return this.$printMarginColumn;
|
|
};
|
|
|
|
this.setShowGutter = function(show){
|
|
this.$gutter.style.display = show ? "block" : "none";
|
|
this.showGutter = show;
|
|
this.onResize();
|
|
}
|
|
|
|
this.$updatePrintMargin = function() {
|
|
if (!this.$showPrintMargin && !this.$printMarginEl)
|
|
return;
|
|
|
|
if (!this.$printMarginEl) {
|
|
this.$printMarginEl = document.createElement("div");
|
|
this.$printMarginEl.className = "ace_printMargin";
|
|
this.content.insertBefore(this.$printMarginEl, this.$textLayer.element);
|
|
}
|
|
|
|
var style = this.$printMarginEl.style;
|
|
style.left = (this.characterWidth * this.$printMarginColumn) + "px";
|
|
style.visibility = this.$showPrintMargin ? "visible" : "hidden";
|
|
};
|
|
|
|
this.getContainerElement = function() {
|
|
return this.container;
|
|
};
|
|
|
|
this.getMouseEventTarget = function() {
|
|
return this.content;
|
|
};
|
|
|
|
this.getFirstVisibleRow = function() {
|
|
return (this.layerConfig || {}).firstRow || 0;
|
|
};
|
|
|
|
this.getFirstFullyVisibleRow = function(){
|
|
if (!this.layerConfig)
|
|
return 0;
|
|
|
|
return this.layerConfig.firstRow + (this.layerConfig.offset == 0 ? 0 : 1);
|
|
}
|
|
|
|
this.getLastFullyVisibleRow = function() {
|
|
if (!this.layerConfig)
|
|
return 0;
|
|
|
|
var flint = Math.floor((this.layerConfig.height + this.layerConfig.offset) / this.layerConfig.lineHeight);
|
|
return this.layerConfig.firstRow - 1 + flint;
|
|
}
|
|
|
|
this.getLastVisibleRow = function() {
|
|
return (this.layerConfig || {}).lastRow || 0;
|
|
};
|
|
|
|
this.$padding = null;
|
|
this.setPadding = function(padding) {
|
|
this.$padding = padding;
|
|
this.content.style.padding = "0 " + padding + "px";
|
|
this.$loop.schedule(this.CHANGE_FULL);
|
|
};
|
|
|
|
this.onScroll = function(e) {
|
|
this.scrollToY(e.data);
|
|
};
|
|
|
|
this.$updateScrollBar = function() {
|
|
this.scrollBar.setInnerHeight(this.doc.getLength() * this.lineHeight);
|
|
this.scrollBar.setScrollTop(this.scrollTop);
|
|
};
|
|
|
|
this.$renderChanges = function(changes) {
|
|
if (!changes || !this.doc || !this.$tokenizer)
|
|
return;
|
|
|
|
// text, scrolling and resize changes can cause the view port size to change
|
|
if (!this.layerConfig ||
|
|
changes & this.CHANGE_FULL ||
|
|
changes & this.CHANGE_SIZE ||
|
|
changes & this.CHANGE_TEXT ||
|
|
changes & this.CHANGE_LINES ||
|
|
changes & this.CHANGE_SCROLL
|
|
)
|
|
this.$computeLayerConfig();
|
|
|
|
// full
|
|
if (changes & this.CHANGE_FULL) {
|
|
this.$textLayer.update(this.layerConfig);
|
|
this.showGutter && this.$gutterLayer.update(this.layerConfig);
|
|
this.$markerLayer.update(this.layerConfig);
|
|
this.$cursorLayer.update(this.layerConfig);
|
|
this.$updateScrollBar();
|
|
return;
|
|
}
|
|
|
|
// scrolling
|
|
if (changes & this.CHANGE_SCROLL) {
|
|
if (changes & this.CHANGE_TEXT || changes & this.CHANGE_LINES)
|
|
this.$textLayer.update(this.layerConfig);
|
|
else
|
|
this.$textLayer.scrollLines(this.layerConfig);
|
|
this.showGutter && this.$gutterLayer.update(this.layerConfig);
|
|
this.$markerLayer.update(this.layerConfig);
|
|
this.$cursorLayer.update(this.layerConfig);
|
|
this.$updateScrollBar();
|
|
return;
|
|
}
|
|
|
|
if (changes & this.CHANGE_TEXT) {
|
|
this.$textLayer.update(this.layerConfig);
|
|
this.showGutter && this.$gutterLayer.update(this.layerConfig);
|
|
}
|
|
else if (changes & this.CHANGE_LINES) {
|
|
this.$updateLines();
|
|
this.$updateScrollBar();
|
|
this.showGutter && this.$gutterLayer.update(this.layerConfig);
|
|
} else if (changes & this.CHANGE_GUTTER) {
|
|
this.showGutter && this.$gutterLayer.update(this.layerConfig);
|
|
}
|
|
|
|
if (changes & this.CHANGE_CURSOR)
|
|
this.$cursorLayer.update(this.layerConfig);
|
|
|
|
if (changes & this.CHANGE_MARKER) {
|
|
this.$markerLayer.update(this.layerConfig);
|
|
}
|
|
|
|
if (changes & this.CHANGE_SIZE)
|
|
this.$updateScrollBar();
|
|
};
|
|
|
|
this.$computeLayerConfig = function() {
|
|
var offset = this.scrollTop % this.lineHeight;
|
|
var minHeight = this.$size.scrollerHeight + this.lineHeight;
|
|
|
|
var longestLine = this.$getLongestLine();
|
|
var widthChanged = !this.layerConfig ? true : (this.layerConfig.width != longestLine);
|
|
|
|
var lineCount = Math.ceil(minHeight / this.lineHeight);
|
|
var firstRow = Math.max(0, Math.round((this.scrollTop - offset) / this.lineHeight));
|
|
var lastRow = Math.max(0, Math.min(this.lines.length, firstRow + lineCount) - 1);
|
|
|
|
var layerConfig = this.layerConfig = {
|
|
width : longestLine,
|
|
padding : this.$padding,
|
|
firstRow : firstRow,
|
|
lastRow : lastRow,
|
|
lineHeight : this.lineHeight,
|
|
characterWidth : this.characterWidth,
|
|
minHeight : minHeight,
|
|
offset : offset,
|
|
height : this.$size.scrollerHeight
|
|
};
|
|
|
|
for ( var i = 0; i < this.layers.length; i++) {
|
|
var layer = this.layers[i];
|
|
if (widthChanged) {
|
|
var style = layer.element.style;
|
|
style.width = longestLine + "px";
|
|
}
|
|
};
|
|
|
|
this.$gutterLayer.element.style.marginTop = (-offset) + "px";
|
|
this.content.style.marginTop = (-offset) + "px";
|
|
this.content.style.width = longestLine + "px";
|
|
this.content.style.height = minHeight + "px";
|
|
};
|
|
|
|
this.$updateLines = function() {
|
|
var firstRow = this.$changedLines.firstRow;
|
|
var lastRow = this.$changedLines.lastRow;
|
|
this.$changedLines = null;
|
|
|
|
var layerConfig = this.layerConfig;
|
|
|
|
// if the update changes the width of the document do a full redraw
|
|
if (layerConfig.width != this.$getLongestLine())
|
|
return this.$textLayer.update(layerConfig);
|
|
|
|
if (firstRow > layerConfig.lastRow + 1) { return; }
|
|
if (lastRow < layerConfig.firstRow) { return; }
|
|
|
|
// if the last row is unknown -> redraw everything
|
|
if (lastRow === Infinity) {
|
|
this.showGutter && this.$gutterLayer.update(layerConfig);
|
|
this.$textLayer.update(layerConfig);
|
|
return;
|
|
}
|
|
|
|
// else update only the changed rows
|
|
this.$textLayer.updateLines(layerConfig, firstRow, lastRow);
|
|
};
|
|
|
|
this.$getLongestLine = function() {
|
|
var charCount = this.doc.getScreenWidth();
|
|
if (this.$showInvisibles)
|
|
charCount += 1;
|
|
|
|
return Math.max(this.$size.scrollerWidth - this.$padding * 2, Math.round(charCount * this.characterWidth));
|
|
};
|
|
|
|
this.addMarker = function(range, clazz, type) {
|
|
var id = this.$markerLayer.addMarker(range, clazz, type);
|
|
this.$loop.schedule(this.CHANGE_MARKER);
|
|
return id;
|
|
};
|
|
|
|
this.removeMarker = function(markerId) {
|
|
this.$markerLayer.removeMarker(markerId);
|
|
this.$loop.schedule(this.CHANGE_MARKER);
|
|
};
|
|
|
|
this.addGutterDecoration = function(row, className){
|
|
this.$gutterLayer.addGutterDecoration(row, className);
|
|
this.$loop.schedule(this.CHANGE_GUTTER);
|
|
}
|
|
|
|
this.removeGutterDecoration = function(row, className){
|
|
this.$gutterLayer.removeGutterDecoration(row, className);
|
|
this.$loop.schedule(this.CHANGE_GUTTER);
|
|
}
|
|
|
|
this.setBreakpoints = function(rows) {
|
|
this.$gutterLayer.setBreakpoints(rows);
|
|
this.$loop.schedule(this.CHANGE_GUTTER);
|
|
};
|
|
|
|
this.updateCursor = function(position, overwrite) {
|
|
this.$cursorLayer.setCursor(position, overwrite);
|
|
this.$loop.schedule(this.CHANGE_CURSOR);
|
|
};
|
|
|
|
this.hideCursor = function() {
|
|
this.$cursorLayer.hideCursor();
|
|
};
|
|
|
|
this.showCursor = function() {
|
|
this.$cursorLayer.showCursor();
|
|
};
|
|
|
|
this.scrollCursorIntoView = function() {
|
|
var pos = this.$cursorLayer.getPixelPosition();
|
|
|
|
var left = pos.left + this.$padding;
|
|
var top = pos.top;
|
|
|
|
if (this.getScrollTop() > top) {
|
|
this.scrollToY(top);
|
|
}
|
|
|
|
if (this.getScrollTop() + this.$size.scrollerHeight < top
|
|
+ this.lineHeight) {
|
|
this.scrollToY(top + this.lineHeight - this.$size.scrollerHeight);
|
|
}
|
|
|
|
if (this.scroller.scrollLeft > left) {
|
|
this.scrollToX(left);
|
|
}
|
|
|
|
if (this.scroller.scrollLeft + this.$size.scrollerWidth < left
|
|
+ this.characterWidth) {
|
|
this.scrollToX(Math.round(left + this.characterWidth
|
|
- this.$size.scrollerWidth));
|
|
}
|
|
},
|
|
|
|
this.getScrollTop = function() {
|
|
return this.scrollTop;
|
|
};
|
|
|
|
this.getScrollLeft = function() {
|
|
return this.scroller.scrollLeft;
|
|
};
|
|
|
|
this.getScrollTopRow = function() {
|
|
return this.scrollTop / this.lineHeight;
|
|
};
|
|
|
|
this.scrollToRow = function(row) {
|
|
this.scrollToY(row * this.lineHeight);
|
|
};
|
|
|
|
this.scrollToY = function(scrollTop) {
|
|
var maxHeight = this.lines.length * this.lineHeight - this.$size.scrollerHeight;
|
|
var scrollTop = Math.max(0, Math.min(maxHeight, scrollTop));
|
|
|
|
if (this.scrollTop !== scrollTop) {
|
|
this.scrollTop = scrollTop;
|
|
this.$loop.schedule(this.CHANGE_SCROLL);
|
|
}
|
|
};
|
|
|
|
this.scrollToX = function(scrollLeft) {
|
|
if (scrollLeft <= this.$padding)
|
|
scrollLeft = 0;
|
|
|
|
this.scroller.scrollLeft = scrollLeft;
|
|
};
|
|
|
|
this.scrollBy = function(deltaX, deltaY) {
|
|
deltaY && this.scrollToY(this.scrollTop + deltaY);
|
|
deltaX && this.scrollToX(this.scroller.scrollLeft + deltaX);
|
|
};
|
|
|
|
this.screenToTextCoordinates = function(pageX, pageY) {
|
|
var canvasPos = this.scroller.getBoundingClientRect();
|
|
|
|
var col = Math.round((pageX + this.scroller.scrollLeft - canvasPos.left - this.$padding)
|
|
/ this.characterWidth);
|
|
var row = Math.floor((pageY + this.scrollTop - canvasPos.top)
|
|
/ this.lineHeight);
|
|
|
|
return {
|
|
row : row,
|
|
column : this.doc.screenToDocumentColumn(Math.max(0, Math.min(row, this.doc.getLength()-1)), col)
|
|
};
|
|
};
|
|
|
|
this.textToScreenCoordinates = function(row, column) {
|
|
var canvasPos = this.scroller.getBoundingClientRect();
|
|
|
|
var x = this.padding + Math.round(this.doc.documentToScreenColumn(row, column) * this.characterWidth);
|
|
var y = row * this.lineHeight;
|
|
|
|
return {
|
|
pageX: canvasPos.left + x - this.getScrollLeft(),
|
|
pageY: canvasPos.top + y - this.getScrollTop()
|
|
}
|
|
};
|
|
|
|
this.visualizeFocus = function() {
|
|
dom.addCssClass(this.container, "ace_focus");
|
|
};
|
|
|
|
this.visualizeBlur = function() {
|
|
dom.removeCssClass(this.container, "ace_focus");
|
|
};
|
|
|
|
this.showComposition = function(position) {
|
|
};
|
|
|
|
this.setCompositionText = function(text) {
|
|
};
|
|
|
|
this.hideComposition = function() {
|
|
};
|
|
|
|
this.setTheme = function(theme) {
|
|
var _self = this;
|
|
if (!theme || typeof theme == "string") {
|
|
theme = theme || "ace/theme/textmate";
|
|
require([theme], function(theme) {
|
|
afterLoad(theme);
|
|
});
|
|
} else {
|
|
afterLoad(theme);
|
|
}
|
|
|
|
var _self = this;
|
|
function afterLoad(theme) {
|
|
if (_self.$theme)
|
|
dom.removeCssClass(_self.container, _self.$theme);
|
|
|
|
_self.$theme = theme ? theme.cssClass : null;
|
|
|
|
if (_self.$theme)
|
|
dom.addCssClass(_self.container, _self.$theme);
|
|
|
|
// force re-measure of the gutter width
|
|
if (_self.$size) {
|
|
_self.$size.width = 0;
|
|
_self.onResize();
|
|
}
|
|
}
|
|
};
|
|
|
|
}).call(VirtualRenderer.prototype);
|
|
|
|
exports.VirtualRenderer = VirtualRenderer;
|
|
});
|