<?xml version="1.0"?>
<bindings xmlns="http://www.mozilla.org/xbl" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <binding id="codearea" display="html:div">
    
    <content>
      <html:div id="body">
        <html:pre id="one-char"><html:span>X</html:span></html:pre>
        <html:pre id="canvas"><html:textarea id="clipboard" readonly="true" style="position:absolute;width:30px;height:30px;visibility:hidden;z-index:-1000"></html:textarea></html:pre>
        <html:pre id="cursors"><html:span id="cursor">&#160;</html:span><html:span id="col-cursor">&#160;</html:span><html:span id="line-cursor">&#160;</html:span><html:span id="selection-plane"><html:span id="start-of-selection" style="display:none">&#160;</html:span><html:span id="end-of-selection" style="display:none">&#160;</html:span></html:span></html:pre>
        <html:pre id="left-margin"><html:span id="line-cursor-in-margin">&#160;</html:span></html:pre>
      </html:div>
      <html:div style="display: none"><children/></html:div>
    </content>
    
    <implementation>
      <property name="formattedCode" onget="return this.codeArea.getFormattedCode();" readonly="true" />
      <property name="value" onget="return this.codeArea.getValue();" onset="return this.codeArea.setValue(val);" />
      <property name="readOnly" onget="return this.codeArea.getReadOnly();" onset="return this.codeArea.setReadOnly(val);" />
      <property name="selection" onget="return this.codeArea.getSelection();" readonly="true" />
      
      <method name="createCodeRange">
        <body>return this.codeArea.createCodeRange();</body>
      </method>
      
      <method name="getLineByNumber">
        <parameter name="number" />
        <body>return this.codeArea.getLineByNumber(number);</body>
      </method>
      
      <method name="findNext">
        <parameter name="s" />
        <parameter name="flags" />
        <body>return this.codeArea.findNext(s, flags);</body>
      </method>
      
      <method name="findPrevious">
        <parameter name="s" />
        <parameter name="flags" />
        <body>return this.codeArea.findPrevious(s, flags);</body>
      </method>
      
      <method name="findAll">
        <parameter name="s" />
        <parameter name="flags" />
        <body>return this.codeArea.findAll(s, flags);</body>
      </method>

      <method name="getAction">
        <parameter name="name" />
        <body>return this.actions[name];</body>
      </method>

      <method name="createAction">
        <parameter name="_perform" />
        <parameter name="options" />
        <body>return this.codeArea.createAction(_perform, options);</body>
      </method>
      
      <method name="addEventListener">
        <parameter name="eventName" />
        <parameter name="handler" />
        <parameter name="useCapture" />
        <body>return this.codeArea.addEventListener(eventName, handler, useCapture);</body>
      </method>
      
      <constructor><![CDATA[
      
var crossBrowser = {
  uid:0,
  getOwnerDocument: function(el)
  {
    if (el.ownerDocument)
      return el.ownerDocument;
    else
    {
      while (el.parentNode)
        el = el.parentNode;
      return el.document;
    }
  },
  getUniqueId: function(el)
  {
    if (!el.uniqueID)
    {
      if (el.id)
        el.uniqueID = el.id
      else
      {
        el.uniqueID = ('__uid' + this.uid++);
        el.id = el.uniqueID;
      }
    }
    return el.uniqueID;
  },
  getIFrameDocument: function(iframe)
  {
    if (iframe.contentWindow)
      return iframe.contentWindow.document;
    else if (iframe.contentDocument)
      return iframe.contentDocument;
    else
    {
      var iframes = comp.getElementsByTagName('iframe');
      for (var i=0; i<iframes.length; i++)
        if (iframes[i] == iframe)
          return window.frames[i].document;
//alert(window.frames[0].comp.location.href);
    }
//    else
//      return null;
  },
  getInnerText: function(el)
  {
    if (typeof comp.body.innerText == 'string')
      return el.innerText;
    else
    {
      var html = el.innerHTML;
      var txt = html.replace(/<[^>]+>/g, '');
      return txt;
    }
  },
  getCurrentStyleAttribute: function(el, attr)
  {
    var val = 'inherit';
    while (el && ((val == 'inherit') || (val == 'transparent')))
    {
      if (typeof el.currentStyle == 'object')
        val = el.currentStyle[attr];
      else
      {
        var cssVal = null;

        switch (attr)
        {
          case "margin":
          case "padding":
            cssVal = "";

            var composites = { "top":1, "right":1, "bottom":1, "left":1 };
            for (var direction in composites)
            {
              var singleVal = this.__getMozStyleAttribute(el, attr + "-" + direction);
              cssVal += singleVal + " ";
            }
            break;
          default:
            cssVal = this.__getMozStyleAttribute(el, attr);
            break;
        }

        val = cssVal;
      }

      el = el.parentNode;
    }
      
    return val;
  },
  __getMozStyleAttribute : function(el, attr)
  {
    var cssAttr = attr.replace(/([A-Z])/g, function(s){ return "-" + s.toLowerCase(); });
    return window.getComputedStyle(el, "").getPropertyValue(cssAttr);
  },
  setStyleAttribute: function(el, attr, value)
  {
    // See if this style attr is stored
    // if so: a runtimeStyle attr is overriding it
    if (el.storedStyle && typeof(el.storedStyle[attr]) != 'undefined')
      // Override stored value
      el.storedStyle[attr] = value;
    else
      // Override live value
      el.style[attr] = value;
  },
  removeStyleAttribute: function(el, attr)
  {
    // See if this style attr is stored
    // if so: a runtimeStyle attr is overriding it
    if (el.storedStyle && typeof(el.storedStyle[attr]) != 'undefined')
      // Override the stored attribute with a null value
      el.storedStyle[attr] = null;
    else
      if (el.style.removeAttribute)
        // Remove the style attribute
        el.style.removeAttribute(attr);
      else
        el.style[attr] = '';
  },
  setRuntimeStyleAttribute: function(el, attr, value)
  {
    if (!el.storedStyle)
      el.storedStyle = {};
    if (typeof(el.storedStyle[attr]) == 'undefined')
      el.storedStyle[attr] = el.style[attr];
    el.style[attr] = value;
  },
  removeRuntimeStyleAttribute: function(el, attr)
  {
//if (attr == 'overflow')
//alert(el.storedStyle[attr] + ', ' + ((el.storedStyle[attr])?'xxx':'yyy'));
//el.storedStyle[attr] = 'visible';
    if (el.storedStyle)
    {
      if (el.storedStyle[attr])
        el.style[attr] = el.storedStyle[attr];
      else if (typeof(el.storedStyle[attr]) != 'undefined')
        if (el.style.removeAttribute)
          el.style.removeAttribute(attr);
        else
          el.style[attr] = '';
      delete el.storedStyle[attr];
    }
  },

  // returns node.xml,  because the crap called safari can't do it and doesn't allow dom prototyping either, woohoo, praise safari
  getXML : function(node)
  {
    if (typeof node.xml == 'string')
      return node.xml; // duh

    return new XMLSerializer().serializeToString(node);
  },

  getOuterHTML: function(el)
  {
    if (typeof el.outerHTML == 'string')
      return el.outerHTML;
    else
    {
      var div = document.createElement('div');
      div.appendChild(el.cloneNode(true));
      return div.innerHTML;
    }
  },

  getNthChildElement: function(el, n)
  {
    el = el.firstChild;
    while (el && (el.nodeType != 1) && (n>=0))
    {
      if (el.nodeType == 1)
        n--;
      el = el.nextSibling;
    }
    return el;
  },

  // returns node.text
  getText : function(node)
  {
    if (typeof node.text == 'string')
      return node.text;

    if (!node.firstChild)
      return "";

    return node.firstChild.nodeValue;
  },

  // sets node.text
  setText : function(node, text)
  {
    if (typeof node.text == 'string')
      node.text = text;
    else
    {
      while (node.firstChild)
        node.removeChild(node.firstChild);

      node.appendChild(this.getOwnerDocument(node).createTextNode(text));
    }
  },
  
  appendHTML : function(el, html)
  {
    if (el.insertAdjacentHTML)
      el.insertAdjacentHTML("beforeend", html);
    else
    {
      var div = document.createElement("div");
      div.innerHTML = html;
      while (div.firstChild)
        el.appendChild(div.firstChild);
    }
  },

  // returns box element
  getBoundingClientRect : function(el)
  {
    if (typeof el.getBoundingClientRect != 'undefined')
      return el.getBoundingClientRect();
    else
    {
      var rect = { left: 0, top: 0, right: 0, bottom: 0 };

      var tempElt = el;
      while (tempElt)
      {
        rect.top  += (tempElt.offsetTop || 0) - (tempElt.scrollTop || 0);
        rect.left += (tempElt.offsetLeft || 0) - (tempElt.scrollLeft || 0);
        tempElt = tempElt.offsetParent;
      }
      rect.bottom = rect.top + el.offsetHeight;
      rect.right  = rect.left + el.offsetWidth;

      return rect;
    }
  },

  // gets the position relative to top/left of #body
  getBoundingBodyRect : function(el)
  {
    var box =
    {
      top : 0,
      left : 0,
      width : el.offsetWidth,
      height : el.offsetHeight
    };

    var opEl = el;
    while (opEl && (opEl.getAttribute("id") != "body"))
    {
      box.left += opEl.offsetLeft;
      box.top += opEl.offsetTop;

      opEl = opEl.offsetParent;
    }

    box.right = box.left + box.width;
    box.bottom = box.top + box.height;

    if ((el.nodeName.toLowerCase() == 'li') && (this.getCurrentStyleAttribute(el, 'display') == 'block'))
    {
      var corrWidth = 20;
      box.left -= corrWidth;
      box.width += corrWidth;
    }

    return box;
  },

  // swap nodes
  swapNode : function(el1, el2)
  {
    if (typeof el1.swapNode != 'undefined')
      el1.swapNode(el2);
    else
    {
      var temp = document.createElement("div");

      var pn = el1.parentNode;
      pn.replaceChild(temp, el1);
      el2.parentNode.replaceChild(el1, el2);
      pn.replaceChild(el2, temp);

      temp = null;
    }
  },

  pasteText: function(s)
  {
    var rng = this.getSelectionRange();
    if (!rng) return;
    if (typeof rng.setText != 'undefined')
    {
      rng.setText(s);
      return;
    }
    else
    {
      var sel = window.getSelection();
      sel.removeAllRanges();
      
      var textNode = rng.setText(s);
      rng = this.createRange();
      rng.moveToElementText(textNode);
      rng.collapse();
      
      sel.addRange(rng.rng);
    }
  },
  getSelectionRange: function()
  {
    if (typeof comp.selection == 'object')
    {
      var sel = comp.selection;
      var rngs = sel.createRangeCollection();
      
      return rngs.length > 0 ? 
        new IERange(rngs.item(0)) :
        false;
    }
    else
    {
      var sel = window.getSelection();

      return sel.rangeCount > 0 ?
        new MozRange(sel.getRangeAt(0)) :
        false;
    }
  },
  setInputSelectionRange: function(inp, start, end)
  {
    if (inp.createTextRange)
    {
      var tr = inp.createTextRange();
      tr.move('character', start);
      tr.moveEnd('character', end - start);
      tr.select();
    }
    else
    {
      inp.selectionStart = start;
      inp.selectionEnd = end;
    }
  },
  getInputSelectionStart: function(inp)
  {
    if (typeof comp.selection == 'object')
    {
      var sel = comp.selection;
      var rng = sel.createRange();
      rng.collapse();
      rng.moveStart("character", -10000);
      return rng.text.length;
    }
    else
      return inp.selectionStart;
  },
  getInputSelectionEnd: function(inp)
  {
    if (typeof comp.selection == 'object')
    {
      var sel = comp.selection;
      var rng = sel.createRange();
      rng.moveStart("character", -10000);
      return rng.text.length;
    }
    else
      return inp.selectionEnd;
  },
  unselect: function()
  {
    if (comp.selection && comp.selection.empty)
      comp.selection.empty();
    else
    {
      var sel = window.getSelection();
      sel.removeAllRanges();
    }
  },
  createRange: function()
  {
    if (comp.body.createTextRange)
    {
      var rng = comp.body.createTextRange();
      rng.collapse();
      return new IERange(rng);
    }
    else
    {
      var rng = document.createRange();
      rng.setStart(comp.body, 0);
      rng.setEnd(comp.body, 0);
      return new MozRange(rng);
    }
  },
  getStyleSheetNode: function(doc, ssID)
  {
    if (comp.all && window.createPopup)
      return comp.getElementById(ssID);
    else
    {
      for (var i=0; i<comp.styleSheets.length; i++)
      {
        var ss = comp.styleSheets[i];
        if (ss.ownerNode.id == ssID)
          return ss.ownerNode;
      }
      return null;
    }
  },
  getXMLHTTP: function ()
  {
    if (window.XMLHttpRequest)
      return new XMLHttpRequest();
    else if (window.ActiveXObject)
    {
      var XMLHTTPPROGIDS = [
        "MSXML2.XMLHTTP.6.0",
        "MSXML2.XMLHTTP.5.0",
        "MSXML2.XMLHTTP.4.0",
        "MSXML2.XMLHTTP.3.0",
        "MSXML2.XMLHTTP",
        "Microsoft.XMLHTTP"
      ];

      for (var i=0; i<XMLHTTPPROGIDS.length; i++)
      {
        try
        {
          return new ActiveXObject(XMLHTTPPROGIDS[i]);
        }
        catch (e) {}
      }
    }

    return null;
  },
  isPreceding: function(el1, el2)
  {
    if (typeof el1.sourceIndex == 'number')
    {
      return el1.sourceIndex < el2.sourceIndex;
    }
    else
    {
      return !!(el2.compareDocumentPosition(el1) & 0x02);
    }
  },
  
  addClass: function(el, className)
  {
    var s = ' ';
    var cc = s + el.className + s;
    if (cc.indexOf(s + className + s) == -1)
      el.className += s + className;
  },
  
  removeClass: function(el, className)
  {
    var s = ' ';
    var cc = s + el.className + s;
    el.className = cc.replace(s + className + s, '');
  }
  
};

function MozRange (rng)
{
  this.rng = rng;
}

MozRange.prototype =
{
  moveToElementText: function (elt)
  {
    this.rng.selectNode(elt);
  },

  select: function ()
  {
    var sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(this.rng.cloneRange());
  },
  
  collapse: function (toStart)
  {
    this.rng.collapse(toStart);
  },
  
  pasteHTML: function (html)
  {
    if (this.rng.length)
    {
      var ddiv = document.createElement('div');
      ddiv.innerHTML = html;

      var dimg = ddiv.firstChild;
      this.rng(0).parentNode.insertBefore(dimg, this.rng(0));
      this.rng(0).parentNode.removeChild(this.rng(0));
      this.rng.remove(0);
      this.rng.addElement(dimg);
    }
    else
    {
      if (this.parentElement().nodeName.toLowerCase() == 'textarea')
      {
        var ta = oip.canvas.curView.ta;

        var pos1 = ta.selectionStart;
        var pos2 = ta.selectionEnd;
        var pos3 = pos1 + html.length;

        var newText = ta.value.substring(0, pos1) + html + ta.value.substring(pos2, ta.value.length);
        ta.value = newText;

        ta.setSelectionRange(pos1, pos3);
      }
      else
      {
        var doc = this.parentElement().ownerDocument;
        doc.execCommand('inserthtml', false, html);
      }
    }
  },
  
  getParentElement: function ()
  {
    var parent = this.rng.commonAncestorContainer;
  
    if (parent.nodeType == 3)
      return parent.parentNode;
    else
      return parent;
  },
  
  getText: function ()
  {
    var root = document.createElement("x");
    root.appendChild(this.rng.cloneContents());
    return root.innerText;
  },
  
  setText: function (s)
  {
    var t = comp.createTextNode(s);
    this.rng.deleteContents();
    this.rng.insertNode(t);
    return t;
  },
  
  setStartByChars: function(node, chars)
  {
    this.setPosByChars(node, chars, this.rng.setStart);
  },
  
  setEndByChars: function(node, chars)
  {
    this.setPosByChars(node, chars, this.rng.setEnd);
  },
  
  setPosByChars: function(node, chars, method)
  {
    var xpe = new XPathEvaluator();
    var iter = crossBrowser.xpe.evaluate("descendant::text()", node, crossBrowser.nsr, null, null);
    var l = 0; var n; var s='';
    while (n = iter.iterateNext())
    {
      var nl = n.textContent.length;
      if (l + nl >= chars)
        break;
      l+=nl;
    }
    var offset = chars - l;
    if (!n)
    {
      // reached end of line
      n = node;
      offset = node.childNodes.length;
    }
    if (method == this.rng.setStart && this.rng.comparePoint(n, offset) == 1)
    {
      // requesting to set start after end which Mozilla doesn't allow
      // bring start to current end
      this.rng.collapse(false);
      // set end instead
      method = this.rng.setEnd;
    }
    method.call(this.rng, n, offset);
  },
  
  duplicate: function()
  {
    return new MozRange(this.rng.cloneRange());
  }
};

function IERange (rng)
{
  this.rng = rng;
}

IERange.prototype =
{
  moveToElementText: function (elt)
  {
    this.rng.moveToElementText(elt);
  },

  select: function ()
  {
    this.rng.select();
  },

  collapse: function (toStart)
  {
    this.rng.collapse(toStart);
  },

  pasteHTML: function (html)
  {
    if (this.rng.length)
    {
      var ddiv = document.createElement('div');
      ddiv.innerHTML = html;

      var dimg = ddiv.firstChild;
      this.rng(0).parentNode.insertBefore(dimg, this.rng(0));
      this.rng(0).parentNode.removeChild(this.rng(0));
      this.rng.remove(0);
      this.rng.addElement(dimg);
    }
    else
      this.rng.pasteHTML(html);
  },
  
  getParentElement: function ()
  {
    if (this.rng.parentElement)
      return this.rng.parentElement();
    else if (this.rng.length)
      return this.rng(0);
    else
      return null;
  },
  
  getHTMLText: function ()
  {
    return this.rng.htmlText;
  },
  
  getText: function ()
  {
    return this.rng.text;
  },
  
  setText: function (s)
  {
    this.rng.text = s;
  },
  
  setStartByChars: function(node, offset)
  {
    this.setPosByChars(node, offset, "Start");
  },
  
  setEndByChars: function(node, offset)
  {
    this.setPosByChars(node, offset, "End");
  },
  
  setPosByChars: function(node, offset, point)
  {
    var l = node.innerText.length;
    if (offset >= l)
      offset = l;
    var rng = comp.body.createTextRange();
    rng.moveToElementText(node);
    //rng.select();
    if (offset || !l)
      rng.moveStart("character", offset);
    else
    {
      rng.moveStart("character", 1);
      rng.moveStart("character", -1);
    }
    this.rng.setEndPoint(point + "ToStart", rng);
  },
  
  execCommand: function(cmd, ui)
  {
    if (this.rng.execCommand)
      this.rng.execCommand(cmd, ui);
  },
  
  duplicate: function()
  {
    return new IERange(this.rng.duplicate());
  },
  
  getBoundingWidth: function()
  {
    return this.rng.boundingWidth;
  },
  
  toString: function()
  {
    return "IERange: " + this.rng.text;
  }
};

if (window.XPathEvaluator)
{
  crossBrowser.xpe = new XPathEvaluator();
  crossBrowser.nsr = crossBrowser.xpe.createNSResolver(document);
  Node.prototype.__defineGetter__("innerText", function()
  {
    var xpRes = crossBrowser.xpe.evaluate("descendant::text()", this, crossBrowser.nsr, null, null);
    var a = [];
    var n;
    while (n = xpRes.iterateNext())
      a.push(n.nodeValue);
    return a.join('');
  });

  Node.prototype.__defineSetter__("innerText", function(text)
  {
    return this.textContent = text;
  });
};

function LinkedList()
{
  this.__hash = {};
}

LinkedList.prototype = {
  __hash: null,
  firstItem: null,
  lastItem: null,
  curItem: null,
  add: function(item)
  {
    item.prevItem = this.lastItem;
    item.nextItem = null;

    if (this.lastItem)
      this.lastItem.nextItem = item;
    else
      this.firstItem = item;
    this.lastItem = item;

    if (typeof(item.id) != 'undefined')
      this.__hash[item.id] = item;
  },
  remove: function(item)
  {
    if (item.prevItem)
      item.prevItem.nextItem = item.nextItem;
    if (item.nextItem)
      item.nextItem.prevItem = item.prevItem;

    if (item == this.firstItem)
      this.firstItem = item.nextItem;
    if (item == this.lastItem)
      this.lastItem = item.prevItem;

    if (typeof(item.id) != 'undefined')
      delete this.__hash[item.id];
  },
  reset: function(item)
  {
    if (item && (item != this.firstItem) && (this.firstItem != this.lastItem))
    {
      // Close loop
      this.firstItem.prevItem = this.lastItem;
      this.lastItem.nextItem = this.firstItem;
      
      // Store new endpoints
      this.firstItem = item;
      this.lastItem = item.prevItem;
      
      // Open loop
      if (item.prevItem)
        item.prevItem.nextItem = null;
      item.prevItem = null;
    }

    return this.curItem = this.firstItem;
    
//    return this.curItem;
  },
  getNextItem: function()
  {
    if (this.curItem)
      this.curItem = this.curItem.nextItem;
    
    return this.curItem;
  },
  getItemById: function(id)
  {
    return this.__hash[id];
  },
  hasNextItem: function()
  {
    return (this.curItem && this.curItem.nextItem);
  },
  hasItems: function()
  {
    return (this.firstItem != null);
  }
};

function Shredder()
{
}

Shredder.prototype = {
  RUN_ONCE_REQUIRED: 1,
  RUN_ONCE: 2,
  RUN_MANY: 3,
  DEFAULT_RUN_MODE: 2,
  
  __queues: [null, new LinkedList(), new LinkedList(), new LinkedList()],
  __maxRunTime: 30,
  __intervalTime: 1,
  __ih: null,
  __isRunning: false,
  __curTime: null,
  
  extend: function(shred, runMode)
  {
    for (var m in Shred.prototype)
      if (!shred[m])
        shred[m] = Shred.prototype[m];

    if (!runMode)
      runMode = this.DEFAULT_RUN_MODE;
    
    shred.setRunMode(runMode);
  },


  __prepareRun: function()
  {
    if (!shredder.__ih)
      shredder.__ih = setInterval(shredder.__runTasks, shredder.__intervalTime);
  },
  __stopRunning: function()
  {
    clearInterval(shredder.__ih);
    shredder.__ih = null;
  },
i:0,
  __runTasks: function()
  {
    shredder.__isRunning = true;
    
    var tStart = +new Date();
    var tNow = tStart;
    var tEnd = tStart + shredder.__maxRunTime;

    var hasActiveTask = false;
    var runMode = shredder.RUN_ONCE_REQUIRED;

    modeLoop: while (true) 
    {
      var didRun = false;
      
      var curList = shredder.__queues[runMode];

      if (curList.hasItems())
      {
        if (runMode > shredder.RUN_ONCE_REQUIRED)
          curList.reset(curList.getNextItem());

        var shred = curList.firstItem;
        while (shred)
        {
          // Run the next shred.
          if (shred.__active)
          {
            hasActiveTask = true;
            didRun = true;

            shred.run(tStart);
          }
      
          if (runMode > shredder.RUN_ONCE_REQUIRED)
          {
            // See if it is time to stop.
            if (new Date() >= tEnd)
              break modeLoop;
          }

          shred = shred.nextItem;
        }
      }

      // See if there were running tasks.
      if (runMode >= shredder.RUN_MANY && !didRun)
        break modeLoop;

      // See if it is time to stop.
      if (new Date() >= tEnd)
        break modeLoop;

      // Determine next runMode.
      if (runMode < shredder.RUN_MANY)
        runMode++;
    }

    if (!hasActiveTask)
      shredder.__stopRunning();

    shredder.__isRunning = false;
  }
};

function Shred()
{
}

Shred.prototype = {
  __active:  false,
  __runMode: null,
  setRunMode: function(newMode)
  {
    // First remove from old mode queue.
    if (this.__runMode)
      shredder.__queues[this.__runMode].remove(this);
    
    // Store the new settings.
    this.__runMode = newMode;    
    
    // Add this shred.
    shredder.__queues[newMode].add(this);
  },
  start: function()
  {
    shredder.__prepareRun();

    this.__active = true;
  },
  stop: function()
  {
    this.__active = false;
  },
  run: function(t)
  {
  }
};

var shredder = new Shredder();

// sorteren op attrName en dan op element
// dan kan ik colors overslaan als stap te klein

function Animator(anims, modifiers)
{
  this.anims = anims;
  this.modifiers = modifiers;

  this.delay    = modifiers.delay;
  this.duration = modifiers.duration;
  this.runCount = 0;
  this.skipCount = 0;

  if (!document.body.runtimeStyle)
    modifiers.useRuntimeStyle = false;
  this.styleCollectionName = (modifiers.useRuntimeStyle?'runtimeStyle':'style');
    
  
  // Start with a prevPhase of 0 to prevent rendering of phase == 0 which is the start value.
  this.prevPhase = 0;

  var bitDepth = 10;
  var numSteps = 1 << bitDepth;

  var bitDepthColor = 6;
  var numStepsColor = 1 << bitDepthColor;

  var code = [
'this.runCount++\n',
    'var tPhase = (t - this.tStart)/this.duration;\n',
    'if (t - this.tPrev > 200)\n',
    '{\n',
    '  this.tStart += (t - this.tPrev);\n',
    '  this.tStop += (t - this.tPrev);\n',
    '  this.tPrev = t;\n',
    '  return;\n',
    '}\n',
    'this.tPrev = t;\n',
    'if (tPhase >= 1)\n',
    '{\n',
    '  this.run = this.finish;\n',
    '  var phase = ', numSteps, ';\n',
    '  var phaseColor = ', numStepsColor, ';\n',
    '}\n',
    'else\n',
    '{\n',
    '  var f = ', this.profiles[modifiers.profile], ';\n',
    '  var phase = Math.round(', numSteps, '*f);\n',
    '  var phaseColor = Math.round(', numStepsColor, '*f);\n',
    '}\n',
    'if (phase == this.prevPhase)\n',
    '{\n',
'  this.skipCount++;\n',
    '  return;\n',
    '}\n',
    'this.prevPhase = phase;\n'
  ];

  var numAnims = anims.length;
  
  var codeByRange = [];
  for (var i=0; i<numAnims; i++)
  {
    var anim = anims[i];

    if (!anim.el.uniqueID)
      anim.el.uniqueID = 'moz_' + this.uid++;

    for (var attrName in anim.targetState)
    {
      var attrValue = anim.targetState[attrName];

      // Special treatment for non-IE
      if (!anim.el.currentStyle)
      {
        var attrNameMozilla = attrName.replace(/([A-Z])/g, function(s1){ return '-' + s1.toLowerCase(); });

        switch (attrName)
        {
          case 'alpha':
            attrName = 'MozOpacity';
            attrNameMozilla = '-moz-opacity';
            break;
          case 'gradient':
            continue;
            break;
        }
      }

      // Store whatever is needed to set the value of this attribute
      switch (attrName)
      {
        case 'scrollTop':
        case 'scrollLeft':
          this[anim.el.uniqueID + 'el'] = anim.el;
          break;
        case 'top':
        case 'right':
        case 'bottom':
        case 'left':
        case 'width':
        case 'height':
        case 'color':
        case 'backgroundColor':
        case 'borderTopColor':
        case 'borderRightColor':
        case 'borderBottomColor':
        case 'borderLeftColor':
        case 'clip':
        case 'MozOpacity':
          this[anim.el.uniqueID + 'elStyle'] = anim.el[this.styleCollectionName];
          break;
        case 'alpha':
          this[anim.el.uniqueID + 'alpha'] = anim.el.filters.item("DXImageTransform.Microsoft.Alpha");
          break;
        case 'gradient':
          this[anim.el.uniqueID + 'gradient'] = anim.el.filters.item("progid:DXImageTransform.Microsoft.gradient");
          break;
      }
    
      // Get the start value for this attribute
      var vStart = this.getCurrentStyleAttribute(anim.el, attrName);
      
      // Determine these variables to store in the codeByRange sparse array. 
      
      // The line of code that sets the new value.
      var codeLine;
      
      // The least amount the phase needs to increase to make the value set by this animation increase by 1.
      var relPhase;
      
      // The absolute amount that this animation will change its value.
      var absRange;
      
      // Add code to function to set current value of attribute
      switch (attrName)
      {
        case 'top':
        case 'right':
        case 'bottom':
        case 'left':
        case 'width':
        case 'height':
        case 'MozOpacity':
          var relRange = attrValue - vStart;
          absRange = Math.abs(relRange);
          relPhase = Math.ceil((1 << bitDepth) / absRange);
          codeLine = [
            'this.', anim.el.uniqueID, 'elStyle.', attrName, '=(', Math.floor(numSteps*(vStart+0.5)), '+phase*', relRange, ')>> ', bitDepth, ';\n'
          ].join("");
          break;
        case 'color':
        case 'backgroundColor':
        case 'borderTopColor':
        case 'borderRightColor':
        case 'borderBottomColor':
        case 'borderLeftColor':
          attrValue = this.__castColor(attrValue);
          
          var rRange = Math.floor(attrValue[0] - vStart[0]);
          var gRange = Math.floor(attrValue[1] - vStart[1]);
          var bRange = Math.floor(attrValue[2] - vStart[2]);

          absRange = Math.max(Math.abs(rRange), Math.abs(gRange), Math.abs(bRange));
          relPhase = Math.ceil((1 << bitDepthColor) / absRange) << (bitDepth - bitDepthColor);
          
          if (anim.el.currentStyle)
            codeLine = [
              'this.', anim.el.uniqueID, 'elStyle.', attrName, '=',
              '((', Math.floor(numStepsColor*(vStart[0]+0.5)), '+phaseColor*', rRange, ') >> ', bitDepthColor, ' << 16) | ',
              '((', Math.floor(numStepsColor*(vStart[1]+0.5)), '+phaseColor*', gRange, ') >> ', bitDepthColor, ' << 8) | ',
              '((', Math.floor(numStepsColor*(vStart[2]+0.5)), '+phaseColor*', bRange, ') >> ', bitDepthColor, ');\n'
              ].join('');
          else
            codeLine = ['this.', anim.el.uniqueID, 'elStyle.', attrName, '=["rgb(",',
            '(', Math.floor(numStepsColor*(vStart[0]+0.5)), '+phaseColor*', rRange, ')>> ', bitDepthColor, ',",", ',
            '(', Math.floor(numStepsColor*(vStart[1]+0.5)), '+phaseColor*', gRange, ')>> ', bitDepthColor, ',",", ',
            '(', Math.floor(numStepsColor*(vStart[2]+0.5)), '+phaseColor*', bRange, ')>> ', bitDepthColor, ', ")"].join("");\n'].join('');
          break;
        case 'alpha':
          var relRange = Math.floor(attrValue-vStart);
          
          absRange = Math.abs(relRange);
          relPhase = Math.round((1 << bitDepth) / absRange);
          
          codeLine = ['this.', anim.el.uniqueID, 'alpha.opacity=(', numSteps*(vStart+0.5), '+phase*', relRange, ') >> ', bitDepth, ';\n'].join('');
          break;
        case 'gradient':
          attrValue = [
            this.__castColor(attrValue[0]),
            this.__castColor(attrValue[1])
          ];
          
          var rRange = Math.floor(attrValue[0][0] - vStart[0][0]);
          var gRange = Math.floor(attrValue[0][1] - vStart[0][1]);
          var bRange = Math.floor(attrValue[0][2] - vStart[0][2]);

          absRange = Math.max(Math.abs(rRange), Math.abs(gRange), Math.abs(bRange));
          relPhase = Math.ceil((1 << bitDepthColor) / absRange) << (bitDepth - bitDepthColor);
          
          codeLine = ['this.', anim.el.uniqueID, 'gradient.startColorStr=',
          '((', vStart[0][0], '+phase*', rRange, ') << 16) | ',
          '((', vStart[0][1], '+phase*', gRange, ') <<  8) | ',
          ' (', vStart[0][2], '+phase*', bRange, ');\n'].join('');
          break;
        case 'scrollTop':
        case 'scrollLeft':
          var relRange = Math.floor(attrValue-vStart);
          
          absRange = Math.abs(relRange);
          relPhase = Math.ceil((1 << bitDepth) / absRange);
          
          codeLine = ['this.', anim.el.uniqueID, 'el.', attrName, '=(', numSteps*(vStart+0.5), '+phase*', relRange, ') >> ', bitDepth, ';\n'].join('');
          break;
        case 'clip':
          var lRange = attrValue[0] - vStart[0];
          var tRange = attrValue[1] - vStart[1];
          var wRange = attrValue[2] - vStart[2];
          var hRange = attrValue[3] - vStart[3];
          
          absRange = Math.max(Math.abs(lRange), Math.abs(tRange), Math.abs(wRange), Math.abs(hRange));
          relPhase = Math.ceil((1 << bitDepth) / absRange);
          
          codeLine = ['this.', anim.el.uniqueID, 'elStyle.clip=["rect(", ',
            '((', numSteps*(vStart[0]+0.5), ' + phase*', lRange, ')>> ', bitDepth, '), " " + ',
            '((', numSteps*(vStart[1]+0.5), ' + phase*', tRange, ')>> ', bitDepth, '), " " + ',
            '((', numSteps*(vStart[2]+0.5), ' + phase*', wRange, ')>> ', bitDepth, '), " " + ',
            '((', numSteps*(vStart[3]+0.5), ' + phase*', hRange, ')>> ', bitDepth, '), ")"].join("");\n'].join('');

          break;
      }
      
      // No use to store this code if does nothing.
      if (absRange == 0)
        continue;
      
      // Store in codeByRange array.
      if (!codeByRange[relPhase])
        codeByRange[relPhase] = []
      
      codeByRange[relPhase].push(codeLine);
    }
  }
  
  var sortedIndexes = [];
  for (var relPhase in codeByRange)
    sortedIndexes.push(relPhase);
  sortedIndexes.sort(function (a, b) { return a - b; });

  for (var i = 0; i < sortedIndexes.length; i++)
  {
    this["prevRenderPhase" + sortedIndexes[i]] = 0;
    if (sortedIndexes[i] > 1)
    {
      code.push(
        "if (tPhase >= 1 || phase - this.prevRenderPhase", sortedIndexes[i], " >= ", sortedIndexes[i], "){\n"
      );
    }
    
    code.push(codeByRange[sortedIndexes[i]].join(""));
    
    if (sortedIndexes[i] > 1)
      code.push("this.prevRenderPhase", sortedIndexes[i], "=phase;\n",
      "}\n");
  }

  // Create function
//window.open().document.write('<pre>' + code.join('') + '</pre>');
  this.__run = Function('t', code.join(''));

if (typeof(profiler) != 'undefined')
  profiler.wrapFunction(this, "__run", "Animator");

  if (typeof(shredder) != 'undefined')
  {
    shredder.extend(this);
    this.setRunMode(shredder.RUN_ONCE_REQUIRED);
  }
  else
  {
    var me = this;

    this.stop = function() {
      this.stopped = true;
    };
    this.start = function() {
      this.stopped = false;

      var me = this;
      setTimeout(function(){ me.runAlone(); }, 0);
    };
  }

  this.start();
}

Animator.prototype = {
  profiles: {
    0:'tPhase',
    1:'tPhase*tPhase',
    2:'(2 - tPhase)*tPhase',
    3:'(1 - Math.cos(' + Math.PI + ' * tPhase) / 2.0',
    4:'(tPhase < 0.5 ? Math.exp(3*Math.log(tPhase*2))/2 : 1-Math.exp(3*Math.log((1-tPhase)*2))/2)'
  },
  uid:1,
  __wait: function(t)
  {
    if (t >= this.tStart)
      this.run = this.__run;
  },
  run: function(t)
  {
    this.tStart   = +new Date() + this.delay;
    this.tStop    = this.tStart + this.duration;
    this.tPrev    = this.tStart;
    
    if (t >= this.tStart)
      this.run = this.__run;
    else
      this.run = this.__wait;

  },
  finish: function(t)
  {
    if (this.finished)
      return;

    this.finished = true;
    
    if (this.modifiers.removeAfterwards || this.modifiers.finishAlways)
    {
      for (var i=0; i<this.anims.length; i++)
      {
        var anim = this.anims[i];
        
        for (var attrName in anim.targetState)
        {
          switch (attrName)
          {
            case 'alpha':
//              if (this.modifiers.finishAlways)
//                
              break;
            case 'gradient':
//              if (this.modifiers.finishAlways)
//                
              break;
            case 'scrollTop':
            case 'scrollLeft':
              if (this.modifiers.finishAlways)
                anim.el[attrName] = anim.targetState[attrName];
              break;
            default:
              if (this.modifiers.finishAlways)
                anim.el[this.styleCollectionName][attrName] = anim.targetState[attrName];
              if (this.modifiers.removeAfterwards)
                anim.el[this.styleCollectionName][attrName] = null;
          }
        }
      }
    }
    
    if (this.modifiers.endCode)
      this.modifiers.endCode(this.anims);

    this.stop();
  },
cnt:1,
  runAlone: function()
  {
//window.status = this.cnt++;
    this.run(+new Date());

    if (!this.stopped)
    {
      var me = this;
      setTimeout(function(){ me.runAlone(); }, 0);
    }
  },
  __colors: {  "aqua":"#00FFFF","azure":"#F0FFFF","beige":"#F5F5DC",
  "black":"#000000","blue":"#0000FF","brown":"#A52A2A",
  "cyan":"#00FFFF","darkblue":"#00008B","darkcyan":"#008B8B",
  "darkgray":"#A9A9A9","darkgreen":"#006400","darkred":"#8B0000",
  "fuchsia":"#FF00FF","gold":"#FFD700","gray":"#808080",
  "green":"#008000","indigo":"#4B0082","lightblue":"#ADD8E6",
  "lightcyan":"#E0FFFF","lightgreen":"#90EE90","lightgrey":"#D3D3D3",
  "lightyellow":"#FFFFE0","lime":"#00FF00","magenta":"#FF00FF",
  "maroon":"#800000","navy":"#000080","orange":"#FFA500",
  "pink":"#FFC0CB","purple":"#800080","red":"#FF0000",
  "silver":"#C0C0C0","steelblue":"#4682B4","turquoise":"#40E0D0",
  "violet":"#EE82EE","white":"#FFFFFF","yellow":"#FFFF00"
 },
  __castColor: function(c)
  {
    c = this.__colors[c] || c;
  
    if (typeof(c) == 'object')
      return c;
    if (c.indexOf('#') == 0)
      return [
        parseInt(c.substring(1, 3), 16),
        parseInt(c.substring(3, 5), 16),
        parseInt(c.substring(5, 7), 16)
      ];
    if (c.indexOf('rgb(') == 0)
    {
      var cs = c.substring(4, c.length - 1).split(',');
      return [
        parseInt(cs[0]),
        parseInt(cs[1]),
        parseInt(cs[2])
      ];
    }

    return null;
  },
  getCurrentStyleAttribute: function(el, attrName)
  {
    return this.getAttribute(el, 'currentStyle', attrName);
  },
  getStyleAttribute: function(el, attrName)
  {
    return this.getAttribute(el, 'currentStyle', attrName);
    switch (attrName)
    {
      case 'top':
      case 'right':
      case 'bottom':
      case 'left':
      case 'MozOpacity':
      case 'width':
      case 'height':
        return parseInt(el.style[attrName]);
        break;
      case 'scrollTop':
      case 'scrollLeft':
        return el[attrName];
        break;
      case 'clip':
        return [
          parseInt(el.style.clipTop),
          parseInt(el.style.clipRight),
          parseInt(el.style.clipBottom),
          parseInt(el.style.clipLeft)
        ];
        break;
      case 'alpha':
        return el.filters.item("DXImageTransform.Microsoft.Alpha").opacity;
        break;
      case 'gradient':
        return [
          el.filters.item("DXImageTransform.Microsoft.Gradient").startColorStr,
          el.filters.item("DXImageTransform.Microsoft.Gradient").endColorStr
        ];
        break;
      case 'color':
      case 'backgroundColor':
      case 'borderTopColor':
      case 'borderRightColor':
      case 'borderBottomColor':
      case 'borderLeftColor':
          return this.__castColor(el.style[attrName]);
        break;
    }
  },
  getRuntimeStyleAttribute: function(el, attrName)
  {
    return this.getAttribute(el, 'runtimeStyle', attrName);
  },
  getAttribute: function(el, styleName, attrName)
  {
    switch (attrName)
    {
      case 'top':
      case 'right':
      case 'bottom':
      case 'left':
      case 'MozOpacity':
        if (!document.defaultView)
        {
          var v = parseInt(el[styleName][attrName]);
          if (isNaN(v))
            return 0
          else
            return v;
        }
        else
          return parseInt(document.defaultView.getComputedStyle(el, "").getPropertyValue(attrNameMozilla));
        break;
      case 'width':
        return el.offsetWidth;
        break;
      case 'height':
        return el.offsetHeight;
        break;
      case 'scrollTop':
      case 'scrollLeft':
        return el[attrName];
        break;
      case 'clip':
        if (!document.defaultView)
          return [
            parseInt(el[styleName].clipTop),
            parseInt(el[styleName].clipRight),
            parseInt(el[styleName].clipBottom),
            parseInt(el[styleName].clipLeft)
          ];
        else
          return [
            parseInt(document.defaultView.getComputedStyle(el, "").getPropertyValue('clip-top')),
            parseInt(document.defaultView.getComputedStyle(el, "").getPropertyValue('clip-right')),
            parseInt(document.defaultView.getComputedStyle(el, "").getPropertyValue('clip-bottom')),
            parseInt(document.defaultView.getComputedStyle(el, "").getPropertyValue('clip-left'))
          ];
        break;
      case 'alpha':
        return el.filters.item("DXImageTransform.Microsoft.Alpha").opacity;
        break;
      case 'gradient':
debugger;
        return [
          el.filters.item("DXImageTransform.Microsoft.Gradient").startColorStr,
          el.filters.item("DXImageTransform.Microsoft.Gradient").endColorStr
        ];
        break;
      case 'color':
      case 'backgroundColor':
      case 'borderTopColor':
      case 'borderRightColor':
      case 'borderBottomColor':
      case 'borderLeftColor':
        if (!document.defaultView)
          return this.__castColor(el[styleName][attrName]);
        else
          return this.__castColor(document.defaultView.getComputedStyle(el, "").getPropertyValue(attrNameMozilla));
        break;
    }
  },
  getCalculatedProperty:function(el, prop)
  {
    switch(prop)
    {
      case 'visibility':
//TODO
// geef opacity terug!!!!!!
        while (el.currentStyle[prop] == 'inherit')
        {
          if (el.nodeName.toLowerCase() == 'body')
            return 'visible';
          else
            el = el.parentNode;
        }

        return el.currentStyle[prop];
      case 'display':
        return el.currentStyle[prop];
      case 'opacity':
        return this.__getOpacity(el);
      case "width":
        return el.offsetWidth;
      case "height":
        return el.offsetHeight;
      case "scrollTop":
      case "scrollLeft":
        return el[prop];
      case "color":
      case "backgroundColor":
      case "borderColor":
        return this.__getCalculatedColor(el, prop);
      default:
        var val = parseInt(el.currentStyle[prop]);
        if (isNaN(val))
          return 0;
        else
          return val;
    }
  },
  __getCalculatedColor:function(el, prop)
  {
    v = el.currentStyle[prop];

    for (var el2 = el; (v == 'transparent') && el2; el2 = el2.parentNode)
      v = el2.currentStyle[prop];

    v = this.__colors[v.toLowerCase()] || v;

    if (v.indexOf('rgb(') == 0)
    {
      cs = v.substring(4, v.length-1).split(',');
      v = this.__RGBtoHex(parseInt(cs[0]), parseInt(cs[1]), parseInt(cs[2]));
    }

    if (v.charAt(0) != "#")
      v = "#ffffff";

    return v;
  }
};





function Modifiers()
{
  this.delay = 0;
  this.duration = 300;
  this.profile = this.SLOWFASTSLOW;
  this.useRuntimeStyle = false;
  this.removeAfterwards = false;
  this.finishAlways = false;
  this.endCode = null;
}

Modifiers.prototype = {
  LINEAR:0,
  ACCERATING:1,
  DECELERATING:2,
  NORMAL:3,
  SLOWFASTSLOW:4
};

var eventCenter = {
  debugNextEvent: false,
  lastClickEvent:null,
  activate: function()
  {
    this.active = true;
  },
  deactivate: function()
  {
    this.active = false;
  },
  __attachedEvents:[],
  attachEvent: function(el, evtType, hnd, isPublic)
  {
    // for IE
    if (el.attachEvent)
    {
      var type = evtType.toLowerCase();

      var hndWrapper = function(evt)
      {
        var result = hnd.call(el, evt);
        if (result === false)
        {
          evt.returnValue = false;
          evt.cancelBubble = true;
        }
        else if (isPublic === true && window.parentDoc)
        {
          //debugger;
          //alert(element.ownerDocument.getElementById('pe' + evtType.substring(2).toLowerCase()));
          var evtSpawner = 'pe' + evtType.substring(2).toLowerCase();
          var evtObject = parentDoc.createEventObject(evt);
          
          parentDoc.getElementById(evtSpawner).fire(evtObject);
          
          if (evt.keyCode != evtObject.keyCode) {
            evt.keyCode = evtObject.keyCode;
          };
          //} catch (e) {};

          evt.cancelBubble = evtObject.cancelBubble;
          result = evt.returnValue = evtObject.returnValue;
        }
        
        return result;
      };

      el.attachEvent(type, hndWrapper);
    }

    // for Mozilla / Netscape
    else if (el.addEventListener)
    {
      var type = evtType.substring(2).toLowerCase();
      
      var hndWrapper = function(nnevent)
      {
        var evt = {
          type:type,
          srcElement:nnevent.target,
          currentTarget:nnevent.currentTarget,
          x:nnevent.clientX,
          y:nnevent.clientY,
          clientX:nnevent.clientX,
          clientY:nnevent.clientY,
          keyCode:nnevent.keyCode,
          charCode:nnevent.charCode,
          button:{'0':1,'1':4,'2':2}[nnevent.button],
          ctrlKey:nnevent.ctrlKey,
          shiftKey:nnevent.shiftKey,
          altKey:nnevent.altKey,
          fromElement:nnevent.relatedTarget,
          toElement:nnevent.relatedTarget,
          nnevent:nnevent
        };
        window.event = evt;

        var result = hnd.call(el, evt);
        if (result === false)
        {
          nnevent.preventDefault();
          nnevent.stopPropagation();
        }
        if (evt.cancelBubble)
        {
          nnevent.stopPropagation();
        }
        return result;
      };
      el.addEventListener(type, hndWrapper, false);
    }
  }
};

var selection = {
  
  range: null,
  curCol: 0,
  curX: 0,
  curLine: null,
  cursorVisible: false,
  rangeSelectionVisible: false,
  imeEnabled: false,

  setRange: function(rng, className)
  {
    this.range = rng;
    this.show(className);
  },
  
  getRange: function()
  {
    return this.range.duplicate();
  },
  
  show: function(className)
  {
//    codeArea.focus();
    
    this.curCol = this.range.endCol;
    if (this.curLine != this.range.endLine)
    {
      this.curLine = this.range.endLine;
      this.curLineNumber = this.curLine.getLineNumber();
    }
    this.curClassName = className;
    
    this.positionCursor();
    if (this.range.isCollapsed())
    {
      this.showCursor();
      if (this.imeEnabled)
        this.curLine.showIMEInput(this.curCol);
    }
    else
    {
      this.hideCursor();

      var oldSelection = this.rangeSelectionVisible;
      this.rangeSelectionVisible = this.range.duplicate();
      this.rangeSelectionVisible.drawSelection(className, oldSelection);
    }
  },
  
  hide: function()
  {
    if (this.rangeSelectionVisible)
    {
      this.rangeSelectionVisible.hideSelection();
      this.rangeSelectionVisible = false;
    }
  },
  
  replaceContent: function(s)
  {
    this.hide();
    this.range.replaceContent(s);
    this.show();
  },

  updateFromBrowser: function(isReverse)
  {
    var rng = crossBrowser.getSelectionRange();
    if (!rng) return;
    
    var cRng = new CodeRange();
    cRng.moveToTextRange(rng, isReverse);
    this.setRange(cRng);
  },
  
  moveCursorUp: function(modifiers)
  {
    this._moveCursorUp(modifiers);
    this.show();
  },
  
  _moveCursorUp: function(modifiers)
  {
    return this._moveCursorLine(this.curLine.prevLine, this.curLineNumber - 1, modifiers.shiftKey);
  },

  moveCursorDown: function(modifiers)
  {
    this._moveCursorDown(modifiers);
    this.show();
  },
  
  _moveCursorDown: function(modifiers)
  {
    return this._moveCursorLine(this.curLine.nextLine, this.curLineNumber + 1, modifiers.shiftKey);
  },
  
  moveCursorLeft: function(modifiers)
  {
    this._moveCursorLeft(modifiers);
    this.show();
  },
  
  _moveCursorLeft: function(modifiers)
  {
    var chars = this.curLine.getValue();
    var l = chars.length;
    if (this.curCol > l)
      this.curCol = l;
    if (this.curCol == 0)
    {
      if (!this._moveCursorUp(modifiers))
        return false;
      chars = this.curLine.getValue();
      this.curCol = chars.length;
    } 
    else
    {
      this.curCol--;
    }
    if (modifiers.ctrlKey)
    {
      if (chars.length != 0 && this.curCol == chars.length)
        this.curCol--;
      var n = this.curCol;
      while (n)
      {
        var pos = this.curLine.getNextCtrlStop(n);
        if (pos <= this.curCol)
          break;
        n--;
      }
      this.curCol = n ? pos : 0;
    }
    return this._moveCursorCol(this.curCol, modifiers.shiftKey);
  },
  
  moveCursorRight: function(modifiers)
  {
    this._moveCursorRight(modifiers);
    this.show();
  },
  
  _moveCursorRight: function(modifiers)
  {
    if (this.curCol >= this.curLine.getLength())
    {
      if (modifiers.stayOnLine || !this._moveCursorDown(modifiers))
        return false;
      this.curCol = 0;
    } 
    else
    {
      this.curCol++;
    }
    if (modifiers.ctrlKey)
      this.curCol = this.curLine.getNextCtrlStop(this.curCol||1);
    return this._moveCursorCol(this.curCol, modifiers.shiftKey);
  },
  
  moveCursorHome: function(modifiers)
  {
    this._moveCursorHome(modifiers);
    this.show();
  },
  
  _moveCursorHome: function(modifiers)
  {
    if (modifiers.ctrlKey)
      this._moveCursorLine(codeArea.BOF.nextLine, 1, modifiers.shiftKey);
    var homeCol = this.curLine.getSmartHomeCol();
    if (homeCol == this.curCol)
      homeCol = 0;
    return this._moveCursorCol(homeCol, modifiers.shiftKey);
  },
  
  moveCursorEnd: function(modifiers)
  {
    this._moveCursorEnd(modifiers);
    this.show();
  },
  
  _moveCursorEnd: function(modifiers)
  {
    if (modifiers.ctrlKey)
      this._moveCursorLine(codeArea.EOF.prevLine, this.lineCount, modifiers.shiftKey);
    var endCol = Infinity;
    if (endCol == this.curCol)
      endCol = this.curLine.getSmartEndCol();
    return this._moveCursorCol(endCol, modifiers.shiftKey);
  },
  
  moveCursorPageUp: function(modifiers)
  {
    this._moveCursorPageUp(modifiers);
    this.show();
  },
  
  _moveCursorPageUp: function(modifiers)
  {
    var lines = Math.round(comp.body.clientHeight / codeArea.oneCharHeight);
//    codeArea.scrollIntoViewY(comp.body.scrollTop - lines * codeArea.oneCharHeight, 0);
    comp.body.scrollTop -= lines * codeArea.oneCharHeight;
    for (; lines; lines--)
      this._moveCursorUp(modifiers);
  },

  moveCursorPageDown: function(modifiers)
  {
    this._moveCursorPageDown(modifiers);
    this.show();
  },
  
  _moveCursorPageDown: function(modifiers)
  {
    var lines = Math.round(comp.body.clientHeight / codeArea.oneCharHeight);
    comp.body.scrollTop += lines * codeArea.oneCharHeight;
//    codeArea.scrollIntoViewY(comp.body.scrollTop + lines * codeArea.oneCharHeight, 0);
    for (; lines; lines--)
      this._moveCursorDown(modifiers);
  },
  
  moveCursorToPoint: function(clientX, clientY, endOnly)
  {
    var line = codeArea.getLineFromClientY(clientY);
    var col = codeArea.getColFromClientX(clientX, line);
    
    this._moveCursorLine(line, line.getLineNumber(), endOnly);
    this._moveCursorCol(col, endOnly);
    
    this.show();
  },
  
  _moveCursorLine: function(line, lineNumber, endOnly)
  {
    if (line == codeArea.BOF || line == codeArea.EOF)
      return false;

    // update col for non fixed width characters
    if (this.curX)
    {
      var col = line.getColFromX(this.curX);
      if (col != this.curCol)
      {
        this.curCol = col;
        this.range.moveCol(col, endOnly);
      }
    }

    this.curLine = line;
    this.curLineNumber = lineNumber;
    this.range.moveLine(this.curLine, endOnly);
    this.showCurPos();
    return true;
  },
  
  _moveCursorCol: function(col, endOnly)
  {
    this.curCol = col;
    this.curX = this.curLine.getXFromCol(col);
    this.range.moveCol(this.curCol, endOnly);
    this.showCurPos();
    return true;
  },

  selectAll: function()
  {
    this._moveCursorHome({ctrlKey:true});
    this._moveCursorEnd({ctrlKey:true,shiftKey:true});
    this.show();
  },
  
  indent: function()
  {
    this.range.indent();
    this.show();
  },
  
  unindent: function()
  {
    this.range.unindent();
    this.show();
  },
 
  showCurPos: function()
  {
    // all code for debugging only
    var col = this.curLine.getLength()+1;
    if (this.curCol+1 <= col)
      col = this.curCol+1;
    else
      col = ">" + col;
    //window.status = this.curLineNumber + " " + col;
  },
  
  positionCursor: function()
  {
    codeArea.positionCursorLine(this.curLine);
    codeArea.positionCursorCol(this.range.isLineRange ? 0 : this.curCol, this.curLine);
  },
  
  updateCursor: function()
  {
    // reset blinking
    this.cursorBlinkState = 1;
    this.doCursorBlink();
  },
  
  showCursor: function()
  {
    this.updateCursor();
    
    if (this.rangeSelectionVisible)
    {
      this.rangeSelectionVisible.hideSelection();
      this.rangeSelectionVisible = false;
    }

    this.cursorVisible = true;
    this.showCursorState();
  },
  
  hideCursor: function()
  {
    this.cursorVisible = false;
    this.showCursorState();
  },
  
  showCursorState: function()
  {
    codeArea.cursor.style.display = this.cursorVisible && codeArea.hasFocus && !codeArea.readOnly
      ? 'block' : 'none';
  },

  doCursorBlink: function()
  {
    // no this
    
    clearTimeout(selection.doCursorBlinkTO);
    codeArea.cursor.style.visibility = 
      selection.cursorBlinkState ? "visible" : "hidden";
    selection.cursorBlinkState = 1 - selection.cursorBlinkState;

    /*var height = 1.2 * Math.min(1, Math.max(0, Math.sin(new Date / 500 * Math.PI)));
    codeArea.cursor.style.height = height + "em";
    codeArea.cursor.style.marginTop = (1.2 - height) + "em";

    var opacity = Math.min(1, Math.max(0, Math.sin(new Date / 500 * Math.PI)));
    codeArea.cursor.style.opacity = opacity;*/

    selection.doCursorBlinkTO = setTimeout(selection.doCursorBlink, 500);
  },
  
  switchIMEInput: function()
  {
    this.imeEnabled = !this.imeEnabled;
    if (this.imeEnabled)
      this.curLine.showIMEInput(this.curCol);
    return false;
  }
  
};
function Action(o)
{
  this.auxAction = o.auxAction;
  this.undoAction = o.undoAction;
  this.canCombine = o.canCombine;
  
  this._perform = o._perform;
  this.isPerformValid = o.isPerformValid;
  
  var me = this;
  this.perform = function()
  {
    if (!me.auxAction)
    {
      if (codeArea.readOnly)
        return;
      if (!me.undoAction)
        undoStack.addState(me);
    }
    return me._perform.apply(me, arguments);
  };
};
Action.prototype.isEnabled = function()
{
  return (!this.isPerformValid || this.isPerformValid()) && (this.auxAction || !codeArea.readOnly);
};

var actions = {
  typing: 
    new Action({
      canCombine: true,
      _perform: function(chr)
      {
        if (codeArea.overWrite && selection.range.isCollapsed())
          selection._moveCursorRight({shiftKey:true, stayOnLine:true});
        selection.replaceContent(chr);
      }
    }),
  
  deleteKey: 
    new Action({
      canCombine: true,
      _perform: function(evt)
      {
        if (selection.range.isCollapsed())
          selection._moveCursorRight({shiftKey:true, ctrlKey: evt.ctrlKey, altKey: evt.altKey});
        selection.replaceContent("");
      }
    }),
  
  backspace: 
    new Action({
      canCombine: true,
      _perform: function(evt)
      {
        if (selection.range.isCollapsed())
          selection._moveCursorLeft({shiftKey:true, ctrlKey: evt.ctrlKey, altKey: evt.altKey});
        selection.replaceContent("");
      }
    }),
  
  newLine: 
    new Action({
      _perform: function()
      {
        selection.replaceContent("\n");
      }
    }),
  
  indent: 
    new Action({
      _perform: function()
      {
        selection.indent();
      }
    }),
  
  unindent: 
    new Action({
      _perform: function()
      {
        selection.unindent();
      }
    }),
  
  cut: 
    new Action({
      _perform: function()
      {
        actions.copy._perform();
        selection.replaceContent("");
        if (window.clipboardData)
          return false;

        // hide cursor after cut
        setTimeout(function () {
          codeArea.focus();
        }, 0);

        // let Ctrl-X pass through
        return true;
      }
    }),
  
  copy: 
    new Action({
      auxAction: true,
      _perform: function()
      {
        var value = selection.getRange().getContent();
        if (window.clipboardData)
        {
          window.clipboardData.setData("Text", value);
          return false;
        }

        // put the value in a textarea
        codeArea.clipboard.focus();
        codeArea.clipboard.value = value;
        codeArea.clipboard.setSelectionRange(0, codeArea.clipboard.textLength);
        setTimeout(function () {
          codeArea.focus();
        }, 0);
        
        // let Ctrl-C pass through
        return true;
      }
    }),
  
  paste: 
    new Action({
      _perform: function()
      {
        if (window.clipboardData)
        {
          var text = window.clipboardData.getData("Text");
          text = text.replace(/\r\n?/g, '\n');
          selection.replaceContent(text);
          return false;
        }
        
        // get the value from the textarea
        codeArea.clipboard.readOnly = false;
        codeArea.clipboard.style.visibility = "visible";
        codeArea.clipboard.focus();
        codeArea.clipboard.style.visibility = "hidden";
        setTimeout(function () {
          codeArea.clipboard.readOnly = true;
          codeArea.clipboard.setSelectionRange(0, codeArea.clipboard.textLength);
          var text = codeArea.clipboard.value;
          text = text.replace(/\r\n?/g, '\n');
          selection.replaceContent(text);
          codeArea.focus();
        }, 0);
        
        // let Ctrl-V pass through
        return true;
      }
    }),
  
  undo:
    new Action({
      undoAction: true,
      _perform: function()
      {
        undoStack.undo();
        
        // Don't pass Ctrl-Z
        return false;
      },
      isPerformValid: function()
      {
        return codeArea.canUndo;
      }
    }),
  
  redo:
    new Action({
      undoAction: true,
      _perform: function()
      {
        undoStack.redo();

        // Don't pass Ctrl-Y
        return false;
      },
      isPerformValid: function()
      {
        return codeArea.canRedo;
      }
    })
};

function Line(nextLine, value)
{
  this.id = "l" + Line.idCounter++;
  
  if (codeArea.isInitial)
  {
    this._value = value || "";
    this.init(nextLine, value || "");
  }
  else
  {
    undoStack.currentState.addCreatedLine(this);
    this.init(nextLine, " "); 
    this.setValue(value || "");
  }
}
Line.idCounter = 0;
Line.INIT_HTML = "<font>\u00a0</font><span>\u00a0</span>\u00a0";

Line.prototype.init = function(nextLine, value)
{
  codeArea.lines[this.id] = this;
//if (Line.idCounter < 300)
  codeArea.updateLineCount(1);
  
  this.prevLine = nextLine.prevLine;
  nextLine.prevLine.nextLine = this;
  this.nextLine = nextLine;
  nextLine.prevLine = this;

  this.createHTML(nextLine, value);
};

Line.prototype.createHTML = function(nextLine, value)
{
  var div = document.createElement("div");
  div.id = this.id;
  this.div = div;
  this.div.innerHTML = Line.INIT_HTML;
  
  codeArea.canvas.insertBefore(div, nextLine.div);
  
  this._setValue(value);

  //this.selectionDiv = codeArea.selectionPlane.appendChild(document.createElement("div"));
  //this.selectionClassName = "";
};

Line.prototype.del = function()
{
  this._del();

  undoStack.currentState.addDeletedLine(this);
};

Line.prototype._del = function()
{
  delete codeArea.lines[this.id];
  codeArea.updateLineCount(-1);
  
  this.prevLine.nextLine = this.nextLine;
  this.nextLine.prevLine = this.prevLine;
  
  /*this.div.style.position = 'fixed';
  this.div.style.top = "-1000px";
  this.div.style.left = "-1000px";*/
  codeArea.canvas.removeChild(this.div);

  delete this.div;
};

Line.prototype.undel = function()
{
  this.init(this.nextLine, this._value);
};

Line.prototype.getValue = function()
{
  return this._value;
};

Line.prototype.setValue = function(v)
{
  undoStack.currentState.addChangeValue({
    line: this, 
    oldValue: this._value, 
    newValue: v
  });

  return this._setValue(v);
};

Line.prototype._setValue = function(v)
{
  this._value = v;
  this.selectionStartCol = -1;
  
  this.div.firstChild.firstChild.nodeValue = v.replace(/([\s\u00a0]*)(.*)/, "$1");
  this.div.firstChild.nextSibling.firstChild.nodeValue = v.replace(/([\s\u00a0]*)(.*)/, "$2");
  //this.div.innerText = v;
  
  //this.div.innerHTML = v.replace(/&/g,'&amp;').replace(/</g, '&lt;').replace(/ /g, '\u00a0').replace(/([\s\u00a0]*)(.*)/, "<font>$1</font><span>$2</span>\u00a0");
  if (!codeArea.isInitial)
    this.calculateGlyphWidths();
  return v;
};

Line.prototype.getLength = function()
{
  return this._value.length;
};

Line.prototype.getIndentSize = function(defaultToLength)
{
  var m = this._value.match(/^(\s*)\S/);
  if (!m) // empty line
    return defaultToLength ? this.getLength() : null;
  return m[1].length;
};

Line.prototype.getSmartHomeCol = function()
{
  var homeCol = this.getIndentSize();
  if (homeCol === null)
    homeCol = Math.min(this.getLength(), this.prevLine.getSmartHomeCol());
  return homeCol;
};

Line.prototype.getSmartEndCol = function()
{
  var m = this._value.match(/^(.*\S)\s*$/);
  if (!m) // empty line
    return Math.min(this.getLength(), this.prevLine.getSmartHomeCol());
  return m[1].length;
};

Line.prototype.getNextCtrlStop = function(start)
{
  var match = this._value.match(new RegExp(".{" + start + "}\\b\\s*"));
  return match ? match.index + match[0].length : this._value.length;
};

Line.prototype.findNext = function(res, start)
{
  var re = res[0];
  re.lastIndex = start;
  var m = re.exec(this._value);
  return this.handlePartialFind(m, res);
};

Line.prototype.findPrevious = function(res, start)
{
  var re = res[0];
  re.lastindex = 0;
  var m;
  for (var testM; (testM = re.exec(this._value)) && testM.index <= start; re.lastIndex = testM.index + 1)
    m = testM;
  return this.handlePartialFind(m, res);
};

Line.prototype.handlePartialFind = function(m, res)
{
  if (!m)
    return false;
  if (res.length == 1)
    return new CodeRange().moveToLine(this, m.index, m[0].length);
  var followingRange = this.nextLine.findNext(res.slice(1), 0);
  if (!followingRange)
    return false;
  followingRange.startLine = this;
  followingRange.startCol = m.index;
  return followingRange;
}

// delete from startCol to endCol
// create new lines for \n in values
Line.prototype.replaceContent = function(s, startCol, endCol)
{
  if (!endCol && endCol!=0)
    endCol = this.getLength();
  startCol = startCol || 0;

  var newVal = this._value.substr(0, startCol) + s;
  var lines = newVal.split("\n");
  
  var lastIndex = lines.length - 1;
  var cursorCol = lines[lastIndex].length;
  lines[lastIndex] += this._value.substr(endCol);
  
  var lastLine = this; // possibly updated later
  if (lastIndex > 0) // multiple lines pasted
  {
    // calculate indentation insertion
    var startIndentSize = this.getIndentSize(true);
    var indentInsertionCount = Math.min(startCol, startIndentSize);
    var indentInsertionValue = this._value.substr(0, indentInsertionCount);
    cursorCol += indentInsertionCount;

    // create new lines
    for (var i=1; i<lines.length; i++)
      lastLine = new Line(lastLine.nextLine, indentInsertionValue + lines[i]);
  };

  this.setValue(lines[0]);
  
  return {
    line: lastLine,
    col: cursorCol
  };
};

Line.prototype.getLineNumber = function()
{
  var l = this;
  var nr = 0;
  while (l != codeArea.BOF)
  {
    l = l.prevLine;
    nr++;
  }
  return nr;
};

Line.prototype.isPreceding = function(otherLine)
{
  return crossBrowser.isPreceding(this.div, otherLine.div);
};

Line.prototype.addClass = function(className)
{
  crossBrowser.addClass(this.div, className);
};

Line.prototype.removeClass = function(className)
{
  crossBrowser.removeClass(this.div, className);
};

Line.prototype.drawSelection = function(startCol, endCol, useElement, indentRemovalRE)
{
  var className = "";
  if (endCol > this.getLength())
  {
    endCol = this.getLength() + 0.5;
    className = "linebreak-selected";
  }
  
  if (indentRemovalRE)
  {
    var m = this._value.match(indentRemovalRE);
    if (startCol < m[0].length)
      startCol = m[0].length;
  }
  
  if (startCol > endCol)
    return;
/*  
  if (className != this.selectionClassName)
  {
    this.selectionDiv.className = className;
    this.selectionClassName = className;
  }

  if (this.selectionHidden !== false)
  {
    if (this.selectionHidden)
      this.selectionDiv.style.display = "block";
    this.selectionHidden = false;
    this.selectionDiv.style.top = this.div.offsetTop;
    this.selectionDiv.style.height = codeArea.oneCharHeight;
  }

  if (this.selectionStartCol != startCol || this.selectionEndCol != endCol)
  {
    this.selectionDiv.style.width = this.getXFromCol(endCol) - this.getXFromCol(startCol);
    this.selectionDiv.style.left = this.getXFromCol(startCol);
    this.selectionStartCol = startCol;
    this.selectionEndCol = endCol;
  }*/
  
  if (useElement == null)
  {
    if (this.selectionHidden !== false)
      this.addClass("selected");

    if (startCol != this.selectionStartCol)
    {
      this.selectionStartCol = startCol;
      var indentX = this.getXFromCol(this.getIndentSize(true));
      var startX = this.getXFromCol(startCol);
      var extraSel = (indentX - startX) + 'px';
      var span = this.div.firstChild;
      span.style.borderRightWidth = extraSel;
      span.style.marginLeft = "-" + extraSel;
    }
    this.selectionHidden = false;
  }
  else
  {
    var width = this.getXFromCol(endCol) - this.getXFromCol(startCol);
    if (width > 0)
    {
      useElement.style.display = "block";
      useElement.style.top = this.div.offsetTop;
      useElement.style.height = codeArea.oneCharHeight;
      useElement.style.width = width;
      useElement.style.left = this.getXFromCol(startCol);
      useElement.className = className;
    } 
    else
      useElement.style.display = "none";
    this.hideSelection();
  }
    
};

Line.prototype.hideSelection = function()
{
  if (!this.selectionHidden)
  {
    //this.selectionDiv.style.display = "none";
    this.removeClass("selected");
    this.selectionHidden = true;
    var span = this.div.firstChild;
    span.style.borderRightWidth = span.style.marginLeft = "0px";
    this.selectionStartCol = -1;
  }
};

Line.prototype.scrollIntoView = function()
{
  codeArea.scrollIntoViewX(0, this.getXFromCol(this.getLength()));
  codeArea.scrollIntoViewYAnimated(this.div.offsetTop, codeArea.oneCharHeight);
};

var hasTabOrNonAscii = /[\t\u0100-\uffff]/;
Line.prototype.calculateGlyphWidths = function()
{
  this.totalWidth = [];
  this.isSuspicious = hasTabOrNonAscii.test(this._value);
  if (!this.isSuspicious)
    return;

  /*var rng = crossBrowser.createRange();
  rng.setEndByChars(this.div, 0);
  rng.setStartByChars(this.div, 0);
  for (var i=0; i <= this._value.length; i++)
  {
    rng.setEndByChars(this.div, i);
    this.totalWidth[i] = rng.getBoundingWidth() / codeArea.oneCharWidth;
  }*/
  
  var span = document.createElement("span");
  codeArea.cursors.insertBefore(span, codeArea.cursors.firstChild);
  span.innerText = '\u00A0';
  for (var i=0; i <= this._value.length; i++)
  {
    span.firstChild.nodeValue = this._value.substr(0, i);
    this.totalWidth[i] = span.offsetWidth / codeArea.oneCharWidth;
  }
  codeArea.cursors.removeChild(span);
  this.totalWidth[i - 0.5] = this.totalWidth[i - 1] + 0.5;
};

Line.prototype.getColFromX = function(x)
{
  if (!this.totalWidth)
    this.calculateGlyphWidths();

  var col = x / codeArea.oneCharWidth;
  if (this.isSuspicious)
  {
    var minDist = Infinity;
    var length = this.getLength();
    for (var i=0; i<length; i++)
    {
      var dist = Math.abs(col - this.totalWidth[i]);
      if (dist > minDist)
        return i-1;
      minDist = dist;
    }
    return length;
  }
  else
  {
    return Math.min(this.getLength()+0.5, Math.max(0, Math.round(col)));
  }
};

Line.prototype.getXFromCol = function(col)
{
  if (!this.totalWidth)
    this.calculateGlyphWidths();

  col = Math.min(col, this.getLength());
  return (this.isSuspicious ? this.totalWidth[col] : col) * codeArea.oneCharWidth;
};

Line.prototype.showIMEInput = function(col)
{
  this.div.innerHTML = "<input>";
  var input = this.div.firstChild;
  input.readOnly = codeArea.readOnly;
  input.value = this._value;
  input.focus();
  input.style.width = codeArea.canvas.offsetWidth  + 'px';
  input.style.imeMode = "active";
  eventCenter.attachEvent(input, "onblur", onIMEInputBlur);
  eventCenter.attachEvent(input, "onkeydown", onIMEInputKeyDown);
  eventCenter.attachEvent(input, "onkeypress", onIMEInputKeyPress);
  crossBrowser.setInputSelectionRange(input, col, col);
};

Line.prototype.hideIMEInput = function(imeInputValue)
{
  this.div.innerHTML = Line.INIT_HTML;
  this.setValue(imeInputValue);
};

Line.prototype.toString = function()
{
  return "Line " + this.getLineNumber() + ": " + this.getValue();
};
function CodeRange() { 
  this.endLine = this.startLine = codeArea.BOF.nextLine;
  this.endCol = this.startCol = 0;
};

CodeRange.MOVE_END_ONLY = true;

CodeRange.prototype.equals = function(cr)
{
  return this.startCol == cr.startCol &&
         this.endCol == cr.endCol &&
         this.startLine == cr.startLine &&
         this.endLine == cr.endLine;
};

CodeRange.prototype.isCollapsed = function()
{
  return this.startLine == this.endLine && this.endCol == this.startCol;
};

CodeRange.prototype.collapse = function(toStart)
{
  if (toStart)
  {
    this.endLine = this.startLine;
    this.endCol = this.startCol;
  }
  else
  {
    this.startLine = this.endLine;
    this.startCol = this.endCol;
  }
  
  return this;
};

CodeRange.prototype.select = function(className)
{
  selection.setRange(this, className);
  return this;
};

// create range from start of line to end of line
CodeRange.prototype.moveToLine = function(l, startCol, length)
{
  this.endLine = this.startLine = l;
  if (isNaN(startCol))
  {
    this.startCol = 0;
    this.endCol = Infinity;
    this.isLineRange = true;
  }
  else
  {
    this.startCol = startCol;
    this.endCol = startCol + (length || 0);
  }
  return this;
};

CodeRange.prototype.moveToTextRange = function(rng, isReverse)
{
  if (!rng.duplicate)
    return;
  var startPos = this.getPos(rng, true ^ isReverse);
  if (!startPos)
    return;
  var endPos = this.getPos(rng, false ^ isReverse);
  if (!endPos)
    return;
  this.startLine = startPos.line;
  this.startCol = startPos.col;
  this.endLine = endPos.line;
  this.endCol = endPos.col;
  return this;
};

CodeRange.prototype.createTextRange = function()
{
  var rng = crossBrowser.createRange();
  rng.setEndByChars(this.endLine.div, this.endCol);
  rng.setStartByChars(this.startLine.div, this.startCol);
  return rng;
};  

CodeRange.prototype.getPos = function(rng, startOfRng)
{
  rng = rng.duplicate();
  rng.collapse(startOfRng);
  
  // find line div
  var parent = rng.getParentElement();
  while (parent && parent.parentNode!=codeArea.canvas)
    parent = parent.parentNode;
  if (!parent)
    return;
  
  // find col
  rng.setStartByChars(parent, 0);
  return {
    line : codeArea.lines[parent.id],
    col : rng.getText().length
  };
};

// only change line values
CodeRange.prototype.moveLine = function(line, endOnly)
{
  this.endLine = line;
  if (!endOnly)
  {
    this.startLine = line;
    this.startCol = this.endCol;
  }
  return this;
};

// only change col values
CodeRange.prototype.moveCol = function(col, endOnly)
{
  this.isLineRange = false;
  this.endCol = col;
  if (!endOnly)
  {
    this.startLine = this.endLine;
    this.startCol = col;
  }
  return this;
};

CodeRange.prototype.duplicate = function()
{
  var cr = new CodeRange();
  cr.startLine = this.startLine;
  cr.endLine = this.endLine;
  cr.startCol = this.startCol;
  cr.endCol = this.endCol;
  cr.isLineRange = this.isLineRange;
  return cr;
};

CodeRange.prototype.setStartBeforeEnd = function()
{
  if (this.endLine.isPreceding(this.startLine) ||
    (this.endLine == this.startLine && this.endCol < this.startCol))
  {
    var l = this.endLine;
    this.endLine = this.startLine;
    this.startLine = l;
    
    var c = this.endCol;
    this.endCol = this.startCol;
    this.startCol = c;
  }
  return this;
};

CodeRange.prototype.fixInfinitEnd = function()
{
  if (this.endCol == Infinity)
  {
    if (this.startCol == Infinity && this.startLine == this.endLine)
      this.startCol = this.endCol = this.startLine.getLength();
    else
    {
      if (this.endLine.nextLine == codeArea.EOF)
      {
        this.endCol = this.endLine.getLength();
      }
      else
      {
        this.endCol = 0;
        this.endLine = this.endLine.nextLine;
      }
    }
  }
  return this;
};

CodeRange.prototype.fixEmptyEnds = function()
{
  if (!this.isCollapsed())
  {
    if (this.startCol >= this.startLine.getLength())
    {
      this.startCol = 0;
      this.startLine = this.startLine.nextLine;
    }
    if (this.endCol == 0)
    {
      this.endCol = Infinity;
      this.endLine = this.endLine.prevLine;
    }
  }
  return this;
};

CodeRange.prototype.replaceContent = function(s)
{
  this.setStartBeforeEnd();
  this.fixInfinitEnd();
  if (this.startLine == this.endLine)
    var newPos = this.startLine.replaceContent(s, this.startCol, this.endCol);
  else
  {
    // Remember text on the last line after the end of the selection.
    var extraFromLastLine = this.endLine.getValue().substr(this.endCol);
    
    // Remove all selected lines except the start line.
    var l = this.startLine;
    do
    {
      l = l.nextLine;
      l.del();
    } 
    while (l != this.endLine);
    
    // Insert new value.
    var newPos = this.startLine.replaceContent(s, this.startCol);
    
    // Add old text from last line
    newPos.line.setValue(newPos.line.getValue() + extraFromLastLine);
  }
  
  this.endLine = this.startLine = newPos.line;
  this.endCol = this.startCol = newPos.col;
  return this;
};

CodeRange.prototype.getContent = function()
{
  this.setStartBeforeEnd();
  this.fixInfinitEnd();
  if (this.startLine == this.endLine)
    return this.startLine.getValue().substring(this.startCol, this.endCol);
  else
  {
    // calculate indentation removal
    var startIndentSize = this.startLine.getIndentSize(true);
    var indentRemovalCount = Math.min(this.startCol, startIndentSize);
    var indentRemovalRE = new RegExp("^\\s{0,"+indentRemovalCount+"}");
    
    var a = [this.startLine.getValue().substr(this.startCol)];
    var l = this.startLine;
    do
    {
      l = l.nextLine;
      var v = l.getValue();
      a.push(v.replace(indentRemovalRE, ""));
    }
    while (l != this.endLine);
    a[a.length-1] = a[a.length-1].substr(0, this.endCol - indentRemovalCount);
    return a.join("\n");
  }
};

CodeRange.prototype.indent = function()
{
  this.setStartBeforeEnd();
  this.fixEmptyEnds();

  var indentInsertionValue = new Array(codeArea.settings.indentSize+1).join(" ");
  for (var l = this.startLine; l.prevLine != this.endLine; l = l.nextLine)
    l.setValue(indentInsertionValue + l.getValue());

  if (!this.isCollapsed())
  {
    this.startCol = 0;
    this.endCol = Infinity;
  }
  else
  {
    this.startCol += codeArea.settings.indentSize;
    this.endCol += codeArea.settings.indentSize;
  }
  return this;
};

CodeRange.prototype.unindent = function()
{
  this.setStartBeforeEnd();
  this.fixEmptyEnds();

  var indentRemovalRE = new RegExp("^\\s{0,"+codeArea.settings.indentSize+"}");
  for (var l = this.startLine; l.prevLine != this.endLine; l = l.nextLine)
    l.setValue(l.getValue().replace(indentRemovalRE, ""));

  if (!this.isCollapsed())
  {
    this.startCol = 0;
    this.endCol = Infinity;
  }
  else
  {
    // LAURENS: Make sure we never have a negative startCol or endCol.
    this.startCol = Math.max(0, this.startCol - codeArea.settings.indentSize);
    this.endCol   = Math.max(0, this.endCol - codeArea.settings.indentSize);
  }
  return this;
};

CodeRange.prototype.drawSelection = function(className, currentSelection)
{
  codeArea.selectionPlane.className = className || "";

  this.setStartBeforeEnd();
  this.fixInfinitEnd();
  
  // check select all
  if (this.startLine == codeArea.BOF.nextLine && this.startCol == 0 &&
    this.endLine == codeArea.EOF.prevLine && this.endCol == this.endLine.getLength())
  {
    if (currentSelection)
      currentSelection.hideSelection();
    codeArea.canvas.className = "selectAll";
    this.isSelectAll = true;
    return;
  }
  
  if (currentSelection)
  {
    if (currentSelection.isSelectAll)
      codeArea.canvas.className = "";
    else
    {
      if (currentSelection.startLine.isPreceding(this.startLine))
        for (var l = currentSelection.startLine; l != this.startLine; l = l.nextLine)
          l.hideSelection();

      if (this.endLine.isPreceding(currentSelection.endLine))
        for (var l = this.endLine; l != currentSelection.endLine; l = l.nextLine)
          l.nextLine.hideSelection();
    }
  }

  if (this.startLine == this.endLine)
  {
    this.startLine.drawSelection(this.startCol, this.endCol, codeArea.startOfSelection);
    codeArea.endOfSelection.style.display = "none";
  }
  else
  {
    // calculate indentation removal
    var startIndentSize = this.startLine.getIndentSize(true);
    var indentRemovalCount = Math.min(this.startCol, startIndentSize);
    var indentRemovalRE = new RegExp("^\\s{0,"+indentRemovalCount+"}");
    
    this.startLine.drawSelection(this.startCol, Infinity, codeArea.startOfSelection);
    var l = this.startLine;
    while (l.nextLine != this.endLine)
    {
      l = l.nextLine;
      l.drawSelection(0, Infinity, null, indentRemovalRE);
    }
    this.endLine.drawSelection(0, this.endCol, codeArea.endOfSelection, indentRemovalRE);
  }
  return this;
};

CodeRange.prototype.hideSelection = function()
{
  // assuming start before end // this.setStartBeforeEnd();
  if (this.isSelectAll)
  {
    codeArea.canvas.className = "";
    this.isSelectAll = false;
    return;
  }
  
  codeArea.startOfSelection.style.display = "none";
  codeArea.endOfSelection.style.display = "none";

  var l = this.startLine;
  while (l != this.endLine)
  {
    l.hideSelection();
    l = l.nextLine;
  }
  this.endLine.hideSelection();

  return this;
};

var undoStack = {
  
  // BOTTOM implements undoState where needed
  BOTTOM:
  {
    undo: function() 
    {
      return false;
    },
    addCreatedLine: function() {},
    addDeletedLine: function() {},
    addChangeValue: function() {},
    combine: function() { return this; }
  },
  TOP:
  {
    redo: function()
    {
      return false;
    }
  },
  
  init: function()
  {
    this.currentState = this.BOTTOM;
    this.BOTTOM.nextState = this.TOP;
    this.BOTTOM.prevState = this.BOTTOM;
    this.TOP.prevState = this.BOTTOM;
    this.TOP.nextState = this.TOP;
  },
  
  addState: function(action)
  {
    // try to combine the current state
    var lastCurrentState = this.currentState.combine();
    
    this.currentState = new UndoState(action);

    lastCurrentState.nextState = this.currentState;
    this.currentState.prevState = lastCurrentState;

    // remove redo stack
    this.currentState.nextState = undoStack.TOP;

    codeArea.setProperty("canRedo", false);
    codeArea.setProperty("canUndo", true);
  },
  
  undo: function()
  {
    // store current selection
    if (this.currentState.nextState == undoStack.TOP)
      this.currentState.nextState.selection = selection.getRange();
      
    this.currentState = this.currentState.combine();
    
    var result = this.currentState.undo();
    
    codeArea.setProperty("canUndo", this.currentState != this.BOTTOM);
    codeArea.setProperty("canRedo", true);
    
    return result;
  },
  
  redo: function()
  {
    var result = this.currentState.nextState.redo();

    codeArea.setProperty("canUndo", true);
    codeArea.setProperty("canRedo", this.currentState.nextState != this.TOP);
    
    return result;
  }
  
};



function UndoState(action) 
{
  this.action = action;
  this.canCombine = action.canCombine;
  this.selection = selection.getRange();
}

UndoState.prototype.addCreatedLine = function(line)
{
  // never combine line additions
  this.canCombine = false;
  
  if (!this.createdLines)
    this.createdLines = [];
  this.createdLines.push(line);
};

UndoState.prototype.addDeletedLine = function(line)
{
  // never combine line removals
  this.canCombine = false;
  
  if (!this.deletedLines)
    this.deletedLines = [];
  this.deletedLines.push(line);
};

UndoState.prototype.addChangeValue = function(data)
{
  if (!this.changedValues)
    this.changedValues = [];
  this.changedValues.push(data);
};

UndoState.prototype.undo = function()
{
  undoStack.currentState = this.prevState;
  
  if (this.deletedLines)
    for (var i=this.deletedLines.length-1; i>=0; i--)
      this.deletedLines[i].undel();

  if (this.changedValues)
    for (var i=this.changedValues.length-1; i>=0; i--)
    {
      var data = this.changedValues[i];
      data.line._setValue(data.oldValue);
    }

  if (this.createdLines)
    for (var i=this.createdLines.length-1; i>=0; i--)
      this.createdLines[i]._del();
    
  this.selection.select();
};

UndoState.prototype.redo = function()
{
  undoStack.currentState = this;
  
  if (this.createdLines)
    for (var i=this.createdLines.length-1; i>=0; i--)
      this.createdLines[i].undel();

  if (this.changedValues)
    for (var i=this.changedValues.length-1; i>=0; i--)
    {
      var data = this.changedValues[i];
      data.line._setValue(data.newValue);
    }

  if (this.deletedLines)
    for (var i=this.deletedLines.length-1; i>=0; i--)
      this.deletedLines[i]._del();
    
  this.nextState.selection.select();
};

UndoState.prototype.combine = function()
{
  if (!this.canCombine 
    || !this.prevState.canCombine 
    || this.action != this.prevState.action
    || !this.changedValues
    || !this.prevState.changedValues
    || this.changedValues.length != 1
    || this.prevState.changedValues.length != 1
    || this.changedValues[0].line != this.prevState.changedValues[0].line)
    return this;
  
  // combine with prevState
  var data = this.changedValues[0];
  var prevData = this.prevState.changedValues[0];
  if (prevData)
    prevData.newValue = data.newValue;
  else
    this.prevState.changedValues[0] = data;
  
  this.prevState.nextState = this.nextState;
  this.nextState.prevState = this.prevState;
  
  return this.prevState;
};
var codeArea = 
{
  settings:
  {
    indentSize: 2
  },
  
  // BOF and EOF implement Line where needed (see also init)
  // BOF is the line before the first line
  BOF:
  {
    getIndentSize: function() { return 0; },
    getSmartHomeCol: function() { return 0; },
    div: null
  },
  // EOF is the line after the last one
  EOF:
  { 
    div: null,
    findNext: function() { return false; }
  },
  lines: {},
  lineCount: 0,
  lineNumberCount: 0,
  hasFocus: false,
  readOnly: true,
  handlers: {},
  
  subscribedKeys: [],
  
  init: function()
  {
    undoStack.init();
    
    this.leftMargin = comp.getElementById("left-margin");

    this.canvas = comp.getElementById("canvas");
    
    this.oneChar = comp.getElementById("one-char");

    this.cursors = comp.getElementById("cursors");
    this.cursor = comp.getElementById("cursor");
    this.colCursor = comp.getElementById("col-cursor");
    this.lineCursor = comp.getElementById("line-cursor");
    this.lineCursorInMargin = comp.getElementById("line-cursor-in-margin");
    this.selectionPlane = comp.getElementById("selection-plane");
    this.startOfSelection = comp.getElementById("start-of-selection");
    this.endOfSelection = comp.getElementById("end-of-selection");
    this.clipboard = comp.getElementById("clipboard");
    
    this.BOF.nextLine = this.EOF;
    this.EOF.prevLine = this.BOF;
    
    this.readLayoutMetrics();
    
    this.setValue("");

    this.updateLeftMargin();
    
    setTimeout(this.checkFontSize, 200);
  },
  




  setValue: function(s)
  {
    comp.body.removeChild(this.canvas);
    this.removeAllLines();
    
    s = s.replace(/\r/g, '');
    /*for (var i = 0; i<3; i++)
      s += s;*/
    
    this.isInitial = true;
    new Line(this.EOF).replaceContent(s);
    this.isInitial = false;
    
    comp.body.insertBefore(this.canvas, null);
    
    new CodeRange().select();
  },
  
  getValue: function()
  {
    var all = new CodeRange();
    all.moveLine(this.EOF.prevLine, CodeRange.MOVE_END_ONLY);
    all.moveCol(Infinity, CodeRange.MOVE_END_ONLY);
    return all.getContent();
  },
  
  setReadOnly: function(val)
  {
    if (!this.setProperty("readOnly", val))
      return;
    
    this.readOnly = val;
    if (this.readOnly)
      crossBrowser.addClass(comp.body, "read-only");
    else
      crossBrowser.removeClass(comp.body, "read-only");
      
    selection.showCursorState();
    
    this.fireActionUpdate();
  },
  
  getReadOnly: function()
  {
    return this.readOnly;
  },
  
  getSelection: function()
  {
    return selection.range;
  },
 
  getLineByNumber: function(number)
  {
    var l = this.BOF;
    do
    {
      l = l.nextLine;
      number --;
    }
    while (number > 0 && l != this.EOF);
    
    return (l != this.EOF) && l;    
  },
  
  createCodeRange: function()
  {
    return new CodeRange();
  },
  
  createFindRegExps: function(s, flags)
  {
    if (!s)
      return false;

    if (!flags)
      flags = {};
      
    var reText = s;
    var reFlags = 'g';
    
    if (!flags.regExp)
      reText = reText.replace(/(\\|\^|\$|\*|\+|\-|\?|\.|\(|\)|\!|\:|\=|\||\{|\}|\,|\[|\])/g, "\\$1");
    else
      reText = reText.replace(/\\n/g, "\n");
    
    if (flags.wholeWord)
      reText = '\\b' + reText + '\\b';

    if (!flags.caseSensitive)
      reFlags += 'i';
    
    var result = reText.replace(/\n/g, "$\n^");
    result = result.split("\n");
    for (var i=0; i<result.length; i++)
      try
      {
        result[i] = new RegExp(result[i], reFlags);
      }
      catch(e)
      {
        // TODO: user tried an invalid regexp find, throw error?
        return false;
      }

    return result;
  },
  
  // returns coderange or false
  findNext: function(s, flags, fromLine, fromCol)
  {
    var res = this.createFindRegExps(s, flags);
    if (!res)
      return false;
      
    fromLine = fromLine || selection.range.endLine;
    fromCol = fromCol || (selection.range.startCol + (selection.range.isCollapsed() ? 0 : 1));
    var line = fromLine;
    var col = fromCol;
    var foundRange = false;
    
    do
    {
      foundRange = line.findNext(res, col);
      if (foundRange)
        break;
      line = line.nextLine;
      col = 0;
      // always wrap search
      if (line == this.EOF)
        line = this.BOF.nextLine;
    }
    while (line != fromLine);

    if (!foundRange)
      // find again on fromLine, but from start
      foundRange = fromLine.findNext(res, 0);
    
    return foundRange;
  },

  // returns coderange or false
  findPrevious: function(s, flags, fromLine, fromCol)
  {
    var res = this.createFindRegExps(s, flags, true);
    if (!res)
      return false;
      
    fromLine = fromLine || selection.range.startLine;
    fromCol = fromCol || (selection.range.startCol - (selection.range.isCollapsed() ? 0 : 1));
    var line = fromLine;
    var col = fromCol;
    var foundRange = false;

    do
    {
      foundRange = line.findPrevious(res, col);
      if (foundRange)
        break;
      line = line.prevLine;
      col = Infinity;
      // always wrap search
      if (line == this.BOF)
        line = this.EOF.prevLine;
    }
    while (line != fromLine);

    if (!foundRange)
      // find again on fromLine, but from end
      foundRange = fromLine.findPrevious(res, Infinity);
    
    return foundRange;
  },
  
  // returns array of coderanges
  findAll: function(s, flags)
  {
    var firstRng = this.findNext(s, flags);
    if (!firstRng)
      return [];
    
    var results = [];
    var rng = firstRng;
    do
    {
      results.push(rng);
      rng = this.findNext(s, flags, rng.startLine, rng.startCol + 1);
    }
    while (!rng.equals(firstRng));
    
    return results;
  },
  
  createAction: function(_perform, options)
  {
    var o = options || {};
    o._perform = _perform;
    return new Action(o);
  },
  
  addEventListener: function(name, handler, useCapture)
  {
    if (!this.handlers[name])
      this.handlers[name] = [];
    
    this.handlers[name].push(handler);
  },



  updateLeftMargin: function()
  {
    this.updateLeftMarginTO = false;
    
    // update left margin
    var a = [];
    while (this.lineCount > this.lineNumberCount)
      a.push(++this.lineNumberCount);
    if (a.length)
      crossBrowser.appendHTML(this.leftMargin, "<div>" + a.join("</div><div>") + "</div>");
    while (this.lineCount < this.lineNumberCount)
    {
      this.leftMargin.removeChild(this.leftMargin.lastChild);
      this.lineNumberCount--;
    }
    
    this.updateLayout();
  },
  
  readLayoutMetrics: function()
  { 
    this.leftMargin.style.width = '';
    var w = Math.max(30, this.leftMargin.offsetWidth);
    this.leftMargin.style.width = w;
    
    comp.body.style.paddingLeft = w + "px";
    
    this.firstColOffsetLeft = w + 2;
    this.selectionPlane.style.left = this.firstColOffsetLeft + 'px';
    this.oneCharWidth = this.oneChar.firstChild.offsetWidth;
    this.oneCharHeight = this.oneChar.firstChild.offsetHeight;
    
    if (comp != comp.body)
    {
      this.canvas.style.lineHeight = this.oneCharHeight + 'px';
      this.leftMargin.style.lineHeight = this.oneCharHeight + 'px';
    }
  },

  updateLayout: function()
  { 
    this.readLayoutMetrics();
    
    this.positionOnScroll();
    
    // make sure the cursor is recalculated
    this.currentCursorLine = null;
    selection.hide();
    selection.show(selection.curClassName);
  },
  
  updateLineCount: function(delta)
  {
    this.lineCount += delta;
    if (!this.updateLeftMarginTO)
      this.updateLeftMarginTO = setTimeout(codeArea.updateLeftMarginTOH, 0);
  },

  removeAllLines: function()
  {
    var line = this.BOF.nextLine;
    while (line != this.EOF)
    {
      line.del();
      line = line.nextLine;
    }
  },

  positionOnScroll: function()
  {
    this.cursorMaybeNotInView = true;
    this.currentScrollLeftBoundary = comp.body.scrollLeft;
    this.currentScrollRightBoundary = comp.body.scrollLeft + comp.body.clientWidth - codeArea.leftMargin.offsetWidth - this.oneCharWidth;
    
    if (comp == comp.body)
    {
      this.leftMargin.style.left = comp.body.scrollLeft + 'px';
      this.lineCursor.style.left = comp.body.scrollLeft + 'px';
      this.colCursor.style.top = comp.body.scrollTop + 'px';
    }
  },

  positionCursorLine: function(line)
  {
    if (line != this.currentCursorLine)
    {
      this.currentCursorLine = line;
      
      var cursorTop = line.div.offsetTop + 2;
      
      // position cursor
      this.cursor.style.top = cursorTop + 'px';
      this.lineCursor.style.top = cursorTop + 'px';
      this.lineCursorInMargin.style.top = cursorTop + 'px';
      
      // Keep cursor in view and remember this top
      this.storedScrollTop = this.scrollIntoViewY(cursorTop, codeArea.oneCharHeight);
      if (this.storedScrollTop === false)
        this.storedScrollTop = comp.body.scrollTop;
    } 
    else
    {
      if (this.cursorMaybeNotInView)
      {
        // restore stored scrollTop
        var cursorTop = line.div.offsetTop;

        var scrollTopTarget = this.scrollIntoViewY(cursorTop, this.oneCharHeight, this.storedScrollTop);
        if (scrollTopTarget === false)
          // Top is OK, store for next time
          this.storedScrollTop = comp.body.scrollTop;

        this.cursorMaybeNotInView = false;
      }
    }
  },
  
  calculateScrollTop: function(top, height, suggestedTop)
  {
    var padding = 2;
    var scrollTop = comp.body.scrollTop + padding;
    var clientHeight = comp.body.clientHeight - 2*padding;
    var scrollBot = scrollTop + clientHeight;
    
    var bot = top + height;
    
    // is range in view
    if (top >= scrollTop && bot <= scrollBot)
      return false;

    // taller than view and part of range in view
    if (top <= scrollTop && bot >= scrollBot)
      return false;

    if (!isNaN(suggestedTop))
      return suggestedTop;
    
    if (bot > scrollBot)
      return bot - padding - clientHeight;
      
    return top - padding;
  },
  
  scrollIntoViewY: function(top, height, suggestedTop)
  {
    if (this.scrollTopAnimation)
      this.scrollTopAnimation.finish();
      
    var scrollTopTarget = this.calculateScrollTop(top, height, suggestedTop);
    if (scrollTopTarget === false)
      return false;
    
    var diff = Math.abs(comp.body.scrollTop - scrollTopTarget);
    
    // Don't animate small steps
    if (diff <= this.oneCharHeight)
      return comp.body.scrollTop = scrollTopTarget;
    
// LAURENS: Animated scrolling is disabled since it causes problems on long lines.
comp.body.scrollTop = scrollTopTarget;
this.positionOnScroll();
return scrollTopTarget;

    var modifiers = new Modifiers();
    modifiers.duration = Math.max(100, Math.min(300, diff * 2));
    modifiers.finishAlways = true;
    modifiers.endCode = function () {codeArea.positionOnScroll()};

    this.scrollTopAnimation = new Animator(
      [
        {
          el:comp.body,
          targetState:{
            scrollTop:scrollTopTarget
          }
        }
      ],
      modifiers
    );
    
    return scrollTopTarget;
  },
  
  positionCursorCol: function(col, line)
  {
    var cursorLeft = line.getXFromCol(col);
    
    // position cursor
    this.cursor.style.left = this.colCursor.style.left = (this.firstColOffsetLeft + cursorLeft) + 'px';
    
    // Keep cursor in view
    this.scrollIntoViewX(cursorLeft, 0);
  },  
  
  calculateScrollLeft: function(left, width, suggestedLeft)
  {
    var scrollLeft = this.currentScrollLeftBoundary;
    var clientWidth = comp.body.clientWidth;
    var scrollRight = this.currentScrollRightBoundary;
    
    var right = left + width;
    
    // is range in view
    if (left >= scrollLeft && right <= scrollRight)
      return false;

    // wider than view and part of range in view
    if (left <= scrollLeft && right >= scrollRight)
      return false;

    if (!isNaN(suggestedLeft))
      return suggestedLeft;
    
    if (right > scrollRight)
      return right + clientWidth * (0.2 - 1);
      
    if (left < clientWidth * 0.35)
      return 0;
      
    return left - clientWidth * 0.2;
  },
  
  scrollIntoViewX: function(left, width)
  {
    if (this.scrollLeftAnimation)
      this.scrollLeftAnimation.finish();

    var scrollLeftTarget = this.calculateScrollLeft(left, width);
    if (scrollLeftTarget === false)
      return false;

    var diff = Math.abs(comp.body.scrollLeft - scrollLeftTarget);
    
    // Don't animate small steps
    if (diff <= this.oneCharWidth)
      return comp.body.scrollLeft = scrollLeftTarget;
    
// LAURENS: Animated scrolling is disabled since it causes problems on long lines.
comp.body.scrollLeft = scrollLeftTarget;
this.positionOnScroll();
return scrollLeftTarget;
    
    // todo cache this
    var modifiers = new Modifiers();
    modifiers.duration = Math.max(100, Math.min(300, diff * 2));
    modifiers.finishAlways = true;
    modifiers.endCode = function () {codeArea.positionOnScroll()};

    this.scrollLeftAnimation = new Animator(
      [
        {
          el:comp.body,
          targetState:{
            scrollLeft:scrollLeftTarget
          }
        }
      ],
      modifiers
    );
    
    return scrollLeftTarget;
  },
  
  updateLeftMarginTOH: function() 
  {
    // no this
    codeArea.updateLeftMargin(); 
  },
  
  getLineFromClientY: function(y)
  {
    y += comp.body.scrollTop;
    if (comp == comp.body)
    {
      var rect = crossBrowser.getBoundingClientRect(element);
      y -= rect.top;
    }
    
    var bottom = 0;
    var l = this.BOF;
    do
    {
      l = l.nextLine;
      bottom += this.oneCharHeight;
    }
    while (l != this.EOF.prevLine && bottom < y);
    
    return l;
  },
  
  getColFromClientX: function(x, line)
  {
    x += comp.body.scrollLeft;
    
    if (comp == comp.body)
    {
      var rect = crossBrowser.getBoundingClientRect(element);
      x -= rect.left;
    }

    return line.getColFromX(x - codeArea.firstColOffsetLeft);
  },
  
  focus: function()
  {
    if (comp.body.focus)
      comp.body.focus();
    else
    {
      this.clipboard.style.top = (1+comp.body.scrollTop) + 'px';
      this.clipboard.style.left = (1+comp.body.scrollLeft) + 'px';
      if (!this.clipboard.value)
        this.clipboard.value = 'x';
      codeArea.clipboard.setSelectionRange(0, this.clipboard.value.length);
      this.clipboard.focus();
      onFocus();
    }
  },
  
  subscribeOnKey: function(condition, callback)
  {
    condition.callback = callback;
    codeArea.subscribedKeys.push(condition);
  },
  
  setProperty: function(name, value)
  {
    if (this[name] == value)
      return false;
    
    this[name] = value;
    this.fireActionUpdate();
    return true;
  },
  
  fireActionUpdate: function()
  {
    this.fireEvent("actionupdate");
  },
  
  fireEvent: function(eventName)
  {
    if (window.parentDoc)
    {
      var evtObject = parentDoc.createEventObject();
      parentDoc.getElementById("pe"+eventName).fire(evtObject);
    }
    else
    {
      var handlers = this.handlers[eventName];
      if (handlers)
        for (var i=0; i<handlers.length; i++)
          handlers[i].call(element, {});
    }
  },
  
  checkFontSize: function()
  {
    // setTimeout handler, no this
    if (
      codeArea.oneChar.firstChild.offsetHeight != codeArea.oneCharHeight ||
      codeArea.oneChar.firstChild.offsetWidth != codeArea.oneCharWidth
    )
      codeArea.updateLayout();
    
    // refire this function
    setTimeout(arguments.callee, 200);
  }
  
};

function onKeyDown(evt)
{
  if (!codeArea.hasFocus)
      return true;
  
//window.status = element.id + ' onKeyDown: ' + evt.keyCode + ' ' + (comp.hasFocus && comp.hasFocus() ? 'has focus' : 'no focus') + '   ' + (new Date * 1);
  
  // in Mozilla, non-character keys are handled in onKeyPress
  if (evt.charCode == 0)
    return true;
  
  return passCommand(evt.keyCode, evt);
}

function onKeyPress(evt)
{
  /*if (comp.hasFocus && !comp.hasFocus())
      return true;*/
  
  // evt.keyCode lijkt op Mozilla (Win) altijd 0 te zijn
  // voorlopig probeer ik hier evt.charCode -- Salar

//window.status = element.id + ' onKeyDown: ' + evt.keyCode + ' ' + (comp.hasFocus && comp.hasFocus() ? 'has focus' : 'no focus') + '   ' + (new Date * 1);

  
  if (evt.charCode !== undefined)
  {
      if (evt.charCode == 0)
        return passCommand(evt.keyCode, evt);
        
      return passCharInput(evt.charCode, evt);
  }
  else
  {
    return passCharInput(evt.keyCode, evt)
  }
}

function passCharInput(charCode, evt)
{
  switch (charCode)
  {
    case 13:
      actions.newLine.perform();
      return false;
    default:
      if (charCode >= 32)
      {
        if (!evt.ctrlKey && !evt.altKey)
        {
          actions.typing.perform(String.fromCharCode(charCode));
          return false;
        }
        else
        {
          var keyCode = charCode >= 97 ? charCode - 32 : charCode;
          return passCommand(keyCode, evt);
        }
      };
      return true;
  }
}

function passCommand(keyCode, evt)
{
  evt.modifiers = evt.shiftKey + (evt.ctrlKey << 1) + (evt.altKey << 2);

  switch (keyCode)
  {
    case 8: // backspace
      actions.backspace.perform(evt);
      return false;
    case 9: // tab
      // LAURENS: Insert whitespace if we're not in the indentation part of the line.
      if (selection.getRange().isCollapsed() && selection.curCol > selection.curLine.getIndentSize(true))
        selection.replaceContent("  ");
      else if (evt.shiftKey)
        actions.unindent.perform();
      else
        actions.indent.perform();
      return false;
    case 13:
      actions.newLine.perform();
      return false;
    case 27:
      element.focus();
      return false;
    case 33:
      selection.moveCursorPageUp(evt);
      return false;
    case 34:
      selection.moveCursorPageDown(evt);
      return false;
    case 35:
      selection.moveCursorEnd(evt);
      return false;
    case 36:
      selection.moveCursorHome(evt);
      return false;
    case 37:
      selection.moveCursorLeft(evt);
      return false;
    case 38:
      selection.moveCursorUp(evt);
      return false;
    case 39:
      selection.moveCursorRight(evt);
      return false;
    case 40:
      selection.moveCursorDown(evt);
      return false;
    case 45: // insert
      switch (evt.modifiers)
      {
        case 0:
          codeArea.overWrite = !codeArea.overWrite;
          return false;
        case 1: //shift-only
          return actions.paste.perform();
        case 2: //ctrl-only
          return actions.copy.perform();
      }
      return true;
    case 46: // delete
      if (evt.shiftKey)
        return actions.cut.perform();
      actions.deleteKey.perform(evt);
      return false;
    case 118: // F7
      profiler.showTimers();
      return false;
      break;
    case 119: // F8
      profiler.resetTimers();
      return false;
      break;
    case 145: // scroll lock
      if (!evt.ctrlKey)
      {
        codeArea.scrollLock = !codeArea.scrollLock;
        return false;
      }
      return true;
    default:
      if (evt.ctrlKey && !evt.shiftKey && !evt.altKey)
      {
        switch (keyCode)
        {
          case 65: // Ctrl-A
            selection.selectAll();
            return false;
          case 67: // Ctrl-C
            return actions.copy.perform();
          case 73: // Ctrl-I
            return selection.switchIMEInput();
          case 86: // Ctrl-V
            return actions.paste.perform();
          case 88: // Ctrl-X
            return actions.cut.perform();
          case 89: // Ctrl-Y
            return actions.redo.perform();
          case 90: // Ctrl-Z
            return actions.undo.perform();
        };
      };
      
      // Find from custom mappings
      for (var sk, i = 0; i < codeArea.subscribedKeys.length; i++)
      {
          sk = codeArea.subscribedKeys[i];
          if (sk.action == 'down' && (typeof sk.ctrl == 'undefined' || sk.ctrl == evt.ctrlKey) && (typeof sk.alt == 'undefined' || sk.alt == evt.altKey))
          {
              if (typeof sk.code == 'number' && sk.code == keyCode)
              {
                  evt.keyCode = 0;
                  sk.callback();
                  return false;
              };
              // later: sk.range, sk.list invoeren?
          };
      };
      return true;
  }

}


function onKeyUp(evt)
{
  return false;
}

var selecting = false;
var capturing = false;
var lineSelecting = false;
function onMouseDown(evt)
{
  if (evt.button == 1)
  {
    selection.moveCursorToPoint(evt.clientX, evt.clientY, evt.shiftKey);
    selecting = true;
  }
}

function onMouseMove(evt)
{
  if (typeof(evt.button) == "number" && evt.button != 1 && (selecting || lineSelecting))
    onMouseUp(evt);
  
  if (selecting)
  {
    if (!capturing)
    {
      if (codeArea.canvas.setCapture)
        codeArea.canvas.setCapture();
      capturing = true;
    }

    selection.moveCursorToPoint(evt.clientX, evt.clientY, true);
  }

  if (lineSelecting)
  {
    var line = codeArea.getLineFromClientY(evt.clientY);
  
    var cr = new CodeRange();
    cr.startLine = lineSelecting.line;
    cr.endLine = line;
    if (cr.endLine.isPreceding(cr.startLine))
    {
      cr.startCol = Infinity;
      cr.endCol = 0;
    }
    else
    {
      cr.startCol = 0;
      cr.endCol = Infinity;
    }
    cr.select();
  }
}

function onMouseUp(evt)
{
  if (!codeArea.hasFocus)
    codeArea.focus();
  
  if (selecting)
  {
    selection.moveCursorToPoint(evt.clientX, evt.clientY, true);
    selecting = false;
    if (capturing)
    {
      if (document.releaseCapture)
        document.releaseCapture();
      capturing = false;
    }
  }
  
  if (lineSelecting)
  {
    lineSelecting = false;
  }
}

function onMouseDownMargin(evt)
{
  var line = codeArea.getLineFromClientY(evt.clientY);
  lineSelecting = {line: line};
  
  // select line
  var cr = new CodeRange();
  cr.moveToLine(line);
  cr.select();
  
  comp.body.scrollLeft = 0;

  return false;
}

function onFocus(evt)
{
  codeArea.hasFocus = true;
  selection.showCursorState();
}

function onBlur(evt)
{
  codeArea.hasFocus = false;
  selection.showCursorState();
}

function onScroll(evt)
{
  codeArea.positionOnScroll();
}

function onResize(evt)
{
  codeArea.positionOnScroll();
}

function onContextMenu(evt)
{
  return false;
};

function onIMEInputKeyDown(evt)
{
  // in Mozilla, non-character keys are handled in onKeyPress
  if (evt.charCode == 0)
    return true;
  
  return handleIMEInputCommand(this, evt.keyCode, evt);
};

function onIMEInputKeyPress(evt)
{
  if (evt.charCode !== undefined)
  {
    if (evt.charCode == 0)
      return handleIMEInputCommand(this, evt.keyCode, evt);
  }
  
  event.cancelBubble = true;
  return true;
};

function handleIMEInputCommand(inp, keyCode, evt)
{
  switch (true)
  {
    case keyCode == 13: // return
    case keyCode == 33: // page up
    case keyCode == 34: // page down
    case keyCode == 38: // up
    case keyCode == 40: // down
    case keyCode == 73 && evt.ctrlKey && !evt.shiftKey && !evt.altKey: // CTRL-I
      var col = crossBrowser.getInputSelectionStart(inp);
      selection._moveCursorCol(col);
      onIMEInputBlur.call(evt.srcElement);
      passCommand(keyCode, evt);
      return false;
      
    default:
      evt.cancelBubble=true;
      return true;
  }
};

function onIMEInputBlur() 
{ 
  var line = codeArea.lines[this.parentNode.id];
  line.hideIMEInput(this.value);
};
  
function cancelEvent() { return false; }var comp = document.getAnonymousNodes(this)[0];
comp.body = comp;
comp.getElementById = function(id)
{
  var els = comp.body.getElementsByTagName('*');
  for (var i = 0; i < els.length; i++)
    if (els[i].id == id)
      return els[i];
};
comp.createElement = document.createElement;
comp.createRange = document.createRange;

var element = comp.parentNode;

this.codeArea = codeArea;
this.actions = actions;

eventCenter.activate();
codeArea.init();

//this.normalize();
codeArea.setValue(this.innerText);
codeArea.setReadOnly(this.hasAttribute("readonly"));

eventCenter.attachEvent(comp, "onscroll", onScroll);
eventCenter.attachEvent(comp.body, "onkeydown", onKeyDown);
eventCenter.attachEvent(comp.body, "onkeypress", onKeyPress);
eventCenter.attachEvent(comp.body, "onkeyup", onKeyUp);
eventCenter.attachEvent(comp.body, "onfocus", onFocus);
eventCenter.attachEvent(comp.body, "onblur", onBlur);
eventCenter.attachEvent(codeArea.canvas, "onmousedown", onMouseDown);
eventCenter.attachEvent(comp, "onmousemove", onMouseMove);
eventCenter.attachEvent(comp, "oncontextmenu", onContextMenu);
eventCenter.attachEvent(comp, "onmouseup", onMouseUp);
eventCenter.attachEvent(codeArea.leftMargin, "onmousedown", onMouseDownMargin);
eventCenter.attachEvent(comp, "ondraggesture", cancelEvent);

var onload = element.getAttribute("onload");
if (onload)
  codeArea.addEventListener("load", new Function("event", onload), false);

var onactionupdate = element.getAttribute("onactionupdate");
if (onactionupdate)
  codeArea.addEventListener("actionupdate", new Function("event", onactionupdate), false);

codeArea.fireEvent("load");
var spaces = '                    ';
var zeros  = '00000000000000000000';
//var tracer = new ActiveXObject("tracer.tracer");
var traceable = {};
  
function Profiler() {
  for (var i = 0; i <= 10; i++)
    this.regExps[i] = new RegExp(".{"+i+"}$");
  
/*  window._alert = window.alert;
  window.alert = function(s) {window._alert(s)};
  this.wrapFunction(window, 'alert', 'window');

  window._confirm = window.confirm;
  window.confirm = function(s) {return window._confirm(s)};
  this.wrapFunction(window, 'confirm', 'window');*/
  
//  this.wrapFunction(this, 'renderTimersBranch', 'profiler');
//  this.wrapFunction(this, 'tToString', 'profiler');
//  this.wrapFunction(this, 'tToString2', 'profiler');
}

Profiler.pData = {};
Profiler.nData = {};
Profiler.prototype = {
  subscribeConstructors: function(constructors) {
    for (var i = 0; i < constructors.length; i++)
      this.subscribeConstructor(constructors[i]);
  },
  subscribeConstructor: function(constructor) {
    if (constructor.hasBeenProfiled)
      return;
    constructor.hasBeenProfiled = true;
    var cName = (''+constructor).match(/function (\w+)/i)[1];
    this.subscribeObject(constructor, cName);
    this.subscribeObject(constructor.prototype, cName.replace(/^([A-Z])(([A-Z]*)([A-Z]))?/, function(m, m1, m2, m3, m4) {return m1.toLowerCase()+(m2?m3.toLowerCase()+m4:"")}));
    
    this.wrapConstructor(window, cName);
  },
  subscribeObject:function(obj, objName) {
    if (!objName)
      objName = 'NN';
    var functionNames = [];
    for (var propName in obj)
      if (typeof(obj[propName]) == 'function')
        functionNames.push(propName);
    for (var i=0; i<functionNames.length; i++)
      this.wrapFunction(obj, functionNames[i], objName);
  },
  
  wrapConstructor:function(obj, constructorName) {
    var label = "new " + constructorName;
    var f = obj[constructorName];

    obj[constructorName] = function() {
      try {
        tStart(label);
        return new f(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], arguments[5], arguments[6], arguments[7], arguments[8], arguments[9], arguments[10], arguments[11], arguments[12], arguments[13], arguments[14], arguments[15], arguments[16], arguments[17], arguments[18], arguments[19], arguments[20], arguments[21], arguments[22], arguments[23], arguments[24], arguments[25]);
      } finally {
        tStop(label);
      }
    };
    
    // Copy props.
    for (var propName in f)
      obj[constructorName][propName] = f[propName];
  },
  
  wrapFunction:function(obj, functionName, objName) {
    var label = objName + '.' + functionName;
    var f = obj[functionName];

    obj[functionName] = function() {
      try {
        tStart(label);
        return f.apply(this, arguments);
      } finally {
        tStop(label);
      }
    };
  },
  
  showTimers: function () {
    var lines = [];

    lines.push('<h1>P'+'rofiler Results</h1>');
    lines.push('<'+'script>function cl(){var div=this.nextSibling;if (div.nodeName.toLowerCase()=="div") div.style.display = (div.style.display=="none")?"":"none";var sep=this.getElementsByTagName("kbd")[0];sep.innerHTML = (sep.innerHTML=="+")?"-":"+";}</'+'script>');
    lines.push('<style>pre,div{margin:0;cursor:hand;padding:0} u{text-decoration:none;border-top:1px solid #eee}</style>');
    lines.push('<pre>    total time = count x   avg time / local time</pre>');

//    Profiler.nData = {};
//    var pData = Profiler.pData;
//    Profiler.pData = {};
    this.renderTimersBranch(lines, Profiler.pData, '', 0);
    //Profiler.pData = pData;

    lines.push("<hr />");
    lines.push('<pre>tot local time = count x  avg local time</pre>');

    var dataArray = [];
    for (var label in Profiler.nData) 
      dataArray.push(Profiler.nData[label]);
    dataArray = dataArray.sort(function(d1, d2) { return d2.t - d1.t; });
    for (var i=0; i<dataArray.length; i++)
    {
      var lData = dataArray[i];
      lines.push('<pre><u>');
      lines.push(this.tToString(lData.t, 10));
      lines.push(' ms =');
      lines.push(this.tToString(lData.count, 5));
      lines.push(' x');
      lines.push(this.tToString2(lData.t/lData.count, 7));
      lines.push(' ms > ');
      lines.push(lData.label);
      lines.push('</u></pre>');
    }

    var w = window.open();
    w.document.write(lines.join(''));
    w.document.close();
  //  alert(s);
  },

  renderTimersBranch: function (lines, pData, indent, step) {
    var color = this.getColor(step);
    var dataArray = [];
    for (var label in pData) 
      if (label != "__p__")
        dataArray.push(pData[label]);
    dataArray = dataArray.sort(function(d1, d2) { return d2.t - d1.t; });
    var total = 0;

    var nextIndent = indent + '<span style="' + color + '"> </span><u> </u>';
    for (var i=0; i<dataArray.length; i++) {
      var lData = dataArray[i];
      var branch = [];
      var branchtotal = this.renderTimersBranch(branch, lData.pData, nextIndent, step+1);
      localTime = lData.t-branchtotal;
      if (!Profiler.nData[lData.label])
        Profiler.nData[lData.label] = { label:lData.label, count:0, t:0 };
      Profiler.nData[lData.label].t += localTime;
      Profiler.nData[lData.label].count += lData.count;
      var sep = branch.length?"+":"&lt;";
      lines.push('<pre id=p onclick="cl.call(this)"><u>');
      lines.push(this.tToString(lData.t, 10));
      lines.push(' ms =');
      lines.push(this.tToString(lData.count, 5));
      lines.push(' x');
      lines.push(this.tToString(lData.t/lData.count, 7));
      lines.push(' ms / ');
      lines.push(this.tToString2(localTime, 7));
      lines.push(' ms </u>');
      lines.push(indent);
      lines.push('<kbd style="');
      lines.push(color);
      lines.push(';border-top:1px solid white">');
      lines.push(sep);
      lines.push('</kbd> ');
      lines.push(lData.label);
      lines.push('</pre>');
      lines.push('<div style="display:none">');
      lines.push(branch.join(''));
      lines.push('</div>');
      total += lData.t;
    }

    return total;
  },

  getColor: function (step) {
    if (step & 1 == 0)
      return 'background: #DCDCDC';
    else
      return 'background: #EBEBEB';
  },

  resetTimers: function() {
    Profiler.pData = {};
    Profiler.nData = {};
  },
  
  regExps: {},
  tToString: function (t, l) {
    var regExp = this.regExps[l];
    
    var s='';
    while (t>1000)
    {
      t/=10;s+='0';
    }
    t = Math.floor(t) + s;
    return (spaces+t).match(regExp)[0];
    //return spaces.substring(0, l-t.length) + t;
  },

  tToString2: function (t, l) {
    var regExp = this.regExps[l+2];
    
    var a = Math.floor(t);
    t = '' + a + '.' + Math.floor(10*(t-a));
    return (spaces+t).match(regExp)[0];
    //return spaces.substring(0, l+2-t.length) + t;
  }

};

function tStart(label) {
  var lData = Profiler.pData[label];
  if (!lData)
    lData = Profiler.pData[label] = { label:label, count:0, t:0 };

  lData.startT = +new Date();
  
  if (!lData.pData)
    lData.pData = {__p__: Profiler.pData};
    
  Profiler.pData = lData.pData;
}

function tStop(label) {
  Profiler.pData = Profiler.pData.__p__||Profiler.pData;
  
  var lData = Profiler.pData[label];
  if (!lData) return;

  lData.count++;
  lData.t += (new Date() - lData.startT);
}

var profiler = new Profiler();
    
      profiler.subscribeConstructor(Line);
      profiler.subscribeConstructor(Action);
      profiler.subscribeConstructor(CodeRange);
      profiler.subscribeConstructor(UndoState);
      profiler.subscribeConstructor(MozRange);
      profiler.subscribeObject(codeArea, "codeArea");
      profiler.subscribeObject(undoStack, "undoStack");
      profiler.subscribeObject(crossBrowser, "crossBrowser");
    ]]></constructor> 
      
    </implementation>
    
    <resources>
      <stylesheet src="scripts/xbl.css" />
    </resources>
    
    <handlers>
      <handler event="keypress">if(event.keyCode==13) codeArea.canvas.focus();</handler>
    </handlers>
    
  </binding>
</bindings>
