lib/gollum/public/gollum/livepreview/js/ace/lib/ace/virtual_renderer.js in gollum-3.1.2 vs lib/gollum/public/gollum/livepreview/js/ace/lib/ace/virtual_renderer.js in gollum-3.1.3

- old
+ new

@@ -1,22 +1,22 @@ /* ***** 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 @@ -31,56 +31,52 @@ define(function(require, exports, module) { "use strict"; var oop = require("./lib/oop"); var dom = require("./lib/dom"); -var event = require("./lib/event"); -var useragent = require("./lib/useragent"); var config = require("./config"); -var net = require("./lib/net"); +var useragent = require("./lib/useragent"); var GutterLayer = require("./layer/gutter").Gutter; var MarkerLayer = require("./layer/marker").Marker; var TextLayer = require("./layer/text").Text; var CursorLayer = require("./layer/cursor").Cursor; -var ScrollBar = require("./scrollbar").ScrollBar; +var HScrollBar = require("./scrollbar").HScrollBar; +var VScrollBar = require("./scrollbar").VScrollBar; var RenderLoop = require("./renderloop").RenderLoop; +var FontMetrics = require("./layer/font_metrics").FontMetrics; var EventEmitter = require("./lib/event_emitter").EventEmitter; var editorCss = require("./requirejs/text!./css/editor.css"); dom.importCssString(editorCss, "ace_editor"); /** - * - * * The class that is responsible for drawing everything you see on the screen! + * @related editor.renderer * @class VirtualRenderer **/ /** * Constructs a new `VirtualRenderer` within the `container` specified, applying the given `theme`. * @param {DOMElement} container The root element of the editor * @param {String} theme The starting theme * - * - * - * * @constructor **/ var VirtualRenderer = function(container, theme) { var _self = this; - this.container = container; + this.container = container || dom.createElement("div"); // TODO: this breaks rendering in Cloud9 with multiple ace instances -// // Imports CSS once per DOM document ('ace_editor' serves as an identifier). -// dom.importCssString(editorCss, "ace_editor", container.ownerDocument); + // // Imports CSS once per DOM document ('ace_editor' serves as an identifier). + // dom.importCssString(editorCss, "ace_editor", container.ownerDocument); // in IE <= 9 the native cursor always shines through - this.$keepTextAreaAtCursor = !useragent.isIE; + this.$keepTextAreaAtCursor = !useragent.isOldIE; - dom.addCssClass(container, "ace_editor"); + dom.addCssClass(this.container, "ace_editor"); this.setTheme(theme); this.$gutter = dom.createElement("div"); this.$gutter.className = "ace_gutter"; @@ -92,13 +88,12 @@ this.content = dom.createElement("div"); this.content.className = "ace_content"; this.scroller.appendChild(this.content); - this.setHighlightGutterLine(true); this.$gutterLayer = new GutterLayer(this.$gutter); - this.$gutterLayer.on("changeGutterWidth", this.onResize.bind(this, true)); + this.$gutterLayer.on("changeGutterWidth", this.onGutterResize.bind(this)); this.$markerBack = new MarkerLayer(this.content); var textLayer = this.$textLayer = new TextLayer(this.content); this.canvas = textLayer.element; @@ -107,72 +102,85 @@ this.$cursorLayer = new CursorLayer(this.content); // Indicates whether the horizontal scrollbar is visible this.$horizScroll = false; - this.$horizScrollAlwaysVisible = false; + this.$vScroll = false; - this.$animatedScroll = false; - - this.scrollBar = new ScrollBar(container); - this.scrollBar.addEventListener("scroll", function(e) { - if (!_self.$inScrollAnimation) - _self.session.setScrollTop(e.data); + this.scrollBar = + this.scrollBarV = new VScrollBar(this.container, this); + this.scrollBarH = new HScrollBar(this.container, this); + this.scrollBarV.addEventListener("scroll", function(e) { + if (!_self.$scrollAnimation) + _self.session.setScrollTop(e.data - _self.scrollMargin.top); }); + this.scrollBarH.addEventListener("scroll", function(e) { + if (!_self.$scrollAnimation) + _self.session.setScrollLeft(e.data - _self.scrollMargin.left); + }); this.scrollTop = 0; this.scrollLeft = 0; - event.addListener(this.scroller, "scroll", function() { - var scrollLeft = _self.scroller.scrollLeft; - _self.scrollLeft = scrollLeft; - _self.session.setScrollLeft(scrollLeft); - }); - this.cursorPos = { row : 0, column : 0 }; - this.$textLayer.addEventListener("changeCharacterSize", function() { + this.$fontMetrics = new FontMetrics(this.container, 500); + this.$textLayer.$setFontMetrics(this.$fontMetrics); + this.$textLayer.addEventListener("changeCharacterSize", function(e) { _self.updateCharacterSize(); - _self.onResize(true); + _self.onResize(true, _self.gutterWidth, _self.$size.width, _self.$size.height); + _self._signal("changeCharacterSize", e); }); this.$size = { width: 0, height: 0, scrollerHeight: 0, - scrollerWidth: 0 + scrollerWidth: 0, + $dirty: true }; this.layerConfig = { width : 1, padding : 0, firstRow : 0, firstRowScreen: 0, lastRow : 0, - lineHeight : 1, - characterWidth : 1, + lineHeight : 0, + characterWidth : 0, minHeight : 1, maxHeight : 1, offset : 0, - height : 1 + height : 1, + gutterOffset: 1 }; + + this.scrollMargin = { + left: 0, + right: 0, + top: 0, + bottom: 0, + v: 0, + h: 0 + }; this.$loop = new RenderLoop( this.$renderChanges.bind(this), this.container.ownerDocument.defaultView ); this.$loop.schedule(this.CHANGE_FULL); this.updateCharacterSize(); this.setPadding(4); + config.resetOptions(this); + config._emit("renderer", this); }; (function() { - this.showGutter = true; this.CHANGE_CURSOR = 1; this.CHANGE_MARKER = 2; this.CHANGE_GUTTER = 4; this.CHANGE_SCROLL = 8; @@ -182,49 +190,77 @@ this.CHANGE_MARKER_BACK = 128; this.CHANGE_MARKER_FRONT = 256; this.CHANGE_FULL = 512; this.CHANGE_H_SCROLL = 1024; + // this.$logChanges = function(changes) { + // var a = "" + // if (changes & this.CHANGE_CURSOR) a += " cursor"; + // if (changes & this.CHANGE_MARKER) a += " marker"; + // if (changes & this.CHANGE_GUTTER) a += " gutter"; + // if (changes & this.CHANGE_SCROLL) a += " scroll"; + // if (changes & this.CHANGE_LINES) a += " lines"; + // if (changes & this.CHANGE_TEXT) a += " text"; + // if (changes & this.CHANGE_SIZE) a += " size"; + // if (changes & this.CHANGE_MARKER_BACK) a += " marker_back"; + // if (changes & this.CHANGE_MARKER_FRONT) a += " marker_front"; + // if (changes & this.CHANGE_FULL) a += " full"; + // if (changes & this.CHANGE_H_SCROLL) a += " h_scroll"; + // console.log(a.trim()) + // }; + oop.implement(this, EventEmitter); - + this.updateCharacterSize = function() { if (this.$textLayer.allowBoldFonts != this.$allowBoldFonts) { this.$allowBoldFonts = this.$textLayer.allowBoldFonts; this.setStyle("ace_nobold", !this.$allowBoldFonts); } - + + this.layerConfig.characterWidth = this.characterWidth = this.$textLayer.getCharacterWidth(); + this.layerConfig.lineHeight = this.lineHeight = this.$textLayer.getLineHeight(); this.$updatePrintMargin(); }; /** - * + * * Associates the renderer with an [[EditSession `EditSession`]]. **/ this.setSession = function(session) { + if (this.session) + this.session.doc.off("changeNewLineMode", this.onChangeNewLineMode); + this.session = session; - - this.scroller.className = "ace_scroller"; - + if (session && this.scrollMargin.top && session.getScrollTop() <= 0) + session.setScrollTop(-this.scrollMargin.top); + this.$cursorLayer.setSession(session); this.$markerBack.setSession(session); this.$markerFront.setSession(session); this.$gutterLayer.setSession(session); this.$textLayer.setSession(session); + if (!session) + return; + this.$loop.schedule(this.CHANGE_FULL); + this.session.$setFontMetrics(this.$fontMetrics); + this.onChangeNewLineMode = this.onChangeNewLineMode.bind(this); + this.onChangeNewLineMode() + this.session.doc.on("changeNewLineMode", this.onChangeNewLineMode); }; /** * Triggers a partial update of the text, from the range given by the two parameters. * @param {Number} firstRow The first row to update * @param {Number} lastRow The last row to update * - * + * **/ - this.updateLines = function(firstRow, lastRow) { + this.updateLines = function(firstRow, lastRow, force) { if (lastRow === undefined) lastRow = Infinity; if (!this.$changedLines) { this.$changedLines = { @@ -238,13 +274,30 @@ if (this.$changedLines.lastRow < lastRow) this.$changedLines.lastRow = lastRow; } + // If the change happened offscreen above us then it's possible + // that a new line wrap will affect the position of the lines on our + // screen so they need redrawn. + // TODO: better solution is to not change scroll position when text is changed outside of visible area + if (this.$changedLines.lastRow < this.layerConfig.firstRow) { + if (force) + this.$changedLines.lastRow = this.layerConfig.lastRow; + else + return; + } + if (this.$changedLines.firstRow > this.layerConfig.lastRow) + return; this.$loop.schedule(this.CHANGE_LINES); }; + this.onChangeNewLineMode = function() { + this.$loop.schedule(this.CHANGE_TEXT); + this.$textLayer.$updateEolChar(); + }; + this.onChangeTabSize = function() { this.$loop.schedule(this.CHANGE_TEXT | this.CHANGE_MARKER); this.$textLayer.onChangeTabSize(); }; @@ -257,110 +310,167 @@ /** * Triggers a full update of all the layers, for all the rows. * @param {Boolean} force If `true`, forces the changes through * - * + * **/ this.updateFull = function(force) { - if (force){ + if (force) this.$renderChanges(this.CHANGE_FULL, true); - } - else { + else this.$loop.schedule(this.CHANGE_FULL); - } }; /** - * + * * Updates the font size. **/ this.updateFontSize = function() { this.$textLayer.checkForSizeChanges(); }; + this.$changes = 0; + this.$updateSizeAsync = function() { + if (this.$loop.pending) + this.$size.$dirty = true; + else + this.onResize(); + }; /** * [Triggers a resize of the editor.]{: #VirtualRenderer.onResize} * @param {Boolean} force If `true`, recomputes the size, even if the height and width haven't changed * @param {Number} gutterWidth The width of the gutter in pixels * @param {Number} width The width of the editor in pixels * @param {Number} height The hiehgt of the editor, in pixels * - * + * **/ this.onResize = function(force, gutterWidth, width, height) { - var changes = this.CHANGE_SIZE; - var size = this.$size; - if (this.resizing > 2) return; - else if (this.resizing > 1) + else if (this.resizing > 0) this.resizing++; else this.resizing = force ? 1 : 0; - + // `|| el.scrollHeight` is required for outosizing editors on ie + // where elements with clientHeight = 0 alsoe have clientWidth = 0 + var el = this.container; if (!height) - height = dom.getInnerHeight(this.container); - if (force || size.height != height) { + height = el.clientHeight || el.scrollHeight; + if (!width) + width = el.clientWidth || el.scrollWidth; + var changes = this.$updateCachedSize(force, gutterWidth, width, height); + + + if (!this.$size.scrollerHeight || (!width && !height)) + return this.resizing = 0; + + if (force) + this.$gutterLayer.$padding = null; + + if (force) + this.$renderChanges(changes | this.$changes, true); + else + this.$loop.schedule(changes | this.$changes); + + if (this.resizing) + this.resizing = 0; + // reset cached values on scrollbars, needs to be removed when switching to non-native scrollbars + // see https://github.com/ajaxorg/ace/issues/2195 + this.scrollBarV.scrollLeft = this.scrollBarV.scrollTop = null; + }; + + this.$updateCachedSize = function(force, gutterWidth, width, height) { + height -= (this.$extraHeight || 0); + var changes = 0; + var size = this.$size; + var oldSize = { + width: size.width, + height: size.height, + scrollerHeight: size.scrollerHeight, + scrollerWidth: size.scrollerWidth + }; + if (height && (force || size.height != height)) { size.height = height; + changes |= this.CHANGE_SIZE; - this.scroller.style.height = height + "px"; - size.scrollerHeight = this.scroller.clientHeight; - this.scrollBar.setHeight(size.scrollerHeight); + size.scrollerHeight = size.height; + if (this.$horizScroll) + size.scrollerHeight -= this.scrollBarH.getHeight(); + + // this.scrollBarV.setHeight(size.scrollerHeight); + this.scrollBarV.element.style.bottom = this.scrollBarH.getHeight() + "px"; - if (this.session) { - this.session.setScrollTop(this.getScrollTop()); - changes = changes | this.CHANGE_FULL; - } + changes = changes | this.CHANGE_SCROLL; } - if (!width) - width = dom.getInnerWidth(this.container); - if (force || this.resizing > 1 || size.width != width) { + if (width && (force || size.width != width)) { + changes |= this.CHANGE_SIZE; size.width = width; - - var gutterWidth = this.showGutter ? this.$gutter.offsetWidth : 0; + + if (gutterWidth == null) + gutterWidth = this.$showGutter ? this.$gutter.offsetWidth : 0; + + this.gutterWidth = gutterWidth; + + this.scrollBarH.element.style.left = this.scroller.style.left = gutterWidth + "px"; - size.scrollerWidth = Math.max(0, width - gutterWidth - this.scrollBar.getWidth()); - this.scroller.style.right = this.scrollBar.getWidth() + "px"; + size.scrollerWidth = Math.max(0, width - gutterWidth - this.scrollBarV.getWidth()); + + this.scrollBarH.element.style.right = + this.scroller.style.right = this.scrollBarV.getWidth() + "px"; + this.scroller.style.bottom = this.scrollBarH.getHeight() + "px"; + + // this.scrollBarH.element.style.setWidth(size.scrollerWidth); - if (this.session.getUseWrapMode() && this.adjustWrapLimit() || force) - changes = changes | this.CHANGE_FULL; + if (this.session && this.session.getUseWrapMode() && this.adjustWrapLimit() || force) + changes |= this.CHANGE_FULL; } - - if (force) - this.$renderChanges(changes, true); - else - this.$loop.schedule(changes); - if (force) - delete this.resizing; + size.$dirty = !width || !height; + + if (changes) + this._signal("resize", oldSize); + + return changes; }; + this.onGutterResize = function() { + var gutterWidth = this.$showGutter ? this.$gutter.offsetWidth : 0; + if (gutterWidth != this.gutterWidth) + this.$changes |= this.$updateCachedSize(true, gutterWidth, this.$size.width, this.$size.height); + + if (this.session.getUseWrapMode() && this.adjustWrapLimit()) { + this.$loop.schedule(this.CHANGE_FULL); + } else if (this.$size.$dirty) { + this.$loop.schedule(this.CHANGE_FULL); + } else { + this.$computeLayerConfig(); + this.$loop.schedule(this.CHANGE_MARKER); + } + }; + /** - * * Adjusts the wrap limit, which is the number of characters that can fit within the width of the edit area on screen. **/ this.adjustWrapLimit = function() { var availableWidth = this.$size.scrollerWidth - this.$padding * 2; var limit = Math.floor(availableWidth / this.characterWidth); - return this.session.adjustWrapLimit(limit); + return this.session.adjustWrapLimit(limit, this.$showPrintMargin && this.$printMarginColumn); }; /** * Identifies whether you want to have an animated scroll or not. * @param {Boolean} shouldAnimate Set to `true` to show animated scrolls * - * - * **/ this.setAnimatedScroll = function(shouldAnimate){ - this.$animatedScroll = shouldAnimate; + this.setOption("animatedScroll", shouldAnimate); }; /** - * * Returns whether an animated scroll happens or not. * @returns {Boolean} **/ this.getAnimatedScroll = function() { return this.$animatedScroll; @@ -368,143 +478,109 @@ /** * Identifies whether you want to show invisible characters or not. * @param {Boolean} showInvisibles Set to `true` to show invisibles * - * - * **/ this.setShowInvisibles = function(showInvisibles) { - if (this.$textLayer.setShowInvisibles(showInvisibles)) - this.$loop.schedule(this.CHANGE_TEXT); + this.setOption("showInvisibles", showInvisibles); }; /** - * * Returns whether invisible characters are being shown or not. * @returns {Boolean} **/ this.getShowInvisibles = function() { - return this.$textLayer.showInvisibles; + return this.getOption("showInvisibles"); }; - this.getDisplayIndentGuides = function() { - return this.$textLayer.displayIndentGuides; + return this.getOption("displayIndentGuides"); }; - + this.setDisplayIndentGuides = function(display) { - if (this.$textLayer.setDisplayIndentGuides(display)) - this.$loop.schedule(this.CHANGE_TEXT); + this.setOption("displayIndentGuides", display); }; - - this.$showPrintMargin = true; /** * Identifies whether you want to show the print margin or not. * @param {Boolean} showPrintMargin Set to `true` to show the print margin * - * - * **/ this.setShowPrintMargin = function(showPrintMargin) { - this.$showPrintMargin = showPrintMargin; - this.$updatePrintMargin(); + this.setOption("showPrintMargin", showPrintMargin); }; /** * Returns whether the print margin is being shown or not. * @returns {Boolean} **/ this.getShowPrintMargin = function() { - return this.$showPrintMargin; + return this.getOption("showPrintMargin"); }; - - this.$printMarginColumn = 80; - /** * Identifies whether you want to show the print margin column or not. * @param {Boolean} showPrintMargin Set to `true` to show the print margin column * - * - * **/ this.setPrintMarginColumn = function(showPrintMargin) { - this.$printMarginColumn = showPrintMargin; - this.$updatePrintMargin(); + this.setOption("printMarginColumn", showPrintMargin); }; /** - * * Returns whether the print margin column is being shown or not. * @returns {Boolean} **/ this.getPrintMarginColumn = function() { - return this.$printMarginColumn; + return this.getOption("printMarginColumn"); }; /** - * * Returns `true` if the gutter is being shown. * @returns {Boolean} **/ this.getShowGutter = function(){ - return this.showGutter; + return this.getOption("showGutter"); }; /** * Identifies whether you want to show the gutter or not. * @param {Boolean} show Set to `true` to show the gutter * - * **/ this.setShowGutter = function(show){ - if(this.showGutter === show) - return; - this.$gutter.style.display = show ? "block" : "none"; - this.showGutter = show; - this.onResize(true); + return this.setOption("showGutter", show); }; this.getFadeFoldWidgets = function(){ - return dom.hasCssClass(this.$gutter, "ace_fade-fold-widgets"); + return this.getOption("fadeFoldWidgets") }; this.setFadeFoldWidgets = function(show) { - if (show) - dom.addCssClass(this.$gutter, "ace_fade-fold-widgets"); - else - dom.removeCssClass(this.$gutter, "ace_fade-fold-widgets"); + this.setOption("fadeFoldWidgets", show); }; - this.$highlightGutterLine = false; this.setHighlightGutterLine = function(shouldHighlight) { - if (this.$highlightGutterLine == shouldHighlight) - return; - this.$highlightGutterLine = shouldHighlight; - - if (!this.$gutterLineHighlight) { - this.$gutterLineHighlight = dom.createElement("div"); - this.$gutterLineHighlight.className = "ace_gutter-active-line"; - this.$gutter.appendChild(this.$gutterLineHighlight); - return; - } - - this.$gutterLineHighlight.style.display = shouldHighlight ? "" : "none"; - // if cursorlayer have never been updated there's nothing on screen to update - if (this.$cursorLayer.$pixelPos) - this.$updateGutterLineHighlight(); + this.setOption("highlightGutterLine", shouldHighlight); }; this.getHighlightGutterLine = function() { - return this.$highlightGutterLine; + return this.getOption("highlightGutterLine"); }; this.$updateGutterLineHighlight = function() { - this.$gutterLineHighlight.style.top = this.$cursorLayer.$pixelPos.top - this.layerConfig.offset + "px"; - this.$gutterLineHighlight.style.height = this.layerConfig.lineHeight + "px"; + var pos = this.$cursorLayer.$pixelPos; + var height = this.layerConfig.lineHeight; + if (this.session.getUseWrapMode()) { + var cursor = this.session.selection.getCursor(); + cursor.column = 0; + pos = this.$cursorLayer.getPixelPosition(cursor, true); + height *= this.session.getRowLength(cursor.row); + } + this.$gutterLineHighlight.style.top = pos.top - this.layerConfig.offset + "px"; + this.$gutterLineHighlight.style.height = height + "px"; }; - + this.$updatePrintMargin = function() { if (!this.$showPrintMargin && !this.$printMarginEl) return; if (!this.$printMarginEl) { @@ -517,32 +593,35 @@ } var style = this.$printMarginEl.style; style.left = ((this.characterWidth * this.$printMarginColumn) + this.$padding) + "px"; style.visibility = this.$showPrintMargin ? "visible" : "hidden"; + + if (this.session && this.session.$wrap == -1) + this.adjustWrapLimit(); }; /** - * + * * Returns the root element containing this renderer. * @returns {DOMElement} **/ this.getContainerElement = function() { return this.container; }; /** - * + * * Returns the element that the mouse events are attached to * @returns {DOMElement} **/ this.getMouseEventTarget = function() { return this.content; }; /** - * + * * Returns the element to which the hidden text area is added. * @returns {DOMElement} **/ this.getTextAreaContainer = function() { return this.container; @@ -551,64 +630,69 @@ // move text input over the cursor // this is required for iOS and IME this.$moveTextAreaToCursor = function() { if (!this.$keepTextAreaAtCursor) return; - + var config = this.layerConfig; var posTop = this.$cursorLayer.$pixelPos.top; var posLeft = this.$cursorLayer.$pixelPos.left; - posTop -= this.layerConfig.offset; + posTop -= config.offset; - if (posTop < 0 || posTop > this.layerConfig.height - this.lineHeight) + var style = this.textarea.style; + var h = this.lineHeight; + if (posTop < 0 || posTop > config.height - h) { + style.top = style.left = "0"; return; + } var w = this.characterWidth; - if (this.$composition) - w += this.textarea.scrollWidth; + if (this.$composition) { + var val = this.textarea.value.replace(/^\x01+/, ""); + w *= (this.session.$getStringScreenWidth(val)[0]+2); + h += 2; + } posLeft -= this.scrollLeft; if (posLeft > this.$size.scrollerWidth - w) posLeft = this.$size.scrollerWidth - w; - if (this.showGutter) - posLeft += this.$gutterLayer.gutterWidth; - - this.textarea.style.height = this.lineHeight + "px"; - this.textarea.style.width = w + "px"; - this.textarea.style.left = posLeft + "px"; - this.textarea.style.top = posTop - 1 + "px"; + posLeft += this.gutterWidth; + style.height = h + "px"; + style.width = w + "px"; + style.left = Math.min(posLeft, this.$size.scrollerWidth - w) + "px"; + style.top = Math.min(posTop, this.$size.height - h) + "px"; }; /** - * + * * [Returns the index of the first visible row.]{: #VirtualRenderer.getFirstVisibleRow} * @returns {Number} **/ this.getFirstVisibleRow = function() { return this.layerConfig.firstRow; }; /** - * + * * Returns the index of the first fully visible row. "Fully" here means that the characters in the row are not truncated; that the top and the bottom of the row are on the screen. * @returns {Number} **/ this.getFirstFullyVisibleRow = function() { return this.layerConfig.firstRow + (this.layerConfig.offset === 0 ? 0 : 1); }; /** - * + * * Returns the index of the last fully visible row. "Fully" here means that the characters in the row are not truncated; that the top and the bottom of the row are on the screen. * @returns {Number} **/ this.getLastFullyVisibleRow = function() { var flint = Math.floor((this.layerConfig.height + this.layerConfig.offset) / this.layerConfig.lineHeight); return this.layerConfig.firstRow - 1 + flint; }; /** - * + * * [Returns the index of the last visible row.]{: #VirtualRenderer.getLastVisibleRow} * @returns {Number} **/ this.getLastVisibleRow = function() { return this.layerConfig.lastRow; @@ -617,164 +701,289 @@ this.$padding = null; /** * Sets the padding for all the layers. * @param {Number} padding A new padding value (in pixels) - * - * * + * + * **/ this.setPadding = function(padding) { this.$padding = padding; this.$textLayer.setPadding(padding); this.$cursorLayer.setPadding(padding); this.$markerFront.setPadding(padding); this.$markerBack.setPadding(padding); this.$loop.schedule(this.CHANGE_FULL); this.$updatePrintMargin(); }; + + this.setScrollMargin = function(top, bottom, left, right) { + var sm = this.scrollMargin; + sm.top = top|0; + sm.bottom = bottom|0; + sm.right = right|0; + sm.left = left|0; + sm.v = sm.top + sm.bottom; + sm.h = sm.left + sm.right; + if (sm.top && this.scrollTop <= 0 && this.session) + this.session.setScrollTop(-sm.top); + this.updateFull(); + }; /** - * - * Returns whether the horizontal scrollbar is set to be always visible. - * @returns {Boolean} - **/ + * Returns whether the horizontal scrollbar is set to be always visible. + * @returns {Boolean} + **/ this.getHScrollBarAlwaysVisible = function() { - return this.$horizScrollAlwaysVisible; + return this.$hScrollBarAlwaysVisible; }; /** - * Identifies whether you want to show the horizontal scrollbar or not. - * @param {Boolean} alwaysVisible Set to `true` to make the horizontal scroll bar visible - * - * - **/ + * Identifies whether you want to show the horizontal scrollbar or not. + * @param {Boolean} alwaysVisible Set to `true` to make the horizontal scroll bar visible + **/ this.setHScrollBarAlwaysVisible = function(alwaysVisible) { - if (this.$horizScrollAlwaysVisible != alwaysVisible) { - this.$horizScrollAlwaysVisible = alwaysVisible; - if (!this.$horizScrollAlwaysVisible || !this.$horizScroll) - this.$loop.schedule(this.CHANGE_SCROLL); - } + this.setOption("hScrollBarAlwaysVisible", alwaysVisible); }; + /** + * Returns whether the horizontal scrollbar is set to be always visible. + * @returns {Boolean} + **/ + this.getVScrollBarAlwaysVisible = function() { + return this.$vScrollBarAlwaysVisible; + }; - this.$updateScrollBar = function() { - this.scrollBar.setInnerHeight(this.layerConfig.maxHeight); - this.scrollBar.setScrollTop(this.scrollTop); + /** + * Identifies whether you want to show the horizontal scrollbar or not. + * @param {Boolean} alwaysVisible Set to `true` to make the horizontal scroll bar visible + **/ + this.setVScrollBarAlwaysVisible = function(alwaysVisible) { + this.setOption("vScrollBarAlwaysVisible", alwaysVisible); }; - this.$renderChanges = function(changes, force) { - if (!force && (!changes || !this.session || !this.container.offsetWidth)) - return; + this.$updateScrollBarV = function() { + var scrollHeight = this.layerConfig.maxHeight; + var scrollerHeight = this.$size.scrollerHeight; + if (!this.$maxLines && this.$scrollPastEnd) { + scrollHeight -= (scrollerHeight - this.lineHeight) * this.$scrollPastEnd; + if (this.scrollTop > scrollHeight - scrollerHeight) { + scrollHeight = this.scrollTop + scrollerHeight; + this.scrollBarV.scrollTop = null; + } + } + this.scrollBarV.setScrollHeight(scrollHeight + this.scrollMargin.v); + this.scrollBarV.setScrollTop(this.scrollTop + this.scrollMargin.top); + }; + this.$updateScrollBarH = function() { + this.scrollBarH.setScrollWidth(this.layerConfig.width + 2 * this.$padding + this.scrollMargin.h); + this.scrollBarH.setScrollLeft(this.scrollLeft + this.scrollMargin.left); + }; + + this.$frozen = false; + this.freeze = function() { + this.$frozen = true; + }; + + this.unfreeze = function() { + this.$frozen = false; + }; + this.$renderChanges = function(changes, force) { + if (this.$changes) { + changes |= this.$changes; + this.$changes = 0; + } + if ((!this.session || !this.container.offsetWidth || this.$frozen) || (!changes && !force)) { + this.$changes |= changes; + return; + } + if (this.$size.$dirty) { + this.$changes |= changes; + return this.onResize(true); + } + if (!this.lineHeight) { + this.$textLayer.checkForSizeChanges(); + } + // this.$logChanges(changes); + + this._signal("beforeRender"); + var config = this.layerConfig; // text, scrolling and resize changes can cause the view port size to change if (changes & this.CHANGE_FULL || changes & this.CHANGE_SIZE || changes & this.CHANGE_TEXT || changes & this.CHANGE_LINES || - changes & this.CHANGE_SCROLL - ) - this.$computeLayerConfig(); - + changes & this.CHANGE_SCROLL || + changes & this.CHANGE_H_SCROLL + ) { + changes |= this.$computeLayerConfig(); + // If a change is made offscreen and wrapMode is on, then the onscreen + // lines may have been pushed down. If so, the first screen row will not + // have changed, but the first actual row will. In that case, adjust + // scrollTop so that the cursor and onscreen content stays in the same place. + // TODO: find a better way to handle this, that works non wrapped case and doesn't compute layerConfig twice + if (config.firstRow != this.layerConfig.firstRow && config.firstRowScreen == this.layerConfig.firstRowScreen) { + var st = this.scrollTop + (config.firstRow - this.layerConfig.firstRow) * this.lineHeight; + if (st > 0) { + // this check is needed as a workaround for the documentToScreenRow returning -1 if document.length == 0 + this.scrollTop = st; + changes = changes | this.CHANGE_SCROLL; + changes |= this.$computeLayerConfig(); + } + } + config = this.layerConfig; + // update scrollbar first to not lose scroll position when gutter calls resize + this.$updateScrollBarV(); + if (changes & this.CHANGE_H_SCROLL) + this.$updateScrollBarH(); + this.$gutterLayer.element.style.marginTop = (-config.offset) + "px"; + this.content.style.marginTop = (-config.offset) + "px"; + this.content.style.width = config.width + 2 * this.$padding + "px"; + this.content.style.height = config.minHeight + "px"; + } + // horizontal scrolling if (changes & this.CHANGE_H_SCROLL) { - this.scroller.scrollLeft = this.scrollLeft; - - // read the value after writing it since the value might get clipped - var scrollLeft = this.scroller.scrollLeft; - this.scrollLeft = scrollLeft; - this.session.setScrollLeft(scrollLeft); - - this.scroller.className = this.scrollLeft == 0 ? "ace_scroller" : "ace_scroller ace_scroll-left"; + this.content.style.marginLeft = -this.scrollLeft + "px"; + this.scroller.className = this.scrollLeft <= 0 ? "ace_scroller" : "ace_scroller ace_scroll-left"; } // full if (changes & this.CHANGE_FULL) { - this.$textLayer.checkForSizeChanges(); - // update scrollbar first to not lose scroll position when gutter calls resize - this.$updateScrollBar(); - this.$textLayer.update(this.layerConfig); - if (this.showGutter) - this.$gutterLayer.update(this.layerConfig); - this.$markerBack.update(this.layerConfig); - this.$markerFront.update(this.layerConfig); - this.$cursorLayer.update(this.layerConfig); + this.$textLayer.update(config); + if (this.$showGutter) + this.$gutterLayer.update(config); + this.$markerBack.update(config); + this.$markerFront.update(config); + this.$cursorLayer.update(config); this.$moveTextAreaToCursor(); this.$highlightGutterLine && this.$updateGutterLineHighlight(); + this._signal("afterRender"); return; } // scrolling if (changes & this.CHANGE_SCROLL) { - this.$updateScrollBar(); if (changes & this.CHANGE_TEXT || changes & this.CHANGE_LINES) - this.$textLayer.update(this.layerConfig); + this.$textLayer.update(config); else - this.$textLayer.scrollLines(this.layerConfig); + this.$textLayer.scrollLines(config); - if (this.showGutter) - this.$gutterLayer.update(this.layerConfig); - this.$markerBack.update(this.layerConfig); - this.$markerFront.update(this.layerConfig); - this.$cursorLayer.update(this.layerConfig); - this.$moveTextAreaToCursor(); + if (this.$showGutter) + this.$gutterLayer.update(config); + this.$markerBack.update(config); + this.$markerFront.update(config); + this.$cursorLayer.update(config); this.$highlightGutterLine && this.$updateGutterLineHighlight(); + this.$moveTextAreaToCursor(); + this._signal("afterRender"); return; } if (changes & this.CHANGE_TEXT) { - this.$textLayer.update(this.layerConfig); - if (this.showGutter) - this.$gutterLayer.update(this.layerConfig); + this.$textLayer.update(config); + if (this.$showGutter) + this.$gutterLayer.update(config); } else if (changes & this.CHANGE_LINES) { - if (this.$updateLines() || (changes & this.CHANGE_GUTTER) && this.showGutter) - this.$gutterLayer.update(this.layerConfig); + if (this.$updateLines() || (changes & this.CHANGE_GUTTER) && this.$showGutter) + this.$gutterLayer.update(config); } else if (changes & this.CHANGE_TEXT || changes & this.CHANGE_GUTTER) { - if (this.showGutter) - this.$gutterLayer.update(this.layerConfig); + if (this.$showGutter) + this.$gutterLayer.update(config); } if (changes & this.CHANGE_CURSOR) { - this.$cursorLayer.update(this.layerConfig); + this.$cursorLayer.update(config); this.$moveTextAreaToCursor(); this.$highlightGutterLine && this.$updateGutterLineHighlight(); } if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_FRONT)) { - this.$markerFront.update(this.layerConfig); + this.$markerFront.update(config); } if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_BACK)) { - this.$markerBack.update(this.layerConfig); + this.$markerBack.update(config); } - if (changes & this.CHANGE_SIZE) - this.$updateScrollBar(); + this._signal("afterRender"); }; + + this.$autosize = function() { + var height = this.session.getScreenLength() * this.lineHeight; + var maxHeight = this.$maxLines * this.lineHeight; + var desiredHeight = Math.max( + (this.$minLines||1) * this.lineHeight, + Math.min(maxHeight, height) + ) + this.scrollMargin.v + (this.$extraHeight || 0); + var vScroll = height > maxHeight; + + if (desiredHeight != this.desiredHeight || + this.$size.height != this.desiredHeight || vScroll != this.$vScroll) { + if (vScroll != this.$vScroll) { + this.$vScroll = vScroll; + this.scrollBarV.setVisible(vScroll); + } + + var w = this.container.clientWidth; + this.container.style.height = desiredHeight + "px"; + this.$updateCachedSize(true, this.$gutterWidth, w, desiredHeight); + // this.$loop.changes = 0; + this.desiredHeight = desiredHeight; + + this._signal("autosize"); + } + }; + this.$computeLayerConfig = function() { + if (this.$maxLines && this.lineHeight > 1) + this.$autosize(); + var session = this.session; + var size = this.$size; + + var hideScrollbars = size.height <= 2 * this.lineHeight; + var screenLines = this.session.getScreenLength(); + var maxHeight = screenLines * this.lineHeight; var offset = this.scrollTop % this.lineHeight; - var minHeight = this.$size.scrollerHeight + this.lineHeight; + var minHeight = size.scrollerHeight + this.lineHeight; var longestLine = this.$getLongestLine(); + + var horizScroll = !hideScrollbars && (this.$hScrollBarAlwaysVisible || + size.scrollerWidth - longestLine - 2 * this.$padding < 0); - var horizScroll = this.$horizScrollAlwaysVisible || this.$size.scrollerWidth - longestLine < 0; - var horizScrollChanged = this.$horizScroll !== horizScroll; - this.$horizScroll = horizScroll; - if (horizScrollChanged) { - this.scroller.style.overflowX = horizScroll ? "scroll" : "hidden"; - // when we hide scrollbar scroll event isn't emited - // leaving session with wrong scrollLeft value - if (!horizScroll) - this.session.setScrollLeft(0); + var hScrollChanged = this.$horizScroll !== horizScroll; + if (hScrollChanged) { + this.$horizScroll = horizScroll; + this.scrollBarH.setVisible(horizScroll); } - var maxHeight = this.session.getScreenLength() * this.lineHeight; - this.session.setScrollTop(Math.max(0, Math.min(this.scrollTop, maxHeight - this.$size.scrollerHeight))); + + var scrollPastEnd = !this.$maxLines && this.$scrollPastEnd + ? (size.scrollerHeight - this.lineHeight) * this.$scrollPastEnd + : 0; + maxHeight += scrollPastEnd; + + this.session.setScrollTop(Math.max(-this.scrollMargin.top, + Math.min(this.scrollTop, maxHeight - size.scrollerHeight + this.scrollMargin.bottom))); + this.session.setScrollLeft(Math.max(-this.scrollMargin.left, Math.min(this.scrollLeft, + longestLine + 2 * this.$padding - size.scrollerWidth + this.scrollMargin.right))); + + var vScroll = !hideScrollbars && (this.$vScrollBarAlwaysVisible || + size.scrollerHeight - maxHeight + scrollPastEnd < 0 || this.scrollTop); + var vScrollChanged = this.$vScroll !== vScroll; + if (vScrollChanged) { + this.$vScroll = vScroll; + this.scrollBarV.setVisible(vScroll); + } + var lineCount = Math.ceil(minHeight / this.lineHeight) - 1; var firstRow = Math.max(0, Math.round((this.scrollTop - offset) / this.lineHeight)); var lastRow = firstRow + lineCount; // Map lines on the screen to lines in the document. @@ -791,15 +1000,27 @@ firstRowScreen = session.documentToScreenRow(firstRow, 0); firstRowHeight = session.getRowLength(firstRow) * lineHeight; lastRow = Math.min(session.screenToDocumentRow(lastRow, 0), session.getLength() - 1); - minHeight = this.$size.scrollerHeight + session.getRowLength(lastRow) * lineHeight + + minHeight = size.scrollerHeight + session.getRowLength(lastRow) * lineHeight + firstRowHeight; offset = this.scrollTop - firstRowScreen * lineHeight; + var changes = 0; + if (this.layerConfig.width != longestLine) + changes = this.CHANGE_H_SCROLL; + // Horizontal scrollbar visibility may have changed, which changes + // the client height of the scroller + if (hScrollChanged || vScrollChanged) { + changes = this.$updateCachedSize(true, this.gutterWidth, size.width, size.height); + this._signal("scrollbarVisibilityChanged"); + if (vScrollChanged) + longestLine = this.$getLongestLine(); + } + this.layerConfig = { width : longestLine, padding : this.$padding, firstRow : firstRow, firstRowScreen: firstRowScreen, @@ -807,25 +1028,18 @@ lineHeight : lineHeight, characterWidth : this.characterWidth, minHeight : minHeight, maxHeight : maxHeight, offset : offset, + gutterOffset : Math.max(0, Math.ceil((offset + size.height - size.scrollerHeight) / lineHeight)), height : this.$size.scrollerHeight }; // For debugging. // console.log(JSON.stringify(this.layerConfig)); - this.$gutterLayer.element.style.marginTop = (-offset) + "px"; - this.content.style.marginTop = (-offset) + "px"; - this.content.style.width = longestLine + 2 * this.$padding + "px"; - this.content.style.height = minHeight + "px"; - - // Horizontal scrollbar visibility may have changed, which changes - // the client height of the scroller - if (horizScrollChanged) - this.onResize(true); + return changes; }; this.$updateLines = function() { var firstRow = this.$changedLines.firstRow; var lastRow = this.$changedLines.lastRow; @@ -836,11 +1050,11 @@ if (firstRow > layerConfig.lastRow + 1) { return; } if (lastRow < layerConfig.firstRow) { return; } // if the last row is unknown -> redraw everything if (lastRow === Infinity) { - if (this.showGutter) + if (this.$showGutter) this.$gutterLayer.update(layerConfig); this.$textLayer.update(layerConfig); return; } @@ -849,89 +1063,89 @@ return true; }; this.$getLongestLine = function() { var charCount = this.session.getScreenWidth(); - if (this.$textLayer.showInvisibles) + if (this.showInvisibles && !this.session.$useWrapMode) charCount += 1; return Math.max(this.$size.scrollerWidth - 2 * this.$padding, Math.round(charCount * this.characterWidth)); }; /** - * + * * Schedules an update to all the front markers in the document. **/ this.updateFrontMarkers = function() { this.$markerFront.setMarkers(this.session.getMarkers(true)); this.$loop.schedule(this.CHANGE_MARKER_FRONT); }; /** - * + * * Schedules an update to all the back markers in the document. **/ this.updateBackMarkers = function() { this.$markerBack.setMarkers(this.session.getMarkers()); this.$loop.schedule(this.CHANGE_MARKER_BACK); }; - /** - * + /** + * * Deprecated; (moved to [[EditSession]]) * @deprecated **/ this.addGutterDecoration = function(row, className){ this.$gutterLayer.addGutterDecoration(row, className); }; - /** + /** * Deprecated; (moved to [[EditSession]]) * @deprecated **/ this.removeGutterDecoration = function(row, className){ this.$gutterLayer.removeGutterDecoration(row, className); }; /** - * + * * Redraw breakpoints. **/ this.updateBreakpoints = function(rows) { this.$loop.schedule(this.CHANGE_GUTTER); }; /** - * + * * Sets annotations for the gutter. * @param {Array} annotations An array containing annotations * - * + * **/ this.setAnnotations = function(annotations) { this.$gutterLayer.setAnnotations(annotations); this.$loop.schedule(this.CHANGE_GUTTER); }; /** - * + * * Updates the cursor icon. **/ this.updateCursor = function() { this.$loop.schedule(this.CHANGE_CURSOR); }; /** - * + * * Hides the cursor icon. **/ this.hideCursor = function() { this.$cursorLayer.hideCursor(); }; /** - * + * * Shows the cursor icon. **/ this.showCursor = function() { this.$cursorLayer.showCursor(); }; @@ -941,85 +1155,94 @@ this.scrollCursorIntoView(anchor, offset); this.scrollCursorIntoView(lead, offset); }; /** - * + * * Scrolls the cursor into the first visibile area of the editor **/ - this.scrollCursorIntoView = function(cursor, offset) { + this.scrollCursorIntoView = function(cursor, offset, $viewMargin) { // the editor is not visible if (this.$size.scrollerHeight === 0) return; var pos = this.$cursorLayer.getPixelPosition(cursor); var left = pos.left; var top = pos.top; - - if (this.scrollTop > top) { + + var topMargin = $viewMargin && $viewMargin.top || 0; + var bottomMargin = $viewMargin && $viewMargin.bottom || 0; + + var scrollTop = this.$scrollAnimation ? this.session.getScrollTop() : this.scrollTop; + + if (scrollTop + topMargin > top) { if (offset) top -= offset * this.$size.scrollerHeight; + if (top === 0) + top = -this.scrollMargin.top; this.session.setScrollTop(top); - } else if (this.scrollTop + this.$size.scrollerHeight < top + this.lineHeight) { + } else if (scrollTop + this.$size.scrollerHeight - bottomMargin < top + this.lineHeight) { if (offset) top += offset * this.$size.scrollerHeight; this.session.setScrollTop(top + this.lineHeight - this.$size.scrollerHeight); } var scrollLeft = this.scrollLeft; if (scrollLeft > left) { if (left < this.$padding + 2 * this.layerConfig.characterWidth) - left = 0; + left = -this.scrollMargin.left; this.session.setScrollLeft(left); } else if (scrollLeft + this.$size.scrollerWidth < left + this.characterWidth) { this.session.setScrollLeft(Math.round(left + this.characterWidth - this.$size.scrollerWidth)); + } else if (scrollLeft <= this.$padding && left - scrollLeft < this.characterWidth) { + this.session.setScrollLeft(0); } }; - /** + /** * {:EditSession.getScrollTop} * @related EditSession.getScrollTop * @returns {Number} **/ this.getScrollTop = function() { return this.session.getScrollTop(); }; - /** + /** * {:EditSession.getScrollLeft} * @related EditSession.getScrollLeft * @returns {Number} **/ this.getScrollLeft = function() { return this.session.getScrollLeft(); }; /** - * + * * Returns the first visible row, regardless of whether it's fully visible or not. * @returns {Number} **/ this.getScrollTopRow = function() { return this.scrollTop / this.lineHeight; }; /** - * + * * Returns the last visible row, regardless of whether it's fully visible or not. * @returns {Number} **/ this.getScrollBottomRow = function() { return Math.max(0, Math.floor((this.scrollTop + this.$size.scrollerHeight) / this.lineHeight) - 1); }; - /** + /** * Gracefully scrolls from the top of the editor to the row indicated. * @param {Number} row A row id * - * + * * @related EditSession.setScrollTop **/ this.scrollToRow = function(row) { this.session.setScrollTop(row * this.lineHeight); }; @@ -1057,11 +1280,11 @@ * @param {Number} line A line number * @param {Boolean} center If `true`, centers the editor the to indicated line * @param {Boolean} animate If `true` animates scrolling * @param {Function} callback Function to be called after the animation has finished * - * + * **/ this.scrollToLine = function(line, center, animate, callback) { var pos = this.$cursorLayer.getPixelPosition({row: line, column: 0}); var offset = pos.top; if (center) @@ -1073,42 +1296,56 @@ this.animateScrolling(initialScroll, callback); }; this.animateScrolling = function(fromValue, callback) { var toValue = this.scrollTop; - if (this.$animatedScroll && Math.abs(fromValue - toValue) < 100000) { - var _self = this; - var steps = _self.$calcSteps(fromValue, toValue); - this.$inScrollAnimation = true; + if (!this.$animatedScroll) + return; + var _self = this; + + if (fromValue == toValue) + return; + + if (this.$scrollAnimation) { + var oldSteps = this.$scrollAnimation.steps; + if (oldSteps.length) { + fromValue = oldSteps[0]; + if (fromValue == toValue) + return; + } + } + + var steps = _self.$calcSteps(fromValue, toValue); + this.$scrollAnimation = {from: fromValue, to: toValue, steps: steps}; - clearInterval(this.$timer); + clearInterval(this.$timer); - _self.session.setScrollTop(steps.shift()); - this.$timer = setInterval(function() { - if (steps.length) { - _self.session.setScrollTop(steps.shift()); - // trick session to think it's already scrolled to not loose toValue - _self.session.$scrollTop = toValue; - } else if (toValue != null) { - _self.session.$scrollTop = -1; - _self.session.setScrollTop(toValue); - toValue = null; - } else { - // do this on separate step to not get spurious scroll event from scrollbar - _self.$timer = clearInterval(_self.$timer); - _self.$inScrollAnimation = false; - callback && callback(); - } - }, 10); - } + _self.session.setScrollTop(steps.shift()); + // trick session to think it's already scrolled to not loose toValue + _self.session.$scrollTop = toValue; + this.$timer = setInterval(function() { + if (steps.length) { + _self.session.setScrollTop(steps.shift()); + _self.session.$scrollTop = toValue; + } else if (toValue != null) { + _self.session.$scrollTop = -1; + _self.session.setScrollTop(toValue); + toValue = null; + } else { + // do this on separate step to not get spurious scroll event from scrollbar + _self.$timer = clearInterval(_self.$timer); + _self.$scrollAnimation = null; + callback && callback(); + } + }, 10); }; /** * Scrolls the editor to the y pixel indicated. * @param {Number} scrollTop The position to scroll to * - * + * * @returns {Number} **/ this.scrollToY = function(scrollTop) { // after calling scrollBar.setScrollTop // scrollbar sends us event with same scrollTop. ignore it @@ -1120,28 +1357,33 @@ /** * Scrolls the editor across the x-axis to the pixel indicated. * @param {Number} scrollLeft The position to scroll to * - * + * * @returns {Number} **/ this.scrollToX = function(scrollLeft) { - if (scrollLeft < 0) - scrollLeft = 0; - if (this.scrollLeft !== scrollLeft) this.scrollLeft = scrollLeft; this.$loop.schedule(this.CHANGE_H_SCROLL); }; /** * Scrolls the editor across both x- and y-axes. + * @param {Number} x The x value to scroll to + * @param {Number} y The y value to scroll to + **/ + this.scrollTo = function(x, y) { + this.session.setScrollTop(y); + this.session.setScrollLeft(y); + }; + + /** + * Scrolls the editor across both x- and y-axes. * @param {Number} deltaX The x value to scroll by * @param {Number} deltaY The y value to scroll by - * - * **/ this.scrollBy = function(deltaX, deltaY) { deltaY && this.session.setScrollTop(this.session.getScrollTop() + deltaY); deltaX && this.session.setScrollLeft(this.session.getScrollLeft() + deltaX); }; @@ -1149,19 +1391,24 @@ /** * Returns `true` if you can still scroll by either parameter; in other words, you haven't reached the end of the file or line. * @param {Number} deltaX The x value to scroll by * @param {Number} deltaY The y value to scroll by * - * + * * @returns {Boolean} **/ this.isScrollableBy = function(deltaX, deltaY) { - if (deltaY < 0 && this.session.getScrollTop() > 0) + if (deltaY < 0 && this.session.getScrollTop() >= 1 - this.scrollMargin.top) return true; - if (deltaY > 0 && this.session.getScrollTop() + this.$size.scrollerHeight < this.layerConfig.maxHeight) + if (deltaY > 0 && this.session.getScrollTop() + this.$size.scrollerHeight + - this.layerConfig.maxHeight < -1 + this.scrollMargin.bottom) return true; - // todo: handle horizontal scrolling + if (deltaX < 0 && this.session.getScrollLeft() >= 1 - this.scrollMargin.left) + return true; + if (deltaX > 0 && this.session.getScrollLeft() + this.$size.scrollerWidth + - this.layerConfig.width < -1 + this.scrollMargin.right) + return true; }; this.pixelToScreenCoordinates = function(x, y) { var canvasPos = this.scroller.getBoundingClientRect(); @@ -1176,24 +1423,23 @@ var canvasPos = this.scroller.getBoundingClientRect(); var col = Math.round( (x + this.scrollLeft - canvasPos.left - this.$padding) / this.characterWidth ); - var row = Math.floor( - (y + this.scrollTop - canvasPos.top) / this.lineHeight - ); + var row = (y + this.scrollTop - canvasPos.top) / this.lineHeight; + return this.session.screenToDocumentPosition(row, Math.max(col, 0)); }; /** * Returns an object containing the `pageX` and `pageY` coordinates of the document position. * @param {Number} row The document row position * @param {Number} column The document column position * - * * + * * @returns {Object} **/ this.textToScreenCoordinates = function(row, column) { var canvasPos = this.scroller.getBoundingClientRect(); var pos = this.session.documentToScreenPosition(row, column); @@ -1206,30 +1452,30 @@ pageY: canvasPos.top + y - this.scrollTop }; }; /** - * - * Focuses the current container. - **/ + * + * Focuses the current container. + **/ this.visualizeFocus = function() { dom.addCssClass(this.container, "ace_focus"); }; /** - * - * Blurs the current container. - **/ + * + * Blurs the current container. + **/ this.visualizeBlur = function() { dom.removeCssClass(this.container, "ace_focus"); }; - /** - * @param {Number} position - * - * @private - **/ + /** + * @param {Number} position + * + * @private + **/ this.showComposition = function(position) { if (!this.$composition) this.$composition = { keepTextAreaAtCursor: this.$keepTextAreaAtCursor, cssText: this.textarea.style.cssText @@ -1240,140 +1486,279 @@ this.textarea.style.cssText = ""; this.$moveTextAreaToCursor(); }; /** - * @param {String} text A string of text to use - * - * Sets the inner text of the current composition to `text`. - **/ + * @param {String} text A string of text to use + * + * Sets the inner text of the current composition to `text`. + **/ this.setCompositionText = function(text) { this.$moveTextAreaToCursor(); }; /** - * - * Hides the current composition. - **/ + * + * Hides the current composition. + **/ this.hideComposition = function() { if (!this.$composition) return; dom.removeCssClass(this.textarea, "ace_composition"); this.$keepTextAreaAtCursor = this.$composition.keepTextAreaAtCursor; this.textarea.style.cssText = this.$composition.cssText; this.$composition = null; }; - this._loadTheme = function(name, callback) { - if (!config.get("packaged")) - return callback(); - - net.loadScript(config.moduleUrl(name, "theme"), callback); - }; - /** - * [Sets a new theme for the editor. `theme` should exist, and be a directory path, like `ace/theme/textmate`.]{: #VirtualRenderer.setTheme} - * @param {String} theme The path to a theme - * - * - **/ - this.setTheme = function(theme) { + * [Sets a new theme for the editor. `theme` should exist, and be a directory path, like `ace/theme/textmate`.]{: #VirtualRenderer.setTheme} + * @param {String} theme The path to a theme + * @param {Function} cb optional callback + * + **/ + this.setTheme = function(theme, cb) { var _self = this; + this.$themeId = theme; + _self._dispatchEvent('themeChange',{theme:theme}); - this.$themeValue = theme; if (!theme || typeof theme == "string") { - var moduleName = theme || "ace/theme/textmate"; - - var module; - try { - module = require(moduleName); - } catch (e) {}; - if (module) - return afterLoad(module); - - _self._loadTheme(moduleName, function() { - require([moduleName], function(module) { - if (_self.$themeValue !== theme) - return; - - afterLoad(module); - }); - }); + var moduleName = theme || this.$options.theme.initialValue; + config.loadModule(["theme", moduleName], afterLoad); } else { afterLoad(theme); } - function afterLoad(theme) { + function afterLoad(module) { + if (_self.$themeId != theme) + return cb && cb(); + if (!module.cssClass) + return; dom.importCssString( - theme.cssText, - theme.cssClass, + module.cssText, + module.cssClass, _self.container.ownerDocument ); if (_self.theme) dom.removeCssClass(_self.container, _self.theme.cssClass); - // this is kept only for backwards compatibility - _self.$theme = theme.cssClass; - - _self.theme = theme; - dom.addCssClass(_self.container, theme.cssClass); - dom.setCssClass(_self.container, "ace_dark", theme.isDark); - - var padding = theme.padding || 4; + var padding = "padding" in module ? module.padding + : "padding" in (_self.theme || {}) ? 4 : _self.$padding; if (_self.$padding && padding != _self.$padding) _self.setPadding(padding); + + // this is kept only for backwards compatibility + _self.$theme = module.cssClass; + _self.theme = module; + dom.addCssClass(_self.container, module.cssClass); + dom.setCssClass(_self.container, "ace_dark", module.isDark); + // force re-measure of the gutter width if (_self.$size) { _self.$size.width = 0; - _self.onResize(); + _self.$updateSizeAsync(); } + + _self._dispatchEvent('themeLoaded', {theme:module}); + cb && cb(); } }; /** - * [Returns the path of the current theme.]{: #VirtualRenderer.getTheme} - * @returns {String} - **/ + * [Returns the path of the current theme.]{: #VirtualRenderer.getTheme} + * @returns {String} + **/ this.getTheme = function() { - return this.$themeValue; + return this.$themeId; }; // Methods allows to add / remove CSS classnames to the editor element. // This feature can be used by plug-ins to provide a visual indication of // a certain mode that editor is in. /** - * [Adds a new class, `style`, to the editor.]{: #VirtualRenderer.setStyle} - * @param {String} style A class name - * - * - **/ - this.setStyle = function setStyle(style, include) { - dom.setCssClass(this.container, style, include != false); + * [Adds a new class, `style`, to the editor.]{: #VirtualRenderer.setStyle} + * @param {String} style A class name + * + **/ + this.setStyle = function(style, include) { + dom.setCssClass(this.container, style, include !== false); }; /** - * [Removes the class `style` from the editor.]{: #VirtualRenderer.unsetStyle} - * @param {String} style A class name - * - * - **/ - this.unsetStyle = function unsetStyle(style) { + * [Removes the class `style` from the editor.]{: #VirtualRenderer.unsetStyle} + * @param {String} style A class name + * + **/ + this.unsetStyle = function(style) { dom.removeCssClass(this.container, style); }; + + this.setCursorStyle = function(style) { + if (this.scroller.style.cursor != style) + this.scroller.style.cursor = style; + }; /** - * - * Destroys the text and cursor layers for this renderer. - **/ + * @param {String} cursorStyle A css cursor style + * + **/ + this.setMouseCursor = function(cursorStyle) { + this.scroller.style.cursor = cursorStyle; + }; + + /** + * Destroys the text and cursor layers for this renderer. + **/ this.destroy = function() { this.$textLayer.destroy(); this.$cursorLayer.destroy(); }; }).call(VirtualRenderer.prototype); + + +config.defineOptions(VirtualRenderer.prototype, "renderer", { + animatedScroll: {initialValue: false}, + showInvisibles: { + set: function(value) { + if (this.$textLayer.setShowInvisibles(value)) + this.$loop.schedule(this.CHANGE_TEXT); + }, + initialValue: false + }, + showPrintMargin: { + set: function() { this.$updatePrintMargin(); }, + initialValue: true + }, + printMarginColumn: { + set: function() { this.$updatePrintMargin(); }, + initialValue: 80 + }, + printMargin: { + set: function(val) { + if (typeof val == "number") + this.$printMarginColumn = val; + this.$showPrintMargin = !!val; + this.$updatePrintMargin(); + }, + get: function() { + return this.$showPrintMargin && this.$printMarginColumn; + } + }, + showGutter: { + set: function(show){ + this.$gutter.style.display = show ? "block" : "none"; + this.$loop.schedule(this.CHANGE_FULL); + this.onGutterResize(); + }, + initialValue: true + }, + fadeFoldWidgets: { + set: function(show) { + dom.setCssClass(this.$gutter, "ace_fade-fold-widgets", show); + }, + initialValue: false + }, + showFoldWidgets: { + set: function(show) {this.$gutterLayer.setShowFoldWidgets(show)}, + initialValue: true + }, + showLineNumbers: { + set: function(show) { + this.$gutterLayer.setShowLineNumbers(show); + this.$loop.schedule(this.CHANGE_GUTTER); + }, + initialValue: true + }, + displayIndentGuides: { + set: function(show) { + if (this.$textLayer.setDisplayIndentGuides(show)) + this.$loop.schedule(this.CHANGE_TEXT); + }, + initialValue: true + }, + highlightGutterLine: { + set: function(shouldHighlight) { + if (!this.$gutterLineHighlight) { + this.$gutterLineHighlight = dom.createElement("div"); + this.$gutterLineHighlight.className = "ace_gutter-active-line"; + this.$gutter.appendChild(this.$gutterLineHighlight); + return; + } + + this.$gutterLineHighlight.style.display = shouldHighlight ? "" : "none"; + // if cursorlayer have never been updated there's nothing on screen to update + if (this.$cursorLayer.$pixelPos) + this.$updateGutterLineHighlight(); + }, + initialValue: false, + value: true + }, + hScrollBarAlwaysVisible: { + set: function(val) { + if (!this.$hScrollBarAlwaysVisible || !this.$horizScroll) + this.$loop.schedule(this.CHANGE_SCROLL); + }, + initialValue: false + }, + vScrollBarAlwaysVisible: { + set: function(val) { + if (!this.$vScrollBarAlwaysVisible || !this.$vScroll) + this.$loop.schedule(this.CHANGE_SCROLL); + }, + initialValue: false + }, + fontSize: { + set: function(size) { + if (typeof size == "number") + size = size + "px"; + this.container.style.fontSize = size; + this.updateFontSize(); + }, + initialValue: 12 + }, + fontFamily: { + set: function(name) { + this.container.style.fontFamily = name; + this.updateFontSize(); + } + }, + maxLines: { + set: function(val) { + this.updateFull(); + } + }, + minLines: { + set: function(val) { + this.updateFull(); + } + }, + scrollPastEnd: { + set: function(val) { + val = +val || 0; + if (this.$scrollPastEnd == val) + return; + this.$scrollPastEnd = val; + this.$loop.schedule(this.CHANGE_SCROLL); + }, + initialValue: 0, + handlesSet: true + }, + fixedWidthGutter: { + set: function(val) { + this.$gutterLayer.$fixedWidth = !!val; + this.$loop.schedule(this.CHANGE_GUTTER); + } + }, + theme: { + set: function(val) { this.setTheme(val) }, + get: function() { return this.$themeId || this.theme; }, + initialValue: "./theme/textmate", + handlesSet: true + } +}); exports.VirtualRenderer = VirtualRenderer; });