/******************************************************************************* * @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}; });