diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 0bebde252b52..88dc128fb812 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1227,6 +1227,14 @@ pref("devtools.hud.loglimit.console", 200); pref("devtools.editor.tabsize", 4); pref("devtools.editor.expandtab", true); +// Tells which component you want to use for source editing in developer tools. +// +// Available components: +// "orion" - this is the Orion source code editor from the Eclipse project. It +// provides programmer-specific editor features such as syntax highlighting, +// indenting and bracket recognition. +pref("devtools.editor.component", "orion"); + // Enable the Font Inspector pref("devtools.fontinspector.enabled", true); diff --git a/browser/devtools/jar.mn b/browser/devtools/jar.mn index 8f23afb5420b..e4e784b3ec72 100644 --- a/browser/devtools/jar.mn +++ b/browser/devtools/jar.mn @@ -28,6 +28,7 @@ browser.jar: content/browser/devtools/fontinspector/font-inspector.js (fontinspector/font-inspector.js) content/browser/devtools/fontinspector/font-inspector.xhtml (fontinspector/font-inspector.xhtml) content/browser/devtools/fontinspector/font-inspector.css (fontinspector/font-inspector.css) + content/browser/devtools/orion.js (sourceeditor/orion/orion.js) content/browser/devtools/codemirror/codemirror.js (sourceeditor/codemirror/codemirror.js) content/browser/devtools/codemirror/codemirror.css (sourceeditor/codemirror/codemirror.css) content/browser/devtools/codemirror/javascript.js (sourceeditor/codemirror/javascript.js) @@ -44,6 +45,7 @@ browser.jar: content/browser/devtools/codemirror/dialog.js (sourceeditor/codemirror/dialog/dialog.js) content/browser/devtools/codemirror/dialog.css (sourceeditor/codemirror/dialog/dialog.css) content/browser/devtools/codemirror/mozilla.css (sourceeditor/codemirror/mozilla.css) +* content/browser/devtools/source-editor-overlay.xul (sourceeditor/source-editor-overlay.xul) content/browser/devtools/debugger.xul (debugger/debugger.xul) content/browser/devtools/debugger.css (debugger/debugger.css) content/browser/devtools/debugger-controller.js (debugger/debugger-controller.js) diff --git a/browser/devtools/scratchpad/scratchpad.js b/browser/devtools/scratchpad/scratchpad.js index e870fd16cf12..5e786bcf3b7a 100644 --- a/browser/devtools/scratchpad/scratchpad.js +++ b/browser/devtools/scratchpad/scratchpad.js @@ -1516,7 +1516,7 @@ var Scratchpad = { * Add an observer for Scratchpad events. * * The observer implements IScratchpadObserver := { - * onReady: Called when the Scratchpad and its Editor are ready. + * onReady: Called when the Scratchpad and its SourceEditor are ready. * Arguments: (Scratchpad aScratchpad) * } * diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_edit_ui_updates.js b/browser/devtools/scratchpad/test/browser_scratchpad_edit_ui_updates.js index f864d5973e85..70e9e2248e5e 100644 --- a/browser/devtools/scratchpad/test/browser_scratchpad_edit_ui_updates.js +++ b/browser/devtools/scratchpad/test/browser_scratchpad_edit_ui_updates.js @@ -5,6 +5,10 @@ "use strict"; +let tempScope = {}; +Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope); +let SourceEditor = tempScope.SourceEditor; + function test() { waitForExplicitFinish(); diff --git a/browser/devtools/sourceeditor/codemirror/mozilla.css b/browser/devtools/sourceeditor/codemirror/mozilla.css index ccda017122d9..1f79b470f392 100644 --- a/browser/devtools/sourceeditor/codemirror/mozilla.css +++ b/browser/devtools/sourceeditor/codemirror/mozilla.css @@ -18,21 +18,21 @@ } .error { - background-image: url("chrome://browser/skin/devtools/editor-error.png"); + background-image: url("chrome://browser/skin/devtools/orion-error.png"); opacity: 0.75; } .breakpoint { - background-image: url("chrome://browser/skin/devtools/editor-breakpoint.png"); + background-image: url("chrome://browser/skin/devtools/orion-breakpoint.png"); } .debugLocation { - background-image: url("chrome://browser/skin/devtools/editor-debug-location.png"); + background-image: url("chrome://browser/skin/devtools/orion-debug-location.png"); } .breakpoint.debugLocation { - background-image: url("chrome://browser/skin/devtools/editor-debug-location.png"), - url("chrome://browser/skin/devtools/editor-breakpoint.png"); + background-image: url("chrome://browser/skin/devtools/orion-debug-location.png"), + url("chrome://browser/skin/devtools/orion-breakpoint.png"); } .error-line { diff --git a/browser/devtools/sourceeditor/editor.js b/browser/devtools/sourceeditor/editor.js index 6a4d0d28578c..25b0706aea79 100644 --- a/browser/devtools/sourceeditor/editor.js +++ b/browser/devtools/sourceeditor/editor.js @@ -83,7 +83,7 @@ const CM_MAPPING = [ const CM_JUMP_DIALOG = [ L10N.GetStringFromName("gotoLineCmd.promptTitle") - + " " + + " " ]; const { cssProperties, cssValues, cssColors } = getCSSKeywords(); @@ -193,7 +193,7 @@ Editor.prototype = { appendTo: function (el) { let def = promise.defer(); let cm = editors.get(this); - let env = el.ownerDocument.createElementNS(XUL_NS, "iframe"); + let env = el.ownerDocument.createElement("iframe"); env.flex = 1; if (cm) @@ -505,7 +505,7 @@ Editor.prototype = { if (!info) return false; - return ~info.wrapClass.split(" ").indexOf(className); + return info.wrapClass == className; }, /** diff --git a/browser/devtools/sourceeditor/moz.build b/browser/devtools/sourceeditor/moz.build index 9a586a0f3510..f12cb40bb3f5 100644 --- a/browser/devtools/sourceeditor/moz.build +++ b/browser/devtools/sourceeditor/moz.build @@ -10,6 +10,9 @@ JS_MODULES_PATH = 'modules/devtools/sourceeditor' EXTRA_JS_MODULES += [ 'debugger.js', - 'editor.js' + 'editor.js', + 'source-editor-orion.jsm', + 'source-editor-ui.jsm', + 'source-editor.jsm', ] diff --git a/browser/devtools/sourceeditor/orion/LICENSE b/browser/devtools/sourceeditor/orion/LICENSE new file mode 100644 index 000000000000..2d907d73a37e --- /dev/null +++ b/browser/devtools/sourceeditor/orion/LICENSE @@ -0,0 +1,29 @@ +Eclipse Distribution License - v 1.0 + +Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. +* Neither the name of the Eclipse Foundation, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/browser/devtools/sourceeditor/orion/Makefile.dryice.js b/browser/devtools/sourceeditor/orion/Makefile.dryice.js new file mode 100644 index 000000000000..6866ed93771a --- /dev/null +++ b/browser/devtools/sourceeditor/orion/Makefile.dryice.js @@ -0,0 +1,56 @@ +#!/usr/bin/env node +/* vim:set ts=2 sw=2 sts=2 et tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var copy = require('dryice').copy; + +const ORION_EDITOR = "org.eclipse.orion.client.editor/web"; + +var js_src = copy.createDataObject(); + +copy({ + source: [ + ORION_EDITOR + "/orion/textview/global.js", + ORION_EDITOR + "/orion/textview/eventTarget.js", + ORION_EDITOR + "/orion/editor/regex.js", + ORION_EDITOR + "/orion/textview/keyBinding.js", + ORION_EDITOR + "/orion/textview/annotations.js", + ORION_EDITOR + "/orion/textview/rulers.js", + ORION_EDITOR + "/orion/textview/undoStack.js", + ORION_EDITOR + "/orion/textview/textModel.js", + ORION_EDITOR + "/orion/textview/projectionTextModel.js", + ORION_EDITOR + "/orion/textview/tooltip.js", + ORION_EDITOR + "/orion/textview/textView.js", + ORION_EDITOR + "/orion/textview/textDND.js", + ORION_EDITOR + "/orion/editor/htmlGrammar.js", + ORION_EDITOR + "/orion/editor/textMateStyler.js", + ORION_EDITOR + "/examples/textview/textStyler.js", + ], + dest: js_src, +}); + +copy({ + source: js_src, + dest: "orion.js", +}); + +var css_src = copy.createDataObject(); + +copy({ + source: [ + ORION_EDITOR + "/orion/textview/textview.css", + ORION_EDITOR + "/orion/textview/rulers.css", + ORION_EDITOR + "/orion/textview/annotations.css", + ORION_EDITOR + "/examples/textview/textstyler.css", + ORION_EDITOR + "/examples/editor/htmlStyles.css", + ], + dest: css_src, +}); + +copy({ + source: css_src, + dest: "orion.css", +}); + diff --git a/browser/devtools/sourceeditor/orion/README b/browser/devtools/sourceeditor/orion/README new file mode 100644 index 000000000000..c7669099cbfb --- /dev/null +++ b/browser/devtools/sourceeditor/orion/README @@ -0,0 +1,43 @@ +# Introduction + +This is the Orion editor packaged for Mozilla. + +The Orion editor web site: http://www.eclipse.org/orion + +# Upgrade + +To upgrade Orion to a newer version see the UPGRADE file. + +Orion version: git clone from 2012-01-26 + commit hash 1d1150131dacecc9f4d9eb3cdda9103ea1819045 + + + patch for Eclipse Bug 370584 - [Firefox] Edit menu items in context menus + http://git.eclipse.org/c/orion/org.eclipse.orion.client.git/commit/?id=137d5a8e9bbc0fa204caae74ebd25a7d9d4729bd + see https://bugs.eclipse.org/bugs/show_bug.cgi?id=370584 + + + patches for Eclipse Bug 370606 - Problems with UndoStack and deletions at + the beginning of the document + http://git.eclipse.org/c/orion/org.eclipse.orion.client.git/commit/?id=cec71bddaf32251c34d3728df5da13c130d14f33 + http://git.eclipse.org/c/orion/org.eclipse.orion.client.git/commit/?id=3ce24b94f1d8103b16b9cf16f2f50a6302d43b18 + http://git.eclipse.org/c/orion/org.eclipse.orion.client.git/commit/?id=27177e9a3dc70c20b4877e3eab3adfff1d56e342 + see https://bugs.eclipse.org/bugs/show_bug.cgi?id=370606 + + + patch for Mozilla Bug 730532 - remove CSS2Properties aliases for MozOpacity + and MozOutline* + see https://bugzilla.mozilla.org/show_bug.cgi?id=730532#c3 + +# License + +The following files are licensed according to the contents in the LICENSE +file: + orion.js + orion.css + +# Theming + +The syntax highlighting and the editor UI are themed using a style sheet. The +default theme file is browser/themes/*/devtools/orion.css - this is based on the +orion.css found in this folder. + +Please note that the orion.css file from this folder is not used. It is kept +here only as reference. diff --git a/browser/devtools/sourceeditor/orion/UPGRADE b/browser/devtools/sourceeditor/orion/UPGRADE new file mode 100644 index 000000000000..a2c006efe66e --- /dev/null +++ b/browser/devtools/sourceeditor/orion/UPGRADE @@ -0,0 +1,20 @@ +Upgrade notes: + +1. Get the Orion client source code from: +http://www.eclipse.org/orion + +2. Install Dryice from: +https://github.com/mozilla/dryice + +You also need nodejs for Dryice to run: +http://nodejs.org + +3. Copy Makefile.dryice.js to: +org.eclipse.orion.client/bundles/ + +4. Execute Makefile.dryice.js. You should get orion.js and orion.css. + +5. Copy the two files back here. + +6. Make a new build of Firefox. + diff --git a/browser/devtools/sourceeditor/orion/orion.css b/browser/devtools/sourceeditor/orion/orion.css new file mode 100644 index 000000000000..1e3b003cafe5 --- /dev/null +++ b/browser/devtools/sourceeditor/orion/orion.css @@ -0,0 +1,277 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.view { + background-color: white; +} + +.viewContainer { + background-color: #eeeeee; + font-family: monospace; + font-size: 10pt; +} +::-webkit-scrollbar-corner { + background-color: #eeeeee; +} + +.viewContent { +}/* Styles for rulers */ +.ruler { + background-color: white; +} +.ruler.annotations { + border-right: 1px solid lightgray; + width: 16px; +} +.ruler.folding { + border-right: 1px solid lightgray; + width: 14px; +} +.ruler.lines { + border-right: 1px solid lightgray; + text-align: right; +} +.ruler.overview { + border-left: 1px solid lightgray; + width: 14px; +} + +/* Styles for the line number ruler */ +.rulerLines { +} +.rulerLines.even +.rulerLines.odd { +}/* Styles for the annotation ruler (all lines) */ +.annotation { +} +.annotation.error, +.annotation.warning +.annotation.task, +.annotation.bookmark, +.annotation.breakpoint, +.annotation.collapsed +.annotation.expanded { +} + +/* Styles for the annotation ruler (first line) */ +.annotationHTML { + cursor: pointer; + width: 16px; + height: 16px; + display: inline-block; + vertical-align: middle; + background-position: center; + background-repeat: no-repeat; +} +.annotationHTML.error { + /* images/error.gif */ + background-image: url("data:image/gif;base64,R0lGODlhEAAQANUAAPVvcvWHiPVucvRuc+ttcfV6f91KVN5LU99PV/FZY/JhaM4oN84pONE4Rd1ATfJLWutVYPRgbdxpcsgWKMgZKs4lNfE/UvE/U+artcpdSc5uXveimslHPuBhW/eJhfV5efaCgO2CgP+/v+PExP///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAACUALAAAAAAQABAAAAZ+wJJwSCwaScgkySgkjTQZTkYzWhadnE5oE+pwqkSshwQqkzxfa4kkQXxEpA9J9EFI1KQGQQBAigYCBA14ExEWF0gXihETeA0QD3AkD5QQg0NsDnAJmwkOd5gYFSQKpXAFDBhqaxgLBwQBBAapq00YEg0UDRKqTGtKSL7Cw8JBADs="); +} +.annotationHTML.warning { + /* images/warning.gif */ + background-image: url("data:image/gif;base64,R0lGODlhEAAQANUAAP7bc//egf/ij/7ijv/jl/7kl//mnv7lnv/uwf7CTP7DTf7DT/7IW//Na/7Na//NbP7QdP/dmbltAIJNAF03AMSAJMSCLKqASa2DS6uBSquCSrGHTq6ETbCHT7WKUrKIUcCVXL+UXMOYX8GWXsSZYMiib6+ETbOIUcOXX86uhd3Muf///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAACsALAAAAAAQABAAAAZowJVwSCwaj0ihikRSJYcoBEL0XKlGkcjImQQhJBREKFnyICoThKeE/AAW6AXgdPyUAgrLJBEo0YsbAQyDhAEdRRwDDw8OaA4NDQImRBgFEJdglxAEGEQZKQcHBqOkKRpFF6mqq1WtrUEAOw=="); +} +.annotationHTML.task { + /* images/task.gif */ + background-image: url("data:image/gif;base64,R0lGODlhEAAQAMQAAN7s4uTy6ICvY423c2WdP2ugR3mqWYeza2ejOl6VNVqPM1aJMURsJ2GaOnKlT8PbsbPDqGmmO1OCLk98LEhxKGWfOWKaN0t2KkJoJf///////wAAAAAAAAAAAAAAAAAAACH5BAEAABoALAAAAAAQABAAAAVmoCaOZDk+UaquDxkNcCxHJHLceI6QleD/vkCmQrIYjkiDMGAhJRzQ6NKRICkKgYJ2qVWQFktCmEBYkCSNZSbQaDckpAl5TCZMSBdtAaDXX0gUUYJRFCQMSYgGDCQQGI6PkBAmkyUhADs="); +} +.annotationHTML.bookmark { + /* images/bookmark.gif */ + background-image: url("data:image/gif;base64,R0lGODlhEAAQALMAAP7//+/VNPzZS/vifeumAPrBOOSlHOSuRP///wAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAgALAAAAAAQABAAAARLEMlJq5Xn3EvIrkenfRIhCB5pmKhRdbAJAGhssuc8n6eJoAKdkOaTAIdEQeWoA1oGsiZhYAnIcqiApVPjElyUbkFSgCkn5XElLYkAADs="); +} +.annotationHTML.breakpoint { + /* images/breakpoint.gif */ + background-image: url("data:image/gif;base64,R0lGODlhEAAQANUAAFheoFxkoFxnpmt0pmZxpnF7rYyWwmJwpnaFs3aDrWt8rXGBrYycwmZ3mXuNs42cu77F03GIs3aJrYGVu2J5oKCuxeDj6LK/03GLrYieu3aIoIygu6m4zcLN3MTM1m6Rs2aLriRgkSZilXGXtoGcs7LD0QBLhSZikihol3ScubrO2Yaqu5q4xpO0wpm7yabF0ZO9yaXI0r3X3tHj6P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAADQALAAAAAAQABAAAAafQJpwSCwWLYZBIDAwWIw0A+FFpW6aRUPCxe1yE4ahhdCCxWSzmSwGgxGeUceKpUqhUCkVa7UK0wgkJCUjJoUmIyWBBEIEGhoeJ4YmJx6OAUIADQ0QIZIhEJoAQgEUFBUgkiAVpZdRCxIPFx8iIh8XDw4FfhYHDhgZHB0dHBkYEwdwUQoTEc3OEwp+QwYHCBMMDBMIB9JESAJLAk5Q5EVBADs="); +} +.annotationHTML.collapsed { + /* images/collapsed.png */ + width: 14px; + height: 14px; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAWBJREFUeNpi/P//PwMlgImBQkCxASzoAp++fo+6de+Z+fXbD/Jev/nAICoiwKCpqrBBTUlqNR835zJ09YzIYfDxy7eo/cevLmXlYGNQUJAEahZieP3mHcODB08Zfv/4w+BoqR3Nz8O1DKcXzt94HPqXmZlBU1+LgZNfkMHazIOBA0hr6uswgMTP33gYijcMLlx/EMAnLs7w7sc/hg9AG0HgPZB+B8S84hJA+UcBeMPg+at3DJIMnAxZzt5wsUhnXzDdsmIVWB6vAcLCfAys3z4wzN64huEfkJ/uH8IwexOQDQymD2/fgeXxekFLRWHD51evGDhZGRi4WSFSnCwgNjB2Xr1m0AbK4zXAQkdhNdPf3wx3r91g+PruLcOqnasYvn54x3Dv2k0G5r+/GMyB8nijEQTefvoadeH6w9Cbtx8GvH//kUFQkJ9BQ1V+g76m/GphPu5lBA0YenmBYgMAAgwA34GIKjmLxOUAAAAASUVORK5CYII="); +} +.annotationHTML.expanded { + /* images/expanded.png */ + width: 14px; + height: 14px; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAT5JREFUeNrUksFKw0AURW+mTWw67SSEiG209U90r4jddFO34l+5U0HdZCHiFwiCOz9AlMSmGEpMOqk1TWJSFGyFbATR2dyZd+Dw3mOENE3xkyP8PYHrBT3OX7uW43ZefA6FUaw1dJPSyrmu1k8KBYOh37Od4XFZLEPXFdRrFMGIw3U9TKMYqw1tb0VjcxLy9eEF425CCIxWE5JcxSQGxCyNloG87gXhwWIHc4J767lTZQw8ShFGSZbxRyaQmZJxd3NRUJ6ffwQNEi6PzG/L2tjdmvFCgcKqKL2F2Olu43MzggDka+IjPuOFI7Sbujn2fUglYKkkzFIi+R0I/QDrGS8UqDX5QkhiOHYfE84hkhSTkGNgOyDJFCzjhYLTq+vDtrG8r1LZtB6fcHtzB+uhD5VWzLx+lvF/8JV/XfAuwADsrJbMGG4l4AAAAABJRU5ErkJggg=="); +} +.annotationHTML.multiple { + /* images/multiple.gif */ + background-image: url("data:image/gif;base64,R0lGODlhEAAQANUAAOdpa+yJiuFYXOFYXeBYXONwded8f+NwdmhwkHB4iPr7/ezx+fP2+2h4kOzy+Wh4iPr8/gCBwTaczjaXyjaYyjaXyTaYyfr8/QCMzQCMzACHxzao2jal2Dak1zag03iAgI/Ckn64fZrHmX+4fZLCianPopPCiarOoqbLlafLlbnXq7nWq6fLlMTcsoCIeJCQcIiIeKCYaJiQcO16ee16evGVlfGWlfahn/ahoPWhn/WhoPe1tP///////wAAAAAAACH5BAEAAD0ALAAAAAAQABAAAAaRwJ5wSCwaj8WYcslcDmObaDTGq1Zjzw4mk+FQIRcFTzaUeTRoj4zHaI+HL0lkLnnxFgsH7zWEWSoTFBMwVlUwQy6JMDCJjYwuQx8tk5MfOzk4OjcfkSssKCkqHzY0MzQ1nEIJJSYkJCcJAQCzAQlDDyIjISMiCQYEAgMGD0MNIMfHDQUHBc3EQgjR0tPSSNY9QQA7"); +} +.annotationHTML.overlay { + /* images/plus.png */ + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAAXNSR0IArs4c6QAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJEAQvB2JVdrAAAAAdaVRYdENvbW1lbnQAAAAAAENyZWF0ZWQgd2l0aCBHSU1QZC5lBwAAAD1JREFUCNdtjkESADAEAzemf69f66HMqGlOIhYiFRFRtSQBWAY7mzx+EDTL6sSgb1jTk7Q87rxyqe37fXsAa78gLyZnRgEAAAAASUVORK5CYII="); + background-position: right bottom; + position: relative; + top: -16px; +} +.annotationHTML.currentBracket { + /* images/currentBracket.png */ + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sLEBULCGQmEKAAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAnklEQVQ4y7VTsRHDIBATJg1HCUzAHEzFBExAzwZsRMkE9gifKhc72ODYibr/+xcnoQdugq0LAujEwmbn0UxQh4OxpjX1XgshwFqLnPM5PQTQGlprWpbl3RhJ/CSQUm7qPYLp7i8cEpRSoJT6ju0lIaVEQgiKMQ4lHHpQayVjzHWCn5jIOcc8z9dMBADvPZxz3SC1tzCI8vgWdvL+VzwB8JSj2GFTyxIAAAAASUVORK5CYII="); +} +.annotationHTML.matchingBracket { + /* images/matchingBracket.png */ + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sLEBUMAsuyb3kAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAoklEQVQ4y61TsQ3EIAw80DcI0USKGIApWIsB2IGGKbJPugxBR3VfvfRRCOSTvw7LPuPzGXgI8f0gwAsFu5rXIYMdDiEOIdnKW5YFzjnEGH+bhwA/KKVwmibu0BhRnpEZY1BrHTaVT7fQJZjnGeu63tOAJFNKVEox53yqQZfAWstt27oidgm01ve3UEqBaBjnspG89wgh3LiFgZXHt3Dh23/FGxKViehm0X85AAAAAElFTkSuQmCC"); +} +.annotationHTML.currentLine { + /* images/currentLine.gif */ + background-image: url("data:image/gif;base64,R0lGODlhEAAQAMQAALxe0bNWzbdZzrlb0KpPx61RybBTy6VLxadNxZGctIeUroyYsG92hHyMqIKRq2l9nmyAoHGDonaIpStXj6q80k1aXf///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABYALAAAAAAQABAAAAVCoCWOZGmeKDql5ppOMGXBk/zOoltSNO6XrlXwxIPNYiMGq8SoLC2MaNPygEQkDYdikUg6LQcEoWAICAaA5HPNLoUAADs="); +} + +/* Styles for the overview ruler */ +.annotationOverview { + cursor: pointer; + border-radius: 2px; + left: 2px; + width: 8px; +} +.annotationOverview.task { + background-color: lightgreen; + border: 1px solid green; +} +.annotationOverview.breakpoint { + background-color: lightblue; + border: 1px solid blue; +} +.annotationOverview.bookmark { + background-color: yellow; + border: 1px solid orange; +} +.annotationOverview.error { + background-color: lightcoral; + border: 1px solid darkred; +} +.annotationOverview.warning { + background-color: Gold; + border: 1px solid black; +} +.annotationOverview.currentBracket { + background-color: lightgray; + border: 1px solid red; +} +.annotationOverview.matchingBracket { + background-color: lightgray; + border: 1px solid red; +} +.annotationOverview.currentLine { + background-color: #EAF2FE; + border: 1px solid black; +} + +/* Styles for text range */ +.annotationRange { + background-repeat: repeat-x; + background-position: left bottom; +} +.annotationRange.task { + /* images/squiggly_task.png */ + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sLDhEoIrb7JmcAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAGUlEQVQI12NggIH/DGdhDCM45z/DfyiBAADgdQjGhI/4DAAAAABJRU5ErkJggg=="); +} +.annotationRange.breakpoint { + /* images/squiggly_breakpoint.png */ + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sLDhEqHTKradgAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAIklEQVQI11XJMQ0AMAzAMGMafwrFlD19+sUKIJTFo9k+B/kQ+Qr2bIVKOgAAAABJRU5ErkJggg=="); +} +.annotationRange.bookmark { + /* images/squiggly_bookmark.png */ + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); +} +.annotationRange.error { + /* images/squiggly_error.png */ + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg=="); +} +.annotationRange.warning { + /* images/squiggly_warning.png */ + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); +} +.annotationRange.currentBracket { +} +.annotationRange.matchingBracket { + outline: 1px solid red; +} + +/* Styles for lines of text */ +.annotationLine { +} +.annotationLine.currentLine { + background-color: #EAF2FE; +} + +.token_singleline_comment { + color: green; +} + +.token_multiline_comment { + color: green; +} + +.token_doc_comment { + color: #00008F; +} + +.token_doc_html_markup { + color: #7F7F9F; +} + +.token_doc_tag { + color: #7F9FBF; +} + +.token_task_tag { + color: #7F9FBF; +} + +.token_string { + color: blue; +} + +.token_keyword { + color: darkred; + font-weight: bold; +} + +.token_space { + /* images/white_space.png */ + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAIAAABv85FHAAAABnRSTlMA/wAAAACkwsAdAAAAIUlEQVR4nGP4z8CAC+GUIEXuABhgkTuABEiRw2cmae4EAH05X7xDolNRAAAAAElFTkSuQmCC"); + background-repeat: no-repeat; + background-position: center center; +} + +.token_tab { + /* images/white_tab.png */ + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAJCAIAAACJ2loDAAAABnRSTlMA/wD/AP83WBt9AAAAMklEQVR4nGP4TwRgoK6i52c3bz5w6zMSA6tJn28d2Lx589nnCAYu63AaSLxJRLoJPwAAeNk0aG4opfMAAAAASUVORK5CYII="); + background-repeat: no-repeat; + background-position: left center; +} + +.line_caret { + background-color: #EAF2FE; +} + +/* Styling for html syntax highlighting */ +.entity-name-tag { + color: #3f7f7f; +} + +.entity-other-attribute-name { + color: #7f007f; +} + +.punctuation-definition-comment { + color: #3f5fbf; +} + +.comment { + color: #3f5fbf +} + +.string-quoted { + color: #2a00ff; + font-style: italic; +} + +.invalid { + color: red; + font-weight: bold; +} \ No newline at end of file diff --git a/browser/devtools/sourceeditor/orion/orion.js b/browser/devtools/sourceeditor/orion/orion.js new file mode 100644 index 000000000000..06ad42195a09 --- /dev/null +++ b/browser/devtools/sourceeditor/orion/orion.js @@ -0,0 +1,12303 @@ +/******************************************************************************* + * @license + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: + * Felipe Heidrich (IBM Corporation) - initial API and implementation + * Silenio Quarti (IBM Corporation) - initial API and implementation + * Mihai Sucan (Mozilla Foundation) - fix for Bug#364214 + */ + +/*global window */ + +/** + * Evaluates the definition function and mixes in the returned module with + * the module specified by moduleName. + *

+ * This function is intented to by used when RequireJS is not available. + *

+ * + * @param {String} name The mixin module name. + * @param {String[]} deps The array of dependency names. + * @param {Function} callback The definition function. + */ +if (!window.define) { + window.define = function(name, deps, callback) { + var module = this; + var split = (name || "").split("/"), i, j; + for (i = 0; i < split.length - 1; i++) { + module = module[split[i]] = (module[split[i]] || {}); + } + var depModules = [], depModule; + for (j = 0; j < deps.length; j++) { + depModule = this; + split = deps[j].split("/"); + for (i = 0; i < split.length - 1; i++) { + depModule = depModule[split[i]] = (depModule[split[i]] || {}); + } + depModules.push(depModule); + } + var newModule = callback.apply(this, depModules); + for (var p in newModule) { + if (newModule.hasOwnProperty(p)) { + module[p] = newModule[p]; + } + } + }; +} + +/** + * Require/get the defined modules. + *

+ * This function is intented to by used when RequireJS is not available. + *

+ * + * @param {String[]|String} deps The array of dependency names. This can also be + * a string, a single dependency name. + * @param {Function} [callback] Optional, the callback function to execute when + * multiple dependencies are required. The callback arguments will have + * references to each module in the same order as the deps array. + * @returns {Object|undefined} If the deps parameter is a string, then this + * function returns the required module definition, otherwise undefined is + * returned. + */ +if (!window.require) { + window.require = function(deps, callback) { + var depsArr = typeof deps === "string" ? [deps] : deps; + var depModules = [], depModule, split, i, j; + for (j = 0; j < depsArr.length; j++) { + depModule = this; + split = depsArr[j].split("/"); + for (i = 0; i < split.length - 1; i++) { + depModule = depModule[split[i]] = (depModule[split[i]] || {}); + } + depModules.push(depModule); + } + if (callback) { + callback.apply(this, depModules); + } + return typeof deps === "string" ? depModules[0] : undefined; + }; +}/******************************************************************************* + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: + * Felipe Heidrich (IBM Corporation) - initial API and implementation + * Silenio Quarti (IBM Corporation) - initial API and implementation + ******************************************************************************/ + +/*global define */ +define("orion/textview/eventTarget", [], function() { + /** + * Constructs a new EventTarget object. + * + * @class + * @name orion.textview.EventTarget + */ + function EventTarget() { + } + /** + * Adds in the event target interface into the specified object. + * + * @param {Object} object The object to add in the event target interface. + */ + EventTarget.addMixin = function(object) { + var proto = EventTarget.prototype; + for (var p in proto) { + if (proto.hasOwnProperty(p)) { + object[p] = proto[p]; + } + } + }; + EventTarget.prototype = /** @lends orion.textview.EventTarget.prototype */ { + /** + * Adds an event listener to this event target. + * + * @param {String} type The event type. + * @param {Function|EventListener} listener The function or the EventListener that will be executed when the event happens. + * @param {Boolean} [useCapture=false] true if the listener should be trigged in the capture phase. + * + * @see #removeEventListener + */ + addEventListener: function(type, listener, useCapture) { + if (!this._eventTypes) { this._eventTypes = {}; } + var state = this._eventTypes[type]; + if (!state) { + state = this._eventTypes[type] = {level: 0, listeners: []}; + } + var listeners = state.listeners; + listeners.push({listener: listener, useCapture: useCapture}); + }, + /** + * Dispatches the given event to the listeners added to this event target. + * @param {Event} evt The event to dispatch. + */ + dispatchEvent: function(evt) { + if (!this._eventTypes) { return; } + var type = evt.type; + var state = this._eventTypes[type]; + if (state) { + var listeners = state.listeners; + try { + state.level++; + if (listeners) { + for (var i=0, len=listeners.length; i < len; i++) { + if (listeners[i]) { + var l = listeners[i].listener; + if (typeof l === "function") { + l.call(this, evt); + } else if (l.handleEvent && typeof l.handleEvent === "function") { + l.handleEvent(evt); + } + } + } + } + } finally { + state.level--; + if (state.compact && state.level === 0) { + for (var j=listeners.length - 1; j >= 0; j--) { + if (!listeners[j]) { + listeners.splice(j, 1); + } + } + if (listeners.length === 0) { + delete this._eventTypes[type]; + } + state.compact = false; + } + } + } + }, + /** + * Returns whether there is a listener for the specified event type. + * + * @param {String} type The event type + * + * @see #addEventListener + * @see #removeEventListener + */ + isListening: function(type) { + if (!this._eventTypes) { return false; } + return this._eventTypes[type] !== undefined; + }, + /** + * Removes an event listener from the event target. + *

+ * All the parameters must be the same ones used to add the listener. + *

+ * + * @param {String} type The event type + * @param {Function|EventListener} listener The function or the EventListener that will be executed when the event happens. + * @param {Boolean} [useCapture=false] true if the listener should be trigged in the capture phase. + * + * @see #addEventListener + */ + removeEventListener: function(type, listener, useCapture){ + if (!this._eventTypes) { return; } + var state = this._eventTypes[type]; + if (state) { + var listeners = state.listeners; + for (var i=0, len=listeners.length; i < len; i++) { + var l = listeners[i]; + if (l && l.listener === listener && l.useCapture === useCapture) { + if (state.level !== 0) { + listeners[i] = null; + state.compact = true; + } else { + listeners.splice(i, 1); + } + break; + } + } + if (listeners.length === 0) { + delete this._eventTypes[type]; + } + } + } + }; + return {EventTarget: EventTarget}; +}); +/******************************************************************************* + * @license + * Copyright (c) 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +/*global define */ +/*jslint browser:true regexp:false*/ +/** + * @name orion.editor.regex + * @class Utilities for dealing with regular expressions. + * @description Utilities for dealing with regular expressions. + */ +define("orion/editor/regex", [], function() { + /** + * @methodOf orion.editor.regex + * @static + * @description Escapes regex special characters in the input string. + * @param {String} str The string to escape. + * @returns {String} A copy of str with regex special characters escaped. + */ + function escape(str) { + return str.replace(/([\\$\^*\/+?\.\(\)|{}\[\]])/g, "\\$&"); + } + + /** + * @methodOf orion.editor.regex + * @static + * @description Parses a pattern and flags out of a regex literal string. + * @param {String} str The string to parse. Should look something like "/ab+c/" or "/ab+c/i". + * @returns {Object} If str looks like a regex literal, returns an object with properties + *
+ *
pattern
{String}
+ *
flags
{String}
+ *
otherwise returns null. + */ + function parse(str) { + var regexp = /^\s*\/(.+)\/([gim]{0,3})\s*$/.exec(str); + if (regexp) { + return { + pattern : regexp[1], + flags : regexp[2] + }; + } + return null; + } + + return { + escape: escape, + parse: parse + }; +}); +/******************************************************************************* + * @license + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: + * Felipe Heidrich (IBM Corporation) - initial API and implementation + * Silenio Quarti (IBM Corporation) - initial API and implementation + ******************************************************************************/ + +/*global window define */ + +define("orion/textview/keyBinding", [], function() { + var isMac = window.navigator.platform.indexOf("Mac") !== -1; + + /** + * Constructs a new key binding with the given key code and modifiers. + * + * @param {String|Number} keyCode the key code. + * @param {Boolean} mod1 the primary modifier (usually Command on Mac and Control on other platforms). + * @param {Boolean} mod2 the secondary modifier (usually Shift). + * @param {Boolean} mod3 the third modifier (usually Alt). + * @param {Boolean} mod4 the fourth modifier (usually Control on the Mac). + * + * @class A KeyBinding represents of a key code and a modifier state that can be triggered by the user using the keyboard. + * @name orion.textview.KeyBinding + * + * @property {String|Number} keyCode The key code. + * @property {Boolean} mod1 The primary modifier (usually Command on Mac and Control on other platforms). + * @property {Boolean} mod2 The secondary modifier (usually Shift). + * @property {Boolean} mod3 The third modifier (usually Alt). + * @property {Boolean} mod4 The fourth modifier (usually Control on the Mac). + * + * @see orion.textview.TextView#setKeyBinding + */ + function KeyBinding (keyCode, mod1, mod2, mod3, mod4) { + if (typeof(keyCode) === "string") { + this.keyCode = keyCode.toUpperCase().charCodeAt(0); + } else { + this.keyCode = keyCode; + } + this.mod1 = mod1 !== undefined && mod1 !== null ? mod1 : false; + this.mod2 = mod2 !== undefined && mod2 !== null ? mod2 : false; + this.mod3 = mod3 !== undefined && mod3 !== null ? mod3 : false; + this.mod4 = mod4 !== undefined && mod4 !== null ? mod4 : false; + } + KeyBinding.prototype = /** @lends orion.textview.KeyBinding.prototype */ { + /** + * Returns whether this key binding matches the given key event. + * + * @param e the key event. + * @returns {Boolean} true whether the key binding matches the key event. + */ + match: function (e) { + if (this.keyCode === e.keyCode) { + var mod1 = isMac ? e.metaKey : e.ctrlKey; + if (this.mod1 !== mod1) { return false; } + if (this.mod2 !== e.shiftKey) { return false; } + if (this.mod3 !== e.altKey) { return false; } + if (isMac && this.mod4 !== e.ctrlKey) { return false; } + return true; + } + return false; + }, + /** + * Returns whether this key binding is the same as the given parameter. + * + * @param {orion.textview.KeyBinding} kb the key binding to compare with. + * @returns {Boolean} whether or not the parameter and the receiver describe the same key binding. + */ + equals: function(kb) { + if (!kb) { return false; } + if (this.keyCode !== kb.keyCode) { return false; } + if (this.mod1 !== kb.mod1) { return false; } + if (this.mod2 !== kb.mod2) { return false; } + if (this.mod3 !== kb.mod3) { return false; } + if (this.mod4 !== kb.mod4) { return false; } + return true; + } + }; + return {KeyBinding: KeyBinding}; +}); +/******************************************************************************* + * @license + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: + * Felipe Heidrich (IBM Corporation) - initial API and implementation + * Silenio Quarti (IBM Corporation) - initial API and implementation + ******************************************************************************/ + +/*global define */ + +define("orion/textview/annotations", ['orion/textview/eventTarget'], function(mEventTarget) { + /** + * @class This object represents a decoration attached to a range of text. Annotations are added to a + * AnnotationModel which is attached to a TextModel. + *

+ * See:
+ * {@link orion.textview.AnnotationModel}
+ * {@link orion.textview.Ruler}
+ *

+ * @name orion.textview.Annotation + * + * @property {String} type The annotation type (for example, orion.annotation.error). + * @property {Number} start The start offset of the annotation in the text model. + * @property {Number} end The end offset of the annotation in the text model. + * @property {String} html The HTML displayed for the annotation. + * @property {String} title The text description for the annotation. + * @property {orion.textview.Style} style The style information for the annotation used in the annotations ruler and tooltips. + * @property {orion.textview.Style} overviewStyle The style information for the annotation used in the overview ruler. + * @property {orion.textview.Style} rangeStyle The style information for the annotation used in the text view to decorate a range of text. + * @property {orion.textview.Style} lineStyle The style information for the annotation used in the text view to decorate a line of text. + */ + /** + * Constructs a new folding annotation. + * + * @param {orion.textview.ProjectionTextModel} projectionModel The projection text model. + * @param {String} type The annotation type. + * @param {Number} start The start offset of the annotation in the text model. + * @param {Number} end The end offset of the annotation in the text model. + * @param {String} expandedHTML The HTML displayed for this annotation when it is expanded. + * @param {orion.textview.Style} expandedStyle The style information for the annotation when it is expanded. + * @param {String} collapsedHTML The HTML displayed for this annotation when it is collapsed. + * @param {orion.textview.Style} collapsedStyle The style information for the annotation when it is collapsed. + * + * @class This object represents a folding annotation. + * @name orion.textview.FoldingAnnotation + */ + function FoldingAnnotation (projectionModel, type, start, end, expandedHTML, expandedStyle, collapsedHTML, collapsedStyle) { + this.type = type; + this.start = start; + this.end = end; + this._projectionModel = projectionModel; + this._expandedHTML = this.html = expandedHTML; + this._expandedStyle = this.style = expandedStyle; + this._collapsedHTML = collapsedHTML; + this._collapsedStyle = collapsedStyle; + this.expanded = true; + } + + FoldingAnnotation.prototype = /** @lends orion.textview.FoldingAnnotation.prototype */ { + /** + * Collapses the annotation. + */ + collapse: function () { + if (!this.expanded) { return; } + this.expanded = false; + this.html = this._collapsedHTML; + this.style = this._collapsedStyle; + var projectionModel = this._projectionModel; + var baseModel = projectionModel.getBaseModel(); + this._projection = { + start: baseModel.getLineStart(baseModel.getLineAtOffset(this.start) + 1), + end: baseModel.getLineEnd(baseModel.getLineAtOffset(this.end), true) + }; + projectionModel.addProjection(this._projection); + }, + /** + * Expands the annotation. + */ + expand: function () { + if (this.expanded) { return; } + this.expanded = true; + this.html = this._expandedHTML; + this.style = this._expandedStyle; + this._projectionModel.removeProjection(this._projection); + } + }; + + /** + * Constructs a new AnnotationTypeList object. + * + * @class + * @name orion.textview.AnnotationTypeList + */ + function AnnotationTypeList () { + } + /** + * Adds in the annotation type interface into the specified object. + * + * @param {Object} object The object to add in the annotation type interface. + */ + AnnotationTypeList.addMixin = function(object) { + var proto = AnnotationTypeList.prototype; + for (var p in proto) { + if (proto.hasOwnProperty(p)) { + object[p] = proto[p]; + } + } + }; + AnnotationTypeList.prototype = /** @lends orion.textview.AnnotationTypeList.prototype */ { + /** + * Adds an annotation type to the receiver. + *

+ * Only annotations of the specified types will be shown by + * the receiver. + *

+ * + * @param {Object} type the annotation type to be shown + * + * @see #removeAnnotationType + * @see #isAnnotationTypeVisible + */ + addAnnotationType: function(type) { + if (!this._annotationTypes) { this._annotationTypes = []; } + this._annotationTypes.push(type); + }, + /** + * Gets the annotation type priority. The priority is determined by the + * order the annotation type is added to the receiver. Annotation types + * added first have higher priority. + *

+ * Returns 0 if the annotation type is not added. + *

+ * + * @param {Object} type the annotation type + * + * @see #addAnnotationType + * @see #removeAnnotationType + * @see #isAnnotationTypeVisible + */ + getAnnotationTypePriority: function(type) { + if (this._annotationTypes) { + for (var i = 0; i < this._annotationTypes.length; i++) { + if (this._annotationTypes[i] === type) { + return i + 1; + } + } + } + return 0; + }, + /** + * Returns an array of annotations in the specified annotation model for the given range of text sorted by type. + * + * @param {orion.textview.AnnotationModel} annotationModel the annotation model. + * @param {Number} start the start offset of the range. + * @param {Number} end the end offset of the range. + * @return {orion.textview.Annotation[]} an annotation array. + */ + getAnnotationsByType: function(annotationModel, start, end) { + var iter = annotationModel.getAnnotations(start, end); + var annotation, annotations = []; + while (iter.hasNext()) { + annotation = iter.next(); + var priority = this.getAnnotationTypePriority(annotation.type); + if (priority === 0) { continue; } + annotations.push(annotation); + } + var self = this; + annotations.sort(function(a, b) { + return self.getAnnotationTypePriority(a.type) - self.getAnnotationTypePriority(b.type); + }); + return annotations; + }, + /** + * Returns whether the receiver shows annotations of the specified type. + * + * @param {Object} type the annotation type + * @returns {Boolean} whether the specified annotation type is shown + * + * @see #addAnnotationType + * @see #removeAnnotationType + */ + isAnnotationTypeVisible: function(type) { + return this.getAnnotationTypePriority(type) !== 0; + }, + /** + * Removes an annotation type from the receiver. + * + * @param {Object} type the annotation type to be removed + * + * @see #addAnnotationType + * @see #isAnnotationTypeVisible + */ + removeAnnotationType: function(type) { + if (!this._annotationTypes) { return; } + for (var i = 0; i < this._annotationTypes.length; i++) { + if (this._annotationTypes[i] === type) { + this._annotationTypes.splice(i, 1); + break; + } + } + } + }; + + /** + * Constructs an annotation model. + * + * @param {textModel} textModel The text model. + * + * @class This object manages annotations for a TextModel. + *

+ * See:
+ * {@link orion.textview.Annotation}
+ * {@link orion.textview.TextModel}
+ *

+ * @name orion.textview.AnnotationModel + * @borrows orion.textview.EventTarget#addEventListener as #addEventListener + * @borrows orion.textview.EventTarget#removeEventListener as #removeEventListener + * @borrows orion.textview.EventTarget#dispatchEvent as #dispatchEvent + */ + function AnnotationModel(textModel) { + this._annotations = []; + var self = this; + this._listener = { + onChanged: function(modelChangedEvent) { + self._onChanged(modelChangedEvent); + } + }; + this.setTextModel(textModel); + } + + AnnotationModel.prototype = /** @lends orion.textview.AnnotationModel.prototype */ { + /** + * Adds an annotation to the annotation model. + *

The annotation model listeners are notified of this change.

+ * + * @param {orion.textview.Annotation} annotation the annotation to be added. + * + * @see #removeAnnotation + */ + addAnnotation: function(annotation) { + if (!annotation) { return; } + var annotations = this._annotations; + var index = this._binarySearch(annotations, annotation.start); + annotations.splice(index, 0, annotation); + var e = { + type: "Changed", + added: [annotation], + removed: [], + changed: [] + }; + this.onChanged(e); + }, + /** + * Returns the text model. + * + * @return {orion.textview.TextModel} The text model. + * + * @see #setTextModel + */ + getTextModel: function() { + return this._model; + }, + /** + * @class This object represents an annotation iterator. + *

+ * See:
+ * {@link orion.textview.AnnotationModel#getAnnotations}
+ *

+ * @name orion.textview.AnnotationIterator + * + * @property {Function} hasNext Determines whether there are more annotations in the iterator. + * @property {Function} next Returns the next annotation in the iterator. + */ + /** + * Returns an iterator of annotations for the given range of text. + * + * @param {Number} start the start offset of the range. + * @param {Number} end the end offset of the range. + * @return {orion.textview.AnnotationIterator} an annotation iterartor. + */ + getAnnotations: function(start, end) { + var annotations = this._annotations, current; + //TODO binary search does not work for range intersection when there are overlaping ranges, need interval search tree for this + var i = 0; + var skip = function() { + while (i < annotations.length) { + var a = annotations[i++]; + if ((start === a.start) || (start > a.start ? start < a.end : a.start < end)) { + return a; + } + if (a.start >= end) { + break; + } + } + return null; + }; + current = skip(); + return { + next: function() { + var result = current; + if (result) { current = skip(); } + return result; + }, + hasNext: function() { + return current !== null; + } + }; + }, + /** + * Notifies the annotation model that the given annotation has been modified. + *

The annotation model listeners are notified of this change.

+ * + * @param {orion.textview.Annotation} annotation the modified annotation. + * + * @see #addAnnotation + */ + modifyAnnotation: function(annotation) { + if (!annotation) { return; } + var index = this._getAnnotationIndex(annotation); + if (index < 0) { return; } + var e = { + type: "Changed", + added: [], + removed: [], + changed: [annotation] + }; + this.onChanged(e); + }, + /** + * Notifies all listeners that the annotation model has changed. + * + * @param {orion.textview.Annotation[]} added The list of annotation being added to the model. + * @param {orion.textview.Annotation[]} changed The list of annotation modified in the model. + * @param {orion.textview.Annotation[]} removed The list of annotation being removed from the model. + * @param {ModelChangedEvent} textModelChangedEvent the text model changed event that trigger this change, can be null if the change was trigger by a method call (for example, {@link #addAnnotation}). + */ + onChanged: function(e) { + return this.dispatchEvent(e); + }, + /** + * Removes all annotations of the given type. All annotations + * are removed if the type is not specified. + *

The annotation model listeners are notified of this change. Only one changed event is generated.

+ * + * @param {Object} type the type of annotations to be removed. + * + * @see #removeAnnotation + */ + removeAnnotations: function(type) { + var annotations = this._annotations; + var removed, i; + if (type) { + removed = []; + for (i = annotations.length - 1; i >= 0; i--) { + var annotation = annotations[i]; + if (annotation.type === type) { + annotations.splice(i, 1); + } + removed.splice(0, 0, annotation); + } + } else { + removed = annotations; + annotations = []; + } + var e = { + type: "Changed", + removed: removed, + added: [], + changed: [] + }; + this.onChanged(e); + }, + /** + * Removes an annotation from the annotation model. + *

The annotation model listeners are notified of this change.

+ * + * @param {orion.textview.Annotation} annotation the annotation to be removed. + * + * @see #addAnnotation + */ + removeAnnotation: function(annotation) { + if (!annotation) { return; } + var index = this._getAnnotationIndex(annotation); + if (index < 0) { return; } + var e = { + type: "Changed", + removed: this._annotations.splice(index, 1), + added: [], + changed: [] + }; + this.onChanged(e); + }, + /** + * Removes and adds the specifed annotations to the annotation model. + *

The annotation model listeners are notified of this change. Only one changed event is generated.

+ * + * @param {orion.textview.Annotation} remove the annotations to be removed. + * @param {orion.textview.Annotation} add the annotations to be added. + * + * @see #addAnnotation + * @see #removeAnnotation + */ + replaceAnnotations: function(remove, add) { + var annotations = this._annotations, i, index, annotation, removed = []; + if (remove) { + for (i = remove.length - 1; i >= 0; i--) { + annotation = remove[i]; + index = this._getAnnotationIndex(annotation); + if (index < 0) { continue; } + annotations.splice(index, 1); + removed.splice(0, 0, annotation); + } + } + if (!add) { add = []; } + for (i = 0; i < add.length; i++) { + annotation = add[i]; + index = this._binarySearch(annotations, annotation.start); + annotations.splice(index, 0, annotation); + } + var e = { + type: "Changed", + removed: removed, + added: add, + changed: [] + }; + this.onChanged(e); + }, + /** + * Sets the text model of the annotation model. The annotation + * model listens for changes in the text model to update and remove + * annotations that are affected by the change. + * + * @param {orion.textview.TextModel} textModel the text model. + * + * @see #getTextModel + */ + setTextModel: function(textModel) { + if (this._model) { + this._model.removeEventListener("Changed", this._listener.onChanged); + } + this._model = textModel; + if (this._model) { + this._model.addEventListener("Changed", this._listener.onChanged); + } + }, + /** @ignore */ + _binarySearch: function (array, offset) { + var high = array.length, low = -1, index; + while (high - low > 1) { + index = Math.floor((high + low) / 2); + if (offset <= array[index].start) { + high = index; + } else { + low = index; + } + } + return high; + }, + /** @ignore */ + _getAnnotationIndex: function(annotation) { + var annotations = this._annotations; + var index = this._binarySearch(annotations, annotation.start); + while (index < annotations.length && annotations[index].start === annotation.start) { + if (annotations[index] === annotation) { + return index; + } + index++; + } + return -1; + }, + /** @ignore */ + _onChanged: function(modelChangedEvent) { + var start = modelChangedEvent.start; + var addedCharCount = modelChangedEvent.addedCharCount; + var removedCharCount = modelChangedEvent.removedCharCount; + var annotations = this._annotations, end = start + removedCharCount; + //TODO binary search does not work for range intersection when there are overlaping ranges, need interval search tree for this + var startIndex = 0; + if (!(0 <= startIndex && startIndex < annotations.length)) { return; } + var e = { + type: "Changed", + added: [], + removed: [], + changed: [], + textModelChangedEvent: modelChangedEvent + }; + var changeCount = addedCharCount - removedCharCount, i; + for (i = startIndex; i < annotations.length; i++) { + var annotation = annotations[i]; + if (annotation.start >= end) { + annotation.start += changeCount; + annotation.end += changeCount; + e.changed.push(annotation); + } else if (annotation.end <= start) { + //nothing + } else if (annotation.start < start && end < annotation.end) { + annotation.end += changeCount; + e.changed.push(annotation); + } else { + annotations.splice(i, 1); + e.removed.push(annotation); + i--; + } + } + if (e.added.length > 0 || e.removed.length > 0 || e.changed.length > 0) { + this.onChanged(e); + } + } + }; + mEventTarget.EventTarget.addMixin(AnnotationModel.prototype); + + /** + * Constructs a new styler for annotations. + * + * @param {orion.textview.TextView} view The styler view. + * @param {orion.textview.AnnotationModel} view The styler annotation model. + * + * @class This object represents a styler for annotation attached to a text view. + * @name orion.textview.AnnotationStyler + * @borrows orion.textview.AnnotationTypeList#addAnnotationType as #addAnnotationType + * @borrows orion.textview.AnnotationTypeList#getAnnotationTypePriority as #getAnnotationTypePriority + * @borrows orion.textview.AnnotationTypeList#getAnnotationsByType as #getAnnotationsByType + * @borrows orion.textview.AnnotationTypeList#isAnnotationTypeVisible as #isAnnotationTypeVisible + * @borrows orion.textview.AnnotationTypeList#removeAnnotationType as #removeAnnotationType + */ + function AnnotationStyler (view, annotationModel) { + this._view = view; + this._annotationModel = annotationModel; + var self = this; + this._listener = { + onDestroy: function(e) { + self._onDestroy(e); + }, + onLineStyle: function(e) { + self._onLineStyle(e); + }, + onChanged: function(e) { + self._onAnnotationModelChanged(e); + } + }; + view.addEventListener("Destroy", this._listener.onDestroy); + view.addEventListener("LineStyle", this._listener.onLineStyle); + annotationModel.addEventListener("Changed", this._listener.onChanged); + } + AnnotationStyler.prototype = /** @lends orion.textview.AnnotationStyler.prototype */ { + /** + * Destroys the styler. + *

+ * Removes all listeners added by this styler. + *

+ */ + destroy: function() { + var view = this._view; + if (view) { + view.removeEventListener("Destroy", this._listener.onDestroy); + view.removeEventListener("LineStyle", this._listener.onLineStyle); + this.view = null; + } + var annotationModel = this._annotationModel; + if (annotationModel) { + annotationModel.removeEventListener("Changed", this._listener.onChanged); + annotationModel = null; + } + }, + _mergeStyle: function(result, style) { + if (style) { + if (!result) { result = {}; } + if (result.styleClass && style.styleClass && result.styleClass !== style.styleClass) { + result.styleClass += " " + style.styleClass; + } else { + result.styleClass = style.styleClass; + } + var prop; + if (style.style) { + if (!result.style) { result.style = {}; } + for (prop in style.style) { + if (!result.style[prop]) { + result.style[prop] = style.style[prop]; + } + } + } + if (style.attributes) { + if (!result.attributes) { result.attributes = {}; } + for (prop in style.attributes) { + if (!result.attributes[prop]) { + result.attributes[prop] = style.attributes[prop]; + } + } + } + } + return result; + }, + _mergeStyleRanges: function(ranges, styleRange) { + if (!ranges) { return; } + for (var i=0; i= range.end) { continue; } + var mergedStyle = this._mergeStyle({}, range.style); + mergedStyle = this._mergeStyle(mergedStyle, styleRange.style); + if (styleRange.start <= range.start && styleRange.end >= range.end) { + ranges[i] = {start: range.start, end: range.end, style: mergedStyle}; + } else if (styleRange.start > range.start && styleRange.end < range.end) { + ranges.splice(i, 1, + {start: range.start, end: styleRange.start, style: range.style}, + {start: styleRange.start, end: styleRange.end, style: mergedStyle}, + {start: styleRange.end, end: range.end, style: range.style}); + i += 2; + } else if (styleRange.start > range.start) { + ranges.splice(i, 1, + {start: range.start, end: styleRange.start, style: range.style}, + {start: styleRange.start, end: range.end, style: mergedStyle}); + i += 1; + } else if (styleRange.end < range.end) { + ranges.splice(i, 1, + {start: range.start, end: styleRange.end, style: mergedStyle}, + {start: styleRange.end, end: range.end, style: range.style}); + i += 1; + } + } + }, + _onAnnotationModelChanged: function(e) { + if (e.textModelChangedEvent) { + return; + } + var view = this._view; + if (!view) { return; } + var self = this; + var model = view.getModel(); + function redraw(changes) { + for (var i = 0; i < changes.length; i++) { + if (!self.isAnnotationTypeVisible(changes[i].type)) { continue; } + var start = changes[i].start; + var end = changes[i].end; + if (model.getBaseModel) { + start = model.mapOffset(start, true); + end = model.mapOffset(end, true); + } + if (start !== -1 && end !== -1) { + view.redrawRange(start, end); + } + } + } + redraw(e.added); + redraw(e.removed); + redraw(e.changed); + }, + _onDestroy: function(e) { + this.destroy(); + }, + _onLineStyle: function (e) { + var annotationModel = this._annotationModel; + var viewModel = this._view.getModel(); + var baseModel = annotationModel.getTextModel(); + var start = e.lineStart; + var end = e.lineStart + e.lineText.length; + if (baseModel !== viewModel) { + start = viewModel.mapOffset(start); + end = viewModel.mapOffset(end); + } + var annotations = annotationModel.getAnnotations(start, end); + while (annotations.hasNext()) { + var annotation = annotations.next(); + if (!this.isAnnotationTypeVisible(annotation.type)) { continue; } + if (annotation.rangeStyle) { + var annotationStart = annotation.start; + var annotationEnd = annotation.end; + if (baseModel !== viewModel) { + annotationStart = viewModel.mapOffset(annotationStart, true); + annotationEnd = viewModel.mapOffset(annotationEnd, true); + } + this._mergeStyleRanges(e.ranges, {start: annotationStart, end: annotationEnd, style: annotation.rangeStyle}); + } + if (annotation.lineStyle) { + e.style = this._mergeStyle({}, e.style); + e.style = this._mergeStyle(e.style, annotation.lineStyle); + } + } + } + }; + AnnotationTypeList.addMixin(AnnotationStyler.prototype); + + return { + FoldingAnnotation: FoldingAnnotation, + AnnotationTypeList: AnnotationTypeList, + AnnotationModel: AnnotationModel, + AnnotationStyler: AnnotationStyler + }; +}); +/******************************************************************************* + * @license + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: IBM Corporation - initial API and implementation + ******************************************************************************/ + +/*global define setTimeout clearTimeout setInterval clearInterval Node */ + +define("orion/textview/rulers", ['orion/textview/annotations', 'orion/textview/tooltip'], function(mAnnotations, mTooltip) { + + /** + * Constructs a new ruler. + *

+ * The default implementation does not implement all the methods in the interface + * and is useful only for objects implementing rulers. + *

+ * + * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler. + * @param {String} [rulerLocation="left"] the location for the ruler. + * @param {String} [rulerOverview="page"] the overview for the ruler. + * @param {orion.textview.Style} [rulerStyle] the style for the ruler. + * + * @class This interface represents a ruler for the text view. + *

+ * A Ruler is a graphical element that is placed either on the left or on the right side of + * the view. It can be used to provide the view with per line decoration such as line numbering, + * bookmarks, breakpoints, folding disclosures, etc. + *

+ * There are two types of rulers: page and document. A page ruler only shows the content for the lines that are + * visible, while a document ruler always shows the whole content. + *

+ * See:
+ * {@link orion.textview.LineNumberRuler}
+ * {@link orion.textview.AnnotationRuler}
+ * {@link orion.textview.OverviewRuler}
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#addRuler} + *

+ * @name orion.textview.Ruler + * @borrows orion.textview.AnnotationTypeList#addAnnotationType as #addAnnotationType + * @borrows orion.textview.AnnotationTypeList#getAnnotationTypePriority as #getAnnotationTypePriority + * @borrows orion.textview.AnnotationTypeList#getAnnotationsByType as #getAnnotationsByType + * @borrows orion.textview.AnnotationTypeList#isAnnotationTypeVisible as #isAnnotationTypeVisible + * @borrows orion.textview.AnnotationTypeList#removeAnnotationType as #removeAnnotationType + */ + function Ruler (annotationModel, rulerLocation, rulerOverview, rulerStyle) { + this._location = rulerLocation || "left"; + this._overview = rulerOverview || "page"; + this._rulerStyle = rulerStyle; + this._view = null; + var self = this; + this._listener = { + onTextModelChanged: function(e) { + self._onTextModelChanged(e); + }, + onAnnotationModelChanged: function(e) { + self._onAnnotationModelChanged(e); + } + }; + this.setAnnotationModel(annotationModel); + } + Ruler.prototype = /** @lends orion.textview.Ruler.prototype */ { + /** + * Returns the annotations for a given line range merging multiple + * annotations when necessary. + *

+ * This method is called by the text view when the ruler is redrawn. + *

+ * + * @param {Number} startLine the start line index + * @param {Number} endLine the end line index + * @return {orion.textview.Annotation[]} the annotations for the line range. The array might be sparse. + */ + getAnnotations: function(startLine, endLine) { + var annotationModel = this._annotationModel; + if (!annotationModel) { return []; } + var model = this._view.getModel(); + var start = model.getLineStart(startLine); + var end = model.getLineEnd(endLine - 1); + var baseModel = model; + if (model.getBaseModel) { + baseModel = model.getBaseModel(); + start = model.mapOffset(start); + end = model.mapOffset(end); + } + var result = []; + var annotations = this.getAnnotationsByType(annotationModel, start, end); + for (var i = 0; i < annotations.length; i++) { + var annotation = annotations[i]; + var annotationLineStart = baseModel.getLineAtOffset(annotation.start); + var annotationLineEnd = baseModel.getLineAtOffset(Math.max(annotation.start, annotation.end - 1)); + for (var lineIndex = annotationLineStart; lineIndex<=annotationLineEnd; lineIndex++) { + var visualLineIndex = lineIndex; + if (model !== baseModel) { + var ls = baseModel.getLineStart(lineIndex); + ls = model.mapOffset(ls, true); + if (ls === -1) { continue; } + visualLineIndex = model.getLineAtOffset(ls); + } + if (!(startLine <= visualLineIndex && visualLineIndex < endLine)) { continue; } + var rulerAnnotation = this._mergeAnnotation(result[visualLineIndex], annotation, lineIndex - annotationLineStart, annotationLineEnd - annotationLineStart + 1); + if (rulerAnnotation) { + result[visualLineIndex] = rulerAnnotation; + } + } + } + if (!this._multiAnnotation && this._multiAnnotationOverlay) { + for (var k in result) { + if (result[k]._multiple) { + result[k].html = result[k].html + this._multiAnnotationOverlay.html; + } + } + } + return result; + }, + /** + * Returns the annotation model. + * + * @returns {orion.textview.AnnotationModel} the ruler annotation model. + * + * @see #setAnnotationModel + */ + getAnnotationModel: function() { + return this._annotationModel; + }, + /** + * Returns the ruler location. + * + * @returns {String} the ruler location, which is either "left" or "right". + * + * @see #getOverview + */ + getLocation: function() { + return this._location; + }, + /** + * Returns the ruler overview type. + * + * @returns {String} the overview type, which is either "page" or "document". + * + * @see #getLocation + */ + getOverview: function() { + return this._overview; + }, + /** + * Returns the style information for the ruler. + * + * @returns {orion.textview.Style} the style information. + */ + getRulerStyle: function() { + return this._rulerStyle; + }, + /** + * Returns the widest annotation which determines the width of the ruler. + *

+ * If the ruler does not have a fixed width it should provide the widest + * annotation to avoid the ruler from changing size as the view scrolls. + *

+ *

+ * This method is called by the text view when the ruler is redrawn. + *

+ * + * @returns {orion.textview.Annotation} the widest annotation. + * + * @see #getAnnotations + */ + getWidestAnnotation: function() { + return null; + }, + /** + * Sets the annotation model for the ruler. + * + * @param {orion.textview.AnnotationModel} annotationModel the annotation model. + * + * @see #getAnnotationModel + */ + setAnnotationModel: function (annotationModel) { + if (this._annotationModel) { + this._annotationModel.removEventListener("Changed", this._listener.onAnnotationModelChanged); + } + this._annotationModel = annotationModel; + if (this._annotationModel) { + this._annotationModel.addEventListener("Changed", this._listener.onAnnotationModelChanged); + } + }, + /** + * Sets the annotation that is displayed when a given line contains multiple + * annotations. This annotation is used when there are different types of + * annotations in a given line. + * + * @param {orion.textview.Annotation} annotation the annotation for lines with multiple annotations. + * + * @see #setMultiAnnotationOverlay + */ + setMultiAnnotation: function(annotation) { + this._multiAnnotation = annotation; + }, + /** + * Sets the annotation that overlays a line with multiple annotations. This annotation is displayed on + * top of the computed annotation for a given line when there are multiple annotations of the same type + * in the line. It is also used when the multiple annotation is not set. + * + * @param {orion.textview.Annotation} annotation the annotation overlay for lines with multiple annotations. + * + * @see #setMultiAnnotation + */ + setMultiAnnotationOverlay: function(annotation) { + this._multiAnnotationOverlay = annotation; + }, + /** + * Sets the view for the ruler. + *

+ * This method is called by the text view when the ruler + * is added to the view. + *

+ * + * @param {orion.textview.TextView} view the text view. + */ + setView: function (view) { + if (this._onTextModelChanged && this._view) { + this._view.removeEventListener("ModelChanged", this._listener.onTextModelChanged); + } + this._view = view; + if (this._onTextModelChanged && this._view) { + this._view.addEventListener("ModelChanged", this._listener.onTextModelChanged); + } + }, + /** + * This event is sent when the user clicks a line annotation. + * + * @event + * @param {Number} lineIndex the line index of the annotation under the pointer. + * @param {DOMEvent} e the click event. + */ + onClick: function(lineIndex, e) { + }, + /** + * This event is sent when the user double clicks a line annotation. + * + * @event + * @param {Number} lineIndex the line index of the annotation under the pointer. + * @param {DOMEvent} e the double click event. + */ + onDblClick: function(lineIndex, e) { + }, + /** + * This event is sent when the user moves the mouse over a line annotation. + * + * @event + * @param {Number} lineIndex the line index of the annotation under the pointer. + * @param {DOMEvent} e the mouse move event. + */ + onMouseMove: function(lineIndex, e) { + var tooltip = mTooltip.Tooltip.getTooltip(this._view); + if (!tooltip) { return; } + if (tooltip.isVisible() && this._tooltipLineIndex === lineIndex) { return; } + this._tooltipLineIndex = lineIndex; + var self = this; + tooltip.setTarget({ + y: e.clientY, + getTooltipInfo: function() { + return self._getTooltipInfo(self._tooltipLineIndex, this.y); + } + }); + }, + /** + * This event is sent when the mouse pointer enters a line annotation. + * + * @event + * @param {Number} lineIndex the line index of the annotation under the pointer. + * @param {DOMEvent} e the mouse over event. + */ + onMouseOver: function(lineIndex, e) { + this.onMouseMove(lineIndex, e); + }, + /** + * This event is sent when the mouse pointer exits a line annotation. + * + * @event + * @param {Number} lineIndex the line index of the annotation under the pointer. + * @param {DOMEvent} e the mouse out event. + */ + onMouseOut: function(lineIndex, e) { + var tooltip = mTooltip.Tooltip.getTooltip(this._view); + if (!tooltip) { return; } + tooltip.setTarget(null); + }, + /** @ignore */ + _getTooltipInfo: function(lineIndex, y) { + if (lineIndex === undefined) { return; } + var view = this._view; + var model = view.getModel(); + var annotationModel = this._annotationModel; + var annotations = []; + if (annotationModel) { + var start = model.getLineStart(lineIndex); + var end = model.getLineEnd(lineIndex); + if (model.getBaseModel) { + start = model.mapOffset(start); + end = model.mapOffset(end); + } + annotations = this.getAnnotationsByType(annotationModel, start, end); + } + var contents = this._getTooltipContents(lineIndex, annotations); + if (!contents) { return null; } + var info = { + contents: contents, + anchor: this.getLocation() + }; + var rect = view.getClientArea(); + if (this.getOverview() === "document") { + rect.y = view.convert({y: y}, "view", "document").y; + } else { + rect.y = view.getLocationAtOffset(model.getLineStart(lineIndex)).y; + } + view.convert(rect, "document", "page"); + info.x = rect.x; + info.y = rect.y; + if (info.anchor === "right") { + info.x += rect.width; + } + info.maxWidth = rect.width; + info.maxHeight = rect.height - (rect.y - view._parent.getBoundingClientRect().top); + return info; + }, + /** @ignore */ + _getTooltipContents: function(lineIndex, annotations) { + return annotations; + }, + /** @ignore */ + _onAnnotationModelChanged: function(e) { + var view = this._view; + if (!view) { return; } + var model = view.getModel(), self = this; + var lineCount = model.getLineCount(); + if (e.textModelChangedEvent) { + var start = e.textModelChangedEvent.start; + if (model.getBaseModel) { start = model.mapOffset(start, true); } + var startLine = model.getLineAtOffset(start); + view.redrawLines(startLine, lineCount, self); + return; + } + function redraw(changes) { + for (var i = 0; i < changes.length; i++) { + if (!self.isAnnotationTypeVisible(changes[i].type)) { continue; } + var start = changes[i].start; + var end = changes[i].end; + if (model.getBaseModel) { + start = model.mapOffset(start, true); + end = model.mapOffset(end, true); + } + if (start !== -1 && end !== -1) { + view.redrawLines(model.getLineAtOffset(start), model.getLineAtOffset(Math.max(start, end - 1)) + 1, self); + } + } + } + redraw(e.added); + redraw(e.removed); + redraw(e.changed); + }, + /** @ignore */ + _mergeAnnotation: function(result, annotation, annotationLineIndex, annotationLineCount) { + if (!result) { result = {}; } + if (annotationLineIndex === 0) { + if (result.html && annotation.html) { + if (annotation.html !== result.html) { + if (!result._multiple && this._multiAnnotation) { + result.html = this._multiAnnotation.html; + } + } + result._multiple = true; + } else { + result.html = annotation.html; + } + } + result.style = this._mergeStyle(result.style, annotation.style); + return result; + }, + /** @ignore */ + _mergeStyle: function(result, style) { + if (style) { + if (!result) { result = {}; } + if (result.styleClass && style.styleClass && result.styleClass !== style.styleClass) { + result.styleClass += " " + style.styleClass; + } else { + result.styleClass = style.styleClass; + } + var prop; + if (style.style) { + if (!result.style) { result.style = {}; } + for (prop in style.style) { + if (!result.style[prop]) { + result.style[prop] = style.style[prop]; + } + } + } + if (style.attributes) { + if (!result.attributes) { result.attributes = {}; } + for (prop in style.attributes) { + if (!result.attributes[prop]) { + result.attributes[prop] = style.attributes[prop]; + } + } + } + } + return result; + } + }; + mAnnotations.AnnotationTypeList.addMixin(Ruler.prototype); + + /** + * Constructs a new line numbering ruler. + * + * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler. + * @param {String} [rulerLocation="left"] the location for the ruler. + * @param {orion.textview.Style} [rulerStyle=undefined] the style for the ruler. + * @param {orion.textview.Style} [oddStyle={style: {backgroundColor: "white"}] the style for lines with odd line index. + * @param {orion.textview.Style} [evenStyle={backgroundColor: "white"}] the style for lines with even line index. + * + * @augments orion.textview.Ruler + * @class This objects implements a line numbering ruler. + * + *

See:
+ * {@link orion.textview.Ruler} + *

+ * @name orion.textview.LineNumberRuler + */ + function LineNumberRuler (annotationModel, rulerLocation, rulerStyle, oddStyle, evenStyle) { + Ruler.call(this, annotationModel, rulerLocation, "page", rulerStyle); + this._oddStyle = oddStyle || {style: {backgroundColor: "white"}}; + this._evenStyle = evenStyle || {style: {backgroundColor: "white"}}; + this._numOfDigits = 0; + } + LineNumberRuler.prototype = new Ruler(); + /** @ignore */ + LineNumberRuler.prototype.getAnnotations = function(startLine, endLine) { + var result = Ruler.prototype.getAnnotations.call(this, startLine, endLine); + var model = this._view.getModel(); + for (var lineIndex = startLine; lineIndex < endLine; lineIndex++) { + var style = lineIndex & 1 ? this._oddStyle : this._evenStyle; + var mapLine = lineIndex; + if (model.getBaseModel) { + var lineStart = model.getLineStart(mapLine); + mapLine = model.getBaseModel().getLineAtOffset(model.mapOffset(lineStart)); + } + if (!result[lineIndex]) { result[lineIndex] = {}; } + result[lineIndex].html = (mapLine + 1) + ""; + if (!result[lineIndex].style) { result[lineIndex].style = style; } + } + return result; + }; + /** @ignore */ + LineNumberRuler.prototype.getWidestAnnotation = function() { + var lineCount = this._view.getModel().getLineCount(); + return this.getAnnotations(lineCount - 1, lineCount)[lineCount - 1]; + }; + /** @ignore */ + LineNumberRuler.prototype._onTextModelChanged = function(e) { + var start = e.start; + var model = this._view.getModel(); + var lineCount = model.getBaseModel ? model.getBaseModel().getLineCount() : model.getLineCount(); + var numOfDigits = (lineCount+"").length; + if (this._numOfDigits !== numOfDigits) { + this._numOfDigits = numOfDigits; + var startLine = model.getLineAtOffset(start); + this._view.redrawLines(startLine, model.getLineCount(), this); + } + }; + + /** + * @class This is class represents an annotation for the AnnotationRuler. + *

+ * See:
+ * {@link orion.textview.AnnotationRuler} + *

+ * + * @name orion.textview.Annotation + * + * @property {String} [html=""] The html content for the annotation, typically contains an image. + * @property {orion.textview.Style} [style] the style for the annotation. + * @property {orion.textview.Style} [overviewStyle] the style for the annotation in the overview ruler. + */ + /** + * Constructs a new annotation ruler. + * + * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler. + * @param {String} [rulerLocation="left"] the location for the ruler. + * @param {orion.textview.Style} [rulerStyle=undefined] the style for the ruler. + * @param {orion.textview.Annotation} [defaultAnnotation] the default annotation. + * + * @augments orion.textview.Ruler + * @class This objects implements an annotation ruler. + * + *

See:
+ * {@link orion.textview.Ruler}
+ * {@link orion.textview.Annotation} + *

+ * @name orion.textview.AnnotationRuler + */ + function AnnotationRuler (annotationModel, rulerLocation, rulerStyle) { + Ruler.call(this, annotationModel, rulerLocation, "page", rulerStyle); + } + AnnotationRuler.prototype = new Ruler(); + + /** + * Constructs a new overview ruler. + *

+ * The overview ruler is used in conjunction with a AnnotationRuler, for each annotation in the + * AnnotationRuler this ruler displays a mark in the overview. Clicking on the mark causes the + * view to scroll to the annotated line. + *

+ * + * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler. + * @param {String} [rulerLocation="left"] the location for the ruler. + * @param {orion.textview.Style} [rulerStyle=undefined] the style for the ruler. + * + * @augments orion.textview.Ruler + * @class This objects implements an overview ruler. + * + *

See:
+ * {@link orion.textview.AnnotationRuler}
+ * {@link orion.textview.Ruler} + *

+ * @name orion.textview.OverviewRuler + */ + function OverviewRuler (annotationModel, rulerLocation, rulerStyle) { + Ruler.call(this, annotationModel, rulerLocation, "document", rulerStyle); + } + OverviewRuler.prototype = new Ruler(); + + /** @ignore */ + OverviewRuler.prototype.getRulerStyle = function() { + var result = {style: {lineHeight: "1px", fontSize: "1px"}}; + result = this._mergeStyle(result, this._rulerStyle); + return result; + }; + /** @ignore */ + OverviewRuler.prototype.onClick = function(lineIndex, e) { + if (lineIndex === undefined) { return; } + this._view.setTopIndex(lineIndex); + }; + /** @ignore */ + OverviewRuler.prototype._getTooltipContents = function(lineIndex, annotations) { + if (annotations.length === 0) { + var model = this._view.getModel(); + var mapLine = lineIndex; + if (model.getBaseModel) { + var lineStart = model.getLineStart(mapLine); + mapLine = model.getBaseModel().getLineAtOffset(model.mapOffset(lineStart)); + } + return "Line: " + (mapLine + 1); + } + return Ruler.prototype._getTooltipContents.call(this, lineIndex, annotations); + }; + /** @ignore */ + OverviewRuler.prototype._mergeAnnotation = function(previousAnnotation, annotation, annotationLineIndex, annotationLineCount) { + if (annotationLineIndex !== 0) { return undefined; } + var result = previousAnnotation; + if (!result) { + //TODO annotationLineCount does not work when there are folded lines + var height = 3 * annotationLineCount; + result = {html: " ", style: { style: {height: height + "px"}}}; + result.style = this._mergeStyle(result.style, annotation.overviewStyle); + } + return result; + }; + + /** + * Constructs a new folding ruler. + * + * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler. + * @param {String} [rulerLocation="left"] the location for the ruler. + * @param {orion.textview.Style} [rulerStyle=undefined] the style for the ruler. + * + * @augments orion.textview.Ruler + * @class This objects implements an overview ruler. + * + *

See:
+ * {@link orion.textview.AnnotationRuler}
+ * {@link orion.textview.Ruler} + *

+ * @name orion.textview.OverviewRuler + */ + function FoldingRuler (annotationModel, rulerLocation, rulerStyle) { + AnnotationRuler.call(this, annotationModel, rulerLocation, rulerStyle); + } + FoldingRuler.prototype = new AnnotationRuler(); + + /** @ignore */ + FoldingRuler.prototype.onClick = function(lineIndex, e) { + if (lineIndex === undefined) { return; } + var annotationModel = this._annotationModel; + if (!annotationModel) { return; } + var view = this._view; + var model = view.getModel(); + var start = model.getLineStart(lineIndex); + var end = model.getLineEnd(lineIndex, true); + if (model.getBaseModel) { + start = model.mapOffset(start); + end = model.mapOffset(end); + } + var annotation, iter = annotationModel.getAnnotations(start, end); + while (!annotation && iter.hasNext()) { + var a = iter.next(); + if (!this.isAnnotationTypeVisible(a.type)) { continue; } + annotation = a; + } + if (annotation) { + var tooltip = mTooltip.Tooltip.getTooltip(this._view); + if (tooltip) { + tooltip.setTarget(null); + } + if (annotation.expanded) { + annotation.collapse(); + } else { + annotation.expand(); + } + this._annotationModel.modifyAnnotation(annotation); + } + }; + /** @ignore */ + FoldingRuler.prototype._getTooltipContents = function(lineIndex, annotations) { + if (annotations.length === 1) { + if (annotations[0].expanded) { + return null; + } + } + return AnnotationRuler.prototype._getTooltipContents.call(this, lineIndex, annotations); + }; + /** @ignore */ + FoldingRuler.prototype._onAnnotationModelChanged = function(e) { + if (e.textModelChangedEvent) { + AnnotationRuler.prototype._onAnnotationModelChanged.call(this, e); + return; + } + var view = this._view; + if (!view) { return; } + var model = view.getModel(), self = this, i; + var lineCount = model.getLineCount(), lineIndex = lineCount; + function redraw(changes) { + for (i = 0; i < changes.length; i++) { + if (!self.isAnnotationTypeVisible(changes[i].type)) { continue; } + var start = changes[i].start; + if (model.getBaseModel) { + start = model.mapOffset(start, true); + } + if (start !== -1) { + lineIndex = Math.min(lineIndex, model.getLineAtOffset(start)); + } + } + } + redraw(e.added); + redraw(e.removed); + redraw(e.changed); + var rulers = view.getRulers(); + for (i = 0; i < rulers.length; i++) { + view.redrawLines(lineIndex, lineCount, rulers[i]); + } + }; + + return { + Ruler: Ruler, + AnnotationRuler: AnnotationRuler, + LineNumberRuler: LineNumberRuler, + OverviewRuler: OverviewRuler, + FoldingRuler: FoldingRuler + }; +}); +/******************************************************************************* + * @license + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: IBM Corporation - initial API and implementation + ******************************************************************************/ + +/*global define */ + +define("orion/textview/undoStack", [], function() { + + /** + * Constructs a new Change object. + * + * @class + * @name orion.textview.Change + * @private + */ + function Change(offset, text, previousText) { + this.offset = offset; + this.text = text; + this.previousText = previousText; + } + Change.prototype = { + /** @ignore */ + undo: function (view, select) { + this._doUndoRedo(this.offset, this.previousText, this.text, view, select); + }, + /** @ignore */ + redo: function (view, select) { + this._doUndoRedo(this.offset, this.text, this.previousText, view, select); + }, + _doUndoRedo: function(offset, text, previousText, view, select) { + var model = view.getModel(); + /* + * TODO UndoStack should be changing the text in the base model. + * This is code needs to change when modifications in the base + * model are supported properly by the projection model. + */ + if (model.mapOffset && view.annotationModel) { + var mapOffset = model.mapOffset(offset, true); + if (mapOffset < 0) { + var annotationModel = view.annotationModel; + var iter = annotationModel.getAnnotations(offset, offset + 1); + while (iter.hasNext()) { + var annotation = iter.next(); + if (annotation.type === "orion.annotation.folding") { + annotation.expand(); + mapOffset = model.mapOffset(offset, true); + break; + } + } + } + if (mapOffset < 0) { return; } + offset = mapOffset; + } + view.setText(text, offset, offset + previousText.length); + if (select) { + view.setSelection(offset, offset + text.length); + } + } + }; + + /** + * Constructs a new CompoundChange object. + * + * @class + * @name orion.textview.CompoundChange + * @private + */ + function CompoundChange () { + this.changes = []; + } + CompoundChange.prototype = { + /** @ignore */ + add: function (change) { + this.changes.push(change); + }, + /** @ignore */ + end: function (view) { + this.endSelection = view.getSelection(); + this.endCaret = view.getCaretOffset(); + }, + /** @ignore */ + undo: function (view, select) { + for (var i=this.changes.length - 1; i >= 0; i--) { + this.changes[i].undo(view, false); + } + if (select) { + var start = this.startSelection.start; + var end = this.startSelection.end; + view.setSelection(this.startCaret ? start : end, this.startCaret ? end : start); + } + }, + /** @ignore */ + redo: function (view, select) { + for (var i = 0; i < this.changes.length; i++) { + this.changes[i].redo(view, false); + } + if (select) { + var start = this.endSelection.start; + var end = this.endSelection.end; + view.setSelection(this.endCaret ? start : end, this.endCaret ? end : start); + } + }, + /** @ignore */ + start: function (view) { + this.startSelection = view.getSelection(); + this.startCaret = view.getCaretOffset(); + } + }; + + /** + * Constructs a new UndoStack on a text view. + * + * @param {orion.textview.TextView} view the text view for the undo stack. + * @param {Number} [size=100] the size for the undo stack. + * + * @name orion.textview.UndoStack + * @class The UndoStack is used to record the history of a text model associated to an view. Every + * change to the model is added to stack, allowing the application to undo and redo these changes. + * + *

+ * See:
+ * {@link orion.textview.TextView}
+ *

+ */ + function UndoStack (view, size) { + this.view = view; + this.size = size !== undefined ? size : 100; + this.reset(); + var model = view.getModel(); + if (model.getBaseModel) { + model = model.getBaseModel(); + } + this.model = model; + var self = this; + this._listener = { + onChanging: function(e) { + self._onChanging(e); + }, + onDestroy: function(e) { + self._onDestroy(e); + } + }; + model.addEventListener("Changing", this._listener.onChanging); + view.addEventListener("Destroy", this._listener.onDestroy); + } + UndoStack.prototype = /** @lends orion.textview.UndoStack.prototype */ { + /** + * Adds a change to the stack. + * + * @param change the change to add. + * @param {Number} change.offset the offset of the change + * @param {String} change.text the new text of the change + * @param {String} change.previousText the previous text of the change + */ + add: function (change) { + if (this.compoundChange) { + this.compoundChange.add(change); + } else { + var length = this.stack.length; + this.stack.splice(this.index, length-this.index, change); + this.index++; + if (this.stack.length > this.size) { + this.stack.shift(); + this.index--; + this.cleanIndex--; + } + } + }, + /** + * Marks the current state of the stack as clean. + * + *

+ * This function is typically called when the content of view associated with the stack is saved. + *

+ * + * @see #isClean + */ + markClean: function() { + this.endCompoundChange(); + this._commitUndo(); + this.cleanIndex = this.index; + }, + /** + * Returns true if current state of stack is the same + * as the state when markClean() was called. + * + *

+ * For example, the application calls markClean(), then calls undo() four times and redo() four times. + * At this point isClean() returns true. + *

+ *

+ * This function is typically called to determine if the content of the view associated with the stack + * has changed since the last time it was saved. + *

+ * + * @return {Boolean} returns if the state is the same as the state when markClean() was called. + * + * @see #markClean + */ + isClean: function() { + return this.cleanIndex === this.getSize().undo; + }, + /** + * Returns true if there is at least one change to undo. + * + * @return {Boolean} returns true if there is at least one change to undo. + * + * @see #canRedo + * @see #undo + */ + canUndo: function() { + return this.getSize().undo > 0; + }, + /** + * Returns true if there is at least one change to redo. + * + * @return {Boolean} returns true if there is at least one change to redo. + * + * @see #canUndo + * @see #redo + */ + canRedo: function() { + return this.getSize().redo > 0; + }, + /** + * Finishes a compound change. + * + * @see #startCompoundChange + */ + endCompoundChange: function() { + if (this.compoundChange) { + this.compoundChange.end(this.view); + } + this.compoundChange = undefined; + }, + /** + * Returns the sizes of the stack. + * + * @return {object} a object where object.undo is the number of changes that can be un-done, + * and object.redo is the number of changes that can be re-done. + * + * @see #canUndo + * @see #canRedo + */ + getSize: function() { + var index = this.index; + var length = this.stack.length; + if (this._undoStart !== undefined) { + index++; + } + return {undo: index, redo: (length - index)}; + }, + /** + * Undo the last change in the stack. + * + * @return {Boolean} returns true if a change was un-done. + * + * @see #redo + * @see #canUndo + */ + undo: function() { + this._commitUndo(); + if (this.index <= 0) { + return false; + } + var change = this.stack[--this.index]; + this._ignoreUndo = true; + change.undo(this.view, true); + this._ignoreUndo = false; + return true; + }, + /** + * Redo the last change in the stack. + * + * @return {Boolean} returns true if a change was re-done. + * + * @see #undo + * @see #canRedo + */ + redo: function() { + this._commitUndo(); + if (this.index >= this.stack.length) { + return false; + } + var change = this.stack[this.index++]; + this._ignoreUndo = true; + change.redo(this.view, true); + this._ignoreUndo = false; + return true; + }, + /** + * Reset the stack to its original state. All changes in the stack are thrown away. + */ + reset: function() { + this.index = this.cleanIndex = 0; + this.stack = []; + this._undoStart = undefined; + this._undoText = ""; + this._undoType = 0; + this._ignoreUndo = false; + this._compoundChange = undefined; + }, + /** + * Starts a compound change. + *

+ * All changes added to stack from the time startCompoundChange() is called + * to the time that endCompoundChange() is called are compound on one change that can be un-done or re-done + * with one single call to undo() or redo(). + *

+ * + * @see #endCompoundChange + */ + startCompoundChange: function() { + this._commitUndo(); + var change = new CompoundChange(); + this.add(change); + this.compoundChange = change; + this.compoundChange.start(this.view); + }, + _commitUndo: function () { + if (this._undoStart !== undefined) { + if (this._undoType === -1) { + this.add(new Change(this._undoStart, "", this._undoText, "")); + } else { + this.add(new Change(this._undoStart, this._undoText, "")); + } + this._undoStart = undefined; + this._undoText = ""; + this._undoType = 0; + } + }, + _onDestroy: function(evt) { + this.model.removeEventListener("Changing", this._listener.onChanging); + this.view.removeEventListener("Destroy", this._listener.onDestroy); + }, + _onChanging: function(e) { + var newText = e.text; + var start = e.start; + var removedCharCount = e.removedCharCount; + var addedCharCount = e.addedCharCount; + if (this._ignoreUndo) { + return; + } + if (this._undoStart !== undefined && + !((addedCharCount === 1 && removedCharCount === 0 && this._undoType === 1 && start === this._undoStart + this._undoText.length) || + (addedCharCount === 0 && removedCharCount === 1 && this._undoType === -1 && (((start + 1) === this._undoStart) || (start === this._undoStart))))) + { + this._commitUndo(); + } + if (!this.compoundChange) { + if (addedCharCount === 1 && removedCharCount === 0) { + if (this._undoStart === undefined) { + this._undoStart = start; + } + this._undoText = this._undoText + newText; + this._undoType = 1; + return; + } else if (addedCharCount === 0 && removedCharCount === 1) { + var deleting = this._undoText.length > 0 && this._undoStart === start; + this._undoStart = start; + this._undoType = -1; + if (deleting) { + this._undoText = this._undoText + this.model.getText(start, start + removedCharCount); + } else { + this._undoText = this.model.getText(start, start + removedCharCount) + this._undoText; + } + return; + } + } + this.add(new Change(start, newText, this.model.getText(start, start + removedCharCount))); + } + }; + + return { + UndoStack: UndoStack + }; +}); +/******************************************************************************* + * @license + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: + * Felipe Heidrich (IBM Corporation) - initial API and implementation + * Silenio Quarti (IBM Corporation) - initial API and implementation + ******************************************************************************/ + +/*global define window*/ + +define("orion/textview/textModel", ['orion/textview/eventTarget'], function(mEventTarget) { + var isWindows = window.navigator.platform.indexOf("Win") !== -1; + + /** + * Constructs a new TextModel with the given text and default line delimiter. + * + * @param {String} [text=""] the text that the model will store + * @param {String} [lineDelimiter=platform delimiter] the line delimiter used when inserting new lines to the model. + * + * @name orion.textview.TextModel + * @class The TextModel is an interface that provides text for the view. Applications may + * implement the TextModel interface to provide a custom store for the view content. The + * view interacts with its text model in order to access and update the text that is being + * displayed and edited in the view. This is the default implementation. + *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#setModel} + *

+ * @borrows orion.textview.EventTarget#addEventListener as #addEventListener + * @borrows orion.textview.EventTarget#removeEventListener as #removeEventListener + * @borrows orion.textview.EventTarget#dispatchEvent as #dispatchEvent + */ + function TextModel(text, lineDelimiter) { + this._lastLineIndex = -1; + this._text = [""]; + this._lineOffsets = [0]; + this.setText(text); + this.setLineDelimiter(lineDelimiter); + } + + TextModel.prototype = /** @lends orion.textview.TextModel.prototype */ { + /** + * Returns the number of characters in the model. + * + * @returns {Number} the number of characters in the model. + */ + getCharCount: function() { + var count = 0; + for (var i = 0; i + * The valid indices are 0 to line count exclusive. Returns null + * if the index is out of range. + *

+ * + * @param {Number} lineIndex the zero based index of the line. + * @param {Boolean} [includeDelimiter=false] whether or not to include the line delimiter. + * @returns {String} the line text or null if out of range. + * + * @see #getLineAtOffset + */ + getLine: function(lineIndex, includeDelimiter) { + var lineCount = this.getLineCount(); + if (!(0 <= lineIndex && lineIndex < lineCount)) { + return null; + } + var start = this._lineOffsets[lineIndex]; + if (lineIndex + 1 < lineCount) { + var text = this.getText(start, this._lineOffsets[lineIndex + 1]); + if (includeDelimiter) { + return text; + } + var end = text.length, c; + while (((c = text.charCodeAt(end - 1)) === 10) || (c === 13)) { + end--; + } + return text.substring(0, end); + } else { + return this.getText(start); + } + }, + /** + * Returns the line index at the given character offset. + *

+ * The valid offsets are 0 to char count inclusive. The line index for + * char count is line count - 1. Returns -1 if + * the offset is out of range. + *

+ * + * @param {Number} offset a character offset. + * @returns {Number} the zero based line index or -1 if out of range. + */ + getLineAtOffset: function(offset) { + var charCount = this.getCharCount(); + if (!(0 <= offset && offset <= charCount)) { + return -1; + } + var lineCount = this.getLineCount(); + if (offset === charCount) { + return lineCount - 1; + } + var lineStart, lineEnd; + var index = this._lastLineIndex; + if (0 <= index && index < lineCount) { + lineStart = this._lineOffsets[index]; + lineEnd = index + 1 < lineCount ? this._lineOffsets[index + 1] : charCount; + if (lineStart <= offset && offset < lineEnd) { + return index; + } + } + var high = lineCount; + var low = -1; + while (high - low > 1) { + index = Math.floor((high + low) / 2); + lineStart = this._lineOffsets[index]; + lineEnd = index + 1 < lineCount ? this._lineOffsets[index + 1] : charCount; + if (offset <= lineStart) { + high = index; + } else if (offset < lineEnd) { + high = index; + break; + } else { + low = index; + } + } + this._lastLineIndex = high; + return high; + }, + /** + * Returns the number of lines in the model. + *

+ * The model always has at least one line. + *

+ * + * @returns {Number} the number of lines. + */ + getLineCount: function() { + return this._lineOffsets.length; + }, + /** + * Returns the line delimiter that is used by the view + * when inserting new lines. New lines entered using key strokes + * and paste operations use this line delimiter. + * + * @return {String} the line delimiter that is used by the view when inserting new lines. + */ + getLineDelimiter: function() { + return this._lineDelimiter; + }, + /** + * Returns the end character offset for the given line. + *

+ * The end offset is not inclusive. This means that when the line delimiter is included, the + * offset is either the start offset of the next line or char count. When the line delimiter is + * not included, the offset is the offset of the line delimiter. + *

+ *

+ * The valid indices are 0 to line count exclusive. Returns -1 + * if the index is out of range. + *

+ * + * @param {Number} lineIndex the zero based index of the line. + * @param {Boolean} [includeDelimiter=false] whether or not to include the line delimiter. + * @return {Number} the line end offset or -1 if out of range. + * + * @see #getLineStart + */ + getLineEnd: function(lineIndex, includeDelimiter) { + var lineCount = this.getLineCount(); + if (!(0 <= lineIndex && lineIndex < lineCount)) { + return -1; + } + if (lineIndex + 1 < lineCount) { + var end = this._lineOffsets[lineIndex + 1]; + if (includeDelimiter) { + return end; + } + var text = this.getText(Math.max(this._lineOffsets[lineIndex], end - 2), end); + var i = text.length, c; + while (((c = text.charCodeAt(i - 1)) === 10) || (c === 13)) { + i--; + } + return end - (text.length - i); + } else { + return this.getCharCount(); + } + }, + /** + * Returns the start character offset for the given line. + *

+ * The valid indices are 0 to line count exclusive. Returns -1 + * if the index is out of range. + *

+ * + * @param {Number} lineIndex the zero based index of the line. + * @return {Number} the line start offset or -1 if out of range. + * + * @see #getLineEnd + */ + getLineStart: function(lineIndex) { + if (!(0 <= lineIndex && lineIndex < this.getLineCount())) { + return -1; + } + return this._lineOffsets[lineIndex]; + }, + /** + * Returns the text for the given range. + *

+ * The end offset is not inclusive. This means that character at the end offset + * is not included in the returned text. + *

+ * + * @param {Number} [start=0] the zero based start offset of text range. + * @param {Number} [end=char count] the zero based end offset of text range. + * + * @see #setText + */ + getText: function(start, end) { + if (start === undefined) { start = 0; } + if (end === undefined) { end = this.getCharCount(); } + if (start === end) { return ""; } + var offset = 0, chunk = 0, length; + while (chunk + * This notification is intended to be used only by the view. Application clients should + * use {@link orion.textview.TextView#event:onModelChanging}. + *

+ *

+ * NOTE: This method is not meant to called directly by application code. It is called internally by the TextModel + * as part of the implementation of {@link #setText}. This method is included in the public API for documentation + * purposes and to allow integration with other toolkit frameworks. + *

+ * + * @param {orion.textview.ModelChangingEvent} modelChangingEvent the changing event + */ + onChanging: function(modelChangingEvent) { + return this.dispatchEvent(modelChangingEvent); + }, + /** + * Notifies all listeners that the text has changed. + *

+ * This notification is intended to be used only by the view. Application clients should + * use {@link orion.textview.TextView#event:onModelChanged}. + *

+ *

+ * NOTE: This method is not meant to called directly by application code. It is called internally by the TextModel + * as part of the implementation of {@link #setText}. This method is included in the public API for documentation + * purposes and to allow integration with other toolkit frameworks. + *

+ * + * @param {orion.textview.ModelChangedEvent} modelChangedEvent the changed event + */ + onChanged: function(modelChangedEvent) { + return this.dispatchEvent(modelChangedEvent); + }, + /** + * Sets the line delimiter that is used by the view + * when new lines are inserted in the model due to key + * strokes and paste operations. + *

+ * If lineDelimiter is "auto", the delimiter is computed to be + * the first delimiter found the in the current text. If lineDelimiter + * is undefined or if there are no delimiters in the current text, the + * platform delimiter is used. + *

+ * + * @param {String} lineDelimiter the line delimiter that is used by the view when inserting new lines. + */ + setLineDelimiter: function(lineDelimiter) { + if (lineDelimiter === "auto") { + lineDelimiter = undefined; + if (this.getLineCount() > 1) { + lineDelimiter = this.getText(this.getLineEnd(0), this.getLineEnd(0, true)); + } + } + this._lineDelimiter = lineDelimiter ? lineDelimiter : (isWindows ? "\r\n" : "\n"); + }, + /** + * Replaces the text in the given range with the given text. + *

+ * The end offset is not inclusive. This means that the character at the + * end offset is not replaced. + *

+ *

+ * The text model must notify the listeners before and after the + * the text is changed by calling {@link #onChanging} and {@link #onChanged} + * respectively. + *

+ * + * @param {String} [text=""] the new text. + * @param {Number} [start=0] the zero based start offset of text range. + * @param {Number} [end=char count] the zero based end offset of text range. + * + * @see #getText + */ + setText: function(text, start, end) { + if (text === undefined) { text = ""; } + if (start === undefined) { start = 0; } + if (end === undefined) { end = this.getCharCount(); } + if (start === end && text === "") { return; } + var startLine = this.getLineAtOffset(start); + var endLine = this.getLineAtOffset(end); + var eventStart = start; + var removedCharCount = end - start; + var removedLineCount = endLine - startLine; + var addedCharCount = text.length; + var addedLineCount = 0; + var lineCount = this.getLineCount(); + + var cr = 0, lf = 0, index = 0; + var newLineOffsets = []; + while (true) { + if (cr !== -1 && cr <= index) { cr = text.indexOf("\r", index); } + if (lf !== -1 && lf <= index) { lf = text.indexOf("\n", index); } + if (lf === -1 && cr === -1) { break; } + if (cr !== -1 && lf !== -1) { + if (cr + 1 === lf) { + index = lf + 1; + } else { + index = (cr < lf ? cr : lf) + 1; + } + } else if (cr !== -1) { + index = cr + 1; + } else { + index = lf + 1; + } + newLineOffsets.push(start + index); + addedLineCount++; + } + + var modelChangingEvent = { + type: "Changing", + text: text, + start: eventStart, + removedCharCount: removedCharCount, + addedCharCount: addedCharCount, + removedLineCount: removedLineCount, + addedLineCount: addedLineCount + }; + this.onChanging(modelChangingEvent); + + //TODO this should be done the loops below to avoid getText() + if (newLineOffsets.length === 0) { + var startLineOffset = this.getLineStart(startLine), endLineOffset; + if (endLine + 1 < lineCount) { + endLineOffset = this.getLineStart(endLine + 1); + } else { + endLineOffset = this.getCharCount(); + } + if (start !== startLineOffset) { + text = this.getText(startLineOffset, start) + text; + start = startLineOffset; + } + if (end !== endLineOffset) { + text = text + this.getText(end, endLineOffset); + end = endLineOffset; + } + } + + var changeCount = addedCharCount - removedCharCount; + for (var j = startLine + removedLineCount + 1; j < lineCount; j++) { + this._lineOffsets[j] += changeCount; + } + var args = [startLine + 1, removedLineCount].concat(newLineOffsets); + Array.prototype.splice.apply(this._lineOffsets, args); + + var offset = 0, chunk = 0, length; + while (chunk + * See:
+ * {@link orion.textview.ProjectionTextModel}
+ * {@link orion.textview.ProjectionTextModel#addProjection}
+ *

+ * @name orion.textview.Projection + * + * @property {Number} start The start offset of the projection range. + * @property {Number} end The end offset of the projection range. This offset is exclusive. + * @property {String|orion.textview.TextModel} [text=""] The projection text to be inserted + */ + /** + * Constructs a new ProjectionTextModel based on the specified TextModel. + * + * @param {orion.textview.TextModel} baseModel The base text model. + * + * @name orion.textview.ProjectionTextModel + * @class The ProjectionTextModel represents a projection of its base text + * model. Projection ranges can be added to the projection text model to hide and/or insert + * ranges to the base text model. + *

+ * The contents of the projection text model is modified when changes occur in the base model, + * projection model or by calls to {@link #addProjection} and {@link #removeProjection}. + *

+ *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextModel} + * {@link orion.textview.TextView#setModel} + *

+ * @borrows orion.textview.EventTarget#addEventListener as #addEventListener + * @borrows orion.textview.EventTarget#removeEventListener as #removeEventListener + * @borrows orion.textview.EventTarget#dispatchEvent as #dispatchEvent + */ + function ProjectionTextModel(baseModel) { + this._model = baseModel; /* Base Model */ + this._projections = []; + } + + ProjectionTextModel.prototype = /** @lends orion.textview.ProjectionTextModel.prototype */ { + /** + * Adds a projection range to the model. + *

+ * The model must notify the listeners before and after the the text is + * changed by calling {@link #onChanging} and {@link #onChanged} respectively. + *

+ * @param {orion.textview.Projection} projection The projection range to be added. + * + * @see #removeProjection + */ + addProjection: function(projection) { + if (!projection) {return;} + //start and end can't overlap any exist projection + var model = this._model, projections = this._projections; + projection._lineIndex = model.getLineAtOffset(projection.start); + projection._lineCount = model.getLineAtOffset(projection.end) - projection._lineIndex; + var text = projection.text; + if (!text) { text = ""; } + if (typeof text === "string") { + projection._model = new mTextModel.TextModel(text, model.getLineDelimiter()); + } else { + projection._model = text; + } + var eventStart = this.mapOffset(projection.start, true); + var removedCharCount = projection.end - projection.start; + var removedLineCount = projection._lineCount; + var addedCharCount = projection._model.getCharCount(); + var addedLineCount = projection._model.getLineCount() - 1; + var modelChangingEvent = { + type: "Changing", + text: projection._model.getText(), + start: eventStart, + removedCharCount: removedCharCount, + addedCharCount: addedCharCount, + removedLineCount: removedLineCount, + addedLineCount: addedLineCount + }; + this.onChanging(modelChangingEvent); + var index = this._binarySearch(projections, projection.start); + projections.splice(index, 0, projection); + var modelChangedEvent = { + type: "Changed", + start: eventStart, + removedCharCount: removedCharCount, + addedCharCount: addedCharCount, + removedLineCount: removedLineCount, + addedLineCount: addedLineCount + }; + this.onChanged(modelChangedEvent); + }, + /** + * Returns all projection ranges of this model. + * + * @return {orion.textview.Projection[]} The projection ranges. + * + * @see #addProjection + */ + getProjections: function() { + return this._projections.slice(0); + }, + /** + * Gets the base text model. + * + * @return {orion.textview.TextModel} The base text model. + */ + getBaseModel: function() { + return this._model; + }, + /** + * Maps offsets between the projection model and its base model. + * + * @param {Number} offset The offset to be mapped. + * @param {Boolean} [baseOffset=false] true if offset is in base model and + * should be mapped to the projection model. + * @return {Number} The mapped offset + */ + mapOffset: function(offset, baseOffset) { + var projections = this._projections, delta = 0, i, projection; + if (baseOffset) { + for (i = 0; i < projections.length; i++) { + projection = projections[i]; + if (projection.start > offset) { break; } + if (projection.end > offset) { return -1; } + delta += projection._model.getCharCount() - (projection.end - projection.start); + } + return offset + delta; + } + for (i = 0; i < projections.length; i++) { + projection = projections[i]; + if (projection.start > offset - delta) { break; } + var charCount = projection._model.getCharCount(); + if (projection.start + charCount > offset - delta) { + return -1; + } + delta += charCount - (projection.end - projection.start); + } + return offset - delta; + }, + /** + * Removes a projection range from the model. + *

+ * The model must notify the listeners before and after the the text is + * changed by calling {@link #onChanging} and {@link #onChanged} respectively. + *

+ * + * @param {orion.textview.Projection} projection The projection range to be removed. + * + * @see #addProjection + */ + removeProjection: function(projection) { + //TODO remove listeners from model + var i, delta = 0; + for (i = 0; i < this._projections.length; i++) { + var p = this._projections[i]; + if (p === projection) { + projection = p; + break; + } + delta += p._model.getCharCount() - (p.end - p.start); + } + if (i < this._projections.length) { + var model = this._model; + var eventStart = projection.start + delta; + var addedCharCount = projection.end - projection.start; + var addedLineCount = projection._lineCount; + var removedCharCount = projection._model.getCharCount(); + var removedLineCount = projection._model.getLineCount() - 1; + var modelChangingEvent = { + type: "Changing", + text: model.getText(projection.start, projection.end), + start: eventStart, + removedCharCount: removedCharCount, + addedCharCount: addedCharCount, + removedLineCount: removedLineCount, + addedLineCount: addedLineCount + }; + this.onChanging(modelChangingEvent); + this._projections.splice(i, 1); + var modelChangedEvent = { + type: "Changed", + start: eventStart, + removedCharCount: removedCharCount, + addedCharCount: addedCharCount, + removedLineCount: removedLineCount, + addedLineCount: addedLineCount + }; + this.onChanged(modelChangedEvent); + } + }, + /** @ignore */ + _binarySearch: function (array, offset) { + var high = array.length, low = -1, index; + while (high - low > 1) { + index = Math.floor((high + low) / 2); + if (offset <= array[index].start) { + high = index; + } else { + low = index; + } + } + return high; + }, + /** + * @see orion.textview.TextModel#getCharCount + */ + getCharCount: function() { + var count = this._model.getCharCount(), projections = this._projections; + for (var i = 0; i < projections.length; i++) { + var projection = projections[i]; + count += projection._model.getCharCount() - (projection.end - projection.start); + } + return count; + }, + /** + * @see orion.textview.TextModel#getLine + */ + getLine: function(lineIndex, includeDelimiter) { + if (lineIndex < 0) { return null; } + var model = this._model, projections = this._projections; + var delta = 0, result = [], offset = 0, i, lineCount, projection; + for (i = 0; i < projections.length; i++) { + projection = projections[i]; + if (projection._lineIndex >= lineIndex - delta) { break; } + lineCount = projection._model.getLineCount() - 1; + if (projection._lineIndex + lineCount >= lineIndex - delta) { + var projectionLineIndex = lineIndex - (projection._lineIndex + delta); + if (projectionLineIndex < lineCount) { + return projection._model.getLine(projectionLineIndex, includeDelimiter); + } else { + result.push(projection._model.getLine(lineCount)); + } + } + offset = projection.end; + delta += lineCount - projection._lineCount; + } + offset = Math.max(offset, model.getLineStart(lineIndex - delta)); + for (; i < projections.length; i++) { + projection = projections[i]; + if (projection._lineIndex > lineIndex - delta) { break; } + result.push(model.getText(offset, projection.start)); + lineCount = projection._model.getLineCount() - 1; + if (projection._lineIndex + lineCount > lineIndex - delta) { + result.push(projection._model.getLine(0, includeDelimiter)); + return result.join(""); + } + result.push(projection._model.getText()); + offset = projection.end; + delta += lineCount - projection._lineCount; + } + var end = model.getLineEnd(lineIndex - delta, includeDelimiter); + if (offset < end) { + result.push(model.getText(offset, end)); + } + return result.join(""); + }, + /** + * @see orion.textview.TextModel#getLineAtOffset + */ + getLineAtOffset: function(offset) { + var model = this._model, projections = this._projections; + var delta = 0, lineDelta = 0; + for (var i = 0; i < projections.length; i++) { + var projection = projections[i]; + if (projection.start > offset - delta) { break; } + var charCount = projection._model.getCharCount(); + if (projection.start + charCount > offset - delta) { + var projectionOffset = offset - (projection.start + delta); + lineDelta += projection._model.getLineAtOffset(projectionOffset); + delta += projectionOffset; + break; + } + lineDelta += projection._model.getLineCount() - 1 - projection._lineCount; + delta += charCount - (projection.end - projection.start); + } + return model.getLineAtOffset(offset - delta) + lineDelta; + }, + /** + * @see orion.textview.TextModel#getLineCount + */ + getLineCount: function() { + var model = this._model, projections = this._projections; + var count = model.getLineCount(); + for (var i = 0; i < projections.length; i++) { + var projection = projections[i]; + count += projection._model.getLineCount() - 1 - projection._lineCount; + } + return count; + }, + /** + * @see orion.textview.TextModel#getLineDelimiter + */ + getLineDelimiter: function() { + return this._model.getLineDelimiter(); + }, + /** + * @see orion.textview.TextModel#getLineEnd + */ + getLineEnd: function(lineIndex, includeDelimiter) { + if (lineIndex < 0) { return -1; } + var model = this._model, projections = this._projections; + var delta = 0, offsetDelta = 0; + for (var i = 0; i < projections.length; i++) { + var projection = projections[i]; + if (projection._lineIndex > lineIndex - delta) { break; } + var lineCount = projection._model.getLineCount() - 1; + if (projection._lineIndex + lineCount > lineIndex - delta) { + var projectionLineIndex = lineIndex - (projection._lineIndex + delta); + return projection._model.getLineEnd (projectionLineIndex, includeDelimiter) + projection.start + offsetDelta; + } + offsetDelta += projection._model.getCharCount() - (projection.end - projection.start); + delta += lineCount - projection._lineCount; + } + return model.getLineEnd(lineIndex - delta, includeDelimiter) + offsetDelta; + }, + /** + * @see orion.textview.TextModel#getLineStart + */ + getLineStart: function(lineIndex) { + if (lineIndex < 0) { return -1; } + var model = this._model, projections = this._projections; + var delta = 0, offsetDelta = 0; + for (var i = 0; i < projections.length; i++) { + var projection = projections[i]; + if (projection._lineIndex >= lineIndex - delta) { break; } + var lineCount = projection._model.getLineCount() - 1; + if (projection._lineIndex + lineCount >= lineIndex - delta) { + var projectionLineIndex = lineIndex - (projection._lineIndex + delta); + return projection._model.getLineStart (projectionLineIndex) + projection.start + offsetDelta; + } + offsetDelta += projection._model.getCharCount() - (projection.end - projection.start); + delta += lineCount - projection._lineCount; + } + return model.getLineStart(lineIndex - delta) + offsetDelta; + }, + /** + * @see orion.textview.TextModel#getText + */ + getText: function(start, end) { + if (start === undefined) { start = 0; } + var model = this._model, projections = this._projections; + var delta = 0, result = [], i, projection, charCount; + for (i = 0; i < projections.length; i++) { + projection = projections[i]; + if (projection.start > start - delta) { break; } + charCount = projection._model.getCharCount(); + if (projection.start + charCount > start - delta) { + if (end !== undefined && projection.start + charCount > end - delta) { + return projection._model.getText(start - (projection.start + delta), end - (projection.start + delta)); + } else { + result.push(projection._model.getText(start - (projection.start + delta))); + start = projection.end + delta + charCount - (projection.end - projection.start); + } + } + delta += charCount - (projection.end - projection.start); + } + var offset = start - delta; + if (end !== undefined) { + for (; i < projections.length; i++) { + projection = projections[i]; + if (projection.start > end - delta) { break; } + result.push(model.getText(offset, projection.start)); + charCount = projection._model.getCharCount(); + if (projection.start + charCount > end - delta) { + result.push(projection._model.getText(0, end - (projection.start + delta))); + return result.join(""); + } + result.push(projection._model.getText()); + offset = projection.end; + delta += charCount - (projection.end - projection.start); + } + result.push(model.getText(offset, end - delta)); + } else { + for (; i < projections.length; i++) { + projection = projections[i]; + result.push(model.getText(offset, projection.start)); + result.push(projection._model.getText()); + offset = projection.end; + } + result.push(model.getText(offset)); + } + return result.join(""); + }, + /** @ignore */ + _onChanging: function(text, start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) { + var model = this._model, projections = this._projections, i, projection, delta = 0, lineDelta; + var end = start + removedCharCount; + for (; i < projections.length; i++) { + projection = projections[i]; + if (projection.start > start) { break; } + delta += projection._model.getCharCount() - (projection.end - projection.start); + } + /*TODO add stuff saved by setText*/ + var mapStart = start + delta, rangeStart = i; + for (; i < projections.length; i++) { + projection = projections[i]; + if (projection.start > end) { break; } + delta += projection._model.getCharCount() - (projection.end - projection.start); + lineDelta += projection._model.getLineCount() - 1 - projection._lineCount; + } + /*TODO add stuff saved by setText*/ + var mapEnd = end + delta, rangeEnd = i; + this.onChanging(mapStart, mapEnd - mapStart, addedCharCount/*TODO add stuff saved by setText*/, removedLineCount + lineDelta/*TODO add stuff saved by setText*/, addedLineCount/*TODO add stuff saved by setText*/); + projections.splice(projections, rangeEnd - rangeStart); + var count = text.length - (mapEnd - mapStart); + for (; i < projections.length; i++) { + projection = projections[i]; + projection.start += count; + projection.end += count; + projection._lineIndex = model.getLineAtOffset(projection.start); + } + }, + /** + * @see orion.textview.TextModel#onChanging + */ + onChanging: function(modelChangingEvent) { + return this.dispatchEvent(modelChangingEvent); + }, + /** + * @see orion.textview.TextModel#onChanged + */ + onChanged: function(modelChangedEvent) { + return this.dispatchEvent(modelChangedEvent); + }, + /** + * @see orion.textview.TextModel#setLineDelimiter + */ + setLineDelimiter: function(lineDelimiter) { + this._model.setLineDelimiter(lineDelimiter); + }, + /** + * @see orion.textview.TextModel#setText + */ + setText: function(text, start, end) { + if (text === undefined) { text = ""; } + if (start === undefined) { start = 0; } + var eventStart = start, eventEnd = end; + var model = this._model, projections = this._projections; + var delta = 0, lineDelta = 0, i, projection, charCount, startProjection, endProjection, startLineDelta = 0; + for (i = 0; i < projections.length; i++) { + projection = projections[i]; + if (projection.start > start - delta) { break; } + charCount = projection._model.getCharCount(); + if (projection.start + charCount > start - delta) { + if (end !== undefined && projection.start + charCount > end - delta) { + projection._model.setText(text, start - (projection.start + delta), end - (projection.start + delta)); + //TODO events - special case + return; + } else { + startLineDelta = projection._model.getLineCount() - 1 - projection._model.getLineAtOffset(start - (projection.start + delta)); + startProjection = { + projection: projection, + start: start - (projection.start + delta) + }; + start = projection.end + delta + charCount - (projection.end - projection.start); + } + } + lineDelta += projection._model.getLineCount() - 1 - projection._lineCount; + delta += charCount - (projection.end - projection.start); + } + var mapStart = start - delta, rangeStart = i, startLine = model.getLineAtOffset(mapStart) + lineDelta - startLineDelta; + if (end !== undefined) { + for (; i < projections.length; i++) { + projection = projections[i]; + if (projection.start > end - delta) { break; } + charCount = projection._model.getCharCount(); + if (projection.start + charCount > end - delta) { + lineDelta += projection._model.getLineAtOffset(end - (projection.start + delta)); + charCount = end - (projection.start + delta); + end = projection.end + delta; + endProjection = { + projection: projection, + end: charCount + }; + break; + } + lineDelta += projection._model.getLineCount() - 1 - projection._lineCount; + delta += charCount - (projection.end - projection.start); + } + } else { + for (; i < projections.length; i++) { + projection = projections[i]; + lineDelta += projection._model.getLineCount() - 1 - projection._lineCount; + delta += projection._model.getCharCount() - (projection.end - projection.start); + } + end = eventEnd = model.getCharCount() + delta; + } + var mapEnd = end - delta, rangeEnd = i, endLine = model.getLineAtOffset(mapEnd) + lineDelta; + + //events + var removedCharCount = eventEnd - eventStart; + var removedLineCount = endLine - startLine; + var addedCharCount = text.length; + var addedLineCount = 0; + var cr = 0, lf = 0, index = 0; + while (true) { + if (cr !== -1 && cr <= index) { cr = text.indexOf("\r", index); } + if (lf !== -1 && lf <= index) { lf = text.indexOf("\n", index); } + if (lf === -1 && cr === -1) { break; } + if (cr !== -1 && lf !== -1) { + if (cr + 1 === lf) { + index = lf + 1; + } else { + index = (cr < lf ? cr : lf) + 1; + } + } else if (cr !== -1) { + index = cr + 1; + } else { + index = lf + 1; + } + addedLineCount++; + } + + var modelChangingEvent = { + type: "Changing", + text: text, + start: eventStart, + removedCharCount: removedCharCount, + addedCharCount: addedCharCount, + removedLineCount: removedLineCount, + addedLineCount: addedLineCount + }; + this.onChanging(modelChangingEvent); + +// var changeLineCount = model.getLineAtOffset(mapEnd) - model.getLineAtOffset(mapStart) + addedLineCount; + model.setText(text, mapStart, mapEnd); + if (startProjection) { + projection = startProjection.projection; + projection._model.setText("", startProjection.start); + } + if (endProjection) { + projection = endProjection.projection; + projection._model.setText("", 0, endProjection.end); + projection.start = projection.end; + projection._lineCount = 0; + } + projections.splice(rangeStart, rangeEnd - rangeStart); + var changeCount = text.length - (mapEnd - mapStart); + for (i = rangeEnd; i < projections.length; i++) { + projection = projections[i]; + projection.start += changeCount; + projection.end += changeCount; +// if (projection._lineIndex + changeLineCount !== model.getLineAtOffset(projection.start)) { +// log("here"); +// } + projection._lineIndex = model.getLineAtOffset(projection.start); +// projection._lineIndex += changeLineCount; + } + + var modelChangedEvent = { + type: "Changed", + start: eventStart, + removedCharCount: removedCharCount, + addedCharCount: addedCharCount, + removedLineCount: removedLineCount, + addedLineCount: addedLineCount + }; + this.onChanged(modelChangedEvent); + } + }; + mEventTarget.EventTarget.addMixin(ProjectionTextModel.prototype); + + return {ProjectionTextModel: ProjectionTextModel}; +}); +/******************************************************************************* + * @license + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: IBM Corporation - initial API and implementation + ******************************************************************************/ + +/*global define setTimeout clearTimeout setInterval clearInterval Node */ + +define("orion/textview/tooltip", ['orion/textview/textView', 'orion/textview/textModel', 'orion/textview/projectionTextModel'], function(mTextView, mTextModel, mProjectionTextModel) { + + /** @private */ + function Tooltip (view) { + this._view = view; + //TODO add API to get the parent of the view + this._create(view._parent.ownerDocument); + view.addEventListener("Destroy", this, this.destroy); + } + Tooltip.getTooltip = function(view) { + if (!view._tooltip) { + view._tooltip = new Tooltip(view); + } + return view._tooltip; + }; + Tooltip.prototype = /** @lends orion.textview.Tooltip.prototype */ { + _create: function(document) { + if (this._domNode) { return; } + this._document = document; + var domNode = this._domNode = document.createElement("DIV"); + domNode.className = "viewTooltip"; + var viewParent = this._viewParent = document.createElement("DIV"); + domNode.appendChild(viewParent); + var htmlParent = this._htmlParent = document.createElement("DIV"); + domNode.appendChild(htmlParent); + document.body.appendChild(domNode); + this.hide(); + }, + destroy: function() { + if (!this._domNode) { return; } + if (this._contentsView) { + this._contentsView.destroy(); + this._contentsView = null; + this._emptyModel = null; + } + var parent = this._domNode.parentNode; + if (parent) { parent.removeChild(this._domNode); } + this._domNode = null; + }, + hide: function() { + if (this._contentsView) { + this._contentsView.setModel(this._emptyModel); + } + if (this._viewParent) { + this._viewParent.style.left = "-10000px"; + this._viewParent.style.position = "fixed"; + this._viewParent.style.visibility = "hidden"; + } + if (this._htmlParent) { + this._htmlParent.style.left = "-10000px"; + this._htmlParent.style.position = "fixed"; + this._htmlParent.style.visibility = "hidden"; + this._htmlParent.innerHTML = ""; + } + if (this._domNode) { + this._domNode.style.visibility = "hidden"; + } + if (this._showTimeout) { + clearTimeout(this._showTimeout); + this._showTimeout = null; + } + if (this._hideTimeout) { + clearTimeout(this._hideTimeout); + this._hideTimeout = null; + } + if (this._fadeTimeout) { + clearInterval(this._fadeTimeout); + this._fadeTimeout = null; + } + }, + isVisible: function() { + return this._domNode && this._domNode.style.visibility === "visible"; + }, + setTarget: function(target) { + if (this.target === target) { return; } + this._target = target; + this.hide(); + if (target) { + var self = this; + self._showTimeout = setTimeout(function() { + self.show(true); + }, 1000); + } + }, + show: function(autoHide) { + if (!this._target) { return; } + var info = this._target.getTooltipInfo(); + if (!info) { return; } + var domNode = this._domNode; + domNode.style.left = domNode.style.right = domNode.style.width = domNode.style.height = "auto"; + var contents = info.contents, contentsDiv; + if (contents instanceof Array) { + contents = this._getAnnotationContents(contents); + } + if (typeof contents === "string") { + (contentsDiv = this._htmlParent).innerHTML = contents; + } else if (contents instanceof Node) { + (contentsDiv = this._htmlParent).appendChild(contents); + } else if (contents instanceof mProjectionTextModel.ProjectionTextModel) { + if (!this._contentsView) { + this._emptyModel = new mTextModel.TextModel(""); + //TODO need hook into setup.js (or editor.js) to create a text view (and styler) + var newView = this._contentsView = new mTextView.TextView({ + model: this._emptyModel, + parent: this._viewParent, + tabSize: 4, + sync: true, + stylesheet: ["/orion/textview/tooltip.css", "/orion/textview/rulers.css", + "/examples/textview/textstyler.css", "/css/default-theme.css"] + }); + //TODO this is need to avoid IE from getting focus + newView._clientDiv.contentEditable = false; + //TODO need to find a better way of sharing the styler for multiple views + var view = this._view; + var listener = { + onLineStyle: function(e) { + view.onLineStyle(e); + } + }; + newView.addEventListener("LineStyle", listener.onLineStyle); + } + var contentsView = this._contentsView; + contentsView.setModel(contents); + var size = contentsView.computeSize(); + contentsDiv = this._viewParent; + //TODO always make the width larger than the size of the scrollbar to avoid bug in updatePage + contentsDiv.style.width = (size.width + 20) + "px"; + contentsDiv.style.height = size.height + "px"; + } else { + return; + } + contentsDiv.style.left = "auto"; + contentsDiv.style.position = "static"; + contentsDiv.style.visibility = "visible"; + var left = parseInt(this._getNodeStyle(domNode, "padding-left", "0"), 10); + left += parseInt(this._getNodeStyle(domNode, "border-left-width", "0"), 10); + if (info.anchor === "right") { + var right = parseInt(this._getNodeStyle(domNode, "padding-right", "0"), 10); + right += parseInt(this._getNodeStyle(domNode, "border-right-width", "0"), 10); + domNode.style.right = (domNode.ownerDocument.body.getBoundingClientRect().right - info.x + left + right) + "px"; + } else { + domNode.style.left = (info.x - left) + "px"; + } + var top = parseInt(this._getNodeStyle(domNode, "padding-top", "0"), 10); + top += parseInt(this._getNodeStyle(domNode, "border-top-width", "0"), 10); + domNode.style.top = (info.y - top) + "px"; + domNode.style.maxWidth = info.maxWidth + "px"; + domNode.style.maxHeight = info.maxHeight + "px"; + domNode.style.opacity = "1"; + domNode.style.visibility = "visible"; + if (autoHide) { + var self = this; + self._hideTimeout = setTimeout(function() { + var opacity = parseFloat(self._getNodeStyle(domNode, "opacity", "1")); + self._fadeTimeout = setInterval(function() { + if (domNode.style.visibility === "visible" && opacity > 0) { + opacity -= 0.1; + domNode.style.opacity = opacity; + return; + } + self.hide(); + }, 50); + }, 5000); + } + }, + _getAnnotationContents: function(annotations) { + if (annotations.length === 0) { + return null; + } + var model = this._view.getModel(), annotation; + var baseModel = model.getBaseModel ? model.getBaseModel() : model; + function getText(start, end) { + var textStart = baseModel.getLineStart(baseModel.getLineAtOffset(start)); + var textEnd = baseModel.getLineEnd(baseModel.getLineAtOffset(end), true); + return baseModel.getText(textStart, textEnd); + } + var title; + if (annotations.length === 1) { + annotation = annotations[0]; + if (annotation.title) { + title = annotation.title.replace(//g, ">"); + return "
" + annotation.html + " " + title + "
"; + } else { + var newModel = new mProjectionTextModel.ProjectionTextModel(baseModel); + var lineStart = baseModel.getLineStart(baseModel.getLineAtOffset(annotation.start)); + newModel.addProjection({start: annotation.end, end: newModel.getCharCount()}); + newModel.addProjection({start: 0, end: lineStart}); + return newModel; + } + } else { + var tooltipHTML = "
Multiple annotations:
"; + for (var i = 0; i < annotations.length; i++) { + annotation = annotations[i]; + title = annotation.title; + if (!title) { + title = getText(annotation.start, annotation.end); + } + title = title.replace(//g, ">"); + tooltipHTML += "
" + annotation.html + " " + title + "
"; + } + return tooltipHTML; + } + }, + _getNodeStyle: function(node, prop, defaultValue) { + var value; + if (node) { + value = node.style[prop]; + if (!value) { + if (node.currentStyle) { + var index = 0, p = prop; + while ((index = p.indexOf("-", index)) !== -1) { + p = p.substring(0, index) + p.substring(index + 1, index + 2).toUpperCase() + p.substring(index + 2); + } + value = node.currentStyle[p]; + } else { + var css = node.ownerDocument.defaultView.getComputedStyle(node, null); + value = css ? css.getPropertyValue(prop) : null; + } + } + } + return value || defaultValue; + } + }; + return {Tooltip: Tooltip}; +}); +/******************************************************************************* + * @license + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: + * Felipe Heidrich (IBM Corporation) - initial API and implementation + * Silenio Quarti (IBM Corporation) - initial API and implementation + * Mihai Sucan (Mozilla Foundation) - fix for Bug#334583 Bug#348471 Bug#349485 Bug#350595 Bug#360726 Bug#361180 Bug#362835 Bug#362428 Bug#362286 Bug#354270 Bug#361474 Bug#363945 Bug#366312 Bug#370584 + ******************************************************************************/ + +/*global window document navigator setTimeout clearTimeout XMLHttpRequest define DOMException */ + +define("orion/textview/textView", ['orion/textview/textModel', 'orion/textview/keyBinding', 'orion/textview/eventTarget'], function(mTextModel, mKeyBinding, mEventTarget) { + + /** @private */ + function addHandler(node, type, handler, capture) { + if (typeof node.addEventListener === "function") { + node.addEventListener(type, handler, capture === true); + } else { + node.attachEvent("on" + type, handler); + } + } + /** @private */ + function removeHandler(node, type, handler, capture) { + if (typeof node.removeEventListener === "function") { + node.removeEventListener(type, handler, capture === true); + } else { + node.detachEvent("on" + type, handler); + } + } + var userAgent = navigator.userAgent; + var isIE; + if (document.selection && window.ActiveXObject && /MSIE/.test(userAgent)) { + isIE = document.documentMode ? document.documentMode : 7; + } + var isFirefox = parseFloat(userAgent.split("Firefox/")[1] || userAgent.split("Minefield/")[1]) || undefined; + var isOpera = userAgent.indexOf("Opera") !== -1; + var isChrome = userAgent.indexOf("Chrome") !== -1; + var isSafari = userAgent.indexOf("Safari") !== -1 && !isChrome; + var isWebkit = userAgent.indexOf("WebKit") !== -1; + var isPad = userAgent.indexOf("iPad") !== -1; + var isMac = navigator.platform.indexOf("Mac") !== -1; + var isWindows = navigator.platform.indexOf("Win") !== -1; + var isLinux = navigator.platform.indexOf("Linux") !== -1; + var isW3CEvents = typeof window.document.documentElement.addEventListener === "function"; + var isRangeRects = (!isIE || isIE >= 9) && typeof window.document.createRange().getBoundingClientRect === "function"; + var platformDelimiter = isWindows ? "\r\n" : "\n"; + + /** + * Constructs a new Selection object. + * + * @class A Selection represents a range of selected text in the view. + * @name orion.textview.Selection + */ + function Selection (start, end, caret) { + /** + * The selection start offset. + * + * @name orion.textview.Selection#start + */ + this.start = start; + /** + * The selection end offset. + * + * @name orion.textview.Selection#end + */ + this.end = end; + /** @private */ + this.caret = caret; //true if the start, false if the caret is at end + } + Selection.prototype = /** @lends orion.textview.Selection.prototype */ { + /** @private */ + clone: function() { + return new Selection(this.start, this.end, this.caret); + }, + /** @private */ + collapse: function() { + if (this.caret) { + this.end = this.start; + } else { + this.start = this.end; + } + }, + /** @private */ + extend: function (offset) { + if (this.caret) { + this.start = offset; + } else { + this.end = offset; + } + if (this.start > this.end) { + var tmp = this.start; + this.start = this.end; + this.end = tmp; + this.caret = !this.caret; + } + }, + /** @private */ + setCaret: function(offset) { + this.start = offset; + this.end = offset; + this.caret = false; + }, + /** @private */ + getCaret: function() { + return this.caret ? this.start : this.end; + }, + /** @private */ + toString: function() { + return "start=" + this.start + " end=" + this.end + (this.caret ? " caret is at start" : " caret is at end"); + }, + /** @private */ + isEmpty: function() { + return this.start === this.end; + }, + /** @private */ + equals: function(object) { + return this.caret === object.caret && this.start === object.start && this.end === object.end; + } + }; + /** + * @class This object describes the options for the text view. + *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#setOptions} + * {@link orion.textview.TextView#getOptions} + *

+ * @name orion.textview.TextViewOptions + * + * @property {String|DOMElement} parent the parent element for the view, it can be either a DOM element or an ID for a DOM element. + * @property {orion.textview.TextModel} [model] the text model for the view. If it is not set the view creates an empty {@link orion.textview.TextModel}. + * @property {Boolean} [readonly=false] whether or not the view is read-only. + * @property {Boolean} [fullSelection=true] whether or not the view is in full selection mode. + * @property {Boolean} [sync=false] whether or not the view creation should be synchronous (if possible). + * @property {Boolean} [expandTab=false] whether or not the tab key inserts white spaces. + * @property {String|String[]} [stylesheet] one or more stylesheet for the view. Each stylesheet can be either a URI or a string containing the CSS rules. + * @property {String} [themeClass] the CSS class for the view theming. + * @property {Number} [tabSize] The number of spaces in a tab. + */ + /** + * Constructs a new text view. + * + * @param {orion.textview.TextViewOptions} options the view options. + * + * @class A TextView is a user interface for editing text. + * @name orion.textview.TextView + * @borrows orion.textview.EventTarget#addEventListener as #addEventListener + * @borrows orion.textview.EventTarget#removeEventListener as #removeEventListener + * @borrows orion.textview.EventTarget#dispatchEvent as #dispatchEvent + */ + function TextView (options) { + this._init(options); + } + + TextView.prototype = /** @lends orion.textview.TextView.prototype */ { + /** + * Adds a ruler to the text view. + * + * @param {orion.textview.Ruler} ruler the ruler. + */ + addRuler: function (ruler) { + this._rulers.push(ruler); + ruler.setView(this); + this._createRuler(ruler); + this._updatePage(); + }, + computeSize: function() { + var w = 0, h = 0; + var model = this._model, clientDiv = this._clientDiv; + if (!clientDiv) { return {width: w, height: h}; } + var clientWidth = clientDiv.style.width; + /* + * Feature in WekKit. Webkit limits the width of the lines + * computed below to the width of the client div. This causes + * the lines to be wrapped even though "pre" is set. The fix + * is to set the width of the client div to a larger number + * before computing the lines width. Note that this value is + * reset to the appropriate value further down. + */ + if (isWebkit) { + clientDiv.style.width = (0x7FFFF).toString() + "px"; + } + var lineCount = model.getLineCount(); + var document = this._frameDocument; + for (var lineIndex=0; lineIndexThe supported coordinate spaces are: + *
    + *
  • "document" - relative to document, the origin is the top-left corner of first line
  • + *
  • "page" - relative to html page that contains the text view
  • + *
  • "view" - relative to text view, the origin is the top-left corner of the view container
  • + *
+ *

+ *

All methods in the view that take or return a position are in the document coordinate space.

+ * + * @param rect the rectangle to convert. + * @param rect.x the x of the rectangle. + * @param rect.y the y of the rectangle. + * @param rect.width the width of the rectangle. + * @param rect.height the height of the rectangle. + * @param {String} from the source coordinate space. + * @param {String} to the destination coordinate space. + * + * @see #getLocationAtOffset + * @see #getOffsetAtLocation + * @see #getTopPixel + * @see #setTopPixel + */ + convert: function(rect, from, to) { + if (!this._clientDiv) { return; } + var scroll = this._getScroll(); + var viewPad = this._getViewPadding(); + var frame = this._frame.getBoundingClientRect(); + var viewRect = this._viewDiv.getBoundingClientRect(); + switch(from) { + case "document": + if (rect.x !== undefined) { + rect.x += - scroll.x + viewRect.left + viewPad.left; + } + if (rect.y !== undefined) { + rect.y += - scroll.y + viewRect.top + viewPad.top; + } + break; + case "page": + if (rect.x !== undefined) { + rect.x += - frame.left; + } + if (rect.y !== undefined) { + rect.y += - frame.top; + } + break; + } + //At this point rect is in the widget coordinate space + switch (to) { + case "document": + if (rect.x !== undefined) { + rect.x += scroll.x - viewRect.left - viewPad.left; + } + if (rect.y !== undefined) { + rect.y += scroll.y - viewRect.top - viewPad.top; + } + break; + case "page": + if (rect.x !== undefined) { + rect.x += frame.left; + } + if (rect.y !== undefined) { + rect.y += frame.top; + } + break; + } + return rect; + }, + /** + * Destroys the text view. + *

+ * Removes the view from the page and frees all resources created by the view. + * Calling this function causes the "Destroy" event to be fire so that all components + * attached to view can release their references. + *

+ * + * @see #onDestroy + */ + destroy: function() { + /* Destroy rulers*/ + for (var i=0; i< this._rulers.length; i++) { + this._rulers[i].setView(null); + } + this.rulers = null; + + /* + * Note that when the frame is removed, the unload event is trigged + * and the view contents and handlers is released properly by + * destroyView(). + */ + this._destroyFrame(); + + var e = {type: "Destroy"}; + this.onDestroy(e); + + this._parent = null; + this._parentDocument = null; + this._model = null; + this._selection = null; + this._doubleClickSelection = null; + this._keyBindings = null; + this._actions = null; + }, + /** + * Gives focus to the text view. + */ + focus: function() { + if (!this._clientDiv) { return; } + /* + * Feature in Chrome. When focus is called in the clientDiv without + * setting selection the browser will set the selection to the first dom + * element, which can be above the client area. When this happen the + * browser also scrolls the window to show that element. + * The fix is to call _updateDOMSelection() before calling focus(). + */ + this._updateDOMSelection(); + if (isPad) { + this._textArea.focus(); + } else { + if (isOpera) { this._clientDiv.blur(); } + this._clientDiv.focus(); + } + /* + * Feature in Safari. When focus is called the browser selects the clientDiv + * itself. The fix is to call _updateDOMSelection() after calling focus(). + */ + this._updateDOMSelection(); + }, + /** + * Check if the text view has focus. + * + * @returns {Boolean} true if the text view has focus, otherwise false. + */ + hasFocus: function() { + return this._hasFocus; + }, + /** + * Returns all action names defined in the text view. + *

+ * There are two types of actions, the predefined actions of the view + * and the actions added by application code. + *

+ *

+ * The predefined actions are: + *

    + *
  • Navigation actions. These actions move the caret collapsing the selection.
  • + *
      + *
    • "lineUp" - moves the caret up by one line
    • + *
    • "lineDown" - moves the caret down by one line
    • + *
    • "lineStart" - moves the caret to beginning of the current line
    • + *
    • "lineEnd" - moves the caret to end of the current line
    • + *
    • "charPrevious" - moves the caret to the previous character
    • + *
    • "charNext" - moves the caret to the next character
    • + *
    • "pageUp" - moves the caret up by one page
    • + *
    • "pageDown" - moves the caret down by one page
    • + *
    • "wordPrevious" - moves the caret to the previous word
    • + *
    • "wordNext" - moves the caret to the next word
    • + *
    • "textStart" - moves the caret to the beginning of the document
    • + *
    • "textEnd" - moves the caret to the end of the document
    • + *
    + *
  • Selection actions. These actions move the caret extending the selection.
  • + *
      + *
    • "selectLineUp" - moves the caret up by one line
    • + *
    • "selectLineDown" - moves the caret down by one line
    • + *
    • "selectLineStart" - moves the caret to beginning of the current line
    • + *
    • "selectLineEnd" - moves the caret to end of the current line
    • + *
    • "selectCharPrevious" - moves the caret to the previous character
    • + *
    • "selectCharNext" - moves the caret to the next character
    • + *
    • "selectPageUp" - moves the caret up by one page
    • + *
    • "selectPageDown" - moves the caret down by one page
    • + *
    • "selectWordPrevious" - moves the caret to the previous word
    • + *
    • "selectWordNext" - moves the caret to the next word
    • + *
    • "selectTextStart" - moves the caret to the beginning of the document
    • + *
    • "selectTextEnd" - moves the caret to the end of the document
    • + *
    • "selectAll" - selects the entire document
    • + *
    + *
  • Edit actions. These actions modify the text view text
  • + *
      + *
    • "deletePrevious" - deletes the character preceding the caret
    • + *
    • "deleteNext" - deletes the charecter following the caret
    • + *
    • "deleteWordPrevious" - deletes the word preceding the caret
    • + *
    • "deleteWordNext" - deletes the word following the caret
    • + *
    • "tab" - inserts a tab character at the caret
    • + *
    • "enter" - inserts a line delimiter at the caret
    • + *
    + *
  • Clipboard actions.
  • + *
      + *
    • "copy" - copies the selected text to the clipboard
    • + *
    • "cut" - copies the selected text to the clipboard and deletes the selection
    • + *
    • "paste" - replaces the selected text with the clipboard contents
    • + *
    + *
+ *

+ * + * @param {Boolean} [defaultAction=false] whether or not the predefined actions are included. + * @returns {String[]} an array of action names defined in the text view. + * + * @see #invokeAction + * @see #setAction + * @see #setKeyBinding + * @see #getKeyBindings + */ + getActions: function (defaultAction) { + var result = []; + var actions = this._actions; + for (var i = 0; i < actions.length; i++) { + if (!defaultAction && actions[i].defaultHandler) { continue; } + result.push(actions[i].name); + } + return result; + }, + /** + * Returns the bottom index. + *

+ * The bottom index is the line that is currently at the bottom of the view. This + * line may be partially visible depending on the vertical scroll of the view. The parameter + * fullyVisible determines whether to return only fully visible lines. + *

+ * + * @param {Boolean} [fullyVisible=false] if true, returns the index of the last fully visible line. This + * parameter is ignored if the view is not big enough to show one line. + * @returns {Number} the index of the bottom line. + * + * @see #getTopIndex + * @see #setTopIndex + */ + getBottomIndex: function(fullyVisible) { + if (!this._clientDiv) { return 0; } + return this._getBottomIndex(fullyVisible); + }, + /** + * Returns the bottom pixel. + *

+ * The bottom pixel is the pixel position that is currently at + * the bottom edge of the view. This position is relative to the + * beginning of the document. + *

+ * + * @returns {Number} the bottom pixel. + * + * @see #getTopPixel + * @see #setTopPixel + * @see #convert + */ + getBottomPixel: function() { + if (!this._clientDiv) { return 0; } + return this._getScroll().y + this._getClientHeight(); + }, + /** + * Returns the caret offset relative to the start of the document. + * + * @returns the caret offset relative to the start of the document. + * + * @see #setCaretOffset + * @see #setSelection + * @see #getSelection + */ + getCaretOffset: function () { + var s = this._getSelection(); + return s.getCaret(); + }, + /** + * Returns the client area. + *

+ * The client area is the portion in pixels of the document that is visible. The + * client area position is relative to the beginning of the document. + *

+ * + * @returns the client area rectangle {x, y, width, height}. + * + * @see #getTopPixel + * @see #getBottomPixel + * @see #getHorizontalPixel + * @see #convert + */ + getClientArea: function() { + if (!this._clientDiv) { return {x: 0, y: 0, width: 0, height: 0}; } + var scroll = this._getScroll(); + return {x: scroll.x, y: scroll.y, width: this._getClientWidth(), height: this._getClientHeight()}; + }, + /** + * Returns the horizontal pixel. + *

+ * The horizontal pixel is the pixel position that is currently at + * the left edge of the view. This position is relative to the + * beginning of the document. + *

+ * + * @returns {Number} the horizontal pixel. + * + * @see #setHorizontalPixel + * @see #convert + */ + getHorizontalPixel: function() { + if (!this._clientDiv) { return 0; } + return this._getScroll().x; + }, + /** + * Returns all the key bindings associated to the given action name. + * + * @param {String} name the action name. + * @returns {orion.textview.KeyBinding[]} the array of key bindings associated to the given action name. + * + * @see #setKeyBinding + * @see #setAction + */ + getKeyBindings: function (name) { + var result = []; + var keyBindings = this._keyBindings; + for (var i = 0; i < keyBindings.length; i++) { + if (keyBindings[i].name === name) { + result.push(keyBindings[i].keyBinding); + } + } + return result; + }, + /** + * Returns the line height for a given line index. Returns the default line + * height if the line index is not specified. + * + * @param {Number} [lineIndex] the line index. + * @returns {Number} the height of the line in pixels. + * + * @see #getLinePixel + */ + getLineHeight: function(lineIndex) { + if (!this._clientDiv) { return 0; } + return this._getLineHeight(); + }, + /** + * Returns the top pixel position of a given line index relative to the beginning + * of the document. + *

+ * Clamps out of range indices. + *

+ * + * @param {Number} lineIndex the line index. + * @returns {Number} the pixel position of the line. + * + * @see #setTopPixel + * @see #convert + */ + getLinePixel: function(lineIndex) { + if (!this._clientDiv) { return 0; } + lineIndex = Math.min(Math.max(0, lineIndex), this._model.getLineCount()); + var lineHeight = this._getLineHeight(); + return lineHeight * lineIndex; + }, + /** + * Returns the {x, y} pixel location of the top-left corner of the character + * bounding box at the specified offset in the document. The pixel location + * is relative to the document. + *

+ * Clamps out of range offsets. + *

+ * + * @param {Number} offset the character offset + * @returns the {x, y} pixel location of the given offset. + * + * @see #getOffsetAtLocation + * @see #convert + */ + getLocationAtOffset: function(offset) { + if (!this._clientDiv) { return {x: 0, y: 0}; } + var model = this._model; + offset = Math.min(Math.max(0, offset), model.getCharCount()); + var lineIndex = model.getLineAtOffset(offset); + var scroll = this._getScroll(); + var viewRect = this._viewDiv.getBoundingClientRect(); + var viewPad = this._getViewPadding(); + var x = this._getOffsetToX(offset) + scroll.x - viewRect.left - viewPad.left; + var y = this.getLinePixel(lineIndex); + return {x: x, y: y}; + }, + /** + * Returns the specified view options. + *

+ * The returned value is either a orion.textview.TextViewOptions or an option value. An option value is returned when only one string paremeter + * is specified. A orion.textview.TextViewOptions is returned when there are no paremeters, or the parameters are a list of options names or a + * orion.textview.TextViewOptions. All view options are returned when there no paremeters. + *

+ * + * @param {String|orion.textview.TextViewOptions} [options] The options to return. + * @return {Object|orion.textview.TextViewOptions} The requested options or an option value. + * + * @see #setOptions + */ + getOptions: function() { + var options; + if (arguments.length === 0) { + options = this._defaultOptions(); + } else if (arguments.length === 1) { + var arg = arguments[0]; + if (typeof arg === "string") { + return this._clone(this["_" + arg]); + } + options = arg; + } else { + options = {}; + for (var index in arguments) { + if (arguments.hasOwnProperty(index)) { + options[arguments[index]] = undefined; + } + } + } + for (var option in options) { + if (options.hasOwnProperty(option)) { + options[option] = this._clone(this["_" + option]); + } + } + return options; + }, + /** + * Returns the text model of the text view. + * + * @returns {orion.textview.TextModel} the text model of the view. + */ + getModel: function() { + return this._model; + }, + /** + * Returns the character offset nearest to the given pixel location. The + * pixel location is relative to the document. + * + * @param x the x of the location + * @param y the y of the location + * @returns the character offset at the given location. + * + * @see #getLocationAtOffset + */ + getOffsetAtLocation: function(x, y) { + if (!this._clientDiv) { return 0; } + var scroll = this._getScroll(); + var viewRect = this._viewDiv.getBoundingClientRect(); + var viewPad = this._getViewPadding(); + var lineIndex = this._getYToLine(y - scroll.y); + x += -scroll.x + viewRect.left + viewPad.left; + var offset = this._getXToOffset(lineIndex, x); + return offset; + }, + /** + * Get the view rulers. + * + * @returns the view rulers + * + * @see #addRuler + */ + getRulers: function() { + return this._rulers.slice(0); + }, + /** + * Returns the text view selection. + *

+ * The selection is defined by a start and end character offset relative to the + * document. The character at end offset is not included in the selection. + *

+ * + * @returns {orion.textview.Selection} the view selection + * + * @see #setSelection + */ + getSelection: function () { + var s = this._getSelection(); + return {start: s.start, end: s.end}; + }, + /** + * Returns the text for the given range. + *

+ * The text does not include the character at the end offset. + *

+ * + * @param {Number} [start=0] the start offset of text range. + * @param {Number} [end=char count] the end offset of text range. + * + * @see #setText + */ + getText: function(start, end) { + var model = this._model; + return model.getText(start, end); + }, + /** + * Returns the top index. + *

+ * The top index is the line that is currently at the top of the view. This + * line may be partially visible depending on the vertical scroll of the view. The parameter + * fullyVisible determines whether to return only fully visible lines. + *

+ * + * @param {Boolean} [fullyVisible=false] if true, returns the index of the first fully visible line. This + * parameter is ignored if the view is not big enough to show one line. + * @returns {Number} the index of the top line. + * + * @see #getBottomIndex + * @see #setTopIndex + */ + getTopIndex: function(fullyVisible) { + if (!this._clientDiv) { return 0; } + return this._getTopIndex(fullyVisible); + }, + /** + * Returns the top pixel. + *

+ * The top pixel is the pixel position that is currently at + * the top edge of the view. This position is relative to the + * beginning of the document. + *

+ * + * @returns {Number} the top pixel. + * + * @see #getBottomPixel + * @see #setTopPixel + * @see #convert + */ + getTopPixel: function() { + if (!this._clientDiv) { return 0; } + return this._getScroll().y; + }, + /** + * Executes the action handler associated with the given name. + *

+ * The application defined action takes precedence over predefined actions unless + * the defaultAction paramater is true. + *

+ *

+ * If the application defined action returns false, the text view predefined + * action is executed if present. + *

+ * + * @param {String} name the action name. + * @param {Boolean} [defaultAction] whether to always execute the predefined action. + * @returns {Boolean} true if the action was executed. + * + * @see #setAction + * @see #getActions + */ + invokeAction: function (name, defaultAction) { + if (!this._clientDiv) { return; } + var actions = this._actions; + for (var i = 0; i < actions.length; i++) { + var a = actions[i]; + if (a.name && a.name === name) { + if (!defaultAction && a.userHandler) { + if (a.userHandler()) { return; } + } + if (a.defaultHandler) { return a.defaultHandler(); } + return false; + } + } + return false; + }, + /** + * Returns if the view is loaded. + *

+ * @returns {Boolean} true if the view is loaded. + * + * @see #onLoad + */ + isLoaded: function () { + return !!this._clientDiv; + }, + /** + * @class This is the event sent when the user right clicks or otherwise invokes the context menu of the view. + *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onContextMenu} + *

+ * + * @name orion.textview.ContextMenuEvent + * + * @property {Number} x The pointer location on the x axis, relative to the document the user is editing. + * @property {Number} y The pointer location on the y axis, relative to the document the user is editing. + * @property {Number} screenX The pointer location on the x axis, relative to the screen. This is copied from the DOM contextmenu event.screenX property. + * @property {Number} screenY The pointer location on the y axis, relative to the screen. This is copied from the DOM contextmenu event.screenY property. + */ + /** + * This event is sent when the user invokes the view context menu. + * + * @event + * @param {orion.textview.ContextMenuEvent} contextMenuEvent the event + */ + onContextMenu: function(contextMenuEvent) { + return this.dispatchEvent(contextMenuEvent); + }, + onDragStart: function(dragEvent) { + return this.dispatchEvent(dragEvent); + }, + onDrag: function(dragEvent) { + return this.dispatchEvent(dragEvent); + }, + onDragEnd: function(dragEvent) { + return this.dispatchEvent(dragEvent); + }, + onDragEnter: function(dragEvent) { + return this.dispatchEvent(dragEvent); + }, + onDragOver: function(dragEvent) { + return this.dispatchEvent(dragEvent); + }, + onDragLeave: function(dragEvent) { + return this.dispatchEvent(dragEvent); + }, + onDrop: function(dragEvent) { + return this.dispatchEvent(dragEvent); + }, + /** + * @class This is the event sent when the text view is destroyed. + *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onDestroy} + *

+ * @name orion.textview.DestroyEvent + */ + /** + * This event is sent when the text view has been destroyed. + * + * @event + * @param {orion.textview.DestroyEvent} destroyEvent the event + * + * @see #destroy + */ + onDestroy: function(destroyEvent) { + return this.dispatchEvent(destroyEvent); + }, + /** + * @class This object is used to define style information for the text view. + *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onLineStyle} + *

+ * @name orion.textview.Style + * + * @property {String} styleClass A CSS class name. + * @property {Object} style An object with CSS properties. + * @property {String} tagName A DOM tag name. + * @property {Object} attributes An object with DOM attributes. + */ + /** + * @class This object is used to style range. + *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onLineStyle} + *

+ * @name orion.textview.StyleRange + * + * @property {Number} start The start character offset, relative to the document, where the style should be applied. + * @property {Number} end The end character offset (exclusive), relative to the document, where the style should be applied. + * @property {orion.textview.Style} style The style for the range. + */ + /** + * @class This is the event sent when the text view needs the style information for a line. + *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onLineStyle} + *

+ * @name orion.textview.LineStyleEvent + * + * @property {orion.textview.TextView} textView The text view. + * @property {Number} lineIndex The line index. + * @property {String} lineText The line text. + * @property {Number} lineStart The character offset, relative to document, of the first character in the line. + * @property {orion.textview.Style} style The style for the entire line (output argument). + * @property {orion.textview.StyleRange[]} ranges An array of style ranges for the line (output argument). + */ + /** + * This event is sent when the text view needs the style information for a line. + * + * @event + * @param {orion.textview.LineStyleEvent} lineStyleEvent the event + */ + onLineStyle: function(lineStyleEvent) { + return this.dispatchEvent(lineStyleEvent); + }, + /** + * @class This is the event sent when the text view has loaded its contents. + *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onLoad} + *

+ * @name orion.textview.LoadEvent + */ + /** + * This event is sent when the text view has loaded its contents. + * + * @event + * @param {orion.textview.LoadEvent} loadEvent the event + */ + onLoad: function(loadEvent) { + return this.dispatchEvent(loadEvent); + }, + /** + * @class This is the event sent when the text in the model has changed. + *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onModelChanged}
+ * {@link orion.textview.TextModel#onChanged} + *

+ * @name orion.textview.ModelChangedEvent + * + * @property {Number} start The character offset in the model where the change has occurred. + * @property {Number} removedCharCount The number of characters removed from the model. + * @property {Number} addedCharCount The number of characters added to the model. + * @property {Number} removedLineCount The number of lines removed from the model. + * @property {Number} addedLineCount The number of lines added to the model. + */ + /** + * This event is sent when the text in the model has changed. + * + * @event + * @param {orion.textview.ModelChangedEvent} modelChangedEvent the event + */ + onModelChanged: function(modelChangedEvent) { + return this.dispatchEvent(modelChangedEvent); + }, + /** + * @class This is the event sent when the text in the model is about to change. + *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onModelChanging}
+ * {@link orion.textview.TextModel#onChanging} + *

+ * @name orion.textview.ModelChangingEvent + * + * @property {String} text The text that is about to be inserted in the model. + * @property {Number} start The character offset in the model where the change will occur. + * @property {Number} removedCharCount The number of characters being removed from the model. + * @property {Number} addedCharCount The number of characters being added to the model. + * @property {Number} removedLineCount The number of lines being removed from the model. + * @property {Number} addedLineCount The number of lines being added to the model. + */ + /** + * This event is sent when the text in the model is about to change. + * + * @event + * @param {orion.textview.ModelChangingEvent} modelChangingEvent the event + */ + onModelChanging: function(modelChangingEvent) { + return this.dispatchEvent(modelChangingEvent); + }, + /** + * @class This is the event sent when the text is modified by the text view. + *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onModify} + *

+ * @name orion.textview.ModifyEvent + */ + /** + * This event is sent when the text view has changed text in the model. + *

+ * If the text is changed directly through the model API, this event + * is not sent. + *

+ * + * @event + * @param {orion.textview.ModifyEvent} modifyEvent the event + */ + onModify: function(modifyEvent) { + return this.dispatchEvent(modifyEvent); + }, + onMouseDown: function(mouseEvent) { + return this.dispatchEvent(mouseEvent); + }, + onMouseUp: function(mouseEvent) { + return this.dispatchEvent(mouseEvent); + }, + onMouseMove: function(mouseEvent) { + return this.dispatchEvent(mouseEvent); + }, + onMouseOver: function(mouseEvent) { + return this.dispatchEvent(mouseEvent); + }, + onMouseOut: function(mouseEvent) { + return this.dispatchEvent(mouseEvent); + }, + /** + * @class This is the event sent when the selection changes in the text view. + *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onSelection} + *

+ * @name orion.textview.SelectionEvent + * + * @property {orion.textview.Selection} oldValue The old selection. + * @property {orion.textview.Selection} newValue The new selection. + */ + /** + * This event is sent when the text view selection has changed. + * + * @event + * @param {orion.textview.SelectionEvent} selectionEvent the event + */ + onSelection: function(selectionEvent) { + return this.dispatchEvent(selectionEvent); + }, + /** + * @class This is the event sent when the text view scrolls. + *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onScroll} + *

+ * @name orion.textview.ScrollEvent + * + * @property oldValue The old scroll {x,y}. + * @property newValue The new scroll {x,y}. + */ + /** + * This event is sent when the text view scrolls vertically or horizontally. + * + * @event + * @param {orion.textview.ScrollEvent} scrollEvent the event + */ + onScroll: function(scrollEvent) { + return this.dispatchEvent(scrollEvent); + }, + /** + * @class This is the event sent when the text is about to be modified by the text view. + *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onVerify} + *

+ * @name orion.textview.VerifyEvent + * + * @property {String} text The text being inserted. + * @property {Number} start The start offset of the text range to be replaced. + * @property {Number} end The end offset (exclusive) of the text range to be replaced. + */ + /** + * This event is sent when the text view is about to change text in the model. + *

+ * If the text is changed directly through the model API, this event + * is not sent. + *

+ *

+ * Listeners are allowed to change these parameters. Setting text to null + * or undefined stops the change. + *

+ * + * @event + * @param {orion.textview.VerifyEvent} verifyEvent the event + */ + onVerify: function(verifyEvent) { + return this.dispatchEvent(verifyEvent); + }, + /** + * @class This is the event sent when the text view has unloaded its contents. + *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onLoad} + *

+ * @name orion.textview.UnloadEvent + */ + /** + * This event is sent when the text view has unloaded its contents. + * + * @event + * @param {orion.textview.UnloadEvent} unloadEvent the event + */ + onUnload: function(unloadEvent) { + return this.dispatchEvent(unloadEvent); + }, + /** + * @class This is the event sent when the text view is focused. + *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onFocus}
+ *

+ * @name orion.textview.FocusEvent + */ + /** + * This event is sent when the text view is focused. + * + * @event + * @param {orion.textview.FocusEvent} focusEvent the event + */ + onFocus: function(focusEvent) { + return this.dispatchEvent(focusEvent); + }, + /** + * @class This is the event sent when the text view goes out of focus. + *

+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onBlur}
+ *

+ * @name orion.textview.BlurEvent + */ + /** + * This event is sent when the text view goes out of focus. + * + * @event + * @param {orion.textview.BlurEvent} blurEvent the event + */ + onBlur: function(blurEvent) { + return this.dispatchEvent(blurEvent); + }, + /** + * Redraws the entire view, including rulers. + * + * @see #redrawLines + * @see #redrawRange + * @see #setRedraw + */ + redraw: function() { + if (this._redrawCount > 0) { return; } + var lineCount = this._model.getLineCount(); + var rulers = this.getRulers(); + for (var i = 0; i < rulers.length; i++) { + this.redrawLines(0, lineCount, rulers[i]); + } + this.redrawLines(0, lineCount); + }, + /** + * Redraws the text in the given line range. + *

+ * The line at the end index is not redrawn. + *

+ * + * @param {Number} [startLine=0] the start line + * @param {Number} [endLine=line count] the end line + * + * @see #redraw + * @see #redrawRange + * @see #setRedraw + */ + redrawLines: function(startLine, endLine, ruler) { + if (this._redrawCount > 0) { return; } + if (startLine === undefined) { startLine = 0; } + if (endLine === undefined) { endLine = this._model.getLineCount(); } + if (startLine === endLine) { return; } + var div = this._clientDiv; + if (!div) { return; } + if (ruler) { + var location = ruler.getLocation();//"left" or "right" + var divRuler = location === "left" ? this._leftDiv : this._rightDiv; + var cells = divRuler.firstChild.rows[0].cells; + for (var i = 0; i < cells.length; i++) { + if (cells[i].firstChild._ruler === ruler) { + div = cells[i].firstChild; + break; + } + } + } + if (ruler) { + div.rulerChanged = true; + } + if (!ruler || ruler.getOverview() === "page") { + var child = div.firstChild; + while (child) { + var lineIndex = child.lineIndex; + if (startLine <= lineIndex && lineIndex < endLine) { + child.lineChanged = true; + } + child = child.nextSibling; + } + } + if (!ruler) { + if (startLine <= this._maxLineIndex && this._maxLineIndex < endLine) { + this._checkMaxLineIndex = this._maxLineIndex; + this._maxLineIndex = -1; + this._maxLineWidth = 0; + } + } + this._queueUpdatePage(); + }, + /** + * Redraws the text in the given range. + *

+ * The character at the end offset is not redrawn. + *

+ * + * @param {Number} [start=0] the start offset of text range + * @param {Number} [end=char count] the end offset of text range + * + * @see #redraw + * @see #redrawLines + * @see #setRedraw + */ + redrawRange: function(start, end) { + if (this._redrawCount > 0) { return; } + var model = this._model; + if (start === undefined) { start = 0; } + if (end === undefined) { end = model.getCharCount(); } + var startLine = model.getLineAtOffset(start); + var endLine = model.getLineAtOffset(Math.max(start, end - 1)) + 1; + this.redrawLines(startLine, endLine); + }, + /** + * Removes a ruler from the text view. + * + * @param {orion.textview.Ruler} ruler the ruler. + */ + removeRuler: function (ruler) { + var rulers = this._rulers; + for (var i=0; i + * If the action name is a predefined action, the given handler executes before + * the default action handler. If the given handler returns true, the + * default action handler is not called. + *

+ * + * @param {String} name the action name. + * @param {Function} handler the action handler. + * + * @see #getActions + * @see #invokeAction + */ + setAction: function(name, handler) { + if (!name) { return; } + var actions = this._actions; + for (var i = 0; i < actions.length; i++) { + var a = actions[i]; + if (a.name === name) { + a.userHandler = handler; + return; + } + } + actions.push({name: name, userHandler: handler}); + }, + /** + * Associates a key binding with the given action name. Any previous + * association with the specified key binding is overwriten. If the + * action name is null, the association is removed. + * + * @param {orion.textview.KeyBinding} keyBinding the key binding + * @param {String} name the action + */ + setKeyBinding: function(keyBinding, name) { + var keyBindings = this._keyBindings; + for (var i = 0; i < keyBindings.length; i++) { + var kb = keyBindings[i]; + if (kb.keyBinding.equals(keyBinding)) { + if (name) { + kb.name = name; + } else { + if (kb.predefined) { + kb.name = null; + } else { + var oldName = kb.name; + keyBindings.splice(i, 1); + var index = 0; + while (index < keyBindings.length && oldName !== keyBindings[index].name) { + index++; + } + if (index === keyBindings.length) { + /*

+ * Removing all the key bindings associated to an user action will cause + * the user action to be removed. TextView predefined actions are never + * removed (so they can be reinstalled in the future). + *

+ */ + var actions = this._actions; + for (var j = 0; j < actions.length; j++) { + if (actions[j].name === oldName) { + if (!actions[j].defaultHandler) { + actions.splice(j, 1); + } + } + } + } + } + } + return; + } + } + if (name) { + keyBindings.push({keyBinding: keyBinding, name: name}); + } + }, + /** + * Sets the caret offset relative to the start of the document. + * + * @param {Number} caret the caret offset relative to the start of the document. + * @param {Boolean} [show=true] if true, the view will scroll if needed to show the caret location. + * + * @see #getCaretOffset + * @see #setSelection + * @see #getSelection + */ + setCaretOffset: function(offset, show) { + var charCount = this._model.getCharCount(); + offset = Math.max(0, Math.min (offset, charCount)); + var selection = new Selection(offset, offset, false); + this._setSelection (selection, show === undefined || show); + }, + /** + * Sets the horizontal pixel. + *

+ * The horizontal pixel is the pixel position that is currently at + * the left edge of the view. This position is relative to the + * beginning of the document. + *

+ * + * @param {Number} pixel the horizontal pixel. + * + * @see #getHorizontalPixel + * @see #convert + */ + setHorizontalPixel: function(pixel) { + if (!this._clientDiv) { return; } + pixel = Math.max(0, pixel); + this._scrollView(pixel - this._getScroll().x, 0); + }, + /** + * Sets whether the view should update the DOM. + *

+ * This can be used to improve the performance. + *

+ * When the flag is set to true, + * the entire view is marked as needing to be redrawn. + * Nested calls to this method are stacked. + *

+ * + * @param {Boolean} redraw the new redraw state + * + * @see #redraw + */ + setRedraw: function(redraw) { + if (redraw) { + if (--this._redrawCount === 0) { + this.redraw(); + } + } else { + this._redrawCount++; + } + }, + /** + * Sets the text model of the text view. + * + * @param {orion.textview.TextModel} model the text model of the view. + */ + setModel: function(model) { + if (!model) { return; } + if (model === this._model) { return; } + this._model.removeEventListener("Changing", this._modelListener.onChanging); + this._model.removeEventListener("Changed", this._modelListener.onChanged); + var oldLineCount = this._model.getLineCount(); + var oldCharCount = this._model.getCharCount(); + var newLineCount = model.getLineCount(); + var newCharCount = model.getCharCount(); + var newText = model.getText(); + var e = { + type: "ModelChanging", + text: newText, + start: 0, + removedCharCount: oldCharCount, + addedCharCount: newCharCount, + removedLineCount: oldLineCount, + addedLineCount: newLineCount + }; + this.onModelChanging(e); + this._model = model; + e = { + type: "ModelChanged", + start: 0, + removedCharCount: oldCharCount, + addedCharCount: newCharCount, + removedLineCount: oldLineCount, + addedLineCount: newLineCount + }; + this.onModelChanged(e); + this._model.addEventListener("Changing", this._modelListener.onChanging); + this._model.addEventListener("Changed", this._modelListener.onChanged); + this._reset(); + this._updatePage(); + }, + /** + * Sets the view options for the view. + * + * @param {orion.textview.TextViewOptions} options the view options. + * + * @see #getOptions + */ + setOptions: function (options) { + var defaultOptions = this._defaultOptions(); + var recreate = false, option, created = this._clientDiv; + if (created) { + for (option in options) { + if (options.hasOwnProperty(option)) { + if (defaultOptions[option].recreate) { + recreate = true; + break; + } + } + } + } + var changed = false; + for (option in options) { + if (options.hasOwnProperty(option)) { + var newValue = options[option], oldValue = this["_" + option]; + if (this._compare(oldValue, newValue)) { continue; } + changed = true; + if (!recreate) { + var update = defaultOptions[option].update; + if (created && update) { + if (update.call(this, newValue)) { + recreate = true; + } + continue; + } + } + this["_" + option] = this._clone(newValue); + } + } + if (changed) { + if (recreate) { + var oldParent = this._frame.parentNode; + oldParent.removeChild(this._frame); + this._parent.appendChild(this._frame); + } + } + }, + /** + * Sets the text view selection. + *

+ * The selection is defined by a start and end character offset relative to the + * document. The character at end offset is not included in the selection. + *

+ *

+ * The caret is always placed at the end offset. The start offset can be + * greater than the end offset to place the caret at the beginning of the + * selection. + *

+ *

+ * Clamps out of range offsets. + *

+ * + * @param {Number} start the start offset of the selection + * @param {Number} end the end offset of the selection + * @param {Boolean} [show=true] if true, the view will scroll if needed to show the caret location. + * + * @see #getSelection + */ + setSelection: function (start, end, show) { + var caret = start > end; + if (caret) { + var tmp = start; + start = end; + end = tmp; + } + var charCount = this._model.getCharCount(); + start = Math.max(0, Math.min (start, charCount)); + end = Math.max(0, Math.min (end, charCount)); + var selection = new Selection(start, end, caret); + this._setSelection(selection, show === undefined || show); + }, + /** + * Replaces the text in the given range with the given text. + *

+ * The character at the end offset is not replaced. + *

+ *

+ * When both start and end parameters + * are not specified, the text view places the caret at the beginning + * of the document and scrolls to make it visible. + *

+ * + * @param {String} text the new text. + * @param {Number} [start=0] the start offset of text range. + * @param {Number} [end=char count] the end offset of text range. + * + * @see #getText + */ + setText: function (text, start, end) { + var reset = start === undefined && end === undefined; + if (start === undefined) { start = 0; } + if (end === undefined) { end = this._model.getCharCount(); } + this._modifyContent({text: text, start: start, end: end, _code: true}, !reset); + if (reset) { + this._columnX = -1; + this._setSelection(new Selection (0, 0, false), true); + + /* + * Bug in Firefox. For some reason, the caret does not show after the + * view is refreshed. The fix is to toggle the contentEditable state and + * force the clientDiv to loose and receive focus if it is focused. + */ + if (isFirefox) { + this._fixCaret(); + } + } + }, + /** + * Sets the top index. + *

+ * The top index is the line that is currently at the top of the text view. This + * line may be partially visible depending on the vertical scroll of the view. + *

+ * + * @param {Number} topIndex the index of the top line. + * + * @see #getBottomIndex + * @see #getTopIndex + */ + setTopIndex: function(topIndex) { + if (!this._clientDiv) { return; } + var model = this._model; + if (model.getCharCount() === 0) { + return; + } + var lineCount = model.getLineCount(); + var lineHeight = this._getLineHeight(); + var pageSize = Math.max(1, Math.min(lineCount, Math.floor(this._getClientHeight () / lineHeight))); + if (topIndex < 0) { + topIndex = 0; + } else if (topIndex > lineCount - pageSize) { + topIndex = lineCount - pageSize; + } + var pixel = topIndex * lineHeight - this._getScroll().y; + this._scrollView(0, pixel); + }, + /** + * Sets the top pixel. + *

+ * The top pixel is the pixel position that is currently at + * the top edge of the view. This position is relative to the + * beginning of the document. + *

+ * + * @param {Number} pixel the top pixel. + * + * @see #getBottomPixel + * @see #getTopPixel + * @see #convert + */ + setTopPixel: function(pixel) { + if (!this._clientDiv) { return; } + var lineHeight = this._getLineHeight(); + var clientHeight = this._getClientHeight(); + var lineCount = this._model.getLineCount(); + pixel = Math.min(Math.max(0, pixel), lineHeight * lineCount - clientHeight); + this._scrollView(0, pixel - this._getScroll().y); + }, + /** + * Scrolls the selection into view if needed. + * + * @returns true if the view was scrolled. + * + * @see #getSelection + * @see #setSelection + */ + showSelection: function() { + return this._showCaret(true); + }, + + /**************************************** Event handlers *********************************/ + _handleBodyMouseDown: function (e) { + if (!e) { e = window.event; } + if (isFirefox && e.which === 1) { + this._clientDiv.contentEditable = false; + (this._overlayDiv || this._clientDiv).draggable = true; + this._ignoreBlur = true; + } + + /* + * Prevent clicks outside of the view from taking focus + * away the view. Note that in Firefox and Opera clicking on the + * scrollbar also take focus from the view. Other browsers + * do not have this problem and stopping the click over the + * scrollbar for them causes mouse capture problems. + */ + var topNode = isOpera || (isFirefox && !this._overlayDiv) ? this._clientDiv : this._overlayDiv || this._viewDiv; + + var temp = e.target ? e.target : e.srcElement; + while (temp) { + if (topNode === temp) { + return; + } + temp = temp.parentNode; + } + if (e.preventDefault) { e.preventDefault(); } + if (e.stopPropagation){ e.stopPropagation(); } + if (!isW3CEvents) { + /* In IE 8 is not possible to prevent the default handler from running + * during mouse down event using usual API. The workaround is to use + * setCapture/releaseCapture. + */ + topNode.setCapture(); + setTimeout(function() { topNode.releaseCapture(); }, 0); + } + }, + _handleBodyMouseUp: function (e) { + if (!e) { e = window.event; } + if (isFirefox && e.which === 1) { + this._clientDiv.contentEditable = true; + (this._overlayDiv || this._clientDiv).draggable = false; + + /* + * Bug in Firefox. For some reason, Firefox stops showing the caret + * in some cases. For example when the user cancels a drag operation + * by pressing ESC. The fix is to detect that the drag operation was + * cancelled, toggle the contentEditable state and force the clientDiv + * to loose and receive focus if it is focused. + */ + this._fixCaret(); + this._ignoreBlur = false; + } + }, + _handleBlur: function (e) { + if (!e) { e = window.event; } + if (this._ignoreBlur) { return; } + this._hasFocus = false; + /* + * Bug in IE 8 and earlier. For some reason when text is deselected + * the overflow selection at the end of some lines does not get redrawn. + * The fix is to create a DOM element in the body to force a redraw. + */ + if (isIE < 9) { + if (!this._getSelection().isEmpty()) { + var document = this._frameDocument; + var child = document.createElement("DIV"); + var body = document.body; + body.appendChild(child); + body.removeChild(child); + } + } + if (isFirefox || isIE) { + if (this._selDiv1) { + var color = isIE ? "transparent" : "#AFAFAF"; + this._selDiv1.style.background = color; + this._selDiv2.style.background = color; + this._selDiv3.style.background = color; + } + } + if (!this._ignoreFocus) { + this.onBlur({type: "Blur"}); + } + }, + _handleContextMenu: function (e) { + if (!e) { e = window.event; } + if (isFirefox && this._lastMouseButton === 3) { + // We need to update the DOM selection, because on + // right-click the caret moves to the mouse location. + // See bug 366312. + var timeDiff = e.timeStamp - this._lastMouseTime; + if (timeDiff <= this._clickTime) { + this._updateDOMSelection(); + } + } + if (this.isListening("ContextMenu")) { + var evt = this._createMouseEvent("ContextMenu", e); + evt.screenX = e.screenX; + evt.screenY = e.screenY; + this.onContextMenu(evt); + } + if (e.preventDefault) { e.preventDefault(); } + return false; + }, + _handleCopy: function (e) { + if (this._ignoreCopy) { return; } + if (!e) { e = window.event; } + if (this._doCopy(e)) { + if (e.preventDefault) { e.preventDefault(); } + return false; + } + }, + _handleCut: function (e) { + if (!e) { e = window.event; } + if (this._doCut(e)) { + if (e.preventDefault) { e.preventDefault(); } + return false; + } + }, + _handleDOMAttrModified: function (e) { + if (!e) { e = window.event; } + var ancestor = false; + var parent = this._parent; + while (parent) { + if (parent === e.target) { + ancestor = true; + break; + } + parent = parent.parentNode; + } + if (!ancestor) { return; } + var state = this._getVisible(); + if (state === "visible") { + this._createView(); + } else if (state === "hidden") { + this._destroyView(); + } + }, + _handleDataModified: function(e) { + this._startIME(); + }, + _handleDblclick: function (e) { + if (!e) { e = window.event; } + var time = e.timeStamp ? e.timeStamp : new Date().getTime(); + this._lastMouseTime = time; + if (this._clickCount !== 2) { + this._clickCount = 2; + this._handleMouse(e); + } + }, + _handleDragStart: function (e) { + if (!e) { e = window.event; } + if (isFirefox) { + var self = this; + setTimeout(function() { + self._clientDiv.contentEditable = true; + self._clientDiv.draggable = false; + self._ignoreBlur = false; + }, 0); + } + if (this.isListening("DragStart") && this._dragOffset !== -1) { + this._isMouseDown = false; + this.onDragStart(this._createMouseEvent("DragStart", e)); + this._dragOffset = -1; + } else { + if (e.preventDefault) { e.preventDefault(); } + return false; + } + }, + _handleDrag: function (e) { + if (!e) { e = window.event; } + if (this.isListening("Drag")) { + this.onDrag(this._createMouseEvent("Drag", e)); + } + }, + _handleDragEnd: function (e) { + if (!e) { e = window.event; } + this._dropTarget = false; + this._dragOffset = -1; + if (this.isListening("DragEnd")) { + this.onDragEnd(this._createMouseEvent("DragEnd", e)); + } + if (isFirefox) { + this._fixCaret(); + /* + * Bug in Firefox. For some reason, Firefox stops showing the caret when the + * selection is dropped onto itself. The fix is to detected the case and + * call fixCaret() a second time. + */ + if (e.dataTransfer.dropEffect === "none" && !e.dataTransfer.mozUserCancelled) { + this._fixCaret(); + } + } + }, + _handleDragEnter: function (e) { + if (!e) { e = window.event; } + var prevent = true; + this._dropTarget = true; + if (this.isListening("DragEnter")) { + prevent = false; + this.onDragEnter(this._createMouseEvent("DragEnter", e)); + } + /* + * Webkit will not send drop events if this event is not prevented, as spec in HTML5. + * Firefox and IE do not follow this spec for contentEditable. Note that preventing this + * event will result is loss of functionality (insertion mark, etc). + */ + if (isWebkit || prevent) { + if (e.preventDefault) { e.preventDefault(); } + return false; + } + }, + _handleDragOver: function (e) { + if (!e) { e = window.event; } + var prevent = true; + if (this.isListening("DragOver")) { + prevent = false; + this.onDragOver(this._createMouseEvent("DragOver", e)); + } + /* + * Webkit will not send drop events if this event is not prevented, as spec in HTML5. + * Firefox and IE do not follow this spec for contentEditable. Note that preventing this + * event will result is loss of functionality (insertion mark, etc). + */ + if (isWebkit || prevent) { + if (prevent) { e.dataTransfer.dropEffect = "none"; } + if (e.preventDefault) { e.preventDefault(); } + return false; + } + }, + _handleDragLeave: function (e) { + if (!e) { e = window.event; } + this._dropTarget = false; + if (this.isListening("DragLeave")) { + this.onDragLeave(this._createMouseEvent("DragLeave", e)); + } + }, + _handleDrop: function (e) { + if (!e) { e = window.event; } + this._dropTarget = false; + if (this.isListening("Drop")) { + this.onDrop(this._createMouseEvent("Drop", e)); + } + /* + * This event must be prevented otherwise the user agent will modify + * the DOM. Note that preventing the event on some user agents (i.e. IE) + * indicates that the operation is cancelled. This causes the dropEffect to + * be set to none in the dragend event causing the implementor to not execute + * the code responsible by the move effect. + */ + if (e.preventDefault) { e.preventDefault(); } + return false; + }, + _handleDocFocus: function (e) { + if (!e) { e = window.event; } + this._clientDiv.focus(); + }, + _handleFocus: function (e) { + if (!e) { e = window.event; } + this._hasFocus = true; + /* + * Feature in IE. The selection is not restored when the + * view gets focus and the caret is always placed at the + * beginning of the document. The fix is to update the DOM + * selection during the focus event. + */ + if (isIE) { + this._updateDOMSelection(); + } + if (isFirefox || isIE) { + if (this._selDiv1) { + var color = this._hightlightRGB; + this._selDiv1.style.background = color; + this._selDiv2.style.background = color; + this._selDiv3.style.background = color; + } + } + if (!this._ignoreFocus) { + this.onFocus({type: "Focus"}); + } + }, + _handleKeyDown: function (e) { + if (!e) { e = window.event; } + if (isPad) { + if (e.keyCode === 8) { + this._doBackspace({}); + e.preventDefault(); + } + return; + } + switch (e.keyCode) { + case 16: /* Shift */ + case 17: /* Control */ + case 18: /* Alt */ + case 91: /* Command */ + break; + default: + this._setLinksVisible(false); + } + if (e.keyCode === 229) { + if (this._readonly) { + if (e.preventDefault) { e.preventDefault(); } + return false; + } + var startIME = true; + + /* + * Bug in Safari. Some Control+key combinations send key events + * with keyCode equals to 229. This is unexpected and causes the + * view to start an IME composition. The fix is to ignore these + * events. + */ + if (isSafari && isMac) { + if (e.ctrlKey) { + startIME = false; + } + } + if (startIME) { + this._startIME(); + } + } else { + this._commitIME(); + } + /* + * Feature in Firefox. When a key is held down the browser sends + * right number of keypress events but only one keydown. This is + * unexpected and causes the view to only execute an action + * just one time. The fix is to ignore the keydown event and + * execute the actions from the keypress handler. + * Note: This only happens on the Mac and Linux (Firefox 3.6). + * + * Feature in Opera. Opera sends keypress events even for non-printable + * keys. The fix is to handle actions in keypress instead of keydown. + */ + if (((isMac || isLinux) && isFirefox < 4) || isOpera) { + this._keyDownEvent = e; + return true; + } + + if (this._doAction(e)) { + if (e.preventDefault) { + e.preventDefault(); + } else { + e.cancelBubble = true; + e.returnValue = false; + e.keyCode = 0; + } + return false; + } + }, + _handleKeyPress: function (e) { + if (!e) { e = window.event; } + /* + * Feature in Embedded WebKit. Embedded WekKit on Mac runs in compatibility mode and + * generates key press events for these Unicode values (Function keys). This does not + * happen in Safari or Chrome. The fix is to ignore these key events. + */ + if (isMac && isWebkit) { + if ((0xF700 <= e.keyCode && e.keyCode <= 0xF7FF) || e.keyCode === 13 || e.keyCode === 8) { + if (e.preventDefault) { e.preventDefault(); } + return false; + } + } + if (((isMac || isLinux) && isFirefox < 4) || isOpera) { + if (this._doAction(this._keyDownEvent)) { + if (e.preventDefault) { e.preventDefault(); } + return false; + } + } + var ctrlKey = isMac ? e.metaKey : e.ctrlKey; + if (e.charCode !== undefined) { + if (ctrlKey) { + switch (e.charCode) { + /* + * In Firefox and Safari if ctrl+v, ctrl+c ctrl+x is canceled + * the clipboard events are not sent. The fix to allow + * the browser to handles these key events. + */ + case 99://c + case 118://v + case 120://x + return true; + } + } + } + var ignore = false; + if (isMac) { + if (e.ctrlKey || e.metaKey) { ignore = true; } + } else { + if (isFirefox) { + //Firefox clears the state mask when ALT GR generates input + if (e.ctrlKey || e.altKey) { ignore = true; } + } else { + //IE and Chrome only send ALT GR when input is generated + if (e.ctrlKey ^ e.altKey) { ignore = true; } + } + } + if (!ignore) { + var key = isOpera ? e.which : (e.charCode !== undefined ? e.charCode : e.keyCode); + if (key > 31) { + this._doContent(String.fromCharCode (key)); + if (e.preventDefault) { e.preventDefault(); } + return false; + } + } + }, + _handleKeyUp: function (e) { + if (!e) { e = window.event; } + var ctrlKey = isMac ? e.metaKey : e.ctrlKey; + if (!ctrlKey) { + this._setLinksVisible(false); + } + // don't commit for space (it happens during JP composition) + if (e.keyCode === 13) { + this._commitIME(); + } + }, + _handleLinkClick: function (e) { + if (!e) { e = window.event; } + var ctrlKey = isMac ? e.metaKey : e.ctrlKey; + if (!ctrlKey) { + if (e.preventDefault) { e.preventDefault(); } + return false; + } + }, + _handleLoad: function (e) { + var state = this._getVisible(); + if (state === "visible" || (state === "hidden" && isWebkit)) { + this._createView(); + } + }, + _handleMouse: function (e) { + var result = true; + var target = this._frameWindow; + if (isIE || (isFirefox && !this._overlayDiv)) { target = this._clientDiv; } + if (this._overlayDiv) { + if (this._hasFocus) { + this._ignoreFocus = true; + } + var self = this; + setTimeout(function () { + self.focus(); + self._ignoreFocus = false; + }, 0); + } + if (this._clickCount === 1) { + result = this._setSelectionTo(e.clientX, e.clientY, e.shiftKey, !isOpera && this.isListening("DragStart")); + if (result) { this._setGrab(target); } + } else { + /* + * Feature in IE8 and older, the sequence of events in the IE8 event model + * for a doule-click is: + * + * down + * up + * up + * dblclick + * + * Given that the mouse down/up events are not balanced, it is not possible to + * grab on mouse down and ungrab on mouse up. The fix is to grab on the first + * mouse down and ungrab on mouse move when the button 1 is not set. + */ + if (isW3CEvents) { this._setGrab(target); } + + this._doubleClickSelection = null; + this._setSelectionTo(e.clientX, e.clientY, e.shiftKey); + this._doubleClickSelection = this._getSelection(); + } + return result; + }, + _handleMouseDown: function (e) { + if (!e) { e = window.event; } + if (this.isListening("MouseDown")) { + this.onMouseDown(this._createMouseEvent("MouseDown", e)); + } + if (this._linksVisible) { + var target = e.target || e.srcElement; + if (target.tagName !== "A") { + this._setLinksVisible(false); + } else { + return; + } + } + this._commitIME(); + + var button = e.which; // 1 - left, 2 - middle, 3 - right + if (!button) { + // if IE 8 or older + if (e.button === 4) { button = 2; } + if (e.button === 2) { button = 3; } + if (e.button === 1) { button = 1; } + } + + // For middle click we always need getTime(). See _getClipboardText(). + var time = button !== 2 && e.timeStamp ? e.timeStamp : new Date().getTime(); + var timeDiff = time - this._lastMouseTime; + var deltaX = Math.abs(this._lastMouseX - e.clientX); + var deltaY = Math.abs(this._lastMouseY - e.clientY); + var sameButton = this._lastMouseButton === button; + this._lastMouseX = e.clientX; + this._lastMouseY = e.clientY; + this._lastMouseTime = time; + this._lastMouseButton = button; + + if (button === 1) { + this._isMouseDown = true; + if (sameButton && timeDiff <= this._clickTime && deltaX <= this._clickDist && deltaY <= this._clickDist) { + this._clickCount++; + } else { + this._clickCount = 1; + } + if (this._handleMouse(e) && (isOpera || isChrome || (isFirefox && !this._overlayDiv))) { + if (!this._hasFocus) { + this.focus(); + } + e.preventDefault(); + } + } + }, + _handleMouseOver: function (e) { + if (!e) { e = window.event; } + if (this.isListening("MouseOver")) { + this.onMouseOver(this._createMouseEvent("MouseOver", e)); + } + }, + _handleMouseOut: function (e) { + if (!e) { e = window.event; } + if (this.isListening("MouseOut")) { + this.onMouseOut(this._createMouseEvent("MouseOut", e)); + } + }, + _handleMouseMove: function (e) { + if (!e) { e = window.event; } + if (this.isListening("MouseMove")) { + var topNode = this._overlayDiv || this._clientDiv; + var temp = e.target ? e.target : e.srcElement; + while (temp) { + if (topNode === temp) { + this.onMouseMove(this._createMouseEvent("MouseMove", e)); + break; + } + temp = temp.parentNode; + } + } + if (this._dropTarget) { + return; + } + /* + * Bug in IE9. IE sends one mouse event when the user changes the text by + * pasting or undo. These operations usually happen with the Ctrl key + * down which causes the view to enter link mode. Link mode does not end + * because there are no further events. The fix is to only enter link + * mode when the coordinates of the mouse move event have changed. + */ + var changed = this._linksVisible || this._lastMouseMoveX !== e.clientX || this._lastMouseMoveY !== e.clientY; + this._lastMouseMoveX = e.clientX; + this._lastMouseMoveY = e.clientY; + this._setLinksVisible(changed && !this._isMouseDown && (isMac ? e.metaKey : e.ctrlKey)); + + /* + * Feature in IE8 and older, the sequence of events in the IE8 event model + * for a doule-click is: + * + * down + * up + * up + * dblclick + * + * Given that the mouse down/up events are not balanced, it is not possible to + * grab on mouse down and ungrab on mouse up. The fix is to grab on the first + * mouse down and ungrab on mouse move when the button 1 is not set. + * + * In order to detect double-click and drag gestures, it is necessary to send + * a mouse down event from mouse move when the button is still down and isMouseDown + * flag is not set. + */ + if (!isW3CEvents) { + if (e.button === 0) { + this._setGrab(null); + return true; + } + if (!this._isMouseDown && e.button === 1 && (this._clickCount & 1) !== 0) { + this._clickCount = 2; + return this._handleMouse(e, this._clickCount); + } + } + if (!this._isMouseDown || this._dragOffset !== -1) { + return; + } + + var x = e.clientX; + var y = e.clientY; + if (isChrome) { + if (e.currentTarget !== this._frameWindow) { + var rect = this._frame.getBoundingClientRect(); + x -= rect.left; + y -= rect.top; + } + } + var viewPad = this._getViewPadding(); + var viewRect = this._viewDiv.getBoundingClientRect(); + var width = this._getClientWidth (), height = this._getClientHeight(); + var leftEdge = viewRect.left + viewPad.left; + var topEdge = viewRect.top + viewPad.top; + var rightEdge = viewRect.left + viewPad.left + width; + var bottomEdge = viewRect.top + viewPad.top + height; + var model = this._model; + var caretLine = model.getLineAtOffset(this._getSelection().getCaret()); + if (y < topEdge && caretLine !== 0) { + this._doAutoScroll("up", x, y - topEdge); + } else if (y > bottomEdge && caretLine !== model.getLineCount() - 1) { + this._doAutoScroll("down", x, y - bottomEdge); + } else if (x < leftEdge) { + this._doAutoScroll("left", x - leftEdge, y); + } else if (x > rightEdge) { + this._doAutoScroll("right", x - rightEdge, y); + } else { + this._endAutoScroll(); + this._setSelectionTo(x, y, true); + /* + * Feature in IE. IE does redraw the selection background right + * away after the selection changes because of mouse move events. + * The fix is to call getBoundingClientRect() on the + * body element to force the selection to be redraw. Some how + * calling this method forces a redraw. + */ + if (isIE) { + var body = this._frameDocument.body; + body.getBoundingClientRect(); + } + } + }, + _createMouseEvent: function(type, e) { + var scroll = this._getScroll(); + var viewRect = this._viewDiv.getBoundingClientRect(); + var viewPad = this._getViewPadding(); + var x = e.clientX + scroll.x - viewRect.left - viewPad.left; + var y = e.clientY + scroll.y - viewRect.top; + return { + type: type, + event: e, + x: x, + y: y + }; + }, + _handleMouseUp: function (e) { + if (!e) { e = window.event; } + if (this.isListening("MouseUp")) { + this.onMouseUp(this._createMouseEvent("MouseUp", e)); + } + if (this._linksVisible) { + return; + } + var left = e.which ? e.button === 0 : e.button === 1; + if (left) { + if (this._dragOffset !== -1) { + var selection = this._getSelection(); + selection.extend(this._dragOffset); + selection.collapse(); + this._setSelection(selection, true, true); + this._dragOffset = -1; + } + this._isMouseDown = false; + this._endAutoScroll(); + + /* + * Feature in IE8 and older, the sequence of events in the IE8 event model + * for a doule-click is: + * + * down + * up + * up + * dblclick + * + * Given that the mouse down/up events are not balanced, it is not possible to + * grab on mouse down and ungrab on mouse up. The fix is to grab on the first + * mouse down and ungrab on mouse move when the button 1 is not set. + */ + if (isW3CEvents) { this._setGrab(null); } + + /* + * Note that there cases when Firefox sets the DOM selection in mouse up. + * This happens for example after a cancelled drag operation. + * + * Note that on Chrome and IE, the caret stops blicking if mouse up is + * prevented. + */ + if (isFirefox) { + e.preventDefault(); + } + } + }, + _handleMouseWheel: function (e) { + if (!e) { e = window.event; } + var lineHeight = this._getLineHeight(); + var pixelX = 0, pixelY = 0; + // Note: On the Mac the correct behaviour is to scroll by pixel. + if (isFirefox) { + var pixel; + if (isMac) { + pixel = e.detail * 3; + } else { + var limit = 256; + pixel = Math.max(-limit, Math.min(limit, e.detail)) * lineHeight; + } + if (e.axis === e.HORIZONTAL_AXIS) { + pixelX = pixel; + } else { + pixelY = pixel; + } + } else { + //Webkit + if (isMac) { + /* + * In Safari, the wheel delta is a multiple of 120. In order to + * convert delta to pixel values, it is necessary to divide delta + * by 40. + * + * In Chrome and Safari 5, the wheel delta depends on the type of the + * mouse. In general, it is the pixel value for Mac mice and track pads, + * but it is a multiple of 120 for other mice. There is no presise + * way to determine if it is pixel value or a multiple of 120. + * + * Note that the current approach does not calculate the correct + * pixel value for Mac mice when the delta is a multiple of 120. + */ + var denominatorX = 40, denominatorY = 40; + if (e.wheelDeltaX % 120 !== 0) { denominatorX = 1; } + if (e.wheelDeltaY % 120 !== 0) { denominatorY = 1; } + pixelX = -e.wheelDeltaX / denominatorX; + if (-1 < pixelX && pixelX < 0) { pixelX = -1; } + if (0 < pixelX && pixelX < 1) { pixelX = 1; } + pixelY = -e.wheelDeltaY / denominatorY; + if (-1 < pixelY && pixelY < 0) { pixelY = -1; } + if (0 < pixelY && pixelY < 1) { pixelY = 1; } + } else { + pixelX = -e.wheelDeltaX; + var linesToScroll = 8; + pixelY = (-e.wheelDeltaY / 120 * linesToScroll) * lineHeight; + } + } + /* + * Feature in Safari. If the event target is removed from the DOM + * safari stops smooth scrolling. The fix is keep the element target + * in the DOM and remove it on a later time. + * + * Note: Using a timer is not a solution, because the timeout needs to + * be at least as long as the gesture (which is too long). + */ + if (isSafari) { + var lineDiv = e.target; + while (lineDiv && lineDiv.lineIndex === undefined) { + lineDiv = lineDiv.parentNode; + } + this._mouseWheelLine = lineDiv; + } + var oldScroll = this._getScroll(); + this._scrollView(pixelX, pixelY); + var newScroll = this._getScroll(); + if (isSafari) { this._mouseWheelLine = null; } + if (oldScroll.x !== newScroll.x || oldScroll.y !== newScroll.y) { + if (e.preventDefault) { e.preventDefault(); } + return false; + } + }, + _handlePaste: function (e) { + if (this._ignorePaste) { return; } + if (!e) { e = window.event; } + if (this._doPaste(e)) { + if (isIE) { + /* + * Bug in IE, + */ + var self = this; + this._ignoreFocus = true; + setTimeout(function() { + self._updateDOMSelection(); + this._ignoreFocus = false; + }, 0); + } + if (e.preventDefault) { e.preventDefault(); } + return false; + } + }, + _handleResize: function (e) { + if (!e) { e = window.event; } + var element = this._frameDocument.documentElement; + var newWidth = element.clientWidth; + var newHeight = element.clientHeight; + if (this._frameWidth !== newWidth || this._frameHeight !== newHeight) { + this._frameWidth = newWidth; + this._frameHeight = newHeight; + /* + * Feature in IE7. For some reason, sometimes Internet Explorer 7 + * returns incorrect values for element.getBoundingClientRect() when + * inside a resize handler. The fix is to queue the work. + */ + if (isIE < 9) { + this._queueUpdatePage(); + } else { + this._updatePage(); + } + } + }, + _handleRulerEvent: function (e) { + if (!e) { e = window.event; } + var target = e.target ? e.target : e.srcElement; + var lineIndex = target.lineIndex; + var element = target; + while (element && !element._ruler) { + if (lineIndex === undefined && element.lineIndex !== undefined) { + lineIndex = element.lineIndex; + } + element = element.parentNode; + } + var ruler = element ? element._ruler : null; + if (lineIndex === undefined && ruler && ruler.getOverview() === "document") { + var buttonHeight = isPad ? 0 : 17; + var clientHeight = this._getClientHeight (); + var lineCount = this._model.getLineCount (); + var viewPad = this._getViewPadding(); + var trackHeight = clientHeight + viewPad.top + viewPad.bottom - 2 * buttonHeight; + lineIndex = Math.floor((e.clientY - buttonHeight) * lineCount / trackHeight); + if (!(0 <= lineIndex && lineIndex < lineCount)) { + lineIndex = undefined; + } + } + if (ruler) { + switch (e.type) { + case "click": + if (ruler.onClick) { ruler.onClick(lineIndex, e); } + break; + case "dblclick": + if (ruler.onDblClick) { ruler.onDblClick(lineIndex, e); } + break; + case "mousemove": + if (ruler.onMouseMove) { ruler.onMouseMove(lineIndex, e); } + break; + case "mouseover": + if (ruler.onMouseOver) { ruler.onMouseOver(lineIndex, e); } + break; + case "mouseout": + if (ruler.onMouseOut) { ruler.onMouseOut(lineIndex, e); } + break; + } + } + }, + _handleScroll: function () { + var scroll = this._getScroll(); + var oldX = this._hScroll; + var oldY = this._vScroll; + if (oldX !== scroll.x || oldY !== scroll.y) { + this._hScroll = scroll.x; + this._vScroll = scroll.y; + this._commitIME(); + this._updatePage(oldY === scroll.y); + var e = { + type: "Scroll", + oldValue: {x: oldX, y: oldY}, + newValue: scroll + }; + this.onScroll(e); + } + }, + _handleSelectStart: function (e) { + if (!e) { e = window.event; } + if (this._ignoreSelect) { + if (e && e.preventDefault) { e.preventDefault(); } + return false; + } + }, + _handleUnload: function (e) { + if (!e) { e = window.event; } + this._destroyView(); + }, + _handleInput: function (e) { + var textArea = this._textArea; + this._doContent(textArea.value); + textArea.selectionStart = textArea.selectionEnd = 0; + textArea.value = ""; + e.preventDefault(); + }, + _handleTextInput: function (e) { + this._doContent(e.data); + e.preventDefault(); + }, + _touchConvert: function (touch) { + var rect = this._frame.getBoundingClientRect(); + var body = this._parentDocument.body; + return {left: touch.clientX - rect.left - body.scrollLeft, top: touch.clientY - rect.top - body.scrollTop}; + }, + _handleTextAreaClick: function (e) { + var pt = this._touchConvert(e); + this._clickCount = 1; + this._ignoreDOMSelection = false; + this._setSelectionTo(pt.left, pt.top, false); + var textArea = this._textArea; + textArea.focus(); + }, + _handleTouchStart: function (e) { + var touches = e.touches, touch, pt, sel; + this._touchMoved = false; + this._touchStartScroll = undefined; + if (touches.length === 1) { + touch = touches[0]; + var pageX = touch.pageX; + var pageY = touch.pageY; + this._touchStartX = pageX; + this._touchStartY = pageY; + this._touchStartTime = e.timeStamp; + this._touchStartScroll = this._getScroll(); + sel = this._getSelection(); + pt = this._touchConvert(touches[0]); + this._touchGesture = "none"; + if (!sel.isEmpty()) { + if (this._hitOffset(sel.end, pt.left, pt.top)) { + this._touchGesture = "extendEnd"; + } else if (this._hitOffset(sel.start, pt.left, pt.top)) { + this._touchGesture = "extendStart"; + } + } + if (this._touchGesture === "none") { + var textArea = this._textArea; + textArea.value = ""; + textArea.style.left = "-1000px"; + textArea.style.top = "-1000px"; + textArea.style.width = "3000px"; + textArea.style.height = "3000px"; + } + } else if (touches.length === 2) { + this._touchGesture = "select"; + if (this._touchTimeout) { + clearTimeout(this._touchTimeout); + this._touchTimeout = null; + } + pt = this._touchConvert(touches[0]); + var offset1 = this._getXToOffset(this._getYToLine(pt.top), pt.left); + pt = this._touchConvert(touches[1]); + var offset2 = this._getXToOffset(this._getYToLine(pt.top), pt.left); + sel = this._getSelection(); + sel.setCaret(offset1); + sel.extend(offset2); + this._setSelection(sel, true, true); + } + //Cannot prevent to show magnifier +// e.preventDefault(); + }, + _handleTouchMove: function (e) { + this._touchMoved = true; + var touches = e.touches, pt, sel; + if (touches.length === 1) { + var touch = touches[0]; + var pageX = touch.pageX; + var pageY = touch.pageY; + var deltaX = this._touchStartX - pageX; + var deltaY = this._touchStartY - pageY; + pt = this._touchConvert(touch); + sel = this._getSelection(); + if (this._touchGesture === "none") { + if ((e.timeStamp - this._touchStartTime) < 200 && (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5)) { + this._touchGesture = "scroll"; + } else { + this._touchGesture = "caret"; + } + } + if (this._touchGesture === "select") { + if (this._hitOffset(sel.end, pt.left, pt.top)) { + this._touchGesture = "extendEnd"; + } else if (this._hitOffset(sel.start, pt.left, pt.top)) { + this._touchGesture = "extendStart"; + } else { + this._touchGesture = "caret"; + } + } + switch (this._touchGesture) { + case "scroll": + this._touchStartX = pageX; + this._touchStartY = pageY; + this._scrollView(deltaX, deltaY); + break; + case "extendStart": + case "extendEnd": + this._clickCount = 1; + var lineIndex = this._getYToLine(pt.top); + var offset = this._getXToOffset(lineIndex, pt.left); + sel.setCaret(this._touchGesture === "extendStart" ? sel.end : sel.start); + sel.extend(offset); + if (offset >= sel.end && this._touchGesture === "extendStart") { + this._touchGesture = "extendEnd"; + } + if (offset <= sel.start && this._touchGesture === "extendEnd") { + this._touchGesture = "extendStart"; + } + this._setSelection(sel, true, true); + break; + case "caret": + this._setSelectionTo(pt.left, pt.top, false); + break; + } + } else if (touches.length === 2) { + pt = this._touchConvert(touches[0]); + var offset1 = this._getXToOffset(this._getYToLine(pt.top), pt.left); + pt = this._touchConvert(touches[1]); + var offset2 = this._getXToOffset(this._getYToLine(pt.top), pt.left); + sel = this._getSelection(); + sel.setCaret(offset1); + sel.extend(offset2); + this._setSelection(sel, true, true); + } + e.preventDefault(); + }, + _handleTouchEnd: function (e) { + var self = this; + if (!this._touchMoved) { + if (e.touches.length === 0 && e.changedTouches.length === 1) { + var touch = e.changedTouches[0]; + var pt = this._touchConvert(touch); + var textArea = this._textArea; + textArea.value = ""; + textArea.style.left = "-1000px"; + textArea.style.top = "-1000px"; + textArea.style.width = "3000px"; + textArea.style.height = "3000px"; + setTimeout(function() { + self._clickCount = 1; + self._ignoreDOMSelection = false; + self._setSelectionTo(pt.left, pt.top, false); + }, 300); + } + } + if (e.touches.length === 0) { + setTimeout(function() { + var selection = self._getSelection(); + var text = self._model.getText(selection.start, selection.end); + var textArea = self._textArea; + textArea.value = text; + textArea.selectionStart = 0; + textArea.selectionEnd = text.length; + if (!selection.isEmpty()) { + var touchRect = self._touchDiv.getBoundingClientRect(); + var bounds = self._getOffsetBounds(selection.start); + textArea.style.left = (touchRect.width / 2) + "px"; + textArea.style.top = ((bounds.top > 40 ? bounds.top - 30 : bounds.top + 30)) + "px"; + } + }, 0); + } +// e.preventDefault(); + }, + + /************************************ Actions ******************************************/ + _doAction: function (e) { + var keyBindings = this._keyBindings; + for (var i = 0; i < keyBindings.length; i++) { + var kb = keyBindings[i]; + if (kb.keyBinding.match(e)) { + if (kb.name) { + var actions = this._actions; + for (var j = 0; j < actions.length; j++) { + var a = actions[j]; + if (a.name === kb.name) { + if (a.userHandler) { + if (!a.userHandler()) { + if (a.defaultHandler) { + a.defaultHandler(); + } else { + return false; + } + } + } else if (a.defaultHandler) { + a.defaultHandler(); + } + break; + } + } + } + return true; + } + } + return false; + }, + _doBackspace: function (args) { + var selection = this._getSelection(); + if (selection.isEmpty()) { + var model = this._model; + var caret = selection.getCaret(); + var lineIndex = model.getLineAtOffset(caret); + var lineStart = model.getLineStart(lineIndex); + if (caret === lineStart) { + if (lineIndex > 0) { + selection.extend(model.getLineEnd(lineIndex - 1)); + } + } else { + var removeTab = false; + if (this._expandTab && args.unit === "character" && (caret - lineStart) % this._tabSize === 0) { + var lineText = model.getText(lineStart, caret); + removeTab = !/[^ ]/.test(lineText); // Only spaces between line start and caret. + } + if (removeTab) { + selection.extend(caret - this._tabSize); + } else { + selection.extend(this._getOffset(caret, args.unit, -1)); + } + } + } + this._modifyContent({text: "", start: selection.start, end: selection.end}, true); + return true; + }, + _doContent: function (text) { + var selection = this._getSelection(); + this._modifyContent({text: text, start: selection.start, end: selection.end, _ignoreDOMSelection: true}, true); + }, + _doCopy: function (e) { + var selection = this._getSelection(); + if (!selection.isEmpty()) { + var text = this._getBaseText(selection.start, selection.end); + return this._setClipboardText(text, e); + } + return true; + }, + _doCursorNext: function (args) { + if (!args.select) { + if (this._clearSelection("next")) { return true; } + } + var model = this._model; + var selection = this._getSelection(); + var caret = selection.getCaret(); + var lineIndex = model.getLineAtOffset(caret); + if (caret === model.getLineEnd(lineIndex)) { + if (lineIndex + 1 < model.getLineCount()) { + selection.extend(model.getLineStart(lineIndex + 1)); + } + } else { + selection.extend(this._getOffset(caret, args.unit, 1)); + } + if (!args.select) { selection.collapse(); } + this._setSelection(selection, true); + return true; + }, + _doCursorPrevious: function (args) { + if (!args.select) { + if (this._clearSelection("previous")) { return true; } + } + var model = this._model; + var selection = this._getSelection(); + var caret = selection.getCaret(); + var lineIndex = model.getLineAtOffset(caret); + if (caret === model.getLineStart(lineIndex)) { + if (lineIndex > 0) { + selection.extend(model.getLineEnd(lineIndex - 1)); + } + } else { + selection.extend(this._getOffset(caret, args.unit, -1)); + } + if (!args.select) { selection.collapse(); } + this._setSelection(selection, true); + return true; + }, + _doCut: function (e) { + var selection = this._getSelection(); + if (!selection.isEmpty()) { + var text = this._getBaseText(selection.start, selection.end); + this._doContent(""); + return this._setClipboardText(text, e); + } + return true; + }, + _doDelete: function (args) { + var selection = this._getSelection(); + if (selection.isEmpty()) { + var model = this._model; + var caret = selection.getCaret(); + var lineIndex = model.getLineAtOffset(caret); + if (caret === model.getLineEnd (lineIndex)) { + if (lineIndex + 1 < model.getLineCount()) { + selection.extend(model.getLineStart(lineIndex + 1)); + } + } else { + selection.extend(this._getOffset(caret, args.unit, 1)); + } + } + this._modifyContent({text: "", start: selection.start, end: selection.end}, true); + return true; + }, + _doEnd: function (args) { + var selection = this._getSelection(); + var model = this._model; + if (args.ctrl) { + selection.extend(model.getCharCount()); + } else { + var lineIndex = model.getLineAtOffset(selection.getCaret()); + selection.extend(model.getLineEnd(lineIndex)); + } + if (!args.select) { selection.collapse(); } + this._setSelection(selection, true); + return true; + }, + _doEnter: function (args) { + var model = this._model; + var selection = this._getSelection(); + this._doContent(model.getLineDelimiter()); + if (args && args.noCursor) { + selection.end = selection.start; + this._setSelection(selection); + } + return true; + }, + _doHome: function (args) { + var selection = this._getSelection(); + var model = this._model; + if (args.ctrl) { + selection.extend(0); + } else { + var lineIndex = model.getLineAtOffset(selection.getCaret()); + selection.extend(model.getLineStart(lineIndex)); + } + if (!args.select) { selection.collapse(); } + this._setSelection(selection, true); + return true; + }, + _doLineDown: function (args) { + var model = this._model; + var selection = this._getSelection(); + var caret = selection.getCaret(); + var lineIndex = model.getLineAtOffset(caret); + if (lineIndex + 1 < model.getLineCount()) { + var scrollX = this._getScroll().x; + var x = this._columnX; + if (x === -1 || args.wholeLine || (args.select && isIE)) { + var offset = args.wholeLine ? model.getLineEnd(lineIndex + 1) : caret; + x = this._getOffsetToX(offset) + scrollX; + } + selection.extend(this._getXToOffset(lineIndex + 1, x - scrollX)); + if (!args.select) { selection.collapse(); } + this._setSelection(selection, true, true); + this._columnX = x; + } + return true; + }, + _doLineUp: function (args) { + var model = this._model; + var selection = this._getSelection(); + var caret = selection.getCaret(); + var lineIndex = model.getLineAtOffset(caret); + if (lineIndex > 0) { + var scrollX = this._getScroll().x; + var x = this._columnX; + if (x === -1 || args.wholeLine || (args.select && isIE)) { + var offset = args.wholeLine ? model.getLineStart(lineIndex - 1) : caret; + x = this._getOffsetToX(offset) + scrollX; + } + selection.extend(this._getXToOffset(lineIndex - 1, x - scrollX)); + if (!args.select) { selection.collapse(); } + this._setSelection(selection, true, true); + this._columnX = x; + } + return true; + }, + _doPageDown: function (args) { + var model = this._model; + var selection = this._getSelection(); + var caret = selection.getCaret(); + var caretLine = model.getLineAtOffset(caret); + var lineCount = model.getLineCount(); + if (caretLine < lineCount - 1) { + var scroll = this._getScroll(); + var clientHeight = this._getClientHeight(); + var lineHeight = this._getLineHeight(); + var lines = Math.floor(clientHeight / lineHeight); + var scrollLines = Math.min(lineCount - caretLine - 1, lines); + scrollLines = Math.max(1, scrollLines); + var x = this._columnX; + if (x === -1 || (args.select && isIE)) { + x = this._getOffsetToX(caret) + scroll.x; + } + selection.extend(this._getXToOffset(caretLine + scrollLines, x - scroll.x)); + if (!args.select) { selection.collapse(); } + var verticalMaximum = lineCount * lineHeight; + var scrollOffset = scroll.y + scrollLines * lineHeight; + if (scrollOffset + clientHeight > verticalMaximum) { + scrollOffset = verticalMaximum - clientHeight; + } + this._setSelection(selection, true, true, scrollOffset - scroll.y); + this._columnX = x; + } + return true; + }, + _doPageUp: function (args) { + var model = this._model; + var selection = this._getSelection(); + var caret = selection.getCaret(); + var caretLine = model.getLineAtOffset(caret); + if (caretLine > 0) { + var scroll = this._getScroll(); + var clientHeight = this._getClientHeight(); + var lineHeight = this._getLineHeight(); + var lines = Math.floor(clientHeight / lineHeight); + var scrollLines = Math.max(1, Math.min(caretLine, lines)); + var x = this._columnX; + if (x === -1 || (args.select && isIE)) { + x = this._getOffsetToX(caret) + scroll.x; + } + selection.extend(this._getXToOffset(caretLine - scrollLines, x - scroll.x)); + if (!args.select) { selection.collapse(); } + var scrollOffset = Math.max(0, scroll.y - scrollLines * lineHeight); + this._setSelection(selection, true, true, scrollOffset - scroll.y); + this._columnX = x; + } + return true; + }, + _doPaste: function(e) { + var self = this; + var result = this._getClipboardText(e, function(text) { + if (text) { + if (isLinux && self._lastMouseButton === 2) { + var timeDiff = new Date().getTime() - self._lastMouseTime; + if (timeDiff <= self._clickTime) { + self._setSelectionTo(self._lastMouseX, self._lastMouseY); + } + } + self._doContent(text); + } + }); + return result !== null; + }, + _doScroll: function (args) { + var type = args.type; + var model = this._model; + var lineCount = model.getLineCount(); + var clientHeight = this._getClientHeight(); + var lineHeight = this._getLineHeight(); + var verticalMaximum = lineCount * lineHeight; + var verticalScrollOffset = this._getScroll().y; + var pixel; + switch (type) { + case "textStart": pixel = 0; break; + case "textEnd": pixel = verticalMaximum - clientHeight; break; + case "pageDown": pixel = verticalScrollOffset + clientHeight; break; + case "pageUp": pixel = verticalScrollOffset - clientHeight; break; + case "centerLine": + var selection = this._getSelection(); + var lineStart = model.getLineAtOffset(selection.start); + var lineEnd = model.getLineAtOffset(selection.end); + var selectionHeight = (lineEnd - lineStart + 1) * lineHeight; + pixel = (lineStart * lineHeight) - (clientHeight / 2) + (selectionHeight / 2); + break; + } + if (pixel !== undefined) { + pixel = Math.min(Math.max(0, pixel), verticalMaximum - clientHeight); + this._scrollView(0, pixel - verticalScrollOffset); + } + }, + _doSelectAll: function (args) { + var model = this._model; + var selection = this._getSelection(); + selection.setCaret(0); + selection.extend(model.getCharCount()); + this._setSelection(selection, false); + return true; + }, + _doTab: function (args) { + var text = "\t"; + if (this._expandTab) { + var model = this._model; + var caret = this._getSelection().getCaret(); + var lineIndex = model.getLineAtOffset(caret); + var lineStart = model.getLineStart(lineIndex); + var spaces = this._tabSize - ((caret - lineStart) % this._tabSize); + text = (new Array(spaces + 1)).join(" "); + } + this._doContent(text); + return true; + }, + + /************************************ Internals ******************************************/ + _applyStyle: function(style, node, reset) { + if (reset) { + var attrs = node.attributes; + for (var i= attrs.length; i-->0;) { + if (attrs[i].specified) { + node.removeAttributeNode(attrs[i]); + } + } + } + if (!style) { + return; + } + if (style.styleClass) { + node.className = style.styleClass; + } + var properties = style.style; + if (properties) { + for (var s in properties) { + if (properties.hasOwnProperty(s)) { + node.style[s] = properties[s]; + } + } + } + var attributes = style.attributes; + if (attributes) { + for (var a in attributes) { + if (attributes.hasOwnProperty(a)) { + node.setAttribute(a, attributes[a]); + } + } + } + }, + _autoScroll: function () { + var selection = this._getSelection(); + var line; + var x = this._autoScrollX; + if (this._autoScrollDir === "up" || this._autoScrollDir === "down") { + var scroll = this._autoScrollY / this._getLineHeight(); + scroll = scroll < 0 ? Math.floor(scroll) : Math.ceil(scroll); + line = this._model.getLineAtOffset(selection.getCaret()); + line = Math.max(0, Math.min(this._model.getLineCount() - 1, line + scroll)); + } else if (this._autoScrollDir === "left" || this._autoScrollDir === "right") { + line = this._getYToLine(this._autoScrollY); + x += this._getOffsetToX(selection.getCaret()); + } + selection.extend(this._getXToOffset(line, x)); + this._setSelection(selection, true); + }, + _autoScrollTimer: function () { + this._autoScroll(); + var self = this; + this._autoScrollTimerID = setTimeout(function () {self._autoScrollTimer();}, this._AUTO_SCROLL_RATE); + }, + _calculateLineHeight: function() { + var parent = this._clientDiv; + var document = this._frameDocument; + var c = " "; + var line = document.createElement("DIV"); + line.style.position = "fixed"; + line.style.left = "-1000px"; + var span1 = document.createElement("SPAN"); + span1.appendChild(document.createTextNode(c)); + line.appendChild(span1); + var span2 = document.createElement("SPAN"); + span2.style.fontStyle = "italic"; + span2.appendChild(document.createTextNode(c)); + line.appendChild(span2); + var span3 = document.createElement("SPAN"); + span3.style.fontWeight = "bold"; + span3.appendChild(document.createTextNode(c)); + line.appendChild(span3); + var span4 = document.createElement("SPAN"); + span4.style.fontWeight = "bold"; + span4.style.fontStyle = "italic"; + span4.appendChild(document.createTextNode(c)); + line.appendChild(span4); + parent.appendChild(line); + var lineRect = line.getBoundingClientRect(); + var spanRect1 = span1.getBoundingClientRect(); + var spanRect2 = span2.getBoundingClientRect(); + var spanRect3 = span3.getBoundingClientRect(); + var spanRect4 = span4.getBoundingClientRect(); + var h1 = spanRect1.bottom - spanRect1.top; + var h2 = spanRect2.bottom - spanRect2.top; + var h3 = spanRect3.bottom - spanRect3.top; + var h4 = spanRect4.bottom - spanRect4.top; + var fontStyle = 0; + var lineHeight = lineRect.bottom - lineRect.top; + if (h2 > h1) { + fontStyle = 1; + } + if (h3 > h2) { + fontStyle = 2; + } + if (h4 > h3) { + fontStyle = 3; + } + var style; + if (fontStyle !== 0) { + style = {style: {}}; + if ((fontStyle & 1) !== 0) { + style.style.fontStyle = "italic"; + } + if ((fontStyle & 2) !== 0) { + style.style.fontWeight = "bold"; + } + } + this._largestFontStyle = style; + parent.removeChild(line); + return lineHeight; + }, + _calculatePadding: function() { + var document = this._frameDocument; + var parent = this._clientDiv; + var pad = this._getPadding(this._viewDiv); + var div1 = document.createElement("DIV"); + div1.style.position = "fixed"; + div1.style.left = "-1000px"; + div1.style.paddingLeft = pad.left + "px"; + div1.style.paddingTop = pad.top + "px"; + div1.style.paddingRight = pad.right + "px"; + div1.style.paddingBottom = pad.bottom + "px"; + div1.style.width = "100px"; + div1.style.height = "100px"; + var div2 = document.createElement("DIV"); + div2.style.width = "100%"; + div2.style.height = "100%"; + div1.appendChild(div2); + parent.appendChild(div1); + var rect1 = div1.getBoundingClientRect(); + var rect2 = div2.getBoundingClientRect(); + parent.removeChild(div1); + pad = { + left: rect2.left - rect1.left, + top: rect2.top - rect1.top, + right: rect1.right - rect2.right, + bottom: rect1.bottom - rect2.bottom + }; + return pad; + }, + _clearSelection: function (direction) { + var selection = this._getSelection(); + if (selection.isEmpty()) { return false; } + if (direction === "next") { + selection.start = selection.end; + } else { + selection.end = selection.start; + } + this._setSelection(selection, true); + return true; + }, + _clone: function (obj) { + /*Note that this code only works because of the limited types used in TextViewOptions */ + if (obj instanceof Array) { + return obj.slice(0); + } + return obj; + }, + _compare: function (s1, s2) { + if (s1 === s2) { return true; } + if (s1 && !s2 || !s1 && s2) { return false; } + if ((s1 && s1.constructor === String) || (s2 && s2.constructor === String)) { return false; } + if (s1 instanceof Array || s2 instanceof Array) { + if (!(s1 instanceof Array && s2 instanceof Array)) { return false; } + if (s1.length !== s2.length) { return false; } + for (var i = 0; i < s1.length; i++) { + if (!this._compare(s1[i], s2[i])) { + return false; + } + } + return true; + } + if (!(s1 instanceof Object) || !(s2 instanceof Object)) { return false; } + var p; + for (p in s1) { + if (s1.hasOwnProperty(p)) { + if (!s2.hasOwnProperty(p)) { return false; } + if (!this._compare(s1[p], s2[p])) {return false; } + } + } + for (p in s2) { + if (!s1.hasOwnProperty(p)) { return false; } + } + return true; + }, + _commitIME: function () { + if (this._imeOffset === -1) { return; } + // make the state of the IME match the state the view expects it be in + // when the view commits the text and IME also need to be committed + // this can be accomplished by changing the focus around + this._scrollDiv.focus(); + this._clientDiv.focus(); + + var model = this._model; + var lineIndex = model.getLineAtOffset(this._imeOffset); + var lineStart = model.getLineStart(lineIndex); + var newText = this._getDOMText(lineIndex); + var oldText = model.getLine(lineIndex); + var start = this._imeOffset - lineStart; + var end = start + newText.length - oldText.length; + if (start !== end) { + var insertText = newText.substring(start, end); + this._doContent(insertText); + } + this._imeOffset = -1; + }, + _convertDelimiter: function (text, addTextFunc, addDelimiterFunc) { + var cr = 0, lf = 0, index = 0, length = text.length; + while (index < length) { + if (cr !== -1 && cr <= index) { cr = text.indexOf("\r", index); } + if (lf !== -1 && lf <= index) { lf = text.indexOf("\n", index); } + var start = index, end; + if (lf === -1 && cr === -1) { + addTextFunc(text.substring(index)); + break; + } + if (cr !== -1 && lf !== -1) { + if (cr + 1 === lf) { + end = cr; + index = lf + 1; + } else { + end = cr < lf ? cr : lf; + index = (cr < lf ? cr : lf) + 1; + } + } else if (cr !== -1) { + end = cr; + index = cr + 1; + } else { + end = lf; + index = lf + 1; + } + addTextFunc(text.substring(start, end)); + addDelimiterFunc(); + } + }, + _createActions: function () { + var KeyBinding = mKeyBinding.KeyBinding; + //no duplicate keybindings + var bindings = this._keyBindings = []; + + // Cursor Navigation + bindings.push({name: "lineUp", keyBinding: new KeyBinding(38), predefined: true}); + bindings.push({name: "lineDown", keyBinding: new KeyBinding(40), predefined: true}); + bindings.push({name: "charPrevious", keyBinding: new KeyBinding(37), predefined: true}); + bindings.push({name: "charNext", keyBinding: new KeyBinding(39), predefined: true}); + if (isMac) { + bindings.push({name: "scrollPageUp", keyBinding: new KeyBinding(33), predefined: true}); + bindings.push({name: "scrollPageDown", keyBinding: new KeyBinding(34), predefined: true}); + bindings.push({name: "pageUp", keyBinding: new KeyBinding(33, null, null, true), predefined: true}); + bindings.push({name: "pageDown", keyBinding: new KeyBinding(34, null, null, true), predefined: true}); + bindings.push({name: "lineStart", keyBinding: new KeyBinding(37, true), predefined: true}); + bindings.push({name: "lineEnd", keyBinding: new KeyBinding(39, true), predefined: true}); + bindings.push({name: "wordPrevious", keyBinding: new KeyBinding(37, null, null, true), predefined: true}); + bindings.push({name: "wordNext", keyBinding: new KeyBinding(39, null, null, true), predefined: true}); + bindings.push({name: "scrollTextStart", keyBinding: new KeyBinding(36), predefined: true}); + bindings.push({name: "scrollTextEnd", keyBinding: new KeyBinding(35), predefined: true}); + bindings.push({name: "textStart", keyBinding: new KeyBinding(38, true), predefined: true}); + bindings.push({name: "textEnd", keyBinding: new KeyBinding(40, true), predefined: true}); + bindings.push({name: "scrollPageUp", keyBinding: new KeyBinding(38, null, null, null, true), predefined: true}); + bindings.push({name: "scrollPageDown", keyBinding: new KeyBinding(40, null, null, null, true), predefined: true}); + bindings.push({name: "lineStart", keyBinding: new KeyBinding(37, null, null, null, true), predefined: true}); + bindings.push({name: "lineEnd", keyBinding: new KeyBinding(39, null, null, null, true), predefined: true}); + //TODO These two actions should be changed to paragraph start and paragraph end when word wrap is implemented + bindings.push({name: "lineStart", keyBinding: new KeyBinding(38, null, null, true), predefined: true}); + bindings.push({name: "lineEnd", keyBinding: new KeyBinding(40, null, null, true), predefined: true}); + } else { + bindings.push({name: "pageUp", keyBinding: new KeyBinding(33), predefined: true}); + bindings.push({name: "pageDown", keyBinding: new KeyBinding(34), predefined: true}); + bindings.push({name: "lineStart", keyBinding: new KeyBinding(36), predefined: true}); + bindings.push({name: "lineEnd", keyBinding: new KeyBinding(35), predefined: true}); + bindings.push({name: "wordPrevious", keyBinding: new KeyBinding(37, true), predefined: true}); + bindings.push({name: "wordNext", keyBinding: new KeyBinding(39, true), predefined: true}); + bindings.push({name: "textStart", keyBinding: new KeyBinding(36, true), predefined: true}); + bindings.push({name: "textEnd", keyBinding: new KeyBinding(35, true), predefined: true}); + } + if (isFirefox && isLinux) { + bindings.push({name: "lineUp", keyBinding: new KeyBinding(38, true), predefined: true}); + bindings.push({name: "lineDown", keyBinding: new KeyBinding(40, true), predefined: true}); + } + + // Select Cursor Navigation + bindings.push({name: "selectLineUp", keyBinding: new KeyBinding(38, null, true), predefined: true}); + bindings.push({name: "selectLineDown", keyBinding: new KeyBinding(40, null, true), predefined: true}); + bindings.push({name: "selectCharPrevious", keyBinding: new KeyBinding(37, null, true), predefined: true}); + bindings.push({name: "selectCharNext", keyBinding: new KeyBinding(39, null, true), predefined: true}); + bindings.push({name: "selectPageUp", keyBinding: new KeyBinding(33, null, true), predefined: true}); + bindings.push({name: "selectPageDown", keyBinding: new KeyBinding(34, null, true), predefined: true}); + if (isMac) { + bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(37, true, true), predefined: true}); + bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(39, true, true), predefined: true}); + bindings.push({name: "selectWordPrevious", keyBinding: new KeyBinding(37, null, true, true), predefined: true}); + bindings.push({name: "selectWordNext", keyBinding: new KeyBinding(39, null, true, true), predefined: true}); + bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(36, null, true), predefined: true}); + bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(35, null, true), predefined: true}); + bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(38, true, true), predefined: true}); + bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(40, true, true), predefined: true}); + bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(37, null, true, null, true), predefined: true}); + bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(39, null, true, null, true), predefined: true}); + //TODO These two actions should be changed to select paragraph start and select paragraph end when word wrap is implemented + bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(38, null, true, true), predefined: true}); + bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(40, null, true, true), predefined: true}); + } else { + if (isLinux) { + bindings.push({name: "selectWholeLineUp", keyBinding: new KeyBinding(38, true, true), predefined: true}); + bindings.push({name: "selectWholeLineDown", keyBinding: new KeyBinding(40, true, true), predefined: true}); + } + bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(36, null, true), predefined: true}); + bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(35, null, true), predefined: true}); + bindings.push({name: "selectWordPrevious", keyBinding: new KeyBinding(37, true, true), predefined: true}); + bindings.push({name: "selectWordNext", keyBinding: new KeyBinding(39, true, true), predefined: true}); + bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(36, true, true), predefined: true}); + bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(35, true, true), predefined: true}); + } + + //Misc + bindings.push({name: "deletePrevious", keyBinding: new KeyBinding(8), predefined: true}); + bindings.push({name: "deletePrevious", keyBinding: new KeyBinding(8, null, true), predefined: true}); + bindings.push({name: "deleteNext", keyBinding: new KeyBinding(46), predefined: true}); + bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, true), predefined: true}); + bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, true, true), predefined: true}); + bindings.push({name: "deleteWordNext", keyBinding: new KeyBinding(46, true), predefined: true}); + bindings.push({name: "tab", keyBinding: new KeyBinding(9), predefined: true}); + bindings.push({name: "enter", keyBinding: new KeyBinding(13), predefined: true}); + bindings.push({name: "enter", keyBinding: new KeyBinding(13, null, true), predefined: true}); + bindings.push({name: "selectAll", keyBinding: new KeyBinding('a', true), predefined: true}); + if (isMac) { + bindings.push({name: "deleteNext", keyBinding: new KeyBinding(46, null, true), predefined: true}); + bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, null, null, true), predefined: true}); + bindings.push({name: "deleteWordNext", keyBinding: new KeyBinding(46, null, null, true), predefined: true}); + } + + /* + * Feature in IE/Chrome: prevent ctrl+'u', ctrl+'i', and ctrl+'b' from applying styles to the text. + * + * Note that Chrome applies the styles on the Mac with Ctrl instead of Cmd. + */ + if (!isFirefox) { + var isMacChrome = isMac && isChrome; + bindings.push({name: null, keyBinding: new KeyBinding('u', !isMacChrome, false, false, isMacChrome), predefined: true}); + bindings.push({name: null, keyBinding: new KeyBinding('i', !isMacChrome, false, false, isMacChrome), predefined: true}); + bindings.push({name: null, keyBinding: new KeyBinding('b', !isMacChrome, false, false, isMacChrome), predefined: true}); + } + + if (isFirefox) { + bindings.push({name: "copy", keyBinding: new KeyBinding(45, true), predefined: true}); + bindings.push({name: "paste", keyBinding: new KeyBinding(45, null, true), predefined: true}); + bindings.push({name: "cut", keyBinding: new KeyBinding(46, null, true), predefined: true}); + } + + // Add the emacs Control+ ... key bindings. + if (isMac) { + bindings.push({name: "lineStart", keyBinding: new KeyBinding("a", false, false, false, true), predefined: true}); + bindings.push({name: "lineEnd", keyBinding: new KeyBinding("e", false, false, false, true), predefined: true}); + bindings.push({name: "lineUp", keyBinding: new KeyBinding("p", false, false, false, true), predefined: true}); + bindings.push({name: "lineDown", keyBinding: new KeyBinding("n", false, false, false, true), predefined: true}); + bindings.push({name: "charPrevious", keyBinding: new KeyBinding("b", false, false, false, true), predefined: true}); + bindings.push({name: "charNext", keyBinding: new KeyBinding("f", false, false, false, true), predefined: true}); + bindings.push({name: "deletePrevious", keyBinding: new KeyBinding("h", false, false, false, true), predefined: true}); + bindings.push({name: "deleteNext", keyBinding: new KeyBinding("d", false, false, false, true), predefined: true}); + bindings.push({name: "deleteLineEnd", keyBinding: new KeyBinding("k", false, false, false, true), predefined: true}); + if (isFirefox) { + bindings.push({name: "scrollPageDown", keyBinding: new KeyBinding("v", false, false, false, true), predefined: true}); + bindings.push({name: "deleteLineStart", keyBinding: new KeyBinding("u", false, false, false, true), predefined: true}); + bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding("w", false, false, false, true), predefined: true}); + } else { + bindings.push({name: "pageDown", keyBinding: new KeyBinding("v", false, false, false, true), predefined: true}); + bindings.push({name: "centerLine", keyBinding: new KeyBinding("l", false, false, false, true), predefined: true}); + bindings.push({name: "enterNoCursor", keyBinding: new KeyBinding("o", false, false, false, true), predefined: true}); + //TODO implement: y (yank), t (transpose) + } + } + + //1 to 1, no duplicates + var self = this; + this._actions = [ + {name: "lineUp", defaultHandler: function() {return self._doLineUp({select: false});}}, + {name: "lineDown", defaultHandler: function() {return self._doLineDown({select: false});}}, + {name: "lineStart", defaultHandler: function() {return self._doHome({select: false, ctrl:false});}}, + {name: "lineEnd", defaultHandler: function() {return self._doEnd({select: false, ctrl:false});}}, + {name: "charPrevious", defaultHandler: function() {return self._doCursorPrevious({select: false, unit:"character"});}}, + {name: "charNext", defaultHandler: function() {return self._doCursorNext({select: false, unit:"character"});}}, + {name: "pageUp", defaultHandler: function() {return self._doPageUp({select: false});}}, + {name: "pageDown", defaultHandler: function() {return self._doPageDown({select: false});}}, + {name: "scrollPageUp", defaultHandler: function() {return self._doScroll({type: "pageUp"});}}, + {name: "scrollPageDown", defaultHandler: function() {return self._doScroll({type: "pageDown"});}}, + {name: "wordPrevious", defaultHandler: function() {return self._doCursorPrevious({select: false, unit:"word"});}}, + {name: "wordNext", defaultHandler: function() {return self._doCursorNext({select: false, unit:"word"});}}, + {name: "textStart", defaultHandler: function() {return self._doHome({select: false, ctrl:true});}}, + {name: "textEnd", defaultHandler: function() {return self._doEnd({select: false, ctrl:true});}}, + {name: "scrollTextStart", defaultHandler: function() {return self._doScroll({type: "textStart"});}}, + {name: "scrollTextEnd", defaultHandler: function() {return self._doScroll({type: "textEnd"});}}, + {name: "centerLine", defaultHandler: function() {return self._doScroll({type: "centerLine"});}}, + + {name: "selectLineUp", defaultHandler: function() {return self._doLineUp({select: true});}}, + {name: "selectLineDown", defaultHandler: function() {return self._doLineDown({select: true});}}, + {name: "selectWholeLineUp", defaultHandler: function() {return self._doLineUp({select: true, wholeLine: true});}}, + {name: "selectWholeLineDown", defaultHandler: function() {return self._doLineDown({select: true, wholeLine: true});}}, + {name: "selectLineStart", defaultHandler: function() {return self._doHome({select: true, ctrl:false});}}, + {name: "selectLineEnd", defaultHandler: function() {return self._doEnd({select: true, ctrl:false});}}, + {name: "selectCharPrevious", defaultHandler: function() {return self._doCursorPrevious({select: true, unit:"character"});}}, + {name: "selectCharNext", defaultHandler: function() {return self._doCursorNext({select: true, unit:"character"});}}, + {name: "selectPageUp", defaultHandler: function() {return self._doPageUp({select: true});}}, + {name: "selectPageDown", defaultHandler: function() {return self._doPageDown({select: true});}}, + {name: "selectWordPrevious", defaultHandler: function() {return self._doCursorPrevious({select: true, unit:"word"});}}, + {name: "selectWordNext", defaultHandler: function() {return self._doCursorNext({select: true, unit:"word"});}}, + {name: "selectTextStart", defaultHandler: function() {return self._doHome({select: true, ctrl:true});}}, + {name: "selectTextEnd", defaultHandler: function() {return self._doEnd({select: true, ctrl:true});}}, + + {name: "deletePrevious", defaultHandler: function() {return self._doBackspace({unit:"character"});}}, + {name: "deleteNext", defaultHandler: function() {return self._doDelete({unit:"character"});}}, + {name: "deleteWordPrevious", defaultHandler: function() {return self._doBackspace({unit:"word"});}}, + {name: "deleteWordNext", defaultHandler: function() {return self._doDelete({unit:"word"});}}, + {name: "deleteLineStart", defaultHandler: function() {return self._doBackspace({unit: "line"});}}, + {name: "deleteLineEnd", defaultHandler: function() {return self._doDelete({unit: "line"});}}, + {name: "tab", defaultHandler: function() {return self._doTab();}}, + {name: "enter", defaultHandler: function() {return self._doEnter();}}, + {name: "enterNoCursor", defaultHandler: function() {return self._doEnter({noCursor:true});}}, + {name: "selectAll", defaultHandler: function() {return self._doSelectAll();}}, + {name: "copy", defaultHandler: function() {return self._doCopy();}}, + {name: "cut", defaultHandler: function() {return self._doCut();}}, + {name: "paste", defaultHandler: function() {return self._doPaste();}} + ]; + }, + _createLine: function(parent, div, document, lineIndex, model) { + var lineText = model.getLine(lineIndex); + var lineStart = model.getLineStart(lineIndex); + var e = {type:"LineStyle", textView: this, lineIndex: lineIndex, lineText: lineText, lineStart: lineStart}; + this.onLineStyle(e); + var lineDiv = div || document.createElement("DIV"); + if (!div || !this._compare(div.viewStyle, e.style)) { + this._applyStyle(e.style, lineDiv, div); + lineDiv.viewStyle = e.style; + } + lineDiv.lineIndex = lineIndex; + var ranges = []; + var data = {tabOffset: 0, ranges: ranges}; + this._createRanges(e.ranges, lineText, 0, lineText.length, lineStart, data); + + /* + * A trailing span with a whitespace is added for three different reasons: + * 1. Make sure the height of each line is the largest of the default font + * in normal, italic, bold, and italic-bold. + * 2. When full selection is off, Firefox, Opera and IE9 do not extend the + * selection at the end of the line when the line is fully selected. + * 3. The height of a div with only an empty span is zero. + */ + var c = " "; + if (!this._fullSelection && isIE < 9) { + /* + * IE8 already selects extra space at end of a line fully selected, + * adding another space at the end of the line causes the selection + * to look too big. The fix is to use a zero-width space (\uFEFF) instead. + */ + c = "\uFEFF"; + } + if (isWebkit) { + /* + * Feature in WekKit. Adding a regular white space to the line will + * cause the longest line in the view to wrap even though "pre" is set. + * The fix is to use the zero-width non-joiner character (\u200C) instead. + * Note: To not use \uFEFF because in old version of Chrome this character + * shows a glyph; + */ + c = "\u200C"; + } + ranges.push({text: c, style: this._largestFontStyle, ignoreChars: 1}); + + var range, span, style, oldSpan, oldStyle, text, oldText, end = 0, oldEnd = 0, next; + var changeCount, changeStart; + if (div) { + var modelChangedEvent = div.modelChangedEvent; + if (modelChangedEvent) { + if (modelChangedEvent.removedLineCount === 0 && modelChangedEvent.addedLineCount === 0) { + changeStart = modelChangedEvent.start - lineStart; + changeCount = modelChangedEvent.addedCharCount - modelChangedEvent.removedCharCount; + } else { + changeStart = -1; + } + div.modelChangedEvent = undefined; + } + oldSpan = div.firstChild; + } + for (var i = 0; i < ranges.length; i++) { + range = ranges[i]; + text = range.text; + end += text.length; + style = range.style; + if (oldSpan) { + oldText = oldSpan.firstChild.data; + oldStyle = oldSpan.viewStyle; + if (oldText === text && this._compare(style, oldStyle)) { + oldEnd += oldText.length; + oldSpan._rectsCache = undefined; + span = oldSpan = oldSpan.nextSibling; + continue; + } else { + while (oldSpan) { + if (changeStart !== -1) { + var spanEnd = end; + if (spanEnd >= changeStart) { + spanEnd -= changeCount; + } + var length = oldSpan.firstChild.data.length; + if (oldEnd + length > spanEnd) { break; } + oldEnd += length; + } + next = oldSpan.nextSibling; + lineDiv.removeChild(oldSpan); + oldSpan = next; + } + } + } + span = this._createSpan(lineDiv, document, text, style, range.ignoreChars); + if (oldSpan) { + lineDiv.insertBefore(span, oldSpan); + } else { + lineDiv.appendChild(span); + } + if (div) { + div.lineWidth = undefined; + } + } + if (div) { + var tmp = span ? span.nextSibling : null; + while (tmp) { + next = tmp.nextSibling; + div.removeChild(tmp); + tmp = next; + } + } else { + parent.appendChild(lineDiv); + } + return lineDiv; + }, + _createRanges: function(ranges, text, start, end, lineStart, data) { + if (start >= end) { return; } + if (ranges) { + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + if (range.end <= lineStart + start) { continue; } + var styleStart = Math.max(lineStart + start, range.start) - lineStart; + if (styleStart >= end) { break; } + var styleEnd = Math.min(lineStart + end, range.end) - lineStart; + if (styleStart < styleEnd) { + styleStart = Math.max(start, styleStart); + styleEnd = Math.min(end, styleEnd); + if (start < styleStart) { + this._createRange(text, start, styleStart, null, data); + } + while (i + 1 < ranges.length && ranges[i + 1].start - lineStart === styleEnd && this._compare(range.style, ranges[i + 1].style)) { + range = ranges[i + 1]; + styleEnd = Math.min(lineStart + end, range.end) - lineStart; + i++; + } + this._createRange(text, styleStart, styleEnd, range.style, data); + start = styleEnd; + } + } + } + if (start < end) { + this._createRange(text, start, end, null, data); + } + }, + _createRange: function(text, start, end, style, data) { + if (start >= end) { return; } + var tabSize = this._customTabSize, range; + if (tabSize && tabSize !== 8) { + var tabIndex = text.indexOf("\t", start); + while (tabIndex !== -1 && tabIndex < end) { + if (start < tabIndex) { + range = {text: text.substring(start, tabIndex), style: style}; + data.ranges.push(range); + data.tabOffset += range.text.length; + } + var spacesCount = tabSize - (data.tabOffset % tabSize); + if (spacesCount > 0) { + //TODO hack to preserve text length in getDOMText() + var spaces = "\u00A0"; + for (var i = 1; i < spacesCount; i++) { + spaces += " "; + } + range = {text: spaces, style: style, ignoreChars: spacesCount - 1}; + data.ranges.push(range); + data.tabOffset += range.text.length; + } + start = tabIndex + 1; + tabIndex = text.indexOf("\t", start); + } + } + if (start < end) { + range = {text: text.substring(start, end), style: style}; + data.ranges.push(range); + data.tabOffset += range.text.length; + } + }, + _createSpan: function(parent, document, text, style, ignoreChars) { + var isLink = style && style.tagName === "A"; + if (isLink) { parent.hasLink = true; } + var tagName = isLink && this._linksVisible ? "A" : "SPAN"; + var child = document.createElement(tagName); + child.appendChild(document.createTextNode(text)); + this._applyStyle(style, child); + if (tagName === "A") { + var self = this; + addHandler(child, "click", function(e) { return self._handleLinkClick(e); }, false); + } + child.viewStyle = style; + if (ignoreChars) { + child.ignoreChars = ignoreChars; + } + return child; + }, + _createRuler: function(ruler) { + if (!this._clientDiv) { return; } + var document = this._frameDocument; + var body = document.body; + var side = ruler.getLocation(); + var rulerParent = side === "left" ? this._leftDiv : this._rightDiv; + if (!rulerParent) { + rulerParent = document.createElement("DIV"); + rulerParent.style.overflow = "hidden"; + rulerParent.style.MozUserSelect = "none"; + rulerParent.style.WebkitUserSelect = "none"; + if (isIE) { + rulerParent.attachEvent("onselectstart", function() {return false;}); + } + rulerParent.style.position = "absolute"; + rulerParent.style.top = "0px"; + rulerParent.style.cursor = "default"; + body.appendChild(rulerParent); + if (side === "left") { + this._leftDiv = rulerParent; + rulerParent.className = "viewLeftRuler"; + } else { + this._rightDiv = rulerParent; + rulerParent.className = "viewRightRuler"; + } + var table = document.createElement("TABLE"); + rulerParent.appendChild(table); + table.cellPadding = "0px"; + table.cellSpacing = "0px"; + table.border = "0px"; + table.insertRow(0); + var self = this; + addHandler(rulerParent, "click", function(e) { self._handleRulerEvent(e); }); + addHandler(rulerParent, "dblclick", function(e) { self._handleRulerEvent(e); }); + addHandler(rulerParent, "mousemove", function(e) { self._handleRulerEvent(e); }); + addHandler(rulerParent, "mouseover", function(e) { self._handleRulerEvent(e); }); + addHandler(rulerParent, "mouseout", function(e) { self._handleRulerEvent(e); }); + } + var div = document.createElement("DIV"); + div._ruler = ruler; + div.rulerChanged = true; + div.style.position = "relative"; + var row = rulerParent.firstChild.rows[0]; + var index = row.cells.length; + var cell = row.insertCell(index); + cell.vAlign = "top"; + cell.appendChild(div); + }, + _createFrame: function() { + if (this.frame) { return; } + var parent = this._parent; + while (parent.hasChildNodes()) { parent.removeChild(parent.lastChild); } + var parentDocument = parent.ownerDocument; + this._parentDocument = parentDocument; + var frame = parentDocument.createElement("IFRAME"); + this._frame = frame; + frame.frameBorder = "0px";//for IE, needs to be set before the frame is added to the parent + frame.style.border = "0px"; + frame.style.width = "100%"; + frame.style.height = "100%"; + frame.scrolling = "no"; + var self = this; + /* + * Note that it is not possible to create the contents of the frame if the + * parent is not connected to the document. Only create it when the load + * event is trigged. + */ + this._loadHandler = function(e) { + self._handleLoad(e); + }; + addHandler(frame, "load", this._loadHandler, !!isFirefox); + if (!isWebkit) { + /* + * Feature in IE and Firefox. It is not possible to get the style of an + * element if it is not layed out because one of the ancestor has + * style.display = none. This means that the view cannot be created in this + * situations, since no measuring can be performed. The fix is to listen + * for DOMAttrModified and create or destroy the view when the style.display + * attribute changes. + */ + addHandler(parentDocument, "DOMAttrModified", this._attrModifiedHandler = function(e) { + self._handleDOMAttrModified(e); + }); + } + parent.appendChild(frame); + /* create synchronously if possible */ + if (this._sync) { + this._handleLoad(); + } + }, + _getFrameHTML: function() { + var html = []; + html.push(""); + html.push(""); + html.push(""); + if (isIE < 9) { + html.push(""); + } + html.push(""); + if (this._stylesheet) { + var stylesheet = typeof(this._stylesheet) === "string" ? [this._stylesheet] : this._stylesheet; + for (var i = 0; i < stylesheet.length; i++) { + var sheet = stylesheet[i]; + var isLink = this._isLinkURL(sheet); + if (isLink && this._sync) { + try { + var objXml = new XMLHttpRequest(); + if (objXml.overrideMimeType) { + objXml.overrideMimeType("text/css"); + } + objXml.open("GET", sheet, false); + objXml.send(null); + sheet = objXml.responseText; + isLink = false; + } catch (e) {} + } + if (isLink) { + html.push(""); + } else { + html.push(""); + } + } + } + /* + * Feature in WebKit. In WebKit, window load will not wait for the style sheets + * to be loaded unless there is script element after the style sheet link elements. + */ + html.push(""); + html.push(""); + html.push(""); + html.push(""); + return html.join(""); + }, + _createView: function() { + if (this._frameDocument) { return; } + var frameWindow = this._frameWindow = this._frame.contentWindow; + var frameDocument = this._frameDocument = frameWindow.document; + var self = this; + function write() { + frameDocument.open("text/html", "replace"); + frameDocument.write(self._getFrameHTML()); + frameDocument.close(); + self._windowLoadHandler = function(e) { + /* + * Bug in Safari. Safari sends the window load event before the + * style sheets are loaded. The fix is to defer creation of the + * contents until the document readyState changes to complete. + */ + if (self._isDocumentReady()) { + self._createContent(); + } + }; + addHandler(frameWindow, "load", self._windowLoadHandler); + } + write(); + if (this._sync) { + this._createContent(); + } else { + /* + * Bug in Webkit. Webkit does not send the load event for the iframe window when the main page + * loads as a result of backward or forward navigation. + * The fix is to use a timer to create the content only when the document is ready. + */ + this._createViewTimer = function() { + if (self._clientDiv) { return; } + if (self._isDocumentReady()) { + self._createContent(); + } else { + setTimeout(self._createViewTimer, 10); + } + }; + setTimeout(this._createViewTimer, 10); + } + }, + _isDocumentReady: function() { + var frameDocument = this._frameDocument; + if (!frameDocument) { return false; } + if (frameDocument.readyState === "complete") { + return true; + } else if (frameDocument.readyState === "interactive" && isFirefox) { + /* + * Bug in Firefox. Firefox does not change the document ready state to complete + * all the time. The fix is to wait for the ready state to be "interactive" and check that + * all css rules are initialized. + */ + var styleSheets = frameDocument.styleSheets; + var styleSheetCount = 1; + if (this._stylesheet) { + styleSheetCount += typeof(this._stylesheet) === "string" ? 1 : this._stylesheet.length; + } + if (styleSheetCount === styleSheets.length) { + var index = 0; + while (index < styleSheets.length) { + var count = 0; + try { + count = styleSheets.item(index).cssRules.length; + } catch (ex) { + /* + * Feature in Firefox. To determine if a stylesheet is loaded the number of css rules is used, if the + * stylesheet is not loaded this operation will throw an invalid access error. When a stylesheet from + * a different domain is loaded, accessing the css rules will result in a security exception. In this + * case count is set to 1 to indicate the stylesheet is loaded. + */ + if (ex.code !== DOMException.INVALID_ACCESS_ERR) { + count = 1; + } + } + if (count === 0) { break; } + index++; + } + return index === styleSheets.length; + } + } + return false; + }, + _createContent: function() { + if (this._clientDiv) { return; } + var parent = this._parent; + var parentDocument = this._parentDocument; + var frameDocument = this._frameDocument; + var body = frameDocument.body; + this._setThemeClass(this._themeClass, true); + body.style.margin = "0px"; + body.style.borderWidth = "0px"; + body.style.padding = "0px"; + + var textArea; + if (isPad) { + var touchDiv = parentDocument.createElement("DIV"); + this._touchDiv = touchDiv; + touchDiv.style.position = "absolute"; + touchDiv.style.border = "0px"; + touchDiv.style.padding = "0px"; + touchDiv.style.margin = "0px"; + touchDiv.style.zIndex = "2"; + touchDiv.style.overflow = "hidden"; + touchDiv.style.background="transparent"; + touchDiv.style.WebkitUserSelect = "none"; + parent.appendChild(touchDiv); + + textArea = parentDocument.createElement("TEXTAREA"); + this._textArea = textArea; + textArea.style.position = "absolute"; + textArea.style.whiteSpace = "pre"; + textArea.style.left = "-1000px"; + textArea.tabIndex = 1; + textArea.autocapitalize = "off"; + textArea.autocorrect = "off"; + textArea.className = "viewContainer"; + textArea.style.background = "transparent"; + textArea.style.color = "transparent"; + textArea.style.border = "0px"; + textArea.style.padding = "0px"; + textArea.style.margin = "0px"; + textArea.style.borderRadius = "0px"; + textArea.style.WebkitAppearance = "none"; + textArea.style.WebkitTapHighlightColor = "transparent"; + touchDiv.appendChild(textArea); + } + if (isFirefox) { + var clipboardDiv = frameDocument.createElement("DIV"); + this._clipboardDiv = clipboardDiv; + clipboardDiv.style.position = "fixed"; + clipboardDiv.style.whiteSpace = "pre"; + clipboardDiv.style.left = "-1000px"; + body.appendChild(clipboardDiv); + } + + var viewDiv = frameDocument.createElement("DIV"); + viewDiv.className = "view"; + this._viewDiv = viewDiv; + viewDiv.id = "viewDiv"; + viewDiv.tabIndex = -1; + viewDiv.style.overflow = "auto"; + viewDiv.style.position = "absolute"; + viewDiv.style.top = "0px"; + viewDiv.style.borderWidth = "0px"; + viewDiv.style.margin = "0px"; + viewDiv.style.outline = "none"; + body.appendChild(viewDiv); + + var scrollDiv = frameDocument.createElement("DIV"); + this._scrollDiv = scrollDiv; + scrollDiv.id = "scrollDiv"; + scrollDiv.style.margin = "0px"; + scrollDiv.style.borderWidth = "0px"; + scrollDiv.style.padding = "0px"; + viewDiv.appendChild(scrollDiv); + + if (isFirefox) { + var clipDiv = frameDocument.createElement("DIV"); + this._clipDiv = clipDiv; + clipDiv.id = "clipDiv"; + clipDiv.style.position = "fixed"; + clipDiv.style.overflow = "hidden"; + clipDiv.style.margin = "0px"; + clipDiv.style.borderWidth = "0px"; + clipDiv.style.padding = "0px"; + scrollDiv.appendChild(clipDiv); + + var clipScrollDiv = frameDocument.createElement("DIV"); + this._clipScrollDiv = clipScrollDiv; + clipScrollDiv.id = "clipScrollDiv"; + clipScrollDiv.style.position = "absolute"; + clipScrollDiv.style.height = "1px"; + clipScrollDiv.style.top = "-1000px"; + clipDiv.appendChild(clipScrollDiv); + } + + this._setFullSelection(this._fullSelection, true); + + var clientDiv = frameDocument.createElement("DIV"); + clientDiv.className = "viewContent"; + this._clientDiv = clientDiv; + clientDiv.id = "clientDiv"; + clientDiv.style.whiteSpace = "pre"; + clientDiv.style.position = this._clipDiv ? "absolute" : "fixed"; + clientDiv.style.borderWidth = "0px"; + clientDiv.style.margin = "0px"; + clientDiv.style.padding = "0px"; + clientDiv.style.outline = "none"; + clientDiv.style.zIndex = "1"; + if (isPad) { + clientDiv.style.WebkitTapHighlightColor = "transparent"; + } + (this._clipDiv || scrollDiv).appendChild(clientDiv); + + if (isFirefox && !clientDiv.setCapture) { + var overlayDiv = frameDocument.createElement("DIV"); + this._overlayDiv = overlayDiv; + overlayDiv.id = "overlayDiv"; + overlayDiv.style.position = clientDiv.style.position; + overlayDiv.style.borderWidth = clientDiv.style.borderWidth; + overlayDiv.style.margin = clientDiv.style.margin; + overlayDiv.style.padding = clientDiv.style.padding; + overlayDiv.style.cursor = "text"; + overlayDiv.style.zIndex = "2"; + (this._clipDiv || scrollDiv).appendChild(overlayDiv); + } + if (!isPad) { + clientDiv.contentEditable = "true"; + } + this._lineHeight = this._calculateLineHeight(); + this._viewPadding = this._calculatePadding(); + if (isIE) { + body.style.lineHeight = this._lineHeight + "px"; + } + this._setTabSize(this._tabSize, true); + this._hookEvents(); + var rulers = this._rulers; + for (var i=0; i 0 || v > 0) { + viewDiv.scrollLeft = h; + viewDiv.scrollTop = v; + } + this.onLoad({type: "Load"}); + }, + _defaultOptions: function() { + return { + parent: {value: undefined, recreate: true, update: null}, + model: {value: undefined, recreate: false, update: this.setModel}, + readonly: {value: false, recreate: false, update: null}, + fullSelection: {value: true, recreate: false, update: this._setFullSelection}, + tabSize: {value: 8, recreate: false, update: this._setTabSize}, + expandTab: {value: false, recreate: false, update: null}, + stylesheet: {value: [], recreate: false, update: this._setStyleSheet}, + themeClass: {value: undefined, recreate: false, update: this._setThemeClass}, + sync: {value: false, recreate: false, update: null} + }; + }, + _destroyFrame: function() { + var frame = this._frame; + if (!frame) { return; } + if (this._loadHandler) { + removeHandler(frame, "load", this._loadHandler, !!isFirefox); + this._loadHandler = null; + } + if (this._attrModifiedHandler) { + removeHandler(this._parentDocument, "DOMAttrModified", this._attrModifiedHandler); + this._attrModifiedHandler = null; + } + frame.parentNode.removeChild(frame); + this._frame = null; + }, + _destroyRuler: function(ruler) { + var side = ruler.getLocation(); + var rulerParent = side === "left" ? this._leftDiv : this._rightDiv; + if (rulerParent) { + var row = rulerParent.firstChild.rows[0]; + var cells = row.cells; + for (var index = 0; index < cells.length; index++) { + var cell = cells[index]; + if (cell.firstChild._ruler === ruler) { break; } + } + if (index === cells.length) { return; } + row.cells[index]._ruler = undefined; + row.deleteCell(index); + } + }, + _destroyView: function() { + var clientDiv = this._clientDiv; + if (!clientDiv) { return; } + this._setGrab(null); + this._unhookEvents(); + if (this._windowLoadHandler) { + removeHandler(this._frameWindow, "load", this._windowLoadHandler); + this._windowLoadHandler = null; + } + + /* Destroy timers */ + if (this._autoScrollTimerID) { + clearTimeout(this._autoScrollTimerID); + this._autoScrollTimerID = null; + } + if (this._updateTimer) { + clearTimeout(this._updateTimer); + this._updateTimer = null; + } + + /* Destroy DOM */ + var parent = this._frameDocument.body; + while (parent.hasChildNodes()) { parent.removeChild(parent.lastChild); } + if (this._touchDiv) { + this._parent.removeChild(this._touchDiv); + this._touchDiv = null; + } + this._selDiv1 = null; + this._selDiv2 = null; + this._selDiv3 = null; + this._insertedSelRule = false; + this._textArea = null; + this._clipboardDiv = null; + this._scrollDiv = null; + this._viewDiv = null; + this._clipDiv = null; + this._clipScrollDiv = null; + this._clientDiv = null; + this._overlayDiv = null; + this._leftDiv = null; + this._rightDiv = null; + this._frameDocument = null; + this._frameWindow = null; + this.onUnload({type: "Unload"}); + }, + _doAutoScroll: function (direction, x, y) { + this._autoScrollDir = direction; + this._autoScrollX = x; + this._autoScrollY = y; + if (!this._autoScrollTimerID) { + this._autoScrollTimer(); + } + }, + _endAutoScroll: function () { + if (this._autoScrollTimerID) { clearTimeout(this._autoScrollTimerID); } + this._autoScrollDir = undefined; + this._autoScrollTimerID = undefined; + }, + _fixCaret: function() { + var clientDiv = this._clientDiv; + if (clientDiv) { + var hasFocus = this._hasFocus; + this._ignoreFocus = true; + if (hasFocus) { clientDiv.blur(); } + clientDiv.contentEditable = false; + clientDiv.contentEditable = true; + if (hasFocus) { clientDiv.focus(); } + this._ignoreFocus = false; + } + }, + _getBaseText: function(start, end) { + var model = this._model; + /* This is the only case the view access the base model, alternatively the view could use a event to application to customize the text */ + if (model.getBaseModel) { + start = model.mapOffset(start); + end = model.mapOffset(end); + model = model.getBaseModel(); + } + return model.getText(start, end); + }, + _getBoundsAtOffset: function (offset) { + var model = this._model; + var document = this._frameDocument; + var clientDiv = this._clientDiv; + var lineIndex = model.getLineAtOffset(offset); + var dummy; + var child = this._getLineNode(lineIndex); + if (!child) { + child = dummy = this._createLine(clientDiv, null, document, lineIndex, model); + } + var result = null; + if (offset < model.getLineEnd(lineIndex)) { + var lineOffset = model.getLineStart(lineIndex); + var lineChild = child.firstChild; + while (lineChild) { + var textNode = lineChild.firstChild; + var nodeLength = textNode.length; + if (lineChild.ignoreChars) { + nodeLength -= lineChild.ignoreChars; + } + if (lineOffset + nodeLength > offset) { + var index = offset - lineOffset; + var range; + if (isRangeRects) { + range = document.createRange(); + range.setStart(textNode, index); + range.setEnd(textNode, index + 1); + result = range.getBoundingClientRect(); + } else if (isIE) { + range = document.body.createTextRange(); + range.moveToElementText(lineChild); + range.collapse(); + range.moveEnd("character", index + 1); + range.moveStart("character", index); + result = range.getBoundingClientRect(); + } else { + var text = textNode.data; + lineChild.removeChild(textNode); + lineChild.appendChild(document.createTextNode(text.substring(0, index))); + var span = document.createElement("SPAN"); + span.appendChild(document.createTextNode(text.substring(index, index + 1))); + lineChild.appendChild(span); + lineChild.appendChild(document.createTextNode(text.substring(index + 1))); + result = span.getBoundingClientRect(); + lineChild.innerHTML = ""; + lineChild.appendChild(textNode); + if (!dummy) { + /* + * Removing the element node that holds the selection start or end + * causes the selection to be lost. The fix is to detect this case + * and restore the selection. + */ + var s = this._getSelection(); + if ((lineOffset <= s.start && s.start < lineOffset + nodeLength) || (lineOffset <= s.end && s.end < lineOffset + nodeLength)) { + this._updateDOMSelection(); + } + } + } + if (isIE) { + var logicalXDPI = window.screen.logicalXDPI; + var deviceXDPI = window.screen.deviceXDPI; + result.left = result.left * logicalXDPI / deviceXDPI; + result.right = result.right * logicalXDPI / deviceXDPI; + } + break; + } + lineOffset += nodeLength; + lineChild = lineChild.nextSibling; + } + } + if (!result) { + var rect = this._getLineBoundingClientRect(child); + result = {left: rect.right, right: rect.right}; + } + if (dummy) { clientDiv.removeChild(dummy); } + return result; + }, + _getBottomIndex: function (fullyVisible) { + var child = this._bottomChild; + if (fullyVisible && this._getClientHeight() > this._getLineHeight()) { + var rect = child.getBoundingClientRect(); + var clientRect = this._clientDiv.getBoundingClientRect(); + if (rect.bottom > clientRect.bottom) { + child = this._getLinePrevious(child) || child; + } + } + return child.lineIndex; + }, + _getFrameHeight: function() { + return this._frameDocument.documentElement.clientHeight; + }, + _getFrameWidth: function() { + return this._frameDocument.documentElement.clientWidth; + }, + _getClientHeight: function() { + var viewPad = this._getViewPadding(); + return Math.max(0, this._viewDiv.clientHeight - viewPad.top - viewPad.bottom); + }, + _getClientWidth: function() { + var viewPad = this._getViewPadding(); + return Math.max(0, this._viewDiv.clientWidth - viewPad.left - viewPad.right); + }, + _getClipboardText: function (event, handler) { + var delimiter = this._model.getLineDelimiter(); + var clipboadText, text; + if (this._frameWindow.clipboardData) { + //IE + clipboadText = []; + text = this._frameWindow.clipboardData.getData("Text"); + this._convertDelimiter(text, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);}); + text = clipboadText.join(""); + if (handler) { handler(text); } + return text; + } + if (isFirefox) { + this._ignoreFocus = true; + var document = this._frameDocument; + var clipboardDiv = this._clipboardDiv; + clipboardDiv.innerHTML = "
";
+				clipboardDiv.firstChild.focus();
+				var self = this;
+				var _getText = function() {
+					var noteText = self._getTextFromElement(clipboardDiv);
+					clipboardDiv.innerHTML = "";
+					clipboadText = [];
+					self._convertDelimiter(noteText, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);});
+					return clipboadText.join("");
+				};
+				
+				/* Try execCommand first. Works on firefox with clipboard permission. */
+				var result = false;
+				this._ignorePaste = true;
+
+				/* Do not try execCommand if middle-click is used, because if we do, we get the clipboard text, not the primary selection text. */
+				if (!isLinux || this._lastMouseButton !== 2) {
+					try {
+						result = document.execCommand("paste", false, null);
+					} catch (ex) {
+						/* Firefox can throw even when execCommand() works, see bug 362835. */
+						result = clipboardDiv.childNodes.length > 1 || clipboardDiv.firstChild && clipboardDiv.firstChild.childNodes.length > 0;
+					}
+				}
+				this._ignorePaste = false;
+				if (!result) {
+					/* Try native paste in DOM, works for firefox during the paste event. */
+					if (event) {
+						setTimeout(function() {
+							self.focus();
+							text = _getText();
+							if (text && handler) {
+								handler(text);
+							}
+							self._ignoreFocus = false;
+						}, 0);
+						return null;
+					} else {
+						/* no event and no clipboard permission, paste can't be performed */
+						this.focus();
+						this._ignoreFocus = false;
+						return "";
+					}
+				}
+				this.focus();
+				this._ignoreFocus = false;
+				text = _getText();
+				if (text && handler) {
+					handler(text);
+				}
+				return text;
+			}
+			//webkit
+			if (event && event.clipboardData) {
+				/*
+				* Webkit (Chrome/Safari) allows getData during the paste event
+				* Note: setData is not allowed, not even during copy/cut event
+				*/
+				clipboadText = [];
+				text = event.clipboardData.getData("text/plain");
+				this._convertDelimiter(text, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);});
+				text = clipboadText.join("");
+				if (text && handler) {
+					handler(text);
+				}
+				return text;
+			} else {
+				//TODO try paste using extension (Chrome only)
+			}
+			return "";
+		},
+		_getDOMText: function(lineIndex) {
+			var child = this._getLineNode(lineIndex);
+			var lineChild = child.firstChild;
+			var text = "";
+			while (lineChild) {
+				var textNode = lineChild.firstChild;
+				while (textNode) {
+					if (lineChild.ignoreChars) {
+						for (var i = 0; i < textNode.length; i++) {
+							var ch = textNode.data.substring(i, i + 1);
+							if (ch !== " ") {
+								text += ch;
+							}
+						}
+					} else {
+						text += textNode.data;
+					}
+					textNode = textNode.nextSibling;
+				}
+				lineChild = lineChild.nextSibling;
+			}
+			return text;
+		},
+		_getTextFromElement: function(element) {
+			var document = element.ownerDocument;
+			var window = document.defaultView;
+			if (!window.getSelection) {
+				return element.innerText || element.textContent;
+			}
+
+			var newRange = document.createRange();
+			newRange.selectNode(element);
+
+			var selection = window.getSelection();
+			var oldRanges = [], i;
+			for (i = 0; i < selection.rangeCount; i++) {
+				oldRanges.push(selection.getRangeAt(i));
+			}
+
+			this._ignoreSelect = true;
+			selection.removeAllRanges();
+			selection.addRange(newRange);
+
+			var text = selection.toString();
+
+			selection.removeAllRanges();
+			for (i = 0; i < oldRanges.length; i++) {
+				selection.addRange(oldRanges[i]);
+			}
+
+			this._ignoreSelect = false;
+			return text;
+		},
+		_getViewPadding: function() {
+			return this._viewPadding;
+		},
+		_getLineBoundingClientRect: function (child) {
+			var rect = child.getBoundingClientRect();
+			var lastChild = child.lastChild;
+			//Remove any artificial trailing whitespace in the line
+			while (lastChild && lastChild.ignoreChars === lastChild.firstChild.length) {
+				lastChild = lastChild.previousSibling;
+			}
+			if (!lastChild) {
+				return {left: rect.left, top: rect.top, right: rect.left, bottom: rect.bottom};
+			}
+			var lastRect = lastChild.getBoundingClientRect();
+			return {left: rect.left, top: rect.top, right: lastRect.right, bottom: rect.bottom};
+		},
+		_getLineHeight: function() {
+			return this._lineHeight;
+		},
+		_getLineNode: function (lineIndex) {
+			var clientDiv = this._clientDiv;
+			var child = clientDiv.firstChild;
+			while (child) {
+				if (lineIndex === child.lineIndex) {
+					return child;
+				}
+				child = child.nextSibling;
+			}
+			return undefined;
+		},
+		_getLineNext: function (lineNode) {
+			var node = lineNode ? lineNode.nextSibling : this._clientDiv.firstChild;
+			while (node && node.lineIndex === -1) {
+				node = node.nextSibling;
+			}
+			return node;
+		},
+		_getLinePrevious: function (lineNode) {
+			var node = lineNode ? lineNode.previousSibling : this._clientDiv.lastChild;
+			while (node && node.lineIndex === -1) {
+				node = node.previousSibling;
+			}
+			return node;
+		},
+		_getOffset: function (offset, unit, direction) {
+			if (unit === "line") {
+				var model = this._model;
+				var lineIndex = model.getLineAtOffset(offset);
+				if (direction > 0) {
+					return model.getLineEnd(lineIndex);
+				}
+				return model.getLineStart(lineIndex);
+			}
+			if (unit === "wordend") {
+				return this._getOffset_W3C(offset, unit, direction);
+			}
+			return isIE ? this._getOffset_IE(offset, unit, direction) : this._getOffset_W3C(offset, unit, direction);
+		},
+		_getOffset_W3C: function (offset, unit, direction) {
+			function _isPunctuation(c) {
+				return (33 <= c && c <= 47) || (58 <= c && c <= 64) || (91 <= c && c <= 94) || c === 96 || (123 <= c && c <= 126);
+			}
+			function _isWhitespace(c) {
+				return c === 32 || c === 9;
+			}
+			if (unit === "word" || unit === "wordend") {
+				var model = this._model;
+				var lineIndex = model.getLineAtOffset(offset);
+				var lineText = model.getLine(lineIndex);
+				var lineStart = model.getLineStart(lineIndex);
+				var lineEnd = model.getLineEnd(lineIndex);
+				var lineLength = lineText.length;
+				var offsetInLine = offset - lineStart;
+				
+				
+				var c, previousPunctuation, previousLetterOrDigit, punctuation, letterOrDigit;
+				if (direction > 0) {
+					if (offsetInLine === lineLength) { return lineEnd; }
+					c = lineText.charCodeAt(offsetInLine);
+					previousPunctuation = _isPunctuation(c); 
+					previousLetterOrDigit = !previousPunctuation && !_isWhitespace(c);
+					offsetInLine++;
+					while (offsetInLine < lineLength) {
+						c = lineText.charCodeAt(offsetInLine);
+						punctuation = _isPunctuation(c);
+						if (unit === "wordend") {
+							if (!punctuation && previousPunctuation) { break; }
+						} else {
+							if (punctuation && !previousPunctuation) { break; }
+						}
+						letterOrDigit  = !punctuation && !_isWhitespace(c);
+						if (unit === "wordend") {
+							if (!letterOrDigit && previousLetterOrDigit) { break; }
+						} else {
+							if (letterOrDigit && !previousLetterOrDigit) { break; }
+						}
+						previousLetterOrDigit = letterOrDigit;
+						previousPunctuation = punctuation;
+						offsetInLine++;
+					}
+				} else {
+					if (offsetInLine === 0) { return lineStart; }
+					offsetInLine--;
+					c = lineText.charCodeAt(offsetInLine);
+					previousPunctuation = _isPunctuation(c); 
+					previousLetterOrDigit = !previousPunctuation && !_isWhitespace(c);
+					while (0 < offsetInLine) {
+						c = lineText.charCodeAt(offsetInLine - 1);
+						punctuation = _isPunctuation(c);
+						if (unit === "wordend") {
+							if (punctuation && !previousPunctuation) { break; }
+						} else {
+							if (!punctuation && previousPunctuation) { break; }
+						}
+						letterOrDigit  = !punctuation && !_isWhitespace(c);
+						if (unit === "wordend") {
+							if (letterOrDigit && !previousLetterOrDigit) { break; }
+						} else {
+							if (!letterOrDigit && previousLetterOrDigit) { break; }
+						}
+						previousLetterOrDigit = letterOrDigit;
+						previousPunctuation = punctuation;
+						offsetInLine--;
+					}
+				}
+				return lineStart + offsetInLine;
+			}
+			return offset + direction;
+		},
+		_getOffset_IE: function (offset, unit, direction) {
+			var document = this._frameDocument;
+			var model = this._model;
+			var lineIndex = model.getLineAtOffset(offset);
+			var clientDiv = this._clientDiv;
+			var dummy;
+			var child = this._getLineNode(lineIndex);
+			if (!child) {
+				child = dummy = this._createLine(clientDiv, null, document, lineIndex, model);
+			}
+			var result = 0, range, length;
+			var lineOffset = model.getLineStart(lineIndex);
+			if (offset === model.getLineEnd(lineIndex)) {
+				range = document.body.createTextRange();
+				range.moveToElementText(child.lastChild);
+				length = range.text.length;
+				range.moveEnd(unit, direction);
+				result = offset + range.text.length - length;
+			} else if (offset === lineOffset && direction < 0) {
+				result = lineOffset;
+			} else {
+				var lineChild = child.firstChild;
+				while (lineChild) {
+					var textNode = lineChild.firstChild;
+					var nodeLength = textNode.length;
+					if (lineChild.ignoreChars) {
+						nodeLength -= lineChild.ignoreChars;
+					}
+					if (lineOffset + nodeLength > offset) {
+						range = document.body.createTextRange();
+						if (offset === lineOffset && direction < 0) {
+							range.moveToElementText(lineChild.previousSibling);
+						} else {
+							range.moveToElementText(lineChild);
+							range.collapse();
+							range.moveEnd("character", offset - lineOffset);
+						}
+						length = range.text.length;
+						range.moveEnd(unit, direction);
+						result = offset + range.text.length - length;
+						break;
+					}
+					lineOffset = nodeLength + lineOffset;
+					lineChild = lineChild.nextSibling;
+				}
+			}
+			if (dummy) { clientDiv.removeChild(dummy); }
+			return result;
+		},
+		_getOffsetToX: function (offset) {
+			return this._getBoundsAtOffset(offset).left;
+		},
+		_getPadding: function (node) {
+			var left,top,right,bottom;
+			if (node.currentStyle) {
+				left = node.currentStyle.paddingLeft;
+				top = node.currentStyle.paddingTop;
+				right = node.currentStyle.paddingRight;
+				bottom = node.currentStyle.paddingBottom;
+			} else if (this._frameWindow.getComputedStyle) {
+				var style = this._frameWindow.getComputedStyle(node, null);
+				left = style.getPropertyValue("padding-left");
+				top = style.getPropertyValue("padding-top");
+				right = style.getPropertyValue("padding-right");
+				bottom = style.getPropertyValue("padding-bottom");
+			}
+			return {
+					left: parseInt(left, 10), 
+					top: parseInt(top, 10),
+					right: parseInt(right, 10),
+					bottom: parseInt(bottom, 10)
+			};
+		},
+		_getScroll: function() {
+			var viewDiv = this._viewDiv;
+			return {x: viewDiv.scrollLeft, y: viewDiv.scrollTop};
+		},
+		_getSelection: function () {
+			return this._selection.clone();
+		},
+		_getTopIndex: function (fullyVisible) {
+			var child = this._topChild;
+			if (fullyVisible && this._getClientHeight() > this._getLineHeight()) {
+				var rect = child.getBoundingClientRect();
+				var viewPad = this._getViewPadding();
+				var viewRect = this._viewDiv.getBoundingClientRect();
+				if (rect.top < viewRect.top + viewPad.top) {
+					child = this._getLineNext(child) || child;
+				}
+			}
+			return child.lineIndex;
+		},
+		_getXToOffset: function (lineIndex, x) {
+			var model = this._model;
+			var lineStart = model.getLineStart(lineIndex);
+			var lineEnd = model.getLineEnd(lineIndex);
+			if (lineStart === lineEnd) {
+				return lineStart;
+			}
+			var document = this._frameDocument;
+			var clientDiv = this._clientDiv;
+			var dummy;
+			var child = this._getLineNode(lineIndex);
+			if (!child) {
+				child = dummy = this._createLine(clientDiv, null, document, lineIndex, model);
+			}
+			var lineRect = this._getLineBoundingClientRect(child);
+			if (x < lineRect.left) { x = lineRect.left; }
+			if (x > lineRect.right) { x = lineRect.right; }
+			/*
+			* Bug in IE 8 and earlier. The coordinates of getClientRects() are relative to
+			* the browser window.  The fix is to convert to the frame window before using it. 
+			*/
+			var deltaX = 0, rects;
+			if (isIE < 9) {
+				rects = child.getClientRects();
+				var minLeft = rects[0].left;
+				for (var i=1; i 1) {
+								var mid = Math.floor((high + low) / 2);
+								start = low + 1;
+								end = mid === nodeLength - 1 && lineChild.ignoreChars ? textNode.length : mid + 1;
+								if (isRangeRects) {
+									range.setStart(textNode, start);
+									range.setEnd(textNode, end);
+								} else {
+									range.moveToElementText(lineChild);
+									range.move("character", start);
+									range.moveEnd("character", end - start);
+								}
+								rects = range.getClientRects();
+								var found = false;
+								for (var k = 0; k < rects.length; k++) {
+									rect = rects[k];
+									var rangeLeft = rect.left * logicalXDPI / deviceXDPI - deltaX;
+									var rangeRight = rect.right * logicalXDPI / deviceXDPI - deltaX;
+									if (rangeLeft <= x && x < rangeRight) {
+										found = true;
+										break;
+									}
+								}
+								if (found) {
+									high = mid;
+								} else {
+									low = mid;
+								}
+							}
+							offset += high;
+							start = high;
+							end = high === nodeLength - 1 && lineChild.ignoreChars ? textNode.length : Math.min(high + 1, textNode.length);
+							if (isRangeRects) {
+								range.setStart(textNode, start);
+								range.setEnd(textNode, end);
+							} else {
+								range.moveToElementText(lineChild);
+								range.move("character", start);
+								range.moveEnd("character", end - start);
+							}
+							rect = range.getClientRects()[0];
+							//TODO test for character trailing (wrong for bidi)
+							if (x > ((rect.left * logicalXDPI / deviceXDPI - deltaX) + ((rect.right - rect.left) * logicalXDPI / deviceXDPI / 2))) {
+								offset++;
+							}
+						} else {
+							var newText = [];
+							for (var q = 0; q < nodeLength; q++) {
+								newText.push("");
+								if (q === nodeLength - 1) {
+									newText.push(textNode.data.substring(q));
+								} else {
+									newText.push(textNode.data.substring(q, q + 1));
+								}
+								newText.push("");
+							}
+							lineChild.innerHTML = newText.join("");
+							var rangeChild = lineChild.firstChild;
+							while (rangeChild) {
+								rect = rangeChild.getBoundingClientRect();
+								if (rect.left <= x && x < rect.right) {
+									//TODO test for character trailing (wrong for bidi)
+									if (x > rect.left + (rect.right - rect.left) / 2) {
+										offset++;
+									}
+									break;
+								}
+								offset++;
+								rangeChild = rangeChild.nextSibling;
+							}
+							if (!dummy) {
+								lineChild.innerHTML = "";
+								lineChild.appendChild(textNode);
+								/*
+								 * Removing the element node that holds the selection start or end
+								 * causes the selection to be lost. The fix is to detect this case
+								 * and restore the selection. 
+								 */
+								var s = this._getSelection();
+								if ((offset <= s.start && s.start < offset + nodeLength) || (offset <= s.end && s.end < offset + nodeLength)) {
+									this._updateDOMSelection();
+								}
+							}
+						}
+						break done;
+					}
+				}
+				offset += nodeLength;
+				lineChild = lineChild.nextSibling;
+			}
+			if (dummy) { clientDiv.removeChild(dummy); }
+			return Math.min(lineEnd, Math.max(lineStart, offset));
+		},
+		_getYToLine: function (y) {
+			var viewPad = this._getViewPadding();
+			var viewRect = this._viewDiv.getBoundingClientRect();
+			y -= viewRect.top + viewPad.top;
+			var lineHeight = this._getLineHeight();
+			var lineIndex = Math.floor((y + this._getScroll().y) / lineHeight);
+			var lineCount = this._model.getLineCount();
+			return Math.max(0, Math.min(lineCount - 1, lineIndex));
+		},
+		_getOffsetBounds: function(offset) {
+			var model = this._model;
+			var lineIndex = model.getLineAtOffset(offset);
+			var lineHeight = this._getLineHeight();
+			var scroll = this._getScroll();
+			var viewPad = this._getViewPadding();
+			var viewRect = this._viewDiv.getBoundingClientRect();
+			var bounds = this._getBoundsAtOffset(offset);
+			var left = bounds.left;
+			var right = bounds.right;
+			var top = (lineIndex * lineHeight) - scroll.y + viewRect.top + viewPad.top;
+			var bottom = top + lineHeight;
+			return {left: left, top: top, right: right, bottom: bottom};
+		},
+		_getVisible: function() {
+			var temp = this._parent;
+			var parentDocument = temp.ownerDocument;
+			while (temp !== parentDocument) {
+				var hidden;
+				if (isIE < 9) {
+					hidden = temp.currentStyle && temp.currentStyle.display === "none";
+				} else {
+					var tempStyle = parentDocument.defaultView.getComputedStyle(temp, null);
+					hidden = tempStyle && tempStyle.getPropertyValue("display") === "none";
+				}
+				if (hidden) { return "hidden"; }
+				temp =  temp.parentNode;
+				if (!temp) { return "disconnected"; }
+			}
+			return "visible";
+		},
+		_hitOffset: function (offset, x, y) {
+			var bounds = this._getOffsetBounds(offset);
+			var left = bounds.left;
+			var right = bounds.right;
+			var top = bounds.top;
+			var bottom = bounds.bottom;
+			var area = 20;
+			left -= area;
+			top -= area;
+			right += area;
+			bottom += area;
+			return (left <= x && x <= right && top <= y && y <= bottom);
+		},
+		_hookEvents: function() {
+			var self = this;
+			this._modelListener = {
+				/** @private */
+				onChanging: function(modelChangingEvent) {
+					self._onModelChanging(modelChangingEvent);
+				},
+				/** @private */
+				onChanged: function(modelChangedEvent) {
+					self._onModelChanged(modelChangedEvent);
+				}
+			};
+			this._model.addEventListener("Changing", this._modelListener.onChanging);
+			this._model.addEventListener("Changed", this._modelListener.onChanged);
+			
+			var clientDiv = this._clientDiv;
+			var viewDiv = this._viewDiv;
+			var body = this._frameDocument.body; 
+			var handlers = this._handlers = [];
+			var resizeNode = isIE < 9 ? this._frame : this._frameWindow;
+			var focusNode = isPad ? this._textArea : (isIE ||  isFirefox ? this._clientDiv: this._frameWindow);
+			handlers.push({target: this._frameWindow, type: "unload", handler: function(e) { return self._handleUnload(e);}});
+			handlers.push({target: resizeNode, type: "resize", handler: function(e) { return self._handleResize(e);}});
+			handlers.push({target: focusNode, type: "blur", handler: function(e) { return self._handleBlur(e);}});
+			handlers.push({target: focusNode, type: "focus", handler: function(e) { return self._handleFocus(e);}});
+			handlers.push({target: viewDiv, type: "scroll", handler: function(e) { return self._handleScroll(e);}});
+			if (isPad) {
+				var touchDiv = this._touchDiv;
+				var textArea = this._textArea;
+				handlers.push({target: textArea, type: "keydown", handler: function(e) { return self._handleKeyDown(e);}});
+				handlers.push({target: textArea, type: "input", handler: function(e) { return self._handleInput(e); }});
+				handlers.push({target: textArea, type: "textInput", handler: function(e) { return self._handleTextInput(e); }});
+				handlers.push({target: textArea, type: "click", handler: function(e) { return self._handleTextAreaClick(e); }});
+				handlers.push({target: touchDiv, type: "touchstart", handler: function(e) { return self._handleTouchStart(e); }});
+				handlers.push({target: touchDiv, type: "touchmove", handler: function(e) { return self._handleTouchMove(e); }});
+				handlers.push({target: touchDiv, type: "touchend", handler: function(e) { return self._handleTouchEnd(e); }});
+			} else {
+				var topNode = this._overlayDiv || this._clientDiv;
+				var grabNode = isIE ? clientDiv : this._frameWindow;
+				handlers.push({target: clientDiv, type: "keydown", handler: function(e) { return self._handleKeyDown(e);}});
+				handlers.push({target: clientDiv, type: "keypress", handler: function(e) { return self._handleKeyPress(e);}});
+				handlers.push({target: clientDiv, type: "keyup", handler: function(e) { return self._handleKeyUp(e);}});
+				handlers.push({target: clientDiv, type: "selectstart", handler: function(e) { return self._handleSelectStart(e);}});
+				handlers.push({target: clientDiv, type: "contextmenu", handler: function(e) { return self._handleContextMenu(e);}});
+				handlers.push({target: clientDiv, type: "copy", handler: function(e) { return self._handleCopy(e);}});
+				handlers.push({target: clientDiv, type: "cut", handler: function(e) { return self._handleCut(e);}});
+				handlers.push({target: clientDiv, type: "paste", handler: function(e) { return self._handlePaste(e);}});
+				handlers.push({target: clientDiv, type: "mousedown", handler: function(e) { return self._handleMouseDown(e);}});
+				handlers.push({target: clientDiv, type: "mouseover", handler: function(e) { return self._handleMouseOver(e);}});
+				handlers.push({target: clientDiv, type: "mouseout", handler: function(e) { return self._handleMouseOut(e);}});
+				handlers.push({target: grabNode, type: "mouseup", handler: function(e) { return self._handleMouseUp(e);}});
+				handlers.push({target: grabNode, type: "mousemove", handler: function(e) { return self._handleMouseMove(e);}});
+				handlers.push({target: body, type: "mousedown", handler: function(e) { return self._handleBodyMouseDown(e);}});
+				handlers.push({target: body, type: "mouseup", handler: function(e) { return self._handleBodyMouseUp(e);}});
+				handlers.push({target: topNode, type: "dragstart", handler: function(e) { return self._handleDragStart(e);}});
+				handlers.push({target: topNode, type: "drag", handler: function(e) { return self._handleDrag(e);}});
+				handlers.push({target: topNode, type: "dragend", handler: function(e) { return self._handleDragEnd(e);}});
+				handlers.push({target: topNode, type: "dragenter", handler: function(e) { return self._handleDragEnter(e);}});
+				handlers.push({target: topNode, type: "dragover", handler: function(e) { return self._handleDragOver(e);}});
+				handlers.push({target: topNode, type: "dragleave", handler: function(e) { return self._handleDragLeave(e);}});
+				handlers.push({target: topNode, type: "drop", handler: function(e) { return self._handleDrop(e);}});
+				if (isChrome) {
+					handlers.push({target: this._parentDocument, type: "mousemove", handler: function(e) { return self._handleMouseMove(e);}});
+					handlers.push({target: this._parentDocument, type: "mouseup", handler: function(e) { return self._handleMouseUp(e);}});
+				}
+				if (isIE) {
+					handlers.push({target: this._frameDocument, type: "activate", handler: function(e) { return self._handleDocFocus(e); }});
+				}
+				if (isFirefox) {
+					handlers.push({target: this._frameDocument, type: "focus", handler: function(e) { return self._handleDocFocus(e); }});
+				}
+				if (!isIE && !isOpera) {
+					var wheelEvent = isFirefox ? "DOMMouseScroll" : "mousewheel";
+					handlers.push({target: this._viewDiv, type: wheelEvent, handler: function(e) { return self._handleMouseWheel(e); }});
+				}
+				if (isFirefox && !isWindows) {
+					handlers.push({target: this._clientDiv, type: "DOMCharacterDataModified", handler: function (e) { return self._handleDataModified(e); }});
+				}
+				if (this._overlayDiv) {
+					handlers.push({target: this._overlayDiv, type: "mousedown", handler: function(e) { return self._handleMouseDown(e);}});
+					handlers.push({target: this._overlayDiv, type: "mouseover", handler: function(e) { return self._handleMouseOver(e);}});
+					handlers.push({target: this._overlayDiv, type: "mouseout", handler: function(e) { return self._handleMouseOut(e);}});
+					handlers.push({target: this._overlayDiv, type: "contextmenu", handler: function(e) { return self._handleContextMenu(e); }});
+				}
+				if (!isW3CEvents) {
+					handlers.push({target: this._clientDiv, type: "dblclick", handler: function(e) { return self._handleDblclick(e); }});
+				}
+			}
+			for (var i=0; i start) {
+				if (selection.end > start && selection.start < start + removedCharCount) {
+					// selection intersects replaced text. set caret behind text change
+					selection.setCaret(start + addedCharCount);
+				} else {
+					// move selection to keep same text selected
+					selection.start +=  addedCharCount - removedCharCount;
+					selection.end +=  addedCharCount - removedCharCount;
+				}
+				this._setSelection(selection, false, false);
+			}
+			
+			var model = this._model;
+			var startLine = model.getLineAtOffset(start);
+			var child = this._getLineNext();
+			while (child) {
+				var lineIndex = child.lineIndex;
+				if (startLine <= lineIndex && lineIndex <= startLine + removedLineCount) {
+					if (startLine === lineIndex && !child.modelChangedEvent && !child.lineRemoved) {
+						child.modelChangedEvent = modelChangedEvent;
+						child.lineChanged = true;
+					} else {
+						child.lineRemoved = true;
+						child.lineChanged = false;
+						child.modelChangedEvent = null;
+					}
+				}
+				if (lineIndex > startLine + removedLineCount) {
+					child.lineIndex = lineIndex + addedLineCount - removedLineCount;
+				}
+				child = this._getLineNext(child);
+			}
+			if (startLine <= this._maxLineIndex && this._maxLineIndex <= startLine + removedLineCount) {
+				this._checkMaxLineIndex = this._maxLineIndex;
+				this._maxLineIndex = -1;
+				this._maxLineWidth = 0;
+			}
+			this._updatePage();
+		},
+		_onModelChanging: function(modelChangingEvent) {
+			modelChangingEvent.type = "ModelChanging";
+			this.onModelChanging(modelChangingEvent);
+			modelChangingEvent.type = "Changing";
+		},
+		_queueUpdatePage: function() {
+			if (this._updateTimer) { return; }
+			var self = this;
+			this._updateTimer = setTimeout(function() { 
+				self._updateTimer = null;
+				self._updatePage();
+			}, 0);
+		},
+		_reset: function() {
+			this._maxLineIndex = -1;
+			this._maxLineWidth = 0;
+			this._columnX = -1;
+			this._topChild = null;
+			this._bottomChild = null;
+			this._partialY = 0;
+			this._setSelection(new Selection (0, 0, false), false, false);
+			if (this._viewDiv) {
+				this._viewDiv.scrollLeft = 0;
+				this._viewDiv.scrollTop = 0;
+			}
+			var clientDiv = this._clientDiv;
+			if (clientDiv) {
+				var child = clientDiv.firstChild;
+				while (child) {
+					child.lineRemoved = true;
+					child = child.nextSibling;
+				}
+				/*
+				* Bug in Firefox.  For some reason, the caret does not show after the
+				* view is refreshed.  The fix is to toggle the contentEditable state and
+				* force the clientDiv to loose and receive focus if it is focused.
+				*/
+				if (isFirefox) {
+					this._ignoreFocus = false;
+					var hasFocus = this._hasFocus;
+					if (hasFocus) { clientDiv.blur(); }
+					clientDiv.contentEditable = false;
+					clientDiv.contentEditable = true;
+					if (hasFocus) { clientDiv.focus(); }
+					this._ignoreFocus = false;
+				}
+			}
+		},
+		_resizeTouchDiv: function() {
+			var viewRect = this._viewDiv.getBoundingClientRect();
+			var parentRect = this._frame.getBoundingClientRect();
+			var temp = this._frame;
+			while (temp) {
+				if (temp.style && temp.style.top) { break; }
+				temp = temp.parentNode;
+			}
+			var parentTop = parentRect.top;
+			if (temp) {
+				parentTop -= temp.getBoundingClientRect().top;
+			} else {
+				parentTop += this._parentDocument.body.scrollTop;
+			}
+			temp = this._frame;
+			while (temp) {
+				if (temp.style && temp.style.left) { break; }
+				temp = temp.parentNode;
+			}
+			var parentLeft = parentRect.left;
+			if (temp) {
+				parentLeft -= temp.getBoundingClientRect().left;
+			} else {
+				parentLeft += this._parentDocument.body.scrollLeft;
+			}
+			var touchDiv = this._touchDiv;
+			touchDiv.style.left = (parentLeft + viewRect.left) + "px";
+			touchDiv.style.top = (parentTop + viewRect.top) + "px";
+			touchDiv.style.width = viewRect.width + "px";
+			touchDiv.style.height = viewRect.height + "px";
+		},
+		_scrollView: function (pixelX, pixelY) {
+			/*
+			* Always set _ensureCaretVisible to false so that the view does not scroll
+			* to show the caret when scrollView is not called from showCaret().
+			*/
+			this._ensureCaretVisible = false;
+			
+			/*
+			* Scrolling is done only by setting the scrollLeft and scrollTop fields in the
+			* view div. This causes an updatePage from the scroll event. In some browsers 
+			* this event is asynchronous and forcing update page to run synchronously
+			* leads to redraw problems. 
+			* On Chrome 11, the view redrawing at times when holding PageDown/PageUp key.
+			* On Firefox 4 for Linux, the view redraws the first page when holding 
+			* PageDown/PageUp key, but it will not redraw again until the key is released.
+			*/
+			var viewDiv = this._viewDiv;
+			if (pixelX) { viewDiv.scrollLeft += pixelX; }
+			if (pixelY) { viewDiv.scrollTop += pixelY; }
+		},
+		_setClipboardText: function (text, event) {
+			var clipboardText;
+			if (this._frameWindow.clipboardData) {
+				//IE
+				clipboardText = [];
+				this._convertDelimiter(text, function(t) {clipboardText.push(t);}, function() {clipboardText.push(platformDelimiter);});
+				return this._frameWindow.clipboardData.setData("Text", clipboardText.join(""));
+			}
+			/* Feature in Chrome, clipboardData.setData is no-op on Chrome even though it returns true */
+			if (isChrome || isFirefox || !event) {
+				var window = this._frameWindow;
+				var document = this._frameDocument;
+				var child = document.createElement("PRE");
+				child.style.position = "fixed";
+				child.style.left = "-1000px";
+				this._convertDelimiter(text, 
+					function(t) {
+						child.appendChild(document.createTextNode(t));
+					}, 
+					function() {
+						child.appendChild(document.createElement("BR"));
+					}
+				);
+				child.appendChild(document.createTextNode(" "));
+				this._clientDiv.appendChild(child);
+				var range = document.createRange();
+				range.setStart(child.firstChild, 0);
+				range.setEndBefore(child.lastChild);
+				var sel = window.getSelection();
+				if (sel.rangeCount > 0) { sel.removeAllRanges(); }
+				sel.addRange(range);
+				var self = this;
+				/** @ignore */
+				var cleanup = function() {
+					if (child && child.parentNode === self._clientDiv) {
+						self._clientDiv.removeChild(child);
+					}
+					self._updateDOMSelection();
+				};
+				var result = false;
+				/* 
+				* Try execCommand first, it works on firefox with clipboard permission,
+				* chrome 5, safari 4.
+				*/
+				this._ignoreCopy = true;
+				try {
+					result = document.execCommand("copy", false, null);
+				} catch (e) {}
+				this._ignoreCopy = false;
+				if (!result) {
+					if (event) {
+						setTimeout(cleanup, 0);
+						return false;
+					}
+				}
+				/* no event and no permission, copy can not be done */
+				cleanup();
+				return true;
+			}
+			if (event && event.clipboardData) {
+				//webkit
+				clipboardText = [];
+				this._convertDelimiter(text, function(t) {clipboardText.push(t);}, function() {clipboardText.push(platformDelimiter);});
+				return event.clipboardData.setData("text/plain", clipboardText.join("")); 
+			}
+		},
+		_setDOMSelection: function (startNode, startOffset, endNode, endOffset) {
+			var window = this._frameWindow;
+			var document = this._frameDocument;
+			var startLineNode, startLineOffset, endLineNode, endLineOffset;
+			var offset = 0;
+			var lineChild = startNode.firstChild;
+			var node, nodeLength, model = this._model;
+			var startLineEnd = model.getLine(startNode.lineIndex).length;
+			while (lineChild) {
+				node = lineChild.firstChild;
+				nodeLength = node.length;
+				if (lineChild.ignoreChars) {
+					nodeLength -= lineChild.ignoreChars;
+				}
+				if (offset + nodeLength > startOffset || offset + nodeLength >= startLineEnd) {
+					startLineNode = node;
+					startLineOffset = startOffset - offset;
+					if (lineChild.ignoreChars && nodeLength > 0 && startLineOffset === nodeLength) {
+						startLineOffset += lineChild.ignoreChars; 
+					}
+					break;
+				}
+				offset += nodeLength;
+				lineChild = lineChild.nextSibling;
+			}
+			offset = 0;
+			lineChild = endNode.firstChild;
+			var endLineEnd = this._model.getLine(endNode.lineIndex).length;
+			while (lineChild) {
+				node = lineChild.firstChild;
+				nodeLength = node.length;
+				if (lineChild.ignoreChars) {
+					nodeLength -= lineChild.ignoreChars;
+				}
+				if (nodeLength + offset > endOffset || offset + nodeLength >= endLineEnd) {
+					endLineNode = node;
+					endLineOffset = endOffset - offset;
+					if (lineChild.ignoreChars && nodeLength > 0 && endLineOffset === nodeLength) {
+						endLineOffset += lineChild.ignoreChars; 
+					}
+					break;
+				}
+				offset += nodeLength;
+				lineChild = lineChild.nextSibling;
+			}
+			
+			this._setDOMFullSelection(startNode, startOffset, startLineEnd, endNode, endOffset, endLineEnd);
+			if (isPad) { return; }
+
+			var range;
+			if (window.getSelection) {
+				//W3C
+				range = document.createRange();
+				range.setStart(startLineNode, startLineOffset);
+				range.setEnd(endLineNode, endLineOffset);
+				var sel = window.getSelection();
+				this._ignoreSelect = false;
+				if (sel.rangeCount > 0) { sel.removeAllRanges(); }
+				sel.addRange(range);
+				this._ignoreSelect = true;
+			} else if (document.selection) {
+				//IE < 9
+				var body = document.body;
+
+				/*
+				* Bug in IE. For some reason when text is deselected the overflow
+				* selection at the end of some lines does not get redrawn.  The
+				* fix is to create a DOM element in the body to force a redraw.
+				*/
+				var child = document.createElement("DIV");
+				body.appendChild(child);
+				body.removeChild(child);
+				
+				range = body.createTextRange();
+				range.moveToElementText(startLineNode.parentNode);
+				range.moveStart("character", startLineOffset);
+				var endRange = body.createTextRange();
+				endRange.moveToElementText(endLineNode.parentNode);
+				endRange.moveStart("character", endLineOffset);
+				range.setEndPoint("EndToStart", endRange);
+				this._ignoreSelect = false;
+				range.select();
+				this._ignoreSelect = true;
+			}
+		},
+		_setDOMFullSelection: function(startNode, startOffset, startLineEnd, endNode, endOffset, endLineEnd) {
+			var model = this._model;
+			if (this._selDiv1) {
+				var startLineBounds, l;
+				startLineBounds = this._getLineBoundingClientRect(startNode);
+				if (startOffset === 0) {
+					l = startLineBounds.left;
+				} else {
+					if (startOffset >= startLineEnd) {
+						l = startLineBounds.right;
+					} else {
+						this._ignoreDOMSelection = true;
+						l = this._getBoundsAtOffset(model.getLineStart(startNode.lineIndex) + startOffset).left;
+						this._ignoreDOMSelection = false;
+					}
+				}
+				var textArea = this._textArea;
+				if (textArea && isPad) {
+					textArea.selectionStart = textArea.selectionEnd = 0;
+					var rect = this._frame.getBoundingClientRect();
+					var touchRect = this._touchDiv.getBoundingClientRect();
+					var viewBounds = this._viewDiv.getBoundingClientRect();
+					if (!(viewBounds.left <= l && l <= viewBounds.left + viewBounds.width &&
+						viewBounds.top <= startLineBounds.top && startLineBounds.top <= viewBounds.top + viewBounds.height) ||
+						!(startNode === endNode && startOffset === endOffset))
+					{
+						textArea.style.left = "-1000px";
+					} else {
+						textArea.style.left = (l - 4 + rect.left - touchRect.left) + "px";
+					}
+					textArea.style.top = (startLineBounds.top + rect.top - touchRect.top) + "px";
+					textArea.style.width = "6px";
+					textArea.style.height = (startLineBounds.bottom - startLineBounds.top) + "px";
+				}
+			
+				var selDiv = this._selDiv1;
+				selDiv.style.width = "0px";
+				selDiv.style.height = "0px";
+				selDiv = this._selDiv2;
+				selDiv.style.width = "0px";
+				selDiv.style.height = "0px";
+				selDiv = this._selDiv3;
+				selDiv.style.width = "0px";
+				selDiv.style.height = "0px";
+				if (!(startNode === endNode && startOffset === endOffset)) {
+					var handleWidth = isPad ? 2 : 0;
+					var handleBorder = handleWidth + "px blue solid";
+					var viewPad = this._getViewPadding();
+					var clientRect = this._clientDiv.getBoundingClientRect();
+					var viewRect = this._viewDiv.getBoundingClientRect();
+					var left = viewRect.left + viewPad.left;
+					var right = clientRect.right;
+					var top = viewRect.top + viewPad.top;
+					var bottom = clientRect.bottom;
+					var hd = 0, vd = 0;
+					if (this._clipDiv) {
+						var clipRect = this._clipDiv.getBoundingClientRect();
+						hd = clipRect.left - this._clipDiv.scrollLeft;
+						vd = clipRect.top;
+					}
+					var r;
+					var endLineBounds = this._getLineBoundingClientRect(endNode);
+					if (endOffset === 0) {
+						r = endLineBounds.left;
+					} else {
+						if (endOffset >= endLineEnd) {
+							r = endLineBounds.right;
+						} else {
+							this._ignoreDOMSelection = true;
+							r = this._getBoundsAtOffset(model.getLineStart(endNode.lineIndex) + endOffset).left;
+							this._ignoreDOMSelection = false;
+						}
+					}
+					var sel1Div = this._selDiv1;
+					var sel1Left = Math.min(right, Math.max(left, l));
+					var sel1Top = Math.min(bottom, Math.max(top, startLineBounds.top));
+					var sel1Right = right;
+					var sel1Bottom = Math.min(bottom, Math.max(top, startLineBounds.bottom));
+					sel1Div.style.left = (sel1Left - hd) + "px";
+					sel1Div.style.top = (sel1Top - vd) + "px";
+					sel1Div.style.width = Math.max(0, sel1Right - sel1Left) + "px";
+					sel1Div.style.height = Math.max(0, sel1Bottom - sel1Top) + (isPad ? 1 : 0) + "px";
+					if (isPad) {
+						sel1Div.style.borderLeft = handleBorder;
+						sel1Div.style.borderRight = "0px";
+					}
+					if (startNode === endNode) {
+						sel1Right = Math.min(r, right);
+						sel1Div.style.width = Math.max(0, sel1Right - sel1Left - handleWidth * 2) + "px";
+						if (isPad) {
+							sel1Div.style.borderRight = handleBorder;
+						}
+					} else {
+						var sel3Left = left;
+						var sel3Top = Math.min(bottom, Math.max(top, endLineBounds.top));
+						var sel3Right = Math.min(right, Math.max(left, r));
+						var sel3Bottom = Math.min(bottom, Math.max(top, endLineBounds.bottom));
+						var sel3Div = this._selDiv3;
+						sel3Div.style.left = (sel3Left - hd) + "px";
+						sel3Div.style.top = (sel3Top - vd) + "px";
+						sel3Div.style.width = Math.max(0, sel3Right - sel3Left - handleWidth) + "px";
+						sel3Div.style.height = Math.max(0, sel3Bottom - sel3Top) + "px";
+						if (isPad) {
+							sel3Div.style.borderRight = handleBorder;
+						}
+						if (sel3Top - sel1Bottom > 0) {
+							var sel2Div = this._selDiv2;
+							sel2Div.style.left = (left - hd)  + "px";
+							sel2Div.style.top = (sel1Bottom - vd) + "px";
+							sel2Div.style.width = Math.max(0, right - left) + "px";
+							sel2Div.style.height = Math.max(0, sel3Top - sel1Bottom) + (isPad ? 1 : 0) + "px";
+						}
+					}
+				}
+			}
+		},
+		_setGrab: function (target) {
+			if (target === this._grabControl) { return; }
+			if (target) {
+				if (target.setCapture) { target.setCapture(); }
+				this._grabControl = target;
+			} else {
+				if (this._grabControl.releaseCapture) { this._grabControl.releaseCapture(); }
+				this._grabControl = null;
+			}
+		},
+		_setLinksVisible: function(visible) {
+			if (this._linksVisible === visible) { return; }
+			this._linksVisible = visible;
+			/*
+			* Feature in IE.  The client div looses focus and does not regain it back
+			* when the content editable flag is reset. The fix is to remember that it
+			* had focus when the flag is cleared and give focus back to the div when
+			* the flag is set.
+			*/
+			if (isIE && visible) {
+				this._hadFocus = this._hasFocus;
+			}
+			var clientDiv = this._clientDiv;
+			clientDiv.contentEditable = !visible;
+			if (this._hadFocus && !visible) {
+				clientDiv.focus();
+			}
+			if (this._overlayDiv) {
+				this._overlayDiv.style.zIndex = visible ? "-1" : "1";
+			}
+			var document = this._frameDocument;
+			var line = this._getLineNext();
+			while (line) {
+				if (line.hasLink) {
+					var lineChild = line.firstChild;
+					while (lineChild) {
+						var next = lineChild.nextSibling;
+						var style = lineChild.viewStyle;
+						if (style && style.tagName === "A") {
+							line.replaceChild(this._createSpan(line, document, lineChild.firstChild.data, style), lineChild);
+						}
+						lineChild = next;
+					}
+				}
+				line = this._getLineNext(line);
+			}
+		},
+		_setSelection: function (selection, scroll, update, pageScroll) {
+			if (selection) {
+				this._columnX = -1;
+				if (update === undefined) { update = true; }
+				var oldSelection = this._selection; 
+				if (!oldSelection.equals(selection)) {
+					this._selection = selection;
+					var e = {
+						type: "Selection",
+						oldValue: {start:oldSelection.start, end:oldSelection.end},
+						newValue: {start:selection.start, end:selection.end}
+					};
+					this.onSelection(e);
+				}
+				/* 
+				* Always showCaret(), even when the selection is not changing, to ensure the
+				* caret is visible. Note that some views do not scroll to show the caret during
+				* keyboard navigation when the selection does not chanage. For example, line down
+				* when the caret is already at the last line.
+				*/
+				if (scroll) { update = !this._showCaret(false, pageScroll); }
+				
+				/* 
+				* Sometimes the browser changes the selection 
+				* as result of method calls or "leaked" events. 
+				* The fix is to set the visual selection even
+				* when the logical selection is not changed.
+				*/
+				if (update) { this._updateDOMSelection(); }
+			}
+		},
+		_setSelectionTo: function (x, y, extent, drag) {
+			var model = this._model, offset;
+			var selection = this._getSelection();
+			var lineIndex = this._getYToLine(y);
+			if (this._clickCount === 1) {
+				offset = this._getXToOffset(lineIndex, x);
+				if (drag && !extent) {
+					if (selection.start <= offset && offset < selection.end) {
+						this._dragOffset = offset;
+						return false;
+					}
+				}
+				selection.extend(offset);
+				if (!extent) { selection.collapse(); }
+			} else {
+				var word = (this._clickCount & 1) === 0;
+				var start, end;
+				if (word) {
+					offset = this._getXToOffset(lineIndex, x);
+					if (this._doubleClickSelection) {
+						if (offset >= this._doubleClickSelection.start) {
+							start = this._doubleClickSelection.start;
+							end = this._getOffset(offset, "wordend", +1);
+						} else {
+							start = this._getOffset(offset, "word", -1);
+							end = this._doubleClickSelection.end;
+						}
+					} else {
+						start = this._getOffset(offset, "word", -1);
+						end = this._getOffset(start, "wordend", +1);
+					}
+				} else {
+					if (this._doubleClickSelection) {
+						var doubleClickLine = model.getLineAtOffset(this._doubleClickSelection.start);
+						if (lineIndex >= doubleClickLine) {
+							start = model.getLineStart(doubleClickLine);
+							end = model.getLineEnd(lineIndex);
+						} else {
+							start = model.getLineStart(lineIndex);
+							end = model.getLineEnd(doubleClickLine);
+						}
+					} else {
+						start = model.getLineStart(lineIndex);
+						end = model.getLineEnd(lineIndex);
+					}
+				}
+				selection.setCaret(start);
+				selection.extend(end);
+			} 
+			this._setSelection(selection, true, true);
+			return true;
+		},
+		_setStyleSheet: function(stylesheet) {
+			var oldstylesheet = this._stylesheet;
+			if (!(oldstylesheet instanceof Array)) {
+				oldstylesheet = [oldstylesheet];
+			}
+			this._stylesheet = stylesheet;
+			if (!(stylesheet instanceof Array)) {
+				stylesheet = [stylesheet];
+			}
+			var document = this._frameDocument;
+			var documentStylesheet = document.styleSheets;
+			var head = document.getElementsByTagName("head")[0];
+			var changed = false;
+			var i = 0, sheet, oldsheet, documentSheet, ownerNode, styleNode, textNode;
+			while (i < stylesheet.length) {
+				if (i >= oldstylesheet.length) { break; }
+				sheet = stylesheet[i];
+				oldsheet = oldstylesheet[i];
+				if (sheet !== oldsheet) {
+					if (this._isLinkURL(sheet)) {
+						return true;
+					} else {
+						documentSheet = documentStylesheet[i+1];
+						ownerNode = documentSheet.ownerNode;
+						styleNode = document.createElement('STYLE');
+						textNode = document.createTextNode(sheet);
+						styleNode.appendChild(textNode);
+						head.replaceChild(styleNode, ownerNode);
+						changed = true;
+					}
+				}
+				i++;
+			}
+			if (i < oldstylesheet.length) {
+				while (i < oldstylesheet.length) {
+					sheet = oldstylesheet[i];
+					if (this._isLinkURL(sheet)) {
+						return true;
+					} else {
+						documentSheet = documentStylesheet[i+1];
+						ownerNode = documentSheet.ownerNode;
+						head.removeChild(ownerNode);
+						changed = true;
+					}
+					i++;
+				}
+			} else {
+				while (i < stylesheet.length) {
+					sheet = stylesheet[i];
+					if (this._isLinkURL(sheet)) {
+						return true;
+					} else {
+						styleNode = document.createElement('STYLE');
+						textNode = document.createTextNode(sheet);
+						styleNode.appendChild(textNode);
+						head.appendChild(styleNode);
+						changed = true;
+					}
+					i++;
+				}
+			}
+			if (changed) {
+				this._updateStyle();
+			}
+			return false;
+		},
+		_setFullSelection: function(fullSelection, init) {
+			this._fullSelection = fullSelection;
+			
+			/* 
+			* Bug in IE 8. For some reason, during scrolling IE does not reflow the elements
+			* that are used to compute the location for the selection divs. This causes the
+			* divs to be placed at the wrong location. The fix is to disabled full selection for IE8.
+			*/
+			if (isIE < 9) {
+				this._fullSelection = false;
+			}
+			if (isWebkit) {
+				this._fullSelection = true;
+			}
+			var parent = this._clipDiv || this._scrollDiv;
+			if (!parent) {
+				return;
+			}
+			if (!isPad && !this._fullSelection) {
+				if (this._selDiv1) {
+					parent.removeChild(this._selDiv1);
+					this._selDiv1 = null;
+				}
+				if (this._selDiv2) {
+					parent.removeChild(this._selDiv2);
+					this._selDiv2 = null;
+				}
+				if (this._selDiv3) {
+					parent.removeChild(this._selDiv3);
+					this._selDiv3 = null;
+				}
+				return;
+			}
+			
+			if (!this._selDiv1 && (isPad || (this._fullSelection && !isWebkit))) {
+				var frameDocument = this._frameDocument;
+				this._hightlightRGB = "Highlight";
+				var selDiv1 = frameDocument.createElement("DIV");
+				this._selDiv1 = selDiv1;
+				selDiv1.id = "selDiv1";
+				selDiv1.style.position = this._clipDiv ? "absolute" : "fixed";
+				selDiv1.style.borderWidth = "0px";
+				selDiv1.style.margin = "0px";
+				selDiv1.style.padding = "0px";
+				selDiv1.style.outline = "none";
+				selDiv1.style.background = this._hightlightRGB;
+				selDiv1.style.width = "0px";
+				selDiv1.style.height = "0px";
+				selDiv1.style.zIndex = "0";
+				parent.appendChild(selDiv1);
+				var selDiv2 = frameDocument.createElement("DIV");
+				this._selDiv2 = selDiv2;
+				selDiv2.id = "selDiv2";
+				selDiv2.style.position = this._clipDiv ? "absolute" : "fixed";
+				selDiv2.style.borderWidth = "0px";
+				selDiv2.style.margin = "0px";
+				selDiv2.style.padding = "0px";
+				selDiv2.style.outline = "none";
+				selDiv2.style.background = this._hightlightRGB;
+				selDiv2.style.width = "0px";
+				selDiv2.style.height = "0px";
+				selDiv2.style.zIndex = "0";
+				parent.appendChild(selDiv2);
+				var selDiv3 = frameDocument.createElement("DIV");
+				this._selDiv3 = selDiv3;
+				selDiv3.id = "selDiv3";
+				selDiv3.style.position = this._clipDiv ? "absolute" : "fixed";
+				selDiv3.style.borderWidth = "0px";
+				selDiv3.style.margin = "0px";
+				selDiv3.style.padding = "0px";
+				selDiv3.style.outline = "none";
+				selDiv3.style.background = this._hightlightRGB;
+				selDiv3.style.width = "0px";
+				selDiv3.style.height = "0px";
+				selDiv3.style.zIndex = "0";
+				parent.appendChild(selDiv3);
+				
+				/*
+				* Bug in Firefox. The Highlight color is mapped to list selection
+				* background instead of the text selection background.  The fix
+				* is to map known colors using a table or fallback to light blue.
+				*/
+				if (isFirefox && isMac) {
+					var style = this._frameWindow.getComputedStyle(selDiv3, null);
+					var rgb = style.getPropertyValue("background-color");
+					switch (rgb) {
+						case "rgb(119, 141, 168)": rgb = "rgb(199, 208, 218)"; break;
+						case "rgb(127, 127, 127)": rgb = "rgb(198, 198, 198)"; break;
+						case "rgb(255, 193, 31)": rgb = "rgb(250, 236, 115)"; break;
+						case "rgb(243, 70, 72)": rgb = "rgb(255, 176, 139)"; break;
+						case "rgb(255, 138, 34)": rgb = "rgb(255, 209, 129)"; break;
+						case "rgb(102, 197, 71)": rgb = "rgb(194, 249, 144)"; break;
+						case "rgb(140, 78, 184)": rgb = "rgb(232, 184, 255)"; break;
+						default: rgb = "rgb(180, 213, 255)"; break;
+					}
+					this._hightlightRGB = rgb;
+					selDiv1.style.background = rgb;
+					selDiv2.style.background = rgb;
+					selDiv3.style.background = rgb;
+					if (!this._insertedSelRule) {
+						var styleSheet = frameDocument.styleSheets[0];
+						styleSheet.insertRule("::-moz-selection {background: " + rgb + "; }", 0);
+						this._insertedSelRule = true;
+					}
+				}
+				if (!init) {
+					this._updateDOMSelection();
+				}
+			}
+		},
+		_setTabSize: function (tabSize, init) {
+			this._tabSize = tabSize;
+			this._customTabSize = undefined;
+			var clientDiv = this._clientDiv;
+			if (isOpera) {
+				if (clientDiv) { clientDiv.style.OTabSize = this._tabSize+""; }
+			} else if (isFirefox >= 4) {
+				if (clientDiv) {  clientDiv.style.MozTabSize = this._tabSize+""; }
+			} else if (this._tabSize !== 8) {
+				this._customTabSize = this._tabSize;
+				if (!init) {
+					this.redrawLines();
+				}
+			}
+		},
+		_setThemeClass: function (themeClass, init) {
+			this._themeClass = themeClass;
+			var document = this._frameDocument;
+			if (document) {
+				var viewContainerClass = "viewContainer";
+				if (this._themeClass) { viewContainerClass += " " + this._themeClass; }
+				document.body.className = viewContainerClass;
+				if (!init) {
+					this._updateStyle();
+				}
+			}
+		},
+		_showCaret: function (allSelection, pageScroll) {
+			if (!this._clientDiv) { return; }
+			var model = this._model;
+			var selection = this._getSelection();
+			var scroll = this._getScroll();
+			var caret = selection.getCaret();
+			var start = selection.start;
+			var end = selection.end;
+			var startLine = model.getLineAtOffset(start); 
+			var endLine = model.getLineAtOffset(end);
+			var endInclusive = Math.max(Math.max(start, model.getLineStart(endLine)), end - 1);
+			var viewPad = this._getViewPadding();
+			
+			var clientWidth = this._getClientWidth();
+			var leftEdge = viewPad.left;
+			var rightEdge = viewPad.left + clientWidth;
+			var bounds = this._getBoundsAtOffset(caret === start ? start : endInclusive);
+			var left = bounds.left;
+			var right = bounds.right;
+			var minScroll = clientWidth / 4;
+			if (allSelection && !selection.isEmpty() && startLine === endLine) {
+				bounds = this._getBoundsAtOffset(caret === end ? start : endInclusive);
+				var selectionWidth = caret === start ? bounds.right - left : right - bounds.left;
+				if ((clientWidth - minScroll) > selectionWidth) {
+					if (left > bounds.left) { left = bounds.left; }
+					if (right < bounds.right) { right = bounds.right; }
+				}
+			}
+			var viewRect = this._viewDiv.getBoundingClientRect(); 
+			left -= viewRect.left;
+			right -= viewRect.left;
+			var pixelX = 0;
+			if (left < leftEdge) {
+				pixelX = Math.min(left - leftEdge, -minScroll);
+			}
+			if (right > rightEdge) {
+				var maxScroll = this._scrollDiv.scrollWidth - scroll.x - clientWidth;
+				pixelX = Math.min(maxScroll,  Math.max(right - rightEdge, minScroll));
+			}
+
+			var pixelY = 0;
+			var topIndex = this._getTopIndex(true);
+			var bottomIndex = this._getBottomIndex(true);
+			var caretLine = model.getLineAtOffset(caret);
+			var clientHeight = this._getClientHeight();
+			if (!(topIndex <= caretLine && caretLine <= bottomIndex)) {
+				var lineHeight = this._getLineHeight();
+				var selectionHeight = allSelection ? (endLine - startLine) * lineHeight : 0;
+				pixelY = caretLine * lineHeight;
+				pixelY -= scroll.y;
+				if (pixelY + lineHeight > clientHeight) {
+					pixelY -= clientHeight - lineHeight;
+					if (caret === start && start !== end) {
+						pixelY += Math.min(clientHeight - lineHeight, selectionHeight);
+					}
+				} else {
+					if (caret === end) {
+						pixelY -= Math.min (clientHeight - lineHeight, selectionHeight);
+					}
+				}
+				if (pageScroll) {
+					if (pageScroll > 0) {
+						if (pixelY > 0) {
+							pixelY = Math.max(pixelY, pageScroll);
+						}
+					} else {
+						if (pixelY < 0) {
+							pixelY = Math.min(pixelY, pageScroll);
+						}
+					}
+				}
+			}
+
+			if (pixelX !== 0 || pixelY !== 0) {
+				this._scrollView (pixelX, pixelY);
+				/*
+				* When the view scrolls it is possible that one of the scrollbars can show over the caret.
+				* Depending on the browser scrolling can be synchronous (Safari), in which case the change 
+				* can be detected before showCaret() returns. When scrolling is asynchronous (most browsers), 
+				* the detection is done during the next update page.
+				*/
+				if (clientHeight !== this._getClientHeight() || clientWidth !== this._getClientWidth()) {
+					this._showCaret();
+				} else {
+					this._ensureCaretVisible = true;
+				}
+				return true;
+			}
+			return false;
+		},
+		_startIME: function () {
+			if (this._imeOffset !== -1) { return; }
+			var selection = this._getSelection();
+			if (!selection.isEmpty()) {
+				this._modifyContent({text: "", start: selection.start, end: selection.end}, true);
+			}
+			this._imeOffset = selection.start;
+		},
+		_unhookEvents: function() {
+			this._model.removeEventListener("Changing", this._modelListener.onChanging);
+			this._model.removeEventListener("Changed", this._modelListener.onChanged);
+			this._modelListener = null;
+			for (var i=0; i lastNode.lineIndex) {
+				topNode = lastNode;
+				topOffset = 0;
+			} else {
+				topNode = this._getLineNode(startLine);
+				topOffset = selection.start - model.getLineStart(startLine);
+			}
+
+			if (endLine < firstNode.lineIndex) {
+				bottomNode = firstNode;
+				bottomOffset = 0;
+			} else if (endLine > lastNode.lineIndex) {
+				bottomNode = lastNode;
+				bottomOffset = 0;
+			} else {
+				bottomNode = this._getLineNode(endLine);
+				bottomOffset = selection.end - model.getLineStart(endLine);
+			}
+			this._setDOMSelection(topNode, topOffset, bottomNode, bottomOffset);
+		},
+		_updatePage: function(hScrollOnly) {
+			if (this._redrawCount > 0) { return; }
+			if (this._updateTimer) {
+				clearTimeout(this._updateTimer);
+				this._updateTimer = null;
+				hScrollOnly = false;
+			}
+			var clientDiv = this._clientDiv;
+			if (!clientDiv) { return; }
+			var model = this._model;
+			var scroll = this._getScroll();
+			var viewPad = this._getViewPadding();
+			var lineCount = model.getLineCount();
+			var lineHeight = this._getLineHeight();
+			var firstLine = Math.max(0, scroll.y) / lineHeight;
+			var topIndex = Math.floor(firstLine);
+			var lineStart = Math.max(0, topIndex - 1);
+			var top = Math.round((firstLine - lineStart) * lineHeight);
+			var partialY = this._partialY = Math.round((firstLine - topIndex) * lineHeight);
+			var scrollWidth, scrollHeight = lineCount * lineHeight;
+			var leftWidth, clientWidth, clientHeight;
+			if (hScrollOnly) {
+				clientWidth = this._getClientWidth();
+				clientHeight = this._getClientHeight();
+				leftWidth = this._leftDiv ? this._leftDiv.scrollWidth : 0;
+				scrollWidth = Math.max(this._maxLineWidth, clientWidth);
+			} else {
+				var document = this._frameDocument;
+				var frameWidth = this._getFrameWidth();
+				var frameHeight = this._getFrameHeight();
+				document.body.style.width = frameWidth + "px";
+				document.body.style.height = frameHeight + "px";
+
+				/* Update view height in order to have client height computed */
+				var viewDiv = this._viewDiv;
+				viewDiv.style.height = Math.max(0, (frameHeight - viewPad.top - viewPad.bottom)) + "px";
+				clientHeight = this._getClientHeight();
+				var linesPerPage = Math.floor((clientHeight + partialY) / lineHeight);
+				var bottomIndex = Math.min(topIndex + linesPerPage, lineCount - 1);
+				var lineEnd = Math.min(bottomIndex + 1, lineCount - 1);
+				
+				var lineIndex, lineWidth;
+				var child = clientDiv.firstChild;
+				while (child) {
+					lineIndex = child.lineIndex;
+					var nextChild = child.nextSibling;
+					if (!(lineStart <= lineIndex && lineIndex <= lineEnd) || child.lineRemoved || child.lineIndex === -1) {
+						if (this._mouseWheelLine === child) {
+							child.style.display = "none";
+							child.lineIndex = -1;
+						} else {
+							clientDiv.removeChild(child);
+						}
+					}
+					child = nextChild;
+				}
+	
+				child = this._getLineNext();
+				var frag = document.createDocumentFragment();
+				for (lineIndex=lineStart; lineIndex<=lineEnd; lineIndex++) {
+					if (!child || child.lineIndex > lineIndex) {
+						this._createLine(frag, null, document, lineIndex, model);
+					} else {
+						if (frag.firstChild) {
+							clientDiv.insertBefore(frag, child);
+							frag = document.createDocumentFragment();
+						}
+						if (child && child.lineChanged) {
+							child = this._createLine(frag, child, document, lineIndex, model);
+							child.lineChanged = false;
+						}
+						child = this._getLineNext(child);
+					}
+				}
+				if (frag.firstChild) { clientDiv.insertBefore(frag, child); }
+	
+				/*
+				* Feature in WekKit. Webkit limits the width of the lines
+				* computed below to the width of the client div.  This causes
+				* the lines to be wrapped even though "pre" is set.  The fix
+				* is to set the width of the client div to a larger number
+				* before computing the lines width.  Note that this value is
+				* reset to the appropriate value further down.
+				*/ 
+				if (isWebkit) {
+					clientDiv.style.width = (0x7FFFF).toString() + "px";
+				}
+	
+				var rect;
+				child = this._getLineNext();
+				while (child) {
+					lineWidth = child.lineWidth;
+					if (lineWidth === undefined) {
+						rect = this._getLineBoundingClientRect(child);
+						lineWidth = child.lineWidth = rect.right - rect.left;
+					}
+					if (lineWidth >= this._maxLineWidth) {
+						this._maxLineWidth = lineWidth;
+						this._maxLineIndex = child.lineIndex;
+					}
+					if (child.lineIndex === topIndex) { this._topChild = child; }
+					if (child.lineIndex === bottomIndex) { this._bottomChild = child; }
+					if (this._checkMaxLineIndex === child.lineIndex) { this._checkMaxLineIndex = -1; }
+					child = this._getLineNext(child);
+				}
+				if (this._checkMaxLineIndex !== -1) {
+					lineIndex = this._checkMaxLineIndex;
+					this._checkMaxLineIndex = -1;
+					if (0 <= lineIndex && lineIndex < lineCount) {
+						var dummy = this._createLine(clientDiv, null, document, lineIndex, model);
+						rect = this._getLineBoundingClientRect(dummy);
+						lineWidth = rect.right - rect.left;
+						if (lineWidth >= this._maxLineWidth) {
+							this._maxLineWidth = lineWidth;
+							this._maxLineIndex = lineIndex;
+						}
+						clientDiv.removeChild(dummy);
+					}
+				}
+	
+				// Update rulers
+				this._updateRuler(this._leftDiv, topIndex, bottomIndex);
+				this._updateRuler(this._rightDiv, topIndex, bottomIndex);
+				
+				leftWidth = this._leftDiv ? this._leftDiv.scrollWidth : 0;
+				var rightWidth = this._rightDiv ? this._rightDiv.scrollWidth : 0;
+				viewDiv.style.left = leftWidth + "px";
+				viewDiv.style.width = Math.max(0, frameWidth - leftWidth - rightWidth - viewPad.left - viewPad.right) + "px";
+				if (this._rightDiv) {
+					this._rightDiv.style.left = (frameWidth - rightWidth) + "px"; 
+				}
+				/* Need to set the height first in order for the width to consider the vertical scrollbar */
+				var scrollDiv = this._scrollDiv;
+				scrollDiv.style.height = scrollHeight + "px";
+				/*
+				* TODO if frameHeightWithoutHScrollbar < scrollHeight  < frameHeightWithHScrollbar and the horizontal bar is visible, 
+				* then the clientWidth is wrong because the vertical scrollbar is showing. To correct code should hide both scrollbars 
+				* at this point.
+				*/
+				clientWidth = this._getClientWidth();
+				var width = Math.max(this._maxLineWidth, clientWidth);
+				/*
+				* Except by IE 8 and earlier, all other browsers are not allocating enough space for the right padding 
+				* in the scrollbar. It is possible this a bug since all other paddings are considered.
+				*/
+				scrollWidth = width;
+				if (!isIE || isIE >= 9) { width += viewPad.right; }
+				scrollDiv.style.width = width + "px";
+				if (this._clipScrollDiv) {
+					this._clipScrollDiv.style.width = width + "px";
+				}
+				/* Get the left scroll after setting the width of the scrollDiv as this can change the horizontal scroll offset. */
+				scroll = this._getScroll();
+				var rulerHeight = clientHeight + viewPad.top + viewPad.bottom;
+				this._updateRulerSize(this._leftDiv, rulerHeight);
+				this._updateRulerSize(this._rightDiv, rulerHeight);
+			}
+			var left = scroll.x;	
+			var clipDiv = this._clipDiv;
+			var overlayDiv = this._overlayDiv;
+			var clipLeft, clipTop;
+			if (clipDiv) {
+				clipDiv.scrollLeft = left;			
+				clipLeft = leftWidth + viewPad.left;
+				clipTop = viewPad.top;
+				var clipWidth = clientWidth;
+				var clipHeight = clientHeight;
+				var clientLeft = 0, clientTop = -top;
+				if (scroll.x === 0) {
+					clipLeft -= viewPad.left;
+					clipWidth += viewPad.left;
+					clientLeft = viewPad.left;
+				} 
+				if (scroll.x + clientWidth === scrollWidth) {
+					clipWidth += viewPad.right;
+				}
+				if (scroll.y === 0) {
+					clipTop -= viewPad.top;
+					clipHeight += viewPad.top;
+					clientTop += viewPad.top;
+				}
+				if (scroll.y + clientHeight === scrollHeight) { 
+					clipHeight += viewPad.bottom; 
+				}
+				clipDiv.style.left = clipLeft + "px";
+				clipDiv.style.top = clipTop + "px";
+				clipDiv.style.width = clipWidth + "px";
+				clipDiv.style.height = clipHeight + "px";
+				clientDiv.style.left = clientLeft + "px";
+				clientDiv.style.top = clientTop + "px";
+				clientDiv.style.width = scrollWidth + "px";
+				clientDiv.style.height = (clientHeight + top) + "px";
+				if (overlayDiv) {
+					overlayDiv.style.left = clientDiv.style.left;
+					overlayDiv.style.top = clientDiv.style.top;
+					overlayDiv.style.width = clientDiv.style.width;
+					overlayDiv.style.height = clientDiv.style.height;
+				}
+			} else {
+				clipLeft = left;
+				clipTop = top;
+				var clipRight = left + clientWidth;
+				var clipBottom = top + clientHeight;
+				if (clipLeft === 0) { clipLeft -= viewPad.left; }
+				if (clipTop === 0) { clipTop -= viewPad.top; }
+				if (clipRight === scrollWidth) { clipRight += viewPad.right; }
+				if (scroll.y + clientHeight === scrollHeight) { clipBottom += viewPad.bottom; }
+				clientDiv.style.clip = "rect(" + clipTop + "px," + clipRight + "px," + clipBottom + "px," + clipLeft + "px)";
+				clientDiv.style.left = (-left + leftWidth + viewPad.left) + "px";
+				clientDiv.style.width = (isWebkit ? scrollWidth : clientWidth + left) + "px";
+				if (!hScrollOnly) {
+					clientDiv.style.top = (-top + viewPad.top) + "px";
+					clientDiv.style.height = (clientHeight + top) + "px";
+				}
+				if (overlayDiv) {
+					overlayDiv.style.clip = clientDiv.style.clip;
+					overlayDiv.style.left = clientDiv.style.left;
+					overlayDiv.style.width = clientDiv.style.width;
+					if (!hScrollOnly) {
+						overlayDiv.style.top = clientDiv.style.top;
+						overlayDiv.style.height = clientDiv.style.height;
+					}
+				}
+			}
+			this._updateDOMSelection();
+
+			/*
+			* If the client height changed during the update page it means that scrollbar has either been shown or hidden.
+			* When this happens update page has to run again to ensure that the top and bottom lines div are correct.
+			* 
+			* Note: On IE, updateDOMSelection() has to be called before getting the new client height because it
+			* forces the client area to be recomputed.
+			*/
+			var ensureCaretVisible = this._ensureCaretVisible;
+			this._ensureCaretVisible = false;
+			if (clientHeight !== this._getClientHeight()) {
+				this._updatePage();
+				if (ensureCaretVisible) {
+					this._showCaret();
+				}
+			}
+			if (isPad) {
+				var self = this;
+				setTimeout(function() {self._resizeTouchDiv();}, 0);
+			}
+		},
+		_updateRulerSize: function (divRuler, rulerHeight) {
+			if (!divRuler) { return; }
+			var partialY = this._partialY;
+			var lineHeight = this._getLineHeight();
+			var cells = divRuler.firstChild.rows[0].cells;
+			for (var i = 0; i < cells.length; i++) {
+				var div = cells[i].firstChild;
+				var offset = lineHeight;
+				if (div._ruler.getOverview() === "page") { offset += partialY; }
+				div.style.top = -offset + "px";
+				div.style.height = (rulerHeight + offset) + "px";
+				div = div.nextSibling;
+			}
+			divRuler.style.height = rulerHeight + "px";
+		},
+		_updateRuler: function (divRuler, topIndex, bottomIndex) {
+			if (!divRuler) { return; }
+			var cells = divRuler.firstChild.rows[0].cells;
+			var lineHeight = this._getLineHeight();
+			var parentDocument = this._frameDocument;
+			var viewPad = this._getViewPadding();
+			for (var i = 0; i < cells.length; i++) {
+				var div = cells[i].firstChild;
+				var ruler = div._ruler;
+				if (div.rulerChanged) {
+					this._applyStyle(ruler.getRulerStyle(), div);
+				}
+				
+				var widthDiv;
+				var child = div.firstChild;
+				if (child) {
+					widthDiv = child;
+					child = child.nextSibling;
+				} else {
+					widthDiv = parentDocument.createElement("DIV");
+					widthDiv.style.visibility = "hidden";
+					div.appendChild(widthDiv);
+				}
+				var lineIndex, annotation;
+				if (div.rulerChanged) {
+					if (widthDiv) {
+						lineIndex = -1;
+						annotation = ruler.getWidestAnnotation();
+						if (annotation) {
+							this._applyStyle(annotation.style, widthDiv);
+							if (annotation.html) {
+								widthDiv.innerHTML = annotation.html;
+							}
+						}
+						widthDiv.lineIndex = lineIndex;
+						widthDiv.style.height = (lineHeight + viewPad.top) + "px";
+					}
+				}
+
+				var overview = ruler.getOverview(), lineDiv, frag, annotations;
+				if (overview === "page") {
+					annotations = ruler.getAnnotations(topIndex, bottomIndex + 1);
+					while (child) {
+						lineIndex = child.lineIndex;
+						var nextChild = child.nextSibling;
+						if (!(topIndex <= lineIndex && lineIndex <= bottomIndex) || child.lineChanged) {
+							div.removeChild(child);
+						}
+						child = nextChild;
+					}
+					child = div.firstChild.nextSibling;
+					frag = parentDocument.createDocumentFragment();
+					for (lineIndex=topIndex; lineIndex<=bottomIndex; lineIndex++) {
+						if (!child || child.lineIndex > lineIndex) {
+							lineDiv = parentDocument.createElement("DIV");
+							annotation = annotations[lineIndex];
+							if (annotation) {
+								this._applyStyle(annotation.style, lineDiv);
+								if (annotation.html) {
+									lineDiv.innerHTML = annotation.html;
+								}
+								lineDiv.annotation = annotation;
+							}
+							lineDiv.lineIndex = lineIndex;
+							lineDiv.style.height = lineHeight + "px";
+							frag.appendChild(lineDiv);
+						} else {
+							if (frag.firstChild) {
+								div.insertBefore(frag, child);
+								frag = parentDocument.createDocumentFragment();
+							}
+							if (child) {
+								child = child.nextSibling;
+							}
+						}
+					}
+					if (frag.firstChild) { div.insertBefore(frag, child); }
+				} else {
+					var buttonHeight = isPad ? 0 : 17;
+					var clientHeight = this._getClientHeight ();
+					var lineCount = this._model.getLineCount ();
+					var contentHeight = lineHeight * lineCount;
+					var trackHeight = clientHeight + viewPad.top + viewPad.bottom - 2 * buttonHeight;
+					var divHeight;
+					if (contentHeight < trackHeight) {
+						divHeight = lineHeight;
+					} else {
+						divHeight = trackHeight / lineCount;
+					}
+					if (div.rulerChanged) {
+						var count = div.childNodes.length;
+						while (count > 1) {
+							div.removeChild(div.lastChild);
+							count--;
+						}
+						annotations = ruler.getAnnotations(0, lineCount);
+						frag = parentDocument.createDocumentFragment();
+						for (var prop in annotations) {
+							lineIndex = prop >>> 0;
+							if (lineIndex < 0) { continue; }
+							lineDiv = parentDocument.createElement("DIV");
+							annotation = annotations[prop];
+							this._applyStyle(annotation.style, lineDiv);
+							lineDiv.style.position = "absolute";
+							lineDiv.style.top = buttonHeight + lineHeight + Math.floor(lineIndex * divHeight) + "px";
+							if (annotation.html) {
+								lineDiv.innerHTML = annotation.html;
+							}
+							lineDiv.annotation = annotation;
+							lineDiv.lineIndex = lineIndex;
+							frag.appendChild(lineDiv);
+						}
+						div.appendChild(frag);
+					} else if (div._oldTrackHeight !== trackHeight) {
+						lineDiv = div.firstChild ? div.firstChild.nextSibling : null;
+						while (lineDiv) {
+							lineDiv.style.top = buttonHeight + lineHeight + Math.floor(lineDiv.lineIndex * divHeight) + "px";
+							lineDiv = lineDiv.nextSibling;
+						}
+					}
+					div._oldTrackHeight = trackHeight;
+				}
+				div.rulerChanged = false;
+				div = div.nextSibling;
+			}
+		},
+		_updateStyle: function () {
+			var document = this._frameDocument;
+			if (isIE) {
+				document.body.style.lineHeight = "normal";
+			}
+			this._lineHeight = this._calculateLineHeight();
+			this._viewPadding = this._calculatePadding();
+			if (isIE) {
+				document.body.style.lineHeight = this._lineHeight + "px";
+			}
+			this.redraw();
+		}
+	};//end prototype
+	mEventTarget.EventTarget.addMixin(TextView.prototype);
+	
+	return {TextView: TextView};
+});
+
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made 
+ * available under the terms of the Eclipse Public License v1.0 
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 
+ * 
+ * Contributors: 
+ *		Felipe Heidrich (IBM Corporation) - initial API and implementation
+ *		Silenio Quarti (IBM Corporation) - initial API and implementation
+ ******************************************************************************/
+ 
+/*global define */
+
+define("orion/textview/textDND", [], function() {
+
+	function TextDND(view, undoStack) {
+		this._view = view;
+		this._undoStack = undoStack;
+		this._dragSelection = null;
+		this._dropOffset = -1;
+		this._dropText = null;
+		var self = this;
+		this._listener = {
+			onDragStart: function (evt) {
+				self._onDragStart(evt);
+			},
+			onDragEnd: function (evt) {
+				self._onDragEnd(evt);
+			},
+			onDragEnter: function (evt) {
+				self._onDragEnter(evt);
+			},
+			onDragOver: function (evt) {
+				self._onDragOver(evt);
+			},
+			onDrop: function (evt) {
+				self._onDrop(evt);
+			},
+			onDestroy: function (evt) {
+				self._onDestroy(evt);
+			}
+		};
+		view.addEventListener("DragStart", this._listener.onDragStart);
+		view.addEventListener("DragEnd", this._listener.onDragEnd);
+		view.addEventListener("DragEnter", this._listener.onDragEnter);
+		view.addEventListener("DragOver", this._listener.onDragOver);
+		view.addEventListener("Drop", this._listener.onDrop);
+		view.addEventListener("Destroy", this._listener.onDestroy);
+	}
+	TextDND.prototype = {
+		destroy: function() {
+			var view = this._view;
+			if (!view) { return; }
+			view.removeEventListener("DragStart", this._listener.onDragStart);
+			view.removeEventListener("DragEnd", this._listener.onDragEnd);
+			view.removeEventListener("DragEnter", this._listener.onDragEnter);
+			view.removeEventListener("DragOver", this._listener.onDragOver);
+			view.removeEventListener("Drop", this._listener.onDrop);
+			view.removeEventListener("Destroy", this._listener.onDestroy);
+			this._view = null;
+		},
+		_onDestroy: function(e) {
+			this.destroy();
+		},
+		_onDragStart: function(e) {
+			var view = this._view;
+			var selection = view.getSelection();
+			var model = view.getModel();
+			if (model.getBaseModel) {
+				selection.start = model.mapOffset(selection.start);
+				selection.end = model.mapOffset(selection.end);
+				model = model.getBaseModel();
+			}
+			var text = model.getText(selection.start, selection.end);
+			if (text) {
+				this._dragSelection = selection;
+				e.event.dataTransfer.effectAllowed = "copyMove";
+				e.event.dataTransfer.setData("Text", text);
+			}
+		},
+		_onDragEnd: function(e) {
+			var view = this._view;
+			if (this._dragSelection) {
+				if (this._undoStack) { this._undoStack.startCompoundChange(); }
+				var move = e.event.dataTransfer.dropEffect === "move";
+				if (move) {
+					view.setText("", this._dragSelection.start, this._dragSelection.end);
+				}
+				if (this._dropText) {
+					var text = this._dropText;
+					var offset = this._dropOffset;
+					if (move) {
+						if (offset >= this._dragSelection.end) {
+							offset -= this._dragSelection.end - this._dragSelection.start;
+						} else if (offset >= this._dragSelection.start) {
+							offset = this._dragSelection.start;
+						}
+					}
+					view.setText(text, offset, offset);
+					view.setSelection(offset, offset + text.length);
+					this._dropText = null;
+					this._dropOffset = -1;
+				}
+				if (this._undoStack) { this._undoStack.endCompoundChange(); }
+			}
+			this._dragSelection = null;
+		},
+		_onDragEnter: function(e) {
+			this._onDragOver(e);
+		},
+		_onDragOver: function(e) {
+			var types = e.event.dataTransfer.types;
+			if (types) {
+				var allowed = types.contains ? types.contains("text/plain") : types.indexOf("text/plain") !== -1;
+				if (!allowed) {
+					e.event.dataTransfer.dropEffect = "none";
+				}
+			}
+		},
+		_onDrop: function(e) {
+			var view = this._view;
+			var text = e.event.dataTransfer.getData("Text");
+			if (text) {
+				var offset = view.getOffsetAtLocation(e.x, e.y);
+				if (this._dragSelection) {
+					this._dropOffset = offset;
+					this._dropText = text;
+				} else {
+					view.setText(text, offset, offset);
+					view.setSelection(offset, offset + text.length);
+				}
+			}
+		}
+	};
+
+	return {TextDND: TextDND};
+});/******************************************************************************* 
+ * @license
+ * Copyright (c) 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made 
+ * available under the terms of the Eclipse Public License v1.0 
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 
+ * 
+ * Contributors: IBM Corporation - initial API and implementation 
+ ******************************************************************************/
+
+/*jslint */
+/*global define */
+
+define("orion/editor/htmlGrammar", [], function() {
+
+	/**
+	 * Provides a grammar that can do some very rough syntax highlighting for HTML.
+	 * @class orion.syntax.HtmlGrammar
+	 */
+	function HtmlGrammar() {
+		/**
+		 * Object containing the grammar rules.
+		 * @public
+		 * @type Object
+		 */
+		return {
+			"name": "HTML",
+			"scopeName": "source.html",
+			"uuid": "3B5C76FB-EBB5-D930-F40C-047D082CE99B",
+			"patterns": [
+				// TODO unicode?
+				{
+					"match": "]+>",
+					"name": "entity.name.tag.doctype.html"
+				},
+				{
+					"begin": "",
+					"beginCaptures": {
+						"0": { "name": "punctuation.definition.comment.html" }
+					},
+					"endCaptures": {
+						"0": { "name": "punctuation.definition.comment.html" }
+					},
+					"patterns": [
+						{
+							"match": "--",
+							"name": "invalid.illegal.badcomment.html"
+						}
+					],
+					"contentName": "comment.block.html"
+				},
+				{ // startDelimiter + tagName
+					"match": "<[A-Za-z0-9_\\-:]+(?= ?)",
+					"name": "entity.name.tag.html"
+				},
+				{ "include": "#attrName" },
+				{ "include": "#qString" },
+				{ "include": "#qqString" },
+				// TODO attrName, qString, qqString should be applied first while inside a tag
+				{ // startDelimiter + slash + tagName + endDelimiter
+					"match": "",
+					"name": "entity.name.tag.html"
+				},
+				{ // end delimiter of open tag
+					"match": ">", 
+					"name": "entity.name.tag.html"
+				} ],
+			"repository": {
+				"attrName": { // attribute name
+					"match": "[A-Za-z\\-:]+(?=\\s*=\\s*['\"])",
+					"name": "entity.other.attribute.name.html"
+				},
+				"qqString": { // double quoted string
+					"match": "(\")[^\"]+(\")",
+					"name": "string.quoted.double.html"
+				},
+				"qString": { // single quoted string
+					"match": "(')[^']+(\')",
+					"name": "string.quoted.single.html"
+				}
+			}
+		};
+	}
+
+	return {HtmlGrammar: HtmlGrammar};
+});
+/******************************************************************************* 
+ * @license
+ * Copyright (c) 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made 
+ * available under the terms of the Eclipse Public License v1.0 
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 
+ * 
+ * Contributors: IBM Corporation - initial API and implementation 
+ ******************************************************************************/
+
+/*jslint regexp:false laxbreak:true*/
+/*global define */
+
+define("orion/editor/textMateStyler", ['orion/editor/regex'], function(mRegex) {
+
+var RegexUtil = {
+	// Rules to detect some unsupported Oniguruma features
+	unsupported: [
+		{regex: /\(\?[ims\-]:/, func: function(match) { return "option on/off for subexp"; }},
+		{regex: /\(\?<([=!])/, func: function(match) { return (match[1] === "=") ? "lookbehind" : "negative lookbehind"; }},
+		{regex: /\(\?>/, func: function(match) { return "atomic group"; }}
+	],
+	
+	/**
+	 * @param {String} str String giving a regular expression pattern from a TextMate grammar.
+	 * @param {String} [flags] [ismg]+
+	 * @returns {RegExp}
+	 */
+	toRegExp: function(str) {
+		function fail(feature, match) {
+			throw new Error("Unsupported regex feature \"" + feature + "\": \"" + match[0] + "\" at index: "
+					+ match.index + " in " + match.input);
+		}
+		// Turns an extended regex pattern into a normal one
+		function normalize(/**String*/ str) {
+			var result = "";
+			var insideCharacterClass = false;
+			var len = str.length;
+			for (var i=0; i < len; ) {
+				var chr = str[i];
+				if (!insideCharacterClass && chr === "#") {
+					// skip to eol
+					while (i < len && chr !== "\r" && chr !== "\n") {
+						chr = str[++i];
+					}
+				} else if (!insideCharacterClass && /\s/.test(chr)) {
+					// skip whitespace
+					while (i < len && /\s/.test(chr)) { 
+						chr = str[++i];
+					}
+				} else if (chr === "\\") {
+					result += chr;
+					if (!/\s/.test(str[i+1])) {
+						result += str[i+1];
+						i += 1;
+					}
+					i += 1;
+				} else if (chr === "[") {
+					insideCharacterClass = true;
+					result += chr;
+					i += 1;
+				} else if (chr === "]") {
+					insideCharacterClass = false;
+					result += chr;
+					i += 1;
+				} else {
+					result += chr;
+					i += 1;
+				}
+			}
+			return result;
+		}
+		
+		var flags = "";
+		var i;
+		
+		// Handle global "x" flag (whitespace/comments)
+		str = RegexUtil.processGlobalFlag("x", str, function(subexp) {
+				return normalize(subexp);
+			});
+		
+		// Handle global "i" flag (case-insensitive)
+		str = RegexUtil.processGlobalFlag("i", str, function(subexp) {
+				flags += "i";
+				return subexp;
+			});
+		
+		// Check for remaining unsupported syntax
+		for (i=0; i < this.unsupported.length; i++) {
+			var match;
+			if ((match = this.unsupported[i].regex.exec(str))) {
+				fail(this.unsupported[i].func(match), match);
+			}
+		}
+		
+		return new RegExp(str, flags);
+	},
+	
+	/**
+	 * Checks if flag applies to entire pattern. If so, obtains replacement string by calling processor
+	 * on the unwrapped pattern. Handles 2 possible syntaxes: (?f)pat and (?f:pat)
+	 */
+	processGlobalFlag: function(/**String*/ flag, /**String*/ str, /**Function*/ processor) {
+		function getMatchingCloseParen(/*String*/pat, /*Number*/start) {
+			var depth = 0,
+			    len = pat.length,
+			    flagStop = -1;
+			for (var i=start; i < len && flagStop === -1; i++) {
+				switch (pat[i]) {
+					case "\\":
+						i++; // escape: skip next char
+						break;
+					case "(":
+						depth++;
+						break;
+					case ")":
+						depth--;
+						if (depth === 0) {
+							flagStop = i;
+						}
+						break;
+				}
+			}
+			return flagStop;
+		}
+		var flag1 = "(?" + flag + ")",
+		    flag2 = "(?" + flag + ":";
+		if (str.substring(0, flag1.length) === flag1) {
+			return processor(str.substring(flag1.length));
+		} else if (str.substring(0, flag2.length) === flag2) {
+			var flagStop = getMatchingCloseParen(str, 0);
+			if (flagStop < str.length-1) {
+				throw new Error("Only a " + flag2 + ") group that encloses the entire regex is supported in: " + str);
+			}
+			return processor(str.substring(flag2.length, flagStop));
+		}
+		return str;
+	},
+	
+	hasBackReference: function(/**RegExp*/ regex) {
+		return (/\\\d+/).test(regex.source);
+	},
+	
+	/** @returns {RegExp} A regex made by substituting any backreferences in regex for the value of the property
+	 * in sub with the same name as the backreferenced group number. */
+	getSubstitutedRegex: function(/**RegExp*/ regex, /**Object*/ sub, /**Boolean*/ escape) {
+		escape = (typeof escape === "undefined") ? true : false;
+		var exploded = regex.source.split(/(\\\d+)/g);
+		var array = [];
+		for (var i=0; i < exploded.length; i++) {
+			var term = exploded[i];
+			var backrefMatch = /\\(\d+)/.exec(term);
+			if (backrefMatch) {
+				var text = sub[backrefMatch[1]] || "";
+				array.push(escape ? mRegex.escape(text) : text);
+			} else {
+				array.push(term);
+			}
+		}
+		return new RegExp(array.join(""));
+	},
+	
+	/**
+	 * Builds a version of regex with every non-capturing term converted into a capturing group. This is a workaround
+	 * for JavaScript's lack of API to get the index at which a matched group begins in the input string.

+ * Using the "groupified" regex, we can sum the lengths of matches from consuming groups 1..n-1 to obtain the + * starting index of group n. (A consuming group is a capturing group that is not inside a lookahead assertion).

+ * Example: groupify(/(a+)x+(b+)/) === /(a+)(x+)(b+)/
+ * Example: groupify(/(?:x+(a+))b+/) === /(?:(x+)(a+))(b+)/ + * @param {RegExp} regex The regex to groupify. + * @param {Object} [backRefOld2NewMap] Optional. If provided, the backreference numbers in regex will be updated using the + * properties of this object rather than the new group numbers of regex itself. + *
  • [0] {RegExp} The groupified version of the input regex.
  • + *
  • [1] {Object} A map containing old-group to new-group info. Each property is a capturing group number of regex + * and its value is the corresponding capturing group number of [0].
  • + *
  • [2] {Object} A map indicating which capturing groups of [0] are also consuming groups. If a group number is found + * as a property in this object, then it's a consuming group.
+ */ + groupify: function(regex, backRefOld2NewMap) { + var NON_CAPTURING = 1, + CAPTURING = 2, + LOOKAHEAD = 3, + NEW_CAPTURING = 4; + var src = regex.source, + len = src.length; + var groups = [], + lookaheadDepth = 0, + newGroups = [], + oldGroupNumber = 1, + newGroupNumber = 1; + var result = [], + old2New = {}, + consuming = {}; + for (var i=0; i < len; i++) { + var curGroup = groups[groups.length-1]; + var chr = src[i]; + switch (chr) { + case "(": + // If we're in new capturing group, close it since ( signals end-of-term + if (curGroup === NEW_CAPTURING) { + groups.pop(); + result.push(")"); + newGroups[newGroups.length-1].end = i; + } + var peek2 = (i + 2 < len) ? (src[i+1] + "" + src[i+2]) : null; + if (peek2 === "?:" || peek2 === "?=" || peek2 === "?!") { + // Found non-capturing group or lookahead assertion. Note that we preserve non-capturing groups + // as such, but any term inside them will become a new capturing group (unless it happens to + // also be inside a lookahead). + var groupType; + if (peek2 === "?:") { + groupType = NON_CAPTURING; + } else { + groupType = LOOKAHEAD; + lookaheadDepth++; + } + groups.push(groupType); + newGroups.push({ start: i, end: -1, type: groupType /*non capturing*/ }); + result.push(chr); + result.push(peek2); + i += peek2.length; + } else { + groups.push(CAPTURING); + newGroups.push({ start: i, end: -1, type: CAPTURING, oldNum: oldGroupNumber, num: newGroupNumber }); + result.push(chr); + if (lookaheadDepth === 0) { + consuming[newGroupNumber] = null; + } + old2New[oldGroupNumber] = newGroupNumber; + oldGroupNumber++; + newGroupNumber++; + } + break; + case ")": + var group = groups.pop(); + if (group === LOOKAHEAD) { lookaheadDepth--; } + newGroups[newGroups.length-1].end = i; + result.push(chr); + break; + case "*": + case "+": + case "?": + case "}": + // Unary operator. If it's being applied to a capturing group, we need to add a new capturing group + // enclosing the pair + var op = chr; + var prev = src[i-1], + prevIndex = i-1; + if (chr === "}") { + for (var j=i-1; src[j] !== "{" && j >= 0; j--) {} + prev = src[j-1]; + prevIndex = j-1; + op = src.substring(j, i+1); + } + var lastGroup = newGroups[newGroups.length-1]; + if (prev === ")" && (lastGroup.type === CAPTURING || lastGroup.type === NEW_CAPTURING)) { + // Shove in the new group's (, increment num/start in from [lastGroup.start .. end] + result.splice(lastGroup.start, 0, "("); + result.push(op); + result.push(")"); + var newGroup = { start: lastGroup.start, end: result.length-1, type: NEW_CAPTURING, num: lastGroup.num }; + for (var k=0; k < newGroups.length; k++) { + group = newGroups[k]; + if (group.type === CAPTURING || group.type === NEW_CAPTURING) { + if (group.start >= lastGroup.start && group.end <= prevIndex) { + group.start += 1; + group.end += 1; + group.num = group.num + 1; + if (group.type === CAPTURING) { + old2New[group.oldNum] = group.num; + } + } + } + } + newGroups.push(newGroup); + newGroupNumber++; + break; + } else { + // Fallthrough to default + } + default: + if (chr !== "|" && curGroup !== CAPTURING && curGroup !== NEW_CAPTURING) { + // Not in a capturing group, so make a new one to hold this term. + // Perf improvement: don't create the new group if we're inside a lookahead, since we don't + // care about them (nothing inside a lookahead actually consumes input so we don't need it) + if (lookaheadDepth === 0) { + groups.push(NEW_CAPTURING); + newGroups.push({ start: i, end: -1, type: NEW_CAPTURING, num: newGroupNumber }); + result.push("("); + consuming[newGroupNumber] = null; + newGroupNumber++; + } + } + result.push(chr); + if (chr === "\\") { + var peek = src[i+1]; + // Eat next so following iteration doesn't think it's a real special character + result.push(peek); + i += 1; + } + break; + } + } + while (groups.length) { + // Close any remaining new capturing groups + groups.pop(); + result.push(")"); + } + var newRegex = new RegExp(result.join("")); + + // Update backreferences so they refer to the new group numbers. Use backRefOld2NewMap if provided + var subst = {}; + backRefOld2NewMap = backRefOld2NewMap || old2New; + for (var prop in backRefOld2NewMap) { + if (backRefOld2NewMap.hasOwnProperty(prop)) { + subst[prop] = "\\" + backRefOld2NewMap[prop]; + } + } + newRegex = this.getSubstitutedRegex(newRegex, subst, false); + + return [newRegex, old2New, consuming]; + }, + + /** @returns {Boolean} True if the captures object assigns scope to a matching group other than "0". */ + complexCaptures: function(capturesObj) { + if (!capturesObj) { return false; } + for (var prop in capturesObj) { + if (capturesObj.hasOwnProperty(prop)) { + if (prop !== "0") { + return true; + } + } + } + return false; + } +}; + + /** + * @name orion.editor.TextMateStyler + * @class A styler that knows how to apply a subset of the TextMate grammar format to style a line. + * + *

Styling from a grammar:

+ *

Each scope name given in the grammar is converted to an array of CSS class names. For example + * a region of text with scope keyword.control.php will be assigned the CSS classes
+ * keyword, keyword-control, keyword-control-php

+ * + *

A CSS file can give rules matching any of these class names to provide generic or more specific styling. + * For example,

+ *

.keyword { font-color: blue; }

+ *

colors all keywords blue, while

+ *

.keyword-control-php { font-weight: bold; }

+ *

bolds only PHP control keywords.

+ * + *

This is useful when using grammars that adhere to TextMate's + * scope name conventions, + * as a single CSS rule can provide consistent styling to similar constructs across different languages.

+ * + *

Top-level grammar constructs:

+ *
  • patterns, repository (with limitations, see "Other Features") are supported.
  • + *
  • scopeName, firstLineMatch, foldingStartMarker, foldingStopMarker are not supported.
  • + *
  • fileTypes is not supported. When using the Orion service registry, the "orion.edit.highlighter" + * service serves a similar purpose.
  • + *
+ * + *

Regular expression constructs:

+ *
    + *
  • match patterns are supported.
  • + *
  • begin .. end patterns are supported.
  • + *
  • The "extended" regex forms (?x) and (?x:...) are supported, but only when they + * apply to the entire regex pattern.
  • + *
  • Matching is done using native JavaScript RegExps. As a result, many features of the Oniguruma regex + * engine used by TextMate are not supported. + * Unsupported features include: + *
    • Named captures
    • + *
    • Setting flags inside subgroups (eg. (?i:a)b)
    • + *
    • Lookbehind and negative lookbehind
    • + *
    • Subexpression call
    • + *
    • etc.
    • + *
    + *
  • + *
+ * + *

Scope-assignment constructs:

+ *
    + *
  • captures, beginCaptures, endCaptures are supported.
  • + *
  • name and contentName are supported.
  • + *
+ * + *

Other features:

+ *
    + *
  • applyEndPatternLast is supported.
  • + *
  • include is supported, but only when it references a rule in the current grammar's repository. + * Including $self, $base, or rule.from.another.grammar is not supported.
  • + *
+ * + * @description Creates a new TextMateStyler. + * @extends orion.editor.AbstractStyler + * @param {orion.textview.TextView} textView The TextView to provide styling for. + * @param {Object} grammar The TextMate grammar to use for styling the TextView, as a JavaScript object. You can + * produce this object by running a PList-to-JavaScript conversion tool on a TextMate .tmLanguage file. + * @param {Object[]} [externalGrammars] Additional grammar objects that will be used to resolve named rule references. + */ + function TextMateStyler(textView, grammar, externalGrammars) { + this.initialize(textView); + // Copy grammar object(s) since we will mutate them + this.grammar = this.copy(grammar); + this.externalGrammars = externalGrammars ? this.copy(externalGrammars) : []; + + this._styles = {}; /* key: {String} scopeName, value: {String[]} cssClassNames */ + this._tree = null; + this._allGrammars = {}; /* key: {String} scopeName of grammar, value: {Object} grammar */ + this.preprocess(this.grammar); + } + TextMateStyler.prototype = /** @lends orion.editor.TextMateStyler.prototype */ { + initialize: function(textView) { + this.textView = textView; + var self = this; + this._listener = { + onModelChanged: function(e) { + self.onModelChanged(e); + }, + onDestroy: function(e) { + self.onDestroy(e); + }, + onLineStyle: function(e) { + self.onLineStyle(e); + } + }; + textView.addEventListener("ModelChanged", this._listener.onModelChanged); + textView.addEventListener("Destroy", this._listener.onDestroy); + textView.addEventListener("LineStyle", this._listener.onLineStyle); + textView.redrawLines(); + }, + onDestroy: function(/**eclipse.DestroyEvent*/ e) { + this.destroy(); + }, + destroy: function() { + if (this.textView) { + this.textView.removeEventListener("ModelChanged", this._listener.onModelChanged); + this.textView.removeEventListener("Destroy", this._listener.onDestroy); + this.textView.removeEventListener("LineStyle", this._listener.onLineStyle); + this.textView = null; + } + this.grammar = null; + this._styles = null; + this._tree = null; + this._listener = null; + }, + /** @private */ + copy: function(obj) { + return JSON.parse(JSON.stringify(obj)); + }, + /** @private */ + preprocess: function(grammar) { + var stack = [grammar]; + for (; stack.length !== 0; ) { + var rule = stack.pop(); + if (rule._resolvedRule && rule._typedRule) { + continue; + } +// console.debug("Process " + (rule.include || rule.name)); + + // Look up include'd rule, create typed *Rule instance + rule._resolvedRule = this._resolve(rule); + rule._typedRule = this._createTypedRule(rule); + + // Convert the scope names to styles and cache them for later + this.addStyles(rule.name); + this.addStyles(rule.contentName); + this.addStylesForCaptures(rule.captures); + this.addStylesForCaptures(rule.beginCaptures); + this.addStylesForCaptures(rule.endCaptures); + + if (rule._resolvedRule !== rule) { + // Add include target + stack.push(rule._resolvedRule); + } + if (rule.patterns) { + // Add subrules + for (var i=0; i < rule.patterns.length; i++) { + stack.push(rule.patterns[i]); + } + } + } + }, + + /** + * @private + * Adds eclipse.Style objects for scope to our _styles cache. + * @param {String} scope A scope name, like "constant.character.php". + */ + addStyles: function(scope) { + if (scope && !this._styles[scope]) { + this._styles[scope] = []; + var scopeArray = scope.split("."); + for (var i = 0; i < scopeArray.length; i++) { + this._styles[scope].push(scopeArray.slice(0, i + 1).join("-")); + } + } + }, + /** @private */ + addStylesForCaptures: function(/**Object*/ captures) { + for (var prop in captures) { + if (captures.hasOwnProperty(prop)) { + var scope = captures[prop].name; + this.addStyles(scope); + } + } + }, + /** + * A rule that contains subrules ("patterns" in TextMate parlance) but has no "begin" or "end". + * Also handles top level of grammar. + * @private + */ + ContainerRule: (function() { + function ContainerRule(/**Object*/ rule) { + this.rule = rule; + this.subrules = rule.patterns; + } + ContainerRule.prototype.valueOf = function() { return "aa"; }; + return ContainerRule; + }()), + /** + * A rule that is delimited by "begin" and "end" matches, which may be separated by any number of + * lines. This type of rule may contain subrules, which apply only inside the begin .. end region. + * @private + */ + BeginEndRule: (function() { + function BeginEndRule(/**Object*/ rule) { + this.rule = rule; + // TODO: the TextMate blog claims that "end" is optional. + this.beginRegex = RegexUtil.toRegExp(rule.begin); + this.endRegex = RegexUtil.toRegExp(rule.end); + this.subrules = rule.patterns || []; + + this.endRegexHasBackRef = RegexUtil.hasBackReference(this.endRegex); + + // Deal with non-0 captures + var complexCaptures = RegexUtil.complexCaptures(rule.captures); + var complexBeginEnd = RegexUtil.complexCaptures(rule.beginCaptures) || RegexUtil.complexCaptures(rule.endCaptures); + this.isComplex = complexCaptures || complexBeginEnd; + if (this.isComplex) { + var bg = RegexUtil.groupify(this.beginRegex); + this.beginRegex = bg[0]; + this.beginOld2New = bg[1]; + this.beginConsuming = bg[2]; + + var eg = RegexUtil.groupify(this.endRegex, this.beginOld2New /*Update end's backrefs to begin's new group #s*/); + this.endRegex = eg[0]; + this.endOld2New = eg[1]; + this.endConsuming = eg[2]; + } + } + BeginEndRule.prototype.valueOf = function() { return this.beginRegex; }; + return BeginEndRule; + }()), + /** + * A rule with a "match" pattern. + * @private + */ + MatchRule: (function() { + function MatchRule(/**Object*/ rule) { + this.rule = rule; + this.matchRegex = RegexUtil.toRegExp(rule.match); + this.isComplex = RegexUtil.complexCaptures(rule.captures); + if (this.isComplex) { + var mg = RegexUtil.groupify(this.matchRegex); + this.matchRegex = mg[0]; + this.matchOld2New = mg[1]; + this.matchConsuming = mg[2]; + } + } + MatchRule.prototype.valueOf = function() { return this.matchRegex; }; + return MatchRule; + }()), + /** + * @param {Object} rule A rule from the grammar. + * @returns {MatchRule|BeginEndRule|ContainerRule} + * @private + */ + _createTypedRule: function(rule) { + if (rule.match) { + return new this.MatchRule(rule); + } else if (rule.begin) { + return new this.BeginEndRule(rule); + } else { + return new this.ContainerRule(rule); + } + }, + /** + * Resolves a rule from the grammar (which may be an include) into the real rule that it points to. + * @private + */ + _resolve: function(rule) { + var resolved = rule; + if (rule.include) { + if (rule.begin || rule.end || rule.match) { + throw new Error("Unexpected regex pattern in \"include\" rule " + rule.include); + } + var name = rule.include; + if (name[0] === "#") { + resolved = this.grammar.repository && this.grammar.repository[name.substring(1)]; + if (!resolved) { throw new Error("Couldn't find included rule " + name + " in grammar repository"); } + } else if (name === "$self") { + resolved = this.grammar; + } else if (name === "$base") { + // $base is only relevant when including rules from foreign grammars + throw new Error("Include \"$base\" is not supported"); + } else { + resolved = this._allGrammars[name]; + if (!resolved) { + for (var i=0; i < this.externalGrammars.length; i++) { + var grammar = this.externalGrammars[i]; + if (grammar.scopeName === name) { + this.preprocess(grammar); + this._allGrammars[name] = grammar; + resolved = grammar; + break; + } + } + } + } + } + return resolved; + }, + /** @private */ + ContainerNode: (function() { + function ContainerNode(parent, rule) { + this.parent = parent; + this.rule = rule; + this.children = []; + + this.start = null; + this.end = null; + } + ContainerNode.prototype.addChild = function(child) { + this.children.push(child); + }; + ContainerNode.prototype.valueOf = function() { + var r = this.rule; + return "ContainerNode { " + (r.include || "") + " " + (r.name || "") + (r.comment || "") + "}"; + }; + return ContainerNode; + }()), + /** @private */ + BeginEndNode: (function() { + function BeginEndNode(parent, rule, beginMatch) { + this.parent = parent; + this.rule = rule; + this.children = []; + + this.setStart(beginMatch); + this.end = null; // will be set eventually during parsing (may be EOF) + this.endMatch = null; // may remain null if we never match our "end" pattern + + // Build a new regex if the "end" regex has backrefs since they refer to matched groups of beginMatch + if (rule.endRegexHasBackRef) { + this.endRegexSubstituted = RegexUtil.getSubstitutedRegex(rule.endRegex, beginMatch); + } else { + this.endRegexSubstituted = null; + } + } + BeginEndNode.prototype.addChild = function(child) { + this.children.push(child); + }; + /** @return {Number} This node's index in its parent's "children" list */ + BeginEndNode.prototype.getIndexInParent = function(node) { + return this.parent ? this.parent.children.indexOf(this) : -1; + }; + /** @param {RegExp.match} beginMatch */ + BeginEndNode.prototype.setStart = function(beginMatch) { + this.start = beginMatch.index; + this.beginMatch = beginMatch; + }; + /** @param {RegExp.match|Number} endMatchOrLastChar */ + BeginEndNode.prototype.setEnd = function(endMatchOrLastChar) { + if (endMatchOrLastChar && typeof(endMatchOrLastChar) === "object") { + var endMatch = endMatchOrLastChar; + this.endMatch = endMatch; + this.end = endMatch.index + endMatch[0].length; + } else { + var lastChar = endMatchOrLastChar; + this.endMatch = null; + this.end = lastChar; + } + }; + BeginEndNode.prototype.shiftStart = function(amount) { + this.start += amount; + this.beginMatch.index += amount; + }; + BeginEndNode.prototype.shiftEnd = function(amount) { + this.end += amount; + if (this.endMatch) { this.endMatch.index += amount; } + }; + BeginEndNode.prototype.valueOf = function() { + return "{" + this.rule.beginRegex + " range=" + this.start + ".." + this.end + "}"; + }; + return BeginEndNode; + }()), + /** Pushes rules onto stack such that rules[startFrom] is on top + * @private + */ + push: function(/**Array*/ stack, /**Array*/ rules) { + if (!rules) { return; } + for (var i = rules.length; i > 0; ) { + stack.push(rules[--i]); + } + }, + /** Executes regex on text, and returns the match object with its index + * offset by the given amount. + * @returns {RegExp.match} + * @private + */ + exec: function(/**RegExp*/ regex, /**String*/ text, /**Number*/ offset) { + var match = regex.exec(text); + if (match) { match.index += offset; } + regex.lastIndex = 0; // Just in case + return match; + }, + /** @returns {Number} The position immediately following the match. + * @private + */ + afterMatch: function(/**RegExp.match*/ match) { + return match.index + match[0].length; + }, + /** + * @returns {RegExp.match} If node is a BeginEndNode and its rule's "end" pattern matches the text. + * @private + */ + getEndMatch: function(/**Node*/ node, /**String*/ text, /**Number*/ offset) { + if (node instanceof this.BeginEndNode) { + var rule = node.rule; + var endRegex = node.endRegexSubstituted || rule.endRegex; + if (!endRegex) { return null; } + return this.exec(endRegex, text, offset); + } + return null; + }, + /** Called once when file is first loaded to build the parse tree. Tree is updated incrementally thereafter + * as buffer is modified. + * @private + */ + initialParse: function() { + var last = this.textView.getModel().getCharCount(); + // First time; make parse tree for whole buffer + var root = new this.ContainerNode(null, this.grammar._typedRule); + this._tree = root; + this.parse(this._tree, false, 0); + }, + onModelChanged: function(/**eclipse.ModelChangedEvent*/ e) { + var addedCharCount = e.addedCharCount, + addedLineCount = e.addedLineCount, + removedCharCount = e.removedCharCount, + removedLineCount = e.removedLineCount, + start = e.start; + if (!this._tree) { + this.initialParse(); + } else { + var model = this.textView.getModel(); + var charCount = model.getCharCount(); + + // For rs, we must rewind to the line preceding the line 'start' is on. We can't rely on start's + // line since it may've been changed in a way that would cause a new beginMatch at its lineStart. + var rs = model.getLineEnd(model.getLineAtOffset(start) - 1); // may be < 0 + var fd = this.getFirstDamaged(rs, rs); + rs = rs === -1 ? 0 : rs; + var stoppedAt; + if (fd) { + // [rs, re] is the region we need to verify. If we find the structure of the tree + // has changed in that area, then we may need to reparse the rest of the file. + stoppedAt = this.parse(fd, true, rs, start, addedCharCount, removedCharCount); + } else { + // FIXME: fd == null ? + stoppedAt = charCount; + } + this.textView.redrawRange(rs, stoppedAt); + } + }, + /** @returns {BeginEndNode|ContainerNode} The result of taking the first (smallest "start" value) + * node overlapping [start,end] and drilling down to get its deepest damaged descendant (if any). + * @private + */ + getFirstDamaged: function(start, end) { + // If start === 0 we actually have to start from the root because there is no position + // we can rely on. (First index is damaged) + if (start < 0) { + return this._tree; + } + + var nodes = [this._tree]; + var result = null; + while (nodes.length) { + var n = nodes.pop(); + if (!n.parent /*n is root*/ || this.isDamaged(n, start, end)) { + // n is damaged by the edit, so go into its children + // Note: If a node is damaged, then some of its descendents MAY be damaged + // If a node is undamaged, then ALL of its descendents are undamaged + if (n instanceof this.BeginEndNode) { + result = n; + } + // Examine children[0] last + for (var i=0; i < n.children.length; i++) { + nodes.push(n.children[i]); + } + } + } + return result || this._tree; + }, + /** @returns true If n overlaps the interval [start,end]. + * @private + */ + isDamaged: function(/**BeginEndNode*/ n, start, end) { + // Note strict > since [2,5] doesn't overlap [5,7] + return (n.start <= end && n.end > start); + }, + /** + * Builds tree from some of the buffer content + * + * TODO cleanup params + * @param {BeginEndNode|ContainerNode} origNode The deepest node that overlaps [rs,rs], or the root. + * @param {Boolean} repairing + * @param {Number} rs See _onModelChanged() + * @param {Number} [editStart] Only used for repairing === true + * @param {Number} [addedCharCount] Only used for repairing === true + * @param {Number} [removedCharCount] Only used for repairing === true + * @returns {Number} The end position that redrawRange should be called for. + * @private + */ + parse: function(origNode, repairing, rs, editStart, addedCharCount, removedCharCount) { + var model = this.textView.getModel(); + var lastLineStart = model.getLineStart(model.getLineCount() - 1); + var eof = model.getCharCount(); + var initialExpected = this.getInitialExpected(origNode, rs); + + // re is best-case stopping point; if we detect change to tree, we must continue past it + var re = -1; + if (repairing) { + origNode.repaired = true; + origNode.endNeedsUpdate = true; + var lastChild = origNode.children[origNode.children.length-1]; + var delta = addedCharCount - removedCharCount; + var lastChildLineEnd = lastChild ? model.getLineEnd(model.getLineAtOffset(lastChild.end + delta)) : -1; + var editLineEnd = model.getLineEnd(model.getLineAtOffset(editStart + removedCharCount)); + re = Math.max(lastChildLineEnd, editLineEnd); + } + re = (re === -1) ? eof : re; + + var expected = initialExpected; + var node = origNode; + var matchedChildOrEnd = false; + var pos = rs; + var redrawEnd = -1; + while (node && (!repairing || (pos < re))) { + var matchInfo = this.getNextMatch(model, node, pos); + if (!matchInfo) { + // Go to next line, if any + pos = (pos >= lastLineStart) ? eof : model.getLineStart(model.getLineAtOffset(pos) + 1); + } + var match = matchInfo && matchInfo.match, + rule = matchInfo && matchInfo.rule, + isSub = matchInfo && matchInfo.isSub, + isEnd = matchInfo && matchInfo.isEnd; + if (isSub) { + pos = this.afterMatch(match); + if (rule instanceof this.BeginEndRule) { + matchedChildOrEnd = true; + // Matched a child. Did we expect that? + if (repairing && rule === expected.rule && node === expected.parent) { + // Yes: matched expected child + var foundChild = expected; + foundChild.setStart(match); + // Note: the 'end' position for this node will either be matched, or fixed up by us post-loop + foundChild.repaired = true; + foundChild.endNeedsUpdate = true; + node = foundChild; // descend + expected = this.getNextExpected(expected, "begin"); + } else { + if (repairing) { + // No: matched unexpected child. + this.prune(node, expected); + repairing = false; + } + + // Add the new child (will replace 'expected' in node's children list) + var subNode = new this.BeginEndNode(node, rule, match); + node.addChild(subNode); + node = subNode; // descend + } + } else { + // Matched a MatchRule; no changes to tree required + } + } else if (isEnd || pos === eof) { + if (node instanceof this.BeginEndNode) { + if (match) { + matchedChildOrEnd = true; + redrawEnd = Math.max(redrawEnd, node.end); // if end moved up, must still redraw to its old value + node.setEnd(match); + pos = this.afterMatch(match); + // Matched node's end. Did we expect that? + if (repairing && node === expected && node.parent === expected.parent) { + // Yes: found the expected end of node + node.repaired = true; + delete node.endNeedsUpdate; + expected = this.getNextExpected(expected, "end"); + } else { + if (repairing) { + // No: found an unexpected end + this.prune(node, expected); + repairing = false; + } + } + } else { + // Force-ending a BeginEndNode that runs until eof + node.setEnd(eof); + delete node.endNeedsUpdate; + } + } + node = node.parent; // ascend + } + + if (repairing && pos >= re && !matchedChildOrEnd) { + // Reached re without matching any begin/end => initialExpected itself was removed => repair fail + this.prune(origNode, initialExpected); + repairing = false; + } + } // end loop + // TODO: do this for every node we end? + this.removeUnrepairedChildren(origNode, repairing, rs); + + //console.debug("parsed " + (pos - rs) + " of " + model.getCharCount + "buf"); + this.cleanup(repairing, origNode, rs, re, eof, addedCharCount, removedCharCount); + if (repairing) { + return Math.max(redrawEnd, pos); + } else { + return pos; // where we stopped reparsing + } + }, + /** Helper for parse() in the repair case. To be called when ending a node, as any children that + * lie in [rs,node.end] and were not repaired must've been deleted. + * @private + */ + removeUnrepairedChildren: function(node, repairing, start) { + if (repairing) { + var children = node.children; + var removeFrom = -1; + for (var i=0; i < children.length; i++) { + var child = children[i]; + if (!child.repaired && this.isDamaged(child, start, Number.MAX_VALUE /*end doesn't matter*/)) { + removeFrom = i; + break; + } + } + if (removeFrom !== -1) { + node.children.length = removeFrom; + } + } + }, + /** Helper for parse() in the repair case + * @private + */ + cleanup: function(repairing, origNode, rs, re, eof, addedCharCount, removedCharCount) { + var i, node, maybeRepairedNodes; + if (repairing) { + // The repair succeeded, so update stale begin/end indices by simple translation. + var delta = addedCharCount - removedCharCount; + // A repaired node's end can't exceed re, but it may exceed re-delta+1. + // TODO: find a way to guarantee disjoint intervals for repaired vs unrepaired, then stop using flag + var maybeUnrepairedNodes = this.getIntersecting(re-delta+1, eof); + maybeRepairedNodes = this.getIntersecting(rs, re); + // Handle unrepaired nodes. They are those intersecting [re-delta+1, eof] that don't have the flag + for (i=0; i < maybeUnrepairedNodes.length; i++) { + node = maybeUnrepairedNodes[i]; + if (!node.repaired && node instanceof this.BeginEndNode) { + node.shiftEnd(delta); + node.shiftStart(delta); + } + } + // Translate 'end' index of repaired node whose 'end' was not matched in loop (>= re) + for (i=0; i < maybeRepairedNodes.length; i++) { + node = maybeRepairedNodes[i]; + if (node.repaired && node.endNeedsUpdate) { + node.shiftEnd(delta); + } + delete node.endNeedsUpdate; + delete node.repaired; + } + } else { + // Clean up after ourself + maybeRepairedNodes = this.getIntersecting(rs, re); + for (i=0; i < maybeRepairedNodes.length; i++) { + delete maybeRepairedNodes[i].repaired; + } + } + }, + /** + * @param model {orion.textview.TextModel} + * @param node {Node} + * @param pos {Number} + * @param [matchRulesOnly] {Boolean} Optional, if true only "match" subrules will be considered. + * @returns {Object} A match info object with properties: + * {Boolean} isEnd + * {Boolean} isSub + * {RegExp.match} match + * {(Match|BeginEnd)Rule} rule + * @private + */ + getNextMatch: function(model, node, pos, matchRulesOnly) { + var lineIndex = model.getLineAtOffset(pos); + var lineEnd = model.getLineEnd(lineIndex); + var line = model.getText(pos, lineEnd); + + var stack = [], + expandedContainers = [], + subMatches = [], + subrules = []; + this.push(stack, node.rule.subrules); + while (stack.length) { + var next = stack.length ? stack.pop() : null; + var subrule = next && next._resolvedRule._typedRule; + if (subrule instanceof this.ContainerRule && expandedContainers.indexOf(subrule) === -1) { + // Expand ContainerRule by pushing its subrules on + expandedContainers.push(subrule); + this.push(stack, subrule.subrules); + continue; + } + if (subrule && matchRulesOnly && !(subrule.matchRegex)) { + continue; + } + var subMatch = subrule && this.exec(subrule.matchRegex || subrule.beginRegex, line, pos); + if (subMatch) { + subMatches.push(subMatch); + subrules.push(subrule); + } + } + + var bestSub = Number.MAX_VALUE, + bestSubIndex = -1; + for (var i=0; i < subMatches.length; i++) { + var match = subMatches[i]; + if (match.index < bestSub) { + bestSub = match.index; + bestSubIndex = i; + } + } + + if (!matchRulesOnly) { + // See if the "end" pattern of the active begin/end node matches. + // TODO: The active begin/end node may not be the same as the node that holds the subrules + var activeBENode = node; + var endMatch = this.getEndMatch(node, line, pos); + if (endMatch) { + var doEndLast = activeBENode.rule.applyEndPatternLast; + var endWins = bestSubIndex === -1 || (endMatch.index < bestSub) || (!doEndLast && endMatch.index === bestSub); + if (endWins) { + return {isEnd: true, rule: activeBENode.rule, match: endMatch}; + } + } + } + return bestSubIndex === -1 ? null : {isSub: true, rule: subrules[bestSubIndex], match: subMatches[bestSubIndex]}; + }, + /** + * Gets the node corresponding to the first match we expect to see in the repair. + * @param {BeginEndNode|ContainerNode} node The node returned via getFirstDamaged(rs,rs) -- may be the root. + * @param {Number} rs See _onModelChanged() + * Note that because rs is a line end (or 0, a line start), it will intersect a beginMatch or + * endMatch either at their 0th character, or not at all. (begin/endMatches can't cross lines). + * This is the only time we rely on the start/end values from the pre-change tree. After this + * we only look at node ordering, never use the old indices. + * @returns {Node} + * @private + */ + getInitialExpected: function(node, rs) { + // TODO: Kind of weird.. maybe ContainerNodes should have start & end set, like BeginEndNodes + var i, child; + if (node === this._tree) { + // get whichever of our children comes after rs + for (i=0; i < node.children.length; i++) { + child = node.children[i]; // BeginEndNode + if (child.start >= rs) { + return child; + } + } + } else if (node instanceof this.BeginEndNode) { + if (node.endMatch) { + // Which comes next after rs: our nodeEnd or one of our children? + var nodeEnd = node.endMatch.index; + for (i=0; i < node.children.length; i++) { + child = node.children[i]; // BeginEndNode + if (child.start >= rs) { + break; + } + } + if (child && child.start < nodeEnd) { + return child; // Expect child as the next match + } + } else { + // No endMatch => node goes until eof => it end should be the next match + } + } + return node; // We expect node to end, so it should be the next match + }, + /** + * Helper for repair() to tell us what kind of event we expect next. + * @param {Node} expected Last value returned by this method. + * @param {String} event "begin" if the last value of expected was matched as "begin", + * or "end" if it was matched as an end. + * @returns {Node} The next expected node to match, or null. + * @private + */ + getNextExpected: function(/**Node*/ expected, event) { + var node = expected; + if (event === "begin") { + var child = node.children[0]; + if (child) { + return child; + } else { + return node; + } + } else if (event === "end") { + var parent = node.parent; + if (parent) { + var nextSibling = parent.children[parent.children.indexOf(node) + 1]; + if (nextSibling) { + return nextSibling; + } else { + return parent; + } + } + } + return null; + }, + /** Helper for parse() when repairing. Prunes out the unmatched nodes from the tree so we can continue parsing. + * @private + */ + prune: function(/**BeginEndNode|ContainerNode*/ node, /**Node*/ expected) { + var expectedAChild = expected.parent === node; + if (expectedAChild) { + // Expected child wasn't matched; prune it and all siblings after it + node.children.length = expected.getIndexInParent(); + } else if (node instanceof this.BeginEndNode) { + // Expected node to end but it didn't; set its end unknown and we'll match it eventually + node.endMatch = null; + node.end = null; + } + // Reparsing from node, so prune the successors outside of node's subtree + if (node.parent) { + node.parent.children.length = node.getIndexInParent() + 1; + } + }, + onLineStyle: function(/**eclipse.LineStyleEvent*/ e) { + function byStart(r1, r2) { + return r1.start - r2.start; + } + + if (!this._tree) { + // In some cases it seems onLineStyle is called before onModelChanged, so we need to parse here + this.initialParse(); + } + var lineStart = e.lineStart, + model = this.textView.getModel(), + lineEnd = model.getLineEnd(e.lineIndex); + + var rs = model.getLineEnd(model.getLineAtOffset(lineStart) - 1); // may be < 0 + var node = this.getFirstDamaged(rs, rs); + + var scopes = this.getLineScope(model, node, lineStart, lineEnd); + e.ranges = this.toStyleRanges(scopes); + // Editor requires StyleRanges must be in ascending order by 'start', or else some will be ignored + e.ranges.sort(byStart); + }, + /** Runs parse algorithm on [start, end] in the context of node, assigning scope as we find matches. + * @private + */ + getLineScope: function(model, node, start, end) { + var pos = start; + var expected = this.getInitialExpected(node, start); + var scopes = [], + gaps = []; + while (node && (pos < end)) { + var matchInfo = this.getNextMatch(model, node, pos); + if (!matchInfo) { + break; // line is over + } + var match = matchInfo && matchInfo.match, + rule = matchInfo && matchInfo.rule, + isSub = matchInfo && matchInfo.isSub, + isEnd = matchInfo && matchInfo.isEnd; + if (match.index !== pos) { + // gap [pos..match.index] + gaps.push({ start: pos, end: match.index, node: node}); + } + if (isSub) { + pos = this.afterMatch(match); + if (rule instanceof this.BeginEndRule) { + // Matched a "begin", assign its scope and descend into it + this.addBeginScope(scopes, match, rule); + node = expected; // descend + expected = this.getNextExpected(expected, "begin"); + } else { + // Matched a child MatchRule; + this.addMatchScope(scopes, match, rule); + } + } else if (isEnd) { + pos = this.afterMatch(match); + // Matched and "end", assign its end scope and go up + this.addEndScope(scopes, match, rule); + expected = this.getNextExpected(expected, "end"); + node = node.parent; // ascend + } + } + if (pos < end) { + gaps.push({ start: pos, end: end, node: node }); + } + var inherited = this.getInheritedLineScope(gaps, start, end); + return scopes.concat(inherited); + }, + /** @private */ + getInheritedLineScope: function(gaps, start, end) { + var scopes = []; + for (var i=0; i < gaps.length; i++) { + var gap = gaps[i]; + var node = gap.node; + while (node) { + // if node defines a contentName or name, apply it + var rule = node.rule.rule; + var name = rule.name, + contentName = rule.contentName; + // TODO: if both are given, we don't resolve the conflict. contentName always wins + var scope = contentName || name; + if (scope) { + this.addScopeRange(scopes, gap.start, gap.end, scope); + break; + } + node = node.parent; + } + } + return scopes; + }, + /** @private */ + addBeginScope: function(scopes, match, typedRule) { + var rule = typedRule.rule; + this.addCapturesScope(scopes, match, (rule.beginCaptures || rule.captures), typedRule.isComplex, typedRule.beginOld2New, typedRule.beginConsuming); + }, + /** @private */ + addEndScope: function(scopes, match, typedRule) { + var rule = typedRule.rule; + this.addCapturesScope(scopes, match, (rule.endCaptures || rule.captures), typedRule.isComplex, typedRule.endOld2New, typedRule.endConsuming); + }, + /** @private */ + addMatchScope: function(scopes, match, typedRule) { + var rule = typedRule.rule, + name = rule.name, + captures = rule.captures; + if (captures) { + // captures takes priority over name + this.addCapturesScope(scopes, match, captures, typedRule.isComplex, typedRule.matchOld2New, typedRule.matchConsuming); + } else { + this.addScope(scopes, match, name); + } + }, + /** @private */ + addScope: function(scopes, match, name) { + if (!name) { return; } + scopes.push({start: match.index, end: this.afterMatch(match), scope: name }); + }, + /** @private */ + addScopeRange: function(scopes, start, end, name) { + if (!name) { return; } + scopes.push({start: start, end: end, scope: name }); + }, + /** @private */ + addCapturesScope: function(/**Array*/scopes, /*RegExp.match*/ match, /**Object*/captures, /**Boolean*/isComplex, /**Object*/old2New, /**Object*/consuming) { + if (!captures) { return; } + if (!isComplex) { + this.addScope(scopes, match, captures[0] && captures[0].name); + } else { + // apply scopes captures[1..n] to matching groups [1]..[n] of match + + // Sum up the lengths of preceding consuming groups to get the start offset for each matched group. + var newGroupStarts = {1: 0}; + var sum = 0; + for (var num = 1; match[num] !== undefined; num++) { + if (consuming[num] !== undefined) { + sum += match[num].length; + } + if (match[num+1] !== undefined) { + newGroupStarts[num + 1] = sum; + } + } + // Map the group numbers referred to in captures object to the new group numbers, and get the actual matched range. + var start = match.index; + for (var oldGroupNum = 1; captures[oldGroupNum]; oldGroupNum++) { + var scope = captures[oldGroupNum].name; + var newGroupNum = old2New[oldGroupNum]; + var groupStart = start + newGroupStarts[newGroupNum]; + // Not every capturing group defined in regex need match every time the regex is run. + // eg. (a)|b matches "b" but group 1 is undefined + if (typeof match[newGroupNum] !== "undefined") { + var groupEnd = groupStart + match[newGroupNum].length; + this.addScopeRange(scopes, groupStart, groupEnd, scope); + } + } + } + }, + /** @returns {Node[]} In depth-first order + * @private + */ + getIntersecting: function(start, end) { + var result = []; + var nodes = this._tree ? [this._tree] : []; + while (nodes.length) { + var n = nodes.pop(); + var visitChildren = false; + if (n instanceof this.ContainerNode) { + visitChildren = true; + } else if (this.isDamaged(n, start, end)) { + visitChildren = true; + result.push(n); + } + if (visitChildren) { + var len = n.children.length; +// for (var i=len-1; i >= 0; i--) { +// nodes.push(n.children[i]); +// } + for (var i=0; i < len; i++) { + nodes.push(n.children[i]); + } + } + } + return result.reverse(); + }, + /** + * Applies the grammar to obtain the {@link eclipse.StyleRange[]} for the given line. + * @returns eclipse.StyleRange[] + * @private + */ + toStyleRanges: function(/**ScopeRange[]*/ scopeRanges) { + var styleRanges = []; + for (var i=0; i < scopeRanges.length; i++) { + var scopeRange = scopeRanges[i]; + var classNames = this._styles[scopeRange.scope]; + if (!classNames) { throw new Error("styles not found for " + scopeRange.scope); } + var classNamesString = classNames.join(" "); + styleRanges.push({start: scopeRange.start, end: scopeRange.end, style: {styleClass: classNamesString}}); +// console.debug("{start " + styleRanges[i].start + ", end " + styleRanges[i].end + ", style: " + styleRanges[i].style.styleClass + "}"); + } + return styleRanges; + } + }; + + return { + RegexUtil: RegexUtil, + TextMateStyler: TextMateStyler + }; +}); +/******************************************************************************* + * @license + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: IBM Corporation - initial API and implementation + * Alex Lakatos - fix for bug#369781 + ******************************************************************************/ + +/*global document window navigator define */ + +define("examples/textview/textStyler", ['orion/textview/annotations'], function(mAnnotations) { + + var JS_KEYWORDS = + ["break", + "case", "class", "catch", "continue", "const", + "debugger", "default", "delete", "do", + "else", "enum", "export", "extends", + "false", "finally", "for", "function", + "if", "implements", "import", "in", "instanceof", "interface", + "let", + "new", "null", + "package", "private", "protected", "public", + "return", + "static", "super", "switch", + "this", "throw", "true", "try", "typeof", + "undefined", + "var", "void", + "while", "with", + "yield"]; + + var JAVA_KEYWORDS = + ["abstract", + "boolean", "break", "byte", + "case", "catch", "char", "class", "continue", + "default", "do", "double", + "else", "extends", + "false", "final", "finally", "float", "for", + "if", "implements", "import", "instanceof", "int", "interface", + "long", + "native", "new", "null", + "package", "private", "protected", "public", + "return", + "short", "static", "super", "switch", "synchronized", + "this", "throw", "throws", "transient", "true", "try", + "void", "volatile", + "while"]; + + var CSS_KEYWORDS = + ["alignment-adjust", "alignment-baseline", "animation", "animation-delay", "animation-direction", "animation-duration", + "animation-iteration-count", "animation-name", "animation-play-state", "animation-timing-function", "appearance", + "azimuth", "backface-visibility", "background", "background-attachment", "background-clip", "background-color", + "background-image", "background-origin", "background-position", "background-repeat", "background-size", "baseline-shift", + "binding", "bleed", "bookmark-label", "bookmark-level", "bookmark-state", "bookmark-target", "border", "border-bottom", + "border-bottom-color", "border-bottom-left-radius", "border-bottom-right-radius", "border-bottom-style", "border-bottom-width", + "border-collapse", "border-color", "border-image", "border-image-outset", "border-image-repeat", "border-image-slice", + "border-image-source", "border-image-width", "border-left", "border-left-color", "border-left-style", "border-left-width", + "border-radius", "border-right", "border-right-color", "border-right-style", "border-right-width", "border-spacing", "border-style", + "border-top", "border-top-color", "border-top-left-radius", "border-top-right-radius", "border-top-style", "border-top-width", + "border-width", "bottom", "box-align", "box-decoration-break", "box-direction", "box-flex", "box-flex-group", "box-lines", + "box-ordinal-group", "box-orient", "box-pack", "box-shadow", "box-sizing", "break-after", "break-before", "break-inside", + "caption-side", "clear", "clip", "color", "color-profile", "column-count", "column-fill", "column-gap", "column-rule", + "column-rule-color", "column-rule-style", "column-rule-width", "column-span", "column-width", "columns", "content", "counter-increment", + "counter-reset", "crop", "cue", "cue-after", "cue-before", "cursor", "direction", "display", "dominant-baseline", + "drop-initial-after-adjust", "drop-initial-after-align", "drop-initial-before-adjust", "drop-initial-before-align", "drop-initial-size", + "drop-initial-value", "elevation", "empty-cells", "fit", "fit-position", "flex-align", "flex-flow", "flex-inline-pack", "flex-order", + "flex-pack", "float", "float-offset", "font", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", + "font-variant", "font-weight", "grid-columns", "grid-rows", "hanging-punctuation", "height", "hyphenate-after", + "hyphenate-before", "hyphenate-character", "hyphenate-lines", "hyphenate-resource", "hyphens", "icon", "image-orientation", + "image-rendering", "image-resolution", "inline-box-align", "left", "letter-spacing", "line-height", "line-stacking", + "line-stacking-ruby", "line-stacking-shift", "line-stacking-strategy", "list-style", "list-style-image", "list-style-position", + "list-style-type", "margin", "margin-bottom", "margin-left", "margin-right", "margin-top", "mark", "mark-after", "mark-before", + "marker-offset", "marks", "marquee-direction", "marquee-loop", "marquee-play-count", "marquee-speed", "marquee-style", "max-height", + "max-width", "min-height", "min-width", "move-to", "nav-down", "nav-index", "nav-left", "nav-right", "nav-up", "opacity", "orphans", + "outline", "outline-color", "outline-offset", "outline-style", "outline-width", "overflow", "overflow-style", "overflow-x", + "overflow-y", "padding", "padding-bottom", "padding-left", "padding-right", "padding-top", "page", "page-break-after", "page-break-before", + "page-break-inside", "page-policy", "pause", "pause-after", "pause-before", "perspective", "perspective-origin", "phonemes", "pitch", + "pitch-range", "play-during", "position", "presentation-level", "punctuation-trim", "quotes", "rendering-intent", "resize", + "rest", "rest-after", "rest-before", "richness", "right", "rotation", "rotation-point", "ruby-align", "ruby-overhang", "ruby-position", + "ruby-span", "size", "speak", "speak-header", "speak-numeral", "speak-punctuation", "speech-rate", "stress", "string-set", "table-layout", + "target", "target-name", "target-new", "target-position", "text-align", "text-align-last", "text-decoration", "text-emphasis", + "text-height", "text-indent", "text-justify", "text-outline", "text-shadow", "text-transform", "text-wrap", "top", "transform", + "transform-origin", "transform-style", "transition", "transition-delay", "transition-duration", "transition-property", + "transition-timing-function", "unicode-bidi", "vector-effect", "vertical-align", "visibility", "voice-balance", "voice-duration", "voice-family", + "voice-pitch", "voice-pitch-range", "voice-rate", "voice-stress", "voice-volume", "volume", "white-space", "white-space-collapse", + "widows", "width", "word-break", "word-spacing", "word-wrap", "z-index" + ]; + + // Scanner constants + var UNKOWN = 1; + var KEYWORD = 2; + var STRING = 3; + var SINGLELINE_COMMENT = 4; + var MULTILINE_COMMENT = 5; + var DOC_COMMENT = 6; + var WHITE = 7; + var WHITE_TAB = 8; + var WHITE_SPACE = 9; + var HTML_MARKUP = 10; + var DOC_TAG = 11; + var TASK_TAG = 12; + + // Styles + var singleCommentStyle = {styleClass: "token_singleline_comment"}; + var multiCommentStyle = {styleClass: "token_multiline_comment"}; + var docCommentStyle = {styleClass: "token_doc_comment"}; + var htmlMarkupStyle = {styleClass: "token_doc_html_markup"}; + var tasktagStyle = {styleClass: "token_task_tag"}; + var doctagStyle = {styleClass: "token_doc_tag"}; + var stringStyle = {styleClass: "token_string"}; + var keywordStyle = {styleClass: "token_keyword"}; + var spaceStyle = {styleClass: "token_space"}; + var tabStyle = {styleClass: "token_tab"}; + var caretLineStyle = {styleClass: "line_caret"}; + + function Scanner (keywords, whitespacesVisible) { + this.keywords = keywords; + this.whitespacesVisible = whitespacesVisible; + this.setText(""); + } + + Scanner.prototype = { + getOffset: function() { + return this.offset; + }, + getStartOffset: function() { + return this.startOffset; + }, + getData: function() { + return this.text.substring(this.startOffset, this.offset); + }, + getDataLength: function() { + return this.offset - this.startOffset; + }, + _default: function(c) { + var keywords = this.keywords; + switch (c) { + case 32: // SPACE + case 9: // TAB + if (this.whitespacesVisible) { + return c === 32 ? WHITE_SPACE : WHITE_TAB; + } + do { + c = this._read(); + } while(c === 32 || c === 9); + this._unread(c); + return WHITE; + case 123: // { + case 125: // } + case 40: // ( + case 41: // ) + case 91: // [ + case 93: // ] + case 60: // < + case 62: // > + // BRACKETS + return c; + default: + var isCSS = this.isCSS; + if ((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57) || (0x2d === c && isCSS)) { //LETTER OR UNDERSCORE OR NUMBER + var off = this.offset - 1; + do { + c = this._read(); + } while((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57) || (0x2d === c && isCSS)); //LETTER OR UNDERSCORE OR NUMBER + this._unread(c); + if (keywords.length > 0) { + var word = this.text.substring(off, this.offset); + //TODO slow + for (var i=0; i comment + c = this._read(); + if (!this.isCSS) { + if (c === 47) { // SLASH -> single line + while (true) { + c = this._read(); + if ((c === -1) || (c === 10) || (c === 13)) { + this._unread(c); + return SINGLELINE_COMMENT; + } + } + } + } + if (c === 42) { // STAR -> multi line + c = this._read(); + var token = MULTILINE_COMMENT; + if (c === 42) { + token = DOC_COMMENT; + } + while (true) { + while (c === 42) { + c = this._read(); + if (c === 47) { + return token; + } + } + if (c === -1) { + this._unread(c); + return token; + } + c = this._read(); + } + } + this._unread(c); + return UNKOWN; + case 39: // SINGLE QUOTE -> char const + while(true) { + c = this._read(); + switch (c) { + case 39: + return STRING; + case 13: + case 10: + case -1: + this._unread(c); + return STRING; + case 92: // BACKSLASH + c = this._read(); + break; + } + } + break; + case 34: // DOUBLE QUOTE -> string + while(true) { + c = this._read(); + switch (c) { + case 34: // DOUBLE QUOTE + return STRING; + case 13: + case 10: + case -1: + this._unread(c); + return STRING; + case 92: // BACKSLASH + c = this._read(); + break; + } + } + break; + default: + return this._default(c); + } + } + }, + setText: function(text) { + this.text = text; + this.offset = 0; + this.startOffset = 0; + } + }; + + function WhitespaceScanner () { + Scanner.call(this, null, true); + } + WhitespaceScanner.prototype = new Scanner(null); + WhitespaceScanner.prototype.nextToken = function() { + this.startOffset = this.offset; + while (true) { + var c = this._read(); + switch (c) { + case -1: return null; + case 32: // SPACE + return WHITE_SPACE; + case 9: // TAB + return WHITE_TAB; + default: + do { + c = this._read(); + } while(!(c === 32 || c === 9 || c === -1)); + this._unread(c); + return UNKOWN; + } + } + }; + + function CommentScanner (whitespacesVisible) { + Scanner.call(this, null, whitespacesVisible); + } + CommentScanner.prototype = new Scanner(null); + CommentScanner.prototype.setType = function(type) { + this._type = type; + }; + CommentScanner.prototype.nextToken = function() { + this.startOffset = this.offset; + while (true) { + var c = this._read(); + switch (c) { + case -1: return null; + case 32: // SPACE + case 9: // TAB + if (this.whitespacesVisible) { + return c === 32 ? WHITE_SPACE : WHITE_TAB; + } + do { + c = this._read(); + } while(c === 32 || c === 9); + this._unread(c); + return WHITE; + case 60: // < + if (this._type === DOC_COMMENT) { + do { + c = this._read(); + } while(!(c === 62 || c === -1)); // > + if (c === 62) { + return HTML_MARKUP; + } + } + return UNKOWN; + case 64: // @ + if (this._type === DOC_COMMENT) { + do { + c = this._read(); + } while((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57)); //LETTER OR UNDERSCORE OR NUMBER + this._unread(c); + return DOC_TAG; + } + return UNKOWN; + case 84: // T + if ((c = this._read()) === 79) { // O + if ((c = this._read()) === 68) { // D + if ((c = this._read()) === 79) { // O + c = this._read(); + if (!((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57))) { + this._unread(c); + return TASK_TAG; + } + this._unread(c); + } else { + this._unread(c); + } + } else { + this._unread(c); + } + } else { + this._unread(c); + } + //FALL THROUGH + default: + do { + c = this._read(); + } while(!(c === 32 || c === 9 || c === -1 || c === 60 || c === 64 || c === 84)); + this._unread(c); + return UNKOWN; + } + } + }; + + function FirstScanner () { + Scanner.call(this, null, false); + } + FirstScanner.prototype = new Scanner(null); + FirstScanner.prototype._default = function(c) { + while(true) { + c = this._read(); + switch (c) { + case 47: // SLASH + case 34: // DOUBLE QUOTE + case 39: // SINGLE QUOTE + case -1: + this._unread(c); + return UNKOWN; + } + } + }; + + function TextStyler (view, lang, annotationModel) { + this.commentStart = "/*"; + this.commentEnd = "*/"; + var keywords = []; + switch (lang) { + case "java": keywords = JAVA_KEYWORDS; break; + case "js": keywords = JS_KEYWORDS; break; + case "css": keywords = CSS_KEYWORDS; break; + } + this.whitespacesVisible = false; + this.detectHyperlinks = true; + this.highlightCaretLine = false; + this.foldingEnabled = true; + this.detectTasks = true; + this._scanner = new Scanner(keywords, this.whitespacesVisible); + this._firstScanner = new FirstScanner(); + this._commentScanner = new CommentScanner(this.whitespacesVisible); + this._whitespaceScanner = new WhitespaceScanner(); + //TODO these scanners are not the best/correct way to parse CSS + if (lang === "css") { + this._scanner.isCSS = true; + this._firstScanner.isCSS = true; + } + this.view = view; + this.annotationModel = annotationModel; + this._bracketAnnotations = undefined; + + var self = this; + this._listener = { + onChanged: function(e) { + self._onModelChanged(e); + }, + onDestroy: function(e) { + self._onDestroy(e); + }, + onLineStyle: function(e) { + self._onLineStyle(e); + }, + onSelection: function(e) { + self._onSelection(e); + } + }; + var model = view.getModel(); + if (model.getBaseModel) { + model.getBaseModel().addEventListener("Changed", this._listener.onChanged); + } else { + //TODO still needed to keep the event order correct (styler before view) + view.addEventListener("ModelChanged", this._listener.onChanged); + } + view.addEventListener("Selection", this._listener.onSelection); + view.addEventListener("Destroy", this._listener.onDestroy); + view.addEventListener("LineStyle", this._listener.onLineStyle); + this._computeComments (); + this._computeFolding(); + view.redrawLines(); + } + + TextStyler.prototype = { + getClassNameForToken: function(token) { + switch (token) { + case "singleLineComment": return singleCommentStyle.styleClass; + case "multiLineComment": return multiCommentStyle.styleClass; + case "docComment": return docCommentStyle.styleClass; + case "docHtmlComment": return htmlMarkupStyle.styleClass; + case "tasktag": return tasktagStyle.styleClass; + case "doctag": return doctagStyle.styleClass; + case "string": return stringStyle.styleClass; + case "keyword": return keywordStyle.styleClass; + case "space": return spaceStyle.styleClass; + case "tab": return tabStyle.styleClass; + case "caretLine": return caretLineStyle.styleClass; + } + return null; + }, + destroy: function() { + var view = this.view; + if (view) { + var model = view.getModel(); + if (model.getBaseModel) { + model.getBaseModel().removeEventListener("Changed", this._listener.onChanged); + } else { + view.removeEventListener("ModelChanged", this._listener.onChanged); + } + view.removeEventListener("Selection", this._listener.onSelection); + view.removeEventListener("Destroy", this._listener.onDestroy); + view.removeEventListener("LineStyle", this._listener.onLineStyle); + this.view = null; + } + }, + setHighlightCaretLine: function(highlight) { + this.highlightCaretLine = highlight; + }, + setWhitespacesVisible: function(visible) { + this.whitespacesVisible = visible; + this._scanner.whitespacesVisible = visible; + this._commentScanner.whitespacesVisible = visible; + }, + setDetectHyperlinks: function(enabled) { + this.detectHyperlinks = enabled; + }, + setFoldingEnabled: function(enabled) { + this.foldingEnabled = enabled; + }, + setDetectTasks: function(enabled) { + this.detectTasks = enabled; + }, + _binarySearch: function (array, offset, inclusive, low, high) { + var index; + if (low === undefined) { low = -1; } + if (high === undefined) { high = array.length; } + while (high - low > 1) { + index = Math.floor((high + low) / 2); + if (offset <= array[index].start) { + high = index; + } else if (inclusive && offset < array[index].end) { + high = index; + break; + } else { + low = index; + } + } + return high; + }, + _computeComments: function() { + var model = this.view.getModel(); + if (model.getBaseModel) { model = model.getBaseModel(); } + this.comments = this._findComments(model.getText()); + }, + _computeFolding: function() { + if (!this.foldingEnabled) { return; } + var view = this.view; + var viewModel = view.getModel(); + if (!viewModel.getBaseModel) { return; } + var annotationModel = this.annotationModel; + if (!annotationModel) { return; } + annotationModel.removeAnnotations("orion.annotation.folding"); + var add = []; + var baseModel = viewModel.getBaseModel(); + var comments = this.comments; + for (var i=0; i
", {styleClass: "annotation expanded"}, + "", {styleClass: "annotation collapsed"}); + }, + _computeTasks: function(type, commentStart, commentEnd) { + if (!this.detectTasks) { return; } + var annotationModel = this.annotationModel; + if (!annotationModel) { return; } + var view = this.view; + var viewModel = view.getModel(), baseModel = viewModel; + if (viewModel.getBaseModel) { baseModel = viewModel.getBaseModel(); } + var annotations = annotationModel.getAnnotations(commentStart, commentEnd); + var remove = []; + var annotationType = "orion.annotation.task"; + while (annotations.hasNext()) { + var annotation = annotations.next(); + if (annotation.type === annotationType) { + remove.push(annotation); + } + } + var add = []; + var scanner = this._commentScanner; + scanner.setText(baseModel.getText(commentStart, commentEnd)); + var token; + while ((token = scanner.nextToken())) { + var tokenStart = scanner.getStartOffset() + commentStart; + if (token === TASK_TAG) { + var end = baseModel.getLineEnd(baseModel.getLineAtOffset(tokenStart)); + if (type !== SINGLELINE_COMMENT) { + end = Math.min(end, commentEnd - this.commentEnd.length); + } + add.push({ + start: tokenStart, + end: end, + type: annotationType, + title: baseModel.getText(tokenStart, end), + style: {styleClass: "annotation task"}, + html: "
", + overviewStyle: {styleClass: "annotationOverview task"}, + rangeStyle: {styleClass: "annotationRange task"} + }); + } + } + annotationModel.replaceAnnotations(remove, add); + }, + _getLineStyle: function(lineIndex) { + if (this.highlightCaretLine) { + var view = this.view; + var model = view.getModel(); + var selection = view.getSelection(); + if (selection.start === selection.end && model.getLineAtOffset(selection.start) === lineIndex) { + return caretLineStyle; + } + } + return null; + }, + _getStyles: function(model, text, start) { + if (model.getBaseModel) { + start = model.mapOffset(start); + } + var end = start + text.length; + + var styles = []; + + // for any sub range that is not a comment, parse code generating tokens (keywords, numbers, brackets, line comments, etc) + var offset = start, comments = this.comments; + var startIndex = this._binarySearch(comments, start, true); + for (var i = startIndex; i < comments.length; i++) { + if (comments[i].start >= end) { break; } + var commentStart = comments[i].start; + var commentEnd = comments[i].end; + if (offset < commentStart) { + this._parse(text.substring(offset - start, commentStart - start), offset, styles); + } + var style = comments[i].type === DOC_COMMENT ? docCommentStyle : multiCommentStyle; + if (this.whitespacesVisible || this.detectHyperlinks) { + var s = Math.max(offset, commentStart); + var e = Math.min(end, commentEnd); + this._parseComment(text.substring(s - start, e - start), s, styles, style, comments[i].type); + } else { + styles.push({start: commentStart, end: commentEnd, style: style}); + } + offset = commentEnd; + } + if (offset < end) { + this._parse(text.substring(offset - start, end - start), offset, styles); + } + if (model.getBaseModel) { + for (var j = 0; j < styles.length; j++) { + var length = styles[j].end - styles[j].start; + styles[j].start = model.mapOffset(styles[j].start, true); + styles[j].end = styles[j].start + length; + } + } + return styles; + }, + _parse: function(text, offset, styles) { + var scanner = this._scanner; + scanner.setText(text); + var token; + while ((token = scanner.nextToken())) { + var tokenStart = scanner.getStartOffset() + offset; + var style = null; + switch (token) { + case KEYWORD: style = keywordStyle; break; + case STRING: + if (this.whitespacesVisible) { + this._parseString(scanner.getData(), tokenStart, styles, stringStyle); + continue; + } else { + style = stringStyle; + } + break; + case DOC_COMMENT: + this._parseComment(scanner.getData(), tokenStart, styles, docCommentStyle, token); + continue; + case SINGLELINE_COMMENT: + this._parseComment(scanner.getData(), tokenStart, styles, singleCommentStyle, token); + continue; + case MULTILINE_COMMENT: + this._parseComment(scanner.getData(), tokenStart, styles, multiCommentStyle, token); + continue; + case WHITE_TAB: + if (this.whitespacesVisible) { + style = tabStyle; + } + break; + case WHITE_SPACE: + if (this.whitespacesVisible) { + style = spaceStyle; + } + break; + } + styles.push({start: tokenStart, end: scanner.getOffset() + offset, style: style}); + } + }, + _parseComment: function(text, offset, styles, s, type) { + var scanner = this._commentScanner; + scanner.setText(text); + scanner.setType(type); + var token; + while ((token = scanner.nextToken())) { + var tokenStart = scanner.getStartOffset() + offset; + var style = s; + switch (token) { + case WHITE_TAB: + if (this.whitespacesVisible) { + style = tabStyle; + } + break; + case WHITE_SPACE: + if (this.whitespacesVisible) { + style = spaceStyle; + } + break; + case HTML_MARKUP: + style = htmlMarkupStyle; + break; + case DOC_TAG: + style = doctagStyle; + break; + case TASK_TAG: + style = tasktagStyle; + break; + default: + if (this.detectHyperlinks) { + style = this._detectHyperlinks(scanner.getData(), tokenStart, styles, style); + } + } + if (style) { + styles.push({start: tokenStart, end: scanner.getOffset() + offset, style: style}); + } + } + }, + _parseString: function(text, offset, styles, s) { + var scanner = this._whitespaceScanner; + scanner.setText(text); + var token; + while ((token = scanner.nextToken())) { + var tokenStart = scanner.getStartOffset() + offset; + var style = s; + switch (token) { + case WHITE_TAB: + if (this.whitespacesVisible) { + style = tabStyle; + } + break; + case WHITE_SPACE: + if (this.whitespacesVisible) { + style = spaceStyle; + } + break; + } + if (style) { + styles.push({start: tokenStart, end: scanner.getOffset() + offset, style: style}); + } + } + }, + _detectHyperlinks: function(text, offset, styles, s) { + var href = null, index, linkStyle; + if ((index = text.indexOf("://")) > 0) { + href = text; + var start = index; + while (start > 0) { + var c = href.charCodeAt(start - 1); + if (!((97 <= c && c <= 122) || (65 <= c && c <= 90) || 0x2d === c || (48 <= c && c <= 57))) { //LETTER OR DASH OR NUMBER + break; + } + start--; + } + if (start > 0) { + var brackets = "\"\"''(){}[]<>"; + index = brackets.indexOf(href.substring(start - 1, start)); + if (index !== -1 && (index & 1) === 0 && (index = href.lastIndexOf(brackets.substring(index + 1, index + 2))) !== -1) { + var end = index; + linkStyle = this._clone(s); + linkStyle.tagName = "A"; + linkStyle.attributes = {href: href.substring(start, end)}; + styles.push({start: offset, end: offset + start, style: s}); + styles.push({start: offset + start, end: offset + end, style: linkStyle}); + styles.push({start: offset + end, end: offset + text.length, style: s}); + return null; + } + } + } else if (text.toLowerCase().indexOf("bug#") === 0) { + href = "https://bugs.eclipse.org/bugs/show_bug.cgi?id=" + parseInt(text.substring(4), 10); + } + if (href) { + linkStyle = this._clone(s); + linkStyle.tagName = "A"; + linkStyle.attributes = {href: href}; + return linkStyle; + } + return s; + }, + _clone: function(obj) { + if (!obj) { return obj; } + var newObj = {}; + for (var p in obj) { + if (obj.hasOwnProperty(p)) { + var value = obj[p]; + newObj[p] = value; + } + } + return newObj; + }, + _findComments: function(text, offset) { + offset = offset || 0; + var scanner = this._firstScanner, token; + scanner.setText(text); + var result = []; + while ((token = scanner.nextToken())) { + if (token === MULTILINE_COMMENT || token === DOC_COMMENT) { + var comment = { + start: scanner.getStartOffset() + offset, + end: scanner.getOffset() + offset, + type: token + }; + result.push(comment); + //TODO can we avoid this work if edition does not overlap comment? + this._computeTasks(token, scanner.getStartOffset() + offset, scanner.getOffset() + offset); + } + if (token === SINGLELINE_COMMENT) { + //TODO can we avoid this work if edition does not overlap comment? + this._computeTasks(token, scanner.getStartOffset() + offset, scanner.getOffset() + offset); + } + } + return result; + }, + _findMatchingBracket: function(model, offset) { + var brackets = "{}()[]<>"; + var bracket = model.getText(offset, offset + 1); + var bracketIndex = brackets.indexOf(bracket, 0); + if (bracketIndex === -1) { return -1; } + var closingBracket; + if (bracketIndex & 1) { + closingBracket = brackets.substring(bracketIndex - 1, bracketIndex); + } else { + closingBracket = brackets.substring(bracketIndex + 1, bracketIndex + 2); + } + var lineIndex = model.getLineAtOffset(offset); + var lineText = model.getLine(lineIndex); + var lineStart = model.getLineStart(lineIndex); + var lineEnd = model.getLineEnd(lineIndex); + brackets = this._findBrackets(bracket, closingBracket, lineText, lineStart, lineStart, lineEnd); + for (var i=0; i= 0 ? 1 : -1; + if (brackets[i] * sign === offset) { + var level = 1; + if (bracketIndex & 1) { + i--; + for (; i>=0; i--) { + sign = brackets[i] >= 0 ? 1 : -1; + level += sign; + if (level === 0) { + return brackets[i] * sign; + } + } + lineIndex -= 1; + while (lineIndex >= 0) { + lineText = model.getLine(lineIndex); + lineStart = model.getLineStart(lineIndex); + lineEnd = model.getLineEnd(lineIndex); + brackets = this._findBrackets(bracket, closingBracket, lineText, lineStart, lineStart, lineEnd); + for (var j=brackets.length - 1; j>=0; j--) { + sign = brackets[j] >= 0 ? 1 : -1; + level += sign; + if (level === 0) { + return brackets[j] * sign; + } + } + lineIndex--; + } + } else { + i++; + for (; i= 0 ? 1 : -1; + level += sign; + if (level === 0) { + return brackets[i] * sign; + } + } + lineIndex += 1; + var lineCount = model.getLineCount (); + while (lineIndex < lineCount) { + lineText = model.getLine(lineIndex); + lineStart = model.getLineStart(lineIndex); + lineEnd = model.getLineEnd(lineIndex); + brackets = this._findBrackets(bracket, closingBracket, lineText, lineStart, lineStart, lineEnd); + for (var k=0; k= 0 ? 1 : -1; + level += sign; + if (level === 0) { + return brackets[k] * sign; + } + } + lineIndex++; + } + } + break; + } + } + return -1; + }, + _findBrackets: function(bracket, closingBracket, text, textOffset, start, end) { + var result = []; + var bracketToken = bracket.charCodeAt(0); + var closingBracketToken = closingBracket.charCodeAt(0); + // for any sub range that is not a comment, parse code generating tokens (keywords, numbers, brackets, line comments, etc) + var offset = start, scanner = this._scanner, token, comments = this.comments; + var startIndex = this._binarySearch(comments, start, true); + for (var i = startIndex; i < comments.length; i++) { + if (comments[i].start >= end) { break; } + var commentStart = comments[i].start; + var commentEnd = comments[i].end; + if (offset < commentStart) { + scanner.setText(text.substring(offset - start, commentStart - start)); + while ((token = scanner.nextToken())) { + if (token === bracketToken) { + result.push(scanner.getStartOffset() + offset - start + textOffset); + } else if (token === closingBracketToken) { + result.push(-(scanner.getStartOffset() + offset - start + textOffset)); + } + } + } + offset = commentEnd; + } + if (offset < end) { + scanner.setText(text.substring(offset - start, end - start)); + while ((token = scanner.nextToken())) { + if (token === bracketToken) { + result.push(scanner.getStartOffset() + offset - start + textOffset); + } else if (token === closingBracketToken) { + result.push(-(scanner.getStartOffset() + offset - start + textOffset)); + } + } + } + return result; + }, + _onDestroy: function(e) { + this.destroy(); + }, + _onLineStyle: function (e) { + if (e.textView === this.view) { + e.style = this._getLineStyle(e.lineIndex); + } + e.ranges = this._getStyles(e.textView.getModel(), e.lineText, e.lineStart); + }, + _onSelection: function(e) { + var oldSelection = e.oldValue; + var newSelection = e.newValue; + var view = this.view; + var model = view.getModel(); + var lineIndex; + if (this.highlightCaretLine) { + var oldLineIndex = model.getLineAtOffset(oldSelection.start); + lineIndex = model.getLineAtOffset(newSelection.start); + var newEmpty = newSelection.start === newSelection.end; + var oldEmpty = oldSelection.start === oldSelection.end; + if (!(oldLineIndex === lineIndex && oldEmpty && newEmpty)) { + if (oldEmpty) { + view.redrawLines(oldLineIndex, oldLineIndex + 1); + } + if ((oldLineIndex !== lineIndex || !oldEmpty) && newEmpty) { + view.redrawLines(lineIndex, lineIndex + 1); + } + } + } + if (!this.annotationModel) { return; } + var remove = this._bracketAnnotations, add, caret; + if (newSelection.start === newSelection.end && (caret = view.getCaretOffset()) > 0) { + var mapCaret = caret - 1; + if (model.getBaseModel) { + mapCaret = model.mapOffset(mapCaret); + model = model.getBaseModel(); + } + var bracket = this._findMatchingBracket(model, mapCaret); + if (bracket !== -1) { + add = [{ + start: bracket, + end: bracket + 1, + type: "orion.annotation.matchingBracket", + title: "Matching Bracket", + html: "
", + overviewStyle: {styleClass: "annotationOverview matchingBracket"}, + rangeStyle: {styleClass: "annotationRange matchingBracket"} + }, + { + start: mapCaret, + end: mapCaret + 1, + type: "orion.annotation.currentBracket", + title: "Current Bracket", + html: "
", + overviewStyle: {styleClass: "annotationOverview currentBracket"}, + rangeStyle: {styleClass: "annotationRange currentBracket"} + }]; + } + } + this._bracketAnnotations = add; + this.annotationModel.replaceAnnotations(remove, add); + }, + _onModelChanged: function(e) { + var start = e.start; + var removedCharCount = e.removedCharCount; + var addedCharCount = e.addedCharCount; + var changeCount = addedCharCount - removedCharCount; + var view = this.view; + var viewModel = view.getModel(); + var baseModel = viewModel.getBaseModel ? viewModel.getBaseModel() : viewModel; + var end = start + removedCharCount; + var charCount = baseModel.getCharCount(); + var commentCount = this.comments.length; + var lineStart = baseModel.getLineStart(baseModel.getLineAtOffset(start)); + var commentStart = this._binarySearch(this.comments, lineStart, true); + var commentEnd = this._binarySearch(this.comments, end, false, commentStart - 1, commentCount); + + var ts; + if (commentStart < commentCount && this.comments[commentStart].start <= lineStart && lineStart < this.comments[commentStart].end) { + ts = this.comments[commentStart].start; + if (ts > start) { ts += changeCount; } + } else { + if (commentStart === commentCount && commentCount > 0 && charCount - changeCount === this.comments[commentCount - 1].end) { + ts = this.comments[commentCount - 1].start; + } else { + ts = lineStart; + } + } + var te; + if (commentEnd < commentCount) { + te = this.comments[commentEnd].end; + if (te > start) { te += changeCount; } + commentEnd += 1; + } else { + commentEnd = commentCount; + te = charCount;//TODO could it be smaller? + } + var text = baseModel.getText(ts, te), comment; + var newComments = this._findComments(text, ts), i; + for (i = commentStart; i < this.comments.length; i++) { + comment = this.comments[i]; + if (comment.start > start) { comment.start += changeCount; } + if (comment.start > start) { comment.end += changeCount; } + } + var redraw = (commentEnd - commentStart) !== newComments.length; + if (!redraw) { + for (i=0; i start) { + annotationStart -= changeCount; + } + if (annotationEnd > start) { + annotationEnd -= changeCount; + } + if (annotationStart <= start && start < annotationEnd && annotationStart <= end && end < annotationEnd) { + var startLine = baseModel.getLineAtOffset(annotation.start); + var endLine = baseModel.getLineAtOffset(annotation.end); + if (startLine !== endLine) { + if (!annotation.expanded) { + annotation.expand(); + annotationModel.modifyAnnotation(annotation); + } + } else { + annotationModel.removeAnnotation(annotation); + } + } + } + } + } + var add = []; + for (i = 0; i < newComments.length; i++) { + comment = newComments[i]; + for (var j = 0; j < all.length; j++) { + if (all[j].start === comment.start && all[j].end === comment.end) { + break; + } + } + if (j === all.length) { + annotation = this._createFoldingAnnotation(viewModel, baseModel, comment.start, comment.end); + if (annotation) { + add.push(annotation); + } + } + } + annotationModel.replaceAnnotations(remove, add); + } + } + }; + + return {TextStyler: TextStyler}; +}); diff --git a/browser/devtools/sourceeditor/source-editor-orion.jsm b/browser/devtools/sourceeditor/source-editor-orion.jsm new file mode 100644 index 000000000000..aa326478332a --- /dev/null +++ b/browser/devtools/sourceeditor/source-editor-orion.jsm @@ -0,0 +1,2131 @@ +/* vim:set ts=2 sw=2 sts=2 et tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Cu = Components.utils; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/devtools/sourceeditor/source-editor-ui.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper", + "@mozilla.org/widget/clipboardhelper;1", + "nsIClipboardHelper"); + +const ORION_SCRIPT = "chrome://browser/content/devtools/orion.js"; +const ORION_IFRAME = "data:text/html;charset=utf8," + + "" + + "" + + "" + + "
" + + ""; + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +/** + * Maximum allowed vertical offset for the line index when you call + * SourceEditor.setCaretPosition(). + * + * @type number + */ +const VERTICAL_OFFSET = 3; + +/** + * The primary selection update delay. On Linux, the X11 primary selection is + * updated to hold the currently selected text. + * + * @type number + */ +const PRIMARY_SELECTION_DELAY = 100; + +/** + * Predefined themes for syntax highlighting. This objects maps + * SourceEditor.THEMES to Orion CSS files. + */ +const ORION_THEMES = { + mozilla: ["chrome://browser/skin/devtools/orion.css"], +}; + +/** + * Known Orion editor events you can listen for. This object maps several of the + * SourceEditor.EVENTS to Orion events. + */ +const ORION_EVENTS = { + ContextMenu: "ContextMenu", + TextChanged: "ModelChanged", + Selection: "Selection", + Focus: "Focus", + Blur: "Blur", + MouseOver: "MouseOver", + MouseOut: "MouseOut", + MouseMove: "MouseMove", +}; + +/** + * Known Orion annotation types. + */ +const ORION_ANNOTATION_TYPES = { + currentBracket: "orion.annotation.currentBracket", + matchingBracket: "orion.annotation.matchingBracket", + breakpoint: "orion.annotation.breakpoint", + task: "orion.annotation.task", + currentLine: "orion.annotation.currentLine", + debugLocation: "mozilla.annotation.debugLocation", +}; + +/** + * Default key bindings in the Orion editor. + */ +const DEFAULT_KEYBINDINGS = [ + { + action: "enter", + code: Ci.nsIDOMKeyEvent.DOM_VK_ENTER, + }, + { + action: "undo", + code: Ci.nsIDOMKeyEvent.DOM_VK_Z, + accel: true, + }, + { + action: "redo", + code: Ci.nsIDOMKeyEvent.DOM_VK_Z, + accel: true, + shift: true, + }, + { + action: "Unindent Lines", + code: Ci.nsIDOMKeyEvent.DOM_VK_TAB, + shift: true, + }, + { + action: "Move Lines Up", + code: Ci.nsIDOMKeyEvent.DOM_VK_UP, + ctrl: Services.appinfo.OS == "Darwin", + alt: true, + }, + { + action: "Move Lines Down", + code: Ci.nsIDOMKeyEvent.DOM_VK_DOWN, + ctrl: Services.appinfo.OS == "Darwin", + alt: true, + }, + { + action: "Comment/Uncomment", + code: Ci.nsIDOMKeyEvent.DOM_VK_SLASH, + accel: true, + }, + { + action: "Move to Bracket Opening", + code: Ci.nsIDOMKeyEvent.DOM_VK_OPEN_BRACKET, + accel: true, + alt: true, + }, + { + action: "Move to Bracket Closing", + code: Ci.nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET, + accel: true, + alt: true, + }, +]; + +if (Services.appinfo.OS == "WINNT" || + Services.appinfo.OS == "Linux") { + DEFAULT_KEYBINDINGS.push({ + action: "redo", + code: Ci.nsIDOMKeyEvent.DOM_VK_Y, + accel: true, + }); +} + +this.EXPORTED_SYMBOLS = ["SourceEditor"]; + +/** + * The SourceEditor object constructor. The SourceEditor component allows you to + * provide users with an editor tailored to the specific needs of editing source + * code, aimed primarily at web developers. + * + * The editor used here is Eclipse Orion (see http://www.eclipse.org/orion). + * + * @constructor + */ +this.SourceEditor = function SourceEditor() { + // Update the SourceEditor defaults from user preferences. + + SourceEditor.DEFAULTS.tabSize = + Services.prefs.getIntPref(SourceEditor.PREFS.TAB_SIZE); + SourceEditor.DEFAULTS.expandTab = + Services.prefs.getBoolPref(SourceEditor.PREFS.EXPAND_TAB); + + this._onOrionSelection = this._onOrionSelection.bind(this); + this._onTextChanged = this._onTextChanged.bind(this); + this._onOrionContextMenu = this._onOrionContextMenu.bind(this); + + this._eventTarget = {}; + this._eventListenersQueue = []; + this.ui = new SourceEditorUI(this); +} + +SourceEditor.prototype = { + _view: null, + _iframe: null, + _model: null, + _undoStack: null, + _linesRuler: null, + _annotationRuler: null, + _overviewRuler: null, + _styler: null, + _annotationStyler: null, + _annotationModel: null, + _dragAndDrop: null, + _currentLineAnnotation: null, + _primarySelectionTimeout: null, + _mode: null, + _expandTab: null, + _tabSize: null, + _iframeWindow: null, + _eventTarget: null, + _eventListenersQueue: null, + _contextMenu: null, + _dirty: false, + + /** + * The Source Editor user interface manager. + * @type object + * An instance of the SourceEditorUI. + */ + ui: null, + + /** + * The editor container element. + * @type nsIDOMElement + */ + parentElement: null, + + /** + * Initialize the editor. + * + * @param nsIDOMElement aElement + * The DOM element where you want the editor to show. + * @param object aConfig + * Editor configuration object. See SourceEditor.DEFAULTS for the + * available configuration options. + * @param function [aCallback] + * Function you want to execute once the editor is loaded and + * initialized. + * @see SourceEditor.DEFAULTS + */ + init: function SE_init(aElement, aConfig, aCallback) + { + if (this._iframe) { + throw new Error("SourceEditor is already initialized!"); + } + + let doc = aElement.ownerDocument; + + this._iframe = doc.createElementNS(XUL_NS, "iframe"); + this._iframe.flex = 1; + + let onIframeLoad = (function() { + this._iframe.removeEventListener("load", onIframeLoad, true); + this._onIframeLoad(); + }).bind(this); + + this._iframe.addEventListener("load", onIframeLoad, true); + + this._iframe.setAttribute("src", ORION_IFRAME); + + aElement.appendChild(this._iframe); + this.parentElement = aElement; + + this._config = {}; + for (let key in SourceEditor.DEFAULTS) { + this._config[key] = key in aConfig ? + aConfig[key] : + SourceEditor.DEFAULTS[key]; + } + + // TODO: Bug 725677 - Remove the deprecated placeholderText option from the + // Source Editor initialization. + if (aConfig.placeholderText) { + this._config.initialText = aConfig.placeholderText; + Services.console.logStringMessage("SourceEditor.init() was called with the placeholderText option which is deprecated, please use initialText."); + } + + this._onReadyCallback = aCallback; + this.ui.init(); + }, + + /** + * The editor iframe load event handler. + * @private + */ + _onIframeLoad: function SE__onIframeLoad() + { + this._iframeWindow = this._iframe.contentWindow.wrappedJSObject; + let window = this._iframeWindow; + let config = this._config; + + Services.scriptloader.loadSubScript(ORION_SCRIPT, window, "utf8"); + + let TextModel = window.require("orion/textview/textModel").TextModel; + let TextView = window.require("orion/textview/textView").TextView; + + this._expandTab = config.expandTab; + this._tabSize = config.tabSize; + + let theme = config.theme; + let stylesheet = theme in ORION_THEMES ? ORION_THEMES[theme] : theme; + + this._model = new TextModel(config.initialText); + this._view = new TextView({ + model: this._model, + parent: "editor", + stylesheet: stylesheet, + tabSize: this._tabSize, + expandTab: this._expandTab, + readonly: config.readOnly, + themeClass: "mozilla" + (config.readOnly ? " readonly" : ""), + }); + + let onOrionLoad = function() { + this._view.removeEventListener("Load", onOrionLoad); + this._onOrionLoad(); + }.bind(this); + + this._view.addEventListener("Load", onOrionLoad); + if (config.highlightCurrentLine || Services.appinfo.OS == "Linux") { + this.addEventListener(SourceEditor.EVENTS.SELECTION, + this._onOrionSelection); + } + this.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, + this._onTextChanged); + + if (typeof config.contextMenu == "string") { + let chromeDocument = this.parentElement.ownerDocument; + this._contextMenu = chromeDocument.getElementById(config.contextMenu); + } else if (typeof config.contextMenu == "object" ) { + this._contextMenu = config._contextMenu; + } + if (this._contextMenu) { + this.addEventListener(SourceEditor.EVENTS.CONTEXT_MENU, + this._onOrionContextMenu); + } + + let KeyBinding = window.require("orion/textview/keyBinding").KeyBinding; + let TextDND = window.require("orion/textview/textDND").TextDND; + let Rulers = window.require("orion/textview/rulers"); + let LineNumberRuler = Rulers.LineNumberRuler; + let AnnotationRuler = Rulers.AnnotationRuler; + let OverviewRuler = Rulers.OverviewRuler; + let UndoStack = window.require("orion/textview/undoStack").UndoStack; + let AnnotationModel = window.require("orion/textview/annotations").AnnotationModel; + + this._annotationModel = new AnnotationModel(this._model); + + if (config.showAnnotationRuler) { + this._annotationRuler = new AnnotationRuler(this._annotationModel, "left", + {styleClass: "ruler annotations"}); + this._annotationRuler.onClick = this._annotationRulerClick.bind(this); + this._annotationRuler.addAnnotationType(ORION_ANNOTATION_TYPES.breakpoint); + this._annotationRuler.addAnnotationType(ORION_ANNOTATION_TYPES.debugLocation); + this._view.addRuler(this._annotationRuler); + } + + if (config.showLineNumbers) { + let rulerClass = this._annotationRuler ? + "ruler lines linesWithAnnotations" : + "ruler lines"; + + this._linesRuler = new LineNumberRuler(this._annotationModel, "left", + {styleClass: rulerClass}, {styleClass: "rulerLines odd"}, + {styleClass: "rulerLines even"}); + + this._linesRuler.onClick = this._linesRulerClick.bind(this); + this._linesRuler.onDblClick = this._linesRulerDblClick.bind(this); + this._view.addRuler(this._linesRuler); + } + + if (config.showOverviewRuler) { + this._overviewRuler = new OverviewRuler(this._annotationModel, "right", + {styleClass: "ruler overview"}); + this._overviewRuler.onClick = this._overviewRulerClick.bind(this); + + this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.matchingBracket); + this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.currentBracket); + this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.breakpoint); + this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.debugLocation); + this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.task); + this._view.addRuler(this._overviewRuler); + } + + this.setMode(config.mode); + + this._undoStack = new UndoStack(this._view, config.undoLimit); + + this._dragAndDrop = new TextDND(this._view, this._undoStack); + + let actions = { + "undo": [this.undo, this], + "redo": [this.redo, this], + "tab": [this._doTab, this], + "Unindent Lines": [this._doUnindentLines, this], + "enter": [this._doEnter, this], + "Find...": [this.ui.find, this.ui], + "Find Next Occurrence": [this.ui.findNext, this.ui], + "Find Previous Occurrence": [this.ui.findPrevious, this.ui], + "Goto Line...": [this.ui.gotoLine, this.ui], + "Move Lines Down": [this._moveLines, this], + "Comment/Uncomment": [this._doCommentUncomment, this], + "Move to Bracket Opening": [this._moveToBracketOpening, this], + "Move to Bracket Closing": [this._moveToBracketClosing, this], + }; + + for (let name in actions) { + let action = actions[name]; + this._view.setAction(name, action[0].bind(action[1])); + } + + this._view.setAction("Move Lines Up", this._moveLines.bind(this, true)); + + let keys = (config.keys || []).concat(DEFAULT_KEYBINDINGS); + keys.forEach(function(aKey) { + // In Orion mod1 refers to Cmd on Macs and Ctrl on Windows and Linux. + // So, if ctrl is in aKey we use it on Windows and Linux, otherwise + // we use aKey.accel for mod1. + let mod1 = Services.appinfo.OS != "Darwin" && + "ctrl" in aKey ? aKey.ctrl : aKey.accel; + let binding = new KeyBinding(aKey.code, mod1, aKey.shift, aKey.alt, + aKey.ctrl); + this._view.setKeyBinding(binding, aKey.action); + + if (aKey.callback) { + this._view.setAction(aKey.action, aKey.callback); + } + }, this); + + this._initEventTarget(); + }, + + /** + * Initialize the private Orion EventTarget object. This is used for tracking + * our own event listeners for events outside of Orion's scope. + * @private + */ + _initEventTarget: function SE__initEventTarget() + { + let EventTarget = + this._iframeWindow.require("orion/textview/eventTarget").EventTarget; + EventTarget.addMixin(this._eventTarget); + + this._eventListenersQueue.forEach(function(aRequest) { + if (aRequest[0] == "add") { + this.addEventListener(aRequest[1], aRequest[2]); + } else { + this.removeEventListener(aRequest[1], aRequest[2]); + } + }, this); + + this._eventListenersQueue = []; + }, + + /** + * Dispatch an event to the SourceEditor event listeners. This covers only the + * SourceEditor-specific events. + * + * @private + * @param object aEvent + * The event object to dispatch to all listeners. + */ + _dispatchEvent: function SE__dispatchEvent(aEvent) + { + this._eventTarget.dispatchEvent(aEvent); + }, + + /** + * The Orion "Load" event handler. This is called when the Orion editor + * completes the initialization. + * @private + */ + _onOrionLoad: function SE__onOrionLoad() + { + this.ui.onReady(); + if (this._onReadyCallback) { + this._onReadyCallback(this); + this._onReadyCallback = null; + } + }, + + /** + * The "tab" editor action implementation. This adds support for expanded tabs + * to spaces, and support for the indentation of multiple lines at once. + * @private + */ + _doTab: function SE__doTab() + { + if (this.readOnly) { + return false; + } + + let indent = "\t"; + let selection = this.getSelection(); + let model = this._model; + let firstLine = model.getLineAtOffset(selection.start); + let firstLineStart = this.getLineStart(firstLine); + let lastLineOffset = selection.end > selection.start ? + selection.end - 1 : selection.end; + let lastLine = model.getLineAtOffset(lastLineOffset); + + if (this._expandTab) { + let offsetFromLineStart = firstLine == lastLine ? + selection.start - firstLineStart : 0; + let spaces = this._tabSize - (offsetFromLineStart % this._tabSize); + indent = (new Array(spaces + 1)).join(" "); + } + + // Do selection indentation. + if (firstLine != lastLine) { + let lines = [""]; + let lastLineEnd = this.getLineEnd(lastLine, true); + let selectedLines = lastLine - firstLine + 1; + + for (let i = firstLine; i <= lastLine; i++) { + lines.push(model.getLine(i, true)); + } + + this.startCompoundChange(); + + this.setText(lines.join(indent), firstLineStart, lastLineEnd); + + let newSelectionStart = firstLineStart == selection.start ? + selection.start : selection.start + indent.length; + let newSelectionEnd = selection.end + (selectedLines * indent.length); + + this._view.setSelection(newSelectionStart, newSelectionEnd); + + this.endCompoundChange(); + return true; + } + + return false; + }, + + /** + * The "Unindent lines" editor action implementation. This method is invoked + * when the user presses Shift-Tab. + * @private + */ + _doUnindentLines: function SE__doUnindentLines() + { + if (this.readOnly) { + return true; + } + + let indent = "\t"; + + let selection = this.getSelection(); + let model = this._model; + let firstLine = model.getLineAtOffset(selection.start); + let lastLineOffset = selection.end > selection.start ? + selection.end - 1 : selection.end; + let lastLine = model.getLineAtOffset(lastLineOffset); + + if (this._expandTab) { + indent = (new Array(this._tabSize + 1)).join(" "); + } + + let lines = []; + for (let line, i = firstLine; i <= lastLine; i++) { + line = model.getLine(i, true); + if (line.indexOf(indent) != 0) { + return true; + } + lines.push(line.substring(indent.length)); + } + + let firstLineStart = this.getLineStart(firstLine); + let lastLineStart = this.getLineStart(lastLine); + let lastLineEnd = this.getLineEnd(lastLine, true); + + this.startCompoundChange(); + + this.setText(lines.join(""), firstLineStart, lastLineEnd); + + let selectedLines = lastLine - firstLine + 1; + let newSelectionStart = firstLineStart == selection.start ? + selection.start : + Math.max(firstLineStart, + selection.start - indent.length); + let newSelectionEnd = selection.end - (selectedLines * indent.length) + + (selection.end == lastLineStart + 1 ? 1 : 0); + if (firstLine == lastLine) { + newSelectionEnd = Math.max(lastLineStart, newSelectionEnd); + } + this._view.setSelection(newSelectionStart, newSelectionEnd); + + this.endCompoundChange(); + + return true; + }, + + /** + * The editor Enter action implementation, which adds simple automatic + * indentation based on the previous line when the user presses the Enter key. + * @private + */ + _doEnter: function SE__doEnter() + { + if (this.readOnly) { + return false; + } + + let selection = this.getSelection(); + if (selection.start != selection.end) { + return false; + } + + let model = this._model; + let lineIndex = model.getLineAtOffset(selection.start); + let lineText = model.getLine(lineIndex, true); + let lineStart = this.getLineStart(lineIndex); + let index = 0; + let lineOffset = selection.start - lineStart; + while (index < lineOffset && /[ \t]/.test(lineText.charAt(index))) { + index++; + } + + if (!index) { + return false; + } + + let prefix = lineText.substring(0, index); + index = lineOffset; + while (index < lineText.length && + /[ \t]/.test(lineText.charAt(index++))) { + selection.end++; + } + + this.setText(this.getLineDelimiter() + prefix, selection.start, + selection.end); + return true; + }, + + /** + * Move lines upwards or downwards, relative to the current caret location. + * + * @private + * @param boolean aLineAbove + * True if moving lines up, false to move lines down. + */ + _moveLines: function SE__moveLines(aLineAbove) + { + if (this.readOnly) { + return false; + } + + let model = this._model; + let selection = this.getSelection(); + let firstLine = model.getLineAtOffset(selection.start); + if (firstLine == 0 && aLineAbove) { + return true; + } + + let lastLine = model.getLineAtOffset(selection.end); + let firstLineStart = this.getLineStart(firstLine); + let lastLineStart = this.getLineStart(lastLine); + if (selection.start != selection.end && lastLineStart == selection.end) { + lastLine--; + } + if (!aLineAbove && (lastLine + 1) == this.getLineCount()) { + return true; + } + + let lastLineEnd = this.getLineEnd(lastLine, true); + let text = this.getText(firstLineStart, lastLineEnd); + + if (aLineAbove) { + let aboveLine = firstLine - 1; + let aboveLineStart = this.getLineStart(aboveLine); + + this.startCompoundChange(); + if (lastLine == (this.getLineCount() - 1)) { + let delimiterStart = this.getLineEnd(aboveLine); + let delimiterEnd = this.getLineEnd(aboveLine, true); + let lineDelimiter = this.getText(delimiterStart, delimiterEnd); + text += lineDelimiter; + this.setText("", firstLineStart - lineDelimiter.length, lastLineEnd); + } else { + this.setText("", firstLineStart, lastLineEnd); + } + this.setText(text, aboveLineStart, aboveLineStart); + this.endCompoundChange(); + this.setSelection(aboveLineStart, aboveLineStart + text.length); + } else { + let belowLine = lastLine + 1; + let belowLineEnd = this.getLineEnd(belowLine, true); + + let insertAt = belowLineEnd - lastLineEnd + firstLineStart; + let lineDelimiter = ""; + if (belowLine == this.getLineCount() - 1) { + let delimiterStart = this.getLineEnd(lastLine); + lineDelimiter = this.getText(delimiterStart, lastLineEnd); + text = lineDelimiter + text.substr(0, text.length - + lineDelimiter.length); + } + this.startCompoundChange(); + this.setText("", firstLineStart, lastLineEnd); + this.setText(text, insertAt, insertAt); + this.endCompoundChange(); + this.setSelection(insertAt + lineDelimiter.length, + insertAt + text.length); + } + return true; + }, + + /** + * The Orion Selection event handler. The current caret line is + * highlighted and for Linux users the selected text is copied into the X11 + * PRIMARY buffer. + * + * @private + * @param object aEvent + * The Orion Selection event object. + */ + _onOrionSelection: function SE__onOrionSelection(aEvent) + { + if (this._config.highlightCurrentLine) { + this._highlightCurrentLine(aEvent); + } + + if (Services.appinfo.OS == "Linux") { + let window = this.parentElement.ownerDocument.defaultView; + + if (this._primarySelectionTimeout) { + window.clearTimeout(this._primarySelectionTimeout); + } + this._primarySelectionTimeout = + window.setTimeout(this._updatePrimarySelection.bind(this), + PRIMARY_SELECTION_DELAY); + } + }, + + /** + * The TextChanged event handler which tracks the dirty state of the editor. + * + * @see SourceEditor.EVENTS.TEXT_CHANGED + * @see SourceEditor.EVENTS.DIRTY_CHANGED + * @see SourceEditor.dirty + * @private + */ + _onTextChanged: function SE__onTextChanged() + { + this._updateDirty(); + }, + + /** + * The Orion contextmenu event handler. This method opens the default or + * the custom context menu popup at the pointer location. + * + * @param object aEvent + * The contextmenu event object coming from Orion. This object should + * hold the screenX and screenY properties. + */ + _onOrionContextMenu: function SE__onOrionContextMenu(aEvent) + { + if (this._contextMenu.state == "closed") { + this._contextMenu.openPopupAtScreen(aEvent.screenX || 0, + aEvent.screenY || 0, true); + } + }, + + /** + * Update the dirty state of the editor based on the undo stack. + * @private + */ + _updateDirty: function SE__updateDirty() + { + this.dirty = !this._undoStack.isClean(); + }, + + /** + * Update the X11 PRIMARY buffer to hold the current selection. + * @private + */ + _updatePrimarySelection: function SE__updatePrimarySelection() + { + this._primarySelectionTimeout = null; + + let text = this.getSelectedText(); + if (!text) { + return; + } + + clipboardHelper.copyStringToClipboard(text, + Ci.nsIClipboard.kSelectionClipboard, + this.parentElement.ownerDocument); + }, + + /** + * Highlight the current line using the Orion annotation model. + * + * @private + * @param object aEvent + * The Selection event object. + */ + _highlightCurrentLine: function SE__highlightCurrentLine(aEvent) + { + let annotationModel = this._annotationModel; + let model = this._model; + let oldAnnotation = this._currentLineAnnotation; + let newSelection = aEvent.newValue; + + let collapsed = newSelection.start == newSelection.end; + if (!collapsed) { + if (oldAnnotation) { + annotationModel.removeAnnotation(oldAnnotation); + this._currentLineAnnotation = null; + } + return; + } + + let line = model.getLineAtOffset(newSelection.start); + let lineStart = this.getLineStart(line); + let lineEnd = this.getLineEnd(line); + + let title = oldAnnotation ? oldAnnotation.title : + SourceEditorUI.strings.GetStringFromName("annotation.currentLine"); + + this._currentLineAnnotation = { + start: lineStart, + end: lineEnd, + type: ORION_ANNOTATION_TYPES.currentLine, + title: title, + html: "
", + overviewStyle: {styleClass: "annotationOverview currentLine"}, + lineStyle: {styleClass: "annotationLine currentLine"}, + }; + + annotationModel.replaceAnnotations(oldAnnotation ? [oldAnnotation] : null, + [this._currentLineAnnotation]); + }, + + /** + * The click event handler for the lines gutter. This function allows the user + * to jump to a line or to perform line selection while holding the Shift key + * down. + * + * @private + * @param number aLineIndex + * The line index where the click event occurred. + * @param object aEvent + * The DOM click event object. + */ + _linesRulerClick: function SE__linesRulerClick(aLineIndex, aEvent) + { + if (aLineIndex === undefined || aLineIndex == -1) { + return; + } + + if (aEvent.shiftKey) { + let model = this._model; + let selection = this.getSelection(); + let selectionLineStart = model.getLineAtOffset(selection.start); + let selectionLineEnd = model.getLineAtOffset(selection.end); + let newStart = aLineIndex <= selectionLineStart ? + this.getLineStart(aLineIndex) : selection.start; + let newEnd = aLineIndex <= selectionLineStart ? + selection.end : this.getLineEnd(aLineIndex); + this.setSelection(newStart, newEnd); + } else { + if (this._annotationRuler) { + this._annotationRulerClick(aLineIndex, aEvent); + } else { + this.setCaretPosition(aLineIndex); + } + } + }, + + /** + * The dblclick event handler for the lines gutter. This function selects the + * whole line where the event occurred. + * + * @private + * @param number aLineIndex + * The line index where the double click event occurred. + * @param object aEvent + * The DOM dblclick event object. + */ + _linesRulerDblClick: function SE__linesRulerDblClick(aLineIndex) + { + if (aLineIndex === undefined) { + return; + } + + let newStart = this.getLineStart(aLineIndex); + let newEnd = this.getLineEnd(aLineIndex); + this.setSelection(newStart, newEnd); + }, + + /** + * Highlight the Orion annotations. This updates the annotation styler as + * needed. + * @private + */ + _highlightAnnotations: function SE__highlightAnnotations() + { + if (this._annotationStyler) { + this._annotationStyler.destroy(); + this._annotationStyler = null; + } + + let AnnotationStyler = + this._iframeWindow.require("orion/textview/annotations").AnnotationStyler; + + let styler = new AnnotationStyler(this._view, this._annotationModel); + this._annotationStyler = styler; + + styler.addAnnotationType(ORION_ANNOTATION_TYPES.matchingBracket); + styler.addAnnotationType(ORION_ANNOTATION_TYPES.currentBracket); + styler.addAnnotationType(ORION_ANNOTATION_TYPES.task); + styler.addAnnotationType(ORION_ANNOTATION_TYPES.debugLocation); + + if (this._config.highlightCurrentLine) { + styler.addAnnotationType(ORION_ANNOTATION_TYPES.currentLine); + } + }, + + /** + * Retrieve the list of Orion Annotations filtered by type for the given text range. + * + * @private + * @param string aType + * The annotation type to filter annotations for. Use one of the keys + * in ORION_ANNOTATION_TYPES. + * @param number aStart + * Offset from where to start finding the annotations. + * @param number aEnd + * End offset for retrieving the annotations. + * @return array + * The array of annotations, filtered by type, within the given text + * range. + */ + _getAnnotationsByType: function SE__getAnnotationsByType(aType, aStart, aEnd) + { + let annotations = this._annotationModel.getAnnotations(aStart, aEnd); + let annotation, result = []; + while (annotation = annotations.next()) { + if (annotation.type == ORION_ANNOTATION_TYPES[aType]) { + result.push(annotation); + } + } + + return result; + }, + + /** + * The click event handler for the annotation ruler. + * + * @private + * @param number aLineIndex + * The line index where the click event occurred. + * @param object aEvent + * The DOM click event object. + */ + _annotationRulerClick: function SE__annotationRulerClick(aLineIndex, aEvent) + { + if (aLineIndex === undefined || aLineIndex == -1) { + return; + } + + let lineStart = this.getLineStart(aLineIndex); + let lineEnd = this.getLineEnd(aLineIndex); + let annotations = this._getAnnotationsByType("breakpoint", lineStart, lineEnd); + if (annotations.length > 0) { + this.removeBreakpoint(aLineIndex); + } else { + this.addBreakpoint(aLineIndex); + } + }, + + /** + * The click event handler for the overview ruler. When the user clicks on an + * annotation the editor jumps to the associated line. + * + * @private + * @param number aLineIndex + * The line index where the click event occurred. + * @param object aEvent + * The DOM click event object. + */ + _overviewRulerClick: function SE__overviewRulerClick(aLineIndex, aEvent) + { + if (aLineIndex === undefined || aLineIndex == -1) { + return; + } + + let model = this._model; + let lineStart = this.getLineStart(aLineIndex); + let lineEnd = this.getLineEnd(aLineIndex); + let annotations = this._annotationModel.getAnnotations(lineStart, lineEnd); + let annotation = annotations.next(); + + // Jump to the line where annotation is. If the annotation is specific to + // a substring part of the line, then select the substring. + if (!annotation || lineStart == annotation.start && lineEnd == annotation.end) { + this.setSelection(lineStart, lineStart); + } else { + this.setSelection(annotation.start, annotation.end); + } + }, + + /** + * Get the editor element. + * + * @return nsIDOMElement + * In this implementation a xul:iframe holds the editor. + */ + get editorElement() { + return this._iframe; + }, + + /** + * Helper function to retrieve the strings used for comments in the current + * editor mode. + * + * @private + * @return object + * An object that holds the following properties: + * - line: the comment string used for the start of a single line + * comment. + * - blockStart: the comment string used for the start of a comment + * block. + * - blockEnd: the comment string used for the end of a block comment. + * Null is returned for unsupported editor modes. + */ + _getCommentStrings: function SE__getCommentStrings() + { + let line = ""; + let blockCommentStart = ""; + let blockCommentEnd = ""; + + switch (this.getMode()) { + case SourceEditor.MODES.JAVASCRIPT: + line = "//"; + blockCommentStart = "/*"; + blockCommentEnd = "*/"; + break; + case SourceEditor.MODES.CSS: + blockCommentStart = "/*"; + blockCommentEnd = "*/"; + break; + case SourceEditor.MODES.HTML: + case SourceEditor.MODES.XML: + blockCommentStart = ""; + break; + default: + return null; + } + return {line: line, blockStart: blockCommentStart, blockEnd: blockCommentEnd}; + }, + + /** + * Decide whether to comment the selection/current line or to uncomment it. + * + * @private + */ + _doCommentUncomment: function SE__doCommentUncomment() + { + if (this.readOnly) { + return false; + } + + let commentObject = this._getCommentStrings(); + if (!commentObject) { + return false; + } + + let selection = this.getSelection(); + let model = this._model; + let firstLine = model.getLineAtOffset(selection.start); + let lastLine = model.getLineAtOffset(selection.end); + + // Checks for block comment. + let firstLineText = model.getLine(firstLine); + let lastLineText = model.getLine(lastLine); + let openIndex = firstLineText.indexOf(commentObject.blockStart); + let closeIndex = lastLineText.lastIndexOf(commentObject.blockEnd); + if (openIndex != -1 && closeIndex != -1 && + (firstLine != lastLine || + (closeIndex - openIndex) >= commentObject.blockStart.length)) { + return this._doUncomment(); + } + + if (!commentObject.line) { + return this._doComment(); + } + + // If the selection is not a block comment, check for the first and the last + // lines to be line commented. + let firstLastCommented = [firstLineText, + lastLineText].every(function(aLineText) { + let openIndex = aLineText.indexOf(commentObject.line); + if (openIndex != -1) { + let textUntilComment = aLineText.slice(0, openIndex); + if (!textUntilComment || /^\s+$/.test(textUntilComment)) { + return true; + } + } + return false; + }); + if (firstLastCommented) { + return this._doUncomment(); + } + + // If we reach here, then we have to comment the selection/line. + return this._doComment(); + }, + + /** + * Wrap the selected text in comments. If nothing is selected the current + * caret line is commented out. Single line and block comments depend on the + * current editor mode. + * + * @private + */ + _doComment: function SE__doComment() + { + if (this.readOnly) { + return false; + } + + let commentObject = this._getCommentStrings(); + if (!commentObject) { + return false; + } + + let selection = this.getSelection(); + + if (selection.start == selection.end) { + let selectionLine = this._model.getLineAtOffset(selection.start); + let lineStartOffset = this.getLineStart(selectionLine); + if (commentObject.line) { + this.setText(commentObject.line, lineStartOffset, lineStartOffset); + } else { + let lineEndOffset = this.getLineEnd(selectionLine); + this.startCompoundChange(); + this.setText(commentObject.blockStart, lineStartOffset, lineStartOffset); + this.setText(commentObject.blockEnd, + lineEndOffset + commentObject.blockStart.length, + lineEndOffset + commentObject.blockStart.length); + this.endCompoundChange(); + } + } else { + this.startCompoundChange(); + this.setText(commentObject.blockStart, selection.start, selection.start); + this.setText(commentObject.blockEnd, + selection.end + commentObject.blockStart.length, + selection.end + commentObject.blockStart.length); + this.endCompoundChange(); + } + + return true; + }, + + /** + * Uncomment the selected text. If nothing is selected the current caret line + * is umcommented. Single line and block comments depend on the current editor + * mode. + * + * @private + */ + _doUncomment: function SE__doUncomment() + { + if (this.readOnly) { + return false; + } + + let commentObject = this._getCommentStrings(); + if (!commentObject) { + return false; + } + + let selection = this.getSelection(); + let firstLine = this._model.getLineAtOffset(selection.start); + let lastLine = this._model.getLineAtOffset(selection.end); + + // Uncomment a block of text. + let firstLineText = this._model.getLine(firstLine); + let lastLineText = this._model.getLine(lastLine); + let openIndex = firstLineText.indexOf(commentObject.blockStart); + let closeIndex = lastLineText.lastIndexOf(commentObject.blockEnd); + if (openIndex != -1 && closeIndex != -1 && + (firstLine != lastLine || + (closeIndex - openIndex) >= commentObject.blockStart.length)) { + let firstLineStartOffset = this.getLineStart(firstLine); + let lastLineStartOffset = this.getLineStart(lastLine); + let openOffset = firstLineStartOffset + openIndex; + let closeOffset = lastLineStartOffset + closeIndex; + + this.startCompoundChange(); + this.setText("", closeOffset, closeOffset + commentObject.blockEnd.length); + this.setText("", openOffset, openOffset + commentObject.blockStart.length); + this.endCompoundChange(); + + return true; + } + + if (!commentObject.line) { + return true; + } + + // If the selected text is not a block of comment, then uncomment each line. + this.startCompoundChange(); + let lineCaret = firstLine; + while (lineCaret <= lastLine) { + let currentLine = this._model.getLine(lineCaret); + let lineStart = this.getLineStart(lineCaret); + let openIndex = currentLine.indexOf(commentObject.line); + let openOffset = lineStart + openIndex; + let textUntilComment = this.getText(lineStart, openOffset); + if (openIndex != -1 && + (!textUntilComment || /^\s+$/.test(textUntilComment))) { + this.setText("", openOffset, openOffset + commentObject.line.length); + } + lineCaret++; + } + this.endCompoundChange(); + + return true; + }, + + /** + * Helper function for _moveToBracket{Opening/Closing} to find the offset of + * matching bracket. + * + * @param number aOffset + * The offset of the bracket for which you want to find the bracket. + * @private + */ + _getMatchingBracketIndex: function SE__getMatchingBracketIndex(aOffset) + { + return this._styler._findMatchingBracket(this._model, aOffset); + }, + + /** + * Move the cursor to the matching opening bracket if at corresponding closing + * bracket, otherwise move to the opening bracket for the current block of code. + * + * @private + */ + _moveToBracketOpening: function SE__moveToBracketOpening() + { + let mode = this.getMode(); + // Returning early if not in JavaScipt or CSS mode. + if (mode != SourceEditor.MODES.JAVASCRIPT && + mode != SourceEditor.MODES.CSS) { + return false; + } + + let caretOffset = this.getCaretOffset() - 1; + let matchingIndex = this._getMatchingBracketIndex(caretOffset); + + // If the caret is not at the closing bracket "}", find the index of the + // opening bracket "{" for the current code block. + if (matchingIndex == -1 || matchingIndex > caretOffset) { + matchingIndex = -1; + let text = this.getText(); + let closingOffset = text.indexOf("}", caretOffset); + while (closingOffset > -1) { + let closingMatchingIndex = this._getMatchingBracketIndex(closingOffset); + if (closingMatchingIndex < caretOffset && closingMatchingIndex != -1) { + matchingIndex = closingMatchingIndex; + break; + } + closingOffset = text.indexOf("}", closingOffset + 1); + } + // Moving to the previous code block starting bracket if caret not inside + // any code block. + if (matchingIndex == -1) { + let lastClosingOffset = text.lastIndexOf("}", caretOffset); + while (lastClosingOffset > -1) { + let closingMatchingIndex = + this._getMatchingBracketIndex(lastClosingOffset); + if (closingMatchingIndex < caretOffset && + closingMatchingIndex != -1) { + matchingIndex = closingMatchingIndex; + break; + } + lastClosingOffset = text.lastIndexOf("}", lastClosingOffset - 1); + } + } + } + + if (matchingIndex > -1) { + this.setCaretOffset(matchingIndex + 1); + } + + return true; + }, + + /** + * Moves the cursor to the matching closing bracket if at corresponding + * opening bracket, otherwise move to the closing bracket for the current + * block of code. + * + * @private + */ + _moveToBracketClosing: function SE__moveToBracketClosing() + { + let mode = this.getMode(); + // Returning early if not in JavaScipt or CSS mode. + if (mode != SourceEditor.MODES.JAVASCRIPT && + mode != SourceEditor.MODES.CSS) { + return false; + } + + let caretOffset = this.getCaretOffset(); + let matchingIndex = this._getMatchingBracketIndex(caretOffset - 1); + + // If the caret is not at the opening bracket "{", find the index of the + // closing bracket "}" for the current code block. + if (matchingIndex == -1 || matchingIndex < caretOffset) { + matchingIndex = -1; + let text = this.getText(); + let openingOffset = text.lastIndexOf("{", caretOffset); + while (openingOffset > -1) { + let openingMatchingIndex = this._getMatchingBracketIndex(openingOffset); + if (openingMatchingIndex > caretOffset) { + matchingIndex = openingMatchingIndex; + break; + } + openingOffset = text.lastIndexOf("{", openingOffset - 1); + } + // Moving to the next code block ending bracket if caret not inside + // any code block. + if (matchingIndex == -1) { + let nextOpeningIndex = text.indexOf("{", caretOffset + 1); + while (nextOpeningIndex > -1) { + let openingMatchingIndex = + this._getMatchingBracketIndex(nextOpeningIndex); + if (openingMatchingIndex > caretOffset) { + matchingIndex = openingMatchingIndex; + break; + } + nextOpeningIndex = text.indexOf("{", nextOpeningIndex + 1); + } + } + } + + if (matchingIndex > -1) { + this.setCaretOffset(matchingIndex); + } + + return true; + }, + + /** + * Add an event listener to the editor. You can use one of the known events. + * + * @see SourceEditor.EVENTS + * + * @param string aEventType + * The event type you want to listen for. + * @param function aCallback + * The function you want executed when the event is triggered. + */ + addEventListener: function SE_addEventListener(aEventType, aCallback) + { + if (this._view && aEventType in ORION_EVENTS) { + this._view.addEventListener(ORION_EVENTS[aEventType], aCallback); + } else if (this._eventTarget.addEventListener) { + this._eventTarget.addEventListener(aEventType, aCallback); + } else { + this._eventListenersQueue.push(["add", aEventType, aCallback]); + } + }, + + /** + * Remove an event listener from the editor. You can use one of the known + * events. + * + * @see SourceEditor.EVENTS + * + * @param string aEventType + * The event type you have a listener for. + * @param function aCallback + * The function you have as the event handler. + */ + removeEventListener: function SE_removeEventListener(aEventType, aCallback) + { + if (this._view && aEventType in ORION_EVENTS) { + this._view.removeEventListener(ORION_EVENTS[aEventType], aCallback); + } else if (this._eventTarget.removeEventListener) { + this._eventTarget.removeEventListener(aEventType, aCallback); + } else { + this._eventListenersQueue.push(["remove", aEventType, aCallback]); + } + }, + + /** + * Undo a change in the editor. + * + * @return boolean + * True if there was a change undone, false otherwise. + */ + undo: function SE_undo() + { + let result = this._undoStack.undo(); + this.ui._onUndoRedo(); + return result; + }, + + /** + * Redo a change in the editor. + * + * @return boolean + * True if there was a change redone, false otherwise. + */ + redo: function SE_redo() + { + let result = this._undoStack.redo(); + this.ui._onUndoRedo(); + return result; + }, + + /** + * Check if there are changes that can be undone. + * + * @return boolean + * True if there are changes that can be undone, false otherwise. + */ + canUndo: function SE_canUndo() + { + return this._undoStack.canUndo(); + }, + + /** + * Check if there are changes that can be repeated. + * + * @return boolean + * True if there are changes that can be repeated, false otherwise. + */ + canRedo: function SE_canRedo() + { + return this._undoStack.canRedo(); + }, + + /** + * Reset the Undo stack. + */ + resetUndo: function SE_resetUndo() + { + this._undoStack.reset(); + this._updateDirty(); + this.ui._onUndoRedo(); + }, + + /** + * Set the "dirty" state of the editor. Set this to false when you save the + * text being edited. The dirty state will become true once the user makes + * changes to the text. + * + * @param boolean aNewValue + * The new dirty state: true if the text is not saved, false if you + * just saved the text. + */ + set dirty(aNewValue) + { + if (aNewValue == this._dirty) { + return; + } + + let event = { + type: SourceEditor.EVENTS.DIRTY_CHANGED, + oldValue: this._dirty, + newValue: aNewValue, + }; + + this._dirty = aNewValue; + if (!this._dirty && !this._undoStack.isClean()) { + this._undoStack.markClean(); + } + this._dispatchEvent(event); + }, + + /** + * Get the editor "dirty" state. This tells if the text is considered saved or + * not. + * + * @see SourceEditor.EVENTS.DIRTY_CHANGED + * @return boolean + * True if there are changes which are not saved, false otherwise. + */ + get dirty() + { + return this._dirty; + }, + + /** + * Start a compound change in the editor. Compound changes are grouped into + * only one change that you can undo later, after you invoke + * endCompoundChange(). + */ + startCompoundChange: function SE_startCompoundChange() + { + this._undoStack.startCompoundChange(); + }, + + /** + * End a compound change in the editor. + */ + endCompoundChange: function SE_endCompoundChange() + { + this._undoStack.endCompoundChange(); + }, + + /** + * Focus the editor. + */ + focus: function SE_focus() + { + this._view.focus(); + }, + + /** + * Get the first visible line number. + * + * @return number + * The line number, counting from 0. + */ + getTopIndex: function SE_getTopIndex() + { + return this._view.getTopIndex(); + }, + + /** + * Set the first visible line number. + * + * @param number aTopIndex + * The line number, counting from 0. + */ + setTopIndex: function SE_setTopIndex(aTopIndex) + { + this._view.setTopIndex(aTopIndex); + }, + + /** + * Check if the editor has focus. + * + * @return boolean + * True if the editor is focused, false otherwise. + */ + hasFocus: function SE_hasFocus() + { + return this._view.hasFocus(); + }, + + /** + * Get the editor content, in the given range. If no range is given you get + * the entire editor content. + * + * @param number [aStart=0] + * Optional, start from the given offset. + * @param number [aEnd=content char count] + * Optional, end offset for the text you want. If this parameter is not + * given, then the text returned goes until the end of the editor + * content. + * @return string + * The text in the given range. + */ + getText: function SE_getText(aStart, aEnd) + { + return this._view.getText(aStart, aEnd); + }, + + /** + * Get the start character offset of the line with index aLineIndex. + * + * @param number aLineIndex + * Zero based index of the line. + * @return number + * Line start offset or -1 if out of range. + */ + getLineStart: function SE_getLineStart(aLineIndex) + { + return this._model.getLineStart(aLineIndex); + }, + + /** + * Get the end character offset of the line with index aLineIndex, + * excluding the end offset. When the line delimiter is present, + * the offset is the start offset of the next line or the char count. + * Otherwise, it is the offset of the line delimiter. + * + * @param number aLineIndex + * Zero based index of the line. + * @param boolean [aIncludeDelimiter = false] + * Optional, whether or not to include the line delimiter. + * @return number + * Line end offset or -1 if out of range. + */ + getLineEnd: function SE_getLineEnd(aLineIndex, aIncludeDelimiter) + { + return this._model.getLineEnd(aLineIndex, aIncludeDelimiter); + }, + + /** + * Get the number of characters in the editor content. + * + * @return number + * The number of editor content characters. + */ + getCharCount: function SE_getCharCount() + { + return this._model.getCharCount(); + }, + + /** + * Get the selected text. + * + * @return string + * The currently selected text. + */ + getSelectedText: function SE_getSelectedText() + { + let selection = this.getSelection(); + return this.getText(selection.start, selection.end); + }, + + /** + * Replace text in the source editor with the given text, in the given range. + * + * @param string aText + * The text you want to put into the editor. + * @param number [aStart=0] + * Optional, the start offset, zero based, from where you want to start + * replacing text in the editor. + * @param number [aEnd=char count] + * Optional, the end offset, zero based, where you want to stop + * replacing text in the editor. + */ + setText: function SE_setText(aText, aStart, aEnd) + { + this._view.setText(aText, aStart, aEnd); + }, + + /** + * Drop the current selection / deselect. + */ + dropSelection: function SE_dropSelection() + { + this.setCaretOffset(this.getCaretOffset()); + }, + + /** + * Select a specific range in the editor. + * + * @param number aStart + * Selection range start. + * @param number aEnd + * Selection range end. + */ + setSelection: function SE_setSelection(aStart, aEnd) + { + this._view.setSelection(aStart, aEnd, true); + }, + + /** + * Get the current selection range. + * + * @return object + * An object with two properties, start and end, that give the + * selection range (zero based offsets). + */ + getSelection: function SE_getSelection() + { + return this._view.getSelection(); + }, + + /** + * Get the current caret offset. + * + * @return number + * The current caret offset. + */ + getCaretOffset: function SE_getCaretOffset() + { + return this._view.getCaretOffset(); + }, + + /** + * Set the caret offset. + * + * @param number aOffset + * The new caret offset you want to set. + */ + setCaretOffset: function SE_setCaretOffset(aOffset) + { + this._view.setCaretOffset(aOffset, true); + }, + + /** + * Get the caret position. + * + * @return object + * An object that holds two properties: + * - line: the line number, counting from 0. + * - col: the column number, counting from 0. + */ + getCaretPosition: function SE_getCaretPosition() + { + let offset = this.getCaretOffset(); + let line = this._model.getLineAtOffset(offset); + let lineStart = this.getLineStart(line); + let column = offset - lineStart; + return {line: line, col: column}; + }, + + /** + * Set the caret position: line and column. + * + * @param number aLine + * The new caret line location. Line numbers start from 0. + * @param number [aColumn=0] + * Optional. The new caret column location. Columns start from 0. + * @param number [aAlign=0] + * Optional. Position of the line with respect to viewport. + * Allowed values are: + * SourceEditor.VERTICAL_ALIGN.TOP target line at top of view. + * SourceEditor.VERTICAL_ALIGN.CENTER target line at center of view. + * SourceEditor.VERTICAL_ALIGN.BOTTOM target line at bottom of view. + */ + setCaretPosition: function SE_setCaretPosition(aLine, aColumn, aAlign) + { + let editorHeight = this._view.getClientArea().height; + let lineHeight = this._view.getLineHeight(); + let linesVisible = Math.floor(editorHeight/lineHeight); + let halfVisible = Math.round(linesVisible/2); + let firstVisible = this.getTopIndex(); + let lastVisible = this._view.getBottomIndex(); + let caretOffset = this.getLineStart(aLine) + (aColumn || 0); + + this._view.setSelection(caretOffset, caretOffset, false); + + // If the target line is in view, skip the vertical alignment part. + if (aLine <= lastVisible && aLine >= firstVisible) { + this._view.showSelection(); + return; + } + + // Setting the offset so that the line always falls in the upper half + // of visible lines (lower half for BOTTOM aligned). + // VERTICAL_OFFSET is the maximum allowed value. + let offset = Math.min(halfVisible, VERTICAL_OFFSET); + + let topIndex; + switch (aAlign) { + case this.VERTICAL_ALIGN.CENTER: + topIndex = Math.max(aLine - halfVisible, 0); + break; + + case this.VERTICAL_ALIGN.BOTTOM: + topIndex = Math.max(aLine - linesVisible + offset, 0); + break; + + default: // this.VERTICAL_ALIGN.TOP. + topIndex = Math.max(aLine - offset, 0); + break; + } + // Bringing down the topIndex to total lines in the editor if exceeding. + topIndex = Math.min(topIndex, this.getLineCount()); + this.setTopIndex(topIndex); + + let location = this._view.getLocationAtOffset(caretOffset); + this._view.setHorizontalPixel(location.x); + }, + + /** + * Get the line count. + * + * @return number + * The number of lines in the document being edited. + */ + getLineCount: function SE_getLineCount() + { + return this._model.getLineCount(); + }, + + /** + * Get the line delimiter used in the document being edited. + * + * @return string + * The line delimiter. + */ + getLineDelimiter: function SE_getLineDelimiter() + { + return this._model.getLineDelimiter(); + }, + + /** + * Get the indentation string used in the document being edited. + * + * @return string + * The indentation string. + */ + getIndentationString: function SE_getIndentationString() + { + if (this._expandTab) { + return (new Array(this._tabSize + 1)).join(" "); + } + return "\t"; + }, + + /** + * Set the source editor mode to the file type you are editing. + * + * @param string aMode + * One of the predefined SourceEditor.MODES. + */ + setMode: function SE_setMode(aMode) + { + if (this._styler) { + this._styler.destroy(); + this._styler = null; + } + + let window = this._iframeWindow; + + switch (aMode) { + case SourceEditor.MODES.JAVASCRIPT: + case SourceEditor.MODES.CSS: + let TextStyler = + window.require("examples/textview/textStyler").TextStyler; + + this._styler = new TextStyler(this._view, aMode, this._annotationModel); + this._styler.setFoldingEnabled(false); + break; + + case SourceEditor.MODES.HTML: + case SourceEditor.MODES.XML: + let TextMateStyler = + window.require("orion/editor/textMateStyler").TextMateStyler; + let HtmlGrammar = + window.require("orion/editor/htmlGrammar").HtmlGrammar; + this._styler = new TextMateStyler(this._view, new HtmlGrammar()); + break; + } + + this._highlightAnnotations(); + this._mode = aMode; + }, + + /** + * Get the current source editor mode. + * + * @return string + * Returns one of the predefined SourceEditor.MODES. + */ + getMode: function SE_getMode() + { + return this._mode; + }, + + /** + * Setter for the read-only state of the editor. + * @param boolean aValue + * Tells if you want the editor to read-only or not. + */ + set readOnly(aValue) + { + this._view.setOptions({ + readonly: aValue, + themeClass: "mozilla" + (aValue ? " readonly" : ""), + }); + }, + + /** + * Getter for the read-only state of the editor. + * @type boolean + */ + get readOnly() + { + return this._view.getOptions("readonly"); + }, + + /** + * Set the current debugger location at the given line index. This is useful in + * a debugger or in any other context where the user needs to track the + * current state, where a debugger-like environment is at. + * + * @param number aLineIndex + * Line index of the current debugger location, starting from 0. + * Use any negative number to clear the current location. + */ + setDebugLocation: function SE_setDebugLocation(aLineIndex) + { + let annotations = this._getAnnotationsByType("debugLocation", 0, + this.getCharCount()); + if (annotations.length > 0) { + annotations.forEach(this._annotationModel.removeAnnotation, + this._annotationModel); + } + if (aLineIndex < 0) { + return; + } + + let lineStart = this._model.getLineStart(aLineIndex); + let lineEnd = this._model.getLineEnd(aLineIndex); + let lineText = this._model.getLine(aLineIndex); + let title = SourceEditorUI.strings. + formatStringFromName("annotation.debugLocation.title", + [lineText], 1); + + let annotation = { + type: ORION_ANNOTATION_TYPES.debugLocation, + start: lineStart, + end: lineEnd, + title: title, + style: {styleClass: "annotation debugLocation"}, + html: "
", + overviewStyle: {styleClass: "annotationOverview debugLocation"}, + rangeStyle: {styleClass: "annotationRange debugLocation"}, + lineStyle: {styleClass: "annotationLine debugLocation"}, + }; + this._annotationModel.addAnnotation(annotation); + }, + + /** + * Retrieve the current debugger line index configured for this editor. + * + * @return number + * The line index starting from 0 where the current debugger is + * paused. If no debugger location has been set -1 is returned. + */ + getDebugLocation: function SE_getDebugLocation() + { + let annotations = this._getAnnotationsByType("debugLocation", 0, + this.getCharCount()); + if (annotations.length > 0) { + return this._model.getLineAtOffset(annotations[0].start); + } + return -1; + }, + + /** + * Add a breakpoint at the given line index. + * + * @param number aLineIndex + * Line index where to add the breakpoint (starts from 0). + * @param string [aCondition] + * Optional breakpoint condition. + */ + addBreakpoint: function SE_addBreakpoint(aLineIndex, aCondition) + { + let lineStart = this.getLineStart(aLineIndex); + let lineEnd = this.getLineEnd(aLineIndex); + + let annotations = this._getAnnotationsByType("breakpoint", lineStart, lineEnd); + if (annotations.length > 0) { + return; + } + + let lineText = this._model.getLine(aLineIndex); + let title = SourceEditorUI.strings. + formatStringFromName("annotation.breakpoint.title", + [lineText], 1); + + let annotation = { + type: ORION_ANNOTATION_TYPES.breakpoint, + start: lineStart, + end: lineEnd, + breakpointCondition: aCondition, + title: title, + style: {styleClass: "annotation breakpoint"}, + html: "
", + overviewStyle: {styleClass: "annotationOverview breakpoint"}, + rangeStyle: {styleClass: "annotationRange breakpoint"} + }; + this._annotationModel.addAnnotation(annotation); + + let event = { + type: SourceEditor.EVENTS.BREAKPOINT_CHANGE, + added: [{line: aLineIndex, condition: aCondition}], + removed: [], + }; + + this._dispatchEvent(event); + }, + + /** + * Remove the current breakpoint from the given line index. + * + * @param number aLineIndex + * Line index from where to remove the breakpoint (starts from 0). + * @return boolean + * True if a breakpoint was removed, false otherwise. + */ + removeBreakpoint: function SE_removeBreakpoint(aLineIndex) + { + let lineStart = this.getLineStart(aLineIndex); + let lineEnd = this.getLineEnd(aLineIndex); + + let event = { + type: SourceEditor.EVENTS.BREAKPOINT_CHANGE, + added: [], + removed: [], + }; + + let annotations = this._getAnnotationsByType("breakpoint", lineStart, lineEnd); + + annotations.forEach(function(annotation) { + this._annotationModel.removeAnnotation(annotation); + event.removed.push({line: aLineIndex, + condition: annotation.breakpointCondition}); + }, this); + + if (event.removed.length > 0) { + this._dispatchEvent(event); + } + + return event.removed.length > 0; + }, + + /** + * Get the list of breakpoints in the Source Editor instance. + * + * @return array + * The array of breakpoints. Each item is an object with two + * properties: line and condition. + */ + getBreakpoints: function SE_getBreakpoints() + { + let annotations = this._getAnnotationsByType("breakpoint", 0, + this.getCharCount()); + let breakpoints = []; + + annotations.forEach(function(annotation) { + breakpoints.push({line: this._model.getLineAtOffset(annotation.start), + condition: annotation.breakpointCondition}); + }, this); + + return breakpoints; + }, + + /** + * Convert the given rectangle from one coordinate reference to another. + * + * Known coordinate references: + * - "document" - gives the coordinates relative to the entire document. + * - "view" - gives the coordinates relative to the editor viewport. + * + * @param object aRect + * The rectangle to convert. Object properties: x, y, width and height. + * @param string aFrom + * The source coordinate reference. + * @param string aTo + * The destination coordinate reference. + * @return object aRect + * Returns the rectangle with changed coordinates. + */ + convertCoordinates: function SE_convertCoordinates(aRect, aFrom, aTo) + { + return this._view.convert(aRect, aFrom, aTo); + }, + + /** + * Get the character offset nearest to the given pixel location. + * + * @param number aX + * @param number aY + * @return number + * Returns the character offset at the given location. + */ + getOffsetAtLocation: function SE_getOffsetAtLocation(aX, aY) + { + return this._view.getOffsetAtLocation(aX, aY); + }, + + /** + * Get the pixel location, relative to the document, at the given character + * offset. + * + * @param number aOffset + * @return object + * The pixel location relative to the document being edited. Two + * properties are included: x and y. + */ + getLocationAtOffset: function SE_getLocationAtOffset(aOffset) + { + return this._view.getLocationAtOffset(aOffset); + }, + + /** + * Get the line location for a given character offset. + * + * @param number aOffset + * @return number + * The line location relative to the give character offset. + */ + getLineAtOffset: function SE_getLineAtOffset(aOffset) + { + return this._model.getLineAtOffset(aOffset); + }, + + /** + * Destroy/uninitialize the editor. + */ + destroy: function SE_destroy() + { + if (this._config.highlightCurrentLine || Services.appinfo.OS == "Linux") { + this.removeEventListener(SourceEditor.EVENTS.SELECTION, + this._onOrionSelection); + } + this._onOrionSelection = null; + + this.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED, + this._onTextChanged); + this._onTextChanged = null; + + if (this._contextMenu) { + this.removeEventListener(SourceEditor.EVENTS.CONTEXT_MENU, + this._onOrionContextMenu); + this._contextMenu = null; + } + this._onOrionContextMenu = null; + + if (this._primarySelectionTimeout) { + let window = this.parentElement.ownerDocument.defaultView; + window.clearTimeout(this._primarySelectionTimeout); + this._primarySelectionTimeout = null; + } + + this._view.destroy(); + this.ui.destroy(); + this.ui = null; + + this.parentElement.removeChild(this._iframe); + this.parentElement = null; + this._iframeWindow = null; + this._iframe = null; + this._undoStack = null; + this._styler = null; + this._linesRuler = null; + this._annotationRuler = null; + this._overviewRuler = null; + this._dragAndDrop = null; + this._annotationModel = null; + this._annotationStyler = null; + this._currentLineAnnotation = null; + this._eventTarget = null; + this._eventListenersQueue = null; + this._view = null; + this._model = null; + this._config = null; + this._lastFind = null; + }, +}; diff --git a/browser/devtools/sourceeditor/source-editor-overlay.xul b/browser/devtools/sourceeditor/source-editor-overlay.xul new file mode 100644 index 000000000000..e4fb7c91f7f9 --- /dev/null +++ b/browser/devtools/sourceeditor/source-editor-overlay.xul @@ -0,0 +1,204 @@ + + + + %editMenuStrings; + + %sourceEditorStrings; +]> + + + + + + + + + + + + + + + + + + + + + + +#ifdef XP_UNIX + +#else + +#endif + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/devtools/sourceeditor/source-editor-ui.jsm b/browser/devtools/sourceeditor/source-editor-ui.jsm new file mode 100644 index 000000000000..8b74d16238f0 --- /dev/null +++ b/browser/devtools/sourceeditor/source-editor-ui.jsm @@ -0,0 +1,332 @@ +/* vim:set ts=2 sw=2 sts=2 et tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +this.EXPORTED_SYMBOLS = ["SourceEditorUI"]; + +/** + * The Source Editor component user interface. + */ +this.SourceEditorUI = function SourceEditorUI(aEditor) +{ + this.editor = aEditor; + this._onDirtyChanged = this._onDirtyChanged.bind(this); +} + +SourceEditorUI.prototype = { + /** + * Initialize the user interface. This is called by the SourceEditor.init() + * method. + */ + init: function SEU_init() + { + this._ownerWindow = this.editor.parentElement.ownerDocument.defaultView; + }, + + /** + * The UI onReady function is executed once the Source Editor completes + * initialization and it is ready for usage. Currently this code sets up the + * nsIController. + */ + onReady: function SEU_onReady() + { + if (this._ownerWindow.controllers) { + this._controller = new SourceEditorController(this.editor); + this._ownerWindow.controllers.insertControllerAt(0, this._controller); + this.editor.addEventListener(this.editor.EVENTS.DIRTY_CHANGED, + this._onDirtyChanged); + } + }, + + /** + * The "go to line" command UI. This displays a prompt that allows the user to + * input the line number to jump to. + */ + gotoLine: function SEU_gotoLine() + { + let oldLine = this.editor.getCaretPosition ? + this.editor.getCaretPosition().line : null; + let newLine = {value: oldLine !== null ? oldLine + 1 : ""}; + + let result = Services.prompt.prompt(this._ownerWindow, + SourceEditorUI.strings.GetStringFromName("gotoLineCmd.promptTitle"), + SourceEditorUI.strings.GetStringFromName("gotoLineCmd.promptMessage"), + newLine, null, {}); + + newLine.value = parseInt(newLine.value); + if (result && !isNaN(newLine.value) && --newLine.value != oldLine) { + if (this.editor.getLineCount) { + let lines = this.editor.getLineCount() - 1; + this.editor.setCaretPosition(Math.max(0, Math.min(lines, newLine.value))); + } else { + this.editor.setCaretPosition(Math.max(0, newLine.value)); + } + } + + return true; + }, + + /** + * The "find" command UI. This displays a prompt that allows the user to input + * the string to search for in the code. By default the current selection is + * used as a search string, or the last search string. + */ + find: function SEU_find() + { + let str = {value: this.editor.getSelectedText()}; + if (!str.value && this.editor.lastFind) { + str.value = this.editor.lastFind.str; + } + + let result = Services.prompt.prompt(this._ownerWindow, + SourceEditorUI.strings.GetStringFromName("findCmd.promptTitle"), + SourceEditorUI.strings.GetStringFromName("findCmd.promptMessage"), + str, null, {}); + + if (result && str.value) { + let start = this.editor.getSelection().end; + let pos = this.editor.find(str.value, {ignoreCase: true, start: start}); + if (pos == -1) { + this.editor.find(str.value, {ignoreCase: true}); + } + this._onFind(); + } + + return true; + }, + + /** + * Find the next occurrence of the last search string. + */ + findNext: function SEU_findNext() + { + let lastFind = this.editor.lastFind; + if (lastFind) { + this.editor.findNext(true); + this._onFind(); + } + + return true; + }, + + /** + * Find the previous occurrence of the last search string. + */ + findPrevious: function SEU_findPrevious() + { + let lastFind = this.editor.lastFind; + if (lastFind) { + this.editor.findPrevious(true); + this._onFind(); + } + + return true; + }, + + /** + * This executed after each find/findNext/findPrevious operation. + * @private + */ + _onFind: function SEU__onFind() + { + let lastFind = this.editor.lastFind; + if (lastFind && lastFind.index > -1) { + this.editor.setSelection(lastFind.index, lastFind.index + lastFind.str.length); + } + + if (this._ownerWindow.goUpdateCommand) { + this._ownerWindow.goUpdateCommand("cmd_findAgain"); + this._ownerWindow.goUpdateCommand("cmd_findPrevious"); + } + }, + + /** + * This is executed after each undo/redo operation. + * @private + */ + _onUndoRedo: function SEU__onUndoRedo() + { + if (this._ownerWindow.goUpdateCommand) { + this._ownerWindow.goUpdateCommand("se-cmd-undo"); + this._ownerWindow.goUpdateCommand("se-cmd-redo"); + } + }, + + /** + * The DirtyChanged event handler for the editor. This tracks the editor state + * changes to make sure the Source Editor overlay Undo/Redo commands are kept + * up to date. + * @private + */ + _onDirtyChanged: function SEU__onDirtyChanged() + { + this._onUndoRedo(); + }, + + /** + * Destroy the SourceEditorUI instance. This is called by the + * SourceEditor.destroy() method. + */ + destroy: function SEU_destroy() + { + if (this._ownerWindow.controllers) { + this.editor.removeEventListener(this.editor.EVENTS.DIRTY_CHANGED, + this._onDirtyChanged); + } + + this._ownerWindow = null; + this.editor = null; + this._controller = null; + }, +}; + +/** + * The Source Editor nsIController implements features that need to be available + * from XUL commands. + * + * @constructor + * @param object aEditor + * SourceEditor object instance for which the controller is instanced. + */ +function SourceEditorController(aEditor) +{ + this._editor = aEditor; +} + +SourceEditorController.prototype = { + /** + * Check if a command is supported by the controller. + * + * @param string aCommand + * The command name you want to check support for. + * @return boolean + * True if the command is supported, false otherwise. + */ + supportsCommand: function SEC_supportsCommand(aCommand) + { + let result; + + switch (aCommand) { + case "cmd_find": + case "cmd_findAgain": + case "cmd_findPrevious": + case "cmd_gotoLine": + case "se-cmd-undo": + case "se-cmd-redo": + case "se-cmd-cut": + case "se-cmd-paste": + case "se-cmd-delete": + case "se-cmd-selectAll": + result = true; + break; + default: + result = false; + break; + } + + return result; + }, + + /** + * Check if a command is enabled or not. + * + * @param string aCommand + * The command name you want to check if it is enabled or not. + * @return boolean + * True if the command is enabled, false otherwise. + */ + isCommandEnabled: function SEC_isCommandEnabled(aCommand) + { + let result; + + switch (aCommand) { + case "cmd_find": + case "cmd_gotoLine": + case "se-cmd-selectAll": + result = true; + break; + case "cmd_findAgain": + case "cmd_findPrevious": + result = this._editor.lastFind && this._editor.lastFind.lastFound != -1; + break; + case "se-cmd-undo": + result = this._editor.canUndo(); + break; + case "se-cmd-redo": + result = this._editor.canRedo(); + break; + case "se-cmd-cut": + case "se-cmd-delete": { + let selection = this._editor.getSelection(); + result = selection.start != selection.end && !this._editor.readOnly; + break; + } + case "se-cmd-paste": { + let window = this._editor._view._frameWindow; + let controller = window.controllers.getControllerForCommand("cmd_paste"); + result = !this._editor.readOnly && + controller.isCommandEnabled("cmd_paste"); + break; + } + default: + result = false; + break; + } + + return result; + }, + + /** + * Perform a command. + * + * @param string aCommand + * The command name you want to execute. + * @return void + */ + doCommand: function SEC_doCommand(aCommand) + { + switch (aCommand) { + case "cmd_find": + this._editor.ui.find(); + break; + case "cmd_findAgain": + this._editor.ui.findNext(); + break; + case "cmd_findPrevious": + this._editor.ui.findPrevious(); + break; + case "cmd_gotoLine": + this._editor.ui.gotoLine(); + break; + case "se-cmd-selectAll": + this._editor._view.invokeAction("selectAll"); + break; + case "se-cmd-undo": + this._editor.undo(); + break; + case "se-cmd-redo": + this._editor.redo(); + break; + case "se-cmd-cut": + this._editor.ui._ownerWindow.goDoCommand("cmd_cut"); + break; + case "se-cmd-paste": + this._editor.ui._ownerWindow.goDoCommand("cmd_paste"); + break; + case "se-cmd-delete": { + let selection = this._editor.getSelection(); + this._editor.setText("", selection.start, selection.end); + break; + } + } + }, + + onEvent: function() { } +}; diff --git a/browser/devtools/sourceeditor/source-editor.jsm b/browser/devtools/sourceeditor/source-editor.jsm new file mode 100644 index 000000000000..deb0f4455df6 --- /dev/null +++ b/browser/devtools/sourceeditor/source-editor.jsm @@ -0,0 +1,455 @@ +/* vim:set ts=2 sw=2 sts=2 et tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/devtools/sourceeditor/source-editor-ui.jsm"); + +const PREF_EDITOR_COMPONENT = "devtools.editor.component"; +const SOURCEEDITOR_L10N = "chrome://browser/locale/devtools/sourceeditor.properties"; + +var component = Services.prefs.getCharPref(PREF_EDITOR_COMPONENT); +var obj = {}; +try { + if (component == "ui") { + throw new Error("The ui editor component is not available."); + } + Cu.import("resource:///modules/devtools/sourceeditor/source-editor-" + component + ".jsm", obj); +} catch (ex) { + Cu.reportError(ex); + Cu.reportError("SourceEditor component failed to load: " + component); + + // If the component does not exist, clear the user pref back to the default. + Services.prefs.clearUserPref(PREF_EDITOR_COMPONENT); + + // Load the default editor component. + component = Services.prefs.getCharPref(PREF_EDITOR_COMPONENT); + Cu.import("resource:///modules/devtools/sourceeditor/source-editor-" + component + ".jsm", obj); +} + +// Export the SourceEditor. +this.SourceEditor = obj.SourceEditor; +this.EXPORTED_SYMBOLS = ["SourceEditor"]; + +// Add the constants used by all SourceEditors. + +XPCOMUtils.defineLazyGetter(SourceEditorUI, "strings", function() { + return Services.strings.createBundle(SOURCEEDITOR_L10N); +}); + +/** + * Known SourceEditor preferences. + */ +SourceEditor.PREFS = { + TAB_SIZE: "devtools.editor.tabsize", + EXPAND_TAB: "devtools.editor.expandtab", + COMPONENT: PREF_EDITOR_COMPONENT, +}; + +/** + * Predefined source editor modes for JavaScript, CSS and other languages. + */ +SourceEditor.MODES = { + JAVASCRIPT: "js", + CSS: "css", + TEXT: "text", + HTML: "html", + XML: "xml", +}; + +/** + * Predefined themes for syntax highlighting. + */ +SourceEditor.THEMES = { + MOZILLA: "mozilla", +}; + +/** + * Source editor configuration defaults. + * @see SourceEditor.init + */ +SourceEditor.DEFAULTS = { + /** + * The text you want shown when the editor opens up. + * @type string + */ + initialText: "", + + /** + * The editor mode, based on the file type you want to edit. You can use one of + * the predefined modes. + * + * @see SourceEditor.MODES + * @type string + */ + mode: SourceEditor.MODES.TEXT, + + /** + * The syntax highlighting theme you want. You can use one of the predefined + * themes, or you can point to your CSS file. + * + * @see SourceEditor.THEMES. + * @type string + */ + theme: SourceEditor.THEMES.MOZILLA, + + /** + * How many steps should the undo stack hold. + * @type number + */ + undoLimit: 200, + + /** + * Define how many spaces to use for a tab character. This value is overridden + * by a user preference, see SourceEditor.PREFS.TAB_SIZE. + * + * @type number + */ + tabSize: 4, + + /** + * Tells if you want tab characters to be expanded to spaces. This value is + * overridden by a user preference, see SourceEditor.PREFS.EXPAND_TAB. + * @type boolean + */ + expandTab: true, + + /** + * Tells if you want the editor to be read only or not. + * @type boolean + */ + readOnly: false, + + /** + * Display the line numbers gutter. + * @type boolean + */ + showLineNumbers: false, + + /** + * Display the annotations gutter/ruler. This gutter currently supports + * annotations of breakpoint type. + * @type boolean + */ + showAnnotationRuler: false, + + /** + * Display the overview gutter/ruler. This gutter presents an overview of the + * current annotations in the editor, for example the breakpoints. + * @type boolean + */ + showOverviewRuler: false, + + /** + * Highlight the current line. + * @type boolean + */ + highlightCurrentLine: true, + + /** + * An array of objects that allows you to define custom editor keyboard + * bindings. Each object can have: + * - action - name of the editor action to invoke. + * - code - keyCode for the shortcut. + * - accel - boolean for the Accel key (Cmd on Macs, Ctrl on Linux/Windows). + * - ctrl - boolean for the Control key + * - shift - boolean for the Shift key. + * - alt - boolean for the Alt key. + * - callback - optional function to invoke, if the action is not predefined + * in the editor. + * @type array + */ + keys: null, + + /** + * The editor context menu you want to display when the user right-clicks + * within the editor. This property can be: + * - a string that tells the ID of the xul:menupopup you want. This needs to + * be available within the editor parentElement.ownerDocument. + * - an nsIDOMElement object reference pointing to the xul:menupopup you + * want to open when the contextmenu event is fired. + * + * Set this property to a falsey value to disable the default context menu. + * + * @see SourceEditor.EVENTS.CONTEXT_MENU for more control over the contextmenu + * event. + * @type string|nsIDOMElement + */ + contextMenu: "sourceEditorContextMenu", +}; + +/** + * Known editor events you can listen for. + */ +SourceEditor.EVENTS = { + /** + * The contextmenu event is fired when the editor context menu is invoked. The + * event object properties: + * - x - the pointer location on the x axis, relative to the document the + * user is editing. + * - y - the pointer location on the y axis, relative to the document the + * user is editing. + * - screenX - the pointer location on the x axis, relative to the screen. + * This value comes from the DOM contextmenu event.screenX property. + * - screenY - the pointer location on the y axis, relative to the screen. + * This value comes from the DOM contextmenu event.screenY property. + * + * @see SourceEditor.DEFAULTS.contextMenu + */ + CONTEXT_MENU: "ContextMenu", + + /** + * The TextChanged event is fired when the editor content changes. The event + * object properties: + * - start - the character offset in the document where the change has + * occured. + * - removedCharCount - the number of characters removed from the document. + * - addedCharCount - the number of characters added to the document. + */ + TEXT_CHANGED: "TextChanged", + + /** + * The Selection event is fired when the editor selection changes. The event + * object properties: + * - oldValue - the old selection range. + * - newValue - the new selection range. + * Both ranges are objects which hold two properties: start and end. + */ + SELECTION: "Selection", + + /** + * The focus event is fired when the editor is focused. + */ + FOCUS: "Focus", + + /** + * The blur event is fired when the editor goes out of focus. + */ + BLUR: "Blur", + + /** + * The MouseMove event is sent when the user moves the mouse over a line. + * The event object properties: + * - event - the DOM mousemove event object. + * - x and y - the mouse coordinates relative to the document being edited. + */ + MOUSE_MOVE: "MouseMove", + + /** + * The MouseOver event is sent when the mouse pointer enters a line. + * The event object properties: + * - event - the DOM mouseover event object. + * - x and y - the mouse coordinates relative to the document being edited. + */ + MOUSE_OVER: "MouseOver", + + /** + * This MouseOut event is sent when the mouse pointer exits a line. + * The event object properties: + * - event - the DOM mouseout event object. + * - x and y - the mouse coordinates relative to the document being edited. + */ + MOUSE_OUT: "MouseOut", + + /** + * The BreakpointChange event is fired when a new breakpoint is added or when + * a breakpoint is removed - either through API use or through the editor UI. + * Event object properties: + * - added - array that holds the new breakpoints. + * - removed - array that holds the breakpoints that have been removed. + * Each object in the added/removed arrays holds two properties: line and + * condition. + */ + BREAKPOINT_CHANGE: "BreakpointChange", + + /** + * The DirtyChanged event is fired when the dirty state of the editor is + * changed. The dirty state of the editor tells if the are text changes that + * have not been saved yet. Event object properties: oldValue and newValue. + * Both are booleans telling the old dirty state and the new state, + * respectively. + */ + DIRTY_CHANGED: "DirtyChanged", +}; + +/** + * Allowed vertical alignment options for the line index + * when you call SourceEditor.setCaretPosition(). + */ +SourceEditor.VERTICAL_ALIGN = { + TOP: 0, + CENTER: 1, + BOTTOM: 2, +}; + +/** + * Extend a destination object with properties from a source object. + * + * @param object aDestination + * @param object aSource + */ +function extend(aDestination, aSource) +{ + for (let name in aSource) { + if (!aDestination.hasOwnProperty(name)) { + aDestination[name] = aSource[name]; + } + } +} + +/** + * Add methods common to all components. + */ +extend(SourceEditor.prototype, { + // Expose the static constants on the SourceEditor instances. + EVENTS: SourceEditor.EVENTS, + MODES: SourceEditor.MODES, + THEMES: SourceEditor.THEMES, + DEFAULTS: SourceEditor.DEFAULTS, + VERTICAL_ALIGN: SourceEditor.VERTICAL_ALIGN, + + _lastFind: null, + + /** + * Find a string in the editor. + * + * @param string aString + * The string you want to search for. If |aString| is not given the + * currently selected text is used. + * @param object [aOptions] + * Optional find options: + * - start: (integer) offset to start searching from. Default: 0 if + * backwards is false. If backwards is true then start = text.length. + * - ignoreCase: (boolean) tells if you want the search to be case + * insensitive or not. Default: false. + * - backwards: (boolean) tells if you want the search to go backwards + * from the given |start| offset. Default: false. + * @return integer + * The offset where the string was found. + */ + find: function SE_find(aString, aOptions) + { + if (typeof(aString) != "string") { + return -1; + } + + aOptions = aOptions || {}; + + let str = aOptions.ignoreCase ? aString.toLowerCase() : aString; + + let text = this.getText(); + if (aOptions.ignoreCase) { + text = text.toLowerCase(); + } + + let index = aOptions.backwards ? + text.lastIndexOf(str, aOptions.start) : + text.indexOf(str, aOptions.start); + + let lastFoundIndex = index; + if (index == -1 && this.lastFind && this.lastFind.index > -1 && + this.lastFind.str === aString && + this.lastFind.ignoreCase === !!aOptions.ignoreCase) { + lastFoundIndex = this.lastFind.index; + } + + this._lastFind = { + str: aString, + index: index, + lastFound: lastFoundIndex, + ignoreCase: !!aOptions.ignoreCase, + }; + + return index; + }, + + /** + * Find the next occurrence of the last search operation. + * + * @param boolean aWrap + * Tells if you want to restart the search from the beginning of the + * document if the string is not found. + * @return integer + * The offset where the string was found. + */ + findNext: function SE_findNext(aWrap) + { + if (!this.lastFind && this.lastFind.lastFound == -1) { + return -1; + } + + let options = { + start: this.lastFind.lastFound + this.lastFind.str.length, + ignoreCase: this.lastFind.ignoreCase, + }; + + let index = this.find(this.lastFind.str, options); + if (index == -1 && aWrap) { + options.start = 0; + index = this.find(this.lastFind.str, options); + } + + return index; + }, + + /** + * Find the previous occurrence of the last search operation. + * + * @param boolean aWrap + * Tells if you want to restart the search from the end of the + * document if the string is not found. + * @return integer + * The offset where the string was found. + */ + findPrevious: function SE_findPrevious(aWrap) + { + if (!this.lastFind && this.lastFind.lastFound == -1) { + return -1; + } + + let options = { + start: this.lastFind.lastFound - this.lastFind.str.length, + ignoreCase: this.lastFind.ignoreCase, + backwards: true, + }; + + let index; + if (options.start > 0) { + index = this.find(this.lastFind.str, options); + } else { + index = this._lastFind.index = -1; + } + + if (index == -1 && aWrap) { + options.start = this.getCharCount() - 1; + index = this.find(this.lastFind.str, options); + } + + return index; + }, +}); + +/** + * Retrieve the last find operation result. This object holds the following + * properties: + * - str: the last search string. + * - index: stores the result of the most recent find operation. This is the + * index in the text where |str| was found or -1 otherwise. + * - lastFound: tracks the index where |str| was last found, throughout + * multiple find operations. This can be -1 if |str| was never found in the + * document. + * - ignoreCase: tells if the search was case insensitive or not. + * @type object + */ +Object.defineProperty(SourceEditor.prototype, "lastFind", { + get: function() { return this._lastFind; }, + enumerable: true, + configurable: true, +}); + diff --git a/browser/devtools/sourceeditor/test/browser.ini b/browser/devtools/sourceeditor/test/browser.ini index 5c3a5fda80f2..71158a11f54e 100644 --- a/browser/devtools/sourceeditor/test/browser.ini +++ b/browser/devtools/sourceeditor/test/browser.ini @@ -9,9 +9,26 @@ support-files = codemirror.html head.js -[browser_editor_basic.js] -[browser_editor_cursor.js] -[browser_editor_history.js] -[browser_editor_markers.js] +[browser_bug650345_find.js] +[browser_bug684546_reset_undo.js] +[browser_bug684862_paste_html.js] +[browser_bug687160_line_api.js] +[browser_bug687568_pagescroll.js] +[browser_bug687573_vscroll.js] +[browser_bug687580_drag_and_drop.js] +[browser_bug695035_middle_click_paste.js] +[browser_bug700893_dirty_state.js] +[browser_bug703692_focus_blur.js] +[browser_bug707987_debugger_breakpoints.js] +[browser_bug712982_line_ruler_click.js] +[browser_bug725388_mouse_events.js] +[browser_bug725392_mouse_coords_char_offset.js] +[browser_bug725430_comment_uncomment.js] +[browser_bug725618_moveLines_shortcut.js] +[browser_bug729480_line_vertical_align.js] +[browser_bug729960_block_bracket_jump.js] +[browser_bug731721_debugger_stepping.js] +[browser_bug744021_next_prev_bracket_jump.js] [browser_codemirror.js] skip-if = os == "linux" +[browser_sourceeditor_initialization.js] diff --git a/browser/devtools/sourceeditor/test/browser_bug650345_find.js b/browser/devtools/sourceeditor/test/browser_bug650345_find.js new file mode 100644 index 000000000000..23a3d2eddf0d --- /dev/null +++ b/browser/devtools/sourceeditor/test/browser_bug650345_find.js @@ -0,0 +1,149 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let tempScope = {}; +Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope); +let SourceEditor = tempScope.SourceEditor; + +let testWin; +let editor; + +function test() +{ + waitForExplicitFinish(); + + const windowUrl = "data:text/xml," + + ""; + const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no"; + + testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null); + testWin.addEventListener("load", function onWindowLoad() { + testWin.removeEventListener("load", onWindowLoad, false); + waitForFocus(initEditor, testWin); + }, false); +} + +function initEditor() +{ + let hbox = testWin.document.querySelector("hbox"); + editor = new SourceEditor(); + editor.init(hbox, {}, editorLoaded); +} + +function editorLoaded() +{ + editor.focus(); + + let text = "foobar bug650345\nBug650345 bazbaz\nfoobar omg\ntest"; + editor.setText(text); + + let needle = "foobar"; + is(editor.find(), -1, "find() works"); + ok(!editor.lastFind, "no editor.lastFind yet"); + + is(editor.find(needle), 0, "find('" + needle + "') works"); + is(editor.lastFind.str, needle, "lastFind.str is correct"); + is(editor.lastFind.index, 0, "lastFind.index is correct"); + is(editor.lastFind.lastFound, 0, "lastFind.lastFound is correct"); + is(editor.lastFind.ignoreCase, false, "lastFind.ignoreCase is correct"); + + let newIndex = text.indexOf(needle, needle.length); + is(editor.findNext(), newIndex, "findNext() works"); + is(editor.lastFind.str, needle, "lastFind.str is correct"); + is(editor.lastFind.index, newIndex, "lastFind.index is correct"); + is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct"); + is(editor.lastFind.ignoreCase, false, "lastFind.ignoreCase is correct"); + + is(editor.findNext(), -1, "findNext() works again"); + is(editor.lastFind.index, -1, "lastFind.index is correct"); + is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct"); + + is(editor.findPrevious(), 0, "findPrevious() works"); + is(editor.lastFind.index, 0, "lastFind.index is correct"); + is(editor.lastFind.lastFound, 0, "lastFind.lastFound is correct"); + + is(editor.findPrevious(), -1, "findPrevious() works again"); + is(editor.lastFind.index, -1, "lastFind.index is correct"); + is(editor.lastFind.lastFound, 0, "lastFind.lastFound is correct"); + + is(editor.findNext(), newIndex, "findNext() works"); + is(editor.lastFind.index, newIndex, "lastFind.index is correct"); + is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct"); + + is(editor.findNext(true), 0, "findNext(true) works"); + is(editor.lastFind.index, 0, "lastFind.index is correct"); + is(editor.lastFind.lastFound, 0, "lastFind.lastFound is correct"); + + is(editor.findNext(true), newIndex, "findNext(true) works again"); + is(editor.lastFind.index, newIndex, "lastFind.index is correct"); + is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct"); + + is(editor.findPrevious(true), 0, "findPrevious(true) works"); + is(editor.lastFind.index, 0, "lastFind.index is correct"); + is(editor.lastFind.lastFound, 0, "lastFind.lastFound is correct"); + + is(editor.findPrevious(true), newIndex, "findPrevious(true) works again"); + is(editor.lastFind.index, newIndex, "lastFind.index is correct"); + is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct"); + + needle = "error"; + is(editor.find(needle), -1, "find('" + needle + "') works"); + is(editor.lastFind.str, needle, "lastFind.str is correct"); + is(editor.lastFind.index, -1, "lastFind.index is correct"); + is(editor.lastFind.lastFound, -1, "lastFind.lastFound is correct"); + is(editor.lastFind.ignoreCase, false, "lastFind.ignoreCase is correct"); + + is(editor.findNext(), -1, "findNext() works"); + is(editor.lastFind.str, needle, "lastFind.str is correct"); + is(editor.lastFind.index, -1, "lastFind.index is correct"); + is(editor.lastFind.lastFound, -1, "lastFind.lastFound is correct"); + is(editor.findNext(true), -1, "findNext(true) works"); + + is(editor.findPrevious(), -1, "findPrevious() works"); + is(editor.findPrevious(true), -1, "findPrevious(true) works"); + + needle = "bug650345"; + newIndex = text.indexOf(needle); + + is(editor.find(needle), newIndex, "find('" + needle + "') works"); + is(editor.findNext(), -1, "findNext() works"); + is(editor.findNext(true), newIndex, "findNext(true) works"); + is(editor.findPrevious(), -1, "findPrevious() works"); + is(editor.findPrevious(true), newIndex, "findPrevious(true) works"); + is(editor.lastFind.index, newIndex, "lastFind.index is correct"); + is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct"); + + is(editor.find(needle, {ignoreCase: 1}), newIndex, + "find('" + needle + "', {ignoreCase: 1}) works"); + is(editor.lastFind.ignoreCase, true, "lastFind.ignoreCase is correct"); + + let newIndex2 = text.toLowerCase().indexOf(needle, newIndex + needle.length); + is(editor.findNext(), newIndex2, "findNext() works"); + is(editor.findNext(), -1, "findNext() works"); + is(editor.lastFind.index, -1, "lastFind.index is correct"); + is(editor.lastFind.lastFound, newIndex2, "lastFind.lastFound is correct"); + + is(editor.findNext(true), newIndex, "findNext(true) works"); + + is(editor.findPrevious(), -1, "findPrevious() works"); + is(editor.findPrevious(true), newIndex2, "findPrevious(true) works"); + is(editor.findPrevious(), newIndex, "findPrevious() works again"); + + needle = "foobar"; + newIndex = text.indexOf(needle, 2); + is(editor.find(needle, {start: 2}), newIndex, + "find('" + needle + "', {start:2}) works"); + is(editor.findNext(), -1, "findNext() works"); + is(editor.findNext(true), 0, "findNext(true) works"); + + editor.destroy(); + + testWin.close(); + testWin = editor = null; + + waitForFocus(finish, window); +} diff --git a/browser/devtools/sourceeditor/test/browser_bug684546_reset_undo.js b/browser/devtools/sourceeditor/test/browser_bug684546_reset_undo.js new file mode 100644 index 000000000000..d54e54ab367d --- /dev/null +++ b/browser/devtools/sourceeditor/test/browser_bug684546_reset_undo.js @@ -0,0 +1,72 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let tempScope = {}; +Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope); +let SourceEditor = tempScope.SourceEditor; + +let testWin; +let editor; + +function test() +{ + waitForExplicitFinish(); + + const windowUrl = "data:application/vnd.mozilla.xul+xml," + + "" + + ""; + const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no"; + + testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null); + testWin.addEventListener("load", function onWindowLoad() { + testWin.removeEventListener("load", onWindowLoad, false); + waitForFocus(initEditor, testWin); + }, false); +} + +function initEditor() +{ + let box = testWin.document.querySelector("box"); + + editor = new SourceEditor(); + editor.init(box, {}, editorLoaded); +} + +function editorLoaded() +{ + editor.setText("First"); + editor.setText("Second", 5); + is(editor.getText(), "FirstSecond", "text set correctly."); + editor.undo(); + is(editor.getText(), "First", "undo works."); + editor.redo(); + is(editor.getText(), "FirstSecond", "redo works."); + editor.resetUndo(); + ok(!editor.canUndo(), "canUndo() is correct"); + ok(!editor.canRedo(), "canRedo() is correct"); + editor.undo(); + is(editor.getText(), "FirstSecond", "reset undo works correctly"); + editor.setText("Third", 11); + is(editor.getText(), "FirstSecondThird", "text set correctly"); + editor.undo(); + is(editor.getText(), "FirstSecond", "undo works after reset"); + editor.redo(); + is(editor.getText(), "FirstSecondThird", "redo works after reset"); + editor.resetUndo(); + ok(!editor.canUndo(), "canUndo() is correct (again)"); + ok(!editor.canRedo(), "canRedo() is correct (again)"); + editor.undo(); + is(editor.getText(), "FirstSecondThird", "reset undo still works correctly"); + + finish(); +} + +registerCleanupFunction(function() { + editor.destroy(); + testWin.close(); + testWin = editor = null; +}); diff --git a/browser/devtools/sourceeditor/test/browser_bug684862_paste_html.js b/browser/devtools/sourceeditor/test/browser_bug684862_paste_html.js new file mode 100644 index 000000000000..3ac37764e323 --- /dev/null +++ b/browser/devtools/sourceeditor/test/browser_bug684862_paste_html.js @@ -0,0 +1,119 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let tempScope = {}; +Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope); +let SourceEditor = tempScope.SourceEditor; + +let testWin; +let editor; + +function test() +{ + waitForExplicitFinish(); + + const pageUrl = "data:text/html,
  • test
  • foobarBug684862"; + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function onLoad() { + gBrowser.selectedBrowser.removeEventListener("load", onLoad, true); + waitForFocus(pageLoaded, content); + }, true); + + content.location = pageUrl; +} + +function pageLoaded() +{ + const windowUrl = "data:application/vnd.mozilla.xul+xml," + + "" + + "