text: /* ***** BEGIN LICENSE BLOCK *****
   * Distributed under the BSD license:
   *
   * Copyright (c) 2010, Ajax.org B.V.
   * All rights reserved.
   *
   * Redistribution and use in source and binary forms, with or without
   * modification, are permitted provided that the following conditions are met:
   *     * Redistributions of source code must retain the above copyright
   *       notice, this list of conditions and the following disclaimer.
   *     * Redistributions in binary form must reproduce the above copyright
   *       notice, this list of conditions and the following disclaimer in the
   *       documentation and/or other materials provided with the distribution.
   *     * Neither the name of Ajax.org B.V. nor the
   *       names of its contributors may be used to endorse or promote products
   *       derived from this software without specific prior written permission.
   *
   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
   * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
   * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
   * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   *
   * ***** END LICENSE BLOCK ***** */
  
  define(function(require, exports, module) {
  "use strict";
  
  var oop = require("./lib/oop");
  var dom = require("./lib/dom");
  var config = require("./config");
  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 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.css");
  
  /**
   * 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 || 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);
  
      // in IE <= 9 the native cursor always shines through
      this.$keepTextAreaAtCursor = !useragent.isOldIE;
  
      dom.addCssClass(this.container, "ace_editor");
  
      this.setTheme(theme);
  
      this.$gutter = dom.createElement("div");
      this.$gutter.className = "ace_gutter";
      this.container.appendChild(this.$gutter);
  
      this.scroller = dom.createElement("div");
      this.scroller.className = "ace_scroller";
      this.container.appendChild(this.scroller);
  
      this.content = dom.createElement("div");
      this.content.className = "ace_content";
      this.scroller.appendChild(this.content);
  
      this.$gutterLayer = new GutterLayer(this.$gutter);
      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;
  
      this.$markerFront = new MarkerLayer(this.content);
  
      this.$cursorLayer = new CursorLayer(this.content);
  
      // Indicates whether the horizontal scrollbar is visible
      this.$horizScroll = false;
      this.$vScroll = false;
  
      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;
  
      this.cursorPos = {
          row : 0,
          column : 0
      };
  
      this.$fontMetrics = new FontMetrics(this.container);
      this.$textLayer.$setFontMetrics(this.$fontMetrics);
      this.$textLayer.addEventListener("changeCharacterSize", function(e) {
          _self.updateCharacterSize();
          _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,
          $dirty: true
      };
  
      this.layerConfig = {
          width : 1,
          padding : 0,
          firstRow : 0,
          firstRowScreen: 0,
          lastRow : 0,
          lineHeight : 0,
          characterWidth : 0,
          minHeight : 1,
          maxHeight : 1,
          offset : 0,
          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.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_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;
          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.scrollBarV.scrollLeft = this.scrollBarV.scrollTop = null;
          
          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, force) {
          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;
          }
  
          // 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();
      };
  
      /**
       * Triggers a full update of the text, for all the rows.
       **/
      this.updateText = function() {
          this.$loop.schedule(this.CHANGE_TEXT);
      };
  
      /**
       * 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)
              this.$renderChanges(this.CHANGE_FULL, true);
          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) {
          if (this.resizing > 2)
              return;
          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 = 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;
  
              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";
  
              changes = changes | this.CHANGE_SCROLL;
          }
  
          if (width && (force || size.width != width)) {
              changes |= this.CHANGE_SIZE;
              size.width = width;
              
              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.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 && this.session.getUseWrapMode() && this.adjustWrapLimit() || force)
                  changes |= this.CHANGE_FULL;
          }
          
          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, 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.setOption("animatedScroll", shouldAnimate);
      };
  
      /**
       * Returns whether an animated scroll happens or not.
       * @returns {Boolean}
       **/
      this.getAnimatedScroll = function() {
          return this.$animatedScroll;
      };
  
      /**
       * Identifies whether you want to show invisible characters or not.
       * @param {Boolean} showInvisibles Set to `true` to show invisibles
       *
       **/
      this.setShowInvisibles = function(showInvisibles) {
          this.setOption("showInvisibles", showInvisibles);
      };
  
      /**
       * Returns whether invisible characters are being shown or not.
       * @returns {Boolean}
       **/
      this.getShowInvisibles = function() {
          return this.getOption("showInvisibles");
      };
      this.getDisplayIndentGuides = function() {
          return this.getOption("displayIndentGuides");
      };
  
      this.setDisplayIndentGuides = function(display) {
          this.setOption("displayIndentGuides", display);
      };
  
      /**
       * 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.setOption("showPrintMargin", showPrintMargin);
      };
  
      /**
       * Returns whether the print margin is being shown or not.
       * @returns {Boolean}
       **/
      this.getShowPrintMargin = function() {
          return this.getOption("showPrintMargin");
      };
      /**
       * 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.setOption("printMarginColumn", showPrintMargin);
      };
  
      /**
       * Returns whether the print margin column is being shown or not.
       * @returns {Boolean}
       **/
      this.getPrintMarginColumn = function() {
          return this.getOption("printMarginColumn");
      };
  
      /**
       * Returns `true` if the gutter is being shown.
       * @returns {Boolean}
       **/
      this.getShowGutter = function(){
          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){
          return this.setOption("showGutter", show);
      };
  
      this.getFadeFoldWidgets = function(){
          return this.getOption("fadeFoldWidgets")
      };
  
      this.setFadeFoldWidgets = function(show) {
          this.setOption("fadeFoldWidgets", show);
      };
  
      this.setHighlightGutterLine = function(shouldHighlight) {
          this.setOption("highlightGutterLine", shouldHighlight);
      };
  
      this.getHighlightGutterLine = function() {
          return this.getOption("highlightGutterLine");
      };
  
      this.$updateGutterLineHighlight = function() {
          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) {
              var containerEl = dom.createElement("div");
              containerEl.className = "ace_layer ace_print-margin-layer";
              this.$printMarginEl = dom.createElement("div");
              this.$printMarginEl.className = "ace_print-margin";
              containerEl.appendChild(this.$printMarginEl);
              this.content.insertBefore(containerEl, this.content.firstChild);
          }
  
          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.scroller;
      };
  
      /**
       *
       * Returns the element to which the hidden text area is added.
       * @returns {DOMElement}
       **/
      this.getTextAreaContainer = function() {
          return this.container;
      };
  
      // 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 -= config.offset;
  
          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) {
              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;
  
          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 config = this.layerConfig;
          var lastRow = config.lastRow
          var top = this.session.documentToScreenRow(lastRow, 0) * config.lineHeight;
          if (top - this.session.getScrollTop() > config.height - config.lineHeight)
              return lastRow - 1;
          return lastRow;
      };
  
      /**
       *
       * [Returns the index of the last visible row.]{: #VirtualRenderer.getLastVisibleRow}
       * @returns {Number}
       **/
      this.getLastVisibleRow = function() {
          return this.layerConfig.lastRow;
      };
  
      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}
       **/
      this.getHScrollBarAlwaysVisible = function() {
          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
       **/
      this.setHScrollBarAlwaysVisible = function(alwaysVisible) {
          this.setOption("hScrollBarAlwaysVisible", alwaysVisible);
      };
      /**
       * Returns whether the horizontal scrollbar is set to be always visible.
       * @returns {Boolean}
       **/
      this.getVScrollBarAlwaysVisible = function() {
          return this.$vScrollBarAlwaysVisible;
      };
  
      /**
       * 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.$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 ||
              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.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.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) {
              if (changes & this.CHANGE_TEXT || changes & this.CHANGE_LINES)
                  this.$textLayer.update(config);
              else
                  this.$textLayer.scrollLines(config);
  
              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(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(config);
          }
          else if (changes & this.CHANGE_TEXT || changes & this.CHANGE_GUTTER) {
              if (this.$showGutter)
                  this.$gutterLayer.update(config);
          }
  
          if (changes & this.CHANGE_CURSOR) {
              this.$cursorLayer.update(config);
              this.$moveTextAreaToCursor();
              this.$highlightGutterLine && this.$updateGutterLineHighlight();
          }
  
          if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_FRONT)) {
              this.$markerFront.update(config);
          }
  
          if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_BACK)) {
              this.$markerBack.update(config);
          }
  
          this._signal("afterRender");
      };
  
      
      this.$autosize = function() {
          var height = this.session.getScreenLength() * this.lineHeight;
          var maxHeight = this.$maxLines * this.lineHeight;
          var desiredHeight = Math.min(maxHeight, 
              Math.max((this.$minLines || 1) * this.lineHeight, height)
          ) + this.scrollMargin.v + (this.$extraHeight || 0);
          if (this.$horizScroll)
              desiredHeight += this.scrollBarH.getHeight();
          if (this.$maxPixelHeight && desiredHeight > this.$maxPixelHeight)
              desiredHeight = this.$maxPixelHeight;
          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() {
          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 longestLine = this.$getLongestLine();
          
          var horizScroll = !hideScrollbars && (this.$hScrollBarAlwaysVisible ||
              size.scrollerWidth - longestLine - 2 * this.$padding < 0);
  
          var hScrollChanged = this.$horizScroll !== horizScroll;
          if (hScrollChanged) {
              this.$horizScroll = horizScroll;
              this.scrollBarH.setVisible(horizScroll);
          }
          var vScrollBefore = this.$vScroll; // autosize can change vscroll value in which case we need to update longestLine
          // autoresize only after updating hscroll to include scrollbar height in desired height
          if (this.$maxLines && this.lineHeight > 1)
              this.$autosize();
  
          var offset = this.scrollTop % this.lineHeight;
          var minHeight = size.scrollerHeight + this.lineHeight;
          
          var scrollPastEnd = !this.$maxLines && this.$scrollPastEnd
              ? (size.scrollerHeight - this.lineHeight) * this.$scrollPastEnd
              : 0;
          maxHeight += scrollPastEnd;
          
          var sm = this.scrollMargin;
          this.session.setScrollTop(Math.max(-sm.top,
              Math.min(this.scrollTop, maxHeight - size.scrollerHeight + sm.bottom)));
  
          this.session.setScrollLeft(Math.max(-sm.left, Math.min(this.scrollLeft, 
              longestLine + 2 * this.$padding - size.scrollerWidth + sm.right)));
          
          var vScroll = !hideScrollbars && (this.$vScrollBarAlwaysVisible ||
              size.scrollerHeight - maxHeight + scrollPastEnd < 0 || this.scrollTop > sm.top);
          var vScrollChanged = vScrollBefore !== 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.
          var firstRowScreen, firstRowHeight;
          var lineHeight = this.lineHeight;
          firstRow = session.screenToDocumentRow(firstRow, 0);
  
          // Check if firstRow is inside of a foldLine. If true, then use the first
          // row of the foldLine.
          var foldLine = session.getFoldLine(firstRow);
          if (foldLine) {
              firstRow = foldLine.start.row;
          }
  
          firstRowScreen = session.documentToScreenRow(firstRow, 0);
          firstRowHeight = session.getRowLength(firstRow) * lineHeight;
  
          lastRow = Math.min(session.screenToDocumentRow(lastRow, 0), session.getLength() - 1);
          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,
              lastRow : lastRow,
              lineHeight : lineHeight,
              characterWidth : this.characterWidth,
              minHeight : minHeight,
              maxHeight : maxHeight,
              offset : offset,
              gutterOffset : lineHeight ? Math.max(0, Math.ceil((offset + size.height - size.scrollerHeight) / lineHeight)) : 0,
              height : this.$size.scrollerHeight
          };
  
          // For debugging.
          // console.log(JSON.stringify(this.layerConfig));
  
          return changes;
      };
  
      this.$updateLines = function() {
          var firstRow = this.$changedLines.firstRow;
          var lastRow = this.$changedLines.lastRow;
          this.$changedLines = null;
  
          var layerConfig = this.layerConfig;
  
          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)
                  this.$gutterLayer.update(layerConfig);
              this.$textLayer.update(layerConfig);
              return;
          }
  
          // else update only the changed rows
          this.$textLayer.updateLines(layerConfig, firstRow, lastRow);
          return true;
      };
  
      this.$getLongestLine = function() {
          var charCount = this.session.getScreenWidth();
          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();
      };
  
      this.scrollSelectionIntoView = function(anchor, lead, offset) {
          // first scroll anchor into view then scroll lead into view
          this.scrollCursorIntoView(anchor, offset);
          this.scrollCursorIntoView(lead, offset);
      };
  
      /**
       *
       * Scrolls the cursor into the first visibile area of the editor
       **/
      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;
          
          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 && scrollTop + topMargin > top + this.lineHeight)
                  top -= offset * this.$size.scrollerHeight;
              if (top === 0)
                  top = -this.scrollMargin.top;
              this.session.setScrollTop(top);
          } else if (scrollTop + this.$size.scrollerHeight - bottomMargin < top + this.lineHeight) {
              if (offset && scrollTop + this.$size.scrollerHeight - bottomMargin < top -  this.lineHeight)
                  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 = -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);
      };
  
      this.alignCursor = function(cursor, alignment) {
          if (typeof cursor == "number")
              cursor = {row: cursor, column: 0};
  
          var pos = this.$cursorLayer.getPixelPosition(cursor);
          var h = this.$size.scrollerHeight - this.lineHeight;
          var offset = pos.top - h * (alignment || 0);
  
          this.session.setScrollTop(offset);
          return offset;
      };
  
      this.STEPS = 8;
      this.$calcSteps = function(fromValue, toValue){
          var i = 0;
          var l = this.STEPS;
          var steps = [];
  
          var func  = function(t, x_min, dx) {
              return dx * (Math.pow(t - 1, 3) + 1) + x_min;
          };
  
          for (i = 0; i < l; ++i)
              steps.push(func(i / this.STEPS, fromValue, toValue - fromValue));
  
          return steps;
      };
  
      /**
       * Gracefully scrolls the editor to the row indicated.
       * @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)
              offset -= this.$size.scrollerHeight / 2;
  
          var initialScroll = this.scrollTop;
          this.session.setScrollTop(offset);
          if (animate !== false)
              this.animateScrolling(initialScroll, callback);
      };
  
      this.animateScrolling = function(fromValue, callback) {
          var toValue = this.scrollTop;
          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);
  
          _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
          if (this.scrollTop !== scrollTop) {
              this.$loop.schedule(this.CHANGE_SCROLL);
              this.scrollTop = scrollTop;
          }
      };
  
      /**
       * 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 (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);
      };
  
      /**
       * 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() >= 1 - this.scrollMargin.top)
             return true;
          if (deltaY > 0 && this.session.getScrollTop() + this.$size.scrollerHeight
              - this.layerConfig.maxHeight < -1 + this.scrollMargin.bottom)
             return true;
          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();
  
          var offset = (x + this.scrollLeft - canvasPos.left - this.$padding) / this.characterWidth;
          var row = Math.floor((y + this.scrollTop - canvasPos.top) / this.lineHeight);
          var col = Math.round(offset);
  
          return {row: row, column: col, side: offset - col > 0 ? 1 : -1};
      };
  
      this.screenToTextCoordinates = function(x, y) {
          var canvasPos = this.scroller.getBoundingClientRect();
  
          var col = Math.round(
              (x + this.scrollLeft - canvasPos.left - this.$padding) / this.characterWidth
          );
  
          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);
  
          var x = this.$padding + Math.round(pos.column * this.characterWidth);
          var y = pos.row * this.lineHeight;
  
          return {
              pageX: canvasPos.left + x - this.scrollLeft,
              pageY: canvasPos.top + y - this.scrollTop
          };
      };
  
      /**
       *
       * Focuses the current container.
       **/
      this.visualizeFocus = function() {
          dom.addCssClass(this.container, "ace_focus");
      };
  
      /**
       *
       * Blurs the current container.
       **/
      this.visualizeBlur = function() {
          dom.removeCssClass(this.container, "ace_focus");
      };
  
      /**
       * @param {Number} position
       *
       * @private
       **/
      this.showComposition = function(position) {
          if (!this.$composition)
              this.$composition = {
                  keepTextAreaAtCursor: this.$keepTextAreaAtCursor,
                  cssText: this.textarea.style.cssText
              };
  
          this.$keepTextAreaAtCursor = true;
          dom.addCssClass(this.textarea, "ace_composition");
          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`.
       **/
      this.setCompositionText = function(text) {
          this.$moveTextAreaToCursor();
      };
  
      /**
       *
       * 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;
      };
  
      /**
       * [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});
  
          if (!theme || typeof theme == "string") {
              var moduleName = theme || this.$options.theme.initialValue;
              config.loadModule(["theme", moduleName], afterLoad);
          } else {
              afterLoad(theme);
          }
  
          function afterLoad(module) {
              if (_self.$themeId != theme)
                  return cb && cb();
              if (!module || !module.cssClass)
                  throw new Error("couldn't load module " + theme + " or it didn't call define");
              dom.importCssString(
                  module.cssText,
                  module.cssClass,
                  _self.container.ownerDocument
              );
  
              if (_self.theme)
                  dom.removeCssClass(_self.container, _self.theme.cssClass);
  
              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.$updateSizeAsync();
              }
  
              _self._dispatchEvent('themeLoaded', {theme:module});
              cb && cb();
          }
      };
  
      /**
       * [Returns the path of the current theme.]{: #VirtualRenderer.getTheme}
       * @returns {String}
       **/
      this.getTheme = function() {
          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(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(style) {
          dom.removeCssClass(this.container, style);
      };
      
      this.setCursorStyle = function(style) {
          if (this.scroller.style.cursor != style)
              this.scroller.style.cursor = style;
      };
  
      /**
       * @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();
          }
      },
      maxPixelHeight: {
          set: function(val) {
              this.updateFull();
          },
          initialValue: 0
      },
      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;
  });
  