text: /* vim:ts=4:sts=4:sw=4:
   * ***** BEGIN LICENSE BLOCK *****
   * Version: MPL 1.1/GPL 2.0/LGPL 2.1
   *
   * The contents of this file are subject to the Mozilla Public License Version
   * 1.1 (the "License"); you may not use this file except in compliance with
   * the License. You may obtain a copy of the License at
   * http://www.mozilla.org/MPL/
   *
   * Software distributed under the License is distributed on an "AS IS" basis,
   * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
   * for the specific language governing rights and limitations under the
   * License.
   *
   * The Original Code is Ajax.org Code Editor (ACE).
   *
   * The Initial Developer of the Original Code is
   * Ajax.org B.V.
   * Portions created by the Initial Developer are Copyright (C) 2010
   * the Initial Developer. All Rights Reserved.
   *
   * Contributor(s):
   *      Fabian Jakobs <fabian@ajax.org>
   *      Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)
   *      Julian Viereck <julian.viereck@gmail.com>
   *
   * Alternatively, the contents of this file may be used under the terms of
   * either the GNU General Public License Version 2 or later (the "GPL"), or
   * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
   * in which case the provisions of the GPL or the LGPL are applicable instead
   * of those above. If you wish to allow use of your version of this file only
   * under the terms of either the GPL or the LGPL, and not to allow others to
   * use your version of this file under the terms of the MPL, indicate your
   * decision by deleting the provisions above and replace them with the notice
   * and other provisions required by the GPL or the LGPL. If you do not delete
   * the provisions above, a recipient may use your version of this file under
   * the terms of any one of the MPL, the GPL or the LGPL.
   *
   * ***** END LICENSE BLOCK ***** */
  
  define(function(require, exports, module) {
  "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 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 RenderLoop = require("./renderloop").RenderLoop;
  var EventEmitter = require("./lib/event_emitter").EventEmitter;
  var editorCss = require("ace/requirejs/text!./css/editor.css");
  
  dom.importCssString(editorCss, "ace_editor");
  
  var VirtualRenderer = function(container, theme) {
      var _self = this;
  
      this.container = container;
  
      // 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);
  
      dom.addCssClass(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.onResize.bind(this, true));
  
      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.characterWidth = textLayer.getCharacterWidth();
      this.lineHeight = textLayer.getLineHeight();
  
      this.$cursorLayer = new CursorLayer(this.content);
      this.$cursorPadding = 8;
  
      // Indicates whether the horizontal scrollbar is visible
      this.$horizScroll = true;
      this.$horizScrollAlwaysVisible = true;
  
      this.$animatedScroll = false;
  
      this.scrollBar = new ScrollBar(container);
      this.scrollBar.addEventListener("scroll", function(e) {
          _self.session.setScrollTop(e.data);
      });
  
      this.scrollTop = 0;
      this.scrollLeft = 0;
  
      event.addListener(this.scroller, "scroll", function() {
          var scrollLeft = _self.scroller.scrollLeft;
          _self.scrollLeft = scrollLeft;
          _self.session.setScrollLeft(scrollLeft);
  
          if (scrollLeft == 0) {
              _self.$gutter.className = "ace_gutter";
          }
          else {
              _self.$gutter.className = "ace_gutter horscroll";
          }
      });
  
      this.cursorPos = {
          row : 0,
          column : 0
      };
  
      this.$textLayer.addEventListener("changeCharacterSize", function() {
          _self.characterWidth = textLayer.getCharacterWidth();
          _self.lineHeight = textLayer.getLineHeight();
          _self.$updatePrintMargin();
          _self.onResize(true);
  
          _self.$loop.schedule(_self.CHANGE_FULL);
      });
  
      this.$size = {
          width: 0,
          height: 0,
          scrollerHeight: 0,
          scrollerWidth: 0
      };
  
      this.layerConfig = {
          width : 1,
          padding : 0,
          firstRow : 0,
          firstRowScreen: 0,
          lastRow : 0,
          lineHeight : 1,
          characterWidth : 1,
          minHeight : 1,
          maxHeight : 1,
          offset : 0,
          height : 1
      };
  
      this.$loop = new RenderLoop(
          this.$renderChanges.bind(this),
          this.container.ownerDocument.defaultView
      );
      this.$loop.schedule(this.CHANGE_FULL);
  
      this.setPadding(4);
      this.$updatePrintMargin();
  };
  
  (function() {
      this.showGutter = true;
  
      this.CHANGE_CURSOR = 1;
      this.CHANGE_MARKER = 2;
      this.CHANGE_GUTTER = 4;
      this.CHANGE_SCROLL = 8;
      this.CHANGE_LINES = 16;
      this.CHANGE_TEXT = 32;
      this.CHANGE_SIZE = 64;
      this.CHANGE_MARKER_BACK = 128;
      this.CHANGE_MARKER_FRONT = 256;
      this.CHANGE_FULL = 512;
      this.CHANGE_H_SCROLL = 1024;
  
      oop.implement(this, EventEmitter);
  
      this.setSession = function(session) {
          this.session = session;
          this.$cursorLayer.setSession(session);
          this.$markerBack.setSession(session);
          this.$markerFront.setSession(session);
          this.$gutterLayer.setSession(session);
          this.$textLayer.setSession(session);
          this.$loop.schedule(this.CHANGE_FULL);
      };
  
      /**
       * Triggers partial update of the text layer
       */
      this.updateLines = function(firstRow, lastRow) {
          if (lastRow === undefined)
              lastRow = Infinity;
  
          if (!this.$changedLines) {
              this.$changedLines = {
                  firstRow: firstRow,
                  lastRow: lastRow
              };
          }
          else {
              if (this.$changedLines.firstRow > firstRow)
                  this.$changedLines.firstRow = firstRow;
  
              if (this.$changedLines.lastRow < lastRow)
                  this.$changedLines.lastRow = lastRow;
          }
  
          this.$loop.schedule(this.CHANGE_LINES);
      };
  
      /**
       * Triggers full update of the text layer
       */
      this.updateText = function() {
          this.$loop.schedule(this.CHANGE_TEXT);
      };
  
      /**
       * Triggers a full update of all layers
       */
      this.updateFull = function() {
          this.$loop.schedule(this.CHANGE_FULL);
      };
  
      this.updateFontSize = function() {
          this.$textLayer.checkForSizeChanges();
      };
  
      /**
       * Triggers resize of the editor
       */
      this.onResize = function(force) {
          var changes = this.CHANGE_SIZE;
          var size = this.$size;
  
          var height = dom.getInnerHeight(this.container);
          if (force || size.height != height) {
              size.height = height;
  
              this.scroller.style.height = height + "px";
              size.scrollerHeight = this.scroller.clientHeight;
              this.scrollBar.setHeight(size.scrollerHeight);
  
              if (this.session) {
                  this.session.setScrollTop(this.getScrollTop());
                  changes = changes | this.CHANGE_FULL;
              }
          }
  
          var width = dom.getInnerWidth(this.container);
          if (force || size.width != width) {
              size.width = width;
  
              var gutterWidth = this.showGutter ? this.$gutter.offsetWidth : 0;
              this.scroller.style.left = gutterWidth + "px";
              size.scrollerWidth = Math.max(0, width - gutterWidth - this.scrollBar.getWidth());
              this.scroller.style.width = size.scrollerWidth + "px";
  
              if (this.session.getUseWrapMode() && this.adjustWrapLimit() || force)
                  changes = changes | this.CHANGE_FULL;
          }
  
          this.$loop.schedule(changes);
      };
  
      this.adjustWrapLimit = function() {
          var availableWidth = this.$size.scrollerWidth - this.$padding * 2;
          var limit = Math.floor(availableWidth / this.characterWidth);
          return this.session.adjustWrapLimit(limit);
      };
  
      this.setAnimatedScroll = function(shouldAnimate){
          this.$animatedScroll = shouldAnimate;
      };
  
      this.getAnimatedScroll = function() {
          return this.$animatedScroll;
      };
  
      this.setShowInvisibles = function(showInvisibles) {
          if (this.$textLayer.setShowInvisibles(showInvisibles))
              this.$loop.schedule(this.CHANGE_TEXT);
      };
  
      this.getShowInvisibles = function() {
          return this.$textLayer.showInvisibles;
      };
  
      this.$showPrintMargin = true;
      this.setShowPrintMargin = function(showPrintMargin) {
          this.$showPrintMargin = showPrintMargin;
          this.$updatePrintMargin();
      };
  
      this.getShowPrintMargin = function() {
          return this.$showPrintMargin;
      };
  
      this.$printMarginColumn = 80;
      this.setPrintMarginColumn = function(showPrintMargin) {
          this.$printMarginColumn = showPrintMargin;
          this.$updatePrintMargin();
      };
  
      this.getPrintMarginColumn = function() {
          return this.$printMarginColumn;
      };
  
      this.getShowGutter = function(){
          return this.showGutter;
      };
  
      this.setShowGutter = function(show){
          if(this.showGutter === show)
              return;
          this.$gutter.style.display = show ? "block" : "none";
          this.showGutter = show;
          this.onResize(true);
      };
  
      this.$updatePrintMargin = function() {
          var containerEl;
  
          if (!this.$showPrintMargin && !this.$printMarginEl)
              return;
  
          if (!this.$printMarginEl) {
              containerEl = dom.createElement("div");
              containerEl.className = "ace_print_margin_layer";
              this.$printMarginEl = dom.createElement("div");
              this.$printMarginEl.className = "ace_print_margin";
              containerEl.appendChild(this.$printMarginEl);
              this.content.insertBefore(containerEl, this.$textLayer.element);
          }
  
          var style = this.$printMarginEl.style;
          style.left = ((this.characterWidth * this.$printMarginColumn) + this.$padding) + "px";
          style.visibility = this.$showPrintMargin ? "visible" : "hidden";
      };
  
      this.getContainerElement = function() {
          return this.container;
      };
  
      this.getMouseEventTarget = function() {
          return this.content;
      };
  
      this.getTextAreaContainer = function() {
          return this.container;
      };
  
      this.moveTextAreaToCursor = function(textarea) {
          // in IE the native cursor always shines through
          // this persists in IE9
          if (useragent.isIE)
              return;
  
          if (this.layerConfig.lastRow === 0)
              return;
  
          var pos = this.$cursorLayer.getPixelPosition();
          if (!pos)
              return;
  
          var bounds = this.content.getBoundingClientRect();
          var offset = this.layerConfig.offset;
  
          textarea.style.left = (bounds.left + pos.left) + "px";
          textarea.style.top = (bounds.top + pos.top - this.scrollTop + offset) + "px";
      };
  
      this.getFirstVisibleRow = function() {
          return this.layerConfig.firstRow;
      };
  
      this.getFirstFullyVisibleRow = function() {
          return this.layerConfig.firstRow + (this.layerConfig.offset === 0 ? 0 : 1);
      };
  
      this.getLastFullyVisibleRow = function() {
          var flint = Math.floor((this.layerConfig.height + this.layerConfig.offset) / this.layerConfig.lineHeight);
          return this.layerConfig.firstRow - 1 + flint;
      };
  
      this.getLastVisibleRow = function() {
          return this.layerConfig.lastRow;
      };
  
      this.$padding = null;
      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.getHScrollBarAlwaysVisible = function() {
          return this.$horizScrollAlwaysVisible;
      };
  
      this.setHScrollBarAlwaysVisible = function(alwaysVisible) {
          if (this.$horizScrollAlwaysVisible != alwaysVisible) {
              this.$horizScrollAlwaysVisible = alwaysVisible;
              if (!this.$horizScrollAlwaysVisible || !this.$horizScroll)
                  this.$loop.schedule(this.CHANGE_SCROLL);
          }
      };
  
      this.$updateScrollBar = function() {
          this.scrollBar.setInnerHeight(this.layerConfig.maxHeight);
          this.scrollBar.setScrollTop(this.scrollTop);
      };
  
      this.$renderChanges = function(changes) {
          if (!changes || !this.session || !this.container.offsetWidth)
              return;
  
          // 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();
  
          // 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);
          }
  
          // 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);
              return;
          }
  
          // scrolling
          if (changes & this.CHANGE_SCROLL) {
              this.$updateScrollBar();
              if (changes & this.CHANGE_TEXT || changes & this.CHANGE_LINES)
                  this.$textLayer.update(this.layerConfig);
              else
                  this.$textLayer.scrollLines(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);
              return;
          }
  
          if (changes & this.CHANGE_TEXT) {
              this.$textLayer.update(this.layerConfig);
              if (this.showGutter)
                  this.$gutterLayer.update(this.layerConfig);
          }
          else if (changes & this.CHANGE_LINES) {
              if (this.$updateLines()) {
                  this.$updateScrollBar();
                  if (this.showGutter)
                      this.$gutterLayer.update(this.layerConfig);
              }
          } else if (changes & this.CHANGE_GUTTER) {
              if (this.showGutter)
                  this.$gutterLayer.update(this.layerConfig);
          }
  
          if (changes & this.CHANGE_CURSOR)
              this.$cursorLayer.update(this.layerConfig);
  
          if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_FRONT)) {
              this.$markerFront.update(this.layerConfig);
          }
  
          if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_BACK)) {
              this.$markerBack.update(this.layerConfig);
          }
  
          if (changes & this.CHANGE_SIZE)
              this.$updateScrollBar();
      };
  
      this.$computeLayerConfig = function() {
          var session = this.session;
  
          var offset = this.scrollTop % this.lineHeight;
          var minHeight = this.$size.scrollerHeight + this.lineHeight;
  
          var longestLine = this.$getLongestLine();
  
          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 maxHeight = this.session.getScreenLength() * this.lineHeight;
          this.session.setScrollTop(Math.max(0, Math.min(this.scrollTop, maxHeight - this.$size.scrollerHeight)));
  
          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 = { 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.getRowHeight(lineHeight, firstRow);
  
          lastRow = Math.min(session.screenToDocumentRow(lastRow, 0), session.getLength() - 1);
          minHeight = this.$size.scrollerHeight + session.getRowHeight(lineHeight, lastRow)+
                                                  firstRowHeight;
  
          offset = this.scrollTop - firstRowScreen * this.lineHeight;
  
          this.layerConfig = {
              width : longestLine,
              padding : this.$padding,
              firstRow : firstRow,
              firstRowScreen: firstRowScreen,
              lastRow : lastRow,
              lineHeight : this.lineHeight,
              characterWidth : this.characterWidth,
              minHeight : minHeight,
              maxHeight : maxHeight,
              offset : offset,
              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);
      };
  
      this.$updateLines = function() {
          var firstRow = this.$changedLines.firstRow;
          var lastRow = this.$changedLines.lastRow;
          this.$changedLines = null;
  
          var layerConfig = this.layerConfig;
  
          // if the update changes the width of the document do a full redraw
          if (layerConfig.width != this.$getLongestLine())
              return this.$textLayer.update(layerConfig);
  
          if (firstRow > layerConfig.lastRow + 1) { return; }
          if (lastRow < layerConfig.firstRow) { return; }
  
          // if the last row is unknown -> redraw everything
          if (lastRow === Infinity) {
              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.$textLayer.showInvisibles)
              charCount += 1;
  
          return Math.max(this.$size.scrollerWidth - 2 * this.$padding, Math.round(charCount * this.characterWidth));
      };
  
      this.updateFrontMarkers = function() {
          this.$markerFront.setMarkers(this.session.getMarkers(true));
          this.$loop.schedule(this.CHANGE_MARKER_FRONT);
      };
  
      this.updateBackMarkers = function() {
          this.$markerBack.setMarkers(this.session.getMarkers());
          this.$loop.schedule(this.CHANGE_MARKER_BACK);
      };
  
      this.addGutterDecoration = function(row, className){
          this.$gutterLayer.addGutterDecoration(row, className);
          this.$loop.schedule(this.CHANGE_GUTTER);
      };
  
      this.removeGutterDecoration = function(row, className){
          this.$gutterLayer.removeGutterDecoration(row, className);
          this.$loop.schedule(this.CHANGE_GUTTER);
      };
  
      this.setBreakpoints = function(rows) {
          this.$gutterLayer.setBreakpoints(rows);
          this.$loop.schedule(this.CHANGE_GUTTER);
      };
  
      this.setAnnotations = function(annotations) {
          this.$gutterLayer.setAnnotations(annotations);
          this.$loop.schedule(this.CHANGE_GUTTER);
      };
  
      this.updateCursor = function() {
          this.$loop.schedule(this.CHANGE_CURSOR);
      };
  
      this.hideCursor = function() {
          this.$cursorLayer.hideCursor();
      };
  
      this.showCursor = function() {
          this.$cursorLayer.showCursor();
      };
  
      this.scrollSelectionIntoView = function(anchor, lead) {
          // first scroll anchor into view then scroll lead into view
          this.scrollCursorIntoView(anchor);
          this.scrollCursorIntoView(lead);
      };
  
      this.scrollCursorIntoView = function(cursor) {
          // 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) {
              this.session.setScrollTop(top);
          }
  
          if (this.scrollTop + this.$size.scrollerHeight < top + this.lineHeight) {
              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;
              this.session.setScrollLeft(left);
          }
  
          if (scrollLeft + this.$size.scrollerWidth < left + this.characterWidth) {
              this.session.setScrollLeft(Math.round(left + this.characterWidth - this.$size.scrollerWidth));
          }
      };
  
      this.getScrollTop = function() {
          return this.session.getScrollTop();
      };
  
      this.getScrollLeft = function() {
          return this.session.getScrollLeft();
      };
  
      this.getScrollTopRow = function() {
          return this.scrollTop / this.lineHeight;
      };
  
      this.getScrollBottomRow = function() {
          return Math.max(0, Math.floor((this.scrollTop + this.$size.scrollerHeight) / this.lineHeight) - 1);
      };
  
      this.scrollToRow = function(row) {
          this.session.setScrollTop(row * this.lineHeight);
      };
  
      this.STEPS = 10;
      this.$calcSteps = function(fromValue, toValue){
          var i = 0;
          var l = this.STEPS;
          var steps = [];
  
          var func  = function(t, x_min, dx) {
              if ((t /= .5) < 1)
                  return dx / 2 * Math.pow(t, 3) + x_min;
              return dx / 2 * (Math.pow(t - 2, 3) + 2) + x_min;
          };
  
          for (i = 0; i < l; ++i)
              steps.push(func(i / this.STEPS, fromValue, toValue - fromValue));
          steps.push(toValue);
  
          return steps;
      };
  
      this.scrollToLine = function(line, center) {
          var pos = this.$cursorLayer.getPixelPosition({row: line, column: 0});
          var offset = pos.top;
          if (center)
              offset -= this.$size.scrollerHeight / 2;
  
          if (this.$animatedScroll && Math.abs(offset - this.scrollTop) < 10000) {
              var _self = this;
              var steps = _self.$calcSteps(this.scrollTop, offset);
  
              clearInterval(this.$timer);
              this.$timer = setInterval(function() {
                  _self.session.setScrollTop(steps.shift());
                  
                  if (!steps.length)
                      clearInterval(_self.$timer);
              }, 10);
          }
          else {
              this.session.setScrollTop(offset);
          }
      };
  
      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;
          }
      };
  
      this.scrollToX = function(scrollLeft) {
          if (scrollLeft <= this.$padding)
              scrollLeft = 0;
  
          if (this.scrollLeft !== scrollLeft)
              this.scrollLeft = scrollLeft;
          this.$loop.schedule(this.CHANGE_H_SCROLL);
      };
  
      this.scrollBy = function(deltaX, deltaY) {
          deltaY && this.session.setScrollTop(this.session.getScrollTop() + deltaY);
          deltaX && this.session.setScrollLeft(this.session.getScrollLeft() + deltaX);
      };
  
      this.isScrollableBy = function(deltaX, deltaY) {
          if (deltaY < 0 && this.session.getScrollTop() > 0)
             return true;
          if (deltaY > 0 && this.session.getScrollTop() + this.$size.scrollerHeight < this.layerConfig.maxHeight)
             return true;
          // todo: handle horizontal scrolling
      };
  
      this.pixelToScreenCoordinates = function(pageX, pageY) {
          var canvasPos = this.scroller.getBoundingClientRect();
  
          var col = Math.round(
              (pageX + this.scrollLeft - canvasPos.left - this.$padding - dom.getPageScrollLeft()) / this.characterWidth
          );
          var row = Math.floor(
              (pageY + this.scrollTop - canvasPos.top - dom.getPageScrollTop()) / this.lineHeight
          );
  
          return {row: row, column: col};
      };
  
      this.screenToTextCoordinates = function(pageX, pageY) {
          var canvasPos = this.scroller.getBoundingClientRect();
  
          var col = Math.round(
              (pageX + this.scrollLeft - canvasPos.left - this.$padding - dom.getPageScrollLeft()) / this.characterWidth
          );
          var row = Math.floor(
              (pageY + this.scrollTop - canvasPos.top - dom.getPageScrollTop()) / this.lineHeight
          );
  
          return this.session.screenToDocumentPosition(row, Math.max(col, 0));
      };
  
      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
          };
      };
  
      this.visualizeFocus = function() {
          dom.addCssClass(this.container, "ace_focus");
      };
  
      this.visualizeBlur = function() {
          dom.removeCssClass(this.container, "ace_focus");
      };
  
      this.showComposition = function(position) {
          if (!this.$composition) {
              this.$composition = dom.createElement("div");
              this.$composition.className = "ace_composition";
              this.content.appendChild(this.$composition);
          }
  
          this.$composition.innerHTML = "&#160;";
  
          var pos = this.$cursorLayer.getPixelPosition();
          var style = this.$composition.style;
          style.top = pos.top + "px";
          style.left = (pos.left + this.$padding) + "px";
          style.height = this.lineHeight + "px";
  
          this.hideCursor();
      };
  
      this.setCompositionText = function(text) {
          dom.setInnerText(this.$composition, text);
      };
  
      this.hideComposition = function() {
          this.showCursor();
  
          if (!this.$composition)
              return;
  
          var style = this.$composition.style;
          style.top = "-10000px";
          style.left = "-10000px";
      };
  
      this._loadTheme = function(name, callback) {
          if (!config.get("packaged"))
              return callback();
  
          var base = name.split("/").pop();
          var filename = config.get("themePath") + "/theme-" + base + config.get("suffix");
          net.loadScript(filename, callback);
      };
  
      this.setTheme = function(theme) {
          var _self = this;
  
          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);
                  });
              });
          } else {
              afterLoad(theme);
          }
  
          function afterLoad(theme) {
              dom.importCssString(
                  theme.cssText,
                  theme.cssClass,
                  _self.container.ownerDocument
              );
  
              if (_self.$theme)
                  dom.removeCssClass(_self.container, _self.$theme);
  
              _self.$theme = theme ? theme.cssClass : null;
  
              if (_self.$theme)
                  dom.addCssClass(_self.container, _self.$theme);
  
              if (theme && theme.isDark)
                  dom.addCssClass(_self.container, "ace_dark");
              else
                  dom.removeCssClass(_self.container, "ace_dark");
  
              // force re-measure of the gutter width
              if (_self.$size) {
                  _self.$size.width = 0;
                  _self.onResize();
              }
          }
      };
  
      this.getTheme = function() {
          return this.$themeValue;
      };
  
      // 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.
  
      this.setStyle = function setStyle(style) {
        dom.addCssClass(this.container, style);
      };
  
      this.unsetStyle = function unsetStyle(style) {
        dom.removeCssClass(this.container, style);
      };
  
      this.destroy = function() {
          this.$textLayer.destroy();
          this.$cursorLayer.destroy();
      };
  
  }).call(VirtualRenderer.prototype);
  
  exports.VirtualRenderer = VirtualRenderer;
  });
  