/*******************************************************************************
* @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}
*
* 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.
*
TextModel
.
*
* See:
* {@link orion.textview.Annotation}
* {@link orion.textview.TextModel}
*
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}
*
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 giventype
. 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* 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:* 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}
*
* See:
* {@link orion.textview.AnnotationRuler}
*
See:
* {@link orion.textview.Ruler}
* {@link orion.textview.Annotation}
*
* 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}
*
See:
* {@link orion.textview.AnnotationRuler}
* {@link orion.textview.Ruler}
*
* See:
* {@link orion.textview.TextView}
*
* 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}
*
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.
*
-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.
*
-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.
*
-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* 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.TextView}
* {@link orion.textview.TextView#setOptions}
* {@link orion.textview.TextView#getOptions}
*
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: *
* 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.
*
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.
*
* 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.
*
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.
*
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}
*
* See:
* {@link orion.textview.TextView}
* {@link orion.textview.TextView#event:onDestroy}
*
* See:
* {@link orion.textview.TextView}
* {@link orion.textview.TextView#event:onLineStyle}
*
* See:
* {@link orion.textview.TextView}
* {@link orion.textview.TextView#event:onLineStyle}
*
* See:
* {@link orion.textview.TextView}
* {@link orion.textview.TextView#event:onLineStyle}
*
* See:
* {@link orion.textview.TextView}
* {@link orion.textview.TextView#event:onLoad}
*
* See:
* {@link orion.textview.TextView}
* {@link orion.textview.TextView#event:onModelChanged}
* {@link orion.textview.TextModel#onChanged}
*
* See:
* {@link orion.textview.TextView}
* {@link orion.textview.TextView#event:onModelChanging}
* {@link orion.textview.TextModel#onChanging}
*
* See:
* {@link orion.textview.TextView}
* {@link orion.textview.TextView#event:onModify}
*
* 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}
*
* See:
* {@link orion.textview.TextView}
* {@link orion.textview.TextView#event:onScroll}
*
* See:
* {@link orion.textview.TextView}
* {@link orion.textview.TextView#event:onVerify}
*
* 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}
*
* See:
* {@link orion.textview.TextView}
* {@link orion.textview.TextView#event:onFocus}
*
* See:
* {@link orion.textview.TextView}
* {@link orion.textview.TextView#event:onBlur}
*
* 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; itrue
, 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] iftrue
, 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.
*
* 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] iftrue
, 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.
*
* 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.MozOutline = "none"; 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.MozOutline = "none"; 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; iregex
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+)/regex
* and its value is the corresponding capturing group number of [0].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.
* *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.match
patterns are supported.begin .. end
patterns are supported.(?x)
and (?x:...)
are supported, but only when they
* apply to the entire regex pattern.RegExp
s. As a result, many features of the Oniguruma regex
* engine used by TextMate are not supported.
* Unsupported features include:
* (?i:a)b
)captures, beginCaptures, endCaptures
are supported.name
and contentName
are supported.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.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", "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