PageRenderTime 128ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 2ms

/browser/devtools/sourceeditor/orion/orion.js

https://bitbucket.org/soko/mozilla-central
JavaScript | 12303 lines | 9235 code | 215 blank | 2853 comment | 2250 complexity | e11ef20919979de346cbfa2ab72118a6 MD5 | raw file
Possible License(s): GPL-2.0, JSON, 0BSD, LGPL-3.0, AGPL-1.0, MIT, MPL-2.0-no-copyleft-exception, BSD-3-Clause, LGPL-2.1, Apache-2.0
  1. /*******************************************************************************
  2. * @license
  3. * Copyright (c) 2010, 2011 IBM Corporation and others.
  4. * All rights reserved. This program and the accompanying materials are made
  5. * available under the terms of the Eclipse Public License v1.0
  6. * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
  7. * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
  8. *
  9. * Contributors:
  10. * Felipe Heidrich (IBM Corporation) - initial API and implementation
  11. * Silenio Quarti (IBM Corporation) - initial API and implementation
  12. * Mihai Sucan (Mozilla Foundation) - fix for Bug#364214
  13. */
  14. /*global window */
  15. /**
  16. * Evaluates the definition function and mixes in the returned module with
  17. * the module specified by <code>moduleName</code>.
  18. * <p>
  19. * This function is intented to by used when RequireJS is not available.
  20. * </p>
  21. *
  22. * @param {String} name The mixin module name.
  23. * @param {String[]} deps The array of dependency names.
  24. * @param {Function} callback The definition function.
  25. */
  26. if (!window.define) {
  27. window.define = function(name, deps, callback) {
  28. var module = this;
  29. var split = (name || "").split("/"), i, j;
  30. for (i = 0; i < split.length - 1; i++) {
  31. module = module[split[i]] = (module[split[i]] || {});
  32. }
  33. var depModules = [], depModule;
  34. for (j = 0; j < deps.length; j++) {
  35. depModule = this;
  36. split = deps[j].split("/");
  37. for (i = 0; i < split.length - 1; i++) {
  38. depModule = depModule[split[i]] = (depModule[split[i]] || {});
  39. }
  40. depModules.push(depModule);
  41. }
  42. var newModule = callback.apply(this, depModules);
  43. for (var p in newModule) {
  44. if (newModule.hasOwnProperty(p)) {
  45. module[p] = newModule[p];
  46. }
  47. }
  48. };
  49. }
  50. /**
  51. * Require/get the defined modules.
  52. * <p>
  53. * This function is intented to by used when RequireJS is not available.
  54. * </p>
  55. *
  56. * @param {String[]|String} deps The array of dependency names. This can also be
  57. * a string, a single dependency name.
  58. * @param {Function} [callback] Optional, the callback function to execute when
  59. * multiple dependencies are required. The callback arguments will have
  60. * references to each module in the same order as the deps array.
  61. * @returns {Object|undefined} If the deps parameter is a string, then this
  62. * function returns the required module definition, otherwise undefined is
  63. * returned.
  64. */
  65. if (!window.require) {
  66. window.require = function(deps, callback) {
  67. var depsArr = typeof deps === "string" ? [deps] : deps;
  68. var depModules = [], depModule, split, i, j;
  69. for (j = 0; j < depsArr.length; j++) {
  70. depModule = this;
  71. split = depsArr[j].split("/");
  72. for (i = 0; i < split.length - 1; i++) {
  73. depModule = depModule[split[i]] = (depModule[split[i]] || {});
  74. }
  75. depModules.push(depModule);
  76. }
  77. if (callback) {
  78. callback.apply(this, depModules);
  79. }
  80. return typeof deps === "string" ? depModules[0] : undefined;
  81. };
  82. }/*******************************************************************************
  83. * Copyright (c) 2010, 2011 IBM Corporation and others.
  84. * All rights reserved. This program and the accompanying materials are made
  85. * available under the terms of the Eclipse Public License v1.0
  86. * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
  87. * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
  88. *
  89. * Contributors:
  90. * Felipe Heidrich (IBM Corporation) - initial API and implementation
  91. * Silenio Quarti (IBM Corporation) - initial API and implementation
  92. ******************************************************************************/
  93. /*global define */
  94. define("orion/textview/eventTarget", [], function() {
  95. /**
  96. * Constructs a new EventTarget object.
  97. *
  98. * @class
  99. * @name orion.textview.EventTarget
  100. */
  101. function EventTarget() {
  102. }
  103. /**
  104. * Adds in the event target interface into the specified object.
  105. *
  106. * @param {Object} object The object to add in the event target interface.
  107. */
  108. EventTarget.addMixin = function(object) {
  109. var proto = EventTarget.prototype;
  110. for (var p in proto) {
  111. if (proto.hasOwnProperty(p)) {
  112. object[p] = proto[p];
  113. }
  114. }
  115. };
  116. EventTarget.prototype = /** @lends orion.textview.EventTarget.prototype */ {
  117. /**
  118. * Adds an event listener to this event target.
  119. *
  120. * @param {String} type The event type.
  121. * @param {Function|EventListener} listener The function or the EventListener that will be executed when the event happens.
  122. * @param {Boolean} [useCapture=false] <code>true</code> if the listener should be trigged in the capture phase.
  123. *
  124. * @see #removeEventListener
  125. */
  126. addEventListener: function(type, listener, useCapture) {
  127. if (!this._eventTypes) { this._eventTypes = {}; }
  128. var state = this._eventTypes[type];
  129. if (!state) {
  130. state = this._eventTypes[type] = {level: 0, listeners: []};
  131. }
  132. var listeners = state.listeners;
  133. listeners.push({listener: listener, useCapture: useCapture});
  134. },
  135. /**
  136. * Dispatches the given event to the listeners added to this event target.
  137. * @param {Event} evt The event to dispatch.
  138. */
  139. dispatchEvent: function(evt) {
  140. if (!this._eventTypes) { return; }
  141. var type = evt.type;
  142. var state = this._eventTypes[type];
  143. if (state) {
  144. var listeners = state.listeners;
  145. try {
  146. state.level++;
  147. if (listeners) {
  148. for (var i=0, len=listeners.length; i < len; i++) {
  149. if (listeners[i]) {
  150. var l = listeners[i].listener;
  151. if (typeof l === "function") {
  152. l.call(this, evt);
  153. } else if (l.handleEvent && typeof l.handleEvent === "function") {
  154. l.handleEvent(evt);
  155. }
  156. }
  157. }
  158. }
  159. } finally {
  160. state.level--;
  161. if (state.compact && state.level === 0) {
  162. for (var j=listeners.length - 1; j >= 0; j--) {
  163. if (!listeners[j]) {
  164. listeners.splice(j, 1);
  165. }
  166. }
  167. if (listeners.length === 0) {
  168. delete this._eventTypes[type];
  169. }
  170. state.compact = false;
  171. }
  172. }
  173. }
  174. },
  175. /**
  176. * Returns whether there is a listener for the specified event type.
  177. *
  178. * @param {String} type The event type
  179. *
  180. * @see #addEventListener
  181. * @see #removeEventListener
  182. */
  183. isListening: function(type) {
  184. if (!this._eventTypes) { return false; }
  185. return this._eventTypes[type] !== undefined;
  186. },
  187. /**
  188. * Removes an event listener from the event target.
  189. * <p>
  190. * All the parameters must be the same ones used to add the listener.
  191. * </p>
  192. *
  193. * @param {String} type The event type
  194. * @param {Function|EventListener} listener The function or the EventListener that will be executed when the event happens.
  195. * @param {Boolean} [useCapture=false] <code>true</code> if the listener should be trigged in the capture phase.
  196. *
  197. * @see #addEventListener
  198. */
  199. removeEventListener: function(type, listener, useCapture){
  200. if (!this._eventTypes) { return; }
  201. var state = this._eventTypes[type];
  202. if (state) {
  203. var listeners = state.listeners;
  204. for (var i=0, len=listeners.length; i < len; i++) {
  205. var l = listeners[i];
  206. if (l && l.listener === listener && l.useCapture === useCapture) {
  207. if (state.level !== 0) {
  208. listeners[i] = null;
  209. state.compact = true;
  210. } else {
  211. listeners.splice(i, 1);
  212. }
  213. break;
  214. }
  215. }
  216. if (listeners.length === 0) {
  217. delete this._eventTypes[type];
  218. }
  219. }
  220. }
  221. };
  222. return {EventTarget: EventTarget};
  223. });
  224. /*******************************************************************************
  225. * @license
  226. * Copyright (c) 2011 IBM Corporation and others.
  227. * All rights reserved. This program and the accompanying materials are made
  228. * available under the terms of the Eclipse Public License v1.0
  229. * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
  230. * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
  231. *
  232. * Contributors:
  233. * IBM Corporation - initial API and implementation
  234. *******************************************************************************/
  235. /*global define */
  236. /*jslint browser:true regexp:false*/
  237. /**
  238. * @name orion.editor.regex
  239. * @class Utilities for dealing with regular expressions.
  240. * @description Utilities for dealing with regular expressions.
  241. */
  242. define("orion/editor/regex", [], function() {
  243. /**
  244. * @methodOf orion.editor.regex
  245. * @static
  246. * @description Escapes regex special characters in the input string.
  247. * @param {String} str The string to escape.
  248. * @returns {String} A copy of <code>str</code> with regex special characters escaped.
  249. */
  250. function escape(str) {
  251. return str.replace(/([\\$\^*\/+?\.\(\)|{}\[\]])/g, "\\$&");
  252. }
  253. /**
  254. * @methodOf orion.editor.regex
  255. * @static
  256. * @description Parses a pattern and flags out of a regex literal string.
  257. * @param {String} str The string to parse. Should look something like <code>"/ab+c/"</code> or <code>"/ab+c/i"</code>.
  258. * @returns {Object} If <code>str</code> looks like a regex literal, returns an object with properties
  259. * <code><dl>
  260. * <dt>pattern</dt><dd>{String}</dd>
  261. * <dt>flags</dt><dd>{String}</dd>
  262. * </dl></code> otherwise returns <code>null</code>.
  263. */
  264. function parse(str) {
  265. var regexp = /^\s*\/(.+)\/([gim]{0,3})\s*$/.exec(str);
  266. if (regexp) {
  267. return {
  268. pattern : regexp[1],
  269. flags : regexp[2]
  270. };
  271. }
  272. return null;
  273. }
  274. return {
  275. escape: escape,
  276. parse: parse
  277. };
  278. });
  279. /*******************************************************************************
  280. * @license
  281. * Copyright (c) 2010, 2011 IBM Corporation and others.
  282. * All rights reserved. This program and the accompanying materials are made
  283. * available under the terms of the Eclipse Public License v1.0
  284. * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
  285. * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
  286. *
  287. * Contributors:
  288. * Felipe Heidrich (IBM Corporation) - initial API and implementation
  289. * Silenio Quarti (IBM Corporation) - initial API and implementation
  290. ******************************************************************************/
  291. /*global window define */
  292. define("orion/textview/keyBinding", [], function() {
  293. var isMac = window.navigator.platform.indexOf("Mac") !== -1;
  294. /**
  295. * Constructs a new key binding with the given key code and modifiers.
  296. *
  297. * @param {String|Number} keyCode the key code.
  298. * @param {Boolean} mod1 the primary modifier (usually Command on Mac and Control on other platforms).
  299. * @param {Boolean} mod2 the secondary modifier (usually Shift).
  300. * @param {Boolean} mod3 the third modifier (usually Alt).
  301. * @param {Boolean} mod4 the fourth modifier (usually Control on the Mac).
  302. *
  303. * @class A KeyBinding represents of a key code and a modifier state that can be triggered by the user using the keyboard.
  304. * @name orion.textview.KeyBinding
  305. *
  306. * @property {String|Number} keyCode The key code.
  307. * @property {Boolean} mod1 The primary modifier (usually Command on Mac and Control on other platforms).
  308. * @property {Boolean} mod2 The secondary modifier (usually Shift).
  309. * @property {Boolean} mod3 The third modifier (usually Alt).
  310. * @property {Boolean} mod4 The fourth modifier (usually Control on the Mac).
  311. *
  312. * @see orion.textview.TextView#setKeyBinding
  313. */
  314. function KeyBinding (keyCode, mod1, mod2, mod3, mod4) {
  315. if (typeof(keyCode) === "string") {
  316. this.keyCode = keyCode.toUpperCase().charCodeAt(0);
  317. } else {
  318. this.keyCode = keyCode;
  319. }
  320. this.mod1 = mod1 !== undefined && mod1 !== null ? mod1 : false;
  321. this.mod2 = mod2 !== undefined && mod2 !== null ? mod2 : false;
  322. this.mod3 = mod3 !== undefined && mod3 !== null ? mod3 : false;
  323. this.mod4 = mod4 !== undefined && mod4 !== null ? mod4 : false;
  324. }
  325. KeyBinding.prototype = /** @lends orion.textview.KeyBinding.prototype */ {
  326. /**
  327. * Returns whether this key binding matches the given key event.
  328. *
  329. * @param e the key event.
  330. * @returns {Boolean} <code>true</code> whether the key binding matches the key event.
  331. */
  332. match: function (e) {
  333. if (this.keyCode === e.keyCode) {
  334. var mod1 = isMac ? e.metaKey : e.ctrlKey;
  335. if (this.mod1 !== mod1) { return false; }
  336. if (this.mod2 !== e.shiftKey) { return false; }
  337. if (this.mod3 !== e.altKey) { return false; }
  338. if (isMac && this.mod4 !== e.ctrlKey) { return false; }
  339. return true;
  340. }
  341. return false;
  342. },
  343. /**
  344. * Returns whether this key binding is the same as the given parameter.
  345. *
  346. * @param {orion.textview.KeyBinding} kb the key binding to compare with.
  347. * @returns {Boolean} whether or not the parameter and the receiver describe the same key binding.
  348. */
  349. equals: function(kb) {
  350. if (!kb) { return false; }
  351. if (this.keyCode !== kb.keyCode) { return false; }
  352. if (this.mod1 !== kb.mod1) { return false; }
  353. if (this.mod2 !== kb.mod2) { return false; }
  354. if (this.mod3 !== kb.mod3) { return false; }
  355. if (this.mod4 !== kb.mod4) { return false; }
  356. return true;
  357. }
  358. };
  359. return {KeyBinding: KeyBinding};
  360. });
  361. /*******************************************************************************
  362. * @license
  363. * Copyright (c) 2010, 2011 IBM Corporation and others.
  364. * All rights reserved. This program and the accompanying materials are made
  365. * available under the terms of the Eclipse Public License v1.0
  366. * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
  367. * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
  368. *
  369. * Contributors:
  370. * Felipe Heidrich (IBM Corporation) - initial API and implementation
  371. * Silenio Quarti (IBM Corporation) - initial API and implementation
  372. ******************************************************************************/
  373. /*global define */
  374. define("orion/textview/annotations", ['orion/textview/eventTarget'], function(mEventTarget) {
  375. /**
  376. * @class This object represents a decoration attached to a range of text. Annotations are added to a
  377. * <code>AnnotationModel</code> which is attached to a <code>TextModel</code>.
  378. * <p>
  379. * <b>See:</b><br/>
  380. * {@link orion.textview.AnnotationModel}<br/>
  381. * {@link orion.textview.Ruler}<br/>
  382. * </p>
  383. * @name orion.textview.Annotation
  384. *
  385. * @property {String} type The annotation type (for example, orion.annotation.error).
  386. * @property {Number} start The start offset of the annotation in the text model.
  387. * @property {Number} end The end offset of the annotation in the text model.
  388. * @property {String} html The HTML displayed for the annotation.
  389. * @property {String} title The text description for the annotation.
  390. * @property {orion.textview.Style} style The style information for the annotation used in the annotations ruler and tooltips.
  391. * @property {orion.textview.Style} overviewStyle The style information for the annotation used in the overview ruler.
  392. * @property {orion.textview.Style} rangeStyle The style information for the annotation used in the text view to decorate a range of text.
  393. * @property {orion.textview.Style} lineStyle The style information for the annotation used in the text view to decorate a line of text.
  394. */
  395. /**
  396. * Constructs a new folding annotation.
  397. *
  398. * @param {orion.textview.ProjectionTextModel} projectionModel The projection text model.
  399. * @param {String} type The annotation type.
  400. * @param {Number} start The start offset of the annotation in the text model.
  401. * @param {Number} end The end offset of the annotation in the text model.
  402. * @param {String} expandedHTML The HTML displayed for this annotation when it is expanded.
  403. * @param {orion.textview.Style} expandedStyle The style information for the annotation when it is expanded.
  404. * @param {String} collapsedHTML The HTML displayed for this annotation when it is collapsed.
  405. * @param {orion.textview.Style} collapsedStyle The style information for the annotation when it is collapsed.
  406. *
  407. * @class This object represents a folding annotation.
  408. * @name orion.textview.FoldingAnnotation
  409. */
  410. function FoldingAnnotation (projectionModel, type, start, end, expandedHTML, expandedStyle, collapsedHTML, collapsedStyle) {
  411. this.type = type;
  412. this.start = start;
  413. this.end = end;
  414. this._projectionModel = projectionModel;
  415. this._expandedHTML = this.html = expandedHTML;
  416. this._expandedStyle = this.style = expandedStyle;
  417. this._collapsedHTML = collapsedHTML;
  418. this._collapsedStyle = collapsedStyle;
  419. this.expanded = true;
  420. }
  421. FoldingAnnotation.prototype = /** @lends orion.textview.FoldingAnnotation.prototype */ {
  422. /**
  423. * Collapses the annotation.
  424. */
  425. collapse: function () {
  426. if (!this.expanded) { return; }
  427. this.expanded = false;
  428. this.html = this._collapsedHTML;
  429. this.style = this._collapsedStyle;
  430. var projectionModel = this._projectionModel;
  431. var baseModel = projectionModel.getBaseModel();
  432. this._projection = {
  433. start: baseModel.getLineStart(baseModel.getLineAtOffset(this.start) + 1),
  434. end: baseModel.getLineEnd(baseModel.getLineAtOffset(this.end), true)
  435. };
  436. projectionModel.addProjection(this._projection);
  437. },
  438. /**
  439. * Expands the annotation.
  440. */
  441. expand: function () {
  442. if (this.expanded) { return; }
  443. this.expanded = true;
  444. this.html = this._expandedHTML;
  445. this.style = this._expandedStyle;
  446. this._projectionModel.removeProjection(this._projection);
  447. }
  448. };
  449. /**
  450. * Constructs a new AnnotationTypeList object.
  451. *
  452. * @class
  453. * @name orion.textview.AnnotationTypeList
  454. */
  455. function AnnotationTypeList () {
  456. }
  457. /**
  458. * Adds in the annotation type interface into the specified object.
  459. *
  460. * @param {Object} object The object to add in the annotation type interface.
  461. */
  462. AnnotationTypeList.addMixin = function(object) {
  463. var proto = AnnotationTypeList.prototype;
  464. for (var p in proto) {
  465. if (proto.hasOwnProperty(p)) {
  466. object[p] = proto[p];
  467. }
  468. }
  469. };
  470. AnnotationTypeList.prototype = /** @lends orion.textview.AnnotationTypeList.prototype */ {
  471. /**
  472. * Adds an annotation type to the receiver.
  473. * <p>
  474. * Only annotations of the specified types will be shown by
  475. * the receiver.
  476. * </p>
  477. *
  478. * @param {Object} type the annotation type to be shown
  479. *
  480. * @see #removeAnnotationType
  481. * @see #isAnnotationTypeVisible
  482. */
  483. addAnnotationType: function(type) {
  484. if (!this._annotationTypes) { this._annotationTypes = []; }
  485. this._annotationTypes.push(type);
  486. },
  487. /**
  488. * Gets the annotation type priority. The priority is determined by the
  489. * order the annotation type is added to the receiver. Annotation types
  490. * added first have higher priority.
  491. * <p>
  492. * Returns <code>0</code> if the annotation type is not added.
  493. * </p>
  494. *
  495. * @param {Object} type the annotation type
  496. *
  497. * @see #addAnnotationType
  498. * @see #removeAnnotationType
  499. * @see #isAnnotationTypeVisible
  500. */
  501. getAnnotationTypePriority: function(type) {
  502. if (this._annotationTypes) {
  503. for (var i = 0; i < this._annotationTypes.length; i++) {
  504. if (this._annotationTypes[i] === type) {
  505. return i + 1;
  506. }
  507. }
  508. }
  509. return 0;
  510. },
  511. /**
  512. * Returns an array of annotations in the specified annotation model for the given range of text sorted by type.
  513. *
  514. * @param {orion.textview.AnnotationModel} annotationModel the annotation model.
  515. * @param {Number} start the start offset of the range.
  516. * @param {Number} end the end offset of the range.
  517. * @return {orion.textview.Annotation[]} an annotation array.
  518. */
  519. getAnnotationsByType: function(annotationModel, start, end) {
  520. var iter = annotationModel.getAnnotations(start, end);
  521. var annotation, annotations = [];
  522. while (iter.hasNext()) {
  523. annotation = iter.next();
  524. var priority = this.getAnnotationTypePriority(annotation.type);
  525. if (priority === 0) { continue; }
  526. annotations.push(annotation);
  527. }
  528. var self = this;
  529. annotations.sort(function(a, b) {
  530. return self.getAnnotationTypePriority(a.type) - self.getAnnotationTypePriority(b.type);
  531. });
  532. return annotations;
  533. },
  534. /**
  535. * Returns whether the receiver shows annotations of the specified type.
  536. *
  537. * @param {Object} type the annotation type
  538. * @returns {Boolean} whether the specified annotation type is shown
  539. *
  540. * @see #addAnnotationType
  541. * @see #removeAnnotationType
  542. */
  543. isAnnotationTypeVisible: function(type) {
  544. return this.getAnnotationTypePriority(type) !== 0;
  545. },
  546. /**
  547. * Removes an annotation type from the receiver.
  548. *
  549. * @param {Object} type the annotation type to be removed
  550. *
  551. * @see #addAnnotationType
  552. * @see #isAnnotationTypeVisible
  553. */
  554. removeAnnotationType: function(type) {
  555. if (!this._annotationTypes) { return; }
  556. for (var i = 0; i < this._annotationTypes.length; i++) {
  557. if (this._annotationTypes[i] === type) {
  558. this._annotationTypes.splice(i, 1);
  559. break;
  560. }
  561. }
  562. }
  563. };
  564. /**
  565. * Constructs an annotation model.
  566. *
  567. * @param {textModel} textModel The text model.
  568. *
  569. * @class This object manages annotations for a <code>TextModel</code>.
  570. * <p>
  571. * <b>See:</b><br/>
  572. * {@link orion.textview.Annotation}<br/>
  573. * {@link orion.textview.TextModel}<br/>
  574. * </p>
  575. * @name orion.textview.AnnotationModel
  576. * @borrows orion.textview.EventTarget#addEventListener as #addEventListener
  577. * @borrows orion.textview.EventTarget#removeEventListener as #removeEventListener
  578. * @borrows orion.textview.EventTarget#dispatchEvent as #dispatchEvent
  579. */
  580. function AnnotationModel(textModel) {
  581. this._annotations = [];
  582. var self = this;
  583. this._listener = {
  584. onChanged: function(modelChangedEvent) {
  585. self._onChanged(modelChangedEvent);
  586. }
  587. };
  588. this.setTextModel(textModel);
  589. }
  590. AnnotationModel.prototype = /** @lends orion.textview.AnnotationModel.prototype */ {
  591. /**
  592. * Adds an annotation to the annotation model.
  593. * <p>The annotation model listeners are notified of this change.</p>
  594. *
  595. * @param {orion.textview.Annotation} annotation the annotation to be added.
  596. *
  597. * @see #removeAnnotation
  598. */
  599. addAnnotation: function(annotation) {
  600. if (!annotation) { return; }
  601. var annotations = this._annotations;
  602. var index = this._binarySearch(annotations, annotation.start);
  603. annotations.splice(index, 0, annotation);
  604. var e = {
  605. type: "Changed",
  606. added: [annotation],
  607. removed: [],
  608. changed: []
  609. };
  610. this.onChanged(e);
  611. },
  612. /**
  613. * Returns the text model.
  614. *
  615. * @return {orion.textview.TextModel} The text model.
  616. *
  617. * @see #setTextModel
  618. */
  619. getTextModel: function() {
  620. return this._model;
  621. },
  622. /**
  623. * @class This object represents an annotation iterator.
  624. * <p>
  625. * <b>See:</b><br/>
  626. * {@link orion.textview.AnnotationModel#getAnnotations}<br/>
  627. * </p>
  628. * @name orion.textview.AnnotationIterator
  629. *
  630. * @property {Function} hasNext Determines whether there are more annotations in the iterator.
  631. * @property {Function} next Returns the next annotation in the iterator.
  632. */
  633. /**
  634. * Returns an iterator of annotations for the given range of text.
  635. *
  636. * @param {Number} start the start offset of the range.
  637. * @param {Number} end the end offset of the range.
  638. * @return {orion.textview.AnnotationIterator} an annotation iterartor.
  639. */
  640. getAnnotations: function(start, end) {
  641. var annotations = this._annotations, current;
  642. //TODO binary search does not work for range intersection when there are overlaping ranges, need interval search tree for this
  643. var i = 0;
  644. var skip = function() {
  645. while (i < annotations.length) {
  646. var a = annotations[i++];
  647. if ((start === a.start) || (start > a.start ? start < a.end : a.start < end)) {
  648. return a;
  649. }
  650. if (a.start >= end) {
  651. break;
  652. }
  653. }
  654. return null;
  655. };
  656. current = skip();
  657. return {
  658. next: function() {
  659. var result = current;
  660. if (result) { current = skip(); }
  661. return result;
  662. },
  663. hasNext: function() {
  664. return current !== null;
  665. }
  666. };
  667. },
  668. /**
  669. * Notifies the annotation model that the given annotation has been modified.
  670. * <p>The annotation model listeners are notified of this change.</p>
  671. *
  672. * @param {orion.textview.Annotation} annotation the modified annotation.
  673. *
  674. * @see #addAnnotation
  675. */
  676. modifyAnnotation: function(annotation) {
  677. if (!annotation) { return; }
  678. var index = this._getAnnotationIndex(annotation);
  679. if (index < 0) { return; }
  680. var e = {
  681. type: "Changed",
  682. added: [],
  683. removed: [],
  684. changed: [annotation]
  685. };
  686. this.onChanged(e);
  687. },
  688. /**
  689. * Notifies all listeners that the annotation model has changed.
  690. *
  691. * @param {orion.textview.Annotation[]} added The list of annotation being added to the model.
  692. * @param {orion.textview.Annotation[]} changed The list of annotation modified in the model.
  693. * @param {orion.textview.Annotation[]} removed The list of annotation being removed from the model.
  694. * @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}).
  695. */
  696. onChanged: function(e) {
  697. return this.dispatchEvent(e);
  698. },
  699. /**
  700. * Removes all annotations of the given <code>type</code>. All annotations
  701. * are removed if the type is not specified.
  702. * <p>The annotation model listeners are notified of this change. Only one changed event is generated.</p>
  703. *
  704. * @param {Object} type the type of annotations to be removed.
  705. *
  706. * @see #removeAnnotation
  707. */
  708. removeAnnotations: function(type) {
  709. var annotations = this._annotations;
  710. var removed, i;
  711. if (type) {
  712. removed = [];
  713. for (i = annotations.length - 1; i >= 0; i--) {
  714. var annotation = annotations[i];
  715. if (annotation.type === type) {
  716. annotations.splice(i, 1);
  717. }
  718. removed.splice(0, 0, annotation);
  719. }
  720. } else {
  721. removed = annotations;
  722. annotations = [];
  723. }
  724. var e = {
  725. type: "Changed",
  726. removed: removed,
  727. added: [],
  728. changed: []
  729. };
  730. this.onChanged(e);
  731. },
  732. /**
  733. * Removes an annotation from the annotation model.
  734. * <p>The annotation model listeners are notified of this change.</p>
  735. *
  736. * @param {orion.textview.Annotation} annotation the annotation to be removed.
  737. *
  738. * @see #addAnnotation
  739. */
  740. removeAnnotation: function(annotation) {
  741. if (!annotation) { return; }
  742. var index = this._getAnnotationIndex(annotation);
  743. if (index < 0) { return; }
  744. var e = {
  745. type: "Changed",
  746. removed: this._annotations.splice(index, 1),
  747. added: [],
  748. changed: []
  749. };
  750. this.onChanged(e);
  751. },
  752. /**
  753. * Removes and adds the specifed annotations to the annotation model.
  754. * <p>The annotation model listeners are notified of this change. Only one changed event is generated.</p>
  755. *
  756. * @param {orion.textview.Annotation} remove the annotations to be removed.
  757. * @param {orion.textview.Annotation} add the annotations to be added.
  758. *
  759. * @see #addAnnotation
  760. * @see #removeAnnotation
  761. */
  762. replaceAnnotations: function(remove, add) {
  763. var annotations = this._annotations, i, index, annotation, removed = [];
  764. if (remove) {
  765. for (i = remove.length - 1; i >= 0; i--) {
  766. annotation = remove[i];
  767. index = this._getAnnotationIndex(annotation);
  768. if (index < 0) { continue; }
  769. annotations.splice(index, 1);
  770. removed.splice(0, 0, annotation);
  771. }
  772. }
  773. if (!add) { add = []; }
  774. for (i = 0; i < add.length; i++) {
  775. annotation = add[i];
  776. index = this._binarySearch(annotations, annotation.start);
  777. annotations.splice(index, 0, annotation);
  778. }
  779. var e = {
  780. type: "Changed",
  781. removed: removed,
  782. added: add,
  783. changed: []
  784. };
  785. this.onChanged(e);
  786. },
  787. /**
  788. * Sets the text model of the annotation model. The annotation
  789. * model listens for changes in the text model to update and remove
  790. * annotations that are affected by the change.
  791. *
  792. * @param {orion.textview.TextModel} textModel the text model.
  793. *
  794. * @see #getTextModel
  795. */
  796. setTextModel: function(textModel) {
  797. if (this._model) {
  798. this._model.removeEventListener("Changed", this._listener.onChanged);
  799. }
  800. this._model = textModel;
  801. if (this._model) {
  802. this._model.addEventListener("Changed", this._listener.onChanged);
  803. }
  804. },
  805. /** @ignore */
  806. _binarySearch: function (array, offset) {
  807. var high = array.length, low = -1, index;
  808. while (high - low > 1) {
  809. index = Math.floor((high + low) / 2);
  810. if (offset <= array[index].start) {
  811. high = index;
  812. } else {
  813. low = index;
  814. }
  815. }
  816. return high;
  817. },
  818. /** @ignore */
  819. _getAnnotationIndex: function(annotation) {
  820. var annotations = this._annotations;
  821. var index = this._binarySearch(annotations, annotation.start);
  822. while (index < annotations.length && annotations[index].start === annotation.start) {
  823. if (annotations[index] === annotation) {
  824. return index;
  825. }
  826. index++;
  827. }
  828. return -1;
  829. },
  830. /** @ignore */
  831. _onChanged: function(modelChangedEvent) {
  832. var start = modelChangedEvent.start;
  833. var addedCharCount = modelChangedEvent.addedCharCount;
  834. var removedCharCount = modelChangedEvent.removedCharCount;
  835. var annotations = this._annotations, end = start + removedCharCount;
  836. //TODO binary search does not work for range intersection when there are overlaping ranges, need interval search tree for this
  837. var startIndex = 0;
  838. if (!(0 <= startIndex && startIndex < annotations.length)) { return; }
  839. var e = {
  840. type: "Changed",
  841. added: [],
  842. removed: [],
  843. changed: [],
  844. textModelChangedEvent: modelChangedEvent
  845. };
  846. var changeCount = addedCharCount - removedCharCount, i;
  847. for (i = startIndex; i < annotations.length; i++) {
  848. var annotation = annotations[i];
  849. if (annotation.start >= end) {
  850. annotation.start += changeCount;
  851. annotation.end += changeCount;
  852. e.changed.push(annotation);
  853. } else if (annotation.end <= start) {
  854. //nothing
  855. } else if (annotation.start < start && end < annotation.end) {
  856. annotation.end += changeCount;
  857. e.changed.push(annotation);
  858. } else {
  859. annotations.splice(i, 1);
  860. e.removed.push(annotation);
  861. i--;
  862. }
  863. }
  864. if (e.added.length > 0 || e.removed.length > 0 || e.changed.length > 0) {
  865. this.onChanged(e);
  866. }
  867. }
  868. };
  869. mEventTarget.EventTarget.addMixin(AnnotationModel.prototype);
  870. /**
  871. * Constructs a new styler for annotations.
  872. *
  873. * @param {orion.textview.TextView} view The styler view.
  874. * @param {orion.textview.AnnotationModel} view The styler annotation model.
  875. *
  876. * @class This object represents a styler for annotation attached to a text view.
  877. * @name orion.textview.AnnotationStyler
  878. * @borrows orion.textview.AnnotationTypeList#addAnnotationType as #addAnnotationType
  879. * @borrows orion.textview.AnnotationTypeList#getAnnotationTypePriority as #getAnnotationTypePriority
  880. * @borrows orion.textview.AnnotationTypeList#getAnnotationsByType as #getAnnotationsByType
  881. * @borrows orion.textview.AnnotationTypeList#isAnnotationTypeVisible as #isAnnotationTypeVisible
  882. * @borrows orion.textview.AnnotationTypeList#removeAnnotationType as #removeAnnotationType
  883. */
  884. function AnnotationStyler (view, annotationModel) {
  885. this._view = view;
  886. this._annotationModel = annotationModel;
  887. var self = this;
  888. this._listener = {
  889. onDestroy: function(e) {
  890. self._onDestroy(e);
  891. },
  892. onLineStyle: function(e) {
  893. self._onLineStyle(e);
  894. },
  895. onChanged: function(e) {
  896. self._onAnnotationModelChanged(e);
  897. }
  898. };
  899. view.addEventListener("Destroy", this._listener.onDestroy);
  900. view.addEventListener("LineStyle", this._listener.onLineStyle);
  901. annotationModel.addEventListener("Changed", this._listener.onChanged);
  902. }
  903. AnnotationStyler.prototype = /** @lends orion.textview.AnnotationStyler.prototype */ {
  904. /**
  905. * Destroys the styler.
  906. * <p>
  907. * Removes all listeners added by this styler.
  908. * </p>
  909. */
  910. destroy: function() {
  911. var view = this._view;
  912. if (view) {
  913. view.removeEventListener("Destroy", this._listener.onDestroy);
  914. view.removeEventListener("LineStyle", this._listener.onLineStyle);
  915. this.view = null;
  916. }
  917. var annotationModel = this._annotationModel;
  918. if (annotationModel) {
  919. annotationModel.removeEventListener("Changed", this._listener.onChanged);
  920. annotationModel = null;
  921. }
  922. },
  923. _mergeStyle: function(result, style) {
  924. if (style) {
  925. if (!result) { result = {}; }
  926. if (result.styleClass && style.styleClass && result.styleClass !== style.styleClass) {
  927. result.styleClass += " " + style.styleClass;
  928. } else {
  929. result.styleClass = style.styleClass;
  930. }
  931. var prop;
  932. if (style.style) {
  933. if (!result.style) { result.style = {}; }
  934. for (prop in style.style) {
  935. if (!result.style[prop]) {
  936. result.style[prop] = style.style[prop];
  937. }
  938. }
  939. }
  940. if (style.attributes) {
  941. if (!result.attributes) { result.attributes = {}; }
  942. for (prop in style.attributes) {
  943. if (!result.attributes[prop]) {
  944. result.attributes[prop] = style.attributes[prop];
  945. }
  946. }
  947. }
  948. }
  949. return result;
  950. },
  951. _mergeStyleRanges: function(ranges, styleRange) {
  952. if (!ranges) { return; }
  953. for (var i=0; i<ranges.length; i++) {
  954. var range = ranges[i];
  955. if (styleRange.end <= range.start) { break; }
  956. if (styleRange.start >= range.end) { continue; }
  957. var mergedStyle = this._mergeStyle({}, range.style);
  958. mergedStyle = this._mergeStyle(mergedStyle, styleRange.style);
  959. if (styleRange.start <= range.start && styleRange.end >= range.end) {
  960. ranges[i] = {start: range.start, end: range.end, style: mergedStyle};
  961. } else if (styleRange.start > range.start && styleRange.end < range.end) {
  962. ranges.splice(i, 1,
  963. {start: range.start, end: styleRange.start, style: range.style},
  964. {start: styleRange.start, end: styleRange.end, style: mergedStyle},
  965. {start: styleRange.end, end: range.end, style: range.style});
  966. i += 2;
  967. } else if (styleRange.start > range.start) {
  968. ranges.splice(i, 1,
  969. {start: range.start, end: styleRange.start, style: range.style},
  970. {start: styleRange.start, end: range.end, style: mergedStyle});
  971. i += 1;
  972. } else if (styleRange.end < range.end) {
  973. ranges.splice(i, 1,
  974. {start: range.start, end: styleRange.end, style: mergedStyle},
  975. {start: styleRange.end, end: range.end, style: range.style});
  976. i += 1;
  977. }
  978. }
  979. },
  980. _onAnnotationModelChanged: function(e) {
  981. if (e.textModelChangedEvent) {
  982. return;
  983. }
  984. var view = this._view;
  985. if (!view) { return; }
  986. var self = this;
  987. var model = view.getModel();
  988. function redraw(changes) {
  989. for (var i = 0; i < changes.length; i++) {
  990. if (!self.isAnnotationTypeVisible(changes[i].type)) { continue; }
  991. var start = changes[i].start;
  992. var end = changes[i].end;
  993. if (model.getBaseModel) {
  994. start = model.mapOffset(start, true);
  995. end = model.mapOffset(end, true);
  996. }
  997. if (start !== -1 && end !== -1) {
  998. view.redrawRange(start, end);
  999. }
  1000. }
  1001. }
  1002. redraw(e.added);
  1003. redraw(e.removed);
  1004. redraw(e.changed);
  1005. },
  1006. _onDestroy: function(e) {
  1007. this.destroy();
  1008. },
  1009. _onLineStyle: function (e) {
  1010. var annotationModel = this._annotationModel;
  1011. var viewModel = this._view.getModel();
  1012. var baseModel = annotationModel.getTextModel();
  1013. var start = e.lineStart;
  1014. var end = e.lineStart + e.lineText.length;
  1015. if (baseModel !== viewModel) {
  1016. start = viewModel.mapOffset(start);
  1017. end = viewModel.mapOffset(end);
  1018. }
  1019. var annotations = annotationModel.getAnnotations(start, end);
  1020. while (annotations.hasNext()) {
  1021. var annotation = annotations.next();
  1022. if (!this.isAnnotationTypeVisible(annotation.type)) { continue; }
  1023. if (annotation.rangeStyle) {
  1024. var annotationStart = annotation.start;
  1025. var annotationEnd = annotation.end;
  1026. if (baseModel !== viewModel) {
  1027. annotationStart = viewModel.mapOffset(annotationStart, true);
  1028. annotationEnd = viewModel.mapOffset(annotationEnd, true);
  1029. }
  1030. this._mergeStyleRanges(e.ranges, {start: annotationStart, end: annotationEnd, style: annotation.rangeStyle});
  1031. }
  1032. if (annotation.lineStyle) {
  1033. e.style = this._mergeStyle({}, e.style);
  1034. e.style = this._mergeStyle(e.style, annotation.lineStyle);
  1035. }
  1036. }
  1037. }
  1038. };
  1039. AnnotationTypeList.addMixin(AnnotationStyler.prototype);
  1040. return {
  1041. FoldingAnnotation: FoldingAnnotation,
  1042. AnnotationTypeList: AnnotationTypeList,
  1043. AnnotationModel: AnnotationModel,
  1044. AnnotationStyler: AnnotationStyler
  1045. };
  1046. });
  1047. /*******************************************************************************
  1048. * @license
  1049. * Copyright (c) 2010, 2011 IBM Corporation and others.
  1050. * All rights reserved. This program and the accompanying materials are made
  1051. * available under the terms of the Eclipse Public License v1.0
  1052. * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
  1053. * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
  1054. *
  1055. * Contributors: IBM Corporation - initial API and implementation
  1056. ******************************************************************************/
  1057. /*global define setTimeout clearTimeout setInterval clearInterval Node */
  1058. define("orion/textview/rulers", ['orion/textview/annotations', 'orion/textview/tooltip'], function(mAnnotations, mTooltip) {
  1059. /**
  1060. * Constructs a new ruler.
  1061. * <p>
  1062. * The default implementation does not implement all the methods in the interface
  1063. * and is useful only for objects implementing rulers.
  1064. * <p/>
  1065. *
  1066. * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler.
  1067. * @param {String} [rulerLocation="left"] the location for the ruler.
  1068. * @param {String} [rulerOverview="page"] the overview for the ruler.
  1069. * @param {orion.textview.Style} [rulerStyle] the style for the ruler.
  1070. *
  1071. * @class This interface represents a ruler for the text view.
  1072. * <p>
  1073. * A Ruler is a graphical element that is placed either on the left or on the right side of
  1074. * the view. It can be used to provide the view with per line decoration such as line numbering,
  1075. * bookmarks, breakpoints, folding disclosures, etc.
  1076. * </p><p>
  1077. * There are two types of rulers: page and document. A page ruler only shows the content for the lines that are
  1078. * visible, while a document ruler always shows the whole content.
  1079. * </p>
  1080. * <b>See:</b><br/>
  1081. * {@link orion.textview.LineNumberRuler}<br/>
  1082. * {@link orion.textview.AnnotationRuler}<br/>
  1083. * {@link orion.textview.OverviewRuler}<br/>
  1084. * {@link orion.textview.TextView}<br/>
  1085. * {@link orion.textview.TextView#addRuler}
  1086. * </p>
  1087. * @name orion.textview.Ruler
  1088. * @borrows orion.textview.AnnotationTypeList#addAnnotationType as #addAnnotationType
  1089. * @borrows orion.textview.AnnotationTypeList#getAnnotationTypePriority as #getAnnotationTypePriority
  1090. * @borrows orion.textview.AnnotationTypeList#getAnnotationsByType as #getAnnotationsByType
  1091. * @borrows orion.textview.AnnotationTypeList#isAnnotationTypeVisible as #isAnnotationTypeVisible
  1092. * @borrows orion.textview.AnnotationTypeList#removeAnnotationType as #removeAnnotationType
  1093. */
  1094. function Ruler (annotationModel, rulerLocation, rulerOverview, rulerStyle) {
  1095. this._location = rulerLocation || "left";
  1096. this._overview = rulerOverview || "page";
  1097. this._rulerStyle = rulerStyle;
  1098. this._view = null;
  1099. var self = this;
  1100. this._listener = {
  1101. onTextModelChanged: function(e) {
  1102. self._onTextModelChanged(e);
  1103. },
  1104. onAnnotationModelChanged: function(e) {
  1105. self._onAnnotationModelChanged(e);
  1106. }
  1107. };
  1108. this.setAnnotationModel(annotationModel);
  1109. }
  1110. Ruler.prototype = /** @lends orion.textview.Ruler.prototype */ {
  1111. /**
  1112. * Returns the annotations for a given line range merging multiple
  1113. * annotations when necessary.
  1114. * <p>
  1115. * This method is called by the text view when the ruler is redrawn.
  1116. * </p>
  1117. *
  1118. * @param {Number} startLine the start line index
  1119. * @param {Number} endLine the end line index
  1120. * @return {orion.textview.Annotation[]} the annotations for the line range. The array might be sparse.
  1121. */
  1122. getAnnotations: function(startLine, endLine) {
  1123. var annotationModel = this._annotationModel;
  1124. if (!annotationModel) { return []; }
  1125. var model = this._view.getModel();
  1126. var start = model.getLineStart(startLine);
  1127. var end = model.getLineEnd(endLine - 1);
  1128. var baseModel = model;
  1129. if (model.getBaseModel) {
  1130. baseModel = model.getBaseModel();
  1131. start = model.mapOffset(start);
  1132. end = model.mapOffset(end);
  1133. }
  1134. var result = [];
  1135. var annotations = this.getAnnotationsByType(annotationModel, start, end);
  1136. for (var i = 0; i < annotations.length; i++) {
  1137. var annotation = annotations[i];
  1138. var annotationLineStart = baseModel.getLineAtOffset(annotation.start);
  1139. var annotationLineEnd = baseModel.getLineAtOffset(Math.max(annotation.start, annotation.end - 1));
  1140. for (var lineIndex = annotationLineStart; lineIndex<=annotationLineEnd; lineIndex++) {
  1141. var visualLineIndex = lineIndex;
  1142. if (model !== baseModel) {
  1143. var ls = baseModel.getLineStart(lineIndex);
  1144. ls = model.mapOffset(ls, true);
  1145. if (ls === -1) { continue; }
  1146. visualLineIndex = model.getLineAtOffset(ls);
  1147. }
  1148. if (!(startLine <= visualLineIndex && visualLineIndex < endLine)) { continue; }
  1149. var rulerAnnotation = this._mergeAnnotation(result[visualLineIndex], annotation, lineIndex - annotationLineStart, annotationLineEnd - annotationLineStart + 1);
  1150. if (rulerAnnotation) {
  1151. result[visualLineIndex] = rulerAnnotation;
  1152. }
  1153. }
  1154. }
  1155. if (!this._multiAnnotation && this._multiAnnotationOverlay) {
  1156. for (var k in result) {
  1157. if (result[k]._multiple) {
  1158. result[k].html = result[k].html + this._multiAnnotationOverlay.html;
  1159. }
  1160. }
  1161. }
  1162. return result;
  1163. },
  1164. /**
  1165. * Returns the annotation model.
  1166. *
  1167. * @returns {orion.textview.AnnotationModel} the ruler annotation model.
  1168. *
  1169. * @see #setAnnotationModel
  1170. */
  1171. getAnnotationModel: function() {
  1172. return this._annotationModel;
  1173. },
  1174. /**
  1175. * Returns the ruler location.
  1176. *
  1177. * @returns {String} the ruler location, which is either "left" or "right".
  1178. *
  1179. * @see #getOverview
  1180. */
  1181. getLocation: function() {
  1182. return this._location;
  1183. },
  1184. /**
  1185. * Returns the ruler overview type.
  1186. *
  1187. * @returns {String} the overview type, which is either "page" or "document".
  1188. *
  1189. * @see #getLocation
  1190. */
  1191. getOverview: function() {
  1192. return this._overview;
  1193. },
  1194. /**
  1195. * Returns the style information for the ruler.
  1196. *
  1197. * @returns {orion.textview.Style} the style information.
  1198. */
  1199. getRulerStyle: function() {
  1200. return this._rulerStyle;
  1201. },
  1202. /**
  1203. * Returns the widest annotation which determines the width of the ruler.
  1204. * <p>
  1205. * If the ruler does not have a fixed width it should provide the widest
  1206. * annotation to avoid the ruler from changing size as the view scrolls.
  1207. * </p>
  1208. * <p>
  1209. * This method is called by the text view when the ruler is redrawn.
  1210. * </p>
  1211. *
  1212. * @returns {orion.textview.Annotation} the widest annotation.
  1213. *
  1214. * @see #getAnnotations
  1215. */
  1216. getWidestAnnotation: function() {
  1217. return null;
  1218. },
  1219. /**
  1220. * Sets the annotation model for the ruler.
  1221. *
  1222. * @param {orion.textview.AnnotationModel} annotationModel the annotation model.
  1223. *
  1224. * @see #getAnnotationModel
  1225. */
  1226. setAnnotationModel: function (annotationModel) {
  1227. if (this._annotationModel) {
  1228. this._annotationModel.removEventListener("Changed", this._listener.onAnnotationModelChanged);
  1229. }
  1230. this._annotationModel = annotationModel;
  1231. if (this._annotationModel) {
  1232. this._annotationModel.addEventListener("Changed", this._listener.onAnnotationModelChanged);
  1233. }
  1234. },
  1235. /**
  1236. * Sets the annotation that is displayed when a given line contains multiple
  1237. * annotations. This annotation is used when there are different types of
  1238. * annotations in a given line.
  1239. *
  1240. * @param {orion.textview.Annotation} annotation the annotation for lines with multiple annotations.
  1241. *
  1242. * @see #setMultiAnnotationOverlay
  1243. */
  1244. setMultiAnnotation: function(annotation) {
  1245. this._multiAnnotation = annotation;
  1246. },
  1247. /**
  1248. * Sets the annotation that overlays a line with multiple annotations. This annotation is displayed on
  1249. * top of the computed annotation for a given line when there are multiple annotations of the same type
  1250. * in the line. It is also used when the multiple annotation is not set.
  1251. *
  1252. * @param {orion.textview.Annotation} annotation the annotation overlay for lines with multiple annotations.
  1253. *
  1254. * @see #setMultiAnnotation
  1255. */
  1256. setMultiAnnotationOverlay: function(annotation) {
  1257. this._multiAnnotationOverlay = annotation;
  1258. },
  1259. /**
  1260. * Sets the view for the ruler.
  1261. * <p>
  1262. * This method is called by the text view when the ruler
  1263. * is added to the view.
  1264. * </p>
  1265. *
  1266. * @param {orion.textview.TextView} view the text view.
  1267. */
  1268. setView: function (view) {
  1269. if (this._onTextModelChanged && this._view) {
  1270. this._view.removeEventListener("ModelChanged", this._listener.onTextModelChanged);
  1271. }
  1272. this._view = view;
  1273. if (this._onTextModelChanged && this._view) {
  1274. this._view.addEventListener("ModelChanged", this._listener.onTextModelChanged);
  1275. }
  1276. },
  1277. /**
  1278. * This event is sent when the user clicks a line annotation.
  1279. *
  1280. * @event
  1281. * @param {Number} lineIndex the line index of the annotation under the pointer.
  1282. * @param {DOMEvent} e the click event.
  1283. */
  1284. onClick: function(lineIndex, e) {
  1285. },
  1286. /**
  1287. * This event is sent when the user double clicks a line annotation.
  1288. *
  1289. * @event
  1290. * @param {Number} lineIndex the line index of the annotation under the pointer.
  1291. * @param {DOMEvent} e the double click event.
  1292. */
  1293. onDblClick: function(lineIndex, e) {
  1294. },
  1295. /**
  1296. * This event is sent when the user moves the mouse over a line annotation.
  1297. *
  1298. * @event
  1299. * @param {Number} lineIndex the line index of the annotation under the pointer.
  1300. * @param {DOMEvent} e the mouse move event.
  1301. */
  1302. onMouseMove: function(lineIndex, e) {
  1303. var tooltip = mTooltip.Tooltip.getTooltip(this._view);
  1304. if (!tooltip) { return; }
  1305. if (tooltip.isVisible() && this._tooltipLineIndex === lineIndex) { return; }
  1306. this._tooltipLineIndex = lineIndex;
  1307. var self = this;
  1308. tooltip.setTarget({
  1309. y: e.clientY,
  1310. getTooltipInfo: function() {
  1311. return self._getTooltipInfo(self._tooltipLineIndex, this.y);
  1312. }
  1313. });
  1314. },
  1315. /**
  1316. * This event is sent when the mouse pointer enters a line annotation.
  1317. *
  1318. * @event
  1319. * @param {Number} lineIndex the line index of the annotation under the pointer.
  1320. * @param {DOMEvent} e the mouse over event.
  1321. */
  1322. onMouseOver: function(lineIndex, e) {
  1323. this.onMouseMove(lineIndex, e);
  1324. },
  1325. /**
  1326. * This event is sent when the mouse pointer exits a line annotation.
  1327. *
  1328. * @event
  1329. * @param {Number} lineIndex the line index of the annotation under the pointer.
  1330. * @param {DOMEvent} e the mouse out event.
  1331. */
  1332. onMouseOut: function(lineIndex, e) {
  1333. var tooltip = mTooltip.Tooltip.getTooltip(this._view);
  1334. if (!tooltip) { return; }
  1335. tooltip.setTarget(null);
  1336. },
  1337. /** @ignore */
  1338. _getTooltipInfo: function(lineIndex, y) {
  1339. if (lineIndex === undefined) { return; }
  1340. var view = this._view;
  1341. var model = view.getModel();
  1342. var annotationModel = this._annotationModel;
  1343. var annotations = [];
  1344. if (annotationModel) {
  1345. var start = model.getLineStart(lineIndex);
  1346. var end = model.getLineEnd(lineIndex);
  1347. if (model.getBaseModel) {
  1348. start = model.mapOffset(start);
  1349. end = model.mapOffset(end);
  1350. }
  1351. annotations = this.getAnnotationsByType(annotationModel, start, end);
  1352. }
  1353. var contents = this._getTooltipContents(lineIndex, annotations);
  1354. if (!contents) { return null; }
  1355. var info = {
  1356. contents: contents,
  1357. anchor: this.getLocation()
  1358. };
  1359. var rect = view.getClientArea();
  1360. if (this.getOverview() === "document") {
  1361. rect.y = view.convert({y: y}, "view", "document").y;
  1362. } else {
  1363. rect.y = view.getLocationAtOffset(model.getLineStart(lineIndex)).y;
  1364. }
  1365. view.convert(rect, "document", "page");
  1366. info.x = rect.x;
  1367. info.y = rect.y;
  1368. if (info.anchor === "right") {
  1369. info.x += rect.width;
  1370. }
  1371. info.maxWidth = rect.width;
  1372. info.maxHeight = rect.height - (rect.y - view._parent.getBoundingClientRect().top);
  1373. return info;
  1374. },
  1375. /** @ignore */
  1376. _getTooltipContents: function(lineIndex, annotations) {
  1377. return annotations;
  1378. },
  1379. /** @ignore */
  1380. _onAnnotationModelChanged: function(e) {
  1381. var view = this._view;
  1382. if (!view) { return; }
  1383. var model = view.getModel(), self = this;
  1384. var lineCount = model.getLineCount();
  1385. if (e.textModelChangedEvent) {
  1386. var start = e.textModelChangedEvent.start;
  1387. if (model.getBaseModel) { start = model.mapOffset(start, true); }
  1388. var startLine = model.getLineAtOffset(start);
  1389. view.redrawLines(startLine, lineCount, self);
  1390. return;
  1391. }
  1392. function redraw(changes) {
  1393. for (var i = 0; i < changes.length; i++) {
  1394. if (!self.isAnnotationTypeVisible(changes[i].type)) { continue; }
  1395. var start = changes[i].start;
  1396. var end = changes[i].end;
  1397. if (model.getBaseModel) {
  1398. start = model.mapOffset(start, true);
  1399. end = model.mapOffset(end, true);
  1400. }
  1401. if (start !== -1 && end !== -1) {
  1402. view.redrawLines(model.getLineAtOffset(start), model.getLineAtOffset(Math.max(start, end - 1)) + 1, self);
  1403. }
  1404. }
  1405. }
  1406. redraw(e.added);
  1407. redraw(e.removed);
  1408. redraw(e.changed);
  1409. },
  1410. /** @ignore */
  1411. _mergeAnnotation: function(result, annotation, annotationLineIndex, annotationLineCount) {
  1412. if (!result) { result = {}; }
  1413. if (annotationLineIndex === 0) {
  1414. if (result.html && annotation.html) {
  1415. if (annotation.html !== result.html) {
  1416. if (!result._multiple && this._multiAnnotation) {
  1417. result.html = this._multiAnnotation.html;
  1418. }
  1419. }
  1420. result._multiple = true;
  1421. } else {
  1422. result.html = annotation.html;
  1423. }
  1424. }
  1425. result.style = this._mergeStyle(result.style, annotation.style);
  1426. return result;
  1427. },
  1428. /** @ignore */
  1429. _mergeStyle: function(result, style) {
  1430. if (style) {
  1431. if (!result) { result = {}; }
  1432. if (result.styleClass && style.styleClass && result.styleClass !== style.styleClass) {
  1433. result.styleClass += " " + style.styleClass;
  1434. } else {
  1435. result.styleClass = style.styleClass;
  1436. }
  1437. var prop;
  1438. if (style.style) {
  1439. if (!result.style) { result.style = {}; }
  1440. for (prop in style.style) {
  1441. if (!result.style[prop]) {
  1442. result.style[prop] = style.style[prop];
  1443. }
  1444. }
  1445. }
  1446. if (style.attributes) {
  1447. if (!result.attributes) { result.attributes = {}; }
  1448. for (prop in style.attributes) {
  1449. if (!result.attributes[prop]) {
  1450. result.attributes[prop] = style.attributes[prop];
  1451. }
  1452. }
  1453. }
  1454. }
  1455. return result;
  1456. }
  1457. };
  1458. mAnnotations.AnnotationTypeList.addMixin(Ruler.prototype);
  1459. /**
  1460. * Constructs a new line numbering ruler.
  1461. *
  1462. * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler.
  1463. * @param {String} [rulerLocation="left"] the location for the ruler.
  1464. * @param {orion.textview.Style} [rulerStyle=undefined] the style for the ruler.
  1465. * @param {orion.textview.Style} [oddStyle={style: {backgroundColor: "white"}] the style for lines with odd line index.
  1466. * @param {orion.textview.Style} [evenStyle={backgroundColor: "white"}] the style for lines with even line index.
  1467. *
  1468. * @augments orion.textview.Ruler
  1469. * @class This objects implements a line numbering ruler.
  1470. *
  1471. * <p><b>See:</b><br/>
  1472. * {@link orion.textview.Ruler}
  1473. * </p>
  1474. * @name orion.textview.LineNumberRuler
  1475. */
  1476. function LineNumberRuler (annotationModel, rulerLocation, rulerStyle, oddStyle, evenStyle) {
  1477. Ruler.call(this, annotationModel, rulerLocation, "page", rulerStyle);
  1478. this._oddStyle = oddStyle || {style: {backgroundColor: "white"}};
  1479. this._evenStyle = evenStyle || {style: {backgroundColor: "white"}};
  1480. this._numOfDigits = 0;
  1481. }
  1482. LineNumberRuler.prototype = new Ruler();
  1483. /** @ignore */
  1484. LineNumberRuler.prototype.getAnnotations = function(startLine, endLine) {
  1485. var result = Ruler.prototype.getAnnotations.call(this, startLine, endLine);
  1486. var model = this._view.getModel();
  1487. for (var lineIndex = startLine; lineIndex < endLine; lineIndex++) {
  1488. var style = lineIndex & 1 ? this._oddStyle : this._evenStyle;
  1489. var mapLine = lineIndex;
  1490. if (model.getBaseModel) {
  1491. var lineStart = model.getLineStart(mapLine);
  1492. mapLine = model.getBaseModel().getLineAtOffset(model.mapOffset(lineStart));
  1493. }
  1494. if (!result[lineIndex]) { result[lineIndex] = {}; }
  1495. result[lineIndex].html = (mapLine + 1) + "";
  1496. if (!result[lineIndex].style) { result[lineIndex].style = style; }
  1497. }
  1498. return result;
  1499. };
  1500. /** @ignore */
  1501. LineNumberRuler.prototype.getWidestAnnotation = function() {
  1502. var lineCount = this._view.getModel().getLineCount();
  1503. return this.getAnnotations(lineCount - 1, lineCount)[lineCount - 1];
  1504. };
  1505. /** @ignore */
  1506. LineNumberRuler.prototype._onTextModelChanged = function(e) {
  1507. var start = e.start;
  1508. var model = this._view.getModel();
  1509. var lineCount = model.getBaseModel ? model.getBaseModel().getLineCount() : model.getLineCount();
  1510. var numOfDigits = (lineCount+"").length;
  1511. if (this._numOfDigits !== numOfDigits) {
  1512. this._numOfDigits = numOfDigits;
  1513. var startLine = model.getLineAtOffset(start);
  1514. this._view.redrawLines(startLine, model.getLineCount(), this);
  1515. }
  1516. };
  1517. /**
  1518. * @class This is class represents an annotation for the AnnotationRuler.
  1519. * <p>
  1520. * <b>See:</b><br/>
  1521. * {@link orion.textview.AnnotationRuler}
  1522. * </p>
  1523. *
  1524. * @name orion.textview.Annotation
  1525. *
  1526. * @property {String} [html=""] The html content for the annotation, typically contains an image.
  1527. * @property {orion.textview.Style} [style] the style for the annotation.
  1528. * @property {orion.textview.Style} [overviewStyle] the style for the annotation in the overview ruler.
  1529. */
  1530. /**
  1531. * Constructs a new annotation ruler.
  1532. *
  1533. * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler.
  1534. * @param {String} [rulerLocation="left"] the location for the ruler.
  1535. * @param {orion.textview.Style} [rulerStyle=undefined] the style for the ruler.
  1536. * @param {orion.textview.Annotation} [defaultAnnotation] the default annotation.
  1537. *
  1538. * @augments orion.textview.Ruler
  1539. * @class This objects implements an annotation ruler.
  1540. *
  1541. * <p><b>See:</b><br/>
  1542. * {@link orion.textview.Ruler}<br/>
  1543. * {@link orion.textview.Annotation}
  1544. * </p>
  1545. * @name orion.textview.AnnotationRuler
  1546. */
  1547. function AnnotationRuler (annotationModel, rulerLocation, rulerStyle) {
  1548. Ruler.call(this, annotationModel, rulerLocation, "page", rulerStyle);
  1549. }
  1550. AnnotationRuler.prototype = new Ruler();
  1551. /**
  1552. * Constructs a new overview ruler.
  1553. * <p>
  1554. * The overview ruler is used in conjunction with a AnnotationRuler, for each annotation in the
  1555. * AnnotationRuler this ruler displays a mark in the overview. Clicking on the mark causes the
  1556. * view to scroll to the annotated line.
  1557. * </p>
  1558. *
  1559. * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler.
  1560. * @param {String} [rulerLocation="left"] the location for the ruler.
  1561. * @param {orion.textview.Style} [rulerStyle=undefined] the style for the ruler.
  1562. *
  1563. * @augments orion.textview.Ruler
  1564. * @class This objects implements an overview ruler.
  1565. *
  1566. * <p><b>See:</b><br/>
  1567. * {@link orion.textview.AnnotationRuler} <br/>
  1568. * {@link orion.textview.Ruler}
  1569. * </p>
  1570. * @name orion.textview.OverviewRuler
  1571. */
  1572. function OverviewRuler (annotationModel, rulerLocation, rulerStyle) {
  1573. Ruler.call(this, annotationModel, rulerLocation, "document", rulerStyle);
  1574. }
  1575. OverviewRuler.prototype = new Ruler();
  1576. /** @ignore */
  1577. OverviewRuler.prototype.getRulerStyle = function() {
  1578. var result = {style: {lineHeight: "1px", fontSize: "1px"}};
  1579. result = this._mergeStyle(result, this._rulerStyle);
  1580. return result;
  1581. };
  1582. /** @ignore */
  1583. OverviewRuler.prototype.onClick = function(lineIndex, e) {
  1584. if (lineIndex === undefined) { return; }
  1585. this._view.setTopIndex(lineIndex);
  1586. };
  1587. /** @ignore */
  1588. OverviewRuler.prototype._getTooltipContents = function(lineIndex, annotations) {
  1589. if (annotations.length === 0) {
  1590. var model = this._view.getModel();
  1591. var mapLine = lineIndex;
  1592. if (model.getBaseModel) {
  1593. var lineStart = model.getLineStart(mapLine);
  1594. mapLine = model.getBaseModel().getLineAtOffset(model.mapOffset(lineStart));
  1595. }
  1596. return "Line: " + (mapLine + 1);
  1597. }
  1598. return Ruler.prototype._getTooltipContents.call(this, lineIndex, annotations);
  1599. };
  1600. /** @ignore */
  1601. OverviewRuler.prototype._mergeAnnotation = function(previousAnnotation, annotation, annotationLineIndex, annotationLineCount) {
  1602. if (annotationLineIndex !== 0) { return undefined; }
  1603. var result = previousAnnotation;
  1604. if (!result) {
  1605. //TODO annotationLineCount does not work when there are folded lines
  1606. var height = 3 * annotationLineCount;
  1607. result = {html: "&nbsp;", style: { style: {height: height + "px"}}};
  1608. result.style = this._mergeStyle(result.style, annotation.overviewStyle);
  1609. }
  1610. return result;
  1611. };
  1612. /**
  1613. * Constructs a new folding ruler.
  1614. *
  1615. * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler.
  1616. * @param {String} [rulerLocation="left"] the location for the ruler.
  1617. * @param {orion.textview.Style} [rulerStyle=undefined] the style for the ruler.
  1618. *
  1619. * @augments orion.textview.Ruler
  1620. * @class This objects implements an overview ruler.
  1621. *
  1622. * <p><b>See:</b><br/>
  1623. * {@link orion.textview.AnnotationRuler} <br/>
  1624. * {@link orion.textview.Ruler}
  1625. * </p>
  1626. * @name orion.textview.OverviewRuler
  1627. */
  1628. function FoldingRuler (annotationModel, rulerLocation, rulerStyle) {
  1629. AnnotationRuler.call(this, annotationModel, rulerLocation, rulerStyle);
  1630. }
  1631. FoldingRuler.prototype = new AnnotationRuler();
  1632. /** @ignore */
  1633. FoldingRuler.prototype.onClick = function(lineIndex, e) {
  1634. if (lineIndex === undefined) { return; }
  1635. var annotationModel = this._annotationModel;
  1636. if (!annotationModel) { return; }
  1637. var view = this._view;
  1638. var model = view.getModel();
  1639. var start = model.getLineStart(lineIndex);
  1640. var end = model.getLineEnd(lineIndex, true);
  1641. if (model.getBaseModel) {
  1642. start = model.mapOffset(start);
  1643. end = model.mapOffset(end);
  1644. }
  1645. var annotation, iter = annotationModel.getAnnotations(start, end);
  1646. while (!annotation && iter.hasNext()) {
  1647. var a = iter.next();
  1648. if (!this.isAnnotationTypeVisible(a.type)) { continue; }
  1649. annotation = a;
  1650. }
  1651. if (annotation) {
  1652. var tooltip = mTooltip.Tooltip.getTooltip(this._view);
  1653. if (tooltip) {
  1654. tooltip.setTarget(null);
  1655. }
  1656. if (annotation.expanded) {
  1657. annotation.collapse();
  1658. } else {
  1659. annotation.expand();
  1660. }
  1661. this._annotationModel.modifyAnnotation(annotation);
  1662. }
  1663. };
  1664. /** @ignore */
  1665. FoldingRuler.prototype._getTooltipContents = function(lineIndex, annotations) {
  1666. if (annotations.length === 1) {
  1667. if (annotations[0].expanded) {
  1668. return null;
  1669. }
  1670. }
  1671. return AnnotationRuler.prototype._getTooltipContents.call(this, lineIndex, annotations);
  1672. };
  1673. /** @ignore */
  1674. FoldingRuler.prototype._onAnnotationModelChanged = function(e) {
  1675. if (e.textModelChangedEvent) {
  1676. AnnotationRuler.prototype._onAnnotationModelChanged.call(this, e);
  1677. return;
  1678. }
  1679. var view = this._view;
  1680. if (!view) { return; }
  1681. var model = view.getModel(), self = this, i;
  1682. var lineCount = model.getLineCount(), lineIndex = lineCount;
  1683. function redraw(changes) {
  1684. for (i = 0; i < changes.length; i++) {
  1685. if (!self.isAnnotationTypeVisible(changes[i].type)) { continue; }
  1686. var start = changes[i].start;
  1687. if (model.getBaseModel) {
  1688. start = model.mapOffset(start, true);
  1689. }
  1690. if (start !== -1) {
  1691. lineIndex = Math.min(lineIndex, model.getLineAtOffset(start));
  1692. }
  1693. }
  1694. }
  1695. redraw(e.added);
  1696. redraw(e.removed);
  1697. redraw(e.changed);
  1698. var rulers = view.getRulers();
  1699. for (i = 0; i < rulers.length; i++) {
  1700. view.redrawLines(lineIndex, lineCount, rulers[i]);
  1701. }
  1702. };
  1703. return {
  1704. Ruler: Ruler,
  1705. AnnotationRuler: AnnotationRuler,
  1706. LineNumberRuler: LineNumberRuler,
  1707. OverviewRuler: OverviewRuler,
  1708. FoldingRuler: FoldingRuler
  1709. };
  1710. });
  1711. /*******************************************************************************
  1712. * @license
  1713. * Copyright (c) 2010, 2011 IBM Corporation and others.
  1714. * All rights reserved. This program and the accompanying materials are made
  1715. * available under the terms of the Eclipse Public License v1.0
  1716. * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
  1717. * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
  1718. *
  1719. * Contributors: IBM Corporation - initial API and implementation
  1720. ******************************************************************************/
  1721. /*global define */
  1722. define("orion/textview/undoStack", [], function() {
  1723. /**
  1724. * Constructs a new Change object.
  1725. *
  1726. * @class
  1727. * @name orion.textview.Change
  1728. * @private
  1729. */
  1730. function Change(offset, text, previousText) {
  1731. this.offset = offset;
  1732. this.text = text;
  1733. this.previousText = previousText;
  1734. }
  1735. Change.prototype = {
  1736. /** @ignore */
  1737. undo: function (view, select) {
  1738. this._doUndoRedo(this.offset, this.previousText, this.text, view, select);
  1739. },
  1740. /** @ignore */
  1741. redo: function (view, select) {
  1742. this._doUndoRedo(this.offset, this.text, this.previousText, view, select);
  1743. },
  1744. _doUndoRedo: function(offset, text, previousText, view, select) {
  1745. var model = view.getModel();
  1746. /*
  1747. * TODO UndoStack should be changing the text in the base model.
  1748. * This is code needs to change when modifications in the base
  1749. * model are supported properly by the projection model.
  1750. */
  1751. if (model.mapOffset && view.annotationModel) {
  1752. var mapOffset = model.mapOffset(offset, true);
  1753. if (mapOffset < 0) {
  1754. var annotationModel = view.annotationModel;
  1755. var iter = annotationModel.getAnnotations(offset, offset + 1);
  1756. while (iter.hasNext()) {
  1757. var annotation = iter.next();
  1758. if (annotation.type === "orion.annotation.folding") {
  1759. annotation.expand();
  1760. mapOffset = model.mapOffset(offset, true);
  1761. break;
  1762. }
  1763. }
  1764. }
  1765. if (mapOffset < 0) { return; }
  1766. offset = mapOffset;
  1767. }
  1768. view.setText(text, offset, offset + previousText.length);
  1769. if (select) {
  1770. view.setSelection(offset, offset + text.length);
  1771. }
  1772. }
  1773. };
  1774. /**
  1775. * Constructs a new CompoundChange object.
  1776. *
  1777. * @class
  1778. * @name orion.textview.CompoundChange
  1779. * @private
  1780. */
  1781. function CompoundChange () {
  1782. this.changes = [];
  1783. }
  1784. CompoundChange.prototype = {
  1785. /** @ignore */
  1786. add: function (change) {
  1787. this.changes.push(change);
  1788. },
  1789. /** @ignore */
  1790. end: function (view) {
  1791. this.endSelection = view.getSelection();
  1792. this.endCaret = view.getCaretOffset();
  1793. },
  1794. /** @ignore */
  1795. undo: function (view, select) {
  1796. for (var i=this.changes.length - 1; i >= 0; i--) {
  1797. this.changes[i].undo(view, false);
  1798. }
  1799. if (select) {
  1800. var start = this.startSelection.start;
  1801. var end = this.startSelection.end;
  1802. view.setSelection(this.startCaret ? start : end, this.startCaret ? end : start);
  1803. }
  1804. },
  1805. /** @ignore */
  1806. redo: function (view, select) {
  1807. for (var i = 0; i < this.changes.length; i++) {
  1808. this.changes[i].redo(view, false);
  1809. }
  1810. if (select) {
  1811. var start = this.endSelection.start;
  1812. var end = this.endSelection.end;
  1813. view.setSelection(this.endCaret ? start : end, this.endCaret ? end : start);
  1814. }
  1815. },
  1816. /** @ignore */
  1817. start: function (view) {
  1818. this.startSelection = view.getSelection();
  1819. this.startCaret = view.getCaretOffset();
  1820. }
  1821. };
  1822. /**
  1823. * Constructs a new UndoStack on a text view.
  1824. *
  1825. * @param {orion.textview.TextView} view the text view for the undo stack.
  1826. * @param {Number} [size=100] the size for the undo stack.
  1827. *
  1828. * @name orion.textview.UndoStack
  1829. * @class The UndoStack is used to record the history of a text model associated to an view. Every
  1830. * change to the model is added to stack, allowing the application to undo and redo these changes.
  1831. *
  1832. * <p>
  1833. * <b>See:</b><br/>
  1834. * {@link orion.textview.TextView}<br/>
  1835. * </p>
  1836. */
  1837. function UndoStack (view, size) {
  1838. this.view = view;
  1839. this.size = size !== undefined ? size : 100;
  1840. this.reset();
  1841. var model = view.getModel();
  1842. if (model.getBaseModel) {
  1843. model = model.getBaseModel();
  1844. }
  1845. this.model = model;
  1846. var self = this;
  1847. this._listener = {
  1848. onChanging: function(e) {
  1849. self._onChanging(e);
  1850. },
  1851. onDestroy: function(e) {
  1852. self._onDestroy(e);
  1853. }
  1854. };
  1855. model.addEventListener("Changing", this._listener.onChanging);
  1856. view.addEventListener("Destroy", this._listener.onDestroy);
  1857. }
  1858. UndoStack.prototype = /** @lends orion.textview.UndoStack.prototype */ {
  1859. /**
  1860. * Adds a change to the stack.
  1861. *
  1862. * @param change the change to add.
  1863. * @param {Number} change.offset the offset of the change
  1864. * @param {String} change.text the new text of the change
  1865. * @param {String} change.previousText the previous text of the change
  1866. */
  1867. add: function (change) {
  1868. if (this.compoundChange) {
  1869. this.compoundChange.add(change);
  1870. } else {
  1871. var length = this.stack.length;
  1872. this.stack.splice(this.index, length-this.index, change);
  1873. this.index++;
  1874. if (this.stack.length > this.size) {
  1875. this.stack.shift();
  1876. this.index--;
  1877. this.cleanIndex--;
  1878. }
  1879. }
  1880. },
  1881. /**
  1882. * Marks the current state of the stack as clean.
  1883. *
  1884. * <p>
  1885. * This function is typically called when the content of view associated with the stack is saved.
  1886. * </p>
  1887. *
  1888. * @see #isClean
  1889. */
  1890. markClean: function() {
  1891. this.endCompoundChange();
  1892. this._commitUndo();
  1893. this.cleanIndex = this.index;
  1894. },
  1895. /**
  1896. * Returns true if current state of stack is the same
  1897. * as the state when markClean() was called.
  1898. *
  1899. * <p>
  1900. * For example, the application calls markClean(), then calls undo() four times and redo() four times.
  1901. * At this point isClean() returns true.
  1902. * </p>
  1903. * <p>
  1904. * This function is typically called to determine if the content of the view associated with the stack
  1905. * has changed since the last time it was saved.
  1906. * </p>
  1907. *
  1908. * @return {Boolean} returns if the state is the same as the state when markClean() was called.
  1909. *
  1910. * @see #markClean
  1911. */
  1912. isClean: function() {
  1913. return this.cleanIndex === this.getSize().undo;
  1914. },
  1915. /**
  1916. * Returns true if there is at least one change to undo.
  1917. *
  1918. * @return {Boolean} returns true if there is at least one change to undo.
  1919. *
  1920. * @see #canRedo
  1921. * @see #undo
  1922. */
  1923. canUndo: function() {
  1924. return this.getSize().undo > 0;
  1925. },
  1926. /**
  1927. * Returns true if there is at least one change to redo.
  1928. *
  1929. * @return {Boolean} returns true if there is at least one change to redo.
  1930. *
  1931. * @see #canUndo
  1932. * @see #redo
  1933. */
  1934. canRedo: function() {
  1935. return this.getSize().redo > 0;
  1936. },
  1937. /**
  1938. * Finishes a compound change.
  1939. *
  1940. * @see #startCompoundChange
  1941. */
  1942. endCompoundChange: function() {
  1943. if (this.compoundChange) {
  1944. this.compoundChange.end(this.view);
  1945. }
  1946. this.compoundChange = undefined;
  1947. },
  1948. /**
  1949. * Returns the sizes of the stack.
  1950. *
  1951. * @return {object} a object where object.undo is the number of changes that can be un-done,
  1952. * and object.redo is the number of changes that can be re-done.
  1953. *
  1954. * @see #canUndo
  1955. * @see #canRedo
  1956. */
  1957. getSize: function() {
  1958. var index = this.index;
  1959. var length = this.stack.length;
  1960. if (this._undoStart !== undefined) {
  1961. index++;
  1962. }
  1963. return {undo: index, redo: (length - index)};
  1964. },
  1965. /**
  1966. * Undo the last change in the stack.
  1967. *
  1968. * @return {Boolean} returns true if a change was un-done.
  1969. *
  1970. * @see #redo
  1971. * @see #canUndo
  1972. */
  1973. undo: function() {
  1974. this._commitUndo();
  1975. if (this.index <= 0) {
  1976. return false;
  1977. }
  1978. var change = this.stack[--this.index];
  1979. this._ignoreUndo = true;
  1980. change.undo(this.view, true);
  1981. this._ignoreUndo = false;
  1982. return true;
  1983. },
  1984. /**
  1985. * Redo the last change in the stack.
  1986. *
  1987. * @return {Boolean} returns true if a change was re-done.
  1988. *
  1989. * @see #undo
  1990. * @see #canRedo
  1991. */
  1992. redo: function() {
  1993. this._commitUndo();
  1994. if (this.index >= this.stack.length) {
  1995. return false;
  1996. }
  1997. var change = this.stack[this.index++];
  1998. this._ignoreUndo = true;
  1999. change.redo(this.view, true);
  2000. this._ignoreUndo = false;
  2001. return true;
  2002. },
  2003. /**
  2004. * Reset the stack to its original state. All changes in the stack are thrown away.
  2005. */
  2006. reset: function() {
  2007. this.index = this.cleanIndex = 0;
  2008. this.stack = [];
  2009. this._undoStart = undefined;
  2010. this._undoText = "";
  2011. this._undoType = 0;
  2012. this._ignoreUndo = false;
  2013. this._compoundChange = undefined;
  2014. },
  2015. /**
  2016. * Starts a compound change.
  2017. * <p>
  2018. * All changes added to stack from the time startCompoundChange() is called
  2019. * to the time that endCompoundChange() is called are compound on one change that can be un-done or re-done
  2020. * with one single call to undo() or redo().
  2021. * </p>
  2022. *
  2023. * @see #endCompoundChange
  2024. */
  2025. startCompoundChange: function() {
  2026. this._commitUndo();
  2027. var change = new CompoundChange();
  2028. this.add(change);
  2029. this.compoundChange = change;
  2030. this.compoundChange.start(this.view);
  2031. },
  2032. _commitUndo: function () {
  2033. if (this._undoStart !== undefined) {
  2034. if (this._undoType === -1) {
  2035. this.add(new Change(this._undoStart, "", this._undoText, ""));
  2036. } else {
  2037. this.add(new Change(this._undoStart, this._undoText, ""));
  2038. }
  2039. this._undoStart = undefined;
  2040. this._undoText = "";
  2041. this._undoType = 0;
  2042. }
  2043. },
  2044. _onDestroy: function(evt) {
  2045. this.model.removeEventListener("Changing", this._listener.onChanging);
  2046. this.view.removeEventListener("Destroy", this._listener.onDestroy);
  2047. },
  2048. _onChanging: function(e) {
  2049. var newText = e.text;
  2050. var start = e.start;
  2051. var removedCharCount = e.removedCharCount;
  2052. var addedCharCount = e.addedCharCount;
  2053. if (this._ignoreUndo) {
  2054. return;
  2055. }
  2056. if (this._undoStart !== undefined &&
  2057. !((addedCharCount === 1 && removedCharCount === 0 && this._undoType === 1 && start === this._undoStart + this._undoText.length) ||
  2058. (addedCharCount === 0 && removedCharCount === 1 && this._undoType === -1 && (((start + 1) === this._undoStart) || (start === this._undoStart)))))
  2059. {
  2060. this._commitUndo();
  2061. }
  2062. if (!this.compoundChange) {
  2063. if (addedCharCount === 1 && removedCharCount === 0) {
  2064. if (this._undoStart === undefined) {
  2065. this._undoStart = start;
  2066. }
  2067. this._undoText = this._undoText + newText;
  2068. this._undoType = 1;
  2069. return;
  2070. } else if (addedCharCount === 0 && removedCharCount === 1) {
  2071. var deleting = this._undoText.length > 0 && this._undoStart === start;
  2072. this._undoStart = start;
  2073. this._undoType = -1;
  2074. if (deleting) {
  2075. this._undoText = this._undoText + this.model.getText(start, start + removedCharCount);
  2076. } else {
  2077. this._undoText = this.model.getText(start, start + removedCharCount) + this._undoText;
  2078. }
  2079. return;
  2080. }
  2081. }
  2082. this.add(new Change(start, newText, this.model.getText(start, start + removedCharCount)));
  2083. }
  2084. };
  2085. return {
  2086. UndoStack: UndoStack
  2087. };
  2088. });
  2089. /*******************************************************************************
  2090. * @license
  2091. * Copyright (c) 2010, 2011 IBM Corporation and others.
  2092. * All rights reserved. This program and the accompanying materials are made
  2093. * available under the terms of the Eclipse Public License v1.0
  2094. * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
  2095. * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
  2096. *
  2097. * Contributors:
  2098. * Felipe Heidrich (IBM Corporation) - initial API and implementation
  2099. * Silenio Quarti (IBM Corporation) - initial API and implementation
  2100. ******************************************************************************/
  2101. /*global define window*/
  2102. define("orion/textview/textModel", ['orion/textview/eventTarget'], function(mEventTarget) {
  2103. var isWindows = window.navigator.platform.indexOf("Win") !== -1;
  2104. /**
  2105. * Constructs a new TextModel with the given text and default line delimiter.
  2106. *
  2107. * @param {String} [text=""] the text that the model will store
  2108. * @param {String} [lineDelimiter=platform delimiter] the line delimiter used when inserting new lines to the model.
  2109. *
  2110. * @name orion.textview.TextModel
  2111. * @class The TextModel is an interface that provides text for the view. Applications may
  2112. * implement the TextModel interface to provide a custom store for the view content. The
  2113. * view interacts with its text model in order to access and update the text that is being
  2114. * displayed and edited in the view. This is the default implementation.
  2115. * <p>
  2116. * <b>See:</b><br/>
  2117. * {@link orion.textview.TextView}<br/>
  2118. * {@link orion.textview.TextView#setModel}
  2119. * </p>
  2120. * @borrows orion.textview.EventTarget#addEventListener as #addEventListener
  2121. * @borrows orion.textview.EventTarget#removeEventListener as #removeEventListener
  2122. * @borrows orion.textview.EventTarget#dispatchEvent as #dispatchEvent
  2123. */
  2124. function TextModel(text, lineDelimiter) {
  2125. this._lastLineIndex = -1;
  2126. this._text = [""];
  2127. this._lineOffsets = [0];
  2128. this.setText(text);
  2129. this.setLineDelimiter(lineDelimiter);
  2130. }
  2131. TextModel.prototype = /** @lends orion.textview.TextModel.prototype */ {
  2132. /**
  2133. * Returns the number of characters in the model.
  2134. *
  2135. * @returns {Number} the number of characters in the model.
  2136. */
  2137. getCharCount: function() {
  2138. var count = 0;
  2139. for (var i = 0; i<this._text.length; i++) {
  2140. count += this._text[i].length;
  2141. }
  2142. return count;
  2143. },
  2144. /**
  2145. * Returns the text of the line at the given index.
  2146. * <p>
  2147. * The valid indices are 0 to line count exclusive. Returns <code>null</code>
  2148. * if the index is out of range.
  2149. * </p>
  2150. *
  2151. * @param {Number} lineIndex the zero based index of the line.
  2152. * @param {Boolean} [includeDelimiter=false] whether or not to include the line delimiter.
  2153. * @returns {String} the line text or <code>null</code> if out of range.
  2154. *
  2155. * @see #getLineAtOffset
  2156. */
  2157. getLine: function(lineIndex, includeDelimiter) {
  2158. var lineCount = this.getLineCount();
  2159. if (!(0 <= lineIndex && lineIndex < lineCount)) {
  2160. return null;
  2161. }
  2162. var start = this._lineOffsets[lineIndex];
  2163. if (lineIndex + 1 < lineCount) {
  2164. var text = this.getText(start, this._lineOffsets[lineIndex + 1]);
  2165. if (includeDelimiter) {
  2166. return text;
  2167. }
  2168. var end = text.length, c;
  2169. while (((c = text.charCodeAt(end - 1)) === 10) || (c === 13)) {
  2170. end--;
  2171. }
  2172. return text.substring(0, end);
  2173. } else {
  2174. return this.getText(start);
  2175. }
  2176. },
  2177. /**
  2178. * Returns the line index at the given character offset.
  2179. * <p>
  2180. * The valid offsets are 0 to char count inclusive. The line index for
  2181. * char count is <code>line count - 1</code>. Returns <code>-1</code> if
  2182. * the offset is out of range.
  2183. * </p>
  2184. *
  2185. * @param {Number} offset a character offset.
  2186. * @returns {Number} the zero based line index or <code>-1</code> if out of range.
  2187. */
  2188. getLineAtOffset: function(offset) {
  2189. var charCount = this.getCharCount();
  2190. if (!(0 <= offset && offset <= charCount)) {
  2191. return -1;
  2192. }
  2193. var lineCount = this.getLineCount();
  2194. if (offset === charCount) {
  2195. return lineCount - 1;
  2196. }
  2197. var lineStart, lineEnd;
  2198. var index = this._lastLineIndex;
  2199. if (0 <= index && index < lineCount) {
  2200. lineStart = this._lineOffsets[index];
  2201. lineEnd = index + 1 < lineCount ? this._lineOffsets[index + 1] : charCount;
  2202. if (lineStart <= offset && offset < lineEnd) {
  2203. return index;
  2204. }
  2205. }
  2206. var high = lineCount;
  2207. var low = -1;
  2208. while (high - low > 1) {
  2209. index = Math.floor((high + low) / 2);
  2210. lineStart = this._lineOffsets[index];
  2211. lineEnd = index + 1 < lineCount ? this._lineOffsets[index + 1] : charCount;
  2212. if (offset <= lineStart) {
  2213. high = index;
  2214. } else if (offset < lineEnd) {
  2215. high = index;
  2216. break;
  2217. } else {
  2218. low = index;
  2219. }
  2220. }
  2221. this._lastLineIndex = high;
  2222. return high;
  2223. },
  2224. /**
  2225. * Returns the number of lines in the model.
  2226. * <p>
  2227. * The model always has at least one line.
  2228. * </p>
  2229. *
  2230. * @returns {Number} the number of lines.
  2231. */
  2232. getLineCount: function() {
  2233. return this._lineOffsets.length;
  2234. },
  2235. /**
  2236. * Returns the line delimiter that is used by the view
  2237. * when inserting new lines. New lines entered using key strokes
  2238. * and paste operations use this line delimiter.
  2239. *
  2240. * @return {String} the line delimiter that is used by the view when inserting new lines.
  2241. */
  2242. getLineDelimiter: function() {
  2243. return this._lineDelimiter;
  2244. },
  2245. /**
  2246. * Returns the end character offset for the given line.
  2247. * <p>
  2248. * The end offset is not inclusive. This means that when the line delimiter is included, the
  2249. * offset is either the start offset of the next line or char count. When the line delimiter is
  2250. * not included, the offset is the offset of the line delimiter.
  2251. * </p>
  2252. * <p>
  2253. * The valid indices are 0 to line count exclusive. Returns <code>-1</code>
  2254. * if the index is out of range.
  2255. * </p>
  2256. *
  2257. * @param {Number} lineIndex the zero based index of the line.
  2258. * @param {Boolean} [includeDelimiter=false] whether or not to include the line delimiter.
  2259. * @return {Number} the line end offset or <code>-1</code> if out of range.
  2260. *
  2261. * @see #getLineStart
  2262. */
  2263. getLineEnd: function(lineIndex, includeDelimiter) {
  2264. var lineCount = this.getLineCount();
  2265. if (!(0 <= lineIndex && lineIndex < lineCount)) {
  2266. return -1;
  2267. }
  2268. if (lineIndex + 1 < lineCount) {
  2269. var end = this._lineOffsets[lineIndex + 1];
  2270. if (includeDelimiter) {
  2271. return end;
  2272. }
  2273. var text = this.getText(Math.max(this._lineOffsets[lineIndex], end - 2), end);
  2274. var i = text.length, c;
  2275. while (((c = text.charCodeAt(i - 1)) === 10) || (c === 13)) {
  2276. i--;
  2277. }
  2278. return end - (text.length - i);
  2279. } else {
  2280. return this.getCharCount();
  2281. }
  2282. },
  2283. /**
  2284. * Returns the start character offset for the given line.
  2285. * <p>
  2286. * The valid indices are 0 to line count exclusive. Returns <code>-1</code>
  2287. * if the index is out of range.
  2288. * </p>
  2289. *
  2290. * @param {Number} lineIndex the zero based index of the line.
  2291. * @return {Number} the line start offset or <code>-1</code> if out of range.
  2292. *
  2293. * @see #getLineEnd
  2294. */
  2295. getLineStart: function(lineIndex) {
  2296. if (!(0 <= lineIndex && lineIndex < this.getLineCount())) {
  2297. return -1;
  2298. }
  2299. return this._lineOffsets[lineIndex];
  2300. },
  2301. /**
  2302. * Returns the text for the given range.
  2303. * <p>
  2304. * The end offset is not inclusive. This means that character at the end offset
  2305. * is not included in the returned text.
  2306. * </p>
  2307. *
  2308. * @param {Number} [start=0] the zero based start offset of text range.
  2309. * @param {Number} [end=char count] the zero based end offset of text range.
  2310. *
  2311. * @see #setText
  2312. */
  2313. getText: function(start, end) {
  2314. if (start === undefined) { start = 0; }
  2315. if (end === undefined) { end = this.getCharCount(); }
  2316. if (start === end) { return ""; }
  2317. var offset = 0, chunk = 0, length;
  2318. while (chunk<this._text.length) {
  2319. length = this._text[chunk].length;
  2320. if (start <= offset + length) { break; }
  2321. offset += length;
  2322. chunk++;
  2323. }
  2324. var firstOffset = offset;
  2325. var firstChunk = chunk;
  2326. while (chunk<this._text.length) {
  2327. length = this._text[chunk].length;
  2328. if (end <= offset + length) { break; }
  2329. offset += length;
  2330. chunk++;
  2331. }
  2332. var lastOffset = offset;
  2333. var lastChunk = chunk;
  2334. if (firstChunk === lastChunk) {
  2335. return this._text[firstChunk].substring(start - firstOffset, end - lastOffset);
  2336. }
  2337. var beforeText = this._text[firstChunk].substring(start - firstOffset);
  2338. var afterText = this._text[lastChunk].substring(0, end - lastOffset);
  2339. return beforeText + this._text.slice(firstChunk+1, lastChunk).join("") + afterText;
  2340. },
  2341. /**
  2342. * Notifies all listeners that the text is about to change.
  2343. * <p>
  2344. * This notification is intended to be used only by the view. Application clients should
  2345. * use {@link orion.textview.TextView#event:onModelChanging}.
  2346. * </p>
  2347. * <p>
  2348. * NOTE: This method is not meant to called directly by application code. It is called internally by the TextModel
  2349. * as part of the implementation of {@link #setText}. This method is included in the public API for documentation
  2350. * purposes and to allow integration with other toolkit frameworks.
  2351. * </p>
  2352. *
  2353. * @param {orion.textview.ModelChangingEvent} modelChangingEvent the changing event
  2354. */
  2355. onChanging: function(modelChangingEvent) {
  2356. return this.dispatchEvent(modelChangingEvent);
  2357. },
  2358. /**
  2359. * Notifies all listeners that the text has changed.
  2360. * <p>
  2361. * This notification is intended to be used only by the view. Application clients should
  2362. * use {@link orion.textview.TextView#event:onModelChanged}.
  2363. * </p>
  2364. * <p>
  2365. * NOTE: This method is not meant to called directly by application code. It is called internally by the TextModel
  2366. * as part of the implementation of {@link #setText}. This method is included in the public API for documentation
  2367. * purposes and to allow integration with other toolkit frameworks.
  2368. * </p>
  2369. *
  2370. * @param {orion.textview.ModelChangedEvent} modelChangedEvent the changed event
  2371. */
  2372. onChanged: function(modelChangedEvent) {
  2373. return this.dispatchEvent(modelChangedEvent);
  2374. },
  2375. /**
  2376. * Sets the line delimiter that is used by the view
  2377. * when new lines are inserted in the model due to key
  2378. * strokes and paste operations.
  2379. * <p>
  2380. * If lineDelimiter is "auto", the delimiter is computed to be
  2381. * the first delimiter found the in the current text. If lineDelimiter
  2382. * is undefined or if there are no delimiters in the current text, the
  2383. * platform delimiter is used.
  2384. * </p>
  2385. *
  2386. * @param {String} lineDelimiter the line delimiter that is used by the view when inserting new lines.
  2387. */
  2388. setLineDelimiter: function(lineDelimiter) {
  2389. if (lineDelimiter === "auto") {
  2390. lineDelimiter = undefined;
  2391. if (this.getLineCount() > 1) {
  2392. lineDelimiter = this.getText(this.getLineEnd(0), this.getLineEnd(0, true));
  2393. }
  2394. }
  2395. this._lineDelimiter = lineDelimiter ? lineDelimiter : (isWindows ? "\r\n" : "\n");
  2396. },
  2397. /**
  2398. * Replaces the text in the given range with the given text.
  2399. * <p>
  2400. * The end offset is not inclusive. This means that the character at the
  2401. * end offset is not replaced.
  2402. * </p>
  2403. * <p>
  2404. * The text model must notify the listeners before and after the
  2405. * the text is changed by calling {@link #onChanging} and {@link #onChanged}
  2406. * respectively.
  2407. * </p>
  2408. *
  2409. * @param {String} [text=""] the new text.
  2410. * @param {Number} [start=0] the zero based start offset of text range.
  2411. * @param {Number} [end=char count] the zero based end offset of text range.
  2412. *
  2413. * @see #getText
  2414. */
  2415. setText: function(text, start, end) {
  2416. if (text === undefined) { text = ""; }
  2417. if (start === undefined) { start = 0; }
  2418. if (end === undefined) { end = this.getCharCount(); }
  2419. if (start === end && text === "") { return; }
  2420. var startLine = this.getLineAtOffset(start);
  2421. var endLine = this.getLineAtOffset(end);
  2422. var eventStart = start;
  2423. var removedCharCount = end - start;
  2424. var removedLineCount = endLine - startLine;
  2425. var addedCharCount = text.length;
  2426. var addedLineCount = 0;
  2427. var lineCount = this.getLineCount();
  2428. var cr = 0, lf = 0, index = 0;
  2429. var newLineOffsets = [];
  2430. while (true) {
  2431. if (cr !== -1 && cr <= index) { cr = text.indexOf("\r", index); }
  2432. if (lf !== -1 && lf <= index) { lf = text.indexOf("\n", index); }
  2433. if (lf === -1 && cr === -1) { break; }
  2434. if (cr !== -1 && lf !== -1) {
  2435. if (cr + 1 === lf) {
  2436. index = lf + 1;
  2437. } else {
  2438. index = (cr < lf ? cr : lf) + 1;
  2439. }
  2440. } else if (cr !== -1) {
  2441. index = cr + 1;
  2442. } else {
  2443. index = lf + 1;
  2444. }
  2445. newLineOffsets.push(start + index);
  2446. addedLineCount++;
  2447. }
  2448. var modelChangingEvent = {
  2449. type: "Changing",
  2450. text: text,
  2451. start: eventStart,
  2452. removedCharCount: removedCharCount,
  2453. addedCharCount: addedCharCount,
  2454. removedLineCount: removedLineCount,
  2455. addedLineCount: addedLineCount
  2456. };
  2457. this.onChanging(modelChangingEvent);
  2458. //TODO this should be done the loops below to avoid getText()
  2459. if (newLineOffsets.length === 0) {
  2460. var startLineOffset = this.getLineStart(startLine), endLineOffset;
  2461. if (endLine + 1 < lineCount) {
  2462. endLineOffset = this.getLineStart(endLine + 1);
  2463. } else {
  2464. endLineOffset = this.getCharCount();
  2465. }
  2466. if (start !== startLineOffset) {
  2467. text = this.getText(startLineOffset, start) + text;
  2468. start = startLineOffset;
  2469. }
  2470. if (end !== endLineOffset) {
  2471. text = text + this.getText(end, endLineOffset);
  2472. end = endLineOffset;
  2473. }
  2474. }
  2475. var changeCount = addedCharCount - removedCharCount;
  2476. for (var j = startLine + removedLineCount + 1; j < lineCount; j++) {
  2477. this._lineOffsets[j] += changeCount;
  2478. }
  2479. var args = [startLine + 1, removedLineCount].concat(newLineOffsets);
  2480. Array.prototype.splice.apply(this._lineOffsets, args);
  2481. var offset = 0, chunk = 0, length;
  2482. while (chunk<this._text.length) {
  2483. length = this._text[chunk].length;
  2484. if (start <= offset + length) { break; }
  2485. offset += length;
  2486. chunk++;
  2487. }
  2488. var firstOffset = offset;
  2489. var firstChunk = chunk;
  2490. while (chunk<this._text.length) {
  2491. length = this._text[chunk].length;
  2492. if (end <= offset + length) { break; }
  2493. offset += length;
  2494. chunk++;
  2495. }
  2496. var lastOffset = offset;
  2497. var lastChunk = chunk;
  2498. var firstText = this._text[firstChunk];
  2499. var lastText = this._text[lastChunk];
  2500. var beforeText = firstText.substring(0, start - firstOffset);
  2501. var afterText = lastText.substring(end - lastOffset);
  2502. var params = [firstChunk, lastChunk - firstChunk + 1];
  2503. if (beforeText) { params.push(beforeText); }
  2504. if (text) { params.push(text); }
  2505. if (afterText) { params.push(afterText); }
  2506. Array.prototype.splice.apply(this._text, params);
  2507. if (this._text.length === 0) { this._text = [""]; }
  2508. var modelChangedEvent = {
  2509. type: "Changed",
  2510. start: eventStart,
  2511. removedCharCount: removedCharCount,
  2512. addedCharCount: addedCharCount,
  2513. removedLineCount: removedLineCount,
  2514. addedLineCount: addedLineCount
  2515. };
  2516. this.onChanged(modelChangedEvent);
  2517. }
  2518. };
  2519. mEventTarget.EventTarget.addMixin(TextModel.prototype);
  2520. return {TextModel: TextModel};
  2521. });/*******************************************************************************
  2522. * @license
  2523. * Copyright (c) 2010, 2011 IBM Corporation and others.
  2524. * All rights reserved. This program and the accompanying materials are made
  2525. * available under the terms of the Eclipse Public License v1.0
  2526. * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
  2527. * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
  2528. *
  2529. * Contributors:
  2530. * Felipe Heidrich (IBM Corporation) - initial API and implementation
  2531. * Silenio Quarti (IBM Corporation) - initial API and implementation
  2532. ******************************************************************************/
  2533. /*global define */
  2534. define("orion/textview/projectionTextModel", ['orion/textview/textModel', 'orion/textview/eventTarget'], function(mTextModel, mEventTarget) {
  2535. /**
  2536. * @class This object represents a projection range. A projection specifies a
  2537. * range of text and the replacement text. The range of text is relative to the
  2538. * base text model associated to a projection model.
  2539. * <p>
  2540. * <b>See:</b><br/>
  2541. * {@link orion.textview.ProjectionTextModel}<br/>
  2542. * {@link orion.textview.ProjectionTextModel#addProjection}<br/>
  2543. * </p>
  2544. * @name orion.textview.Projection
  2545. *
  2546. * @property {Number} start The start offset of the projection range.
  2547. * @property {Number} end The end offset of the projection range. This offset is exclusive.
  2548. * @property {String|orion.textview.TextModel} [text=""] The projection text to be inserted
  2549. */
  2550. /**
  2551. * Constructs a new <code>ProjectionTextModel</code> based on the specified <code>TextModel</code>.
  2552. *
  2553. * @param {orion.textview.TextModel} baseModel The base text model.
  2554. *
  2555. * @name orion.textview.ProjectionTextModel
  2556. * @class The <code>ProjectionTextModel</code> represents a projection of its base text
  2557. * model. Projection ranges can be added to the projection text model to hide and/or insert
  2558. * ranges to the base text model.
  2559. * <p>
  2560. * The contents of the projection text model is modified when changes occur in the base model,
  2561. * projection model or by calls to {@link #addProjection} and {@link #removeProjection}.
  2562. * </p>
  2563. * <p>
  2564. * <b>See:</b><br/>
  2565. * {@link orion.textview.TextView}<br/>
  2566. * {@link orion.textview.TextModel}
  2567. * {@link orion.textview.TextView#setModel}
  2568. * </p>
  2569. * @borrows orion.textview.EventTarget#addEventListener as #addEventListener
  2570. * @borrows orion.textview.EventTarget#removeEventListener as #removeEventListener
  2571. * @borrows orion.textview.EventTarget#dispatchEvent as #dispatchEvent
  2572. */
  2573. function ProjectionTextModel(baseModel) {
  2574. this._model = baseModel; /* Base Model */
  2575. this._projections = [];
  2576. }
  2577. ProjectionTextModel.prototype = /** @lends orion.textview.ProjectionTextModel.prototype */ {
  2578. /**
  2579. * Adds a projection range to the model.
  2580. * <p>
  2581. * The model must notify the listeners before and after the the text is
  2582. * changed by calling {@link #onChanging} and {@link #onChanged} respectively.
  2583. * </p>
  2584. * @param {orion.textview.Projection} projection The projection range to be added.
  2585. *
  2586. * @see #removeProjection
  2587. */
  2588. addProjection: function(projection) {
  2589. if (!projection) {return;}
  2590. //start and end can't overlap any exist projection
  2591. var model = this._model, projections = this._projections;
  2592. projection._lineIndex = model.getLineAtOffset(projection.start);
  2593. projection._lineCount = model.getLineAtOffset(projection.end) - projection._lineIndex;
  2594. var text = projection.text;
  2595. if (!text) { text = ""; }
  2596. if (typeof text === "string") {
  2597. projection._model = new mTextModel.TextModel(text, model.getLineDelimiter());
  2598. } else {
  2599. projection._model = text;
  2600. }
  2601. var eventStart = this.mapOffset(projection.start, true);
  2602. var removedCharCount = projection.end - projection.start;
  2603. var removedLineCount = projection._lineCount;
  2604. var addedCharCount = projection._model.getCharCount();
  2605. var addedLineCount = projection._model.getLineCount() - 1;
  2606. var modelChangingEvent = {
  2607. type: "Changing",
  2608. text: projection._model.getText(),
  2609. start: eventStart,
  2610. removedCharCount: removedCharCount,
  2611. addedCharCount: addedCharCount,
  2612. removedLineCount: removedLineCount,
  2613. addedLineCount: addedLineCount
  2614. };
  2615. this.onChanging(modelChangingEvent);
  2616. var index = this._binarySearch(projections, projection.start);
  2617. projections.splice(index, 0, projection);
  2618. var modelChangedEvent = {
  2619. type: "Changed",
  2620. start: eventStart,
  2621. removedCharCount: removedCharCount,
  2622. addedCharCount: addedCharCount,
  2623. removedLineCount: removedLineCount,
  2624. addedLineCount: addedLineCount
  2625. };
  2626. this.onChanged(modelChangedEvent);
  2627. },
  2628. /**
  2629. * Returns all projection ranges of this model.
  2630. *
  2631. * @return {orion.textview.Projection[]} The projection ranges.
  2632. *
  2633. * @see #addProjection
  2634. */
  2635. getProjections: function() {
  2636. return this._projections.slice(0);
  2637. },
  2638. /**
  2639. * Gets the base text model.
  2640. *
  2641. * @return {orion.textview.TextModel} The base text model.
  2642. */
  2643. getBaseModel: function() {
  2644. return this._model;
  2645. },
  2646. /**
  2647. * Maps offsets between the projection model and its base model.
  2648. *
  2649. * @param {Number} offset The offset to be mapped.
  2650. * @param {Boolean} [baseOffset=false] <code>true</code> if <code>offset</code> is in base model and
  2651. * should be mapped to the projection model.
  2652. * @return {Number} The mapped offset
  2653. */
  2654. mapOffset: function(offset, baseOffset) {
  2655. var projections = this._projections, delta = 0, i, projection;
  2656. if (baseOffset) {
  2657. for (i = 0; i < projections.length; i++) {
  2658. projection = projections[i];
  2659. if (projection.start > offset) { break; }
  2660. if (projection.end > offset) { return -1; }
  2661. delta += projection._model.getCharCount() - (projection.end - projection.start);
  2662. }
  2663. return offset + delta;
  2664. }
  2665. for (i = 0; i < projections.length; i++) {
  2666. projection = projections[i];
  2667. if (projection.start > offset - delta) { break; }
  2668. var charCount = projection._model.getCharCount();
  2669. if (projection.start + charCount > offset - delta) {
  2670. return -1;
  2671. }
  2672. delta += charCount - (projection.end - projection.start);
  2673. }
  2674. return offset - delta;
  2675. },
  2676. /**
  2677. * Removes a projection range from the model.
  2678. * <p>
  2679. * The model must notify the listeners before and after the the text is
  2680. * changed by calling {@link #onChanging} and {@link #onChanged} respectively.
  2681. * </p>
  2682. *
  2683. * @param {orion.textview.Projection} projection The projection range to be removed.
  2684. *
  2685. * @see #addProjection
  2686. */
  2687. removeProjection: function(projection) {
  2688. //TODO remove listeners from model
  2689. var i, delta = 0;
  2690. for (i = 0; i < this._projections.length; i++) {
  2691. var p = this._projections[i];
  2692. if (p === projection) {
  2693. projection = p;
  2694. break;
  2695. }
  2696. delta += p._model.getCharCount() - (p.end - p.start);
  2697. }
  2698. if (i < this._projections.length) {
  2699. var model = this._model;
  2700. var eventStart = projection.start + delta;
  2701. var addedCharCount = projection.end - projection.start;
  2702. var addedLineCount = projection._lineCount;
  2703. var removedCharCount = projection._model.getCharCount();
  2704. var removedLineCount = projection._model.getLineCount() - 1;
  2705. var modelChangingEvent = {
  2706. type: "Changing",
  2707. text: model.getText(projection.start, projection.end),
  2708. start: eventStart,
  2709. removedCharCount: removedCharCount,
  2710. addedCharCount: addedCharCount,
  2711. removedLineCount: removedLineCount,
  2712. addedLineCount: addedLineCount
  2713. };
  2714. this.onChanging(modelChangingEvent);
  2715. this._projections.splice(i, 1);
  2716. var modelChangedEvent = {
  2717. type: "Changed",
  2718. start: eventStart,
  2719. removedCharCount: removedCharCount,
  2720. addedCharCount: addedCharCount,
  2721. removedLineCount: removedLineCount,
  2722. addedLineCount: addedLineCount
  2723. };
  2724. this.onChanged(modelChangedEvent);
  2725. }
  2726. },
  2727. /** @ignore */
  2728. _binarySearch: function (array, offset) {
  2729. var high = array.length, low = -1, index;
  2730. while (high - low > 1) {
  2731. index = Math.floor((high + low) / 2);
  2732. if (offset <= array[index].start) {
  2733. high = index;
  2734. } else {
  2735. low = index;
  2736. }
  2737. }
  2738. return high;
  2739. },
  2740. /**
  2741. * @see orion.textview.TextModel#getCharCount
  2742. */
  2743. getCharCount: function() {
  2744. var count = this._model.getCharCount(), projections = this._projections;
  2745. for (var i = 0; i < projections.length; i++) {
  2746. var projection = projections[i];
  2747. count += projection._model.getCharCount() - (projection.end - projection.start);
  2748. }
  2749. return count;
  2750. },
  2751. /**
  2752. * @see orion.textview.TextModel#getLine
  2753. */
  2754. getLine: function(lineIndex, includeDelimiter) {
  2755. if (lineIndex < 0) { return null; }
  2756. var model = this._model, projections = this._projections;
  2757. var delta = 0, result = [], offset = 0, i, lineCount, projection;
  2758. for (i = 0; i < projections.length; i++) {
  2759. projection = projections[i];
  2760. if (projection._lineIndex >= lineIndex - delta) { break; }
  2761. lineCount = projection._model.getLineCount() - 1;
  2762. if (projection._lineIndex + lineCount >= lineIndex - delta) {
  2763. var projectionLineIndex = lineIndex - (projection._lineIndex + delta);
  2764. if (projectionLineIndex < lineCount) {
  2765. return projection._model.getLine(projectionLineIndex, includeDelimiter);
  2766. } else {
  2767. result.push(projection._model.getLine(lineCount));
  2768. }
  2769. }
  2770. offset = projection.end;
  2771. delta += lineCount - projection._lineCount;
  2772. }
  2773. offset = Math.max(offset, model.getLineStart(lineIndex - delta));
  2774. for (; i < projections.length; i++) {
  2775. projection = projections[i];
  2776. if (projection._lineIndex > lineIndex - delta) { break; }
  2777. result.push(model.getText(offset, projection.start));
  2778. lineCount = projection._model.getLineCount() - 1;
  2779. if (projection._lineIndex + lineCount > lineIndex - delta) {
  2780. result.push(projection._model.getLine(0, includeDelimiter));
  2781. return result.join("");
  2782. }
  2783. result.push(projection._model.getText());
  2784. offset = projection.end;
  2785. delta += lineCount - projection._lineCount;
  2786. }
  2787. var end = model.getLineEnd(lineIndex - delta, includeDelimiter);
  2788. if (offset < end) {
  2789. result.push(model.getText(offset, end));
  2790. }
  2791. return result.join("");
  2792. },
  2793. /**
  2794. * @see orion.textview.TextModel#getLineAtOffset
  2795. */
  2796. getLineAtOffset: function(offset) {
  2797. var model = this._model, projections = this._projections;
  2798. var delta = 0, lineDelta = 0;
  2799. for (var i = 0; i < projections.length; i++) {
  2800. var projection = projections[i];
  2801. if (projection.start > offset - delta) { break; }
  2802. var charCount = projection._model.getCharCount();
  2803. if (projection.start + charCount > offset - delta) {
  2804. var projectionOffset = offset - (projection.start + delta);
  2805. lineDelta += projection._model.getLineAtOffset(projectionOffset);
  2806. delta += projectionOffset;
  2807. break;
  2808. }
  2809. lineDelta += projection._model.getLineCount() - 1 - projection._lineCount;
  2810. delta += charCount - (projection.end - projection.start);
  2811. }
  2812. return model.getLineAtOffset(offset - delta) + lineDelta;
  2813. },
  2814. /**
  2815. * @see orion.textview.TextModel#getLineCount
  2816. */
  2817. getLineCount: function() {
  2818. var model = this._model, projections = this._projections;
  2819. var count = model.getLineCount();
  2820. for (var i = 0; i < projections.length; i++) {
  2821. var projection = projections[i];
  2822. count += projection._model.getLineCount() - 1 - projection._lineCount;
  2823. }
  2824. return count;
  2825. },
  2826. /**
  2827. * @see orion.textview.TextModel#getLineDelimiter
  2828. */
  2829. getLineDelimiter: function() {
  2830. return this._model.getLineDelimiter();
  2831. },
  2832. /**
  2833. * @see orion.textview.TextModel#getLineEnd
  2834. */
  2835. getLineEnd: function(lineIndex, includeDelimiter) {
  2836. if (lineIndex < 0) { return -1; }
  2837. var model = this._model, projections = this._projections;
  2838. var delta = 0, offsetDelta = 0;
  2839. for (var i = 0; i < projections.length; i++) {
  2840. var projection = projections[i];
  2841. if (projection._lineIndex > lineIndex - delta) { break; }
  2842. var lineCount = projection._model.getLineCount() - 1;
  2843. if (projection._lineIndex + lineCount > lineIndex - delta) {
  2844. var projectionLineIndex = lineIndex - (projection._lineIndex + delta);
  2845. return projection._model.getLineEnd (projectionLineIndex, includeDelimiter) + projection.start + offsetDelta;
  2846. }
  2847. offsetDelta += projection._model.getCharCount() - (projection.end - projection.start);
  2848. delta += lineCount - projection._lineCount;
  2849. }
  2850. return model.getLineEnd(lineIndex - delta, includeDelimiter) + offsetDelta;
  2851. },
  2852. /**
  2853. * @see orion.textview.TextModel#getLineStart
  2854. */
  2855. getLineStart: function(lineIndex) {
  2856. if (lineIndex < 0) { return -1; }
  2857. var model = this._model, projections = this._projections;
  2858. var delta = 0, offsetDelta = 0;
  2859. for (var i = 0; i < projections.length; i++) {
  2860. var projection = projections[i];
  2861. if (projection._lineIndex >= lineIndex - delta) { break; }
  2862. var lineCount = projection._model.getLineCount() - 1;
  2863. if (projection._lineIndex + lineCount >= lineIndex - delta) {
  2864. var projectionLineIndex = lineIndex - (projection._lineIndex + delta);
  2865. return projection._model.getLineStart (projectionLineIndex) + projection.start + offsetDelta;
  2866. }
  2867. offsetDelta += projection._model.getCharCount() - (projection.end - projection.start);
  2868. delta += lineCount - projection._lineCount;
  2869. }
  2870. return model.getLineStart(lineIndex - delta) + offsetDelta;
  2871. },
  2872. /**
  2873. * @see orion.textview.TextModel#getText
  2874. */
  2875. getText: function(start, end) {
  2876. if (start === undefined) { start = 0; }
  2877. var model = this._model, projections = this._projections;
  2878. var delta = 0, result = [], i, projection, charCount;
  2879. for (i = 0; i < projections.length; i++) {
  2880. projection = projections[i];
  2881. if (projection.start > start - delta) { break; }
  2882. charCount = projection._model.getCharCount();
  2883. if (projection.start + charCount > start - delta) {
  2884. if (end !== undefined && projection.start + charCount > end - delta) {
  2885. return projection._model.getText(start - (projection.start + delta), end - (projection.start + delta));
  2886. } else {
  2887. result.push(projection._model.getText(start - (projection.start + delta)));
  2888. start = projection.end + delta + charCount - (projection.end - projection.start);
  2889. }
  2890. }
  2891. delta += charCount - (projection.end - projection.start);
  2892. }
  2893. var offset = start - delta;
  2894. if (end !== undefined) {
  2895. for (; i < projections.length; i++) {
  2896. projection = projections[i];
  2897. if (projection.start > end - delta) { break; }
  2898. result.push(model.getText(offset, projection.start));
  2899. charCount = projection._model.getCharCount();
  2900. if (projection.start + charCount > end - delta) {
  2901. result.push(projection._model.getText(0, end - (projection.start + delta)));
  2902. return result.join("");
  2903. }
  2904. result.push(projection._model.getText());
  2905. offset = projection.end;
  2906. delta += charCount - (projection.end - projection.start);
  2907. }
  2908. result.push(model.getText(offset, end - delta));
  2909. } else {
  2910. for (; i < projections.length; i++) {
  2911. projection = projections[i];
  2912. result.push(model.getText(offset, projection.start));
  2913. result.push(projection._model.getText());
  2914. offset = projection.end;
  2915. }
  2916. result.push(model.getText(offset));
  2917. }
  2918. return result.join("");
  2919. },
  2920. /** @ignore */
  2921. _onChanging: function(text, start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) {
  2922. var model = this._model, projections = this._projections, i, projection, delta = 0, lineDelta;
  2923. var end = start + removedCharCount;
  2924. for (; i < projections.length; i++) {
  2925. projection = projections[i];
  2926. if (projection.start > start) { break; }
  2927. delta += projection._model.getCharCount() - (projection.end - projection.start);
  2928. }
  2929. /*TODO add stuff saved by setText*/
  2930. var mapStart = start + delta, rangeStart = i;
  2931. for (; i < projections.length; i++) {
  2932. projection = projections[i];
  2933. if (projection.start > end) { break; }
  2934. delta += projection._model.getCharCount() - (projection.end - projection.start);
  2935. lineDelta += projection._model.getLineCount() - 1 - projection._lineCount;
  2936. }
  2937. /*TODO add stuff saved by setText*/
  2938. var mapEnd = end + delta, rangeEnd = i;
  2939. this.onChanging(mapStart, mapEnd - mapStart, addedCharCount/*TODO add stuff saved by setText*/, removedLineCount + lineDelta/*TODO add stuff saved by setText*/, addedLineCount/*TODO add stuff saved by setText*/);
  2940. projections.splice(projections, rangeEnd - rangeStart);
  2941. var count = text.length - (mapEnd - mapStart);
  2942. for (; i < projections.length; i++) {
  2943. projection = projections[i];
  2944. projection.start += count;
  2945. projection.end += count;
  2946. projection._lineIndex = model.getLineAtOffset(projection.start);
  2947. }
  2948. },
  2949. /**
  2950. * @see orion.textview.TextModel#onChanging
  2951. */
  2952. onChanging: function(modelChangingEvent) {
  2953. return this.dispatchEvent(modelChangingEvent);
  2954. },
  2955. /**
  2956. * @see orion.textview.TextModel#onChanged
  2957. */
  2958. onChanged: function(modelChangedEvent) {
  2959. return this.dispatchEvent(modelChangedEvent);
  2960. },
  2961. /**
  2962. * @see orion.textview.TextModel#setLineDelimiter
  2963. */
  2964. setLineDelimiter: function(lineDelimiter) {
  2965. this._model.setLineDelimiter(lineDelimiter);
  2966. },
  2967. /**
  2968. * @see orion.textview.TextModel#setText
  2969. */
  2970. setText: function(text, start, end) {
  2971. if (text === undefined) { text = ""; }
  2972. if (start === undefined) { start = 0; }
  2973. var eventStart = start, eventEnd = end;
  2974. var model = this._model, projections = this._projections;
  2975. var delta = 0, lineDelta = 0, i, projection, charCount, startProjection, endProjection, startLineDelta = 0;
  2976. for (i = 0; i < projections.length; i++) {
  2977. projection = projections[i];
  2978. if (projection.start > start - delta) { break; }
  2979. charCount = projection._model.getCharCount();
  2980. if (projection.start + charCount > start - delta) {
  2981. if (end !== undefined && projection.start + charCount > end - delta) {
  2982. projection._model.setText(text, start - (projection.start + delta), end - (projection.start + delta));
  2983. //TODO events - special case
  2984. return;
  2985. } else {
  2986. startLineDelta = projection._model.getLineCount() - 1 - projection._model.getLineAtOffset(start - (projection.start + delta));
  2987. startProjection = {
  2988. projection: projection,
  2989. start: start - (projection.start + delta)
  2990. };
  2991. start = projection.end + delta + charCount - (projection.end - projection.start);
  2992. }
  2993. }
  2994. lineDelta += projection._model.getLineCount() - 1 - projection._lineCount;
  2995. delta += charCount - (projection.end - projection.start);
  2996. }
  2997. var mapStart = start - delta, rangeStart = i, startLine = model.getLineAtOffset(mapStart) + lineDelta - startLineDelta;
  2998. if (end !== undefined) {
  2999. for (; i < projections.length; i++) {
  3000. projection = projections[i];
  3001. if (projection.start > end - delta) { break; }
  3002. charCount = projection._model.getCharCount();
  3003. if (projection.start + charCount > end - delta) {
  3004. lineDelta += projection._model.getLineAtOffset(end - (projection.start + delta));
  3005. charCount = end - (projection.start + delta);
  3006. end = projection.end + delta;
  3007. endProjection = {
  3008. projection: projection,
  3009. end: charCount
  3010. };
  3011. break;
  3012. }
  3013. lineDelta += projection._model.getLineCount() - 1 - projection._lineCount;
  3014. delta += charCount - (projection.end - projection.start);
  3015. }
  3016. } else {
  3017. for (; i < projections.length; i++) {
  3018. projection = projections[i];
  3019. lineDelta += projection._model.getLineCount() - 1 - projection._lineCount;
  3020. delta += projection._model.getCharCount() - (projection.end - projection.start);
  3021. }
  3022. end = eventEnd = model.getCharCount() + delta;
  3023. }
  3024. var mapEnd = end - delta, rangeEnd = i, endLine = model.getLineAtOffset(mapEnd) + lineDelta;
  3025. //events
  3026. var removedCharCount = eventEnd - eventStart;
  3027. var removedLineCount = endLine - startLine;
  3028. var addedCharCount = text.length;
  3029. var addedLineCount = 0;
  3030. var cr = 0, lf = 0, index = 0;
  3031. while (true) {
  3032. if (cr !== -1 && cr <= index) { cr = text.indexOf("\r", index); }
  3033. if (lf !== -1 && lf <= index) { lf = text.indexOf("\n", index); }
  3034. if (lf === -1 && cr === -1) { break; }
  3035. if (cr !== -1 && lf !== -1) {
  3036. if (cr + 1 === lf) {
  3037. index = lf + 1;
  3038. } else {
  3039. index = (cr < lf ? cr : lf) + 1;
  3040. }
  3041. } else if (cr !== -1) {
  3042. index = cr + 1;
  3043. } else {
  3044. index = lf + 1;
  3045. }
  3046. addedLineCount++;
  3047. }
  3048. var modelChangingEvent = {
  3049. type: "Changing",
  3050. text: text,
  3051. start: eventStart,
  3052. removedCharCount: removedCharCount,
  3053. addedCharCount: addedCharCount,
  3054. removedLineCount: removedLineCount,
  3055. addedLineCount: addedLineCount
  3056. };
  3057. this.onChanging(modelChangingEvent);
  3058. // var changeLineCount = model.getLineAtOffset(mapEnd) - model.getLineAtOffset(mapStart) + addedLineCount;
  3059. model.setText(text, mapStart, mapEnd);
  3060. if (startProjection) {
  3061. projection = startProjection.projection;
  3062. projection._model.setText("", startProjection.start);
  3063. }
  3064. if (endProjection) {
  3065. projection = endProjection.projection;
  3066. projection._model.setText("", 0, endProjection.end);
  3067. projection.start = projection.end;
  3068. projection._lineCount = 0;
  3069. }
  3070. projections.splice(rangeStart, rangeEnd - rangeStart);
  3071. var changeCount = text.length - (mapEnd - mapStart);
  3072. for (i = rangeEnd; i < projections.length; i++) {
  3073. projection = projections[i];
  3074. projection.start += changeCount;
  3075. projection.end += changeCount;
  3076. // if (projection._lineIndex + changeLineCount !== model.getLineAtOffset(projection.start)) {
  3077. // log("here");
  3078. // }
  3079. projection._lineIndex = model.getLineAtOffset(projection.start);
  3080. // projection._lineIndex += changeLineCount;
  3081. }
  3082. var modelChangedEvent = {
  3083. type: "Changed",
  3084. start: eventStart,
  3085. removedCharCount: removedCharCount,
  3086. addedCharCount: addedCharCount,
  3087. removedLineCount: removedLineCount,
  3088. addedLineCount: addedLineCount
  3089. };
  3090. this.onChanged(modelChangedEvent);
  3091. }
  3092. };
  3093. mEventTarget.EventTarget.addMixin(ProjectionTextModel.prototype);
  3094. return {ProjectionTextModel: ProjectionTextModel};
  3095. });
  3096. /*******************************************************************************
  3097. * @license
  3098. * Copyright (c) 2010, 2011 IBM Corporation and others.
  3099. * All rights reserved. This program and the accompanying materials are made
  3100. * available under the terms of the Eclipse Public License v1.0
  3101. * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
  3102. * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
  3103. *
  3104. * Contributors: IBM Corporation - initial API and implementation
  3105. ******************************************************************************/
  3106. /*global define setTimeout clearTimeout setInterval clearInterval Node */
  3107. define("orion/textview/tooltip", ['orion/textview/textView', 'orion/textview/textModel', 'orion/textview/projectionTextModel'], function(mTextView, mTextModel, mProjectionTextModel) {
  3108. /** @private */
  3109. function Tooltip (view) {
  3110. this._view = view;
  3111. //TODO add API to get the parent of the view
  3112. this._create(view._parent.ownerDocument);
  3113. view.addEventListener("Destroy", this, this.destroy);
  3114. }
  3115. Tooltip.getTooltip = function(view) {
  3116. if (!view._tooltip) {
  3117. view._tooltip = new Tooltip(view);
  3118. }
  3119. return view._tooltip;
  3120. };
  3121. Tooltip.prototype = /** @lends orion.textview.Tooltip.prototype */ {
  3122. _create: function(document) {
  3123. if (this._domNode) { return; }
  3124. this._document = document;
  3125. var domNode = this._domNode = document.createElement("DIV");
  3126. domNode.className = "viewTooltip";
  3127. var viewParent = this._viewParent = document.createElement("DIV");
  3128. domNode.appendChild(viewParent);
  3129. var htmlParent = this._htmlParent = document.createElement("DIV");
  3130. domNode.appendChild(htmlParent);
  3131. document.body.appendChild(domNode);
  3132. this.hide();
  3133. },
  3134. destroy: function() {
  3135. if (!this._domNode) { return; }
  3136. if (this._contentsView) {
  3137. this._contentsView.destroy();
  3138. this._contentsView = null;
  3139. this._emptyModel = null;
  3140. }
  3141. var parent = this._domNode.parentNode;
  3142. if (parent) { parent.removeChild(this._domNode); }
  3143. this._domNode = null;
  3144. },
  3145. hide: function() {
  3146. if (this._contentsView) {
  3147. this._contentsView.setModel(this._emptyModel);
  3148. }
  3149. if (this._viewParent) {
  3150. this._viewParent.style.left = "-10000px";
  3151. this._viewParent.style.position = "fixed";
  3152. this._viewParent.style.visibility = "hidden";
  3153. }
  3154. if (this._htmlParent) {
  3155. this._htmlParent.style.left = "-10000px";
  3156. this._htmlParent.style.position = "fixed";
  3157. this._htmlParent.style.visibility = "hidden";
  3158. this._htmlParent.innerHTML = "";
  3159. }
  3160. if (this._domNode) {
  3161. this._domNode.style.visibility = "hidden";
  3162. }
  3163. if (this._showTimeout) {
  3164. clearTimeout(this._showTimeout);
  3165. this._showTimeout = null;
  3166. }
  3167. if (this._hideTimeout) {
  3168. clearTimeout(this._hideTimeout);
  3169. this._hideTimeout = null;
  3170. }
  3171. if (this._fadeTimeout) {
  3172. clearInterval(this._fadeTimeout);
  3173. this._fadeTimeout = null;
  3174. }
  3175. },
  3176. isVisible: function() {
  3177. return this._domNode && this._domNode.style.visibility === "visible";
  3178. },
  3179. setTarget: function(target) {
  3180. if (this.target === target) { return; }
  3181. this._target = target;
  3182. this.hide();
  3183. if (target) {
  3184. var self = this;
  3185. self._showTimeout = setTimeout(function() {
  3186. self.show(true);
  3187. }, 1000);
  3188. }
  3189. },
  3190. show: function(autoHide) {
  3191. if (!this._target) { return; }
  3192. var info = this._target.getTooltipInfo();
  3193. if (!info) { return; }
  3194. var domNode = this._domNode;
  3195. domNode.style.left = domNode.style.right = domNode.style.width = domNode.style.height = "auto";
  3196. var contents = info.contents, contentsDiv;
  3197. if (contents instanceof Array) {
  3198. contents = this._getAnnotationContents(contents);
  3199. }
  3200. if (typeof contents === "string") {
  3201. (contentsDiv = this._htmlParent).innerHTML = contents;
  3202. } else if (contents instanceof Node) {
  3203. (contentsDiv = this._htmlParent).appendChild(contents);
  3204. } else if (contents instanceof mProjectionTextModel.ProjectionTextModel) {
  3205. if (!this._contentsView) {
  3206. this._emptyModel = new mTextModel.TextModel("");
  3207. //TODO need hook into setup.js (or editor.js) to create a text view (and styler)
  3208. var newView = this._contentsView = new mTextView.TextView({
  3209. model: this._emptyModel,
  3210. parent: this._viewParent,
  3211. tabSize: 4,
  3212. sync: true,
  3213. stylesheet: ["/orion/textview/tooltip.css", "/orion/textview/rulers.css",
  3214. "/examples/textview/textstyler.css", "/css/default-theme.css"]
  3215. });
  3216. //TODO this is need to avoid IE from getting focus
  3217. newView._clientDiv.contentEditable = false;
  3218. //TODO need to find a better way of sharing the styler for multiple views
  3219. var view = this._view;
  3220. var listener = {
  3221. onLineStyle: function(e) {
  3222. view.onLineStyle(e);
  3223. }
  3224. };
  3225. newView.addEventListener("LineStyle", listener.onLineStyle);
  3226. }
  3227. var contentsView = this._contentsView;
  3228. contentsView.setModel(contents);
  3229. var size = contentsView.computeSize();
  3230. contentsDiv = this._viewParent;
  3231. //TODO always make the width larger than the size of the scrollbar to avoid bug in updatePage
  3232. contentsDiv.style.width = (size.width + 20) + "px";
  3233. contentsDiv.style.height = size.height + "px";
  3234. } else {
  3235. return;
  3236. }
  3237. contentsDiv.style.left = "auto";
  3238. contentsDiv.style.position = "static";
  3239. contentsDiv.style.visibility = "visible";
  3240. var left = parseInt(this._getNodeStyle(domNode, "padding-left", "0"), 10);
  3241. left += parseInt(this._getNodeStyle(domNode, "border-left-width", "0"), 10);
  3242. if (info.anchor === "right") {
  3243. var right = parseInt(this._getNodeStyle(domNode, "padding-right", "0"), 10);
  3244. right += parseInt(this._getNodeStyle(domNode, "border-right-width", "0"), 10);
  3245. domNode.style.right = (domNode.ownerDocument.body.getBoundingClientRect().right - info.x + left + right) + "px";
  3246. } else {
  3247. domNode.style.left = (info.x - left) + "px";
  3248. }
  3249. var top = parseInt(this._getNodeStyle(domNode, "padding-top", "0"), 10);
  3250. top += parseInt(this._getNodeStyle(domNode, "border-top-width", "0"), 10);
  3251. domNode.style.top = (info.y - top) + "px";
  3252. domNode.style.maxWidth = info.maxWidth + "px";
  3253. domNode.style.maxHeight = info.maxHeight + "px";
  3254. domNode.style.opacity = "1";
  3255. domNode.style.visibility = "visible";
  3256. if (autoHide) {
  3257. var self = this;
  3258. self._hideTimeout = setTimeout(function() {
  3259. var opacity = parseFloat(self._getNodeStyle(domNode, "opacity", "1"));
  3260. self._fadeTimeout = setInterval(function() {
  3261. if (domNode.style.visibility === "visible" && opacity > 0) {
  3262. opacity -= 0.1;
  3263. domNode.style.opacity = opacity;
  3264. return;
  3265. }
  3266. self.hide();
  3267. }, 50);
  3268. }, 5000);
  3269. }
  3270. },
  3271. _getAnnotationContents: function(annotations) {
  3272. if (annotations.length === 0) {
  3273. return null;
  3274. }
  3275. var model = this._view.getModel(), annotation;
  3276. var baseModel = model.getBaseModel ? model.getBaseModel() : model;
  3277. function getText(start, end) {
  3278. var textStart = baseModel.getLineStart(baseModel.getLineAtOffset(start));
  3279. var textEnd = baseModel.getLineEnd(baseModel.getLineAtOffset(end), true);
  3280. return baseModel.getText(textStart, textEnd);
  3281. }
  3282. var title;
  3283. if (annotations.length === 1) {
  3284. annotation = annotations[0];
  3285. if (annotation.title) {
  3286. title = annotation.title.replace(/</g, "&lt;").replace(/>/g, "&gt;");
  3287. return "<div>" + annotation.html + "&nbsp;<span style='vertical-align:middle;'>" + title + "</span><div>";
  3288. } else {
  3289. var newModel = new mProjectionTextModel.ProjectionTextModel(baseModel);
  3290. var lineStart = baseModel.getLineStart(baseModel.getLineAtOffset(annotation.start));
  3291. newModel.addProjection({start: annotation.end, end: newModel.getCharCount()});
  3292. newModel.addProjection({start: 0, end: lineStart});
  3293. return newModel;
  3294. }
  3295. } else {
  3296. var tooltipHTML = "<div><em>Multiple annotations:</em></div>";
  3297. for (var i = 0; i < annotations.length; i++) {
  3298. annotation = annotations[i];
  3299. title = annotation.title;
  3300. if (!title) {
  3301. title = getText(annotation.start, annotation.end);
  3302. }
  3303. title = title.replace(/</g, "&lt;").replace(/>/g, "&gt;");
  3304. tooltipHTML += "<div>" + annotation.html + "&nbsp;<span style='vertical-align:middle;'>" + title + "</span><div>";
  3305. }
  3306. return tooltipHTML;
  3307. }
  3308. },
  3309. _getNodeStyle: function(node, prop, defaultValue) {
  3310. var value;
  3311. if (node) {
  3312. value = node.style[prop];
  3313. if (!value) {
  3314. if (node.currentStyle) {
  3315. var index = 0, p = prop;
  3316. while ((index = p.indexOf("-", index)) !== -1) {
  3317. p = p.substring(0, index) + p.substring(index + 1, index + 2).toUpperCase() + p.substring(index + 2);
  3318. }
  3319. value = node.currentStyle[p];
  3320. } else {
  3321. var css = node.ownerDocument.defaultView.getComputedStyle(node, null);
  3322. value = css ? css.getPropertyValue(prop) : null;
  3323. }
  3324. }
  3325. }
  3326. return value || defaultValue;
  3327. }
  3328. };
  3329. return {Tooltip: Tooltip};
  3330. });
  3331. /*******************************************************************************
  3332. * @license
  3333. * Copyright (c) 2010, 2011 IBM Corporation and others.
  3334. * All rights reserved. This program and the accompanying materials are made
  3335. * available under the terms of the Eclipse Public License v1.0
  3336. * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
  3337. * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
  3338. *
  3339. * Contributors:
  3340. * Felipe Heidrich (IBM Corporation) - initial API and implementation
  3341. * Silenio Quarti (IBM Corporation) - initial API and implementation
  3342. * Mihai Sucan (Mozilla Foundation) - fix for Bug#334583 Bug#348471 Bug#349485 Bug#350595 Bug#360726 Bug#361180 Bug#362835 Bug#362428 Bug#362286 Bug#354270 Bug#361474 Bug#363945 Bug#366312 Bug#370584
  3343. ******************************************************************************/
  3344. /*global window document navigator setTimeout clearTimeout XMLHttpRequest define DOMException */
  3345. define("orion/textview/textView", ['orion/textview/textModel', 'orion/textview/keyBinding', 'orion/textview/eventTarget'], function(mTextModel, mKeyBinding, mEventTarget) {
  3346. /** @private */
  3347. function addHandler(node, type, handler, capture) {
  3348. if (typeof node.addEventListener === "function") {
  3349. node.addEventListener(type, handler, capture === true);
  3350. } else {
  3351. node.attachEvent("on" + type, handler);
  3352. }
  3353. }
  3354. /** @private */
  3355. function removeHandler(node, type, handler, capture) {
  3356. if (typeof node.removeEventListener === "function") {
  3357. node.removeEventListener(type, handler, capture === true);
  3358. } else {
  3359. node.detachEvent("on" + type, handler);
  3360. }
  3361. }
  3362. var userAgent = navigator.userAgent;
  3363. var isIE;
  3364. if (document.selection && window.ActiveXObject && /MSIE/.test(userAgent)) {
  3365. isIE = document.documentMode ? document.documentMode : 7;
  3366. }
  3367. var isFirefox = parseFloat(userAgent.split("Firefox/")[1] || userAgent.split("Minefield/")[1]) || undefined;
  3368. var isOpera = userAgent.indexOf("Opera") !== -1;
  3369. var isChrome = userAgent.indexOf("Chrome") !== -1;
  3370. var isSafari = userAgent.indexOf("Safari") !== -1 && !isChrome;
  3371. var isWebkit = userAgent.indexOf("WebKit") !== -1;
  3372. var isPad = userAgent.indexOf("iPad") !== -1;
  3373. var isMac = navigator.platform.indexOf("Mac") !== -1;
  3374. var isWindows = navigator.platform.indexOf("Win") !== -1;
  3375. var isLinux = navigator.platform.indexOf("Linux") !== -1;
  3376. var isW3CEvents = typeof window.document.documentElement.addEventListener === "function";
  3377. var isRangeRects = (!isIE || isIE >= 9) && typeof window.document.createRange().getBoundingClientRect === "function";
  3378. var platformDelimiter = isWindows ? "\r\n" : "\n";
  3379. /**
  3380. * Constructs a new Selection object.
  3381. *
  3382. * @class A Selection represents a range of selected text in the view.
  3383. * @name orion.textview.Selection
  3384. */
  3385. function Selection (start, end, caret) {
  3386. /**
  3387. * The selection start offset.
  3388. *
  3389. * @name orion.textview.Selection#start
  3390. */
  3391. this.start = start;
  3392. /**
  3393. * The selection end offset.
  3394. *
  3395. * @name orion.textview.Selection#end
  3396. */
  3397. this.end = end;
  3398. /** @private */
  3399. this.caret = caret; //true if the start, false if the caret is at end
  3400. }
  3401. Selection.prototype = /** @lends orion.textview.Selection.prototype */ {
  3402. /** @private */
  3403. clone: function() {
  3404. return new Selection(this.start, this.end, this.caret);
  3405. },
  3406. /** @private */
  3407. collapse: function() {
  3408. if (this.caret) {
  3409. this.end = this.start;
  3410. } else {
  3411. this.start = this.end;
  3412. }
  3413. },
  3414. /** @private */
  3415. extend: function (offset) {
  3416. if (this.caret) {
  3417. this.start = offset;
  3418. } else {
  3419. this.end = offset;
  3420. }
  3421. if (this.start > this.end) {
  3422. var tmp = this.start;
  3423. this.start = this.end;
  3424. this.end = tmp;
  3425. this.caret = !this.caret;
  3426. }
  3427. },
  3428. /** @private */
  3429. setCaret: function(offset) {
  3430. this.start = offset;
  3431. this.end = offset;
  3432. this.caret = false;
  3433. },
  3434. /** @private */
  3435. getCaret: function() {
  3436. return this.caret ? this.start : this.end;
  3437. },
  3438. /** @private */
  3439. toString: function() {
  3440. return "start=" + this.start + " end=" + this.end + (this.caret ? " caret is at start" : " caret is at end");
  3441. },
  3442. /** @private */
  3443. isEmpty: function() {
  3444. return this.start === this.end;
  3445. },
  3446. /** @private */
  3447. equals: function(object) {
  3448. return this.caret === object.caret && this.start === object.start && this.end === object.end;
  3449. }
  3450. };
  3451. /**
  3452. * @class This object describes the options for the text view.
  3453. * <p>
  3454. * <b>See:</b><br/>
  3455. * {@link orion.textview.TextView}<br/>
  3456. * {@link orion.textview.TextView#setOptions}
  3457. * {@link orion.textview.TextView#getOptions}
  3458. * </p>
  3459. * @name orion.textview.TextViewOptions
  3460. *
  3461. * @property {String|DOMElement} parent the parent element for the view, it can be either a DOM element or an ID for a DOM element.
  3462. * @property {orion.textview.TextModel} [model] the text model for the view. If it is not set the view creates an empty {@link orion.textview.TextModel}.
  3463. * @property {Boolean} [readonly=false] whether or not the view is read-only.
  3464. * @property {Boolean} [fullSelection=true] whether or not the view is in full selection mode.
  3465. * @property {Boolean} [sync=false] whether or not the view creation should be synchronous (if possible).
  3466. * @property {Boolean} [expandTab=false] whether or not the tab key inserts white spaces.
  3467. * @property {String|String[]} [stylesheet] one or more stylesheet for the view. Each stylesheet can be either a URI or a string containing the CSS rules.
  3468. * @property {String} [themeClass] the CSS class for the view theming.
  3469. * @property {Number} [tabSize] The number of spaces in a tab.
  3470. */
  3471. /**
  3472. * Constructs a new text view.
  3473. *
  3474. * @param {orion.textview.TextViewOptions} options the view options.
  3475. *
  3476. * @class A TextView is a user interface for editing text.
  3477. * @name orion.textview.TextView
  3478. * @borrows orion.textview.EventTarget#addEventListener as #addEventListener
  3479. * @borrows orion.textview.EventTarget#removeEventListener as #removeEventListener
  3480. * @borrows orion.textview.EventTarget#dispatchEvent as #dispatchEvent
  3481. */
  3482. function TextView (options) {
  3483. this._init(options);
  3484. }
  3485. TextView.prototype = /** @lends orion.textview.TextView.prototype */ {
  3486. /**
  3487. * Adds a ruler to the text view.
  3488. *
  3489. * @param {orion.textview.Ruler} ruler the ruler.
  3490. */
  3491. addRuler: function (ruler) {
  3492. this._rulers.push(ruler);
  3493. ruler.setView(this);
  3494. this._createRuler(ruler);
  3495. this._updatePage();
  3496. },
  3497. computeSize: function() {
  3498. var w = 0, h = 0;
  3499. var model = this._model, clientDiv = this._clientDiv;
  3500. if (!clientDiv) { return {width: w, height: h}; }
  3501. var clientWidth = clientDiv.style.width;
  3502. /*
  3503. * Feature in WekKit. Webkit limits the width of the lines
  3504. * computed below to the width of the client div. This causes
  3505. * the lines to be wrapped even though "pre" is set. The fix
  3506. * is to set the width of the client div to a larger number
  3507. * before computing the lines width. Note that this value is
  3508. * reset to the appropriate value further down.
  3509. */
  3510. if (isWebkit) {
  3511. clientDiv.style.width = (0x7FFFF).toString() + "px";
  3512. }
  3513. var lineCount = model.getLineCount();
  3514. var document = this._frameDocument;
  3515. for (var lineIndex=0; lineIndex<lineCount; lineIndex++) {
  3516. var child = this._getLineNode(lineIndex), dummy = null;
  3517. if (!child || child.lineChanged || child.lineRemoved) {
  3518. child = dummy = this._createLine(clientDiv, null, document, lineIndex, model);
  3519. }
  3520. var rect = this._getLineBoundingClientRect(child);
  3521. w = Math.max(w, rect.right - rect.left);
  3522. h += rect.bottom - rect.top;
  3523. if (dummy) { clientDiv.removeChild(dummy); }
  3524. }
  3525. if (isWebkit) {
  3526. clientDiv.style.width = clientWidth;
  3527. }
  3528. var viewPadding = this._getViewPadding();
  3529. w += viewPadding.right - viewPadding.left;
  3530. h += viewPadding.bottom - viewPadding.top;
  3531. return {width: w, height: h};
  3532. },
  3533. /**
  3534. * Converts the given rectangle from one coordinate spaces to another.
  3535. * <p>The supported coordinate spaces are:
  3536. * <ul>
  3537. * <li>"document" - relative to document, the origin is the top-left corner of first line</li>
  3538. * <li>"page" - relative to html page that contains the text view</li>
  3539. * <li>"view" - relative to text view, the origin is the top-left corner of the view container</li>
  3540. * </ul>
  3541. * </p>
  3542. * <p>All methods in the view that take or return a position are in the document coordinate space.</p>
  3543. *
  3544. * @param rect the rectangle to convert.
  3545. * @param rect.x the x of the rectangle.
  3546. * @param rect.y the y of the rectangle.
  3547. * @param rect.width the width of the rectangle.
  3548. * @param rect.height the height of the rectangle.
  3549. * @param {String} from the source coordinate space.
  3550. * @param {String} to the destination coordinate space.
  3551. *
  3552. * @see #getLocationAtOffset
  3553. * @see #getOffsetAtLocation
  3554. * @see #getTopPixel
  3555. * @see #setTopPixel
  3556. */
  3557. convert: function(rect, from, to) {
  3558. if (!this._clientDiv) { return; }
  3559. var scroll = this._getScroll();
  3560. var viewPad = this._getViewPadding();
  3561. var frame = this._frame.getBoundingClientRect();
  3562. var viewRect = this._viewDiv.getBoundingClientRect();
  3563. switch(from) {
  3564. case "document":
  3565. if (rect.x !== undefined) {
  3566. rect.x += - scroll.x + viewRect.left + viewPad.left;
  3567. }
  3568. if (rect.y !== undefined) {
  3569. rect.y += - scroll.y + viewRect.top + viewPad.top;
  3570. }
  3571. break;
  3572. case "page":
  3573. if (rect.x !== undefined) {
  3574. rect.x += - frame.left;
  3575. }
  3576. if (rect.y !== undefined) {
  3577. rect.y += - frame.top;
  3578. }
  3579. break;
  3580. }
  3581. //At this point rect is in the widget coordinate space
  3582. switch (to) {
  3583. case "document":
  3584. if (rect.x !== undefined) {
  3585. rect.x += scroll.x - viewRect.left - viewPad.left;
  3586. }
  3587. if (rect.y !== undefined) {
  3588. rect.y += scroll.y - viewRect.top - viewPad.top;
  3589. }
  3590. break;
  3591. case "page":
  3592. if (rect.x !== undefined) {
  3593. rect.x += frame.left;
  3594. }
  3595. if (rect.y !== undefined) {
  3596. rect.y += frame.top;
  3597. }
  3598. break;
  3599. }
  3600. return rect;
  3601. },
  3602. /**
  3603. * Destroys the text view.
  3604. * <p>
  3605. * Removes the view from the page and frees all resources created by the view.
  3606. * Calling this function causes the "Destroy" event to be fire so that all components
  3607. * attached to view can release their references.
  3608. * </p>
  3609. *
  3610. * @see #onDestroy
  3611. */
  3612. destroy: function() {
  3613. /* Destroy rulers*/
  3614. for (var i=0; i< this._rulers.length; i++) {
  3615. this._rulers[i].setView(null);
  3616. }
  3617. this.rulers = null;
  3618. /*
  3619. * Note that when the frame is removed, the unload event is trigged
  3620. * and the view contents and handlers is released properly by
  3621. * destroyView().
  3622. */
  3623. this._destroyFrame();
  3624. var e = {type: "Destroy"};
  3625. this.onDestroy(e);
  3626. this._parent = null;
  3627. this._parentDocument = null;
  3628. this._model = null;
  3629. this._selection = null;
  3630. this._doubleClickSelection = null;
  3631. this._keyBindings = null;
  3632. this._actions = null;
  3633. },
  3634. /**
  3635. * Gives focus to the text view.
  3636. */
  3637. focus: function() {
  3638. if (!this._clientDiv) { return; }
  3639. /*
  3640. * Feature in Chrome. When focus is called in the clientDiv without
  3641. * setting selection the browser will set the selection to the first dom
  3642. * element, which can be above the client area. When this happen the
  3643. * browser also scrolls the window to show that element.
  3644. * The fix is to call _updateDOMSelection() before calling focus().
  3645. */
  3646. this._updateDOMSelection();
  3647. if (isPad) {
  3648. this._textArea.focus();
  3649. } else {
  3650. if (isOpera) { this._clientDiv.blur(); }
  3651. this._clientDiv.focus();
  3652. }
  3653. /*
  3654. * Feature in Safari. When focus is called the browser selects the clientDiv
  3655. * itself. The fix is to call _updateDOMSelection() after calling focus().
  3656. */
  3657. this._updateDOMSelection();
  3658. },
  3659. /**
  3660. * Check if the text view has focus.
  3661. *
  3662. * @returns {Boolean} <code>true</code> if the text view has focus, otherwise <code>false</code>.
  3663. */
  3664. hasFocus: function() {
  3665. return this._hasFocus;
  3666. },
  3667. /**
  3668. * Returns all action names defined in the text view.
  3669. * <p>
  3670. * There are two types of actions, the predefined actions of the view
  3671. * and the actions added by application code.
  3672. * </p>
  3673. * <p>
  3674. * The predefined actions are:
  3675. * <ul>
  3676. * <li>Navigation actions. These actions move the caret collapsing the selection.</li>
  3677. * <ul>
  3678. * <li>"lineUp" - moves the caret up by one line</li>
  3679. * <li>"lineDown" - moves the caret down by one line</li>
  3680. * <li>"lineStart" - moves the caret to beginning of the current line</li>
  3681. * <li>"lineEnd" - moves the caret to end of the current line </li>
  3682. * <li>"charPrevious" - moves the caret to the previous character</li>
  3683. * <li>"charNext" - moves the caret to the next character</li>
  3684. * <li>"pageUp" - moves the caret up by one page</li>
  3685. * <li>"pageDown" - moves the caret down by one page</li>
  3686. * <li>"wordPrevious" - moves the caret to the previous word</li>
  3687. * <li>"wordNext" - moves the caret to the next word</li>
  3688. * <li>"textStart" - moves the caret to the beginning of the document</li>
  3689. * <li>"textEnd" - moves the caret to the end of the document</li>
  3690. * </ul>
  3691. * <li>Selection actions. These actions move the caret extending the selection.</li>
  3692. * <ul>
  3693. * <li>"selectLineUp" - moves the caret up by one line</li>
  3694. * <li>"selectLineDown" - moves the caret down by one line</li>
  3695. * <li>"selectLineStart" - moves the caret to beginning of the current line</li>
  3696. * <li>"selectLineEnd" - moves the caret to end of the current line </li>
  3697. * <li>"selectCharPrevious" - moves the caret to the previous character</li>
  3698. * <li>"selectCharNext" - moves the caret to the next character</li>
  3699. * <li>"selectPageUp" - moves the caret up by one page</li>
  3700. * <li>"selectPageDown" - moves the caret down by one page</li>
  3701. * <li>"selectWordPrevious" - moves the caret to the previous word</li>
  3702. * <li>"selectWordNext" - moves the caret to the next word</li>
  3703. * <li>"selectTextStart" - moves the caret to the beginning of the document</li>
  3704. * <li>"selectTextEnd" - moves the caret to the end of the document</li>
  3705. * <li>"selectAll" - selects the entire document</li>
  3706. * </ul>
  3707. * <li>Edit actions. These actions modify the text view text</li>
  3708. * <ul>
  3709. * <li>"deletePrevious" - deletes the character preceding the caret</li>
  3710. * <li>"deleteNext" - deletes the charecter following the caret</li>
  3711. * <li>"deleteWordPrevious" - deletes the word preceding the caret</li>
  3712. * <li>"deleteWordNext" - deletes the word following the caret</li>
  3713. * <li>"tab" - inserts a tab character at the caret</li>
  3714. * <li>"enter" - inserts a line delimiter at the caret</li>
  3715. * </ul>
  3716. * <li>Clipboard actions.</li>
  3717. * <ul>
  3718. * <li>"copy" - copies the selected text to the clipboard</li>
  3719. * <li>"cut" - copies the selected text to the clipboard and deletes the selection</li>
  3720. * <li>"paste" - replaces the selected text with the clipboard contents</li>
  3721. * </ul>
  3722. * </ul>
  3723. * </p>
  3724. *
  3725. * @param {Boolean} [defaultAction=false] whether or not the predefined actions are included.
  3726. * @returns {String[]} an array of action names defined in the text view.
  3727. *
  3728. * @see #invokeAction
  3729. * @see #setAction
  3730. * @see #setKeyBinding
  3731. * @see #getKeyBindings
  3732. */
  3733. getActions: function (defaultAction) {
  3734. var result = [];
  3735. var actions = this._actions;
  3736. for (var i = 0; i < actions.length; i++) {
  3737. if (!defaultAction && actions[i].defaultHandler) { continue; }
  3738. result.push(actions[i].name);
  3739. }
  3740. return result;
  3741. },
  3742. /**
  3743. * Returns the bottom index.
  3744. * <p>
  3745. * The bottom index is the line that is currently at the bottom of the view. This
  3746. * line may be partially visible depending on the vertical scroll of the view. The parameter
  3747. * <code>fullyVisible</code> determines whether to return only fully visible lines.
  3748. * </p>
  3749. *
  3750. * @param {Boolean} [fullyVisible=false] if <code>true</code>, returns the index of the last fully visible line. This
  3751. * parameter is ignored if the view is not big enough to show one line.
  3752. * @returns {Number} the index of the bottom line.
  3753. *
  3754. * @see #getTopIndex
  3755. * @see #setTopIndex
  3756. */
  3757. getBottomIndex: function(fullyVisible) {
  3758. if (!this._clientDiv) { return 0; }
  3759. return this._getBottomIndex(fullyVisible);
  3760. },
  3761. /**
  3762. * Returns the bottom pixel.
  3763. * <p>
  3764. * The bottom pixel is the pixel position that is currently at
  3765. * the bottom edge of the view. This position is relative to the
  3766. * beginning of the document.
  3767. * </p>
  3768. *
  3769. * @returns {Number} the bottom pixel.
  3770. *
  3771. * @see #getTopPixel
  3772. * @see #setTopPixel
  3773. * @see #convert
  3774. */
  3775. getBottomPixel: function() {
  3776. if (!this._clientDiv) { return 0; }
  3777. return this._getScroll().y + this._getClientHeight();
  3778. },
  3779. /**
  3780. * Returns the caret offset relative to the start of the document.
  3781. *
  3782. * @returns the caret offset relative to the start of the document.
  3783. *
  3784. * @see #setCaretOffset
  3785. * @see #setSelection
  3786. * @see #getSelection
  3787. */
  3788. getCaretOffset: function () {
  3789. var s = this._getSelection();
  3790. return s.getCaret();
  3791. },
  3792. /**
  3793. * Returns the client area.
  3794. * <p>
  3795. * The client area is the portion in pixels of the document that is visible. The
  3796. * client area position is relative to the beginning of the document.
  3797. * </p>
  3798. *
  3799. * @returns the client area rectangle {x, y, width, height}.
  3800. *
  3801. * @see #getTopPixel
  3802. * @see #getBottomPixel
  3803. * @see #getHorizontalPixel
  3804. * @see #convert
  3805. */
  3806. getClientArea: function() {
  3807. if (!this._clientDiv) { return {x: 0, y: 0, width: 0, height: 0}; }
  3808. var scroll = this._getScroll();
  3809. return {x: scroll.x, y: scroll.y, width: this._getClientWidth(), height: this._getClientHeight()};
  3810. },
  3811. /**
  3812. * Returns the horizontal pixel.
  3813. * <p>
  3814. * The horizontal pixel is the pixel position that is currently at
  3815. * the left edge of the view. This position is relative to the
  3816. * beginning of the document.
  3817. * </p>
  3818. *
  3819. * @returns {Number} the horizontal pixel.
  3820. *
  3821. * @see #setHorizontalPixel
  3822. * @see #convert
  3823. */
  3824. getHorizontalPixel: function() {
  3825. if (!this._clientDiv) { return 0; }
  3826. return this._getScroll().x;
  3827. },
  3828. /**
  3829. * Returns all the key bindings associated to the given action name.
  3830. *
  3831. * @param {String} name the action name.
  3832. * @returns {orion.textview.KeyBinding[]} the array of key bindings associated to the given action name.
  3833. *
  3834. * @see #setKeyBinding
  3835. * @see #setAction
  3836. */
  3837. getKeyBindings: function (name) {
  3838. var result = [];
  3839. var keyBindings = this._keyBindings;
  3840. for (var i = 0; i < keyBindings.length; i++) {
  3841. if (keyBindings[i].name === name) {
  3842. result.push(keyBindings[i].keyBinding);
  3843. }
  3844. }
  3845. return result;
  3846. },
  3847. /**
  3848. * Returns the line height for a given line index. Returns the default line
  3849. * height if the line index is not specified.
  3850. *
  3851. * @param {Number} [lineIndex] the line index.
  3852. * @returns {Number} the height of the line in pixels.
  3853. *
  3854. * @see #getLinePixel
  3855. */
  3856. getLineHeight: function(lineIndex) {
  3857. if (!this._clientDiv) { return 0; }
  3858. return this._getLineHeight();
  3859. },
  3860. /**
  3861. * Returns the top pixel position of a given line index relative to the beginning
  3862. * of the document.
  3863. * <p>
  3864. * Clamps out of range indices.
  3865. * </p>
  3866. *
  3867. * @param {Number} lineIndex the line index.
  3868. * @returns {Number} the pixel position of the line.
  3869. *
  3870. * @see #setTopPixel
  3871. * @see #convert
  3872. */
  3873. getLinePixel: function(lineIndex) {
  3874. if (!this._clientDiv) { return 0; }
  3875. lineIndex = Math.min(Math.max(0, lineIndex), this._model.getLineCount());
  3876. var lineHeight = this._getLineHeight();
  3877. return lineHeight * lineIndex;
  3878. },
  3879. /**
  3880. * Returns the {x, y} pixel location of the top-left corner of the character
  3881. * bounding box at the specified offset in the document. The pixel location
  3882. * is relative to the document.
  3883. * <p>
  3884. * Clamps out of range offsets.
  3885. * </p>
  3886. *
  3887. * @param {Number} offset the character offset
  3888. * @returns the {x, y} pixel location of the given offset.
  3889. *
  3890. * @see #getOffsetAtLocation
  3891. * @see #convert
  3892. */
  3893. getLocationAtOffset: function(offset) {
  3894. if (!this._clientDiv) { return {x: 0, y: 0}; }
  3895. var model = this._model;
  3896. offset = Math.min(Math.max(0, offset), model.getCharCount());
  3897. var lineIndex = model.getLineAtOffset(offset);
  3898. var scroll = this._getScroll();
  3899. var viewRect = this._viewDiv.getBoundingClientRect();
  3900. var viewPad = this._getViewPadding();
  3901. var x = this._getOffsetToX(offset) + scroll.x - viewRect.left - viewPad.left;
  3902. var y = this.getLinePixel(lineIndex);
  3903. return {x: x, y: y};
  3904. },
  3905. /**
  3906. * Returns the specified view options.
  3907. * <p>
  3908. * The returned value is either a <code>orion.textview.TextViewOptions</code> or an option value. An option value is returned when only one string paremeter
  3909. * is specified. A <code>orion.textview.TextViewOptions</code> is returned when there are no paremeters, or the parameters are a list of options names or a
  3910. * <code>orion.textview.TextViewOptions</code>. All view options are returned when there no paremeters.
  3911. * </p>
  3912. *
  3913. * @param {String|orion.textview.TextViewOptions} [options] The options to return.
  3914. * @return {Object|orion.textview.TextViewOptions} The requested options or an option value.
  3915. *
  3916. * @see #setOptions
  3917. */
  3918. getOptions: function() {
  3919. var options;
  3920. if (arguments.length === 0) {
  3921. options = this._defaultOptions();
  3922. } else if (arguments.length === 1) {
  3923. var arg = arguments[0];
  3924. if (typeof arg === "string") {
  3925. return this._clone(this["_" + arg]);
  3926. }
  3927. options = arg;
  3928. } else {
  3929. options = {};
  3930. for (var index in arguments) {
  3931. if (arguments.hasOwnProperty(index)) {
  3932. options[arguments[index]] = undefined;
  3933. }
  3934. }
  3935. }
  3936. for (var option in options) {
  3937. if (options.hasOwnProperty(option)) {
  3938. options[option] = this._clone(this["_" + option]);
  3939. }
  3940. }
  3941. return options;
  3942. },
  3943. /**
  3944. * Returns the text model of the text view.
  3945. *
  3946. * @returns {orion.textview.TextModel} the text model of the view.
  3947. */
  3948. getModel: function() {
  3949. return this._model;
  3950. },
  3951. /**
  3952. * Returns the character offset nearest to the given pixel location. The
  3953. * pixel location is relative to the document.
  3954. *
  3955. * @param x the x of the location
  3956. * @param y the y of the location
  3957. * @returns the character offset at the given location.
  3958. *
  3959. * @see #getLocationAtOffset
  3960. */
  3961. getOffsetAtLocation: function(x, y) {
  3962. if (!this._clientDiv) { return 0; }
  3963. var scroll = this._getScroll();
  3964. var viewRect = this._viewDiv.getBoundingClientRect();
  3965. var viewPad = this._getViewPadding();
  3966. var lineIndex = this._getYToLine(y - scroll.y);
  3967. x += -scroll.x + viewRect.left + viewPad.left;
  3968. var offset = this._getXToOffset(lineIndex, x);
  3969. return offset;
  3970. },
  3971. /**
  3972. * Get the view rulers.
  3973. *
  3974. * @returns the view rulers
  3975. *
  3976. * @see #addRuler
  3977. */
  3978. getRulers: function() {
  3979. return this._rulers.slice(0);
  3980. },
  3981. /**
  3982. * Returns the text view selection.
  3983. * <p>
  3984. * The selection is defined by a start and end character offset relative to the
  3985. * document. The character at end offset is not included in the selection.
  3986. * </p>
  3987. *
  3988. * @returns {orion.textview.Selection} the view selection
  3989. *
  3990. * @see #setSelection
  3991. */
  3992. getSelection: function () {
  3993. var s = this._getSelection();
  3994. return {start: s.start, end: s.end};
  3995. },
  3996. /**
  3997. * Returns the text for the given range.
  3998. * <p>
  3999. * The text does not include the character at the end offset.
  4000. * </p>
  4001. *
  4002. * @param {Number} [start=0] the start offset of text range.
  4003. * @param {Number} [end=char count] the end offset of text range.
  4004. *
  4005. * @see #setText
  4006. */
  4007. getText: function(start, end) {
  4008. var model = this._model;
  4009. return model.getText(start, end);
  4010. },
  4011. /**
  4012. * Returns the top index.
  4013. * <p>
  4014. * The top index is the line that is currently at the top of the view. This
  4015. * line may be partially visible depending on the vertical scroll of the view. The parameter
  4016. * <code>fullyVisible</code> determines whether to return only fully visible lines.
  4017. * </p>
  4018. *
  4019. * @param {Boolean} [fullyVisible=false] if <code>true</code>, returns the index of the first fully visible line. This
  4020. * parameter is ignored if the view is not big enough to show one line.
  4021. * @returns {Number} the index of the top line.
  4022. *
  4023. * @see #getBottomIndex
  4024. * @see #setTopIndex
  4025. */
  4026. getTopIndex: function(fullyVisible) {
  4027. if (!this._clientDiv) { return 0; }
  4028. return this._getTopIndex(fullyVisible);
  4029. },
  4030. /**
  4031. * Returns the top pixel.
  4032. * <p>
  4033. * The top pixel is the pixel position that is currently at
  4034. * the top edge of the view. This position is relative to the
  4035. * beginning of the document.
  4036. * </p>
  4037. *
  4038. * @returns {Number} the top pixel.
  4039. *
  4040. * @see #getBottomPixel
  4041. * @see #setTopPixel
  4042. * @see #convert
  4043. */
  4044. getTopPixel: function() {
  4045. if (!this._clientDiv) { return 0; }
  4046. return this._getScroll().y;
  4047. },
  4048. /**
  4049. * Executes the action handler associated with the given name.
  4050. * <p>
  4051. * The application defined action takes precedence over predefined actions unless
  4052. * the <code>defaultAction</code> paramater is <code>true</code>.
  4053. * </p>
  4054. * <p>
  4055. * If the application defined action returns <code>false</code>, the text view predefined
  4056. * action is executed if present.
  4057. * </p>
  4058. *
  4059. * @param {String} name the action name.
  4060. * @param {Boolean} [defaultAction] whether to always execute the predefined action.
  4061. * @returns {Boolean} <code>true</code> if the action was executed.
  4062. *
  4063. * @see #setAction
  4064. * @see #getActions
  4065. */
  4066. invokeAction: function (name, defaultAction) {
  4067. if (!this._clientDiv) { return; }
  4068. var actions = this._actions;
  4069. for (var i = 0; i < actions.length; i++) {
  4070. var a = actions[i];
  4071. if (a.name && a.name === name) {
  4072. if (!defaultAction && a.userHandler) {
  4073. if (a.userHandler()) { return; }
  4074. }
  4075. if (a.defaultHandler) { return a.defaultHandler(); }
  4076. return false;
  4077. }
  4078. }
  4079. return false;
  4080. },
  4081. /**
  4082. * Returns if the view is loaded.
  4083. * <p>
  4084. * @returns {Boolean} <code>true</code> if the view is loaded.
  4085. *
  4086. * @see #onLoad
  4087. */
  4088. isLoaded: function () {
  4089. return !!this._clientDiv;
  4090. },
  4091. /**
  4092. * @class This is the event sent when the user right clicks or otherwise invokes the context menu of the view.
  4093. * <p>
  4094. * <b>See:</b><br/>
  4095. * {@link orion.textview.TextView}<br/>
  4096. * {@link orion.textview.TextView#event:onContextMenu}
  4097. * </p>
  4098. *
  4099. * @name orion.textview.ContextMenuEvent
  4100. *
  4101. * @property {Number} x The pointer location on the x axis, relative to the document the user is editing.
  4102. * @property {Number} y The pointer location on the y axis, relative to the document the user is editing.
  4103. * @property {Number} screenX The pointer location on the x axis, relative to the screen. This is copied from the DOM contextmenu event.screenX property.
  4104. * @property {Number} screenY The pointer location on the y axis, relative to the screen. This is copied from the DOM contextmenu event.screenY property.
  4105. */
  4106. /**
  4107. * This event is sent when the user invokes the view context menu.
  4108. *
  4109. * @event
  4110. * @param {orion.textview.ContextMenuEvent} contextMenuEvent the event
  4111. */
  4112. onContextMenu: function(contextMenuEvent) {
  4113. return this.dispatchEvent(contextMenuEvent);
  4114. },
  4115. onDragStart: function(dragEvent) {
  4116. return this.dispatchEvent(dragEvent);
  4117. },
  4118. onDrag: function(dragEvent) {
  4119. return this.dispatchEvent(dragEvent);
  4120. },
  4121. onDragEnd: function(dragEvent) {
  4122. return this.dispatchEvent(dragEvent);
  4123. },
  4124. onDragEnter: function(dragEvent) {
  4125. return this.dispatchEvent(dragEvent);
  4126. },
  4127. onDragOver: function(dragEvent) {
  4128. return this.dispatchEvent(dragEvent);
  4129. },
  4130. onDragLeave: function(dragEvent) {
  4131. return this.dispatchEvent(dragEvent);
  4132. },
  4133. onDrop: function(dragEvent) {
  4134. return this.dispatchEvent(dragEvent);
  4135. },
  4136. /**
  4137. * @class This is the event sent when the text view is destroyed.
  4138. * <p>
  4139. * <b>See:</b><br/>
  4140. * {@link orion.textview.TextView}<br/>
  4141. * {@link orion.textview.TextView#event:onDestroy}
  4142. * </p>
  4143. * @name orion.textview.DestroyEvent
  4144. */
  4145. /**
  4146. * This event is sent when the text view has been destroyed.
  4147. *
  4148. * @event
  4149. * @param {orion.textview.DestroyEvent} destroyEvent the event
  4150. *
  4151. * @see #destroy
  4152. */
  4153. onDestroy: function(destroyEvent) {
  4154. return this.dispatchEvent(destroyEvent);
  4155. },
  4156. /**
  4157. * @class This object is used to define style information for the text view.
  4158. * <p>
  4159. * <b>See:</b><br/>
  4160. * {@link orion.textview.TextView}<br/>
  4161. * {@link orion.textview.TextView#event:onLineStyle}
  4162. * </p>
  4163. * @name orion.textview.Style
  4164. *
  4165. * @property {String} styleClass A CSS class name.
  4166. * @property {Object} style An object with CSS properties.
  4167. * @property {String} tagName A DOM tag name.
  4168. * @property {Object} attributes An object with DOM attributes.
  4169. */
  4170. /**
  4171. * @class This object is used to style range.
  4172. * <p>
  4173. * <b>See:</b><br/>
  4174. * {@link orion.textview.TextView}<br/>
  4175. * {@link orion.textview.TextView#event:onLineStyle}
  4176. * </p>
  4177. * @name orion.textview.StyleRange
  4178. *
  4179. * @property {Number} start The start character offset, relative to the document, where the style should be applied.
  4180. * @property {Number} end The end character offset (exclusive), relative to the document, where the style should be applied.
  4181. * @property {orion.textview.Style} style The style for the range.
  4182. */
  4183. /**
  4184. * @class This is the event sent when the text view needs the style information for a line.
  4185. * <p>
  4186. * <b>See:</b><br/>
  4187. * {@link orion.textview.TextView}<br/>
  4188. * {@link orion.textview.TextView#event:onLineStyle}
  4189. * </p>
  4190. * @name orion.textview.LineStyleEvent
  4191. *
  4192. * @property {orion.textview.TextView} textView The text view.
  4193. * @property {Number} lineIndex The line index.
  4194. * @property {String} lineText The line text.
  4195. * @property {Number} lineStart The character offset, relative to document, of the first character in the line.
  4196. * @property {orion.textview.Style} style The style for the entire line (output argument).
  4197. * @property {orion.textview.StyleRange[]} ranges An array of style ranges for the line (output argument).
  4198. */
  4199. /**
  4200. * This event is sent when the text view needs the style information for a line.
  4201. *
  4202. * @event
  4203. * @param {orion.textview.LineStyleEvent} lineStyleEvent the event
  4204. */
  4205. onLineStyle: function(lineStyleEvent) {
  4206. return this.dispatchEvent(lineStyleEvent);
  4207. },
  4208. /**
  4209. * @class This is the event sent when the text view has loaded its contents.
  4210. * <p>
  4211. * <b>See:</b><br/>
  4212. * {@link orion.textview.TextView}<br/>
  4213. * {@link orion.textview.TextView#event:onLoad}
  4214. * </p>
  4215. * @name orion.textview.LoadEvent
  4216. */
  4217. /**
  4218. * This event is sent when the text view has loaded its contents.
  4219. *
  4220. * @event
  4221. * @param {orion.textview.LoadEvent} loadEvent the event
  4222. */
  4223. onLoad: function(loadEvent) {
  4224. return this.dispatchEvent(loadEvent);
  4225. },
  4226. /**
  4227. * @class This is the event sent when the text in the model has changed.
  4228. * <p>
  4229. * <b>See:</b><br/>
  4230. * {@link orion.textview.TextView}<br/>
  4231. * {@link orion.textview.TextView#event:onModelChanged}<br/>
  4232. * {@link orion.textview.TextModel#onChanged}
  4233. * </p>
  4234. * @name orion.textview.ModelChangedEvent
  4235. *
  4236. * @property {Number} start The character offset in the model where the change has occurred.
  4237. * @property {Number} removedCharCount The number of characters removed from the model.
  4238. * @property {Number} addedCharCount The number of characters added to the model.
  4239. * @property {Number} removedLineCount The number of lines removed from the model.
  4240. * @property {Number} addedLineCount The number of lines added to the model.
  4241. */
  4242. /**
  4243. * This event is sent when the text in the model has changed.
  4244. *
  4245. * @event
  4246. * @param {orion.textview.ModelChangedEvent} modelChangedEvent the event
  4247. */
  4248. onModelChanged: function(modelChangedEvent) {
  4249. return this.dispatchEvent(modelChangedEvent);
  4250. },
  4251. /**
  4252. * @class This is the event sent when the text in the model is about to change.
  4253. * <p>
  4254. * <b>See:</b><br/>
  4255. * {@link orion.textview.TextView}<br/>
  4256. * {@link orion.textview.TextView#event:onModelChanging}<br/>
  4257. * {@link orion.textview.TextModel#onChanging}
  4258. * </p>
  4259. * @name orion.textview.ModelChangingEvent
  4260. *
  4261. * @property {String} text The text that is about to be inserted in the model.
  4262. * @property {Number} start The character offset in the model where the change will occur.
  4263. * @property {Number} removedCharCount The number of characters being removed from the model.
  4264. * @property {Number} addedCharCount The number of characters being added to the model.
  4265. * @property {Number} removedLineCount The number of lines being removed from the model.
  4266. * @property {Number} addedLineCount The number of lines being added to the model.
  4267. */
  4268. /**
  4269. * This event is sent when the text in the model is about to change.
  4270. *
  4271. * @event
  4272. * @param {orion.textview.ModelChangingEvent} modelChangingEvent the event
  4273. */
  4274. onModelChanging: function(modelChangingEvent) {
  4275. return this.dispatchEvent(modelChangingEvent);
  4276. },
  4277. /**
  4278. * @class This is the event sent when the text is modified by the text view.
  4279. * <p>
  4280. * <b>See:</b><br/>
  4281. * {@link orion.textview.TextView}<br/>
  4282. * {@link orion.textview.TextView#event:onModify}
  4283. * </p>
  4284. * @name orion.textview.ModifyEvent
  4285. */
  4286. /**
  4287. * This event is sent when the text view has changed text in the model.
  4288. * <p>
  4289. * If the text is changed directly through the model API, this event
  4290. * is not sent.
  4291. * </p>
  4292. *
  4293. * @event
  4294. * @param {orion.textview.ModifyEvent} modifyEvent the event
  4295. */
  4296. onModify: function(modifyEvent) {
  4297. return this.dispatchEvent(modifyEvent);
  4298. },
  4299. onMouseDown: function(mouseEvent) {
  4300. return this.dispatchEvent(mouseEvent);
  4301. },
  4302. onMouseUp: function(mouseEvent) {
  4303. return this.dispatchEvent(mouseEvent);
  4304. },
  4305. onMouseMove: function(mouseEvent) {
  4306. return this.dispatchEvent(mouseEvent);
  4307. },
  4308. onMouseOver: function(mouseEvent) {
  4309. return this.dispatchEvent(mouseEvent);
  4310. },
  4311. onMouseOut: function(mouseEvent) {
  4312. return this.dispatchEvent(mouseEvent);
  4313. },
  4314. /**
  4315. * @class This is the event sent when the selection changes in the text view.
  4316. * <p>
  4317. * <b>See:</b><br/>
  4318. * {@link orion.textview.TextView}<br/>
  4319. * {@link orion.textview.TextView#event:onSelection}
  4320. * </p>
  4321. * @name orion.textview.SelectionEvent
  4322. *
  4323. * @property {orion.textview.Selection} oldValue The old selection.
  4324. * @property {orion.textview.Selection} newValue The new selection.
  4325. */
  4326. /**
  4327. * This event is sent when the text view selection has changed.
  4328. *
  4329. * @event
  4330. * @param {orion.textview.SelectionEvent} selectionEvent the event
  4331. */
  4332. onSelection: function(selectionEvent) {
  4333. return this.dispatchEvent(selectionEvent);
  4334. },
  4335. /**
  4336. * @class This is the event sent when the text view scrolls.
  4337. * <p>
  4338. * <b>See:</b><br/>
  4339. * {@link orion.textview.TextView}<br/>
  4340. * {@link orion.textview.TextView#event:onScroll}
  4341. * </p>
  4342. * @name orion.textview.ScrollEvent
  4343. *
  4344. * @property oldValue The old scroll {x,y}.
  4345. * @property newValue The new scroll {x,y}.
  4346. */
  4347. /**
  4348. * This event is sent when the text view scrolls vertically or horizontally.
  4349. *
  4350. * @event
  4351. * @param {orion.textview.ScrollEvent} scrollEvent the event
  4352. */
  4353. onScroll: function(scrollEvent) {
  4354. return this.dispatchEvent(scrollEvent);
  4355. },
  4356. /**
  4357. * @class This is the event sent when the text is about to be modified by the text view.
  4358. * <p>
  4359. * <b>See:</b><br/>
  4360. * {@link orion.textview.TextView}<br/>
  4361. * {@link orion.textview.TextView#event:onVerify}
  4362. * </p>
  4363. * @name orion.textview.VerifyEvent
  4364. *
  4365. * @property {String} text The text being inserted.
  4366. * @property {Number} start The start offset of the text range to be replaced.
  4367. * @property {Number} end The end offset (exclusive) of the text range to be replaced.
  4368. */
  4369. /**
  4370. * This event is sent when the text view is about to change text in the model.
  4371. * <p>
  4372. * If the text is changed directly through the model API, this event
  4373. * is not sent.
  4374. * </p>
  4375. * <p>
  4376. * Listeners are allowed to change these parameters. Setting text to null
  4377. * or undefined stops the change.
  4378. * </p>
  4379. *
  4380. * @event
  4381. * @param {orion.textview.VerifyEvent} verifyEvent the event
  4382. */
  4383. onVerify: function(verifyEvent) {
  4384. return this.dispatchEvent(verifyEvent);
  4385. },
  4386. /**
  4387. * @class This is the event sent when the text view has unloaded its contents.
  4388. * <p>
  4389. * <b>See:</b><br/>
  4390. * {@link orion.textview.TextView}<br/>
  4391. * {@link orion.textview.TextView#event:onLoad}
  4392. * </p>
  4393. * @name orion.textview.UnloadEvent
  4394. */
  4395. /**
  4396. * This event is sent when the text view has unloaded its contents.
  4397. *
  4398. * @event
  4399. * @param {orion.textview.UnloadEvent} unloadEvent the event
  4400. */
  4401. onUnload: function(unloadEvent) {
  4402. return this.dispatchEvent(unloadEvent);
  4403. },
  4404. /**
  4405. * @class This is the event sent when the text view is focused.
  4406. * <p>
  4407. * <b>See:</b><br/>
  4408. * {@link orion.textview.TextView}<br/>
  4409. * {@link orion.textview.TextView#event:onFocus}<br/>
  4410. * </p>
  4411. * @name orion.textview.FocusEvent
  4412. */
  4413. /**
  4414. * This event is sent when the text view is focused.
  4415. *
  4416. * @event
  4417. * @param {orion.textview.FocusEvent} focusEvent the event
  4418. */
  4419. onFocus: function(focusEvent) {
  4420. return this.dispatchEvent(focusEvent);
  4421. },
  4422. /**
  4423. * @class This is the event sent when the text view goes out of focus.
  4424. * <p>
  4425. * <b>See:</b><br/>
  4426. * {@link orion.textview.TextView}<br/>
  4427. * {@link orion.textview.TextView#event:onBlur}<br/>
  4428. * </p>
  4429. * @name orion.textview.BlurEvent
  4430. */
  4431. /**
  4432. * This event is sent when the text view goes out of focus.
  4433. *
  4434. * @event
  4435. * @param {orion.textview.BlurEvent} blurEvent the event
  4436. */
  4437. onBlur: function(blurEvent) {
  4438. return this.dispatchEvent(blurEvent);
  4439. },
  4440. /**
  4441. * Redraws the entire view, including rulers.
  4442. *
  4443. * @see #redrawLines
  4444. * @see #redrawRange
  4445. * @see #setRedraw
  4446. */
  4447. redraw: function() {
  4448. if (this._redrawCount > 0) { return; }
  4449. var lineCount = this._model.getLineCount();
  4450. var rulers = this.getRulers();
  4451. for (var i = 0; i < rulers.length; i++) {
  4452. this.redrawLines(0, lineCount, rulers[i]);
  4453. }
  4454. this.redrawLines(0, lineCount);
  4455. },
  4456. /**
  4457. * Redraws the text in the given line range.
  4458. * <p>
  4459. * The line at the end index is not redrawn.
  4460. * </p>
  4461. *
  4462. * @param {Number} [startLine=0] the start line
  4463. * @param {Number} [endLine=line count] the end line
  4464. *
  4465. * @see #redraw
  4466. * @see #redrawRange
  4467. * @see #setRedraw
  4468. */
  4469. redrawLines: function(startLine, endLine, ruler) {
  4470. if (this._redrawCount > 0) { return; }
  4471. if (startLine === undefined) { startLine = 0; }
  4472. if (endLine === undefined) { endLine = this._model.getLineCount(); }
  4473. if (startLine === endLine) { return; }
  4474. var div = this._clientDiv;
  4475. if (!div) { return; }
  4476. if (ruler) {
  4477. var location = ruler.getLocation();//"left" or "right"
  4478. var divRuler = location === "left" ? this._leftDiv : this._rightDiv;
  4479. var cells = divRuler.firstChild.rows[0].cells;
  4480. for (var i = 0; i < cells.length; i++) {
  4481. if (cells[i].firstChild._ruler === ruler) {
  4482. div = cells[i].firstChild;
  4483. break;
  4484. }
  4485. }
  4486. }
  4487. if (ruler) {
  4488. div.rulerChanged = true;
  4489. }
  4490. if (!ruler || ruler.getOverview() === "page") {
  4491. var child = div.firstChild;
  4492. while (child) {
  4493. var lineIndex = child.lineIndex;
  4494. if (startLine <= lineIndex && lineIndex < endLine) {
  4495. child.lineChanged = true;
  4496. }
  4497. child = child.nextSibling;
  4498. }
  4499. }
  4500. if (!ruler) {
  4501. if (startLine <= this._maxLineIndex && this._maxLineIndex < endLine) {
  4502. this._checkMaxLineIndex = this._maxLineIndex;
  4503. this._maxLineIndex = -1;
  4504. this._maxLineWidth = 0;
  4505. }
  4506. }
  4507. this._queueUpdatePage();
  4508. },
  4509. /**
  4510. * Redraws the text in the given range.
  4511. * <p>
  4512. * The character at the end offset is not redrawn.
  4513. * </p>
  4514. *
  4515. * @param {Number} [start=0] the start offset of text range
  4516. * @param {Number} [end=char count] the end offset of text range
  4517. *
  4518. * @see #redraw
  4519. * @see #redrawLines
  4520. * @see #setRedraw
  4521. */
  4522. redrawRange: function(start, end) {
  4523. if (this._redrawCount > 0) { return; }
  4524. var model = this._model;
  4525. if (start === undefined) { start = 0; }
  4526. if (end === undefined) { end = model.getCharCount(); }
  4527. var startLine = model.getLineAtOffset(start);
  4528. var endLine = model.getLineAtOffset(Math.max(start, end - 1)) + 1;
  4529. this.redrawLines(startLine, endLine);
  4530. },
  4531. /**
  4532. * Removes a ruler from the text view.
  4533. *
  4534. * @param {orion.textview.Ruler} ruler the ruler.
  4535. */
  4536. removeRuler: function (ruler) {
  4537. var rulers = this._rulers;
  4538. for (var i=0; i<rulers.length; i++) {
  4539. if (rulers[i] === ruler) {
  4540. rulers.splice(i, 1);
  4541. ruler.setView(null);
  4542. this._destroyRuler(ruler);
  4543. this._updatePage();
  4544. break;
  4545. }
  4546. }
  4547. },
  4548. /**
  4549. * Associates an application defined handler to an action name.
  4550. * <p>
  4551. * If the action name is a predefined action, the given handler executes before
  4552. * the default action handler. If the given handler returns <code>true</code>, the
  4553. * default action handler is not called.
  4554. * </p>
  4555. *
  4556. * @param {String} name the action name.
  4557. * @param {Function} handler the action handler.
  4558. *
  4559. * @see #getActions
  4560. * @see #invokeAction
  4561. */
  4562. setAction: function(name, handler) {
  4563. if (!name) { return; }
  4564. var actions = this._actions;
  4565. for (var i = 0; i < actions.length; i++) {
  4566. var a = actions[i];
  4567. if (a.name === name) {
  4568. a.userHandler = handler;
  4569. return;
  4570. }
  4571. }
  4572. actions.push({name: name, userHandler: handler});
  4573. },
  4574. /**
  4575. * Associates a key binding with the given action name. Any previous
  4576. * association with the specified key binding is overwriten. If the
  4577. * action name is <code>null</code>, the association is removed.
  4578. *
  4579. * @param {orion.textview.KeyBinding} keyBinding the key binding
  4580. * @param {String} name the action
  4581. */
  4582. setKeyBinding: function(keyBinding, name) {
  4583. var keyBindings = this._keyBindings;
  4584. for (var i = 0; i < keyBindings.length; i++) {
  4585. var kb = keyBindings[i];
  4586. if (kb.keyBinding.equals(keyBinding)) {
  4587. if (name) {
  4588. kb.name = name;
  4589. } else {
  4590. if (kb.predefined) {
  4591. kb.name = null;
  4592. } else {
  4593. var oldName = kb.name;
  4594. keyBindings.splice(i, 1);
  4595. var index = 0;
  4596. while (index < keyBindings.length && oldName !== keyBindings[index].name) {
  4597. index++;
  4598. }
  4599. if (index === keyBindings.length) {
  4600. /* <p>
  4601. * Removing all the key bindings associated to an user action will cause
  4602. * the user action to be removed. TextView predefined actions are never
  4603. * removed (so they can be reinstalled in the future).
  4604. * </p>
  4605. */
  4606. var actions = this._actions;
  4607. for (var j = 0; j < actions.length; j++) {
  4608. if (actions[j].name === oldName) {
  4609. if (!actions[j].defaultHandler) {
  4610. actions.splice(j, 1);
  4611. }
  4612. }
  4613. }
  4614. }
  4615. }
  4616. }
  4617. return;
  4618. }
  4619. }
  4620. if (name) {
  4621. keyBindings.push({keyBinding: keyBinding, name: name});
  4622. }
  4623. },
  4624. /**
  4625. * Sets the caret offset relative to the start of the document.
  4626. *
  4627. * @param {Number} caret the caret offset relative to the start of the document.
  4628. * @param {Boolean} [show=true] if <code>true</code>, the view will scroll if needed to show the caret location.
  4629. *
  4630. * @see #getCaretOffset
  4631. * @see #setSelection
  4632. * @see #getSelection
  4633. */
  4634. setCaretOffset: function(offset, show) {
  4635. var charCount = this._model.getCharCount();
  4636. offset = Math.max(0, Math.min (offset, charCount));
  4637. var selection = new Selection(offset, offset, false);
  4638. this._setSelection (selection, show === undefined || show);
  4639. },
  4640. /**
  4641. * Sets the horizontal pixel.
  4642. * <p>
  4643. * The horizontal pixel is the pixel position that is currently at
  4644. * the left edge of the view. This position is relative to the
  4645. * beginning of the document.
  4646. * </p>
  4647. *
  4648. * @param {Number} pixel the horizontal pixel.
  4649. *
  4650. * @see #getHorizontalPixel
  4651. * @see #convert
  4652. */
  4653. setHorizontalPixel: function(pixel) {
  4654. if (!this._clientDiv) { return; }
  4655. pixel = Math.max(0, pixel);
  4656. this._scrollView(pixel - this._getScroll().x, 0);
  4657. },
  4658. /**
  4659. * Sets whether the view should update the DOM.
  4660. * <p>
  4661. * This can be used to improve the performance.
  4662. * </p><p>
  4663. * When the flag is set to <code>true</code>,
  4664. * the entire view is marked as needing to be redrawn.
  4665. * Nested calls to this method are stacked.
  4666. * </p>
  4667. *
  4668. * @param {Boolean} redraw the new redraw state
  4669. *
  4670. * @see #redraw
  4671. */
  4672. setRedraw: function(redraw) {
  4673. if (redraw) {
  4674. if (--this._redrawCount === 0) {
  4675. this.redraw();
  4676. }
  4677. } else {
  4678. this._redrawCount++;
  4679. }
  4680. },
  4681. /**
  4682. * Sets the text model of the text view.
  4683. *
  4684. * @param {orion.textview.TextModel} model the text model of the view.
  4685. */
  4686. setModel: function(model) {
  4687. if (!model) { return; }
  4688. if (model === this._model) { return; }
  4689. this._model.removeEventListener("Changing", this._modelListener.onChanging);
  4690. this._model.removeEventListener("Changed", this._modelListener.onChanged);
  4691. var oldLineCount = this._model.getLineCount();
  4692. var oldCharCount = this._model.getCharCount();
  4693. var newLineCount = model.getLineCount();
  4694. var newCharCount = model.getCharCount();
  4695. var newText = model.getText();
  4696. var e = {
  4697. type: "ModelChanging",
  4698. text: newText,
  4699. start: 0,
  4700. removedCharCount: oldCharCount,
  4701. addedCharCount: newCharCount,
  4702. removedLineCount: oldLineCount,
  4703. addedLineCount: newLineCount
  4704. };
  4705. this.onModelChanging(e);
  4706. this._model = model;
  4707. e = {
  4708. type: "ModelChanged",
  4709. start: 0,
  4710. removedCharCount: oldCharCount,
  4711. addedCharCount: newCharCount,
  4712. removedLineCount: oldLineCount,
  4713. addedLineCount: newLineCount
  4714. };
  4715. this.onModelChanged(e);
  4716. this._model.addEventListener("Changing", this._modelListener.onChanging);
  4717. this._model.addEventListener("Changed", this._modelListener.onChanged);
  4718. this._reset();
  4719. this._updatePage();
  4720. },
  4721. /**
  4722. * Sets the view options for the view.
  4723. *
  4724. * @param {orion.textview.TextViewOptions} options the view options.
  4725. *
  4726. * @see #getOptions
  4727. */
  4728. setOptions: function (options) {
  4729. var defaultOptions = this._defaultOptions();
  4730. var recreate = false, option, created = this._clientDiv;
  4731. if (created) {
  4732. for (option in options) {
  4733. if (options.hasOwnProperty(option)) {
  4734. if (defaultOptions[option].recreate) {
  4735. recreate = true;
  4736. break;
  4737. }
  4738. }
  4739. }
  4740. }
  4741. var changed = false;
  4742. for (option in options) {
  4743. if (options.hasOwnProperty(option)) {
  4744. var newValue = options[option], oldValue = this["_" + option];
  4745. if (this._compare(oldValue, newValue)) { continue; }
  4746. changed = true;
  4747. if (!recreate) {
  4748. var update = defaultOptions[option].update;
  4749. if (created && update) {
  4750. if (update.call(this, newValue)) {
  4751. recreate = true;
  4752. }
  4753. continue;
  4754. }
  4755. }
  4756. this["_" + option] = this._clone(newValue);
  4757. }
  4758. }
  4759. if (changed) {
  4760. if (recreate) {
  4761. var oldParent = this._frame.parentNode;
  4762. oldParent.removeChild(this._frame);
  4763. this._parent.appendChild(this._frame);
  4764. }
  4765. }
  4766. },
  4767. /**
  4768. * Sets the text view selection.
  4769. * <p>
  4770. * The selection is defined by a start and end character offset relative to the
  4771. * document. The character at end offset is not included in the selection.
  4772. * </p>
  4773. * <p>
  4774. * The caret is always placed at the end offset. The start offset can be
  4775. * greater than the end offset to place the caret at the beginning of the
  4776. * selection.
  4777. * </p>
  4778. * <p>
  4779. * Clamps out of range offsets.
  4780. * </p>
  4781. *
  4782. * @param {Number} start the start offset of the selection
  4783. * @param {Number} end the end offset of the selection
  4784. * @param {Boolean} [show=true] if <code>true</code>, the view will scroll if needed to show the caret location.
  4785. *
  4786. * @see #getSelection
  4787. */
  4788. setSelection: function (start, end, show) {
  4789. var caret = start > end;
  4790. if (caret) {
  4791. var tmp = start;
  4792. start = end;
  4793. end = tmp;
  4794. }
  4795. var charCount = this._model.getCharCount();
  4796. start = Math.max(0, Math.min (start, charCount));
  4797. end = Math.max(0, Math.min (end, charCount));
  4798. var selection = new Selection(start, end, caret);
  4799. this._setSelection(selection, show === undefined || show);
  4800. },
  4801. /**
  4802. * Replaces the text in the given range with the given text.
  4803. * <p>
  4804. * The character at the end offset is not replaced.
  4805. * </p>
  4806. * <p>
  4807. * When both <code>start</code> and <code>end</code> parameters
  4808. * are not specified, the text view places the caret at the beginning
  4809. * of the document and scrolls to make it visible.
  4810. * </p>
  4811. *
  4812. * @param {String} text the new text.
  4813. * @param {Number} [start=0] the start offset of text range.
  4814. * @param {Number} [end=char count] the end offset of text range.
  4815. *
  4816. * @see #getText
  4817. */
  4818. setText: function (text, start, end) {
  4819. var reset = start === undefined && end === undefined;
  4820. if (start === undefined) { start = 0; }
  4821. if (end === undefined) { end = this._model.getCharCount(); }
  4822. this._modifyContent({text: text, start: start, end: end, _code: true}, !reset);
  4823. if (reset) {
  4824. this._columnX = -1;
  4825. this._setSelection(new Selection (0, 0, false), true);
  4826. /*
  4827. * Bug in Firefox. For some reason, the caret does not show after the
  4828. * view is refreshed. The fix is to toggle the contentEditable state and
  4829. * force the clientDiv to loose and receive focus if it is focused.
  4830. */
  4831. if (isFirefox) {
  4832. this._fixCaret();
  4833. }
  4834. }
  4835. },
  4836. /**
  4837. * Sets the top index.
  4838. * <p>
  4839. * The top index is the line that is currently at the top of the text view. This
  4840. * line may be partially visible depending on the vertical scroll of the view.
  4841. * </p>
  4842. *
  4843. * @param {Number} topIndex the index of the top line.
  4844. *
  4845. * @see #getBottomIndex
  4846. * @see #getTopIndex
  4847. */
  4848. setTopIndex: function(topIndex) {
  4849. if (!this._clientDiv) { return; }
  4850. var model = this._model;
  4851. if (model.getCharCount() === 0) {
  4852. return;
  4853. }
  4854. var lineCount = model.getLineCount();
  4855. var lineHeight = this._getLineHeight();
  4856. var pageSize = Math.max(1, Math.min(lineCount, Math.floor(this._getClientHeight () / lineHeight)));
  4857. if (topIndex < 0) {
  4858. topIndex = 0;
  4859. } else if (topIndex > lineCount - pageSize) {
  4860. topIndex = lineCount - pageSize;
  4861. }
  4862. var pixel = topIndex * lineHeight - this._getScroll().y;
  4863. this._scrollView(0, pixel);
  4864. },
  4865. /**
  4866. * Sets the top pixel.
  4867. * <p>
  4868. * The top pixel is the pixel position that is currently at
  4869. * the top edge of the view. This position is relative to the
  4870. * beginning of the document.
  4871. * </p>
  4872. *
  4873. * @param {Number} pixel the top pixel.
  4874. *
  4875. * @see #getBottomPixel
  4876. * @see #getTopPixel
  4877. * @see #convert
  4878. */
  4879. setTopPixel: function(pixel) {
  4880. if (!this._clientDiv) { return; }
  4881. var lineHeight = this._getLineHeight();
  4882. var clientHeight = this._getClientHeight();
  4883. var lineCount = this._model.getLineCount();
  4884. pixel = Math.min(Math.max(0, pixel), lineHeight * lineCount - clientHeight);
  4885. this._scrollView(0, pixel - this._getScroll().y);
  4886. },
  4887. /**
  4888. * Scrolls the selection into view if needed.
  4889. *
  4890. * @returns true if the view was scrolled.
  4891. *
  4892. * @see #getSelection
  4893. * @see #setSelection
  4894. */
  4895. showSelection: function() {
  4896. return this._showCaret(true);
  4897. },
  4898. /**************************************** Event handlers *********************************/
  4899. _handleBodyMouseDown: function (e) {
  4900. if (!e) { e = window.event; }
  4901. if (isFirefox && e.which === 1) {
  4902. this._clientDiv.contentEditable = false;
  4903. (this._overlayDiv || this._clientDiv).draggable = true;
  4904. this._ignoreBlur = true;
  4905. }
  4906. /*
  4907. * Prevent clicks outside of the view from taking focus
  4908. * away the view. Note that in Firefox and Opera clicking on the
  4909. * scrollbar also take focus from the view. Other browsers
  4910. * do not have this problem and stopping the click over the
  4911. * scrollbar for them causes mouse capture problems.
  4912. */
  4913. var topNode = isOpera || (isFirefox && !this._overlayDiv) ? this._clientDiv : this._overlayDiv || this._viewDiv;
  4914. var temp = e.target ? e.target : e.srcElement;
  4915. while (temp) {
  4916. if (topNode === temp) {
  4917. return;
  4918. }
  4919. temp = temp.parentNode;
  4920. }
  4921. if (e.preventDefault) { e.preventDefault(); }
  4922. if (e.stopPropagation){ e.stopPropagation(); }
  4923. if (!isW3CEvents) {
  4924. /* In IE 8 is not possible to prevent the default handler from running
  4925. * during mouse down event using usual API. The workaround is to use
  4926. * setCapture/releaseCapture.
  4927. */
  4928. topNode.setCapture();
  4929. setTimeout(function() { topNode.releaseCapture(); }, 0);
  4930. }
  4931. },
  4932. _handleBodyMouseUp: function (e) {
  4933. if (!e) { e = window.event; }
  4934. if (isFirefox && e.which === 1) {
  4935. this._clientDiv.contentEditable = true;
  4936. (this._overlayDiv || this._clientDiv).draggable = false;
  4937. /*
  4938. * Bug in Firefox. For some reason, Firefox stops showing the caret
  4939. * in some cases. For example when the user cancels a drag operation
  4940. * by pressing ESC. The fix is to detect that the drag operation was
  4941. * cancelled, toggle the contentEditable state and force the clientDiv
  4942. * to loose and receive focus if it is focused.
  4943. */
  4944. this._fixCaret();
  4945. this._ignoreBlur = false;
  4946. }
  4947. },
  4948. _handleBlur: function (e) {
  4949. if (!e) { e = window.event; }
  4950. if (this._ignoreBlur) { return; }
  4951. this._hasFocus = false;
  4952. /*
  4953. * Bug in IE 8 and earlier. For some reason when text is deselected
  4954. * the overflow selection at the end of some lines does not get redrawn.
  4955. * The fix is to create a DOM element in the body to force a redraw.
  4956. */
  4957. if (isIE < 9) {
  4958. if (!this._getSelection().isEmpty()) {
  4959. var document = this._frameDocument;
  4960. var child = document.createElement("DIV");
  4961. var body = document.body;
  4962. body.appendChild(child);
  4963. body.removeChild(child);
  4964. }
  4965. }
  4966. if (isFirefox || isIE) {
  4967. if (this._selDiv1) {
  4968. var color = isIE ? "transparent" : "#AFAFAF";
  4969. this._selDiv1.style.background = color;
  4970. this._selDiv2.style.background = color;
  4971. this._selDiv3.style.background = color;
  4972. }
  4973. }
  4974. if (!this._ignoreFocus) {
  4975. this.onBlur({type: "Blur"});
  4976. }
  4977. },
  4978. _handleContextMenu: function (e) {
  4979. if (!e) { e = window.event; }
  4980. if (isFirefox && this._lastMouseButton === 3) {
  4981. // We need to update the DOM selection, because on
  4982. // right-click the caret moves to the mouse location.
  4983. // See bug 366312.
  4984. var timeDiff = e.timeStamp - this._lastMouseTime;
  4985. if (timeDiff <= this._clickTime) {
  4986. this._updateDOMSelection();
  4987. }
  4988. }
  4989. if (this.isListening("ContextMenu")) {
  4990. var evt = this._createMouseEvent("ContextMenu", e);
  4991. evt.screenX = e.screenX;
  4992. evt.screenY = e.screenY;
  4993. this.onContextMenu(evt);
  4994. }
  4995. if (e.preventDefault) { e.preventDefault(); }
  4996. return false;
  4997. },
  4998. _handleCopy: function (e) {
  4999. if (this._ignoreCopy) { return; }
  5000. if (!e) { e = window.event; }
  5001. if (this._doCopy(e)) {
  5002. if (e.preventDefault) { e.preventDefault(); }
  5003. return false;
  5004. }
  5005. },
  5006. _handleCut: function (e) {
  5007. if (!e) { e = window.event; }
  5008. if (this._doCut(e)) {
  5009. if (e.preventDefault) { e.preventDefault(); }
  5010. return false;
  5011. }
  5012. },
  5013. _handleDOMAttrModified: function (e) {
  5014. if (!e) { e = window.event; }
  5015. var ancestor = false;
  5016. var parent = this._parent;
  5017. while (parent) {
  5018. if (parent === e.target) {
  5019. ancestor = true;
  5020. break;
  5021. }
  5022. parent = parent.parentNode;
  5023. }
  5024. if (!ancestor) { return; }
  5025. var state = this._getVisible();
  5026. if (state === "visible") {
  5027. this._createView();
  5028. } else if (state === "hidden") {
  5029. this._destroyView();
  5030. }
  5031. },
  5032. _handleDataModified: function(e) {
  5033. this._startIME();
  5034. },
  5035. _handleDblclick: function (e) {
  5036. if (!e) { e = window.event; }
  5037. var time = e.timeStamp ? e.timeStamp : new Date().getTime();
  5038. this._lastMouseTime = time;
  5039. if (this._clickCount !== 2) {
  5040. this._clickCount = 2;
  5041. this._handleMouse(e);
  5042. }
  5043. },
  5044. _handleDragStart: function (e) {
  5045. if (!e) { e = window.event; }
  5046. if (isFirefox) {
  5047. var self = this;
  5048. setTimeout(function() {
  5049. self._clientDiv.contentEditable = true;
  5050. self._clientDiv.draggable = false;
  5051. self._ignoreBlur = false;
  5052. }, 0);
  5053. }
  5054. if (this.isListening("DragStart") && this._dragOffset !== -1) {
  5055. this._isMouseDown = false;
  5056. this.onDragStart(this._createMouseEvent("DragStart", e));
  5057. this._dragOffset = -1;
  5058. } else {
  5059. if (e.preventDefault) { e.preventDefault(); }
  5060. return false;
  5061. }
  5062. },
  5063. _handleDrag: function (e) {
  5064. if (!e) { e = window.event; }
  5065. if (this.isListening("Drag")) {
  5066. this.onDrag(this._createMouseEvent("Drag", e));
  5067. }
  5068. },
  5069. _handleDragEnd: function (e) {
  5070. if (!e) { e = window.event; }
  5071. this._dropTarget = false;
  5072. this._dragOffset = -1;
  5073. if (this.isListening("DragEnd")) {
  5074. this.onDragEnd(this._createMouseEvent("DragEnd", e));
  5075. }
  5076. if (isFirefox) {
  5077. this._fixCaret();
  5078. /*
  5079. * Bug in Firefox. For some reason, Firefox stops showing the caret when the
  5080. * selection is dropped onto itself. The fix is to detected the case and
  5081. * call fixCaret() a second time.
  5082. */
  5083. if (e.dataTransfer.dropEffect === "none" && !e.dataTransfer.mozUserCancelled) {
  5084. this._fixCaret();
  5085. }
  5086. }
  5087. },
  5088. _handleDragEnter: function (e) {
  5089. if (!e) { e = window.event; }
  5090. var prevent = true;
  5091. this._dropTarget = true;
  5092. if (this.isListening("DragEnter")) {
  5093. prevent = false;
  5094. this.onDragEnter(this._createMouseEvent("DragEnter", e));
  5095. }
  5096. /*
  5097. * Webkit will not send drop events if this event is not prevented, as spec in HTML5.
  5098. * Firefox and IE do not follow this spec for contentEditable. Note that preventing this
  5099. * event will result is loss of functionality (insertion mark, etc).
  5100. */
  5101. if (isWebkit || prevent) {
  5102. if (e.preventDefault) { e.preventDefault(); }
  5103. return false;
  5104. }
  5105. },
  5106. _handleDragOver: function (e) {
  5107. if (!e) { e = window.event; }
  5108. var prevent = true;
  5109. if (this.isListening("DragOver")) {
  5110. prevent = false;
  5111. this.onDragOver(this._createMouseEvent("DragOver", e));
  5112. }
  5113. /*
  5114. * Webkit will not send drop events if this event is not prevented, as spec in HTML5.
  5115. * Firefox and IE do not follow this spec for contentEditable. Note that preventing this
  5116. * event will result is loss of functionality (insertion mark, etc).
  5117. */
  5118. if (isWebkit || prevent) {
  5119. if (prevent) { e.dataTransfer.dropEffect = "none"; }
  5120. if (e.preventDefault) { e.preventDefault(); }
  5121. return false;
  5122. }
  5123. },
  5124. _handleDragLeave: function (e) {
  5125. if (!e) { e = window.event; }
  5126. this._dropTarget = false;
  5127. if (this.isListening("DragLeave")) {
  5128. this.onDragLeave(this._createMouseEvent("DragLeave", e));
  5129. }
  5130. },
  5131. _handleDrop: function (e) {
  5132. if (!e) { e = window.event; }
  5133. this._dropTarget = false;
  5134. if (this.isListening("Drop")) {
  5135. this.onDrop(this._createMouseEvent("Drop", e));
  5136. }
  5137. /*
  5138. * This event must be prevented otherwise the user agent will modify
  5139. * the DOM. Note that preventing the event on some user agents (i.e. IE)
  5140. * indicates that the operation is cancelled. This causes the dropEffect to
  5141. * be set to none in the dragend event causing the implementor to not execute
  5142. * the code responsible by the move effect.
  5143. */
  5144. if (e.preventDefault) { e.preventDefault(); }
  5145. return false;
  5146. },
  5147. _handleDocFocus: function (e) {
  5148. if (!e) { e = window.event; }
  5149. this._clientDiv.focus();
  5150. },
  5151. _handleFocus: function (e) {
  5152. if (!e) { e = window.event; }
  5153. this._hasFocus = true;
  5154. /*
  5155. * Feature in IE. The selection is not restored when the
  5156. * view gets focus and the caret is always placed at the
  5157. * beginning of the document. The fix is to update the DOM
  5158. * selection during the focus event.
  5159. */
  5160. if (isIE) {
  5161. this._updateDOMSelection();
  5162. }
  5163. if (isFirefox || isIE) {
  5164. if (this._selDiv1) {
  5165. var color = this._hightlightRGB;
  5166. this._selDiv1.style.background = color;
  5167. this._selDiv2.style.background = color;
  5168. this._selDiv3.style.background = color;
  5169. }
  5170. }
  5171. if (!this._ignoreFocus) {
  5172. this.onFocus({type: "Focus"});
  5173. }
  5174. },
  5175. _handleKeyDown: function (e) {
  5176. if (!e) { e = window.event; }
  5177. if (isPad) {
  5178. if (e.keyCode === 8) {
  5179. this._doBackspace({});
  5180. e.preventDefault();
  5181. }
  5182. return;
  5183. }
  5184. switch (e.keyCode) {
  5185. case 16: /* Shift */
  5186. case 17: /* Control */
  5187. case 18: /* Alt */
  5188. case 91: /* Command */
  5189. break;
  5190. default:
  5191. this._setLinksVisible(false);
  5192. }
  5193. if (e.keyCode === 229) {
  5194. if (this._readonly) {
  5195. if (e.preventDefault) { e.preventDefault(); }
  5196. return false;
  5197. }
  5198. var startIME = true;
  5199. /*
  5200. * Bug in Safari. Some Control+key combinations send key events
  5201. * with keyCode equals to 229. This is unexpected and causes the
  5202. * view to start an IME composition. The fix is to ignore these
  5203. * events.
  5204. */
  5205. if (isSafari && isMac) {
  5206. if (e.ctrlKey) {
  5207. startIME = false;
  5208. }
  5209. }
  5210. if (startIME) {
  5211. this._startIME();
  5212. }
  5213. } else {
  5214. this._commitIME();
  5215. }
  5216. /*
  5217. * Feature in Firefox. When a key is held down the browser sends
  5218. * right number of keypress events but only one keydown. This is
  5219. * unexpected and causes the view to only execute an action
  5220. * just one time. The fix is to ignore the keydown event and
  5221. * execute the actions from the keypress handler.
  5222. * Note: This only happens on the Mac and Linux (Firefox 3.6).
  5223. *
  5224. * Feature in Opera. Opera sends keypress events even for non-printable
  5225. * keys. The fix is to handle actions in keypress instead of keydown.
  5226. */
  5227. if (((isMac || isLinux) && isFirefox < 4) || isOpera) {
  5228. this._keyDownEvent = e;
  5229. return true;
  5230. }
  5231. if (this._doAction(e)) {
  5232. if (e.preventDefault) {
  5233. e.preventDefault();
  5234. } else {
  5235. e.cancelBubble = true;
  5236. e.returnValue = false;
  5237. e.keyCode = 0;
  5238. }
  5239. return false;
  5240. }
  5241. },
  5242. _handleKeyPress: function (e) {
  5243. if (!e) { e = window.event; }
  5244. /*
  5245. * Feature in Embedded WebKit. Embedded WekKit on Mac runs in compatibility mode and
  5246. * generates key press events for these Unicode values (Function keys). This does not
  5247. * happen in Safari or Chrome. The fix is to ignore these key events.
  5248. */
  5249. if (isMac && isWebkit) {
  5250. if ((0xF700 <= e.keyCode && e.keyCode <= 0xF7FF) || e.keyCode === 13 || e.keyCode === 8) {
  5251. if (e.preventDefault) { e.preventDefault(); }
  5252. return false;
  5253. }
  5254. }
  5255. if (((isMac || isLinux) && isFirefox < 4) || isOpera) {
  5256. if (this._doAction(this._keyDownEvent)) {
  5257. if (e.preventDefault) { e.preventDefault(); }
  5258. return false;
  5259. }
  5260. }
  5261. var ctrlKey = isMac ? e.metaKey : e.ctrlKey;
  5262. if (e.charCode !== undefined) {
  5263. if (ctrlKey) {
  5264. switch (e.charCode) {
  5265. /*
  5266. * In Firefox and Safari if ctrl+v, ctrl+c ctrl+x is canceled
  5267. * the clipboard events are not sent. The fix to allow
  5268. * the browser to handles these key events.
  5269. */
  5270. case 99://c
  5271. case 118://v
  5272. case 120://x
  5273. return true;
  5274. }
  5275. }
  5276. }
  5277. var ignore = false;
  5278. if (isMac) {
  5279. if (e.ctrlKey || e.metaKey) { ignore = true; }
  5280. } else {
  5281. if (isFirefox) {
  5282. //Firefox clears the state mask when ALT GR generates input
  5283. if (e.ctrlKey || e.altKey) { ignore = true; }
  5284. } else {
  5285. //IE and Chrome only send ALT GR when input is generated
  5286. if (e.ctrlKey ^ e.altKey) { ignore = true; }
  5287. }
  5288. }
  5289. if (!ignore) {
  5290. var key = isOpera ? e.which : (e.charCode !== undefined ? e.charCode : e.keyCode);
  5291. if (key > 31) {
  5292. this._doContent(String.fromCharCode (key));
  5293. if (e.preventDefault) { e.preventDefault(); }
  5294. return false;
  5295. }
  5296. }
  5297. },
  5298. _handleKeyUp: function (e) {
  5299. if (!e) { e = window.event; }
  5300. var ctrlKey = isMac ? e.metaKey : e.ctrlKey;
  5301. if (!ctrlKey) {
  5302. this._setLinksVisible(false);
  5303. }
  5304. // don't commit for space (it happens during JP composition)
  5305. if (e.keyCode === 13) {
  5306. this._commitIME();
  5307. }
  5308. },
  5309. _handleLinkClick: function (e) {
  5310. if (!e) { e = window.event; }
  5311. var ctrlKey = isMac ? e.metaKey : e.ctrlKey;
  5312. if (!ctrlKey) {
  5313. if (e.preventDefault) { e.preventDefault(); }
  5314. return false;
  5315. }
  5316. },
  5317. _handleLoad: function (e) {
  5318. var state = this._getVisible();
  5319. if (state === "visible" || (state === "hidden" && isWebkit)) {
  5320. this._createView();
  5321. }
  5322. },
  5323. _handleMouse: function (e) {
  5324. var result = true;
  5325. var target = this._frameWindow;
  5326. if (isIE || (isFirefox && !this._overlayDiv)) { target = this._clientDiv; }
  5327. if (this._overlayDiv) {
  5328. if (this._hasFocus) {
  5329. this._ignoreFocus = true;
  5330. }
  5331. var self = this;
  5332. setTimeout(function () {
  5333. self.focus();
  5334. self._ignoreFocus = false;
  5335. }, 0);
  5336. }
  5337. if (this._clickCount === 1) {
  5338. result = this._setSelectionTo(e.clientX, e.clientY, e.shiftKey, !isOpera && this.isListening("DragStart"));
  5339. if (result) { this._setGrab(target); }
  5340. } else {
  5341. /*
  5342. * Feature in IE8 and older, the sequence of events in the IE8 event model
  5343. * for a doule-click is:
  5344. *
  5345. * down
  5346. * up
  5347. * up
  5348. * dblclick
  5349. *
  5350. * Given that the mouse down/up events are not balanced, it is not possible to
  5351. * grab on mouse down and ungrab on mouse up. The fix is to grab on the first
  5352. * mouse down and ungrab on mouse move when the button 1 is not set.
  5353. */
  5354. if (isW3CEvents) { this._setGrab(target); }
  5355. this._doubleClickSelection = null;
  5356. this._setSelectionTo(e.clientX, e.clientY, e.shiftKey);
  5357. this._doubleClickSelection = this._getSelection();
  5358. }
  5359. return result;
  5360. },
  5361. _handleMouseDown: function (e) {
  5362. if (!e) { e = window.event; }
  5363. if (this.isListening("MouseDown")) {
  5364. this.onMouseDown(this._createMouseEvent("MouseDown", e));
  5365. }
  5366. if (this._linksVisible) {
  5367. var target = e.target || e.srcElement;
  5368. if (target.tagName !== "A") {
  5369. this._setLinksVisible(false);
  5370. } else {
  5371. return;
  5372. }
  5373. }
  5374. this._commitIME();
  5375. var button = e.which; // 1 - left, 2 - middle, 3 - right
  5376. if (!button) {
  5377. // if IE 8 or older
  5378. if (e.button === 4) { button = 2; }
  5379. if (e.button === 2) { button = 3; }
  5380. if (e.button === 1) { button = 1; }
  5381. }
  5382. // For middle click we always need getTime(). See _getClipboardText().
  5383. var time = button !== 2 && e.timeStamp ? e.timeStamp : new Date().getTime();
  5384. var timeDiff = time - this._lastMouseTime;
  5385. var deltaX = Math.abs(this._lastMouseX - e.clientX);
  5386. var deltaY = Math.abs(this._lastMouseY - e.clientY);
  5387. var sameButton = this._lastMouseButton === button;
  5388. this._lastMouseX = e.clientX;
  5389. this._lastMouseY = e.clientY;
  5390. this._lastMouseTime = time;
  5391. this._lastMouseButton = button;
  5392. if (button === 1) {
  5393. this._isMouseDown = true;
  5394. if (sameButton && timeDiff <= this._clickTime && deltaX <= this._clickDist && deltaY <= this._clickDist) {
  5395. this._clickCount++;
  5396. } else {
  5397. this._clickCount = 1;
  5398. }
  5399. if (this._handleMouse(e) && (isOpera || isChrome || (isFirefox && !this._overlayDiv))) {
  5400. if (!this._hasFocus) {
  5401. this.focus();
  5402. }
  5403. e.preventDefault();
  5404. }
  5405. }
  5406. },
  5407. _handleMouseOver: function (e) {
  5408. if (!e) { e = window.event; }
  5409. if (this.isListening("MouseOver")) {
  5410. this.onMouseOver(this._createMouseEvent("MouseOver", e));
  5411. }
  5412. },
  5413. _handleMouseOut: function (e) {
  5414. if (!e) { e = window.event; }
  5415. if (this.isListening("MouseOut")) {
  5416. this.onMouseOut(this._createMouseEvent("MouseOut", e));
  5417. }
  5418. },
  5419. _handleMouseMove: function (e) {
  5420. if (!e) { e = window.event; }
  5421. if (this.isListening("MouseMove")) {
  5422. var topNode = this._overlayDiv || this._clientDiv;
  5423. var temp = e.target ? e.target : e.srcElement;
  5424. while (temp) {
  5425. if (topNode === temp) {
  5426. this.onMouseMove(this._createMouseEvent("MouseMove", e));
  5427. break;
  5428. }
  5429. temp = temp.parentNode;
  5430. }
  5431. }
  5432. if (this._dropTarget) {
  5433. return;
  5434. }
  5435. /*
  5436. * Bug in IE9. IE sends one mouse event when the user changes the text by
  5437. * pasting or undo. These operations usually happen with the Ctrl key
  5438. * down which causes the view to enter link mode. Link mode does not end
  5439. * because there are no further events. The fix is to only enter link
  5440. * mode when the coordinates of the mouse move event have changed.
  5441. */
  5442. var changed = this._linksVisible || this._lastMouseMoveX !== e.clientX || this._lastMouseMoveY !== e.clientY;
  5443. this._lastMouseMoveX = e.clientX;
  5444. this._lastMouseMoveY = e.clientY;
  5445. this._setLinksVisible(changed && !this._isMouseDown && (isMac ? e.metaKey : e.ctrlKey));
  5446. /*
  5447. * Feature in IE8 and older, the sequence of events in the IE8 event model
  5448. * for a doule-click is:
  5449. *
  5450. * down
  5451. * up
  5452. * up
  5453. * dblclick
  5454. *
  5455. * Given that the mouse down/up events are not balanced, it is not possible to
  5456. * grab on mouse down and ungrab on mouse up. The fix is to grab on the first
  5457. * mouse down and ungrab on mouse move when the button 1 is not set.
  5458. *
  5459. * In order to detect double-click and drag gestures, it is necessary to send
  5460. * a mouse down event from mouse move when the button is still down and isMouseDown
  5461. * flag is not set.
  5462. */
  5463. if (!isW3CEvents) {
  5464. if (e.button === 0) {
  5465. this._setGrab(null);
  5466. return true;
  5467. }
  5468. if (!this._isMouseDown && e.button === 1 && (this._clickCount & 1) !== 0) {
  5469. this._clickCount = 2;
  5470. return this._handleMouse(e, this._clickCount);
  5471. }
  5472. }
  5473. if (!this._isMouseDown || this._dragOffset !== -1) {
  5474. return;
  5475. }
  5476. var x = e.clientX;
  5477. var y = e.clientY;
  5478. if (isChrome) {
  5479. if (e.currentTarget !== this._frameWindow) {
  5480. var rect = this._frame.getBoundingClientRect();
  5481. x -= rect.left;
  5482. y -= rect.top;
  5483. }
  5484. }
  5485. var viewPad = this._getViewPadding();
  5486. var viewRect = this._viewDiv.getBoundingClientRect();
  5487. var width = this._getClientWidth (), height = this._getClientHeight();
  5488. var leftEdge = viewRect.left + viewPad.left;
  5489. var topEdge = viewRect.top + viewPad.top;
  5490. var rightEdge = viewRect.left + viewPad.left + width;
  5491. var bottomEdge = viewRect.top + viewPad.top + height;
  5492. var model = this._model;
  5493. var caretLine = model.getLineAtOffset(this._getSelection().getCaret());
  5494. if (y < topEdge && caretLine !== 0) {
  5495. this._doAutoScroll("up", x, y - topEdge);
  5496. } else if (y > bottomEdge && caretLine !== model.getLineCount() - 1) {
  5497. this._doAutoScroll("down", x, y - bottomEdge);
  5498. } else if (x < leftEdge) {
  5499. this._doAutoScroll("left", x - leftEdge, y);
  5500. } else if (x > rightEdge) {
  5501. this._doAutoScroll("right", x - rightEdge, y);
  5502. } else {
  5503. this._endAutoScroll();
  5504. this._setSelectionTo(x, y, true);
  5505. /*
  5506. * Feature in IE. IE does redraw the selection background right
  5507. * away after the selection changes because of mouse move events.
  5508. * The fix is to call getBoundingClientRect() on the
  5509. * body element to force the selection to be redraw. Some how
  5510. * calling this method forces a redraw.
  5511. */
  5512. if (isIE) {
  5513. var body = this._frameDocument.body;
  5514. body.getBoundingClientRect();
  5515. }
  5516. }
  5517. },
  5518. _createMouseEvent: function(type, e) {
  5519. var scroll = this._getScroll();
  5520. var viewRect = this._viewDiv.getBoundingClientRect();
  5521. var viewPad = this._getViewPadding();
  5522. var x = e.clientX + scroll.x - viewRect.left - viewPad.left;
  5523. var y = e.clientY + scroll.y - viewRect.top;
  5524. return {
  5525. type: type,
  5526. event: e,
  5527. x: x,
  5528. y: y
  5529. };
  5530. },
  5531. _handleMouseUp: function (e) {
  5532. if (!e) { e = window.event; }
  5533. if (this.isListening("MouseUp")) {
  5534. this.onMouseUp(this._createMouseEvent("MouseUp", e));
  5535. }
  5536. if (this._linksVisible) {
  5537. return;
  5538. }
  5539. var left = e.which ? e.button === 0 : e.button === 1;
  5540. if (left) {
  5541. if (this._dragOffset !== -1) {
  5542. var selection = this._getSelection();
  5543. selection.extend(this._dragOffset);
  5544. selection.collapse();
  5545. this._setSelection(selection, true, true);
  5546. this._dragOffset = -1;
  5547. }
  5548. this._isMouseDown = false;
  5549. this._endAutoScroll();
  5550. /*
  5551. * Feature in IE8 and older, the sequence of events in the IE8 event model
  5552. * for a doule-click is:
  5553. *
  5554. * down
  5555. * up
  5556. * up
  5557. * dblclick
  5558. *
  5559. * Given that the mouse down/up events are not balanced, it is not possible to
  5560. * grab on mouse down and ungrab on mouse up. The fix is to grab on the first
  5561. * mouse down and ungrab on mouse move when the button 1 is not set.
  5562. */
  5563. if (isW3CEvents) { this._setGrab(null); }
  5564. /*
  5565. * Note that there cases when Firefox sets the DOM selection in mouse up.
  5566. * This happens for example after a cancelled drag operation.
  5567. *
  5568. * Note that on Chrome and IE, the caret stops blicking if mouse up is
  5569. * prevented.
  5570. */
  5571. if (isFirefox) {
  5572. e.preventDefault();
  5573. }
  5574. }
  5575. },
  5576. _handleMouseWheel: function (e) {
  5577. if (!e) { e = window.event; }
  5578. var lineHeight = this._getLineHeight();
  5579. var pixelX = 0, pixelY = 0;
  5580. // Note: On the Mac the correct behaviour is to scroll by pixel.
  5581. if (isFirefox) {
  5582. var pixel;
  5583. if (isMac) {
  5584. pixel = e.detail * 3;
  5585. } else {
  5586. var limit = 256;
  5587. pixel = Math.max(-limit, Math.min(limit, e.detail)) * lineHeight;
  5588. }
  5589. if (e.axis === e.HORIZONTAL_AXIS) {
  5590. pixelX = pixel;
  5591. } else {
  5592. pixelY = pixel;
  5593. }
  5594. } else {
  5595. //Webkit
  5596. if (isMac) {
  5597. /*
  5598. * In Safari, the wheel delta is a multiple of 120. In order to
  5599. * convert delta to pixel values, it is necessary to divide delta
  5600. * by 40.
  5601. *
  5602. * In Chrome and Safari 5, the wheel delta depends on the type of the
  5603. * mouse. In general, it is the pixel value for Mac mice and track pads,
  5604. * but it is a multiple of 120 for other mice. There is no presise
  5605. * way to determine if it is pixel value or a multiple of 120.
  5606. *
  5607. * Note that the current approach does not calculate the correct
  5608. * pixel value for Mac mice when the delta is a multiple of 120.
  5609. */
  5610. var denominatorX = 40, denominatorY = 40;
  5611. if (e.wheelDeltaX % 120 !== 0) { denominatorX = 1; }
  5612. if (e.wheelDeltaY % 120 !== 0) { denominatorY = 1; }
  5613. pixelX = -e.wheelDeltaX / denominatorX;
  5614. if (-1 < pixelX && pixelX < 0) { pixelX = -1; }
  5615. if (0 < pixelX && pixelX < 1) { pixelX = 1; }
  5616. pixelY = -e.wheelDeltaY / denominatorY;
  5617. if (-1 < pixelY && pixelY < 0) { pixelY = -1; }
  5618. if (0 < pixelY && pixelY < 1) { pixelY = 1; }
  5619. } else {
  5620. pixelX = -e.wheelDeltaX;
  5621. var linesToScroll = 8;
  5622. pixelY = (-e.wheelDeltaY / 120 * linesToScroll) * lineHeight;
  5623. }
  5624. }
  5625. /*
  5626. * Feature in Safari. If the event target is removed from the DOM
  5627. * safari stops smooth scrolling. The fix is keep the element target
  5628. * in the DOM and remove it on a later time.
  5629. *
  5630. * Note: Using a timer is not a solution, because the timeout needs to
  5631. * be at least as long as the gesture (which is too long).
  5632. */
  5633. if (isSafari) {
  5634. var lineDiv = e.target;
  5635. while (lineDiv && lineDiv.lineIndex === undefined) {
  5636. lineDiv = lineDiv.parentNode;
  5637. }
  5638. this._mouseWheelLine = lineDiv;
  5639. }
  5640. var oldScroll = this._getScroll();
  5641. this._scrollView(pixelX, pixelY);
  5642. var newScroll = this._getScroll();
  5643. if (isSafari) { this._mouseWheelLine = null; }
  5644. if (oldScroll.x !== newScroll.x || oldScroll.y !== newScroll.y) {
  5645. if (e.preventDefault) { e.preventDefault(); }
  5646. return false;
  5647. }
  5648. },
  5649. _handlePaste: function (e) {
  5650. if (this._ignorePaste) { return; }
  5651. if (!e) { e = window.event; }
  5652. if (this._doPaste(e)) {
  5653. if (isIE) {
  5654. /*
  5655. * Bug in IE,
  5656. */
  5657. var self = this;
  5658. this._ignoreFocus = true;
  5659. setTimeout(function() {
  5660. self._updateDOMSelection();
  5661. this._ignoreFocus = false;
  5662. }, 0);
  5663. }
  5664. if (e.preventDefault) { e.preventDefault(); }
  5665. return false;
  5666. }
  5667. },
  5668. _handleResize: function (e) {
  5669. if (!e) { e = window.event; }
  5670. var element = this._frameDocument.documentElement;
  5671. var newWidth = element.clientWidth;
  5672. var newHeight = element.clientHeight;
  5673. if (this._frameWidth !== newWidth || this._frameHeight !== newHeight) {
  5674. this._frameWidth = newWidth;
  5675. this._frameHeight = newHeight;
  5676. /*
  5677. * Feature in IE7. For some reason, sometimes Internet Explorer 7
  5678. * returns incorrect values for element.getBoundingClientRect() when
  5679. * inside a resize handler. The fix is to queue the work.
  5680. */
  5681. if (isIE < 9) {
  5682. this._queueUpdatePage();
  5683. } else {
  5684. this._updatePage();
  5685. }
  5686. }
  5687. },
  5688. _handleRulerEvent: function (e) {
  5689. if (!e) { e = window.event; }
  5690. var target = e.target ? e.target : e.srcElement;
  5691. var lineIndex = target.lineIndex;
  5692. var element = target;
  5693. while (element && !element._ruler) {
  5694. if (lineIndex === undefined && element.lineIndex !== undefined) {
  5695. lineIndex = element.lineIndex;
  5696. }
  5697. element = element.parentNode;
  5698. }
  5699. var ruler = element ? element._ruler : null;
  5700. if (lineIndex === undefined && ruler && ruler.getOverview() === "document") {
  5701. var buttonHeight = isPad ? 0 : 17;
  5702. var clientHeight = this._getClientHeight ();
  5703. var lineCount = this._model.getLineCount ();
  5704. var viewPad = this._getViewPadding();
  5705. var trackHeight = clientHeight + viewPad.top + viewPad.bottom - 2 * buttonHeight;
  5706. lineIndex = Math.floor((e.clientY - buttonHeight) * lineCount / trackHeight);
  5707. if (!(0 <= lineIndex && lineIndex < lineCount)) {
  5708. lineIndex = undefined;
  5709. }
  5710. }
  5711. if (ruler) {
  5712. switch (e.type) {
  5713. case "click":
  5714. if (ruler.onClick) { ruler.onClick(lineIndex, e); }
  5715. break;
  5716. case "dblclick":
  5717. if (ruler.onDblClick) { ruler.onDblClick(lineIndex, e); }
  5718. break;
  5719. case "mousemove":
  5720. if (ruler.onMouseMove) { ruler.onMouseMove(lineIndex, e); }
  5721. break;
  5722. case "mouseover":
  5723. if (ruler.onMouseOver) { ruler.onMouseOver(lineIndex, e); }
  5724. break;
  5725. case "mouseout":
  5726. if (ruler.onMouseOut) { ruler.onMouseOut(lineIndex, e); }
  5727. break;
  5728. }
  5729. }
  5730. },
  5731. _handleScroll: function () {
  5732. var scroll = this._getScroll();
  5733. var oldX = this._hScroll;
  5734. var oldY = this._vScroll;
  5735. if (oldX !== scroll.x || oldY !== scroll.y) {
  5736. this._hScroll = scroll.x;
  5737. this._vScroll = scroll.y;
  5738. this._commitIME();
  5739. this._updatePage(oldY === scroll.y);
  5740. var e = {
  5741. type: "Scroll",
  5742. oldValue: {x: oldX, y: oldY},
  5743. newValue: scroll
  5744. };
  5745. this.onScroll(e);
  5746. }
  5747. },
  5748. _handleSelectStart: function (e) {
  5749. if (!e) { e = window.event; }
  5750. if (this._ignoreSelect) {
  5751. if (e && e.preventDefault) { e.preventDefault(); }
  5752. return false;
  5753. }
  5754. },
  5755. _handleUnload: function (e) {
  5756. if (!e) { e = window.event; }
  5757. this._destroyView();
  5758. },
  5759. _handleInput: function (e) {
  5760. var textArea = this._textArea;
  5761. this._doContent(textArea.value);
  5762. textArea.selectionStart = textArea.selectionEnd = 0;
  5763. textArea.value = "";
  5764. e.preventDefault();
  5765. },
  5766. _handleTextInput: function (e) {
  5767. this._doContent(e.data);
  5768. e.preventDefault();
  5769. },
  5770. _touchConvert: function (touch) {
  5771. var rect = this._frame.getBoundingClientRect();
  5772. var body = this._parentDocument.body;
  5773. return {left: touch.clientX - rect.left - body.scrollLeft, top: touch.clientY - rect.top - body.scrollTop};
  5774. },
  5775. _handleTextAreaClick: function (e) {
  5776. var pt = this._touchConvert(e);
  5777. this._clickCount = 1;
  5778. this._ignoreDOMSelection = false;
  5779. this._setSelectionTo(pt.left, pt.top, false);
  5780. var textArea = this._textArea;
  5781. textArea.focus();
  5782. },
  5783. _handleTouchStart: function (e) {
  5784. var touches = e.touches, touch, pt, sel;
  5785. this._touchMoved = false;
  5786. this._touchStartScroll = undefined;
  5787. if (touches.length === 1) {
  5788. touch = touches[0];
  5789. var pageX = touch.pageX;
  5790. var pageY = touch.pageY;
  5791. this._touchStartX = pageX;
  5792. this._touchStartY = pageY;
  5793. this._touchStartTime = e.timeStamp;
  5794. this._touchStartScroll = this._getScroll();
  5795. sel = this._getSelection();
  5796. pt = this._touchConvert(touches[0]);
  5797. this._touchGesture = "none";
  5798. if (!sel.isEmpty()) {
  5799. if (this._hitOffset(sel.end, pt.left, pt.top)) {
  5800. this._touchGesture = "extendEnd";
  5801. } else if (this._hitOffset(sel.start, pt.left, pt.top)) {
  5802. this._touchGesture = "extendStart";
  5803. }
  5804. }
  5805. if (this._touchGesture === "none") {
  5806. var textArea = this._textArea;
  5807. textArea.value = "";
  5808. textArea.style.left = "-1000px";
  5809. textArea.style.top = "-1000px";
  5810. textArea.style.width = "3000px";
  5811. textArea.style.height = "3000px";
  5812. }
  5813. } else if (touches.length === 2) {
  5814. this._touchGesture = "select";
  5815. if (this._touchTimeout) {
  5816. clearTimeout(this._touchTimeout);
  5817. this._touchTimeout = null;
  5818. }
  5819. pt = this._touchConvert(touches[0]);
  5820. var offset1 = this._getXToOffset(this._getYToLine(pt.top), pt.left);
  5821. pt = this._touchConvert(touches[1]);
  5822. var offset2 = this._getXToOffset(this._getYToLine(pt.top), pt.left);
  5823. sel = this._getSelection();
  5824. sel.setCaret(offset1);
  5825. sel.extend(offset2);
  5826. this._setSelection(sel, true, true);
  5827. }
  5828. //Cannot prevent to show magnifier
  5829. // e.preventDefault();
  5830. },
  5831. _handleTouchMove: function (e) {
  5832. this._touchMoved = true;
  5833. var touches = e.touches, pt, sel;
  5834. if (touches.length === 1) {
  5835. var touch = touches[0];
  5836. var pageX = touch.pageX;
  5837. var pageY = touch.pageY;
  5838. var deltaX = this._touchStartX - pageX;
  5839. var deltaY = this._touchStartY - pageY;
  5840. pt = this._touchConvert(touch);
  5841. sel = this._getSelection();
  5842. if (this._touchGesture === "none") {
  5843. if ((e.timeStamp - this._touchStartTime) < 200 && (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5)) {
  5844. this._touchGesture = "scroll";
  5845. } else {
  5846. this._touchGesture = "caret";
  5847. }
  5848. }
  5849. if (this._touchGesture === "select") {
  5850. if (this._hitOffset(sel.end, pt.left, pt.top)) {
  5851. this._touchGesture = "extendEnd";
  5852. } else if (this._hitOffset(sel.start, pt.left, pt.top)) {
  5853. this._touchGesture = "extendStart";
  5854. } else {
  5855. this._touchGesture = "caret";
  5856. }
  5857. }
  5858. switch (this._touchGesture) {
  5859. case "scroll":
  5860. this._touchStartX = pageX;
  5861. this._touchStartY = pageY;
  5862. this._scrollView(deltaX, deltaY);
  5863. break;
  5864. case "extendStart":
  5865. case "extendEnd":
  5866. this._clickCount = 1;
  5867. var lineIndex = this._getYToLine(pt.top);
  5868. var offset = this._getXToOffset(lineIndex, pt.left);
  5869. sel.setCaret(this._touchGesture === "extendStart" ? sel.end : sel.start);
  5870. sel.extend(offset);
  5871. if (offset >= sel.end && this._touchGesture === "extendStart") {
  5872. this._touchGesture = "extendEnd";
  5873. }
  5874. if (offset <= sel.start && this._touchGesture === "extendEnd") {
  5875. this._touchGesture = "extendStart";
  5876. }
  5877. this._setSelection(sel, true, true);
  5878. break;
  5879. case "caret":
  5880. this._setSelectionTo(pt.left, pt.top, false);
  5881. break;
  5882. }
  5883. } else if (touches.length === 2) {
  5884. pt = this._touchConvert(touches[0]);
  5885. var offset1 = this._getXToOffset(this._getYToLine(pt.top), pt.left);
  5886. pt = this._touchConvert(touches[1]);
  5887. var offset2 = this._getXToOffset(this._getYToLine(pt.top), pt.left);
  5888. sel = this._getSelection();
  5889. sel.setCaret(offset1);
  5890. sel.extend(offset2);
  5891. this._setSelection(sel, true, true);
  5892. }
  5893. e.preventDefault();
  5894. },
  5895. _handleTouchEnd: function (e) {
  5896. var self = this;
  5897. if (!this._touchMoved) {
  5898. if (e.touches.length === 0 && e.changedTouches.length === 1) {
  5899. var touch = e.changedTouches[0];
  5900. var pt = this._touchConvert(touch);
  5901. var textArea = this._textArea;
  5902. textArea.value = "";
  5903. textArea.style.left = "-1000px";
  5904. textArea.style.top = "-1000px";
  5905. textArea.style.width = "3000px";
  5906. textArea.style.height = "3000px";
  5907. setTimeout(function() {
  5908. self._clickCount = 1;
  5909. self._ignoreDOMSelection = false;
  5910. self._setSelectionTo(pt.left, pt.top, false);
  5911. }, 300);
  5912. }
  5913. }
  5914. if (e.touches.length === 0) {
  5915. setTimeout(function() {
  5916. var selection = self._getSelection();
  5917. var text = self._model.getText(selection.start, selection.end);
  5918. var textArea = self._textArea;
  5919. textArea.value = text;
  5920. textArea.selectionStart = 0;
  5921. textArea.selectionEnd = text.length;
  5922. if (!selection.isEmpty()) {
  5923. var touchRect = self._touchDiv.getBoundingClientRect();
  5924. var bounds = self._getOffsetBounds(selection.start);
  5925. textArea.style.left = (touchRect.width / 2) + "px";
  5926. textArea.style.top = ((bounds.top > 40 ? bounds.top - 30 : bounds.top + 30)) + "px";
  5927. }
  5928. }, 0);
  5929. }
  5930. // e.preventDefault();
  5931. },
  5932. /************************************ Actions ******************************************/
  5933. _doAction: function (e) {
  5934. var keyBindings = this._keyBindings;
  5935. for (var i = 0; i < keyBindings.length; i++) {
  5936. var kb = keyBindings[i];
  5937. if (kb.keyBinding.match(e)) {
  5938. if (kb.name) {
  5939. var actions = this._actions;
  5940. for (var j = 0; j < actions.length; j++) {
  5941. var a = actions[j];
  5942. if (a.name === kb.name) {
  5943. if (a.userHandler) {
  5944. if (!a.userHandler()) {
  5945. if (a.defaultHandler) {
  5946. a.defaultHandler();
  5947. } else {
  5948. return false;
  5949. }
  5950. }
  5951. } else if (a.defaultHandler) {
  5952. a.defaultHandler();
  5953. }
  5954. break;
  5955. }
  5956. }
  5957. }
  5958. return true;
  5959. }
  5960. }
  5961. return false;
  5962. },
  5963. _doBackspace: function (args) {
  5964. var selection = this._getSelection();
  5965. if (selection.isEmpty()) {
  5966. var model = this._model;
  5967. var caret = selection.getCaret();
  5968. var lineIndex = model.getLineAtOffset(caret);
  5969. var lineStart = model.getLineStart(lineIndex);
  5970. if (caret === lineStart) {
  5971. if (lineIndex > 0) {
  5972. selection.extend(model.getLineEnd(lineIndex - 1));
  5973. }
  5974. } else {
  5975. var removeTab = false;
  5976. if (this._expandTab && args.unit === "character" && (caret - lineStart) % this._tabSize === 0) {
  5977. var lineText = model.getText(lineStart, caret);
  5978. removeTab = !/[^ ]/.test(lineText); // Only spaces between line start and caret.
  5979. }
  5980. if (removeTab) {
  5981. selection.extend(caret - this._tabSize);
  5982. } else {
  5983. selection.extend(this._getOffset(caret, args.unit, -1));
  5984. }
  5985. }
  5986. }
  5987. this._modifyContent({text: "", start: selection.start, end: selection.end}, true);
  5988. return true;
  5989. },
  5990. _doContent: function (text) {
  5991. var selection = this._getSelection();
  5992. this._modifyContent({text: text, start: selection.start, end: selection.end, _ignoreDOMSelection: true}, true);
  5993. },
  5994. _doCopy: function (e) {
  5995. var selection = this._getSelection();
  5996. if (!selection.isEmpty()) {
  5997. var text = this._getBaseText(selection.start, selection.end);
  5998. return this._setClipboardText(text, e);
  5999. }
  6000. return true;
  6001. },
  6002. _doCursorNext: function (args) {
  6003. if (!args.select) {
  6004. if (this._clearSelection("next")) { return true; }
  6005. }
  6006. var model = this._model;
  6007. var selection = this._getSelection();
  6008. var caret = selection.getCaret();
  6009. var lineIndex = model.getLineAtOffset(caret);
  6010. if (caret === model.getLineEnd(lineIndex)) {
  6011. if (lineIndex + 1 < model.getLineCount()) {
  6012. selection.extend(model.getLineStart(lineIndex + 1));
  6013. }
  6014. } else {
  6015. selection.extend(this._getOffset(caret, args.unit, 1));
  6016. }
  6017. if (!args.select) { selection.collapse(); }
  6018. this._setSelection(selection, true);
  6019. return true;
  6020. },
  6021. _doCursorPrevious: function (args) {
  6022. if (!args.select) {
  6023. if (this._clearSelection("previous")) { return true; }
  6024. }
  6025. var model = this._model;
  6026. var selection = this._getSelection();
  6027. var caret = selection.getCaret();
  6028. var lineIndex = model.getLineAtOffset(caret);
  6029. if (caret === model.getLineStart(lineIndex)) {
  6030. if (lineIndex > 0) {
  6031. selection.extend(model.getLineEnd(lineIndex - 1));
  6032. }
  6033. } else {
  6034. selection.extend(this._getOffset(caret, args.unit, -1));
  6035. }
  6036. if (!args.select) { selection.collapse(); }
  6037. this._setSelection(selection, true);
  6038. return true;
  6039. },
  6040. _doCut: function (e) {
  6041. var selection = this._getSelection();
  6042. if (!selection.isEmpty()) {
  6043. var text = this._getBaseText(selection.start, selection.end);
  6044. this._doContent("");
  6045. return this._setClipboardText(text, e);
  6046. }
  6047. return true;
  6048. },
  6049. _doDelete: function (args) {
  6050. var selection = this._getSelection();
  6051. if (selection.isEmpty()) {
  6052. var model = this._model;
  6053. var caret = selection.getCaret();
  6054. var lineIndex = model.getLineAtOffset(caret);
  6055. if (caret === model.getLineEnd (lineIndex)) {
  6056. if (lineIndex + 1 < model.getLineCount()) {
  6057. selection.extend(model.getLineStart(lineIndex + 1));
  6058. }
  6059. } else {
  6060. selection.extend(this._getOffset(caret, args.unit, 1));
  6061. }
  6062. }
  6063. this._modifyContent({text: "", start: selection.start, end: selection.end}, true);
  6064. return true;
  6065. },
  6066. _doEnd: function (args) {
  6067. var selection = this._getSelection();
  6068. var model = this._model;
  6069. if (args.ctrl) {
  6070. selection.extend(model.getCharCount());
  6071. } else {
  6072. var lineIndex = model.getLineAtOffset(selection.getCaret());
  6073. selection.extend(model.getLineEnd(lineIndex));
  6074. }
  6075. if (!args.select) { selection.collapse(); }
  6076. this._setSelection(selection, true);
  6077. return true;
  6078. },
  6079. _doEnter: function (args) {
  6080. var model = this._model;
  6081. var selection = this._getSelection();
  6082. this._doContent(model.getLineDelimiter());
  6083. if (args && args.noCursor) {
  6084. selection.end = selection.start;
  6085. this._setSelection(selection);
  6086. }
  6087. return true;
  6088. },
  6089. _doHome: function (args) {
  6090. var selection = this._getSelection();
  6091. var model = this._model;
  6092. if (args.ctrl) {
  6093. selection.extend(0);
  6094. } else {
  6095. var lineIndex = model.getLineAtOffset(selection.getCaret());
  6096. selection.extend(model.getLineStart(lineIndex));
  6097. }
  6098. if (!args.select) { selection.collapse(); }
  6099. this._setSelection(selection, true);
  6100. return true;
  6101. },
  6102. _doLineDown: function (args) {
  6103. var model = this._model;
  6104. var selection = this._getSelection();
  6105. var caret = selection.getCaret();
  6106. var lineIndex = model.getLineAtOffset(caret);
  6107. if (lineIndex + 1 < model.getLineCount()) {
  6108. var scrollX = this._getScroll().x;
  6109. var x = this._columnX;
  6110. if (x === -1 || args.wholeLine || (args.select && isIE)) {
  6111. var offset = args.wholeLine ? model.getLineEnd(lineIndex + 1) : caret;
  6112. x = this._getOffsetToX(offset) + scrollX;
  6113. }
  6114. selection.extend(this._getXToOffset(lineIndex + 1, x - scrollX));
  6115. if (!args.select) { selection.collapse(); }
  6116. this._setSelection(selection, true, true);
  6117. this._columnX = x;
  6118. }
  6119. return true;
  6120. },
  6121. _doLineUp: function (args) {
  6122. var model = this._model;
  6123. var selection = this._getSelection();
  6124. var caret = selection.getCaret();
  6125. var lineIndex = model.getLineAtOffset(caret);
  6126. if (lineIndex > 0) {
  6127. var scrollX = this._getScroll().x;
  6128. var x = this._columnX;
  6129. if (x === -1 || args.wholeLine || (args.select && isIE)) {
  6130. var offset = args.wholeLine ? model.getLineStart(lineIndex - 1) : caret;
  6131. x = this._getOffsetToX(offset) + scrollX;
  6132. }
  6133. selection.extend(this._getXToOffset(lineIndex - 1, x - scrollX));
  6134. if (!args.select) { selection.collapse(); }
  6135. this._setSelection(selection, true, true);
  6136. this._columnX = x;
  6137. }
  6138. return true;
  6139. },
  6140. _doPageDown: function (args) {
  6141. var model = this._model;
  6142. var selection = this._getSelection();
  6143. var caret = selection.getCaret();
  6144. var caretLine = model.getLineAtOffset(caret);
  6145. var lineCount = model.getLineCount();
  6146. if (caretLine < lineCount - 1) {
  6147. var scroll = this._getScroll();
  6148. var clientHeight = this._getClientHeight();
  6149. var lineHeight = this._getLineHeight();
  6150. var lines = Math.floor(clientHeight / lineHeight);
  6151. var scrollLines = Math.min(lineCount - caretLine - 1, lines);
  6152. scrollLines = Math.max(1, scrollLines);
  6153. var x = this._columnX;
  6154. if (x === -1 || (args.select && isIE)) {
  6155. x = this._getOffsetToX(caret) + scroll.x;
  6156. }
  6157. selection.extend(this._getXToOffset(caretLine + scrollLines, x - scroll.x));
  6158. if (!args.select) { selection.collapse(); }
  6159. var verticalMaximum = lineCount * lineHeight;
  6160. var scrollOffset = scroll.y + scrollLines * lineHeight;
  6161. if (scrollOffset + clientHeight > verticalMaximum) {
  6162. scrollOffset = verticalMaximum - clientHeight;
  6163. }
  6164. this._setSelection(selection, true, true, scrollOffset - scroll.y);
  6165. this._columnX = x;
  6166. }
  6167. return true;
  6168. },
  6169. _doPageUp: function (args) {
  6170. var model = this._model;
  6171. var selection = this._getSelection();
  6172. var caret = selection.getCaret();
  6173. var caretLine = model.getLineAtOffset(caret);
  6174. if (caretLine > 0) {
  6175. var scroll = this._getScroll();
  6176. var clientHeight = this._getClientHeight();
  6177. var lineHeight = this._getLineHeight();
  6178. var lines = Math.floor(clientHeight / lineHeight);
  6179. var scrollLines = Math.max(1, Math.min(caretLine, lines));
  6180. var x = this._columnX;
  6181. if (x === -1 || (args.select && isIE)) {
  6182. x = this._getOffsetToX(caret) + scroll.x;
  6183. }
  6184. selection.extend(this._getXToOffset(caretLine - scrollLines, x - scroll.x));
  6185. if (!args.select) { selection.collapse(); }
  6186. var scrollOffset = Math.max(0, scroll.y - scrollLines * lineHeight);
  6187. this._setSelection(selection, true, true, scrollOffset - scroll.y);
  6188. this._columnX = x;
  6189. }
  6190. return true;
  6191. },
  6192. _doPaste: function(e) {
  6193. var self = this;
  6194. var result = this._getClipboardText(e, function(text) {
  6195. if (text) {
  6196. if (isLinux && self._lastMouseButton === 2) {
  6197. var timeDiff = new Date().getTime() - self._lastMouseTime;
  6198. if (timeDiff <= self._clickTime) {
  6199. self._setSelectionTo(self._lastMouseX, self._lastMouseY);
  6200. }
  6201. }
  6202. self._doContent(text);
  6203. }
  6204. });
  6205. return result !== null;
  6206. },
  6207. _doScroll: function (args) {
  6208. var type = args.type;
  6209. var model = this._model;
  6210. var lineCount = model.getLineCount();
  6211. var clientHeight = this._getClientHeight();
  6212. var lineHeight = this._getLineHeight();
  6213. var verticalMaximum = lineCount * lineHeight;
  6214. var verticalScrollOffset = this._getScroll().y;
  6215. var pixel;
  6216. switch (type) {
  6217. case "textStart": pixel = 0; break;
  6218. case "textEnd": pixel = verticalMaximum - clientHeight; break;
  6219. case "pageDown": pixel = verticalScrollOffset + clientHeight; break;
  6220. case "pageUp": pixel = verticalScrollOffset - clientHeight; break;
  6221. case "centerLine":
  6222. var selection = this._getSelection();
  6223. var lineStart = model.getLineAtOffset(selection.start);
  6224. var lineEnd = model.getLineAtOffset(selection.end);
  6225. var selectionHeight = (lineEnd - lineStart + 1) * lineHeight;
  6226. pixel = (lineStart * lineHeight) - (clientHeight / 2) + (selectionHeight / 2);
  6227. break;
  6228. }
  6229. if (pixel !== undefined) {
  6230. pixel = Math.min(Math.max(0, pixel), verticalMaximum - clientHeight);
  6231. this._scrollView(0, pixel - verticalScrollOffset);
  6232. }
  6233. },
  6234. _doSelectAll: function (args) {
  6235. var model = this._model;
  6236. var selection = this._getSelection();
  6237. selection.setCaret(0);
  6238. selection.extend(model.getCharCount());
  6239. this._setSelection(selection, false);
  6240. return true;
  6241. },
  6242. _doTab: function (args) {
  6243. var text = "\t";
  6244. if (this._expandTab) {
  6245. var model = this._model;
  6246. var caret = this._getSelection().getCaret();
  6247. var lineIndex = model.getLineAtOffset(caret);
  6248. var lineStart = model.getLineStart(lineIndex);
  6249. var spaces = this._tabSize - ((caret - lineStart) % this._tabSize);
  6250. text = (new Array(spaces + 1)).join(" ");
  6251. }
  6252. this._doContent(text);
  6253. return true;
  6254. },
  6255. /************************************ Internals ******************************************/
  6256. _applyStyle: function(style, node, reset) {
  6257. if (reset) {
  6258. var attrs = node.attributes;
  6259. for (var i= attrs.length; i-->0;) {
  6260. if (attrs[i].specified) {
  6261. node.removeAttributeNode(attrs[i]);
  6262. }
  6263. }
  6264. }
  6265. if (!style) {
  6266. return;
  6267. }
  6268. if (style.styleClass) {
  6269. node.className = style.styleClass;
  6270. }
  6271. var properties = style.style;
  6272. if (properties) {
  6273. for (var s in properties) {
  6274. if (properties.hasOwnProperty(s)) {
  6275. node.style[s] = properties[s];
  6276. }
  6277. }
  6278. }
  6279. var attributes = style.attributes;
  6280. if (attributes) {
  6281. for (var a in attributes) {
  6282. if (attributes.hasOwnProperty(a)) {
  6283. node.setAttribute(a, attributes[a]);
  6284. }
  6285. }
  6286. }
  6287. },
  6288. _autoScroll: function () {
  6289. var selection = this._getSelection();
  6290. var line;
  6291. var x = this._autoScrollX;
  6292. if (this._autoScrollDir === "up" || this._autoScrollDir === "down") {
  6293. var scroll = this._autoScrollY / this._getLineHeight();
  6294. scroll = scroll < 0 ? Math.floor(scroll) : Math.ceil(scroll);
  6295. line = this._model.getLineAtOffset(selection.getCaret());
  6296. line = Math.max(0, Math.min(this._model.getLineCount() - 1, line + scroll));
  6297. } else if (this._autoScrollDir === "left" || this._autoScrollDir === "right") {
  6298. line = this._getYToLine(this._autoScrollY);
  6299. x += this._getOffsetToX(selection.getCaret());
  6300. }
  6301. selection.extend(this._getXToOffset(line, x));
  6302. this._setSelection(selection, true);
  6303. },
  6304. _autoScrollTimer: function () {
  6305. this._autoScroll();
  6306. var self = this;
  6307. this._autoScrollTimerID = setTimeout(function () {self._autoScrollTimer();}, this._AUTO_SCROLL_RATE);
  6308. },
  6309. _calculateLineHeight: function() {
  6310. var parent = this._clientDiv;
  6311. var document = this._frameDocument;
  6312. var c = " ";
  6313. var line = document.createElement("DIV");
  6314. line.style.position = "fixed";
  6315. line.style.left = "-1000px";
  6316. var span1 = document.createElement("SPAN");
  6317. span1.appendChild(document.createTextNode(c));
  6318. line.appendChild(span1);
  6319. var span2 = document.createElement("SPAN");
  6320. span2.style.fontStyle = "italic";
  6321. span2.appendChild(document.createTextNode(c));
  6322. line.appendChild(span2);
  6323. var span3 = document.createElement("SPAN");
  6324. span3.style.fontWeight = "bold";
  6325. span3.appendChild(document.createTextNode(c));
  6326. line.appendChild(span3);
  6327. var span4 = document.createElement("SPAN");
  6328. span4.style.fontWeight = "bold";
  6329. span4.style.fontStyle = "italic";
  6330. span4.appendChild(document.createTextNode(c));
  6331. line.appendChild(span4);
  6332. parent.appendChild(line);
  6333. var lineRect = line.getBoundingClientRect();
  6334. var spanRect1 = span1.getBoundingClientRect();
  6335. var spanRect2 = span2.getBoundingClientRect();
  6336. var spanRect3 = span3.getBoundingClientRect();
  6337. var spanRect4 = span4.getBoundingClientRect();
  6338. var h1 = spanRect1.bottom - spanRect1.top;
  6339. var h2 = spanRect2.bottom - spanRect2.top;
  6340. var h3 = spanRect3.bottom - spanRect3.top;
  6341. var h4 = spanRect4.bottom - spanRect4.top;
  6342. var fontStyle = 0;
  6343. var lineHeight = lineRect.bottom - lineRect.top;
  6344. if (h2 > h1) {
  6345. fontStyle = 1;
  6346. }
  6347. if (h3 > h2) {
  6348. fontStyle = 2;
  6349. }
  6350. if (h4 > h3) {
  6351. fontStyle = 3;
  6352. }
  6353. var style;
  6354. if (fontStyle !== 0) {
  6355. style = {style: {}};
  6356. if ((fontStyle & 1) !== 0) {
  6357. style.style.fontStyle = "italic";
  6358. }
  6359. if ((fontStyle & 2) !== 0) {
  6360. style.style.fontWeight = "bold";
  6361. }
  6362. }
  6363. this._largestFontStyle = style;
  6364. parent.removeChild(line);
  6365. return lineHeight;
  6366. },
  6367. _calculatePadding: function() {
  6368. var document = this._frameDocument;
  6369. var parent = this._clientDiv;
  6370. var pad = this._getPadding(this._viewDiv);
  6371. var div1 = document.createElement("DIV");
  6372. div1.style.position = "fixed";
  6373. div1.style.left = "-1000px";
  6374. div1.style.paddingLeft = pad.left + "px";
  6375. div1.style.paddingTop = pad.top + "px";
  6376. div1.style.paddingRight = pad.right + "px";
  6377. div1.style.paddingBottom = pad.bottom + "px";
  6378. div1.style.width = "100px";
  6379. div1.style.height = "100px";
  6380. var div2 = document.createElement("DIV");
  6381. div2.style.width = "100%";
  6382. div2.style.height = "100%";
  6383. div1.appendChild(div2);
  6384. parent.appendChild(div1);
  6385. var rect1 = div1.getBoundingClientRect();
  6386. var rect2 = div2.getBoundingClientRect();
  6387. parent.removeChild(div1);
  6388. pad = {
  6389. left: rect2.left - rect1.left,
  6390. top: rect2.top - rect1.top,
  6391. right: rect1.right - rect2.right,
  6392. bottom: rect1.bottom - rect2.bottom
  6393. };
  6394. return pad;
  6395. },
  6396. _clearSelection: function (direction) {
  6397. var selection = this._getSelection();
  6398. if (selection.isEmpty()) { return false; }
  6399. if (direction === "next") {
  6400. selection.start = selection.end;
  6401. } else {
  6402. selection.end = selection.start;
  6403. }
  6404. this._setSelection(selection, true);
  6405. return true;
  6406. },
  6407. _clone: function (obj) {
  6408. /*Note that this code only works because of the limited types used in TextViewOptions */
  6409. if (obj instanceof Array) {
  6410. return obj.slice(0);
  6411. }
  6412. return obj;
  6413. },
  6414. _compare: function (s1, s2) {
  6415. if (s1 === s2) { return true; }
  6416. if (s1 && !s2 || !s1 && s2) { return false; }
  6417. if ((s1 && s1.constructor === String) || (s2 && s2.constructor === String)) { return false; }
  6418. if (s1 instanceof Array || s2 instanceof Array) {
  6419. if (!(s1 instanceof Array && s2 instanceof Array)) { return false; }
  6420. if (s1.length !== s2.length) { return false; }
  6421. for (var i = 0; i < s1.length; i++) {
  6422. if (!this._compare(s1[i], s2[i])) {
  6423. return false;
  6424. }
  6425. }
  6426. return true;
  6427. }
  6428. if (!(s1 instanceof Object) || !(s2 instanceof Object)) { return false; }
  6429. var p;
  6430. for (p in s1) {
  6431. if (s1.hasOwnProperty(p)) {
  6432. if (!s2.hasOwnProperty(p)) { return false; }
  6433. if (!this._compare(s1[p], s2[p])) {return false; }
  6434. }
  6435. }
  6436. for (p in s2) {
  6437. if (!s1.hasOwnProperty(p)) { return false; }
  6438. }
  6439. return true;
  6440. },
  6441. _commitIME: function () {
  6442. if (this._imeOffset === -1) { return; }
  6443. // make the state of the IME match the state the view expects it be in
  6444. // when the view commits the text and IME also need to be committed
  6445. // this can be accomplished by changing the focus around
  6446. this._scrollDiv.focus();
  6447. this._clientDiv.focus();
  6448. var model = this._model;
  6449. var lineIndex = model.getLineAtOffset(this._imeOffset);
  6450. var lineStart = model.getLineStart(lineIndex);
  6451. var newText = this._getDOMText(lineIndex);
  6452. var oldText = model.getLine(lineIndex);
  6453. var start = this._imeOffset - lineStart;
  6454. var end = start + newText.length - oldText.length;
  6455. if (start !== end) {
  6456. var insertText = newText.substring(start, end);
  6457. this._doContent(insertText);
  6458. }
  6459. this._imeOffset = -1;
  6460. },
  6461. _convertDelimiter: function (text, addTextFunc, addDelimiterFunc) {
  6462. var cr = 0, lf = 0, index = 0, length = text.length;
  6463. while (index < length) {
  6464. if (cr !== -1 && cr <= index) { cr = text.indexOf("\r", index); }
  6465. if (lf !== -1 && lf <= index) { lf = text.indexOf("\n", index); }
  6466. var start = index, end;
  6467. if (lf === -1 && cr === -1) {
  6468. addTextFunc(text.substring(index));
  6469. break;
  6470. }
  6471. if (cr !== -1 && lf !== -1) {
  6472. if (cr + 1 === lf) {
  6473. end = cr;
  6474. index = lf + 1;
  6475. } else {
  6476. end = cr < lf ? cr : lf;
  6477. index = (cr < lf ? cr : lf) + 1;
  6478. }
  6479. } else if (cr !== -1) {
  6480. end = cr;
  6481. index = cr + 1;
  6482. } else {
  6483. end = lf;
  6484. index = lf + 1;
  6485. }
  6486. addTextFunc(text.substring(start, end));
  6487. addDelimiterFunc();
  6488. }
  6489. },
  6490. _createActions: function () {
  6491. var KeyBinding = mKeyBinding.KeyBinding;
  6492. //no duplicate keybindings
  6493. var bindings = this._keyBindings = [];
  6494. // Cursor Navigation
  6495. bindings.push({name: "lineUp", keyBinding: new KeyBinding(38), predefined: true});
  6496. bindings.push({name: "lineDown", keyBinding: new KeyBinding(40), predefined: true});
  6497. bindings.push({name: "charPrevious", keyBinding: new KeyBinding(37), predefined: true});
  6498. bindings.push({name: "charNext", keyBinding: new KeyBinding(39), predefined: true});
  6499. if (isMac) {
  6500. bindings.push({name: "scrollPageUp", keyBinding: new KeyBinding(33), predefined: true});
  6501. bindings.push({name: "scrollPageDown", keyBinding: new KeyBinding(34), predefined: true});
  6502. bindings.push({name: "pageUp", keyBinding: new KeyBinding(33, null, null, true), predefined: true});
  6503. bindings.push({name: "pageDown", keyBinding: new KeyBinding(34, null, null, true), predefined: true});
  6504. bindings.push({name: "lineStart", keyBinding: new KeyBinding(37, true), predefined: true});
  6505. bindings.push({name: "lineEnd", keyBinding: new KeyBinding(39, true), predefined: true});
  6506. bindings.push({name: "wordPrevious", keyBinding: new KeyBinding(37, null, null, true), predefined: true});
  6507. bindings.push({name: "wordNext", keyBinding: new KeyBinding(39, null, null, true), predefined: true});
  6508. bindings.push({name: "scrollTextStart", keyBinding: new KeyBinding(36), predefined: true});
  6509. bindings.push({name: "scrollTextEnd", keyBinding: new KeyBinding(35), predefined: true});
  6510. bindings.push({name: "textStart", keyBinding: new KeyBinding(38, true), predefined: true});
  6511. bindings.push({name: "textEnd", keyBinding: new KeyBinding(40, true), predefined: true});
  6512. bindings.push({name: "scrollPageUp", keyBinding: new KeyBinding(38, null, null, null, true), predefined: true});
  6513. bindings.push({name: "scrollPageDown", keyBinding: new KeyBinding(40, null, null, null, true), predefined: true});
  6514. bindings.push({name: "lineStart", keyBinding: new KeyBinding(37, null, null, null, true), predefined: true});
  6515. bindings.push({name: "lineEnd", keyBinding: new KeyBinding(39, null, null, null, true), predefined: true});
  6516. //TODO These two actions should be changed to paragraph start and paragraph end when word wrap is implemented
  6517. bindings.push({name: "lineStart", keyBinding: new KeyBinding(38, null, null, true), predefined: true});
  6518. bindings.push({name: "lineEnd", keyBinding: new KeyBinding(40, null, null, true), predefined: true});
  6519. } else {
  6520. bindings.push({name: "pageUp", keyBinding: new KeyBinding(33), predefined: true});
  6521. bindings.push({name: "pageDown", keyBinding: new KeyBinding(34), predefined: true});
  6522. bindings.push({name: "lineStart", keyBinding: new KeyBinding(36), predefined: true});
  6523. bindings.push({name: "lineEnd", keyBinding: new KeyBinding(35), predefined: true});
  6524. bindings.push({name: "wordPrevious", keyBinding: new KeyBinding(37, true), predefined: true});
  6525. bindings.push({name: "wordNext", keyBinding: new KeyBinding(39, true), predefined: true});
  6526. bindings.push({name: "textStart", keyBinding: new KeyBinding(36, true), predefined: true});
  6527. bindings.push({name: "textEnd", keyBinding: new KeyBinding(35, true), predefined: true});
  6528. }
  6529. if (isFirefox && isLinux) {
  6530. bindings.push({name: "lineUp", keyBinding: new KeyBinding(38, true), predefined: true});
  6531. bindings.push({name: "lineDown", keyBinding: new KeyBinding(40, true), predefined: true});
  6532. }
  6533. // Select Cursor Navigation
  6534. bindings.push({name: "selectLineUp", keyBinding: new KeyBinding(38, null, true), predefined: true});
  6535. bindings.push({name: "selectLineDown", keyBinding: new KeyBinding(40, null, true), predefined: true});
  6536. bindings.push({name: "selectCharPrevious", keyBinding: new KeyBinding(37, null, true), predefined: true});
  6537. bindings.push({name: "selectCharNext", keyBinding: new KeyBinding(39, null, true), predefined: true});
  6538. bindings.push({name: "selectPageUp", keyBinding: new KeyBinding(33, null, true), predefined: true});
  6539. bindings.push({name: "selectPageDown", keyBinding: new KeyBinding(34, null, true), predefined: true});
  6540. if (isMac) {
  6541. bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(37, true, true), predefined: true});
  6542. bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(39, true, true), predefined: true});
  6543. bindings.push({name: "selectWordPrevious", keyBinding: new KeyBinding(37, null, true, true), predefined: true});
  6544. bindings.push({name: "selectWordNext", keyBinding: new KeyBinding(39, null, true, true), predefined: true});
  6545. bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(36, null, true), predefined: true});
  6546. bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(35, null, true), predefined: true});
  6547. bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(38, true, true), predefined: true});
  6548. bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(40, true, true), predefined: true});
  6549. bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(37, null, true, null, true), predefined: true});
  6550. bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(39, null, true, null, true), predefined: true});
  6551. //TODO These two actions should be changed to select paragraph start and select paragraph end when word wrap is implemented
  6552. bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(38, null, true, true), predefined: true});
  6553. bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(40, null, true, true), predefined: true});
  6554. } else {
  6555. if (isLinux) {
  6556. bindings.push({name: "selectWholeLineUp", keyBinding: new KeyBinding(38, true, true), predefined: true});
  6557. bindings.push({name: "selectWholeLineDown", keyBinding: new KeyBinding(40, true, true), predefined: true});
  6558. }
  6559. bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(36, null, true), predefined: true});
  6560. bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(35, null, true), predefined: true});
  6561. bindings.push({name: "selectWordPrevious", keyBinding: new KeyBinding(37, true, true), predefined: true});
  6562. bindings.push({name: "selectWordNext", keyBinding: new KeyBinding(39, true, true), predefined: true});
  6563. bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(36, true, true), predefined: true});
  6564. bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(35, true, true), predefined: true});
  6565. }
  6566. //Misc
  6567. bindings.push({name: "deletePrevious", keyBinding: new KeyBinding(8), predefined: true});
  6568. bindings.push({name: "deletePrevious", keyBinding: new KeyBinding(8, null, true), predefined: true});
  6569. bindings.push({name: "deleteNext", keyBinding: new KeyBinding(46), predefined: true});
  6570. bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, true), predefined: true});
  6571. bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, true, true), predefined: true});
  6572. bindings.push({name: "deleteWordNext", keyBinding: new KeyBinding(46, true), predefined: true});
  6573. bindings.push({name: "tab", keyBinding: new KeyBinding(9), predefined: true});
  6574. bindings.push({name: "enter", keyBinding: new KeyBinding(13), predefined: true});
  6575. bindings.push({name: "enter", keyBinding: new KeyBinding(13, null, true), predefined: true});
  6576. bindings.push({name: "selectAll", keyBinding: new KeyBinding('a', true), predefined: true});
  6577. if (isMac) {
  6578. bindings.push({name: "deleteNext", keyBinding: new KeyBinding(46, null, true), predefined: true});
  6579. bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, null, null, true), predefined: true});
  6580. bindings.push({name: "deleteWordNext", keyBinding: new KeyBinding(46, null, null, true), predefined: true});
  6581. }
  6582. /*
  6583. * Feature in IE/Chrome: prevent ctrl+'u', ctrl+'i', and ctrl+'b' from applying styles to the text.
  6584. *
  6585. * Note that Chrome applies the styles on the Mac with Ctrl instead of Cmd.
  6586. */
  6587. if (!isFirefox) {
  6588. var isMacChrome = isMac && isChrome;
  6589. bindings.push({name: null, keyBinding: new KeyBinding('u', !isMacChrome, false, false, isMacChrome), predefined: true});
  6590. bindings.push({name: null, keyBinding: new KeyBinding('i', !isMacChrome, false, false, isMacChrome), predefined: true});
  6591. bindings.push({name: null, keyBinding: new KeyBinding('b', !isMacChrome, false, false, isMacChrome), predefined: true});
  6592. }
  6593. if (isFirefox) {
  6594. bindings.push({name: "copy", keyBinding: new KeyBinding(45, true), predefined: true});
  6595. bindings.push({name: "paste", keyBinding: new KeyBinding(45, null, true), predefined: true});
  6596. bindings.push({name: "cut", keyBinding: new KeyBinding(46, null, true), predefined: true});
  6597. }
  6598. // Add the emacs Control+ ... key bindings.
  6599. if (isMac) {
  6600. bindings.push({name: "lineStart", keyBinding: new KeyBinding("a", false, false, false, true), predefined: true});
  6601. bindings.push({name: "lineEnd", keyBinding: new KeyBinding("e", false, false, false, true), predefined: true});
  6602. bindings.push({name: "lineUp", keyBinding: new KeyBinding("p", false, false, false, true), predefined: true});
  6603. bindings.push({name: "lineDown", keyBinding: new KeyBinding("n", false, false, false, true), predefined: true});
  6604. bindings.push({name: "charPrevious", keyBinding: new KeyBinding("b", false, false, false, true), predefined: true});
  6605. bindings.push({name: "charNext", keyBinding: new KeyBinding("f", false, false, false, true), predefined: true});
  6606. bindings.push({name: "deletePrevious", keyBinding: new KeyBinding("h", false, false, false, true), predefined: true});
  6607. bindings.push({name: "deleteNext", keyBinding: new KeyBinding("d", false, false, false, true), predefined: true});
  6608. bindings.push({name: "deleteLineEnd", keyBinding: new KeyBinding("k", false, false, false, true), predefined: true});
  6609. if (isFirefox) {
  6610. bindings.push({name: "scrollPageDown", keyBinding: new KeyBinding("v", false, false, false, true), predefined: true});
  6611. bindings.push({name: "deleteLineStart", keyBinding: new KeyBinding("u", false, false, false, true), predefined: true});
  6612. bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding("w", false, false, false, true), predefined: true});
  6613. } else {
  6614. bindings.push({name: "pageDown", keyBinding: new KeyBinding("v", false, false, false, true), predefined: true});
  6615. bindings.push({name: "centerLine", keyBinding: new KeyBinding("l", false, false, false, true), predefined: true});
  6616. bindings.push({name: "enterNoCursor", keyBinding: new KeyBinding("o", false, false, false, true), predefined: true});
  6617. //TODO implement: y (yank), t (transpose)
  6618. }
  6619. }
  6620. //1 to 1, no duplicates
  6621. var self = this;
  6622. this._actions = [
  6623. {name: "lineUp", defaultHandler: function() {return self._doLineUp({select: false});}},
  6624. {name: "lineDown", defaultHandler: function() {return self._doLineDown({select: false});}},
  6625. {name: "lineStart", defaultHandler: function() {return self._doHome({select: false, ctrl:false});}},
  6626. {name: "lineEnd", defaultHandler: function() {return self._doEnd({select: false, ctrl:false});}},
  6627. {name: "charPrevious", defaultHandler: function() {return self._doCursorPrevious({select: false, unit:"character"});}},
  6628. {name: "charNext", defaultHandler: function() {return self._doCursorNext({select: false, unit:"character"});}},
  6629. {name: "pageUp", defaultHandler: function() {return self._doPageUp({select: false});}},
  6630. {name: "pageDown", defaultHandler: function() {return self._doPageDown({select: false});}},
  6631. {name: "scrollPageUp", defaultHandler: function() {return self._doScroll({type: "pageUp"});}},
  6632. {name: "scrollPageDown", defaultHandler: function() {return self._doScroll({type: "pageDown"});}},
  6633. {name: "wordPrevious", defaultHandler: function() {return self._doCursorPrevious({select: false, unit:"word"});}},
  6634. {name: "wordNext", defaultHandler: function() {return self._doCursorNext({select: false, unit:"word"});}},
  6635. {name: "textStart", defaultHandler: function() {return self._doHome({select: false, ctrl:true});}},
  6636. {name: "textEnd", defaultHandler: function() {return self._doEnd({select: false, ctrl:true});}},
  6637. {name: "scrollTextStart", defaultHandler: function() {return self._doScroll({type: "textStart"});}},
  6638. {name: "scrollTextEnd", defaultHandler: function() {return self._doScroll({type: "textEnd"});}},
  6639. {name: "centerLine", defaultHandler: function() {return self._doScroll({type: "centerLine"});}},
  6640. {name: "selectLineUp", defaultHandler: function() {return self._doLineUp({select: true});}},
  6641. {name: "selectLineDown", defaultHandler: function() {return self._doLineDown({select: true});}},
  6642. {name: "selectWholeLineUp", defaultHandler: function() {return self._doLineUp({select: true, wholeLine: true});}},
  6643. {name: "selectWholeLineDown", defaultHandler: function() {return self._doLineDown({select: true, wholeLine: true});}},
  6644. {name: "selectLineStart", defaultHandler: function() {return self._doHome({select: true, ctrl:false});}},
  6645. {name: "selectLineEnd", defaultHandler: function() {return self._doEnd({select: true, ctrl:false});}},
  6646. {name: "selectCharPrevious", defaultHandler: function() {return self._doCursorPrevious({select: true, unit:"character"});}},
  6647. {name: "selectCharNext", defaultHandler: function() {return self._doCursorNext({select: true, unit:"character"});}},
  6648. {name: "selectPageUp", defaultHandler: function() {return self._doPageUp({select: true});}},
  6649. {name: "selectPageDown", defaultHandler: function() {return self._doPageDown({select: true});}},
  6650. {name: "selectWordPrevious", defaultHandler: function() {return self._doCursorPrevious({select: true, unit:"word"});}},
  6651. {name: "selectWordNext", defaultHandler: function() {return self._doCursorNext({select: true, unit:"word"});}},
  6652. {name: "selectTextStart", defaultHandler: function() {return self._doHome({select: true, ctrl:true});}},
  6653. {name: "selectTextEnd", defaultHandler: function() {return self._doEnd({select: true, ctrl:true});}},
  6654. {name: "deletePrevious", defaultHandler: function() {return self._doBackspace({unit:"character"});}},
  6655. {name: "deleteNext", defaultHandler: function() {return self._doDelete({unit:"character"});}},
  6656. {name: "deleteWordPrevious", defaultHandler: function() {return self._doBackspace({unit:"word"});}},
  6657. {name: "deleteWordNext", defaultHandler: function() {return self._doDelete({unit:"word"});}},
  6658. {name: "deleteLineStart", defaultHandler: function() {return self._doBackspace({unit: "line"});}},
  6659. {name: "deleteLineEnd", defaultHandler: function() {return self._doDelete({unit: "line"});}},
  6660. {name: "tab", defaultHandler: function() {return self._doTab();}},
  6661. {name: "enter", defaultHandler: function() {return self._doEnter();}},
  6662. {name: "enterNoCursor", defaultHandler: function() {return self._doEnter({noCursor:true});}},
  6663. {name: "selectAll", defaultHandler: function() {return self._doSelectAll();}},
  6664. {name: "copy", defaultHandler: function() {return self._doCopy();}},
  6665. {name: "cut", defaultHandler: function() {return self._doCut();}},
  6666. {name: "paste", defaultHandler: function() {return self._doPaste();}}
  6667. ];
  6668. },
  6669. _createLine: function(parent, div, document, lineIndex, model) {
  6670. var lineText = model.getLine(lineIndex);
  6671. var lineStart = model.getLineStart(lineIndex);
  6672. var e = {type:"LineStyle", textView: this, lineIndex: lineIndex, lineText: lineText, lineStart: lineStart};
  6673. this.onLineStyle(e);
  6674. var lineDiv = div || document.createElement("DIV");
  6675. if (!div || !this._compare(div.viewStyle, e.style)) {
  6676. this._applyStyle(e.style, lineDiv, div);
  6677. lineDiv.viewStyle = e.style;
  6678. }
  6679. lineDiv.lineIndex = lineIndex;
  6680. var ranges = [];
  6681. var data = {tabOffset: 0, ranges: ranges};
  6682. this._createRanges(e.ranges, lineText, 0, lineText.length, lineStart, data);
  6683. /*
  6684. * A trailing span with a whitespace is added for three different reasons:
  6685. * 1. Make sure the height of each line is the largest of the default font
  6686. * in normal, italic, bold, and italic-bold.
  6687. * 2. When full selection is off, Firefox, Opera and IE9 do not extend the
  6688. * selection at the end of the line when the line is fully selected.
  6689. * 3. The height of a div with only an empty span is zero.
  6690. */
  6691. var c = " ";
  6692. if (!this._fullSelection && isIE < 9) {
  6693. /*
  6694. * IE8 already selects extra space at end of a line fully selected,
  6695. * adding another space at the end of the line causes the selection
  6696. * to look too big. The fix is to use a zero-width space (\uFEFF) instead.
  6697. */
  6698. c = "\uFEFF";
  6699. }
  6700. if (isWebkit) {
  6701. /*
  6702. * Feature in WekKit. Adding a regular white space to the line will
  6703. * cause the longest line in the view to wrap even though "pre" is set.
  6704. * The fix is to use the zero-width non-joiner character (\u200C) instead.
  6705. * Note: To not use \uFEFF because in old version of Chrome this character
  6706. * shows a glyph;
  6707. */
  6708. c = "\u200C";
  6709. }
  6710. ranges.push({text: c, style: this._largestFontStyle, ignoreChars: 1});
  6711. var range, span, style, oldSpan, oldStyle, text, oldText, end = 0, oldEnd = 0, next;
  6712. var changeCount, changeStart;
  6713. if (div) {
  6714. var modelChangedEvent = div.modelChangedEvent;
  6715. if (modelChangedEvent) {
  6716. if (modelChangedEvent.removedLineCount === 0 && modelChangedEvent.addedLineCount === 0) {
  6717. changeStart = modelChangedEvent.start - lineStart;
  6718. changeCount = modelChangedEvent.addedCharCount - modelChangedEvent.removedCharCount;
  6719. } else {
  6720. changeStart = -1;
  6721. }
  6722. div.modelChangedEvent = undefined;
  6723. }
  6724. oldSpan = div.firstChild;
  6725. }
  6726. for (var i = 0; i < ranges.length; i++) {
  6727. range = ranges[i];
  6728. text = range.text;
  6729. end += text.length;
  6730. style = range.style;
  6731. if (oldSpan) {
  6732. oldText = oldSpan.firstChild.data;
  6733. oldStyle = oldSpan.viewStyle;
  6734. if (oldText === text && this._compare(style, oldStyle)) {
  6735. oldEnd += oldText.length;
  6736. oldSpan._rectsCache = undefined;
  6737. span = oldSpan = oldSpan.nextSibling;
  6738. continue;
  6739. } else {
  6740. while (oldSpan) {
  6741. if (changeStart !== -1) {
  6742. var spanEnd = end;
  6743. if (spanEnd >= changeStart) {
  6744. spanEnd -= changeCount;
  6745. }
  6746. var length = oldSpan.firstChild.data.length;
  6747. if (oldEnd + length > spanEnd) { break; }
  6748. oldEnd += length;
  6749. }
  6750. next = oldSpan.nextSibling;
  6751. lineDiv.removeChild(oldSpan);
  6752. oldSpan = next;
  6753. }
  6754. }
  6755. }
  6756. span = this._createSpan(lineDiv, document, text, style, range.ignoreChars);
  6757. if (oldSpan) {
  6758. lineDiv.insertBefore(span, oldSpan);
  6759. } else {
  6760. lineDiv.appendChild(span);
  6761. }
  6762. if (div) {
  6763. div.lineWidth = undefined;
  6764. }
  6765. }
  6766. if (div) {
  6767. var tmp = span ? span.nextSibling : null;
  6768. while (tmp) {
  6769. next = tmp.nextSibling;
  6770. div.removeChild(tmp);
  6771. tmp = next;
  6772. }
  6773. } else {
  6774. parent.appendChild(lineDiv);
  6775. }
  6776. return lineDiv;
  6777. },
  6778. _createRanges: function(ranges, text, start, end, lineStart, data) {
  6779. if (start >= end) { return; }
  6780. if (ranges) {
  6781. for (var i = 0; i < ranges.length; i++) {
  6782. var range = ranges[i];
  6783. if (range.end <= lineStart + start) { continue; }
  6784. var styleStart = Math.max(lineStart + start, range.start) - lineStart;
  6785. if (styleStart >= end) { break; }
  6786. var styleEnd = Math.min(lineStart + end, range.end) - lineStart;
  6787. if (styleStart < styleEnd) {
  6788. styleStart = Math.max(start, styleStart);
  6789. styleEnd = Math.min(end, styleEnd);
  6790. if (start < styleStart) {
  6791. this._createRange(text, start, styleStart, null, data);
  6792. }
  6793. while (i + 1 < ranges.length && ranges[i + 1].start - lineStart === styleEnd && this._compare(range.style, ranges[i + 1].style)) {
  6794. range = ranges[i + 1];
  6795. styleEnd = Math.min(lineStart + end, range.end) - lineStart;
  6796. i++;
  6797. }
  6798. this._createRange(text, styleStart, styleEnd, range.style, data);
  6799. start = styleEnd;
  6800. }
  6801. }
  6802. }
  6803. if (start < end) {
  6804. this._createRange(text, start, end, null, data);
  6805. }
  6806. },
  6807. _createRange: function(text, start, end, style, data) {
  6808. if (start >= end) { return; }
  6809. var tabSize = this._customTabSize, range;
  6810. if (tabSize && tabSize !== 8) {
  6811. var tabIndex = text.indexOf("\t", start);
  6812. while (tabIndex !== -1 && tabIndex < end) {
  6813. if (start < tabIndex) {
  6814. range = {text: text.substring(start, tabIndex), style: style};
  6815. data.ranges.push(range);
  6816. data.tabOffset += range.text.length;
  6817. }
  6818. var spacesCount = tabSize - (data.tabOffset % tabSize);
  6819. if (spacesCount > 0) {
  6820. //TODO hack to preserve text length in getDOMText()
  6821. var spaces = "\u00A0";
  6822. for (var i = 1; i < spacesCount; i++) {
  6823. spaces += " ";
  6824. }
  6825. range = {text: spaces, style: style, ignoreChars: spacesCount - 1};
  6826. data.ranges.push(range);
  6827. data.tabOffset += range.text.length;
  6828. }
  6829. start = tabIndex + 1;
  6830. tabIndex = text.indexOf("\t", start);
  6831. }
  6832. }
  6833. if (start < end) {
  6834. range = {text: text.substring(start, end), style: style};
  6835. data.ranges.push(range);
  6836. data.tabOffset += range.text.length;
  6837. }
  6838. },
  6839. _createSpan: function(parent, document, text, style, ignoreChars) {
  6840. var isLink = style && style.tagName === "A";
  6841. if (isLink) { parent.hasLink = true; }
  6842. var tagName = isLink && this._linksVisible ? "A" : "SPAN";
  6843. var child = document.createElement(tagName);
  6844. child.appendChild(document.createTextNode(text));
  6845. this._applyStyle(style, child);
  6846. if (tagName === "A") {
  6847. var self = this;
  6848. addHandler(child, "click", function(e) { return self._handleLinkClick(e); }, false);
  6849. }
  6850. child.viewStyle = style;
  6851. if (ignoreChars) {
  6852. child.ignoreChars = ignoreChars;
  6853. }
  6854. return child;
  6855. },
  6856. _createRuler: function(ruler) {
  6857. if (!this._clientDiv) { return; }
  6858. var document = this._frameDocument;
  6859. var body = document.body;
  6860. var side = ruler.getLocation();
  6861. var rulerParent = side === "left" ? this._leftDiv : this._rightDiv;
  6862. if (!rulerParent) {
  6863. rulerParent = document.createElement("DIV");
  6864. rulerParent.style.overflow = "hidden";
  6865. rulerParent.style.MozUserSelect = "none";
  6866. rulerParent.style.WebkitUserSelect = "none";
  6867. if (isIE) {
  6868. rulerParent.attachEvent("onselectstart", function() {return false;});
  6869. }
  6870. rulerParent.style.position = "absolute";
  6871. rulerParent.style.top = "0px";
  6872. rulerParent.style.cursor = "default";
  6873. body.appendChild(rulerParent);
  6874. if (side === "left") {
  6875. this._leftDiv = rulerParent;
  6876. rulerParent.className = "viewLeftRuler";
  6877. } else {
  6878. this._rightDiv = rulerParent;
  6879. rulerParent.className = "viewRightRuler";
  6880. }
  6881. var table = document.createElement("TABLE");
  6882. rulerParent.appendChild(table);
  6883. table.cellPadding = "0px";
  6884. table.cellSpacing = "0px";
  6885. table.border = "0px";
  6886. table.insertRow(0);
  6887. var self = this;
  6888. addHandler(rulerParent, "click", function(e) { self._handleRulerEvent(e); });
  6889. addHandler(rulerParent, "dblclick", function(e) { self._handleRulerEvent(e); });
  6890. addHandler(rulerParent, "mousemove", function(e) { self._handleRulerEvent(e); });
  6891. addHandler(rulerParent, "mouseover", function(e) { self._handleRulerEvent(e); });
  6892. addHandler(rulerParent, "mouseout", function(e) { self._handleRulerEvent(e); });
  6893. }
  6894. var div = document.createElement("DIV");
  6895. div._ruler = ruler;
  6896. div.rulerChanged = true;
  6897. div.style.position = "relative";
  6898. var row = rulerParent.firstChild.rows[0];
  6899. var index = row.cells.length;
  6900. var cell = row.insertCell(index);
  6901. cell.vAlign = "top";
  6902. cell.appendChild(div);
  6903. },
  6904. _createFrame: function() {
  6905. if (this.frame) { return; }
  6906. var parent = this._parent;
  6907. while (parent.hasChildNodes()) { parent.removeChild(parent.lastChild); }
  6908. var parentDocument = parent.ownerDocument;
  6909. this._parentDocument = parentDocument;
  6910. var frame = parentDocument.createElement("IFRAME");
  6911. this._frame = frame;
  6912. frame.frameBorder = "0px";//for IE, needs to be set before the frame is added to the parent
  6913. frame.style.border = "0px";
  6914. frame.style.width = "100%";
  6915. frame.style.height = "100%";
  6916. frame.scrolling = "no";
  6917. var self = this;
  6918. /*
  6919. * Note that it is not possible to create the contents of the frame if the
  6920. * parent is not connected to the document. Only create it when the load
  6921. * event is trigged.
  6922. */
  6923. this._loadHandler = function(e) {
  6924. self._handleLoad(e);
  6925. };
  6926. addHandler(frame, "load", this._loadHandler, !!isFirefox);
  6927. if (!isWebkit) {
  6928. /*
  6929. * Feature in IE and Firefox. It is not possible to get the style of an
  6930. * element if it is not layed out because one of the ancestor has
  6931. * style.display = none. This means that the view cannot be created in this
  6932. * situations, since no measuring can be performed. The fix is to listen
  6933. * for DOMAttrModified and create or destroy the view when the style.display
  6934. * attribute changes.
  6935. */
  6936. addHandler(parentDocument, "DOMAttrModified", this._attrModifiedHandler = function(e) {
  6937. self._handleDOMAttrModified(e);
  6938. });
  6939. }
  6940. parent.appendChild(frame);
  6941. /* create synchronously if possible */
  6942. if (this._sync) {
  6943. this._handleLoad();
  6944. }
  6945. },
  6946. _getFrameHTML: function() {
  6947. var html = [];
  6948. html.push("<!DOCTYPE html>");
  6949. html.push("<html>");
  6950. html.push("<head>");
  6951. if (isIE < 9) {
  6952. html.push("<meta http-equiv='X-UA-Compatible' content='IE=EmulateIE7'/>");
  6953. }
  6954. html.push("<style>");
  6955. html.push(".viewContainer {font-family: monospace; font-size: 10pt;}");
  6956. html.push(".view {padding: 1px 2px;}");
  6957. html.push(".viewContent {}");
  6958. html.push("</style>");
  6959. if (this._stylesheet) {
  6960. var stylesheet = typeof(this._stylesheet) === "string" ? [this._stylesheet] : this._stylesheet;
  6961. for (var i = 0; i < stylesheet.length; i++) {
  6962. var sheet = stylesheet[i];
  6963. var isLink = this._isLinkURL(sheet);
  6964. if (isLink && this._sync) {
  6965. try {
  6966. var objXml = new XMLHttpRequest();
  6967. if (objXml.overrideMimeType) {
  6968. objXml.overrideMimeType("text/css");
  6969. }
  6970. objXml.open("GET", sheet, false);
  6971. objXml.send(null);
  6972. sheet = objXml.responseText;
  6973. isLink = false;
  6974. } catch (e) {}
  6975. }
  6976. if (isLink) {
  6977. html.push("<link rel='stylesheet' type='text/css' ");
  6978. /*
  6979. * Bug in IE7. The window load event is not sent unless a load handler is added to the link node.
  6980. */
  6981. if (isIE < 9) {
  6982. html.push("onload='window' ");
  6983. }
  6984. html.push("href='");
  6985. html.push(sheet);
  6986. html.push("'></link>");
  6987. } else {
  6988. html.push("<style>");
  6989. html.push(sheet);
  6990. html.push("</style>");
  6991. }
  6992. }
  6993. }
  6994. /*
  6995. * Feature in WebKit. In WebKit, window load will not wait for the style sheets
  6996. * to be loaded unless there is script element after the style sheet link elements.
  6997. */
  6998. html.push("<script>");
  6999. html.push("var waitForStyleSheets = true;");
  7000. html.push("</script>");
  7001. html.push("</head>");
  7002. html.push("<body spellcheck='false'></body>");
  7003. html.push("</html>");
  7004. return html.join("");
  7005. },
  7006. _createView: function() {
  7007. if (this._frameDocument) { return; }
  7008. var frameWindow = this._frameWindow = this._frame.contentWindow;
  7009. var frameDocument = this._frameDocument = frameWindow.document;
  7010. var self = this;
  7011. function write() {
  7012. frameDocument.open("text/html", "replace");
  7013. frameDocument.write(self._getFrameHTML());
  7014. frameDocument.close();
  7015. self._windowLoadHandler = function(e) {
  7016. /*
  7017. * Bug in Safari. Safari sends the window load event before the
  7018. * style sheets are loaded. The fix is to defer creation of the
  7019. * contents until the document readyState changes to complete.
  7020. */
  7021. if (self._isDocumentReady()) {
  7022. self._createContent();
  7023. }
  7024. };
  7025. addHandler(frameWindow, "load", self._windowLoadHandler);
  7026. }
  7027. write();
  7028. if (this._sync) {
  7029. this._createContent();
  7030. } else {
  7031. /*
  7032. * Bug in Webkit. Webkit does not send the load event for the iframe window when the main page
  7033. * loads as a result of backward or forward navigation.
  7034. * The fix is to use a timer to create the content only when the document is ready.
  7035. */
  7036. this._createViewTimer = function() {
  7037. if (self._clientDiv) { return; }
  7038. if (self._isDocumentReady()) {
  7039. self._createContent();
  7040. } else {
  7041. setTimeout(self._createViewTimer, 10);
  7042. }
  7043. };
  7044. setTimeout(this._createViewTimer, 10);
  7045. }
  7046. },
  7047. _isDocumentReady: function() {
  7048. var frameDocument = this._frameDocument;
  7049. if (!frameDocument) { return false; }
  7050. if (frameDocument.readyState === "complete") {
  7051. return true;
  7052. } else if (frameDocument.readyState === "interactive" && isFirefox) {
  7053. /*
  7054. * Bug in Firefox. Firefox does not change the document ready state to complete
  7055. * all the time. The fix is to wait for the ready state to be "interactive" and check that
  7056. * all css rules are initialized.
  7057. */
  7058. var styleSheets = frameDocument.styleSheets;
  7059. var styleSheetCount = 1;
  7060. if (this._stylesheet) {
  7061. styleSheetCount += typeof(this._stylesheet) === "string" ? 1 : this._stylesheet.length;
  7062. }
  7063. if (styleSheetCount === styleSheets.length) {
  7064. var index = 0;
  7065. while (index < styleSheets.length) {
  7066. var count = 0;
  7067. try {
  7068. count = styleSheets.item(index).cssRules.length;
  7069. } catch (ex) {
  7070. /*
  7071. * Feature in Firefox. To determine if a stylesheet is loaded the number of css rules is used, if the
  7072. * stylesheet is not loaded this operation will throw an invalid access error. When a stylesheet from
  7073. * a different domain is loaded, accessing the css rules will result in a security exception. In this
  7074. * case count is set to 1 to indicate the stylesheet is loaded.
  7075. */
  7076. if (ex.code !== DOMException.INVALID_ACCESS_ERR) {
  7077. count = 1;
  7078. }
  7079. }
  7080. if (count === 0) { break; }
  7081. index++;
  7082. }
  7083. return index === styleSheets.length;
  7084. }
  7085. }
  7086. return false;
  7087. },
  7088. _createContent: function() {
  7089. if (this._clientDiv) { return; }
  7090. var parent = this._parent;
  7091. var parentDocument = this._parentDocument;
  7092. var frameDocument = this._frameDocument;
  7093. var body = frameDocument.body;
  7094. this._setThemeClass(this._themeClass, true);
  7095. body.style.margin = "0px";
  7096. body.style.borderWidth = "0px";
  7097. body.style.padding = "0px";
  7098. var textArea;
  7099. if (isPad) {
  7100. var touchDiv = parentDocument.createElement("DIV");
  7101. this._touchDiv = touchDiv;
  7102. touchDiv.style.position = "absolute";
  7103. touchDiv.style.border = "0px";
  7104. touchDiv.style.padding = "0px";
  7105. touchDiv.style.margin = "0px";
  7106. touchDiv.style.zIndex = "2";
  7107. touchDiv.style.overflow = "hidden";
  7108. touchDiv.style.background="transparent";
  7109. touchDiv.style.WebkitUserSelect = "none";
  7110. parent.appendChild(touchDiv);
  7111. textArea = parentDocument.createElement("TEXTAREA");
  7112. this._textArea = textArea;
  7113. textArea.style.position = "absolute";
  7114. textArea.style.whiteSpace = "pre";
  7115. textArea.style.left = "-1000px";
  7116. textArea.tabIndex = 1;
  7117. textArea.autocapitalize = "off";
  7118. textArea.autocorrect = "off";
  7119. textArea.className = "viewContainer";
  7120. textArea.style.background = "transparent";
  7121. textArea.style.color = "transparent";
  7122. textArea.style.border = "0px";
  7123. textArea.style.padding = "0px";
  7124. textArea.style.margin = "0px";
  7125. textArea.style.borderRadius = "0px";
  7126. textArea.style.WebkitAppearance = "none";
  7127. textArea.style.WebkitTapHighlightColor = "transparent";
  7128. touchDiv.appendChild(textArea);
  7129. }
  7130. if (isFirefox) {
  7131. var clipboardDiv = frameDocument.createElement("DIV");
  7132. this._clipboardDiv = clipboardDiv;
  7133. clipboardDiv.style.position = "fixed";
  7134. clipboardDiv.style.whiteSpace = "pre";
  7135. clipboardDiv.style.left = "-1000px";
  7136. body.appendChild(clipboardDiv);
  7137. }
  7138. var viewDiv = frameDocument.createElement("DIV");
  7139. viewDiv.className = "view";
  7140. this._viewDiv = viewDiv;
  7141. viewDiv.id = "viewDiv";
  7142. viewDiv.tabIndex = -1;
  7143. viewDiv.style.overflow = "auto";
  7144. viewDiv.style.position = "absolute";
  7145. viewDiv.style.top = "0px";
  7146. viewDiv.style.borderWidth = "0px";
  7147. viewDiv.style.margin = "0px";
  7148. viewDiv.style.outline = "none";
  7149. body.appendChild(viewDiv);
  7150. var scrollDiv = frameDocument.createElement("DIV");
  7151. this._scrollDiv = scrollDiv;
  7152. scrollDiv.id = "scrollDiv";
  7153. scrollDiv.style.margin = "0px";
  7154. scrollDiv.style.borderWidth = "0px";
  7155. scrollDiv.style.padding = "0px";
  7156. viewDiv.appendChild(scrollDiv);
  7157. if (isFirefox) {
  7158. var clipDiv = frameDocument.createElement("DIV");
  7159. this._clipDiv = clipDiv;
  7160. clipDiv.id = "clipDiv";
  7161. clipDiv.style.position = "fixed";
  7162. clipDiv.style.overflow = "hidden";
  7163. clipDiv.style.margin = "0px";
  7164. clipDiv.style.borderWidth = "0px";
  7165. clipDiv.style.padding = "0px";
  7166. scrollDiv.appendChild(clipDiv);
  7167. var clipScrollDiv = frameDocument.createElement("DIV");
  7168. this._clipScrollDiv = clipScrollDiv;
  7169. clipScrollDiv.id = "clipScrollDiv";
  7170. clipScrollDiv.style.position = "absolute";
  7171. clipScrollDiv.style.height = "1px";
  7172. clipScrollDiv.style.top = "-1000px";
  7173. clipDiv.appendChild(clipScrollDiv);
  7174. }
  7175. this._setFullSelection(this._fullSelection, true);
  7176. var clientDiv = frameDocument.createElement("DIV");
  7177. clientDiv.className = "viewContent";
  7178. this._clientDiv = clientDiv;
  7179. clientDiv.id = "clientDiv";
  7180. clientDiv.style.whiteSpace = "pre";
  7181. clientDiv.style.position = this._clipDiv ? "absolute" : "fixed";
  7182. clientDiv.style.borderWidth = "0px";
  7183. clientDiv.style.margin = "0px";
  7184. clientDiv.style.padding = "0px";
  7185. clientDiv.style.outline = "none";
  7186. clientDiv.style.zIndex = "1";
  7187. if (isPad) {
  7188. clientDiv.style.WebkitTapHighlightColor = "transparent";
  7189. }
  7190. (this._clipDiv || scrollDiv).appendChild(clientDiv);
  7191. if (isFirefox && !clientDiv.setCapture) {
  7192. var overlayDiv = frameDocument.createElement("DIV");
  7193. this._overlayDiv = overlayDiv;
  7194. overlayDiv.id = "overlayDiv";
  7195. overlayDiv.style.position = clientDiv.style.position;
  7196. overlayDiv.style.borderWidth = clientDiv.style.borderWidth;
  7197. overlayDiv.style.margin = clientDiv.style.margin;
  7198. overlayDiv.style.padding = clientDiv.style.padding;
  7199. overlayDiv.style.cursor = "text";
  7200. overlayDiv.style.zIndex = "2";
  7201. (this._clipDiv || scrollDiv).appendChild(overlayDiv);
  7202. }
  7203. if (!isPad) {
  7204. clientDiv.contentEditable = "true";
  7205. }
  7206. this._lineHeight = this._calculateLineHeight();
  7207. this._viewPadding = this._calculatePadding();
  7208. if (isIE) {
  7209. body.style.lineHeight = this._lineHeight + "px";
  7210. }
  7211. this._setTabSize(this._tabSize, true);
  7212. this._hookEvents();
  7213. var rulers = this._rulers;
  7214. for (var i=0; i<rulers.length; i++) {
  7215. this._createRuler(rulers[i]);
  7216. }
  7217. this._updatePage();
  7218. var h = this._hScroll, v = this._vScroll;
  7219. this._vScroll = this._hScroll = 0;
  7220. if (h > 0 || v > 0) {
  7221. viewDiv.scrollLeft = h;
  7222. viewDiv.scrollTop = v;
  7223. }
  7224. this.onLoad({type: "Load"});
  7225. },
  7226. _defaultOptions: function() {
  7227. return {
  7228. parent: {value: undefined, recreate: true, update: null},
  7229. model: {value: undefined, recreate: false, update: this.setModel},
  7230. readonly: {value: false, recreate: false, update: null},
  7231. fullSelection: {value: true, recreate: false, update: this._setFullSelection},
  7232. tabSize: {value: 8, recreate: false, update: this._setTabSize},
  7233. expandTab: {value: false, recreate: false, update: null},
  7234. stylesheet: {value: [], recreate: false, update: this._setStyleSheet},
  7235. themeClass: {value: undefined, recreate: false, update: this._setThemeClass},
  7236. sync: {value: false, recreate: false, update: null}
  7237. };
  7238. },
  7239. _destroyFrame: function() {
  7240. var frame = this._frame;
  7241. if (!frame) { return; }
  7242. if (this._loadHandler) {
  7243. removeHandler(frame, "load", this._loadHandler, !!isFirefox);
  7244. this._loadHandler = null;
  7245. }
  7246. if (this._attrModifiedHandler) {
  7247. removeHandler(this._parentDocument, "DOMAttrModified", this._attrModifiedHandler);
  7248. this._attrModifiedHandler = null;
  7249. }
  7250. frame.parentNode.removeChild(frame);
  7251. this._frame = null;
  7252. },
  7253. _destroyRuler: function(ruler) {
  7254. var side = ruler.getLocation();
  7255. var rulerParent = side === "left" ? this._leftDiv : this._rightDiv;
  7256. if (rulerParent) {
  7257. var row = rulerParent.firstChild.rows[0];
  7258. var cells = row.cells;
  7259. for (var index = 0; index < cells.length; index++) {
  7260. var cell = cells[index];
  7261. if (cell.firstChild._ruler === ruler) { break; }
  7262. }
  7263. if (index === cells.length) { return; }
  7264. row.cells[index]._ruler = undefined;
  7265. row.deleteCell(index);
  7266. }
  7267. },
  7268. _destroyView: function() {
  7269. var clientDiv = this._clientDiv;
  7270. if (!clientDiv) { return; }
  7271. this._setGrab(null);
  7272. this._unhookEvents();
  7273. if (this._windowLoadHandler) {
  7274. removeHandler(this._frameWindow, "load", this._windowLoadHandler);
  7275. this._windowLoadHandler = null;
  7276. }
  7277. /* Destroy timers */
  7278. if (this._autoScrollTimerID) {
  7279. clearTimeout(this._autoScrollTimerID);
  7280. this._autoScrollTimerID = null;
  7281. }
  7282. if (this._updateTimer) {
  7283. clearTimeout(this._updateTimer);
  7284. this._updateTimer = null;
  7285. }
  7286. /* Destroy DOM */
  7287. var parent = this._frameDocument.body;
  7288. while (parent.hasChildNodes()) { parent.removeChild(parent.lastChild); }
  7289. if (this._touchDiv) {
  7290. this._parent.removeChild(this._touchDiv);
  7291. this._touchDiv = null;
  7292. }
  7293. this._selDiv1 = null;
  7294. this._selDiv2 = null;
  7295. this._selDiv3 = null;
  7296. this._insertedSelRule = false;
  7297. this._textArea = null;
  7298. this._clipboardDiv = null;
  7299. this._scrollDiv = null;
  7300. this._viewDiv = null;
  7301. this._clipDiv = null;
  7302. this._clipScrollDiv = null;
  7303. this._clientDiv = null;
  7304. this._overlayDiv = null;
  7305. this._leftDiv = null;
  7306. this._rightDiv = null;
  7307. this._frameDocument = null;
  7308. this._frameWindow = null;
  7309. this.onUnload({type: "Unload"});
  7310. },
  7311. _doAutoScroll: function (direction, x, y) {
  7312. this._autoScrollDir = direction;
  7313. this._autoScrollX = x;
  7314. this._autoScrollY = y;
  7315. if (!this._autoScrollTimerID) {
  7316. this._autoScrollTimer();
  7317. }
  7318. },
  7319. _endAutoScroll: function () {
  7320. if (this._autoScrollTimerID) { clearTimeout(this._autoScrollTimerID); }
  7321. this._autoScrollDir = undefined;
  7322. this._autoScrollTimerID = undefined;
  7323. },
  7324. _fixCaret: function() {
  7325. var clientDiv = this._clientDiv;
  7326. if (clientDiv) {
  7327. var hasFocus = this._hasFocus;
  7328. this._ignoreFocus = true;
  7329. if (hasFocus) { clientDiv.blur(); }
  7330. clientDiv.contentEditable = false;
  7331. clientDiv.contentEditable = true;
  7332. if (hasFocus) { clientDiv.focus(); }
  7333. this._ignoreFocus = false;
  7334. }
  7335. },
  7336. _getBaseText: function(start, end) {
  7337. var model = this._model;
  7338. /* This is the only case the view access the base model, alternatively the view could use a event to application to customize the text */
  7339. if (model.getBaseModel) {
  7340. start = model.mapOffset(start);
  7341. end = model.mapOffset(end);
  7342. model = model.getBaseModel();
  7343. }
  7344. return model.getText(start, end);
  7345. },
  7346. _getBoundsAtOffset: function (offset) {
  7347. var model = this._model;
  7348. var document = this._frameDocument;
  7349. var clientDiv = this._clientDiv;
  7350. var lineIndex = model.getLineAtOffset(offset);
  7351. var dummy;
  7352. var child = this._getLineNode(lineIndex);
  7353. if (!child) {
  7354. child = dummy = this._createLine(clientDiv, null, document, lineIndex, model);
  7355. }
  7356. var result = null;
  7357. if (offset < model.getLineEnd(lineIndex)) {
  7358. var lineOffset = model.getLineStart(lineIndex);
  7359. var lineChild = child.firstChild;
  7360. while (lineChild) {
  7361. var textNode = lineChild.firstChild;
  7362. var nodeLength = textNode.length;
  7363. if (lineChild.ignoreChars) {
  7364. nodeLength -= lineChild.ignoreChars;
  7365. }
  7366. if (lineOffset + nodeLength > offset) {
  7367. var index = offset - lineOffset;
  7368. var range;
  7369. if (isRangeRects) {
  7370. range = document.createRange();
  7371. range.setStart(textNode, index);
  7372. range.setEnd(textNode, index + 1);
  7373. result = range.getBoundingClientRect();
  7374. } else if (isIE) {
  7375. range = document.body.createTextRange();
  7376. range.moveToElementText(lineChild);
  7377. range.collapse();
  7378. range.moveEnd("character", index + 1);
  7379. range.moveStart("character", index);
  7380. result = range.getBoundingClientRect();
  7381. } else {
  7382. var text = textNode.data;
  7383. lineChild.removeChild(textNode);
  7384. lineChild.appendChild(document.createTextNode(text.substring(0, index)));
  7385. var span = document.createElement("SPAN");
  7386. span.appendChild(document.createTextNode(text.substring(index, index + 1)));
  7387. lineChild.appendChild(span);
  7388. lineChild.appendChild(document.createTextNode(text.substring(index + 1)));
  7389. result = span.getBoundingClientRect();
  7390. lineChild.innerHTML = "";
  7391. lineChild.appendChild(textNode);
  7392. if (!dummy) {
  7393. /*
  7394. * Removing the element node that holds the selection start or end
  7395. * causes the selection to be lost. The fix is to detect this case
  7396. * and restore the selection.
  7397. */
  7398. var s = this._getSelection();
  7399. if ((lineOffset <= s.start && s.start < lineOffset + nodeLength) || (lineOffset <= s.end && s.end < lineOffset + nodeLength)) {
  7400. this._updateDOMSelection();
  7401. }
  7402. }
  7403. }
  7404. if (isIE) {
  7405. var logicalXDPI = window.screen.logicalXDPI;
  7406. var deviceXDPI = window.screen.deviceXDPI;
  7407. result.left = result.left * logicalXDPI / deviceXDPI;
  7408. result.right = result.right * logicalXDPI / deviceXDPI;
  7409. }
  7410. break;
  7411. }
  7412. lineOffset += nodeLength;
  7413. lineChild = lineChild.nextSibling;
  7414. }
  7415. }
  7416. if (!result) {
  7417. var rect = this._getLineBoundingClientRect(child);
  7418. result = {left: rect.right, right: rect.right};
  7419. }
  7420. if (dummy) { clientDiv.removeChild(dummy); }
  7421. return result;
  7422. },
  7423. _getBottomIndex: function (fullyVisible) {
  7424. var child = this._bottomChild;
  7425. if (fullyVisible && this._getClientHeight() > this._getLineHeight()) {
  7426. var rect = child.getBoundingClientRect();
  7427. var clientRect = this._clientDiv.getBoundingClientRect();
  7428. if (rect.bottom > clientRect.bottom) {
  7429. child = this._getLinePrevious(child) || child;
  7430. }
  7431. }
  7432. return child.lineIndex;
  7433. },
  7434. _getFrameHeight: function() {
  7435. return this._frameDocument.documentElement.clientHeight;
  7436. },
  7437. _getFrameWidth: function() {
  7438. return this._frameDocument.documentElement.clientWidth;
  7439. },
  7440. _getClientHeight: function() {
  7441. var viewPad = this._getViewPadding();
  7442. return Math.max(0, this._viewDiv.clientHeight - viewPad.top - viewPad.bottom);
  7443. },
  7444. _getClientWidth: function() {
  7445. var viewPad = this._getViewPadding();
  7446. return Math.max(0, this._viewDiv.clientWidth - viewPad.left - viewPad.right);
  7447. },
  7448. _getClipboardText: function (event, handler) {
  7449. var delimiter = this._model.getLineDelimiter();
  7450. var clipboadText, text;
  7451. if (this._frameWindow.clipboardData) {
  7452. //IE
  7453. clipboadText = [];
  7454. text = this._frameWindow.clipboardData.getData("Text");
  7455. this._convertDelimiter(text, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);});
  7456. text = clipboadText.join("");
  7457. if (handler) { handler(text); }
  7458. return text;
  7459. }
  7460. if (isFirefox) {
  7461. this._ignoreFocus = true;
  7462. var document = this._frameDocument;
  7463. var clipboardDiv = this._clipboardDiv;
  7464. clipboardDiv.innerHTML = "<pre contenteditable=''></pre>";
  7465. clipboardDiv.firstChild.focus();
  7466. var self = this;
  7467. var _getText = function() {
  7468. var noteText = self._getTextFromElement(clipboardDiv);
  7469. clipboardDiv.innerHTML = "";
  7470. clipboadText = [];
  7471. self._convertDelimiter(noteText, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);});
  7472. return clipboadText.join("");
  7473. };
  7474. /* Try execCommand first. Works on firefox with clipboard permission. */
  7475. var result = false;
  7476. this._ignorePaste = true;
  7477. /* Do not try execCommand if middle-click is used, because if we do, we get the clipboard text, not the primary selection text. */
  7478. if (!isLinux || this._lastMouseButton !== 2) {
  7479. try {
  7480. result = document.execCommand("paste", false, null);
  7481. } catch (ex) {
  7482. /* Firefox can throw even when execCommand() works, see bug 362835. */
  7483. result = clipboardDiv.childNodes.length > 1 || clipboardDiv.firstChild && clipboardDiv.firstChild.childNodes.length > 0;
  7484. }
  7485. }
  7486. this._ignorePaste = false;
  7487. if (!result) {
  7488. /* Try native paste in DOM, works for firefox during the paste event. */
  7489. if (event) {
  7490. setTimeout(function() {
  7491. self.focus();
  7492. text = _getText();
  7493. if (text && handler) {
  7494. handler(text);
  7495. }
  7496. self._ignoreFocus = false;
  7497. }, 0);
  7498. return null;
  7499. } else {
  7500. /* no event and no clipboard permission, paste can't be performed */
  7501. this.focus();
  7502. this._ignoreFocus = false;
  7503. return "";
  7504. }
  7505. }
  7506. this.focus();
  7507. this._ignoreFocus = false;
  7508. text = _getText();
  7509. if (text && handler) {
  7510. handler(text);
  7511. }
  7512. return text;
  7513. }
  7514. //webkit
  7515. if (event && event.clipboardData) {
  7516. /*
  7517. * Webkit (Chrome/Safari) allows getData during the paste event
  7518. * Note: setData is not allowed, not even during copy/cut event
  7519. */
  7520. clipboadText = [];
  7521. text = event.clipboardData.getData("text/plain");
  7522. this._convertDelimiter(text, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);});
  7523. text = clipboadText.join("");
  7524. if (text && handler) {
  7525. handler(text);
  7526. }
  7527. return text;
  7528. } else {
  7529. //TODO try paste using extension (Chrome only)
  7530. }
  7531. return "";
  7532. },
  7533. _getDOMText: function(lineIndex) {
  7534. var child = this._getLineNode(lineIndex);
  7535. var lineChild = child.firstChild;
  7536. var text = "";
  7537. while (lineChild) {
  7538. var textNode = lineChild.firstChild;
  7539. while (textNode) {
  7540. if (lineChild.ignoreChars) {
  7541. for (var i = 0; i < textNode.length; i++) {
  7542. var ch = textNode.data.substring(i, i + 1);
  7543. if (ch !== " ") {
  7544. text += ch;
  7545. }
  7546. }
  7547. } else {
  7548. text += textNode.data;
  7549. }
  7550. textNode = textNode.nextSibling;
  7551. }
  7552. lineChild = lineChild.nextSibling;
  7553. }
  7554. return text;
  7555. },
  7556. _getTextFromElement: function(element) {
  7557. var document = element.ownerDocument;
  7558. var window = document.defaultView;
  7559. if (!window.getSelection) {
  7560. return element.innerText || element.textContent;
  7561. }
  7562. var newRange = document.createRange();
  7563. newRange.selectNode(element);
  7564. var selection = window.getSelection();
  7565. var oldRanges = [], i;
  7566. for (i = 0; i < selection.rangeCount; i++) {
  7567. oldRanges.push(selection.getRangeAt(i));
  7568. }
  7569. this._ignoreSelect = true;
  7570. selection.removeAllRanges();
  7571. selection.addRange(newRange);
  7572. var text = selection.toString();
  7573. selection.removeAllRanges();
  7574. for (i = 0; i < oldRanges.length; i++) {
  7575. selection.addRange(oldRanges[i]);
  7576. }
  7577. this._ignoreSelect = false;
  7578. return text;
  7579. },
  7580. _getViewPadding: function() {
  7581. return this._viewPadding;
  7582. },
  7583. _getLineBoundingClientRect: function (child) {
  7584. var rect = child.getBoundingClientRect();
  7585. var lastChild = child.lastChild;
  7586. //Remove any artificial trailing whitespace in the line
  7587. while (lastChild && lastChild.ignoreChars === lastChild.firstChild.length) {
  7588. lastChild = lastChild.previousSibling;
  7589. }
  7590. if (!lastChild) {
  7591. return {left: rect.left, top: rect.top, right: rect.left, bottom: rect.bottom};
  7592. }
  7593. var lastRect = lastChild.getBoundingClientRect();
  7594. return {left: rect.left, top: rect.top, right: lastRect.right, bottom: rect.bottom};
  7595. },
  7596. _getLineHeight: function() {
  7597. return this._lineHeight;
  7598. },
  7599. _getLineNode: function (lineIndex) {
  7600. var clientDiv = this._clientDiv;
  7601. var child = clientDiv.firstChild;
  7602. while (child) {
  7603. if (lineIndex === child.lineIndex) {
  7604. return child;
  7605. }
  7606. child = child.nextSibling;
  7607. }
  7608. return undefined;
  7609. },
  7610. _getLineNext: function (lineNode) {
  7611. var node = lineNode ? lineNode.nextSibling : this._clientDiv.firstChild;
  7612. while (node && node.lineIndex === -1) {
  7613. node = node.nextSibling;
  7614. }
  7615. return node;
  7616. },
  7617. _getLinePrevious: function (lineNode) {
  7618. var node = lineNode ? lineNode.previousSibling : this._clientDiv.lastChild;
  7619. while (node && node.lineIndex === -1) {
  7620. node = node.previousSibling;
  7621. }
  7622. return node;
  7623. },
  7624. _getOffset: function (offset, unit, direction) {
  7625. if (unit === "line") {
  7626. var model = this._model;
  7627. var lineIndex = model.getLineAtOffset(offset);
  7628. if (direction > 0) {
  7629. return model.getLineEnd(lineIndex);
  7630. }
  7631. return model.getLineStart(lineIndex);
  7632. }
  7633. if (unit === "wordend") {
  7634. return this._getOffset_W3C(offset, unit, direction);
  7635. }
  7636. return isIE ? this._getOffset_IE(offset, unit, direction) : this._getOffset_W3C(offset, unit, direction);
  7637. },
  7638. _getOffset_W3C: function (offset, unit, direction) {
  7639. function _isPunctuation(c) {
  7640. return (33 <= c && c <= 47) || (58 <= c && c <= 64) || (91 <= c && c <= 94) || c === 96 || (123 <= c && c <= 126);
  7641. }
  7642. function _isWhitespace(c) {
  7643. return c === 32 || c === 9;
  7644. }
  7645. if (unit === "word" || unit === "wordend") {
  7646. var model = this._model;
  7647. var lineIndex = model.getLineAtOffset(offset);
  7648. var lineText = model.getLine(lineIndex);
  7649. var lineStart = model.getLineStart(lineIndex);
  7650. var lineEnd = model.getLineEnd(lineIndex);
  7651. var lineLength = lineText.length;
  7652. var offsetInLine = offset - lineStart;
  7653. var c, previousPunctuation, previousLetterOrDigit, punctuation, letterOrDigit;
  7654. if (direction > 0) {
  7655. if (offsetInLine === lineLength) { return lineEnd; }
  7656. c = lineText.charCodeAt(offsetInLine);
  7657. previousPunctuation = _isPunctuation(c);
  7658. previousLetterOrDigit = !previousPunctuation && !_isWhitespace(c);
  7659. offsetInLine++;
  7660. while (offsetInLine < lineLength) {
  7661. c = lineText.charCodeAt(offsetInLine);
  7662. punctuation = _isPunctuation(c);
  7663. if (unit === "wordend") {
  7664. if (!punctuation && previousPunctuation) { break; }
  7665. } else {
  7666. if (punctuation && !previousPunctuation) { break; }
  7667. }
  7668. letterOrDigit = !punctuation && !_isWhitespace(c);
  7669. if (unit === "wordend") {
  7670. if (!letterOrDigit && previousLetterOrDigit) { break; }
  7671. } else {
  7672. if (letterOrDigit && !previousLetterOrDigit) { break; }
  7673. }
  7674. previousLetterOrDigit = letterOrDigit;
  7675. previousPunctuation = punctuation;
  7676. offsetInLine++;
  7677. }
  7678. } else {
  7679. if (offsetInLine === 0) { return lineStart; }
  7680. offsetInLine--;
  7681. c = lineText.charCodeAt(offsetInLine);
  7682. previousPunctuation = _isPunctuation(c);
  7683. previousLetterOrDigit = !previousPunctuation && !_isWhitespace(c);
  7684. while (0 < offsetInLine) {
  7685. c = lineText.charCodeAt(offsetInLine - 1);
  7686. punctuation = _isPunctuation(c);
  7687. if (unit === "wordend") {
  7688. if (punctuation && !previousPunctuation) { break; }
  7689. } else {
  7690. if (!punctuation && previousPunctuation) { break; }
  7691. }
  7692. letterOrDigit = !punctuation && !_isWhitespace(c);
  7693. if (unit === "wordend") {
  7694. if (letterOrDigit && !previousLetterOrDigit) { break; }
  7695. } else {
  7696. if (!letterOrDigit && previousLetterOrDigit) { break; }
  7697. }
  7698. previousLetterOrDigit = letterOrDigit;
  7699. previousPunctuation = punctuation;
  7700. offsetInLine--;
  7701. }
  7702. }
  7703. return lineStart + offsetInLine;
  7704. }
  7705. return offset + direction;
  7706. },
  7707. _getOffset_IE: function (offset, unit, direction) {
  7708. var document = this._frameDocument;
  7709. var model = this._model;
  7710. var lineIndex = model.getLineAtOffset(offset);
  7711. var clientDiv = this._clientDiv;
  7712. var dummy;
  7713. var child = this._getLineNode(lineIndex);
  7714. if (!child) {
  7715. child = dummy = this._createLine(clientDiv, null, document, lineIndex, model);
  7716. }
  7717. var result = 0, range, length;
  7718. var lineOffset = model.getLineStart(lineIndex);
  7719. if (offset === model.getLineEnd(lineIndex)) {
  7720. range = document.body.createTextRange();
  7721. range.moveToElementText(child.lastChild);
  7722. length = range.text.length;
  7723. range.moveEnd(unit, direction);
  7724. result = offset + range.text.length - length;
  7725. } else if (offset === lineOffset && direction < 0) {
  7726. result = lineOffset;
  7727. } else {
  7728. var lineChild = child.firstChild;
  7729. while (lineChild) {
  7730. var textNode = lineChild.firstChild;
  7731. var nodeLength = textNode.length;
  7732. if (lineChild.ignoreChars) {
  7733. nodeLength -= lineChild.ignoreChars;
  7734. }
  7735. if (lineOffset + nodeLength > offset) {
  7736. range = document.body.createTextRange();
  7737. if (offset === lineOffset && direction < 0) {
  7738. range.moveToElementText(lineChild.previousSibling);
  7739. } else {
  7740. range.moveToElementText(lineChild);
  7741. range.collapse();
  7742. range.moveEnd("character", offset - lineOffset);
  7743. }
  7744. length = range.text.length;
  7745. range.moveEnd(unit, direction);
  7746. result = offset + range.text.length - length;
  7747. break;
  7748. }
  7749. lineOffset = nodeLength + lineOffset;
  7750. lineChild = lineChild.nextSibling;
  7751. }
  7752. }
  7753. if (dummy) { clientDiv.removeChild(dummy); }
  7754. return result;
  7755. },
  7756. _getOffsetToX: function (offset) {
  7757. return this._getBoundsAtOffset(offset).left;
  7758. },
  7759. _getPadding: function (node) {
  7760. var left,top,right,bottom;
  7761. if (node.currentStyle) {
  7762. left = node.currentStyle.paddingLeft;
  7763. top = node.currentStyle.paddingTop;
  7764. right = node.currentStyle.paddingRight;
  7765. bottom = node.currentStyle.paddingBottom;
  7766. } else if (this._frameWindow.getComputedStyle) {
  7767. var style = this._frameWindow.getComputedStyle(node, null);
  7768. left = style.getPropertyValue("padding-left");
  7769. top = style.getPropertyValue("padding-top");
  7770. right = style.getPropertyValue("padding-right");
  7771. bottom = style.getPropertyValue("padding-bottom");
  7772. }
  7773. return {
  7774. left: parseInt(left, 10),
  7775. top: parseInt(top, 10),
  7776. right: parseInt(right, 10),
  7777. bottom: parseInt(bottom, 10)
  7778. };
  7779. },
  7780. _getScroll: function() {
  7781. var viewDiv = this._viewDiv;
  7782. return {x: viewDiv.scrollLeft, y: viewDiv.scrollTop};
  7783. },
  7784. _getSelection: function () {
  7785. return this._selection.clone();
  7786. },
  7787. _getTopIndex: function (fullyVisible) {
  7788. var child = this._topChild;
  7789. if (fullyVisible && this._getClientHeight() > this._getLineHeight()) {
  7790. var rect = child.getBoundingClientRect();
  7791. var viewPad = this._getViewPadding();
  7792. var viewRect = this._viewDiv.getBoundingClientRect();
  7793. if (rect.top < viewRect.top + viewPad.top) {
  7794. child = this._getLineNext(child) || child;
  7795. }
  7796. }
  7797. return child.lineIndex;
  7798. },
  7799. _getXToOffset: function (lineIndex, x) {
  7800. var model = this._model;
  7801. var lineStart = model.getLineStart(lineIndex);
  7802. var lineEnd = model.getLineEnd(lineIndex);
  7803. if (lineStart === lineEnd) {
  7804. return lineStart;
  7805. }
  7806. var document = this._frameDocument;
  7807. var clientDiv = this._clientDiv;
  7808. var dummy;
  7809. var child = this._getLineNode(lineIndex);
  7810. if (!child) {
  7811. child = dummy = this._createLine(clientDiv, null, document, lineIndex, model);
  7812. }
  7813. var lineRect = this._getLineBoundingClientRect(child);
  7814. if (x < lineRect.left) { x = lineRect.left; }
  7815. if (x > lineRect.right) { x = lineRect.right; }
  7816. /*
  7817. * Bug in IE 8 and earlier. The coordinates of getClientRects() are relative to
  7818. * the browser window. The fix is to convert to the frame window before using it.
  7819. */
  7820. var deltaX = 0, rects;
  7821. if (isIE < 9) {
  7822. rects = child.getClientRects();
  7823. var minLeft = rects[0].left;
  7824. for (var i=1; i<rects.length; i++) {
  7825. minLeft = Math.min(rects[i].left, minLeft);
  7826. }
  7827. deltaX = minLeft - lineRect.left;
  7828. }
  7829. var scrollX = this._getScroll().x;
  7830. function _getClientRects(element) {
  7831. var rects, newRects, i, r;
  7832. if (!element._rectsCache) {
  7833. rects = element.getClientRects();
  7834. newRects = [rects.length];
  7835. for (i = 0; i<rects.length; i++) {
  7836. r = rects[i];
  7837. newRects[i] = {left: r.left - deltaX + scrollX, top: r.top, right: r.right - deltaX + scrollX, bottom: r.bottom};
  7838. }
  7839. element._rectsCache = newRects;
  7840. }
  7841. rects = element._rectsCache;
  7842. newRects = [rects.length];
  7843. for (i = 0; i<rects.length; i++) {
  7844. r = rects[i];
  7845. newRects[i] = {left: r.left - scrollX, top: r.top, right: r.right - scrollX, bottom: r.bottom};
  7846. }
  7847. return newRects;
  7848. }
  7849. var logicalXDPI = isIE ? window.screen.logicalXDPI : 1;
  7850. var deviceXDPI = isIE ? window.screen.deviceXDPI : 1;
  7851. var offset = lineStart;
  7852. var lineChild = child.firstChild;
  7853. done:
  7854. while (lineChild) {
  7855. var textNode = lineChild.firstChild;
  7856. var nodeLength = textNode.length;
  7857. if (lineChild.ignoreChars) {
  7858. nodeLength -= lineChild.ignoreChars;
  7859. }
  7860. rects = _getClientRects(lineChild);
  7861. for (var j = 0; j < rects.length; j++) {
  7862. var rect = rects[j];
  7863. if (rect.left <= x && x < rect.right) {
  7864. var range, start, end;
  7865. if (isIE || isRangeRects) {
  7866. range = isRangeRects ? document.createRange() : document.body.createTextRange();
  7867. var high = nodeLength;
  7868. var low = -1;
  7869. while ((high - low) > 1) {
  7870. var mid = Math.floor((high + low) / 2);
  7871. start = low + 1;
  7872. end = mid === nodeLength - 1 && lineChild.ignoreChars ? textNode.length : mid + 1;
  7873. if (isRangeRects) {
  7874. range.setStart(textNode, start);
  7875. range.setEnd(textNode, end);
  7876. } else {
  7877. range.moveToElementText(lineChild);
  7878. range.move("character", start);
  7879. range.moveEnd("character", end - start);
  7880. }
  7881. rects = range.getClientRects();
  7882. var found = false;
  7883. for (var k = 0; k < rects.length; k++) {
  7884. rect = rects[k];
  7885. var rangeLeft = rect.left * logicalXDPI / deviceXDPI - deltaX;
  7886. var rangeRight = rect.right * logicalXDPI / deviceXDPI - deltaX;
  7887. if (rangeLeft <= x && x < rangeRight) {
  7888. found = true;
  7889. break;
  7890. }
  7891. }
  7892. if (found) {
  7893. high = mid;
  7894. } else {
  7895. low = mid;
  7896. }
  7897. }
  7898. offset += high;
  7899. start = high;
  7900. end = high === nodeLength - 1 && lineChild.ignoreChars ? textNode.length : Math.min(high + 1, textNode.length);
  7901. if (isRangeRects) {
  7902. range.setStart(textNode, start);
  7903. range.setEnd(textNode, end);
  7904. } else {
  7905. range.moveToElementText(lineChild);
  7906. range.move("character", start);
  7907. range.moveEnd("character", end - start);
  7908. }
  7909. rect = range.getClientRects()[0];
  7910. //TODO test for character trailing (wrong for bidi)
  7911. if (x > ((rect.left * logicalXDPI / deviceXDPI - deltaX) + ((rect.right - rect.left) * logicalXDPI / deviceXDPI / 2))) {
  7912. offset++;
  7913. }
  7914. } else {
  7915. var newText = [];
  7916. for (var q = 0; q < nodeLength; q++) {
  7917. newText.push("<span>");
  7918. if (q === nodeLength - 1) {
  7919. newText.push(textNode.data.substring(q));
  7920. } else {
  7921. newText.push(textNode.data.substring(q, q + 1));
  7922. }
  7923. newText.push("</span>");
  7924. }
  7925. lineChild.innerHTML = newText.join("");
  7926. var rangeChild = lineChild.firstChild;
  7927. while (rangeChild) {
  7928. rect = rangeChild.getBoundingClientRect();
  7929. if (rect.left <= x && x < rect.right) {
  7930. //TODO test for character trailing (wrong for bidi)
  7931. if (x > rect.left + (rect.right - rect.left) / 2) {
  7932. offset++;
  7933. }
  7934. break;
  7935. }
  7936. offset++;
  7937. rangeChild = rangeChild.nextSibling;
  7938. }
  7939. if (!dummy) {
  7940. lineChild.innerHTML = "";
  7941. lineChild.appendChild(textNode);
  7942. /*
  7943. * Removing the element node that holds the selection start or end
  7944. * causes the selection to be lost. The fix is to detect this case
  7945. * and restore the selection.
  7946. */
  7947. var s = this._getSelection();
  7948. if ((offset <= s.start && s.start < offset + nodeLength) || (offset <= s.end && s.end < offset + nodeLength)) {
  7949. this._updateDOMSelection();
  7950. }
  7951. }
  7952. }
  7953. break done;
  7954. }
  7955. }
  7956. offset += nodeLength;
  7957. lineChild = lineChild.nextSibling;
  7958. }
  7959. if (dummy) { clientDiv.removeChild(dummy); }
  7960. return Math.min(lineEnd, Math.max(lineStart, offset));
  7961. },
  7962. _getYToLine: function (y) {
  7963. var viewPad = this._getViewPadding();
  7964. var viewRect = this._viewDiv.getBoundingClientRect();
  7965. y -= viewRect.top + viewPad.top;
  7966. var lineHeight = this._getLineHeight();
  7967. var lineIndex = Math.floor((y + this._getScroll().y) / lineHeight);
  7968. var lineCount = this._model.getLineCount();
  7969. return Math.max(0, Math.min(lineCount - 1, lineIndex));
  7970. },
  7971. _getOffsetBounds: function(offset) {
  7972. var model = this._model;
  7973. var lineIndex = model.getLineAtOffset(offset);
  7974. var lineHeight = this._getLineHeight();
  7975. var scroll = this._getScroll();
  7976. var viewPad = this._getViewPadding();
  7977. var viewRect = this._viewDiv.getBoundingClientRect();
  7978. var bounds = this._getBoundsAtOffset(offset);
  7979. var left = bounds.left;
  7980. var right = bounds.right;
  7981. var top = (lineIndex * lineHeight) - scroll.y + viewRect.top + viewPad.top;
  7982. var bottom = top + lineHeight;
  7983. return {left: left, top: top, right: right, bottom: bottom};
  7984. },
  7985. _getVisible: function() {
  7986. var temp = this._parent;
  7987. var parentDocument = temp.ownerDocument;
  7988. while (temp !== parentDocument) {
  7989. var hidden;
  7990. if (isIE < 9) {
  7991. hidden = temp.currentStyle && temp.currentStyle.display === "none";
  7992. } else {
  7993. var tempStyle = parentDocument.defaultView.getComputedStyle(temp, null);
  7994. hidden = tempStyle && tempStyle.getPropertyValue("display") === "none";
  7995. }
  7996. if (hidden) { return "hidden"; }
  7997. temp = temp.parentNode;
  7998. if (!temp) { return "disconnected"; }
  7999. }
  8000. return "visible";
  8001. },
  8002. _hitOffset: function (offset, x, y) {
  8003. var bounds = this._getOffsetBounds(offset);
  8004. var left = bounds.left;
  8005. var right = bounds.right;
  8006. var top = bounds.top;
  8007. var bottom = bounds.bottom;
  8008. var area = 20;
  8009. left -= area;
  8010. top -= area;
  8011. right += area;
  8012. bottom += area;
  8013. return (left <= x && x <= right && top <= y && y <= bottom);
  8014. },
  8015. _hookEvents: function() {
  8016. var self = this;
  8017. this._modelListener = {
  8018. /** @private */
  8019. onChanging: function(modelChangingEvent) {
  8020. self._onModelChanging(modelChangingEvent);
  8021. },
  8022. /** @private */
  8023. onChanged: function(modelChangedEvent) {
  8024. self._onModelChanged(modelChangedEvent);
  8025. }
  8026. };
  8027. this._model.addEventListener("Changing", this._modelListener.onChanging);
  8028. this._model.addEventListener("Changed", this._modelListener.onChanged);
  8029. var clientDiv = this._clientDiv;
  8030. var viewDiv = this._viewDiv;
  8031. var body = this._frameDocument.body;
  8032. var handlers = this._handlers = [];
  8033. var resizeNode = isIE < 9 ? this._frame : this._frameWindow;
  8034. var focusNode = isPad ? this._textArea : (isIE || isFirefox ? this._clientDiv: this._frameWindow);
  8035. handlers.push({target: this._frameWindow, type: "unload", handler: function(e) { return self._handleUnload(e);}});
  8036. handlers.push({target: resizeNode, type: "resize", handler: function(e) { return self._handleResize(e);}});
  8037. handlers.push({target: focusNode, type: "blur", handler: function(e) { return self._handleBlur(e);}});
  8038. handlers.push({target: focusNode, type: "focus", handler: function(e) { return self._handleFocus(e);}});
  8039. handlers.push({target: viewDiv, type: "scroll", handler: function(e) { return self._handleScroll(e);}});
  8040. if (isPad) {
  8041. var touchDiv = this._touchDiv;
  8042. var textArea = this._textArea;
  8043. handlers.push({target: textArea, type: "keydown", handler: function(e) { return self._handleKeyDown(e);}});
  8044. handlers.push({target: textArea, type: "input", handler: function(e) { return self._handleInput(e); }});
  8045. handlers.push({target: textArea, type: "textInput", handler: function(e) { return self._handleTextInput(e); }});
  8046. handlers.push({target: textArea, type: "click", handler: function(e) { return self._handleTextAreaClick(e); }});
  8047. handlers.push({target: touchDiv, type: "touchstart", handler: function(e) { return self._handleTouchStart(e); }});
  8048. handlers.push({target: touchDiv, type: "touchmove", handler: function(e) { return self._handleTouchMove(e); }});
  8049. handlers.push({target: touchDiv, type: "touchend", handler: function(e) { return self._handleTouchEnd(e); }});
  8050. } else {
  8051. var topNode = this._overlayDiv || this._clientDiv;
  8052. var grabNode = isIE ? clientDiv : this._frameWindow;
  8053. handlers.push({target: clientDiv, type: "keydown", handler: function(e) { return self._handleKeyDown(e);}});
  8054. handlers.push({target: clientDiv, type: "keypress", handler: function(e) { return self._handleKeyPress(e);}});
  8055. handlers.push({target: clientDiv, type: "keyup", handler: function(e) { return self._handleKeyUp(e);}});
  8056. handlers.push({target: clientDiv, type: "selectstart", handler: function(e) { return self._handleSelectStart(e);}});
  8057. handlers.push({target: clientDiv, type: "contextmenu", handler: function(e) { return self._handleContextMenu(e);}});
  8058. handlers.push({target: clientDiv, type: "copy", handler: function(e) { return self._handleCopy(e);}});
  8059. handlers.push({target: clientDiv, type: "cut", handler: function(e) { return self._handleCut(e);}});
  8060. handlers.push({target: clientDiv, type: "paste", handler: function(e) { return self._handlePaste(e);}});
  8061. handlers.push({target: clientDiv, type: "mousedown", handler: function(e) { return self._handleMouseDown(e);}});
  8062. handlers.push({target: clientDiv, type: "mouseover", handler: function(e) { return self._handleMouseOver(e);}});
  8063. handlers.push({target: clientDiv, type: "mouseout", handler: function(e) { return self._handleMouseOut(e);}});
  8064. handlers.push({target: grabNode, type: "mouseup", handler: function(e) { return self._handleMouseUp(e);}});
  8065. handlers.push({target: grabNode, type: "mousemove", handler: function(e) { return self._handleMouseMove(e);}});
  8066. handlers.push({target: body, type: "mousedown", handler: function(e) { return self._handleBodyMouseDown(e);}});
  8067. handlers.push({target: body, type: "mouseup", handler: function(e) { return self._handleBodyMouseUp(e);}});
  8068. handlers.push({target: topNode, type: "dragstart", handler: function(e) { return self._handleDragStart(e);}});
  8069. handlers.push({target: topNode, type: "drag", handler: function(e) { return self._handleDrag(e);}});
  8070. handlers.push({target: topNode, type: "dragend", handler: function(e) { return self._handleDragEnd(e);}});
  8071. handlers.push({target: topNode, type: "dragenter", handler: function(e) { return self._handleDragEnter(e);}});
  8072. handlers.push({target: topNode, type: "dragover", handler: function(e) { return self._handleDragOver(e);}});
  8073. handlers.push({target: topNode, type: "dragleave", handler: function(e) { return self._handleDragLeave(e);}});
  8074. handlers.push({target: topNode, type: "drop", handler: function(e) { return self._handleDrop(e);}});
  8075. if (isChrome) {
  8076. handlers.push({target: this._parentDocument, type: "mousemove", handler: function(e) { return self._handleMouseMove(e);}});
  8077. handlers.push({target: this._parentDocument, type: "mouseup", handler: function(e) { return self._handleMouseUp(e);}});
  8078. }
  8079. if (isIE) {
  8080. handlers.push({target: this._frameDocument, type: "activate", handler: function(e) { return self._handleDocFocus(e); }});
  8081. }
  8082. if (isFirefox) {
  8083. handlers.push({target: this._frameDocument, type: "focus", handler: function(e) { return self._handleDocFocus(e); }});
  8084. }
  8085. if (!isIE && !isOpera) {
  8086. var wheelEvent = isFirefox ? "DOMMouseScroll" : "mousewheel";
  8087. handlers.push({target: this._viewDiv, type: wheelEvent, handler: function(e) { return self._handleMouseWheel(e); }});
  8088. }
  8089. if (isFirefox && !isWindows) {
  8090. handlers.push({target: this._clientDiv, type: "DOMCharacterDataModified", handler: function (e) { return self._handleDataModified(e); }});
  8091. }
  8092. if (this._overlayDiv) {
  8093. handlers.push({target: this._overlayDiv, type: "mousedown", handler: function(e) { return self._handleMouseDown(e);}});
  8094. handlers.push({target: this._overlayDiv, type: "mouseover", handler: function(e) { return self._handleMouseOver(e);}});
  8095. handlers.push({target: this._overlayDiv, type: "mouseout", handler: function(e) { return self._handleMouseOut(e);}});
  8096. handlers.push({target: this._overlayDiv, type: "contextmenu", handler: function(e) { return self._handleContextMenu(e); }});
  8097. }
  8098. if (!isW3CEvents) {
  8099. handlers.push({target: this._clientDiv, type: "dblclick", handler: function(e) { return self._handleDblclick(e); }});
  8100. }
  8101. }
  8102. for (var i=0; i<handlers.length; i++) {
  8103. var h = handlers[i];
  8104. addHandler(h.target, h.type, h.handler, h.capture);
  8105. }
  8106. },
  8107. _init: function(options) {
  8108. var parent = options.parent;
  8109. if (typeof(parent) === "string") {
  8110. parent = window.document.getElementById(parent);
  8111. }
  8112. if (!parent) { throw "no parent"; }
  8113. options.parent = parent;
  8114. options.model = options.model || new mTextModel.TextModel();
  8115. var defaultOptions = this._defaultOptions();
  8116. for (var option in defaultOptions) {
  8117. if (defaultOptions.hasOwnProperty(option)) {
  8118. var value;
  8119. if (options[option] !== undefined) {
  8120. value = options[option];
  8121. } else {
  8122. value = defaultOptions[option].value;
  8123. }
  8124. this["_" + option] = value;
  8125. }
  8126. }
  8127. this._rulers = [];
  8128. this._selection = new Selection (0, 0, false);
  8129. this._linksVisible = false;
  8130. this._redrawCount = 0;
  8131. this._maxLineWidth = 0;
  8132. this._maxLineIndex = -1;
  8133. this._ignoreSelect = true;
  8134. this._ignoreFocus = false;
  8135. this._columnX = -1;
  8136. this._dragOffset = -1;
  8137. /* Auto scroll */
  8138. this._autoScrollX = null;
  8139. this._autoScrollY = null;
  8140. this._autoScrollTimerID = null;
  8141. this._AUTO_SCROLL_RATE = 50;
  8142. this._grabControl = null;
  8143. this._moseMoveClosure = null;
  8144. this._mouseUpClosure = null;
  8145. /* Double click */
  8146. this._lastMouseX = 0;
  8147. this._lastMouseY = 0;
  8148. this._lastMouseTime = 0;
  8149. this._clickCount = 0;
  8150. this._clickTime = 250;
  8151. this._clickDist = 5;
  8152. this._isMouseDown = false;
  8153. this._doubleClickSelection = null;
  8154. /* Scroll */
  8155. this._hScroll = 0;
  8156. this._vScroll = 0;
  8157. /* IME */
  8158. this._imeOffset = -1;
  8159. /* Create elements */
  8160. this._createActions();
  8161. this._createFrame();
  8162. },
  8163. _isLinkURL: function(string) {
  8164. return string.toLowerCase().lastIndexOf(".css") === string.length - 4;
  8165. },
  8166. _modifyContent: function(e, updateCaret) {
  8167. if (this._readonly && !e._code) {
  8168. return;
  8169. }
  8170. e.type = "Verify";
  8171. this.onVerify(e);
  8172. if (e.text === null || e.text === undefined) { return; }
  8173. var model = this._model;
  8174. try {
  8175. if (e._ignoreDOMSelection) { this._ignoreDOMSelection = true; }
  8176. model.setText (e.text, e.start, e.end);
  8177. } finally {
  8178. if (e._ignoreDOMSelection) { this._ignoreDOMSelection = false; }
  8179. }
  8180. if (updateCaret) {
  8181. var selection = this._getSelection ();
  8182. selection.setCaret(e.start + e.text.length);
  8183. this._setSelection(selection, true);
  8184. }
  8185. this.onModify({type: "Modify"});
  8186. },
  8187. _onModelChanged: function(modelChangedEvent) {
  8188. modelChangedEvent.type = "ModelChanged";
  8189. this.onModelChanged(modelChangedEvent);
  8190. modelChangedEvent.type = "Changed";
  8191. var start = modelChangedEvent.start;
  8192. var addedCharCount = modelChangedEvent.addedCharCount;
  8193. var removedCharCount = modelChangedEvent.removedCharCount;
  8194. var addedLineCount = modelChangedEvent.addedLineCount;
  8195. var removedLineCount = modelChangedEvent.removedLineCount;
  8196. var selection = this._getSelection();
  8197. if (selection.end > start) {
  8198. if (selection.end > start && selection.start < start + removedCharCount) {
  8199. // selection intersects replaced text. set caret behind text change
  8200. selection.setCaret(start + addedCharCount);
  8201. } else {
  8202. // move selection to keep same text selected
  8203. selection.start += addedCharCount - removedCharCount;
  8204. selection.end += addedCharCount - removedCharCount;
  8205. }
  8206. this._setSelection(selection, false, false);
  8207. }
  8208. var model = this._model;
  8209. var startLine = model.getLineAtOffset(start);
  8210. var child = this._getLineNext();
  8211. while (child) {
  8212. var lineIndex = child.lineIndex;
  8213. if (startLine <= lineIndex && lineIndex <= startLine + removedLineCount) {
  8214. if (startLine === lineIndex && !child.modelChangedEvent && !child.lineRemoved) {
  8215. child.modelChangedEvent = modelChangedEvent;
  8216. child.lineChanged = true;
  8217. } else {
  8218. child.lineRemoved = true;
  8219. child.lineChanged = false;
  8220. child.modelChangedEvent = null;
  8221. }
  8222. }
  8223. if (lineIndex > startLine + removedLineCount) {
  8224. child.lineIndex = lineIndex + addedLineCount - removedLineCount;
  8225. }
  8226. child = this._getLineNext(child);
  8227. }
  8228. if (startLine <= this._maxLineIndex && this._maxLineIndex <= startLine + removedLineCount) {
  8229. this._checkMaxLineIndex = this._maxLineIndex;
  8230. this._maxLineIndex = -1;
  8231. this._maxLineWidth = 0;
  8232. }
  8233. this._updatePage();
  8234. },
  8235. _onModelChanging: function(modelChangingEvent) {
  8236. modelChangingEvent.type = "ModelChanging";
  8237. this.onModelChanging(modelChangingEvent);
  8238. modelChangingEvent.type = "Changing";
  8239. },
  8240. _queueUpdatePage: function() {
  8241. if (this._updateTimer) { return; }
  8242. var self = this;
  8243. this._updateTimer = setTimeout(function() {
  8244. self._updateTimer = null;
  8245. self._updatePage();
  8246. }, 0);
  8247. },
  8248. _reset: function() {
  8249. this._maxLineIndex = -1;
  8250. this._maxLineWidth = 0;
  8251. this._columnX = -1;
  8252. this._topChild = null;
  8253. this._bottomChild = null;
  8254. this._partialY = 0;
  8255. this._setSelection(new Selection (0, 0, false), false, false);
  8256. if (this._viewDiv) {
  8257. this._viewDiv.scrollLeft = 0;
  8258. this._viewDiv.scrollTop = 0;
  8259. }
  8260. var clientDiv = this._clientDiv;
  8261. if (clientDiv) {
  8262. var child = clientDiv.firstChild;
  8263. while (child) {
  8264. child.lineRemoved = true;
  8265. child = child.nextSibling;
  8266. }
  8267. /*
  8268. * Bug in Firefox. For some reason, the caret does not show after the
  8269. * view is refreshed. The fix is to toggle the contentEditable state and
  8270. * force the clientDiv to loose and receive focus if it is focused.
  8271. */
  8272. if (isFirefox) {
  8273. this._ignoreFocus = false;
  8274. var hasFocus = this._hasFocus;
  8275. if (hasFocus) { clientDiv.blur(); }
  8276. clientDiv.contentEditable = false;
  8277. clientDiv.contentEditable = true;
  8278. if (hasFocus) { clientDiv.focus(); }
  8279. this._ignoreFocus = false;
  8280. }
  8281. }
  8282. },
  8283. _resizeTouchDiv: function() {
  8284. var viewRect = this._viewDiv.getBoundingClientRect();
  8285. var parentRect = this._frame.getBoundingClientRect();
  8286. var temp = this._frame;
  8287. while (temp) {
  8288. if (temp.style && temp.style.top) { break; }
  8289. temp = temp.parentNode;
  8290. }
  8291. var parentTop = parentRect.top;
  8292. if (temp) {
  8293. parentTop -= temp.getBoundingClientRect().top;
  8294. } else {
  8295. parentTop += this._parentDocument.body.scrollTop;
  8296. }
  8297. temp = this._frame;
  8298. while (temp) {
  8299. if (temp.style && temp.style.left) { break; }
  8300. temp = temp.parentNode;
  8301. }
  8302. var parentLeft = parentRect.left;
  8303. if (temp) {
  8304. parentLeft -= temp.getBoundingClientRect().left;
  8305. } else {
  8306. parentLeft += this._parentDocument.body.scrollLeft;
  8307. }
  8308. var touchDiv = this._touchDiv;
  8309. touchDiv.style.left = (parentLeft + viewRect.left) + "px";
  8310. touchDiv.style.top = (parentTop + viewRect.top) + "px";
  8311. touchDiv.style.width = viewRect.width + "px";
  8312. touchDiv.style.height = viewRect.height + "px";
  8313. },
  8314. _scrollView: function (pixelX, pixelY) {
  8315. /*
  8316. * Always set _ensureCaretVisible to false so that the view does not scroll
  8317. * to show the caret when scrollView is not called from showCaret().
  8318. */
  8319. this._ensureCaretVisible = false;
  8320. /*
  8321. * Scrolling is done only by setting the scrollLeft and scrollTop fields in the
  8322. * view div. This causes an updatePage from the scroll event. In some browsers
  8323. * this event is asynchronous and forcing update page to run synchronously
  8324. * leads to redraw problems.
  8325. * On Chrome 11, the view redrawing at times when holding PageDown/PageUp key.
  8326. * On Firefox 4 for Linux, the view redraws the first page when holding
  8327. * PageDown/PageUp key, but it will not redraw again until the key is released.
  8328. */
  8329. var viewDiv = this._viewDiv;
  8330. if (pixelX) { viewDiv.scrollLeft += pixelX; }
  8331. if (pixelY) { viewDiv.scrollTop += pixelY; }
  8332. },
  8333. _setClipboardText: function (text, event) {
  8334. var clipboardText;
  8335. if (this._frameWindow.clipboardData) {
  8336. //IE
  8337. clipboardText = [];
  8338. this._convertDelimiter(text, function(t) {clipboardText.push(t);}, function() {clipboardText.push(platformDelimiter);});
  8339. return this._frameWindow.clipboardData.setData("Text", clipboardText.join(""));
  8340. }
  8341. /* Feature in Chrome, clipboardData.setData is no-op on Chrome even though it returns true */
  8342. if (isChrome || isFirefox || !event) {
  8343. var window = this._frameWindow;
  8344. var document = this._frameDocument;
  8345. var child = document.createElement("PRE");
  8346. child.style.position = "fixed";
  8347. child.style.left = "-1000px";
  8348. this._convertDelimiter(text,
  8349. function(t) {
  8350. child.appendChild(document.createTextNode(t));
  8351. },
  8352. function() {
  8353. child.appendChild(document.createElement("BR"));
  8354. }
  8355. );
  8356. child.appendChild(document.createTextNode(" "));
  8357. this._clientDiv.appendChild(child);
  8358. var range = document.createRange();
  8359. range.setStart(child.firstChild, 0);
  8360. range.setEndBefore(child.lastChild);
  8361. var sel = window.getSelection();
  8362. if (sel.rangeCount > 0) { sel.removeAllRanges(); }
  8363. sel.addRange(range);
  8364. var self = this;
  8365. /** @ignore */
  8366. var cleanup = function() {
  8367. if (child && child.parentNode === self._clientDiv) {
  8368. self._clientDiv.removeChild(child);
  8369. }
  8370. self._updateDOMSelection();
  8371. };
  8372. var result = false;
  8373. /*
  8374. * Try execCommand first, it works on firefox with clipboard permission,
  8375. * chrome 5, safari 4.
  8376. */
  8377. this._ignoreCopy = true;
  8378. try {
  8379. result = document.execCommand("copy", false, null);
  8380. } catch (e) {}
  8381. this._ignoreCopy = false;
  8382. if (!result) {
  8383. if (event) {
  8384. setTimeout(cleanup, 0);
  8385. return false;
  8386. }
  8387. }
  8388. /* no event and no permission, copy can not be done */
  8389. cleanup();
  8390. return true;
  8391. }
  8392. if (event && event.clipboardData) {
  8393. //webkit
  8394. clipboardText = [];
  8395. this._convertDelimiter(text, function(t) {clipboardText.push(t);}, function() {clipboardText.push(platformDelimiter);});
  8396. return event.clipboardData.setData("text/plain", clipboardText.join(""));
  8397. }
  8398. },
  8399. _setDOMSelection: function (startNode, startOffset, endNode, endOffset) {
  8400. var window = this._frameWindow;
  8401. var document = this._frameDocument;
  8402. var startLineNode, startLineOffset, endLineNode, endLineOffset;
  8403. var offset = 0;
  8404. var lineChild = startNode.firstChild;
  8405. var node, nodeLength, model = this._model;
  8406. var startLineEnd = model.getLine(startNode.lineIndex).length;
  8407. while (lineChild) {
  8408. node = lineChild.firstChild;
  8409. nodeLength = node.length;
  8410. if (lineChild.ignoreChars) {
  8411. nodeLength -= lineChild.ignoreChars;
  8412. }
  8413. if (offset + nodeLength > startOffset || offset + nodeLength >= startLineEnd) {
  8414. startLineNode = node;
  8415. startLineOffset = startOffset - offset;
  8416. if (lineChild.ignoreChars && nodeLength > 0 && startLineOffset === nodeLength) {
  8417. startLineOffset += lineChild.ignoreChars;
  8418. }
  8419. break;
  8420. }
  8421. offset += nodeLength;
  8422. lineChild = lineChild.nextSibling;
  8423. }
  8424. offset = 0;
  8425. lineChild = endNode.firstChild;
  8426. var endLineEnd = this._model.getLine(endNode.lineIndex).length;
  8427. while (lineChild) {
  8428. node = lineChild.firstChild;
  8429. nodeLength = node.length;
  8430. if (lineChild.ignoreChars) {
  8431. nodeLength -= lineChild.ignoreChars;
  8432. }
  8433. if (nodeLength + offset > endOffset || offset + nodeLength >= endLineEnd) {
  8434. endLineNode = node;
  8435. endLineOffset = endOffset - offset;
  8436. if (lineChild.ignoreChars && nodeLength > 0 && endLineOffset === nodeLength) {
  8437. endLineOffset += lineChild.ignoreChars;
  8438. }
  8439. break;
  8440. }
  8441. offset += nodeLength;
  8442. lineChild = lineChild.nextSibling;
  8443. }
  8444. this._setDOMFullSelection(startNode, startOffset, startLineEnd, endNode, endOffset, endLineEnd);
  8445. if (isPad) { return; }
  8446. var range;
  8447. if (window.getSelection) {
  8448. //W3C
  8449. range = document.createRange();
  8450. range.setStart(startLineNode, startLineOffset);
  8451. range.setEnd(endLineNode, endLineOffset);
  8452. var sel = window.getSelection();
  8453. this._ignoreSelect = false;
  8454. if (sel.rangeCount > 0) { sel.removeAllRanges(); }
  8455. sel.addRange(range);
  8456. this._ignoreSelect = true;
  8457. } else if (document.selection) {
  8458. //IE < 9
  8459. var body = document.body;
  8460. /*
  8461. * Bug in IE. For some reason when text is deselected the overflow
  8462. * selection at the end of some lines does not get redrawn. The
  8463. * fix is to create a DOM element in the body to force a redraw.
  8464. */
  8465. var child = document.createElement("DIV");
  8466. body.appendChild(child);
  8467. body.removeChild(child);
  8468. range = body.createTextRange();
  8469. range.moveToElementText(startLineNode.parentNode);
  8470. range.moveStart("character", startLineOffset);
  8471. var endRange = body.createTextRange();
  8472. endRange.moveToElementText(endLineNode.parentNode);
  8473. endRange.moveStart("character", endLineOffset);
  8474. range.setEndPoint("EndToStart", endRange);
  8475. this._ignoreSelect = false;
  8476. range.select();
  8477. this._ignoreSelect = true;
  8478. }
  8479. },
  8480. _setDOMFullSelection: function(startNode, startOffset, startLineEnd, endNode, endOffset, endLineEnd) {
  8481. var model = this._model;
  8482. if (this._selDiv1) {
  8483. var startLineBounds, l;
  8484. startLineBounds = this._getLineBoundingClientRect(startNode);
  8485. if (startOffset === 0) {
  8486. l = startLineBounds.left;
  8487. } else {
  8488. if (startOffset >= startLineEnd) {
  8489. l = startLineBounds.right;
  8490. } else {
  8491. this._ignoreDOMSelection = true;
  8492. l = this._getBoundsAtOffset(model.getLineStart(startNode.lineIndex) + startOffset).left;
  8493. this._ignoreDOMSelection = false;
  8494. }
  8495. }
  8496. var textArea = this._textArea;
  8497. if (textArea && isPad) {
  8498. textArea.selectionStart = textArea.selectionEnd = 0;
  8499. var rect = this._frame.getBoundingClientRect();
  8500. var touchRect = this._touchDiv.getBoundingClientRect();
  8501. var viewBounds = this._viewDiv.getBoundingClientRect();
  8502. if (!(viewBounds.left <= l && l <= viewBounds.left + viewBounds.width &&
  8503. viewBounds.top <= startLineBounds.top && startLineBounds.top <= viewBounds.top + viewBounds.height) ||
  8504. !(startNode === endNode && startOffset === endOffset))
  8505. {
  8506. textArea.style.left = "-1000px";
  8507. } else {
  8508. textArea.style.left = (l - 4 + rect.left - touchRect.left) + "px";
  8509. }
  8510. textArea.style.top = (startLineBounds.top + rect.top - touchRect.top) + "px";
  8511. textArea.style.width = "6px";
  8512. textArea.style.height = (startLineBounds.bottom - startLineBounds.top) + "px";
  8513. }
  8514. var selDiv = this._selDiv1;
  8515. selDiv.style.width = "0px";
  8516. selDiv.style.height = "0px";
  8517. selDiv = this._selDiv2;
  8518. selDiv.style.width = "0px";
  8519. selDiv.style.height = "0px";
  8520. selDiv = this._selDiv3;
  8521. selDiv.style.width = "0px";
  8522. selDiv.style.height = "0px";
  8523. if (!(startNode === endNode && startOffset === endOffset)) {
  8524. var handleWidth = isPad ? 2 : 0;
  8525. var handleBorder = handleWidth + "px blue solid";
  8526. var viewPad = this._getViewPadding();
  8527. var clientRect = this._clientDiv.getBoundingClientRect();
  8528. var viewRect = this._viewDiv.getBoundingClientRect();
  8529. var left = viewRect.left + viewPad.left;
  8530. var right = clientRect.right;
  8531. var top = viewRect.top + viewPad.top;
  8532. var bottom = clientRect.bottom;
  8533. var hd = 0, vd = 0;
  8534. if (this._clipDiv) {
  8535. var clipRect = this._clipDiv.getBoundingClientRect();
  8536. hd = clipRect.left - this._clipDiv.scrollLeft;
  8537. vd = clipRect.top;
  8538. }
  8539. var r;
  8540. var endLineBounds = this._getLineBoundingClientRect(endNode);
  8541. if (endOffset === 0) {
  8542. r = endLineBounds.left;
  8543. } else {
  8544. if (endOffset >= endLineEnd) {
  8545. r = endLineBounds.right;
  8546. } else {
  8547. this._ignoreDOMSelection = true;
  8548. r = this._getBoundsAtOffset(model.getLineStart(endNode.lineIndex) + endOffset).left;
  8549. this._ignoreDOMSelection = false;
  8550. }
  8551. }
  8552. var sel1Div = this._selDiv1;
  8553. var sel1Left = Math.min(right, Math.max(left, l));
  8554. var sel1Top = Math.min(bottom, Math.max(top, startLineBounds.top));
  8555. var sel1Right = right;
  8556. var sel1Bottom = Math.min(bottom, Math.max(top, startLineBounds.bottom));
  8557. sel1Div.style.left = (sel1Left - hd) + "px";
  8558. sel1Div.style.top = (sel1Top - vd) + "px";
  8559. sel1Div.style.width = Math.max(0, sel1Right - sel1Left) + "px";
  8560. sel1Div.style.height = Math.max(0, sel1Bottom - sel1Top) + (isPad ? 1 : 0) + "px";
  8561. if (isPad) {
  8562. sel1Div.style.borderLeft = handleBorder;
  8563. sel1Div.style.borderRight = "0px";
  8564. }
  8565. if (startNode === endNode) {
  8566. sel1Right = Math.min(r, right);
  8567. sel1Div.style.width = Math.max(0, sel1Right - sel1Left - handleWidth * 2) + "px";
  8568. if (isPad) {
  8569. sel1Div.style.borderRight = handleBorder;
  8570. }
  8571. } else {
  8572. var sel3Left = left;
  8573. var sel3Top = Math.min(bottom, Math.max(top, endLineBounds.top));
  8574. var sel3Right = Math.min(right, Math.max(left, r));
  8575. var sel3Bottom = Math.min(bottom, Math.max(top, endLineBounds.bottom));
  8576. var sel3Div = this._selDiv3;
  8577. sel3Div.style.left = (sel3Left - hd) + "px";
  8578. sel3Div.style.top = (sel3Top - vd) + "px";
  8579. sel3Div.style.width = Math.max(0, sel3Right - sel3Left - handleWidth) + "px";
  8580. sel3Div.style.height = Math.max(0, sel3Bottom - sel3Top) + "px";
  8581. if (isPad) {
  8582. sel3Div.style.borderRight = handleBorder;
  8583. }
  8584. if (sel3Top - sel1Bottom > 0) {
  8585. var sel2Div = this._selDiv2;
  8586. sel2Div.style.left = (left - hd) + "px";
  8587. sel2Div.style.top = (sel1Bottom - vd) + "px";
  8588. sel2Div.style.width = Math.max(0, right - left) + "px";
  8589. sel2Div.style.height = Math.max(0, sel3Top - sel1Bottom) + (isPad ? 1 : 0) + "px";
  8590. }
  8591. }
  8592. }
  8593. }
  8594. },
  8595. _setGrab: function (target) {
  8596. if (target === this._grabControl) { return; }
  8597. if (target) {
  8598. if (target.setCapture) { target.setCapture(); }
  8599. this._grabControl = target;
  8600. } else {
  8601. if (this._grabControl.releaseCapture) { this._grabControl.releaseCapture(); }
  8602. this._grabControl = null;
  8603. }
  8604. },
  8605. _setLinksVisible: function(visible) {
  8606. if (this._linksVisible === visible) { return; }
  8607. this._linksVisible = visible;
  8608. /*
  8609. * Feature in IE. The client div looses focus and does not regain it back
  8610. * when the content editable flag is reset. The fix is to remember that it
  8611. * had focus when the flag is cleared and give focus back to the div when
  8612. * the flag is set.
  8613. */
  8614. if (isIE && visible) {
  8615. this._hadFocus = this._hasFocus;
  8616. }
  8617. var clientDiv = this._clientDiv;
  8618. clientDiv.contentEditable = !visible;
  8619. if (this._hadFocus && !visible) {
  8620. clientDiv.focus();
  8621. }
  8622. if (this._overlayDiv) {
  8623. this._overlayDiv.style.zIndex = visible ? "-1" : "1";
  8624. }
  8625. var document = this._frameDocument;
  8626. var line = this._getLineNext();
  8627. while (line) {
  8628. if (line.hasLink) {
  8629. var lineChild = line.firstChild;
  8630. while (lineChild) {
  8631. var next = lineChild.nextSibling;
  8632. var style = lineChild.viewStyle;
  8633. if (style && style.tagName === "A") {
  8634. line.replaceChild(this._createSpan(line, document, lineChild.firstChild.data, style), lineChild);
  8635. }
  8636. lineChild = next;
  8637. }
  8638. }
  8639. line = this._getLineNext(line);
  8640. }
  8641. },
  8642. _setSelection: function (selection, scroll, update, pageScroll) {
  8643. if (selection) {
  8644. this._columnX = -1;
  8645. if (update === undefined) { update = true; }
  8646. var oldSelection = this._selection;
  8647. if (!oldSelection.equals(selection)) {
  8648. this._selection = selection;
  8649. var e = {
  8650. type: "Selection",
  8651. oldValue: {start:oldSelection.start, end:oldSelection.end},
  8652. newValue: {start:selection.start, end:selection.end}
  8653. };
  8654. this.onSelection(e);
  8655. }
  8656. /*
  8657. * Always showCaret(), even when the selection is not changing, to ensure the
  8658. * caret is visible. Note that some views do not scroll to show the caret during
  8659. * keyboard navigation when the selection does not chanage. For example, line down
  8660. * when the caret is already at the last line.
  8661. */
  8662. if (scroll) { update = !this._showCaret(false, pageScroll); }
  8663. /*
  8664. * Sometimes the browser changes the selection
  8665. * as result of method calls or "leaked" events.
  8666. * The fix is to set the visual selection even
  8667. * when the logical selection is not changed.
  8668. */
  8669. if (update) { this._updateDOMSelection(); }
  8670. }
  8671. },
  8672. _setSelectionTo: function (x, y, extent, drag) {
  8673. var model = this._model, offset;
  8674. var selection = this._getSelection();
  8675. var lineIndex = this._getYToLine(y);
  8676. if (this._clickCount === 1) {
  8677. offset = this._getXToOffset(lineIndex, x);
  8678. if (drag && !extent) {
  8679. if (selection.start <= offset && offset < selection.end) {
  8680. this._dragOffset = offset;
  8681. return false;
  8682. }
  8683. }
  8684. selection.extend(offset);
  8685. if (!extent) { selection.collapse(); }
  8686. } else {
  8687. var word = (this._clickCount & 1) === 0;
  8688. var start, end;
  8689. if (word) {
  8690. offset = this._getXToOffset(lineIndex, x);
  8691. if (this._doubleClickSelection) {
  8692. if (offset >= this._doubleClickSelection.start) {
  8693. start = this._doubleClickSelection.start;
  8694. end = this._getOffset(offset, "wordend", +1);
  8695. } else {
  8696. start = this._getOffset(offset, "word", -1);
  8697. end = this._doubleClickSelection.end;
  8698. }
  8699. } else {
  8700. start = this._getOffset(offset, "word", -1);
  8701. end = this._getOffset(start, "wordend", +1);
  8702. }
  8703. } else {
  8704. if (this._doubleClickSelection) {
  8705. var doubleClickLine = model.getLineAtOffset(this._doubleClickSelection.start);
  8706. if (lineIndex >= doubleClickLine) {
  8707. start = model.getLineStart(doubleClickLine);
  8708. end = model.getLineEnd(lineIndex);
  8709. } else {
  8710. start = model.getLineStart(lineIndex);
  8711. end = model.getLineEnd(doubleClickLine);
  8712. }
  8713. } else {
  8714. start = model.getLineStart(lineIndex);
  8715. end = model.getLineEnd(lineIndex);
  8716. }
  8717. }
  8718. selection.setCaret(start);
  8719. selection.extend(end);
  8720. }
  8721. this._setSelection(selection, true, true);
  8722. return true;
  8723. },
  8724. _setStyleSheet: function(stylesheet) {
  8725. var oldstylesheet = this._stylesheet;
  8726. if (!(oldstylesheet instanceof Array)) {
  8727. oldstylesheet = [oldstylesheet];
  8728. }
  8729. this._stylesheet = stylesheet;
  8730. if (!(stylesheet instanceof Array)) {
  8731. stylesheet = [stylesheet];
  8732. }
  8733. var document = this._frameDocument;
  8734. var documentStylesheet = document.styleSheets;
  8735. var head = document.getElementsByTagName("head")[0];
  8736. var changed = false;
  8737. var i = 0, sheet, oldsheet, documentSheet, ownerNode, styleNode, textNode;
  8738. while (i < stylesheet.length) {
  8739. if (i >= oldstylesheet.length) { break; }
  8740. sheet = stylesheet[i];
  8741. oldsheet = oldstylesheet[i];
  8742. if (sheet !== oldsheet) {
  8743. if (this._isLinkURL(sheet)) {
  8744. return true;
  8745. } else {
  8746. documentSheet = documentStylesheet[i+1];
  8747. ownerNode = documentSheet.ownerNode;
  8748. styleNode = document.createElement('STYLE');
  8749. textNode = document.createTextNode(sheet);
  8750. styleNode.appendChild(textNode);
  8751. head.replaceChild(styleNode, ownerNode);
  8752. changed = true;
  8753. }
  8754. }
  8755. i++;
  8756. }
  8757. if (i < oldstylesheet.length) {
  8758. while (i < oldstylesheet.length) {
  8759. sheet = oldstylesheet[i];
  8760. if (this._isLinkURL(sheet)) {
  8761. return true;
  8762. } else {
  8763. documentSheet = documentStylesheet[i+1];
  8764. ownerNode = documentSheet.ownerNode;
  8765. head.removeChild(ownerNode);
  8766. changed = true;
  8767. }
  8768. i++;
  8769. }
  8770. } else {
  8771. while (i < stylesheet.length) {
  8772. sheet = stylesheet[i];
  8773. if (this._isLinkURL(sheet)) {
  8774. return true;
  8775. } else {
  8776. styleNode = document.createElement('STYLE');
  8777. textNode = document.createTextNode(sheet);
  8778. styleNode.appendChild(textNode);
  8779. head.appendChild(styleNode);
  8780. changed = true;
  8781. }
  8782. i++;
  8783. }
  8784. }
  8785. if (changed) {
  8786. this._updateStyle();
  8787. }
  8788. return false;
  8789. },
  8790. _setFullSelection: function(fullSelection, init) {
  8791. this._fullSelection = fullSelection;
  8792. /*
  8793. * Bug in IE 8. For some reason, during scrolling IE does not reflow the elements
  8794. * that are used to compute the location for the selection divs. This causes the
  8795. * divs to be placed at the wrong location. The fix is to disabled full selection for IE8.
  8796. */
  8797. if (isIE < 9) {
  8798. this._fullSelection = false;
  8799. }
  8800. if (isWebkit) {
  8801. this._fullSelection = true;
  8802. }
  8803. var parent = this._clipDiv || this._scrollDiv;
  8804. if (!parent) {
  8805. return;
  8806. }
  8807. if (!isPad && !this._fullSelection) {
  8808. if (this._selDiv1) {
  8809. parent.removeChild(this._selDiv1);
  8810. this._selDiv1 = null;
  8811. }
  8812. if (this._selDiv2) {
  8813. parent.removeChild(this._selDiv2);
  8814. this._selDiv2 = null;
  8815. }
  8816. if (this._selDiv3) {
  8817. parent.removeChild(this._selDiv3);
  8818. this._selDiv3 = null;
  8819. }
  8820. return;
  8821. }
  8822. if (!this._selDiv1 && (isPad || (this._fullSelection && !isWebkit))) {
  8823. var frameDocument = this._frameDocument;
  8824. this._hightlightRGB = "Highlight";
  8825. var selDiv1 = frameDocument.createElement("DIV");
  8826. this._selDiv1 = selDiv1;
  8827. selDiv1.id = "selDiv1";
  8828. selDiv1.style.position = this._clipDiv ? "absolute" : "fixed";
  8829. selDiv1.style.borderWidth = "0px";
  8830. selDiv1.style.margin = "0px";
  8831. selDiv1.style.padding = "0px";
  8832. selDiv1.style.outline = "none";
  8833. selDiv1.style.background = this._hightlightRGB;
  8834. selDiv1.style.width = "0px";
  8835. selDiv1.style.height = "0px";
  8836. selDiv1.style.zIndex = "0";
  8837. parent.appendChild(selDiv1);
  8838. var selDiv2 = frameDocument.createElement("DIV");
  8839. this._selDiv2 = selDiv2;
  8840. selDiv2.id = "selDiv2";
  8841. selDiv2.style.position = this._clipDiv ? "absolute" : "fixed";
  8842. selDiv2.style.borderWidth = "0px";
  8843. selDiv2.style.margin = "0px";
  8844. selDiv2.style.padding = "0px";
  8845. selDiv2.style.outline = "none";
  8846. selDiv2.style.background = this._hightlightRGB;
  8847. selDiv2.style.width = "0px";
  8848. selDiv2.style.height = "0px";
  8849. selDiv2.style.zIndex = "0";
  8850. parent.appendChild(selDiv2);
  8851. var selDiv3 = frameDocument.createElement("DIV");
  8852. this._selDiv3 = selDiv3;
  8853. selDiv3.id = "selDiv3";
  8854. selDiv3.style.position = this._clipDiv ? "absolute" : "fixed";
  8855. selDiv3.style.borderWidth = "0px";
  8856. selDiv3.style.margin = "0px";
  8857. selDiv3.style.padding = "0px";
  8858. selDiv3.style.outline = "none";
  8859. selDiv3.style.background = this._hightlightRGB;
  8860. selDiv3.style.width = "0px";
  8861. selDiv3.style.height = "0px";
  8862. selDiv3.style.zIndex = "0";
  8863. parent.appendChild(selDiv3);
  8864. /*
  8865. * Bug in Firefox. The Highlight color is mapped to list selection
  8866. * background instead of the text selection background. The fix
  8867. * is to map known colors using a table or fallback to light blue.
  8868. */
  8869. if (isFirefox && isMac) {
  8870. var style = this._frameWindow.getComputedStyle(selDiv3, null);
  8871. var rgb = style.getPropertyValue("background-color");
  8872. switch (rgb) {
  8873. case "rgb(119, 141, 168)": rgb = "rgb(199, 208, 218)"; break;
  8874. case "rgb(127, 127, 127)": rgb = "rgb(198, 198, 198)"; break;
  8875. case "rgb(255, 193, 31)": rgb = "rgb(250, 236, 115)"; break;
  8876. case "rgb(243, 70, 72)": rgb = "rgb(255, 176, 139)"; break;
  8877. case "rgb(255, 138, 34)": rgb = "rgb(255, 209, 129)"; break;
  8878. case "rgb(102, 197, 71)": rgb = "rgb(194, 249, 144)"; break;
  8879. case "rgb(140, 78, 184)": rgb = "rgb(232, 184, 255)"; break;
  8880. default: rgb = "rgb(180, 213, 255)"; break;
  8881. }
  8882. this._hightlightRGB = rgb;
  8883. selDiv1.style.background = rgb;
  8884. selDiv2.style.background = rgb;
  8885. selDiv3.style.background = rgb;
  8886. if (!this._insertedSelRule) {
  8887. var styleSheet = frameDocument.styleSheets[0];
  8888. styleSheet.insertRule("::-moz-selection {background: " + rgb + "; }", 0);
  8889. this._insertedSelRule = true;
  8890. }
  8891. }
  8892. if (!init) {
  8893. this._updateDOMSelection();
  8894. }
  8895. }
  8896. },
  8897. _setTabSize: function (tabSize, init) {
  8898. this._tabSize = tabSize;
  8899. this._customTabSize = undefined;
  8900. var clientDiv = this._clientDiv;
  8901. if (isOpera) {
  8902. if (clientDiv) { clientDiv.style.OTabSize = this._tabSize+""; }
  8903. } else if (isFirefox >= 4) {
  8904. if (clientDiv) { clientDiv.style.MozTabSize = this._tabSize+""; }
  8905. } else if (this._tabSize !== 8) {
  8906. this._customTabSize = this._tabSize;
  8907. if (!init) {
  8908. this.redrawLines();
  8909. }
  8910. }
  8911. },
  8912. _setThemeClass: function (themeClass, init) {
  8913. this._themeClass = themeClass;
  8914. var document = this._frameDocument;
  8915. if (document) {
  8916. var viewContainerClass = "viewContainer";
  8917. if (this._themeClass) { viewContainerClass += " " + this._themeClass; }
  8918. document.body.className = viewContainerClass;
  8919. if (!init) {
  8920. this._updateStyle();
  8921. }
  8922. }
  8923. },
  8924. _showCaret: function (allSelection, pageScroll) {
  8925. if (!this._clientDiv) { return; }
  8926. var model = this._model;
  8927. var selection = this._getSelection();
  8928. var scroll = this._getScroll();
  8929. var caret = selection.getCaret();
  8930. var start = selection.start;
  8931. var end = selection.end;
  8932. var startLine = model.getLineAtOffset(start);
  8933. var endLine = model.getLineAtOffset(end);
  8934. var endInclusive = Math.max(Math.max(start, model.getLineStart(endLine)), end - 1);
  8935. var viewPad = this._getViewPadding();
  8936. var clientWidth = this._getClientWidth();
  8937. var leftEdge = viewPad.left;
  8938. var rightEdge = viewPad.left + clientWidth;
  8939. var bounds = this._getBoundsAtOffset(caret === start ? start : endInclusive);
  8940. var left = bounds.left;
  8941. var right = bounds.right;
  8942. var minScroll = clientWidth / 4;
  8943. if (allSelection && !selection.isEmpty() && startLine === endLine) {
  8944. bounds = this._getBoundsAtOffset(caret === end ? start : endInclusive);
  8945. var selectionWidth = caret === start ? bounds.right - left : right - bounds.left;
  8946. if ((clientWidth - minScroll) > selectionWidth) {
  8947. if (left > bounds.left) { left = bounds.left; }
  8948. if (right < bounds.right) { right = bounds.right; }
  8949. }
  8950. }
  8951. var viewRect = this._viewDiv.getBoundingClientRect();
  8952. left -= viewRect.left;
  8953. right -= viewRect.left;
  8954. var pixelX = 0;
  8955. if (left < leftEdge) {
  8956. pixelX = Math.min(left - leftEdge, -minScroll);
  8957. }
  8958. if (right > rightEdge) {
  8959. var maxScroll = this._scrollDiv.scrollWidth - scroll.x - clientWidth;
  8960. pixelX = Math.min(maxScroll, Math.max(right - rightEdge, minScroll));
  8961. }
  8962. var pixelY = 0;
  8963. var topIndex = this._getTopIndex(true);
  8964. var bottomIndex = this._getBottomIndex(true);
  8965. var caretLine = model.getLineAtOffset(caret);
  8966. var clientHeight = this._getClientHeight();
  8967. if (!(topIndex <= caretLine && caretLine <= bottomIndex)) {
  8968. var lineHeight = this._getLineHeight();
  8969. var selectionHeight = allSelection ? (endLine - startLine) * lineHeight : 0;
  8970. pixelY = caretLine * lineHeight;
  8971. pixelY -= scroll.y;
  8972. if (pixelY + lineHeight > clientHeight) {
  8973. pixelY -= clientHeight - lineHeight;
  8974. if (caret === start && start !== end) {
  8975. pixelY += Math.min(clientHeight - lineHeight, selectionHeight);
  8976. }
  8977. } else {
  8978. if (caret === end) {
  8979. pixelY -= Math.min (clientHeight - lineHeight, selectionHeight);
  8980. }
  8981. }
  8982. if (pageScroll) {
  8983. if (pageScroll > 0) {
  8984. if (pixelY > 0) {
  8985. pixelY = Math.max(pixelY, pageScroll);
  8986. }
  8987. } else {
  8988. if (pixelY < 0) {
  8989. pixelY = Math.min(pixelY, pageScroll);
  8990. }
  8991. }
  8992. }
  8993. }
  8994. if (pixelX !== 0 || pixelY !== 0) {
  8995. this._scrollView (pixelX, pixelY);
  8996. /*
  8997. * When the view scrolls it is possible that one of the scrollbars can show over the caret.
  8998. * Depending on the browser scrolling can be synchronous (Safari), in which case the change
  8999. * can be detected before showCaret() returns. When scrolling is asynchronous (most browsers),
  9000. * the detection is done during the next update page.
  9001. */
  9002. if (clientHeight !== this._getClientHeight() || clientWidth !== this._getClientWidth()) {
  9003. this._showCaret();
  9004. } else {
  9005. this._ensureCaretVisible = true;
  9006. }
  9007. return true;
  9008. }
  9009. return false;
  9010. },
  9011. _startIME: function () {
  9012. if (this._imeOffset !== -1) { return; }
  9013. var selection = this._getSelection();
  9014. if (!selection.isEmpty()) {
  9015. this._modifyContent({text: "", start: selection.start, end: selection.end}, true);
  9016. }
  9017. this._imeOffset = selection.start;
  9018. },
  9019. _unhookEvents: function() {
  9020. this._model.removeEventListener("Changing", this._modelListener.onChanging);
  9021. this._model.removeEventListener("Changed", this._modelListener.onChanged);
  9022. this._modelListener = null;
  9023. for (var i=0; i<this._handlers.length; i++) {
  9024. var h = this._handlers[i];
  9025. removeHandler(h.target, h.type, h.handler);
  9026. }
  9027. this._handlers = null;
  9028. },
  9029. _updateDOMSelection: function () {
  9030. if (this._ignoreDOMSelection) { return; }
  9031. if (!this._clientDiv) { return; }
  9032. var selection = this._getSelection();
  9033. var model = this._model;
  9034. var startLine = model.getLineAtOffset(selection.start);
  9035. var endLine = model.getLineAtOffset(selection.end);
  9036. var firstNode = this._getLineNext();
  9037. /*
  9038. * Bug in Firefox. For some reason, after a update page sometimes the
  9039. * firstChild returns null incorrectly. The fix is to ignore show selection.
  9040. */
  9041. if (!firstNode) { return; }
  9042. var lastNode = this._getLinePrevious();
  9043. var topNode, bottomNode, topOffset, bottomOffset;
  9044. if (startLine < firstNode.lineIndex) {
  9045. topNode = firstNode;
  9046. topOffset = 0;
  9047. } else if (startLine > lastNode.lineIndex) {
  9048. topNode = lastNode;
  9049. topOffset = 0;
  9050. } else {
  9051. topNode = this._getLineNode(startLine);
  9052. topOffset = selection.start - model.getLineStart(startLine);
  9053. }
  9054. if (endLine < firstNode.lineIndex) {
  9055. bottomNode = firstNode;
  9056. bottomOffset = 0;
  9057. } else if (endLine > lastNode.lineIndex) {
  9058. bottomNode = lastNode;
  9059. bottomOffset = 0;
  9060. } else {
  9061. bottomNode = this._getLineNode(endLine);
  9062. bottomOffset = selection.end - model.getLineStart(endLine);
  9063. }
  9064. this._setDOMSelection(topNode, topOffset, bottomNode, bottomOffset);
  9065. },
  9066. _updatePage: function(hScrollOnly) {
  9067. if (this._redrawCount > 0) { return; }
  9068. if (this._updateTimer) {
  9069. clearTimeout(this._updateTimer);
  9070. this._updateTimer = null;
  9071. hScrollOnly = false;
  9072. }
  9073. var clientDiv = this._clientDiv;
  9074. if (!clientDiv) { return; }
  9075. var model = this._model;
  9076. var scroll = this._getScroll();
  9077. var viewPad = this._getViewPadding();
  9078. var lineCount = model.getLineCount();
  9079. var lineHeight = this._getLineHeight();
  9080. var firstLine = Math.max(0, scroll.y) / lineHeight;
  9081. var topIndex = Math.floor(firstLine);
  9082. var lineStart = Math.max(0, topIndex - 1);
  9083. var top = Math.round((firstLine - lineStart) * lineHeight);
  9084. var partialY = this._partialY = Math.round((firstLine - topIndex) * lineHeight);
  9085. var scrollWidth, scrollHeight = lineCount * lineHeight;
  9086. var leftWidth, clientWidth, clientHeight;
  9087. if (hScrollOnly) {
  9088. clientWidth = this._getClientWidth();
  9089. clientHeight = this._getClientHeight();
  9090. leftWidth = this._leftDiv ? this._leftDiv.scrollWidth : 0;
  9091. scrollWidth = Math.max(this._maxLineWidth, clientWidth);
  9092. } else {
  9093. var document = this._frameDocument;
  9094. var frameWidth = this._getFrameWidth();
  9095. var frameHeight = this._getFrameHeight();
  9096. document.body.style.width = frameWidth + "px";
  9097. document.body.style.height = frameHeight + "px";
  9098. /* Update view height in order to have client height computed */
  9099. var viewDiv = this._viewDiv;
  9100. viewDiv.style.height = Math.max(0, (frameHeight - viewPad.top - viewPad.bottom)) + "px";
  9101. clientHeight = this._getClientHeight();
  9102. var linesPerPage = Math.floor((clientHeight + partialY) / lineHeight);
  9103. var bottomIndex = Math.min(topIndex + linesPerPage, lineCount - 1);
  9104. var lineEnd = Math.min(bottomIndex + 1, lineCount - 1);
  9105. var lineIndex, lineWidth;
  9106. var child = clientDiv.firstChild;
  9107. while (child) {
  9108. lineIndex = child.lineIndex;
  9109. var nextChild = child.nextSibling;
  9110. if (!(lineStart <= lineIndex && lineIndex <= lineEnd) || child.lineRemoved || child.lineIndex === -1) {
  9111. if (this._mouseWheelLine === child) {
  9112. child.style.display = "none";
  9113. child.lineIndex = -1;
  9114. } else {
  9115. clientDiv.removeChild(child);
  9116. }
  9117. }
  9118. child = nextChild;
  9119. }
  9120. child = this._getLineNext();
  9121. var frag = document.createDocumentFragment();
  9122. for (lineIndex=lineStart; lineIndex<=lineEnd; lineIndex++) {
  9123. if (!child || child.lineIndex > lineIndex) {
  9124. this._createLine(frag, null, document, lineIndex, model);
  9125. } else {
  9126. if (frag.firstChild) {
  9127. clientDiv.insertBefore(frag, child);
  9128. frag = document.createDocumentFragment();
  9129. }
  9130. if (child && child.lineChanged) {
  9131. child = this._createLine(frag, child, document, lineIndex, model);
  9132. child.lineChanged = false;
  9133. }
  9134. child = this._getLineNext(child);
  9135. }
  9136. }
  9137. if (frag.firstChild) { clientDiv.insertBefore(frag, child); }
  9138. /*
  9139. * Feature in WekKit. Webkit limits the width of the lines
  9140. * computed below to the width of the client div. This causes
  9141. * the lines to be wrapped even though "pre" is set. The fix
  9142. * is to set the width of the client div to a larger number
  9143. * before computing the lines width. Note that this value is
  9144. * reset to the appropriate value further down.
  9145. */
  9146. if (isWebkit) {
  9147. clientDiv.style.width = (0x7FFFF).toString() + "px";
  9148. }
  9149. var rect;
  9150. child = this._getLineNext();
  9151. while (child) {
  9152. lineWidth = child.lineWidth;
  9153. if (lineWidth === undefined) {
  9154. rect = this._getLineBoundingClientRect(child);
  9155. lineWidth = child.lineWidth = rect.right - rect.left;
  9156. }
  9157. if (lineWidth >= this._maxLineWidth) {
  9158. this._maxLineWidth = lineWidth;
  9159. this._maxLineIndex = child.lineIndex;
  9160. }
  9161. if (child.lineIndex === topIndex) { this._topChild = child; }
  9162. if (child.lineIndex === bottomIndex) { this._bottomChild = child; }
  9163. if (this._checkMaxLineIndex === child.lineIndex) { this._checkMaxLineIndex = -1; }
  9164. child = this._getLineNext(child);
  9165. }
  9166. if (this._checkMaxLineIndex !== -1) {
  9167. lineIndex = this._checkMaxLineIndex;
  9168. this._checkMaxLineIndex = -1;
  9169. if (0 <= lineIndex && lineIndex < lineCount) {
  9170. var dummy = this._createLine(clientDiv, null, document, lineIndex, model);
  9171. rect = this._getLineBoundingClientRect(dummy);
  9172. lineWidth = rect.right - rect.left;
  9173. if (lineWidth >= this._maxLineWidth) {
  9174. this._maxLineWidth = lineWidth;
  9175. this._maxLineIndex = lineIndex;
  9176. }
  9177. clientDiv.removeChild(dummy);
  9178. }
  9179. }
  9180. // Update rulers
  9181. this._updateRuler(this._leftDiv, topIndex, bottomIndex);
  9182. this._updateRuler(this._rightDiv, topIndex, bottomIndex);
  9183. leftWidth = this._leftDiv ? this._leftDiv.scrollWidth : 0;
  9184. var rightWidth = this._rightDiv ? this._rightDiv.scrollWidth : 0;
  9185. viewDiv.style.left = leftWidth + "px";
  9186. viewDiv.style.width = Math.max(0, frameWidth - leftWidth - rightWidth - viewPad.left - viewPad.right) + "px";
  9187. if (this._rightDiv) {
  9188. this._rightDiv.style.left = (frameWidth - rightWidth) + "px";
  9189. }
  9190. /* Need to set the height first in order for the width to consider the vertical scrollbar */
  9191. var scrollDiv = this._scrollDiv;
  9192. scrollDiv.style.height = scrollHeight + "px";
  9193. /*
  9194. * TODO if frameHeightWithoutHScrollbar < scrollHeight < frameHeightWithHScrollbar and the horizontal bar is visible,
  9195. * then the clientWidth is wrong because the vertical scrollbar is showing. To correct code should hide both scrollbars
  9196. * at this point.
  9197. */
  9198. clientWidth = this._getClientWidth();
  9199. var width = Math.max(this._maxLineWidth, clientWidth);
  9200. /*
  9201. * Except by IE 8 and earlier, all other browsers are not allocating enough space for the right padding
  9202. * in the scrollbar. It is possible this a bug since all other paddings are considered.
  9203. */
  9204. scrollWidth = width;
  9205. if (!isIE || isIE >= 9) { width += viewPad.right; }
  9206. scrollDiv.style.width = width + "px";
  9207. if (this._clipScrollDiv) {
  9208. this._clipScrollDiv.style.width = width + "px";
  9209. }
  9210. /* Get the left scroll after setting the width of the scrollDiv as this can change the horizontal scroll offset. */
  9211. scroll = this._getScroll();
  9212. var rulerHeight = clientHeight + viewPad.top + viewPad.bottom;
  9213. this._updateRulerSize(this._leftDiv, rulerHeight);
  9214. this._updateRulerSize(this._rightDiv, rulerHeight);
  9215. }
  9216. var left = scroll.x;
  9217. var clipDiv = this._clipDiv;
  9218. var overlayDiv = this._overlayDiv;
  9219. var clipLeft, clipTop;
  9220. if (clipDiv) {
  9221. clipDiv.scrollLeft = left;
  9222. clipLeft = leftWidth + viewPad.left;
  9223. clipTop = viewPad.top;
  9224. var clipWidth = clientWidth;
  9225. var clipHeight = clientHeight;
  9226. var clientLeft = 0, clientTop = -top;
  9227. if (scroll.x === 0) {
  9228. clipLeft -= viewPad.left;
  9229. clipWidth += viewPad.left;
  9230. clientLeft = viewPad.left;
  9231. }
  9232. if (scroll.x + clientWidth === scrollWidth) {
  9233. clipWidth += viewPad.right;
  9234. }
  9235. if (scroll.y === 0) {
  9236. clipTop -= viewPad.top;
  9237. clipHeight += viewPad.top;
  9238. clientTop += viewPad.top;
  9239. }
  9240. if (scroll.y + clientHeight === scrollHeight) {
  9241. clipHeight += viewPad.bottom;
  9242. }
  9243. clipDiv.style.left = clipLeft + "px";
  9244. clipDiv.style.top = clipTop + "px";
  9245. clipDiv.style.width = clipWidth + "px";
  9246. clipDiv.style.height = clipHeight + "px";
  9247. clientDiv.style.left = clientLeft + "px";
  9248. clientDiv.style.top = clientTop + "px";
  9249. clientDiv.style.width = scrollWidth + "px";
  9250. clientDiv.style.height = (clientHeight + top) + "px";
  9251. if (overlayDiv) {
  9252. overlayDiv.style.left = clientDiv.style.left;
  9253. overlayDiv.style.top = clientDiv.style.top;
  9254. overlayDiv.style.width = clientDiv.style.width;
  9255. overlayDiv.style.height = clientDiv.style.height;
  9256. }
  9257. } else {
  9258. clipLeft = left;
  9259. clipTop = top;
  9260. var clipRight = left + clientWidth;
  9261. var clipBottom = top + clientHeight;
  9262. if (clipLeft === 0) { clipLeft -= viewPad.left; }
  9263. if (clipTop === 0) { clipTop -= viewPad.top; }
  9264. if (clipRight === scrollWidth) { clipRight += viewPad.right; }
  9265. if (scroll.y + clientHeight === scrollHeight) { clipBottom += viewPad.bottom; }
  9266. clientDiv.style.clip = "rect(" + clipTop + "px," + clipRight + "px," + clipBottom + "px," + clipLeft + "px)";
  9267. clientDiv.style.left = (-left + leftWidth + viewPad.left) + "px";
  9268. clientDiv.style.width = (isWebkit ? scrollWidth : clientWidth + left) + "px";
  9269. if (!hScrollOnly) {
  9270. clientDiv.style.top = (-top + viewPad.top) + "px";
  9271. clientDiv.style.height = (clientHeight + top) + "px";
  9272. }
  9273. if (overlayDiv) {
  9274. overlayDiv.style.clip = clientDiv.style.clip;
  9275. overlayDiv.style.left = clientDiv.style.left;
  9276. overlayDiv.style.width = clientDiv.style.width;
  9277. if (!hScrollOnly) {
  9278. overlayDiv.style.top = clientDiv.style.top;
  9279. overlayDiv.style.height = clientDiv.style.height;
  9280. }
  9281. }
  9282. }
  9283. this._updateDOMSelection();
  9284. /*
  9285. * If the client height changed during the update page it means that scrollbar has either been shown or hidden.
  9286. * When this happens update page has to run again to ensure that the top and bottom lines div are correct.
  9287. *
  9288. * Note: On IE, updateDOMSelection() has to be called before getting the new client height because it
  9289. * forces the client area to be recomputed.
  9290. */
  9291. var ensureCaretVisible = this._ensureCaretVisible;
  9292. this._ensureCaretVisible = false;
  9293. if (clientHeight !== this._getClientHeight()) {
  9294. this._updatePage();
  9295. if (ensureCaretVisible) {
  9296. this._showCaret();
  9297. }
  9298. }
  9299. if (isPad) {
  9300. var self = this;
  9301. setTimeout(function() {self._resizeTouchDiv();}, 0);
  9302. }
  9303. },
  9304. _updateRulerSize: function (divRuler, rulerHeight) {
  9305. if (!divRuler) { return; }
  9306. var partialY = this._partialY;
  9307. var lineHeight = this._getLineHeight();
  9308. var cells = divRuler.firstChild.rows[0].cells;
  9309. for (var i = 0; i < cells.length; i++) {
  9310. var div = cells[i].firstChild;
  9311. var offset = lineHeight;
  9312. if (div._ruler.getOverview() === "page") { offset += partialY; }
  9313. div.style.top = -offset + "px";
  9314. div.style.height = (rulerHeight + offset) + "px";
  9315. div = div.nextSibling;
  9316. }
  9317. divRuler.style.height = rulerHeight + "px";
  9318. },
  9319. _updateRuler: function (divRuler, topIndex, bottomIndex) {
  9320. if (!divRuler) { return; }
  9321. var cells = divRuler.firstChild.rows[0].cells;
  9322. var lineHeight = this._getLineHeight();
  9323. var parentDocument = this._frameDocument;
  9324. var viewPad = this._getViewPadding();
  9325. for (var i = 0; i < cells.length; i++) {
  9326. var div = cells[i].firstChild;
  9327. var ruler = div._ruler;
  9328. if (div.rulerChanged) {
  9329. this._applyStyle(ruler.getRulerStyle(), div);
  9330. }
  9331. var widthDiv;
  9332. var child = div.firstChild;
  9333. if (child) {
  9334. widthDiv = child;
  9335. child = child.nextSibling;
  9336. } else {
  9337. widthDiv = parentDocument.createElement("DIV");
  9338. widthDiv.style.visibility = "hidden";
  9339. div.appendChild(widthDiv);
  9340. }
  9341. var lineIndex, annotation;
  9342. if (div.rulerChanged) {
  9343. if (widthDiv) {
  9344. lineIndex = -1;
  9345. annotation = ruler.getWidestAnnotation();
  9346. if (annotation) {
  9347. this._applyStyle(annotation.style, widthDiv);
  9348. if (annotation.html) {
  9349. widthDiv.innerHTML = annotation.html;
  9350. }
  9351. }
  9352. widthDiv.lineIndex = lineIndex;
  9353. widthDiv.style.height = (lineHeight + viewPad.top) + "px";
  9354. }
  9355. }
  9356. var overview = ruler.getOverview(), lineDiv, frag, annotations;
  9357. if (overview === "page") {
  9358. annotations = ruler.getAnnotations(topIndex, bottomIndex + 1);
  9359. while (child) {
  9360. lineIndex = child.lineIndex;
  9361. var nextChild = child.nextSibling;
  9362. if (!(topIndex <= lineIndex && lineIndex <= bottomIndex) || child.lineChanged) {
  9363. div.removeChild(child);
  9364. }
  9365. child = nextChild;
  9366. }
  9367. child = div.firstChild.nextSibling;
  9368. frag = parentDocument.createDocumentFragment();
  9369. for (lineIndex=topIndex; lineIndex<=bottomIndex; lineIndex++) {
  9370. if (!child || child.lineIndex > lineIndex) {
  9371. lineDiv = parentDocument.createElement("DIV");
  9372. annotation = annotations[lineIndex];
  9373. if (annotation) {
  9374. this._applyStyle(annotation.style, lineDiv);
  9375. if (annotation.html) {
  9376. lineDiv.innerHTML = annotation.html;
  9377. }
  9378. lineDiv.annotation = annotation;
  9379. }
  9380. lineDiv.lineIndex = lineIndex;
  9381. lineDiv.style.height = lineHeight + "px";
  9382. frag.appendChild(lineDiv);
  9383. } else {
  9384. if (frag.firstChild) {
  9385. div.insertBefore(frag, child);
  9386. frag = parentDocument.createDocumentFragment();
  9387. }
  9388. if (child) {
  9389. child = child.nextSibling;
  9390. }
  9391. }
  9392. }
  9393. if (frag.firstChild) { div.insertBefore(frag, child); }
  9394. } else {
  9395. var buttonHeight = isPad ? 0 : 17;
  9396. var clientHeight = this._getClientHeight ();
  9397. var lineCount = this._model.getLineCount ();
  9398. var contentHeight = lineHeight * lineCount;
  9399. var trackHeight = clientHeight + viewPad.top + viewPad.bottom - 2 * buttonHeight;
  9400. var divHeight;
  9401. if (contentHeight < trackHeight) {
  9402. divHeight = lineHeight;
  9403. } else {
  9404. divHeight = trackHeight / lineCount;
  9405. }
  9406. if (div.rulerChanged) {
  9407. var count = div.childNodes.length;
  9408. while (count > 1) {
  9409. div.removeChild(div.lastChild);
  9410. count--;
  9411. }
  9412. annotations = ruler.getAnnotations(0, lineCount);
  9413. frag = parentDocument.createDocumentFragment();
  9414. for (var prop in annotations) {
  9415. lineIndex = prop >>> 0;
  9416. if (lineIndex < 0) { continue; }
  9417. lineDiv = parentDocument.createElement("DIV");
  9418. annotation = annotations[prop];
  9419. this._applyStyle(annotation.style, lineDiv);
  9420. lineDiv.style.position = "absolute";
  9421. lineDiv.style.top = buttonHeight + lineHeight + Math.floor(lineIndex * divHeight) + "px";
  9422. if (annotation.html) {
  9423. lineDiv.innerHTML = annotation.html;
  9424. }
  9425. lineDiv.annotation = annotation;
  9426. lineDiv.lineIndex = lineIndex;
  9427. frag.appendChild(lineDiv);
  9428. }
  9429. div.appendChild(frag);
  9430. } else if (div._oldTrackHeight !== trackHeight) {
  9431. lineDiv = div.firstChild ? div.firstChild.nextSibling : null;
  9432. while (lineDiv) {
  9433. lineDiv.style.top = buttonHeight + lineHeight + Math.floor(lineDiv.lineIndex * divHeight) + "px";
  9434. lineDiv = lineDiv.nextSibling;
  9435. }
  9436. }
  9437. div._oldTrackHeight = trackHeight;
  9438. }
  9439. div.rulerChanged = false;
  9440. div = div.nextSibling;
  9441. }
  9442. },
  9443. _updateStyle: function () {
  9444. var document = this._frameDocument;
  9445. if (isIE) {
  9446. document.body.style.lineHeight = "normal";
  9447. }
  9448. this._lineHeight = this._calculateLineHeight();
  9449. this._viewPadding = this._calculatePadding();
  9450. if (isIE) {
  9451. document.body.style.lineHeight = this._lineHeight + "px";
  9452. }
  9453. this.redraw();
  9454. }
  9455. };//end prototype
  9456. mEventTarget.EventTarget.addMixin(TextView.prototype);
  9457. return {TextView: TextView};
  9458. });
  9459. /*******************************************************************************
  9460. * @license
  9461. * Copyright (c) 2010, 2011 IBM Corporation and others.
  9462. * All rights reserved. This program and the accompanying materials are made
  9463. * available under the terms of the Eclipse Public License v1.0
  9464. * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
  9465. * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
  9466. *
  9467. * Contributors:
  9468. * Felipe Heidrich (IBM Corporation) - initial API and implementation
  9469. * Silenio Quarti (IBM Corporation) - initial API and implementation
  9470. ******************************************************************************/
  9471. /*global define */
  9472. define("orion/textview/textDND", [], function() {
  9473. function TextDND(view, undoStack) {
  9474. this._view = view;
  9475. this._undoStack = undoStack;
  9476. this._dragSelection = null;
  9477. this._dropOffset = -1;
  9478. this._dropText = null;
  9479. var self = this;
  9480. this._listener = {
  9481. onDragStart: function (evt) {
  9482. self._onDragStart(evt);
  9483. },
  9484. onDragEnd: function (evt) {
  9485. self._onDragEnd(evt);
  9486. },
  9487. onDragEnter: function (evt) {
  9488. self._onDragEnter(evt);
  9489. },
  9490. onDragOver: function (evt) {
  9491. self._onDragOver(evt);
  9492. },
  9493. onDrop: function (evt) {
  9494. self._onDrop(evt);
  9495. },
  9496. onDestroy: function (evt) {
  9497. self._onDestroy(evt);
  9498. }
  9499. };
  9500. view.addEventListener("DragStart", this._listener.onDragStart);
  9501. view.addEventListener("DragEnd", this._listener.onDragEnd);
  9502. view.addEventListener("DragEnter", this._listener.onDragEnter);
  9503. view.addEventListener("DragOver", this._listener.onDragOver);
  9504. view.addEventListener("Drop", this._listener.onDrop);
  9505. view.addEventListener("Destroy", this._listener.onDestroy);
  9506. }
  9507. TextDND.prototype = {
  9508. destroy: function() {
  9509. var view = this._view;
  9510. if (!view) { return; }
  9511. view.removeEventListener("DragStart", this._listener.onDragStart);
  9512. view.removeEventListener("DragEnd", this._listener.onDragEnd);
  9513. view.removeEventListener("DragEnter", this._listener.onDragEnter);
  9514. view.removeEventListener("DragOver", this._listener.onDragOver);
  9515. view.removeEventListener("Drop", this._listener.onDrop);
  9516. view.removeEventListener("Destroy", this._listener.onDestroy);
  9517. this._view = null;
  9518. },
  9519. _onDestroy: function(e) {
  9520. this.destroy();
  9521. },
  9522. _onDragStart: function(e) {
  9523. var view = this._view;
  9524. var selection = view.getSelection();
  9525. var model = view.getModel();
  9526. if (model.getBaseModel) {
  9527. selection.start = model.mapOffset(selection.start);
  9528. selection.end = model.mapOffset(selection.end);
  9529. model = model.getBaseModel();
  9530. }
  9531. var text = model.getText(selection.start, selection.end);
  9532. if (text) {
  9533. this._dragSelection = selection;
  9534. e.event.dataTransfer.effectAllowed = "copyMove";
  9535. e.event.dataTransfer.setData("Text", text);
  9536. }
  9537. },
  9538. _onDragEnd: function(e) {
  9539. var view = this._view;
  9540. if (this._dragSelection) {
  9541. if (this._undoStack) { this._undoStack.startCompoundChange(); }
  9542. var move = e.event.dataTransfer.dropEffect === "move";
  9543. if (move) {
  9544. view.setText("", this._dragSelection.start, this._dragSelection.end);
  9545. }
  9546. if (this._dropText) {
  9547. var text = this._dropText;
  9548. var offset = this._dropOffset;
  9549. if (move) {
  9550. if (offset >= this._dragSelection.end) {
  9551. offset -= this._dragSelection.end - this._dragSelection.start;
  9552. } else if (offset >= this._dragSelection.start) {
  9553. offset = this._dragSelection.start;
  9554. }
  9555. }
  9556. view.setText(text, offset, offset);
  9557. view.setSelection(offset, offset + text.length);
  9558. this._dropText = null;
  9559. this._dropOffset = -1;
  9560. }
  9561. if (this._undoStack) { this._undoStack.endCompoundChange(); }
  9562. }
  9563. this._dragSelection = null;
  9564. },
  9565. _onDragEnter: function(e) {
  9566. this._onDragOver(e);
  9567. },
  9568. _onDragOver: function(e) {
  9569. var types = e.event.dataTransfer.types;
  9570. if (types) {
  9571. var allowed = types.contains ? types.contains("text/plain") : types.indexOf("text/plain") !== -1;
  9572. if (!allowed) {
  9573. e.event.dataTransfer.dropEffect = "none";
  9574. }
  9575. }
  9576. },
  9577. _onDrop: function(e) {
  9578. var view = this._view;
  9579. var text = e.event.dataTransfer.getData("Text");
  9580. if (text) {
  9581. var offset = view.getOffsetAtLocation(e.x, e.y);
  9582. if (this._dragSelection) {
  9583. this._dropOffset = offset;
  9584. this._dropText = text;
  9585. } else {
  9586. view.setText(text, offset, offset);
  9587. view.setSelection(offset, offset + text.length);
  9588. }
  9589. }
  9590. }
  9591. };
  9592. return {TextDND: TextDND};
  9593. });/*******************************************************************************
  9594. * @license
  9595. * Copyright (c) 2011 IBM Corporation and others.
  9596. * All rights reserved. This program and the accompanying materials are made
  9597. * available under the terms of the Eclipse Public License v1.0
  9598. * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
  9599. * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
  9600. *
  9601. * Contributors: IBM Corporation - initial API and implementation
  9602. ******************************************************************************/
  9603. /*jslint */
  9604. /*global define */
  9605. define("orion/editor/htmlGrammar", [], function() {
  9606. /**
  9607. * Provides a grammar that can do some very rough syntax highlighting for HTML.
  9608. * @class orion.syntax.HtmlGrammar
  9609. */
  9610. function HtmlGrammar() {
  9611. /**
  9612. * Object containing the grammar rules.
  9613. * @public
  9614. * @type Object
  9615. */
  9616. return {
  9617. "name": "HTML",
  9618. "scopeName": "source.html",
  9619. "uuid": "3B5C76FB-EBB5-D930-F40C-047D082CE99B",
  9620. "patterns": [
  9621. // TODO unicode?
  9622. {
  9623. "match": "<!(doctype|DOCTYPE)[^>]+>",
  9624. "name": "entity.name.tag.doctype.html"
  9625. },
  9626. {
  9627. "begin": "<!--",
  9628. "end": "-->",
  9629. "beginCaptures": {
  9630. "0": { "name": "punctuation.definition.comment.html" }
  9631. },
  9632. "endCaptures": {
  9633. "0": { "name": "punctuation.definition.comment.html" }
  9634. },
  9635. "patterns": [
  9636. {
  9637. "match": "--",
  9638. "name": "invalid.illegal.badcomment.html"
  9639. }
  9640. ],
  9641. "contentName": "comment.block.html"
  9642. },
  9643. { // startDelimiter + tagName
  9644. "match": "<[A-Za-z0-9_\\-:]+(?= ?)",
  9645. "name": "entity.name.tag.html"
  9646. },
  9647. { "include": "#attrName" },
  9648. { "include": "#qString" },
  9649. { "include": "#qqString" },
  9650. // TODO attrName, qString, qqString should be applied first while inside a tag
  9651. { // startDelimiter + slash + tagName + endDelimiter
  9652. "match": "</[A-Za-z0-9_\\-:]+>",
  9653. "name": "entity.name.tag.html"
  9654. },
  9655. { // end delimiter of open tag
  9656. "match": ">",
  9657. "name": "entity.name.tag.html"
  9658. } ],
  9659. "repository": {
  9660. "attrName": { // attribute name
  9661. "match": "[A-Za-z\\-:]+(?=\\s*=\\s*['\"])",
  9662. "name": "entity.other.attribute.name.html"
  9663. },
  9664. "qqString": { // double quoted string
  9665. "match": "(\")[^\"]+(\")",
  9666. "name": "string.quoted.double.html"
  9667. },
  9668. "qString": { // single quoted string
  9669. "match": "(')[^']+(\')",
  9670. "name": "string.quoted.single.html"
  9671. }
  9672. }
  9673. };
  9674. }
  9675. return {HtmlGrammar: HtmlGrammar};
  9676. });
  9677. /*******************************************************************************
  9678. * @license
  9679. * Copyright (c) 2011 IBM Corporation and others.
  9680. * All rights reserved. This program and the accompanying materials are made
  9681. * available under the terms of the Eclipse Public License v1.0
  9682. * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
  9683. * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
  9684. *
  9685. * Contributors: IBM Corporation - initial API and implementation
  9686. ******************************************************************************/
  9687. /*jslint regexp:false laxbreak:true*/
  9688. /*global define */
  9689. define("orion/editor/textMateStyler", ['orion/editor/regex'], function(mRegex) {
  9690. var RegexUtil = {
  9691. // Rules to detect some unsupported Oniguruma features
  9692. unsupported: [
  9693. {regex: /\(\?[ims\-]:/, func: function(match) { return "option on/off for subexp"; }},
  9694. {regex: /\(\?<([=!])/, func: function(match) { return (match[1] === "=") ? "lookbehind" : "negative lookbehind"; }},
  9695. {regex: /\(\?>/, func: function(match) { return "atomic group"; }}
  9696. ],
  9697. /**
  9698. * @param {String} str String giving a regular expression pattern from a TextMate grammar.
  9699. * @param {String} [flags] [ismg]+
  9700. * @returns {RegExp}
  9701. */
  9702. toRegExp: function(str) {
  9703. function fail(feature, match) {
  9704. throw new Error("Unsupported regex feature \"" + feature + "\": \"" + match[0] + "\" at index: "
  9705. + match.index + " in " + match.input);
  9706. }
  9707. // Turns an extended regex pattern into a normal one
  9708. function normalize(/**String*/ str) {
  9709. var result = "";
  9710. var insideCharacterClass = false;
  9711. var len = str.length;
  9712. for (var i=0; i < len; ) {
  9713. var chr = str[i];
  9714. if (!insideCharacterClass && chr === "#") {
  9715. // skip to eol
  9716. while (i < len && chr !== "\r" && chr !== "\n") {
  9717. chr = str[++i];
  9718. }
  9719. } else if (!insideCharacterClass && /\s/.test(chr)) {
  9720. // skip whitespace
  9721. while (i < len && /\s/.test(chr)) {
  9722. chr = str[++i];
  9723. }
  9724. } else if (chr === "\\") {
  9725. result += chr;
  9726. if (!/\s/.test(str[i+1])) {
  9727. result += str[i+1];
  9728. i += 1;
  9729. }
  9730. i += 1;
  9731. } else if (chr === "[") {
  9732. insideCharacterClass = true;
  9733. result += chr;
  9734. i += 1;
  9735. } else if (chr === "]") {
  9736. insideCharacterClass = false;
  9737. result += chr;
  9738. i += 1;
  9739. } else {
  9740. result += chr;
  9741. i += 1;
  9742. }
  9743. }
  9744. return result;
  9745. }
  9746. var flags = "";
  9747. var i;
  9748. // Handle global "x" flag (whitespace/comments)
  9749. str = RegexUtil.processGlobalFlag("x", str, function(subexp) {
  9750. return normalize(subexp);
  9751. });
  9752. // Handle global "i" flag (case-insensitive)
  9753. str = RegexUtil.processGlobalFlag("i", str, function(subexp) {
  9754. flags += "i";
  9755. return subexp;
  9756. });
  9757. // Check for remaining unsupported syntax
  9758. for (i=0; i < this.unsupported.length; i++) {
  9759. var match;
  9760. if ((match = this.unsupported[i].regex.exec(str))) {
  9761. fail(this.unsupported[i].func(match), match);
  9762. }
  9763. }
  9764. return new RegExp(str, flags);
  9765. },
  9766. /**
  9767. * Checks if flag applies to entire pattern. If so, obtains replacement string by calling processor
  9768. * on the unwrapped pattern. Handles 2 possible syntaxes: (?f)pat and (?f:pat)
  9769. */
  9770. processGlobalFlag: function(/**String*/ flag, /**String*/ str, /**Function*/ processor) {
  9771. function getMatchingCloseParen(/*String*/pat, /*Number*/start) {
  9772. var depth = 0,
  9773. len = pat.length,
  9774. flagStop = -1;
  9775. for (var i=start; i < len && flagStop === -1; i++) {
  9776. switch (pat[i]) {
  9777. case "\\":
  9778. i++; // escape: skip next char
  9779. break;
  9780. case "(":
  9781. depth++;
  9782. break;
  9783. case ")":
  9784. depth--;
  9785. if (depth === 0) {
  9786. flagStop = i;
  9787. }
  9788. break;
  9789. }
  9790. }
  9791. return flagStop;
  9792. }
  9793. var flag1 = "(?" + flag + ")",
  9794. flag2 = "(?" + flag + ":";
  9795. if (str.substring(0, flag1.length) === flag1) {
  9796. return processor(str.substring(flag1.length));
  9797. } else if (str.substring(0, flag2.length) === flag2) {
  9798. var flagStop = getMatchingCloseParen(str, 0);
  9799. if (flagStop < str.length-1) {
  9800. throw new Error("Only a " + flag2 + ") group that encloses the entire regex is supported in: " + str);
  9801. }
  9802. return processor(str.substring(flag2.length, flagStop));
  9803. }
  9804. return str;
  9805. },
  9806. hasBackReference: function(/**RegExp*/ regex) {
  9807. return (/\\\d+/).test(regex.source);
  9808. },
  9809. /** @returns {RegExp} A regex made by substituting any backreferences in <code>regex</code> for the value of the property
  9810. * in <code>sub</code> with the same name as the backreferenced group number. */
  9811. getSubstitutedRegex: function(/**RegExp*/ regex, /**Object*/ sub, /**Boolean*/ escape) {
  9812. escape = (typeof escape === "undefined") ? true : false;
  9813. var exploded = regex.source.split(/(\\\d+)/g);
  9814. var array = [];
  9815. for (var i=0; i < exploded.length; i++) {
  9816. var term = exploded[i];
  9817. var backrefMatch = /\\(\d+)/.exec(term);
  9818. if (backrefMatch) {
  9819. var text = sub[backrefMatch[1]] || "";
  9820. array.push(escape ? mRegex.escape(text) : text);
  9821. } else {
  9822. array.push(term);
  9823. }
  9824. }
  9825. return new RegExp(array.join(""));
  9826. },
  9827. /**
  9828. * Builds a version of <code>regex</code> with every non-capturing term converted into a capturing group. This is a workaround
  9829. * for JavaScript's lack of API to get the index at which a matched group begins in the input string.<p>
  9830. * Using the "groupified" regex, we can sum the lengths of matches from <i>consuming groups</i> 1..n-1 to obtain the
  9831. * starting index of group n. (A consuming group is a capturing group that is not inside a lookahead assertion).</p>
  9832. * Example: groupify(/(a+)x+(b+)/) === /(a+)(x+)(b+)/<br />
  9833. * Example: groupify(/(?:x+(a+))b+/) === /(?:(x+)(a+))(b+)/
  9834. * @param {RegExp} regex The regex to groupify.
  9835. * @param {Object} [backRefOld2NewMap] Optional. If provided, the backreference numbers in regex will be updated using the
  9836. * properties of this object rather than the new group numbers of regex itself.
  9837. * <ul><li>[0] {RegExp} The groupified version of the input regex.</li>
  9838. * <li>[1] {Object} A map containing old-group to new-group info. Each property is a capturing group number of <code>regex</code>
  9839. * and its value is the corresponding capturing group number of [0].</li>
  9840. * <li>[2] {Object} A map indicating which capturing groups of [0] are also consuming groups. If a group number is found
  9841. * as a property in this object, then it's a consuming group.</li></ul>
  9842. */
  9843. groupify: function(regex, backRefOld2NewMap) {
  9844. var NON_CAPTURING = 1,
  9845. CAPTURING = 2,
  9846. LOOKAHEAD = 3,
  9847. NEW_CAPTURING = 4;
  9848. var src = regex.source,
  9849. len = src.length;
  9850. var groups = [],
  9851. lookaheadDepth = 0,
  9852. newGroups = [],
  9853. oldGroupNumber = 1,
  9854. newGroupNumber = 1;
  9855. var result = [],
  9856. old2New = {},
  9857. consuming = {};
  9858. for (var i=0; i < len; i++) {
  9859. var curGroup = groups[groups.length-1];
  9860. var chr = src[i];
  9861. switch (chr) {
  9862. case "(":
  9863. // If we're in new capturing group, close it since ( signals end-of-term
  9864. if (curGroup === NEW_CAPTURING) {
  9865. groups.pop();
  9866. result.push(")");
  9867. newGroups[newGroups.length-1].end = i;
  9868. }
  9869. var peek2 = (i + 2 < len) ? (src[i+1] + "" + src[i+2]) : null;
  9870. if (peek2 === "?:" || peek2 === "?=" || peek2 === "?!") {
  9871. // Found non-capturing group or lookahead assertion. Note that we preserve non-capturing groups
  9872. // as such, but any term inside them will become a new capturing group (unless it happens to
  9873. // also be inside a lookahead).
  9874. var groupType;
  9875. if (peek2 === "?:") {
  9876. groupType = NON_CAPTURING;
  9877. } else {
  9878. groupType = LOOKAHEAD;
  9879. lookaheadDepth++;
  9880. }
  9881. groups.push(groupType);
  9882. newGroups.push({ start: i, end: -1, type: groupType /*non capturing*/ });
  9883. result.push(chr);
  9884. result.push(peek2);
  9885. i += peek2.length;
  9886. } else {
  9887. groups.push(CAPTURING);
  9888. newGroups.push({ start: i, end: -1, type: CAPTURING, oldNum: oldGroupNumber, num: newGroupNumber });
  9889. result.push(chr);
  9890. if (lookaheadDepth === 0) {
  9891. consuming[newGroupNumber] = null;
  9892. }
  9893. old2New[oldGroupNumber] = newGroupNumber;
  9894. oldGroupNumber++;
  9895. newGroupNumber++;
  9896. }
  9897. break;
  9898. case ")":
  9899. var group = groups.pop();
  9900. if (group === LOOKAHEAD) { lookaheadDepth--; }
  9901. newGroups[newGroups.length-1].end = i;
  9902. result.push(chr);
  9903. break;
  9904. case "*":
  9905. case "+":
  9906. case "?":
  9907. case "}":
  9908. // Unary operator. If it's being applied to a capturing group, we need to add a new capturing group
  9909. // enclosing the pair
  9910. var op = chr;
  9911. var prev = src[i-1],
  9912. prevIndex = i-1;
  9913. if (chr === "}") {
  9914. for (var j=i-1; src[j] !== "{" && j >= 0; j--) {}
  9915. prev = src[j-1];
  9916. prevIndex = j-1;
  9917. op = src.substring(j, i+1);
  9918. }
  9919. var lastGroup = newGroups[newGroups.length-1];
  9920. if (prev === ")" && (lastGroup.type === CAPTURING || lastGroup.type === NEW_CAPTURING)) {
  9921. // Shove in the new group's (, increment num/start in from [lastGroup.start .. end]
  9922. result.splice(lastGroup.start, 0, "(");
  9923. result.push(op);
  9924. result.push(")");
  9925. var newGroup = { start: lastGroup.start, end: result.length-1, type: NEW_CAPTURING, num: lastGroup.num };
  9926. for (var k=0; k < newGroups.length; k++) {
  9927. group = newGroups[k];
  9928. if (group.type === CAPTURING || group.type === NEW_CAPTURING) {
  9929. if (group.start >= lastGroup.start && group.end <= prevIndex) {
  9930. group.start += 1;
  9931. group.end += 1;
  9932. group.num = group.num + 1;
  9933. if (group.type === CAPTURING) {
  9934. old2New[group.oldNum] = group.num;
  9935. }
  9936. }
  9937. }
  9938. }
  9939. newGroups.push(newGroup);
  9940. newGroupNumber++;
  9941. break;
  9942. } else {
  9943. // Fallthrough to default
  9944. }
  9945. default:
  9946. if (chr !== "|" && curGroup !== CAPTURING && curGroup !== NEW_CAPTURING) {
  9947. // Not in a capturing group, so make a new one to hold this term.
  9948. // Perf improvement: don't create the new group if we're inside a lookahead, since we don't
  9949. // care about them (nothing inside a lookahead actually consumes input so we don't need it)
  9950. if (lookaheadDepth === 0) {
  9951. groups.push(NEW_CAPTURING);
  9952. newGroups.push({ start: i, end: -1, type: NEW_CAPTURING, num: newGroupNumber });
  9953. result.push("(");
  9954. consuming[newGroupNumber] = null;
  9955. newGroupNumber++;
  9956. }
  9957. }
  9958. result.push(chr);
  9959. if (chr === "\\") {
  9960. var peek = src[i+1];
  9961. // Eat next so following iteration doesn't think it's a real special character
  9962. result.push(peek);
  9963. i += 1;
  9964. }
  9965. break;
  9966. }
  9967. }
  9968. while (groups.length) {
  9969. // Close any remaining new capturing groups
  9970. groups.pop();
  9971. result.push(")");
  9972. }
  9973. var newRegex = new RegExp(result.join(""));
  9974. // Update backreferences so they refer to the new group numbers. Use backRefOld2NewMap if provided
  9975. var subst = {};
  9976. backRefOld2NewMap = backRefOld2NewMap || old2New;
  9977. for (var prop in backRefOld2NewMap) {
  9978. if (backRefOld2NewMap.hasOwnProperty(prop)) {
  9979. subst[prop] = "\\" + backRefOld2NewMap[prop];
  9980. }
  9981. }
  9982. newRegex = this.getSubstitutedRegex(newRegex, subst, false);
  9983. return [newRegex, old2New, consuming];
  9984. },
  9985. /** @returns {Boolean} True if the captures object assigns scope to a matching group other than "0". */
  9986. complexCaptures: function(capturesObj) {
  9987. if (!capturesObj) { return false; }
  9988. for (var prop in capturesObj) {
  9989. if (capturesObj.hasOwnProperty(prop)) {
  9990. if (prop !== "0") {
  9991. return true;
  9992. }
  9993. }
  9994. }
  9995. return false;
  9996. }
  9997. };
  9998. /**
  9999. * @name orion.editor.TextMateStyler
  10000. * @class A styler that knows how to apply a subset of the TextMate grammar format to style a line.
  10001. *
  10002. * <h4>Styling from a grammar:</h4>
  10003. * <p>Each scope name given in the grammar is converted to an array of CSS class names. For example
  10004. * a region of text with scope <code>keyword.control.php</code> will be assigned the CSS classes<br />
  10005. * <code>keyword, keyword-control, keyword-control-php</code></p>
  10006. *
  10007. * <p>A CSS file can give rules matching any of these class names to provide generic or more specific styling.
  10008. * For example,</p>
  10009. * <p><code>.keyword { font-color: blue; }</code></p>
  10010. * <p>colors all keywords blue, while</p>
  10011. * <p><code>.keyword-control-php { font-weight: bold; }</code></p>
  10012. * <p>bolds only PHP control keywords.</p>
  10013. *
  10014. * <p>This is useful when using grammars that adhere to TextMate's
  10015. * <a href="http://manual.macromates.com/en/language_grammars.html#naming_conventions">scope name conventions</a>,
  10016. * as a single CSS rule can provide consistent styling to similar constructs across different languages.</p>
  10017. *
  10018. * <h4>Top-level grammar constructs:</h4>
  10019. * <ul><li><code>patterns, repository</code> (with limitations, see "Other Features") are supported.</li>
  10020. * <li><code>scopeName, firstLineMatch, foldingStartMarker, foldingStopMarker</code> are <b>not</b> supported.</li>
  10021. * <li><code>fileTypes</code> is <b>not</b> supported. When using the Orion service registry, the "orion.edit.highlighter"
  10022. * service serves a similar purpose.</li>
  10023. * </ul>
  10024. *
  10025. * <h4>Regular expression constructs:</h4>
  10026. * <ul>
  10027. * <li><code>match</code> patterns are supported.</li>
  10028. * <li><code>begin .. end</code> patterns are supported.</li>
  10029. * <li>The "extended" regex forms <code>(?x)</code> and <code>(?x:...)</code> are supported, but <b>only</b> when they
  10030. * apply to the entire regex pattern.</li>
  10031. * <li>Matching is done using native JavaScript <code>RegExp</code>s. As a result, many features of the Oniguruma regex
  10032. * engine used by TextMate are <b>not</b> supported.
  10033. * Unsupported features include:
  10034. * <ul><li>Named captures</li>
  10035. * <li>Setting flags inside subgroups (eg. <code>(?i:a)b</code>)</li>
  10036. * <li>Lookbehind and negative lookbehind</li>
  10037. * <li>Subexpression call</li>
  10038. * <li>etc.</li>
  10039. * </ul>
  10040. * </li>
  10041. * </ul>
  10042. *
  10043. * <h4>Scope-assignment constructs:</h4>
  10044. * <ul>
  10045. * <li><code>captures, beginCaptures, endCaptures</code> are supported.</li>
  10046. * <li><code>name</code> and <code>contentName</code> are supported.</li>
  10047. * </ul>
  10048. *
  10049. * <h4>Other features:</h4>
  10050. * <ul>
  10051. * <li><code>applyEndPatternLast</code> is supported.</li>
  10052. * <li><code>include</code> is supported, but only when it references a rule in the current grammar's <code>repository</code>.
  10053. * Including <code>$self</code>, <code>$base</code>, or <code>rule.from.another.grammar</code> is <b>not</b> supported.</li>
  10054. * </ul>
  10055. *
  10056. * @description Creates a new TextMateStyler.
  10057. * @extends orion.editor.AbstractStyler
  10058. * @param {orion.textview.TextView} textView The <code>TextView</code> to provide styling for.
  10059. * @param {Object} grammar The TextMate grammar to use for styling the <code>TextView</code>, as a JavaScript object. You can
  10060. * produce this object by running a PList-to-JavaScript conversion tool on a TextMate <code>.tmLanguage</code> file.
  10061. * @param {Object[]} [externalGrammars] Additional grammar objects that will be used to resolve named rule references.
  10062. */
  10063. function TextMateStyler(textView, grammar, externalGrammars) {
  10064. this.initialize(textView);
  10065. // Copy grammar object(s) since we will mutate them
  10066. this.grammar = this.copy(grammar);
  10067. this.externalGrammars = externalGrammars ? this.copy(externalGrammars) : [];
  10068. this._styles = {}; /* key: {String} scopeName, value: {String[]} cssClassNames */
  10069. this._tree = null;
  10070. this._allGrammars = {}; /* key: {String} scopeName of grammar, value: {Object} grammar */
  10071. this.preprocess(this.grammar);
  10072. }
  10073. TextMateStyler.prototype = /** @lends orion.editor.TextMateStyler.prototype */ {
  10074. initialize: function(textView) {
  10075. this.textView = textView;
  10076. var self = this;
  10077. this._listener = {
  10078. onModelChanged: function(e) {
  10079. self.onModelChanged(e);
  10080. },
  10081. onDestroy: function(e) {
  10082. self.onDestroy(e);
  10083. },
  10084. onLineStyle: function(e) {
  10085. self.onLineStyle(e);
  10086. }
  10087. };
  10088. textView.addEventListener("ModelChanged", this._listener.onModelChanged);
  10089. textView.addEventListener("Destroy", this._listener.onDestroy);
  10090. textView.addEventListener("LineStyle", this._listener.onLineStyle);
  10091. textView.redrawLines();
  10092. },
  10093. onDestroy: function(/**eclipse.DestroyEvent*/ e) {
  10094. this.destroy();
  10095. },
  10096. destroy: function() {
  10097. if (this.textView) {
  10098. this.textView.removeEventListener("ModelChanged", this._listener.onModelChanged);
  10099. this.textView.removeEventListener("Destroy", this._listener.onDestroy);
  10100. this.textView.removeEventListener("LineStyle", this._listener.onLineStyle);
  10101. this.textView = null;
  10102. }
  10103. this.grammar = null;
  10104. this._styles = null;
  10105. this._tree = null;
  10106. this._listener = null;
  10107. },
  10108. /** @private */
  10109. copy: function(obj) {
  10110. return JSON.parse(JSON.stringify(obj));
  10111. },
  10112. /** @private */
  10113. preprocess: function(grammar) {
  10114. var stack = [grammar];
  10115. for (; stack.length !== 0; ) {
  10116. var rule = stack.pop();
  10117. if (rule._resolvedRule && rule._typedRule) {
  10118. continue;
  10119. }
  10120. // console.debug("Process " + (rule.include || rule.name));
  10121. // Look up include'd rule, create typed *Rule instance
  10122. rule._resolvedRule = this._resolve(rule);
  10123. rule._typedRule = this._createTypedRule(rule);
  10124. // Convert the scope names to styles and cache them for later
  10125. this.addStyles(rule.name);
  10126. this.addStyles(rule.contentName);
  10127. this.addStylesForCaptures(rule.captures);
  10128. this.addStylesForCaptures(rule.beginCaptures);
  10129. this.addStylesForCaptures(rule.endCaptures);
  10130. if (rule._resolvedRule !== rule) {
  10131. // Add include target
  10132. stack.push(rule._resolvedRule);
  10133. }
  10134. if (rule.patterns) {
  10135. // Add subrules
  10136. for (var i=0; i < rule.patterns.length; i++) {
  10137. stack.push(rule.patterns[i]);
  10138. }
  10139. }
  10140. }
  10141. },
  10142. /**
  10143. * @private
  10144. * Adds eclipse.Style objects for scope to our _styles cache.
  10145. * @param {String} scope A scope name, like "constant.character.php".
  10146. */
  10147. addStyles: function(scope) {
  10148. if (scope && !this._styles[scope]) {
  10149. this._styles[scope] = [];
  10150. var scopeArray = scope.split(".");
  10151. for (var i = 0; i < scopeArray.length; i++) {
  10152. this._styles[scope].push(scopeArray.slice(0, i + 1).join("-"));
  10153. }
  10154. }
  10155. },
  10156. /** @private */
  10157. addStylesForCaptures: function(/**Object*/ captures) {
  10158. for (var prop in captures) {
  10159. if (captures.hasOwnProperty(prop)) {
  10160. var scope = captures[prop].name;
  10161. this.addStyles(scope);
  10162. }
  10163. }
  10164. },
  10165. /**
  10166. * A rule that contains subrules ("patterns" in TextMate parlance) but has no "begin" or "end".
  10167. * Also handles top level of grammar.
  10168. * @private
  10169. */
  10170. ContainerRule: (function() {
  10171. function ContainerRule(/**Object*/ rule) {
  10172. this.rule = rule;
  10173. this.subrules = rule.patterns;
  10174. }
  10175. ContainerRule.prototype.valueOf = function() { return "aa"; };
  10176. return ContainerRule;
  10177. }()),
  10178. /**
  10179. * A rule that is delimited by "begin" and "end" matches, which may be separated by any number of
  10180. * lines. This type of rule may contain subrules, which apply only inside the begin .. end region.
  10181. * @private
  10182. */
  10183. BeginEndRule: (function() {
  10184. function BeginEndRule(/**Object*/ rule) {
  10185. this.rule = rule;
  10186. // TODO: the TextMate blog claims that "end" is optional.
  10187. this.beginRegex = RegexUtil.toRegExp(rule.begin);
  10188. this.endRegex = RegexUtil.toRegExp(rule.end);
  10189. this.subrules = rule.patterns || [];
  10190. this.endRegexHasBackRef = RegexUtil.hasBackReference(this.endRegex);
  10191. // Deal with non-0 captures
  10192. var complexCaptures = RegexUtil.complexCaptures(rule.captures);
  10193. var complexBeginEnd = RegexUtil.complexCaptures(rule.beginCaptures) || RegexUtil.complexCaptures(rule.endCaptures);
  10194. this.isComplex = complexCaptures || complexBeginEnd;
  10195. if (this.isComplex) {
  10196. var bg = RegexUtil.groupify(this.beginRegex);
  10197. this.beginRegex = bg[0];
  10198. this.beginOld2New = bg[1];
  10199. this.beginConsuming = bg[2];
  10200. var eg = RegexUtil.groupify(this.endRegex, this.beginOld2New /*Update end's backrefs to begin's new group #s*/);
  10201. this.endRegex = eg[0];
  10202. this.endOld2New = eg[1];
  10203. this.endConsuming = eg[2];
  10204. }
  10205. }
  10206. BeginEndRule.prototype.valueOf = function() { return this.beginRegex; };
  10207. return BeginEndRule;
  10208. }()),
  10209. /**
  10210. * A rule with a "match" pattern.
  10211. * @private
  10212. */
  10213. MatchRule: (function() {
  10214. function MatchRule(/**Object*/ rule) {
  10215. this.rule = rule;
  10216. this.matchRegex = RegexUtil.toRegExp(rule.match);
  10217. this.isComplex = RegexUtil.complexCaptures(rule.captures);
  10218. if (this.isComplex) {
  10219. var mg = RegexUtil.groupify(this.matchRegex);
  10220. this.matchRegex = mg[0];
  10221. this.matchOld2New = mg[1];
  10222. this.matchConsuming = mg[2];
  10223. }
  10224. }
  10225. MatchRule.prototype.valueOf = function() { return this.matchRegex; };
  10226. return MatchRule;
  10227. }()),
  10228. /**
  10229. * @param {Object} rule A rule from the grammar.
  10230. * @returns {MatchRule|BeginEndRule|ContainerRule}
  10231. * @private
  10232. */
  10233. _createTypedRule: function(rule) {
  10234. if (rule.match) {
  10235. return new this.MatchRule(rule);
  10236. } else if (rule.begin) {
  10237. return new this.BeginEndRule(rule);
  10238. } else {
  10239. return new this.ContainerRule(rule);
  10240. }
  10241. },
  10242. /**
  10243. * Resolves a rule from the grammar (which may be an include) into the real rule that it points to.
  10244. * @private
  10245. */
  10246. _resolve: function(rule) {
  10247. var resolved = rule;
  10248. if (rule.include) {
  10249. if (rule.begin || rule.end || rule.match) {
  10250. throw new Error("Unexpected regex pattern in \"include\" rule " + rule.include);
  10251. }
  10252. var name = rule.include;
  10253. if (name[0] === "#") {
  10254. resolved = this.grammar.repository && this.grammar.repository[name.substring(1)];
  10255. if (!resolved) { throw new Error("Couldn't find included rule " + name + " in grammar repository"); }
  10256. } else if (name === "$self") {
  10257. resolved = this.grammar;
  10258. } else if (name === "$base") {
  10259. // $base is only relevant when including rules from foreign grammars
  10260. throw new Error("Include \"$base\" is not supported");
  10261. } else {
  10262. resolved = this._allGrammars[name];
  10263. if (!resolved) {
  10264. for (var i=0; i < this.externalGrammars.length; i++) {
  10265. var grammar = this.externalGrammars[i];
  10266. if (grammar.scopeName === name) {
  10267. this.preprocess(grammar);
  10268. this._allGrammars[name] = grammar;
  10269. resolved = grammar;
  10270. break;
  10271. }
  10272. }
  10273. }
  10274. }
  10275. }
  10276. return resolved;
  10277. },
  10278. /** @private */
  10279. ContainerNode: (function() {
  10280. function ContainerNode(parent, rule) {
  10281. this.parent = parent;
  10282. this.rule = rule;
  10283. this.children = [];
  10284. this.start = null;
  10285. this.end = null;
  10286. }
  10287. ContainerNode.prototype.addChild = function(child) {
  10288. this.children.push(child);
  10289. };
  10290. ContainerNode.prototype.valueOf = function() {
  10291. var r = this.rule;
  10292. return "ContainerNode { " + (r.include || "") + " " + (r.name || "") + (r.comment || "") + "}";
  10293. };
  10294. return ContainerNode;
  10295. }()),
  10296. /** @private */
  10297. BeginEndNode: (function() {
  10298. function BeginEndNode(parent, rule, beginMatch) {
  10299. this.parent = parent;
  10300. this.rule = rule;
  10301. this.children = [];
  10302. this.setStart(beginMatch);
  10303. this.end = null; // will be set eventually during parsing (may be EOF)
  10304. this.endMatch = null; // may remain null if we never match our "end" pattern
  10305. // Build a new regex if the "end" regex has backrefs since they refer to matched groups of beginMatch
  10306. if (rule.endRegexHasBackRef) {
  10307. this.endRegexSubstituted = RegexUtil.getSubstitutedRegex(rule.endRegex, beginMatch);
  10308. } else {
  10309. this.endRegexSubstituted = null;
  10310. }
  10311. }
  10312. BeginEndNode.prototype.addChild = function(child) {
  10313. this.children.push(child);
  10314. };
  10315. /** @return {Number} This node's index in its parent's "children" list */
  10316. BeginEndNode.prototype.getIndexInParent = function(node) {
  10317. return this.parent ? this.parent.children.indexOf(this) : -1;
  10318. };
  10319. /** @param {RegExp.match} beginMatch */
  10320. BeginEndNode.prototype.setStart = function(beginMatch) {
  10321. this.start = beginMatch.index;
  10322. this.beginMatch = beginMatch;
  10323. };
  10324. /** @param {RegExp.match|Number} endMatchOrLastChar */
  10325. BeginEndNode.prototype.setEnd = function(endMatchOrLastChar) {
  10326. if (endMatchOrLastChar && typeof(endMatchOrLastChar) === "object") {
  10327. var endMatch = endMatchOrLastChar;
  10328. this.endMatch = endMatch;
  10329. this.end = endMatch.index + endMatch[0].length;
  10330. } else {
  10331. var lastChar = endMatchOrLastChar;
  10332. this.endMatch = null;
  10333. this.end = lastChar;
  10334. }
  10335. };
  10336. BeginEndNode.prototype.shiftStart = function(amount) {
  10337. this.start += amount;
  10338. this.beginMatch.index += amount;
  10339. };
  10340. BeginEndNode.prototype.shiftEnd = function(amount) {
  10341. this.end += amount;
  10342. if (this.endMatch) { this.endMatch.index += amount; }
  10343. };
  10344. BeginEndNode.prototype.valueOf = function() {
  10345. return "{" + this.rule.beginRegex + " range=" + this.start + ".." + this.end + "}";
  10346. };
  10347. return BeginEndNode;
  10348. }()),
  10349. /** Pushes rules onto stack such that rules[startFrom] is on top
  10350. * @private
  10351. */
  10352. push: function(/**Array*/ stack, /**Array*/ rules) {
  10353. if (!rules) { return; }
  10354. for (var i = rules.length; i > 0; ) {
  10355. stack.push(rules[--i]);
  10356. }
  10357. },
  10358. /** Executes <code>regex</code> on <code>text</code>, and returns the match object with its index
  10359. * offset by the given amount.
  10360. * @returns {RegExp.match}
  10361. * @private
  10362. */
  10363. exec: function(/**RegExp*/ regex, /**String*/ text, /**Number*/ offset) {
  10364. var match = regex.exec(text);
  10365. if (match) { match.index += offset; }
  10366. regex.lastIndex = 0; // Just in case
  10367. return match;
  10368. },
  10369. /** @returns {Number} The position immediately following the match.
  10370. * @private
  10371. */
  10372. afterMatch: function(/**RegExp.match*/ match) {
  10373. return match.index + match[0].length;
  10374. },
  10375. /**
  10376. * @returns {RegExp.match} If node is a BeginEndNode and its rule's "end" pattern matches the text.
  10377. * @private
  10378. */
  10379. getEndMatch: function(/**Node*/ node, /**String*/ text, /**Number*/ offset) {
  10380. if (node instanceof this.BeginEndNode) {
  10381. var rule = node.rule;
  10382. var endRegex = node.endRegexSubstituted || rule.endRegex;
  10383. if (!endRegex) { return null; }
  10384. return this.exec(endRegex, text, offset);
  10385. }
  10386. return null;
  10387. },
  10388. /** Called once when file is first loaded to build the parse tree. Tree is updated incrementally thereafter
  10389. * as buffer is modified.
  10390. * @private
  10391. */
  10392. initialParse: function() {
  10393. var last = this.textView.getModel().getCharCount();
  10394. // First time; make parse tree for whole buffer
  10395. var root = new this.ContainerNode(null, this.grammar._typedRule);
  10396. this._tree = root;
  10397. this.parse(this._tree, false, 0);
  10398. },
  10399. onModelChanged: function(/**eclipse.ModelChangedEvent*/ e) {
  10400. var addedCharCount = e.addedCharCount,
  10401. addedLineCount = e.addedLineCount,
  10402. removedCharCount = e.removedCharCount,
  10403. removedLineCount = e.removedLineCount,
  10404. start = e.start;
  10405. if (!this._tree) {
  10406. this.initialParse();
  10407. } else {
  10408. var model = this.textView.getModel();
  10409. var charCount = model.getCharCount();
  10410. // For rs, we must rewind to the line preceding the line 'start' is on. We can't rely on start's
  10411. // line since it may've been changed in a way that would cause a new beginMatch at its lineStart.
  10412. var rs = model.getLineEnd(model.getLineAtOffset(start) - 1); // may be < 0
  10413. var fd = this.getFirstDamaged(rs, rs);
  10414. rs = rs === -1 ? 0 : rs;
  10415. var stoppedAt;
  10416. if (fd) {
  10417. // [rs, re] is the region we need to verify. If we find the structure of the tree
  10418. // has changed in that area, then we may need to reparse the rest of the file.
  10419. stoppedAt = this.parse(fd, true, rs, start, addedCharCount, removedCharCount);
  10420. } else {
  10421. // FIXME: fd == null ?
  10422. stoppedAt = charCount;
  10423. }
  10424. this.textView.redrawRange(rs, stoppedAt);
  10425. }
  10426. },
  10427. /** @returns {BeginEndNode|ContainerNode} The result of taking the first (smallest "start" value)
  10428. * node overlapping [start,end] and drilling down to get its deepest damaged descendant (if any).
  10429. * @private
  10430. */
  10431. getFirstDamaged: function(start, end) {
  10432. // If start === 0 we actually have to start from the root because there is no position
  10433. // we can rely on. (First index is damaged)
  10434. if (start < 0) {
  10435. return this._tree;
  10436. }
  10437. var nodes = [this._tree];
  10438. var result = null;
  10439. while (nodes.length) {
  10440. var n = nodes.pop();
  10441. if (!n.parent /*n is root*/ || this.isDamaged(n, start, end)) {
  10442. // n is damaged by the edit, so go into its children
  10443. // Note: If a node is damaged, then some of its descendents MAY be damaged
  10444. // If a node is undamaged, then ALL of its descendents are undamaged
  10445. if (n instanceof this.BeginEndNode) {
  10446. result = n;
  10447. }
  10448. // Examine children[0] last
  10449. for (var i=0; i < n.children.length; i++) {
  10450. nodes.push(n.children[i]);
  10451. }
  10452. }
  10453. }
  10454. return result || this._tree;
  10455. },
  10456. /** @returns true If <code>n</code> overlaps the interval [start,end].
  10457. * @private
  10458. */
  10459. isDamaged: function(/**BeginEndNode*/ n, start, end) {
  10460. // Note strict > since [2,5] doesn't overlap [5,7]
  10461. return (n.start <= end && n.end > start);
  10462. },
  10463. /**
  10464. * Builds tree from some of the buffer content
  10465. *
  10466. * TODO cleanup params
  10467. * @param {BeginEndNode|ContainerNode} origNode The deepest node that overlaps [rs,rs], or the root.
  10468. * @param {Boolean} repairing
  10469. * @param {Number} rs See _onModelChanged()
  10470. * @param {Number} [editStart] Only used for repairing === true
  10471. * @param {Number} [addedCharCount] Only used for repairing === true
  10472. * @param {Number} [removedCharCount] Only used for repairing === true
  10473. * @returns {Number} The end position that redrawRange should be called for.
  10474. * @private
  10475. */
  10476. parse: function(origNode, repairing, rs, editStart, addedCharCount, removedCharCount) {
  10477. var model = this.textView.getModel();
  10478. var lastLineStart = model.getLineStart(model.getLineCount() - 1);
  10479. var eof = model.getCharCount();
  10480. var initialExpected = this.getInitialExpected(origNode, rs);
  10481. // re is best-case stopping point; if we detect change to tree, we must continue past it
  10482. var re = -1;
  10483. if (repairing) {
  10484. origNode.repaired = true;
  10485. origNode.endNeedsUpdate = true;
  10486. var lastChild = origNode.children[origNode.children.length-1];
  10487. var delta = addedCharCount - removedCharCount;
  10488. var lastChildLineEnd = lastChild ? model.getLineEnd(model.getLineAtOffset(lastChild.end + delta)) : -1;
  10489. var editLineEnd = model.getLineEnd(model.getLineAtOffset(editStart + removedCharCount));
  10490. re = Math.max(lastChildLineEnd, editLineEnd);
  10491. }
  10492. re = (re === -1) ? eof : re;
  10493. var expected = initialExpected;
  10494. var node = origNode;
  10495. var matchedChildOrEnd = false;
  10496. var pos = rs;
  10497. var redrawEnd = -1;
  10498. while (node && (!repairing || (pos < re))) {
  10499. var matchInfo = this.getNextMatch(model, node, pos);
  10500. if (!matchInfo) {
  10501. // Go to next line, if any
  10502. pos = (pos >= lastLineStart) ? eof : model.getLineStart(model.getLineAtOffset(pos) + 1);
  10503. }
  10504. var match = matchInfo && matchInfo.match,
  10505. rule = matchInfo && matchInfo.rule,
  10506. isSub = matchInfo && matchInfo.isSub,
  10507. isEnd = matchInfo && matchInfo.isEnd;
  10508. if (isSub) {
  10509. pos = this.afterMatch(match);
  10510. if (rule instanceof this.BeginEndRule) {
  10511. matchedChildOrEnd = true;
  10512. // Matched a child. Did we expect that?
  10513. if (repairing && rule === expected.rule && node === expected.parent) {
  10514. // Yes: matched expected child
  10515. var foundChild = expected;
  10516. foundChild.setStart(match);
  10517. // Note: the 'end' position for this node will either be matched, or fixed up by us post-loop
  10518. foundChild.repaired = true;
  10519. foundChild.endNeedsUpdate = true;
  10520. node = foundChild; // descend
  10521. expected = this.getNextExpected(expected, "begin");
  10522. } else {
  10523. if (repairing) {
  10524. // No: matched unexpected child.
  10525. this.prune(node, expected);
  10526. repairing = false;
  10527. }
  10528. // Add the new child (will replace 'expected' in node's children list)
  10529. var subNode = new this.BeginEndNode(node, rule, match);
  10530. node.addChild(subNode);
  10531. node = subNode; // descend
  10532. }
  10533. } else {
  10534. // Matched a MatchRule; no changes to tree required
  10535. }
  10536. } else if (isEnd || pos === eof) {
  10537. if (node instanceof this.BeginEndNode) {
  10538. if (match) {
  10539. matchedChildOrEnd = true;
  10540. redrawEnd = Math.max(redrawEnd, node.end); // if end moved up, must still redraw to its old value
  10541. node.setEnd(match);
  10542. pos = this.afterMatch(match);
  10543. // Matched node's end. Did we expect that?
  10544. if (repairing && node === expected && node.parent === expected.parent) {
  10545. // Yes: found the expected end of node
  10546. node.repaired = true;
  10547. delete node.endNeedsUpdate;
  10548. expected = this.getNextExpected(expected, "end");
  10549. } else {
  10550. if (repairing) {
  10551. // No: found an unexpected end
  10552. this.prune(node, expected);
  10553. repairing = false;
  10554. }
  10555. }
  10556. } else {
  10557. // Force-ending a BeginEndNode that runs until eof
  10558. node.setEnd(eof);
  10559. delete node.endNeedsUpdate;
  10560. }
  10561. }
  10562. node = node.parent; // ascend
  10563. }
  10564. if (repairing && pos >= re && !matchedChildOrEnd) {
  10565. // Reached re without matching any begin/end => initialExpected itself was removed => repair fail
  10566. this.prune(origNode, initialExpected);
  10567. repairing = false;
  10568. }
  10569. } // end loop
  10570. // TODO: do this for every node we end?
  10571. this.removeUnrepairedChildren(origNode, repairing, rs);
  10572. //console.debug("parsed " + (pos - rs) + " of " + model.getCharCount + "buf");
  10573. this.cleanup(repairing, origNode, rs, re, eof, addedCharCount, removedCharCount);
  10574. if (repairing) {
  10575. return Math.max(redrawEnd, pos);
  10576. } else {
  10577. return pos; // where we stopped reparsing
  10578. }
  10579. },
  10580. /** Helper for parse() in the repair case. To be called when ending a node, as any children that
  10581. * lie in [rs,node.end] and were not repaired must've been deleted.
  10582. * @private
  10583. */
  10584. removeUnrepairedChildren: function(node, repairing, start) {
  10585. if (repairing) {
  10586. var children = node.children;
  10587. var removeFrom = -1;
  10588. for (var i=0; i < children.length; i++) {
  10589. var child = children[i];
  10590. if (!child.repaired && this.isDamaged(child, start, Number.MAX_VALUE /*end doesn't matter*/)) {
  10591. removeFrom = i;
  10592. break;
  10593. }
  10594. }
  10595. if (removeFrom !== -1) {
  10596. node.children.length = removeFrom;
  10597. }
  10598. }
  10599. },
  10600. /** Helper for parse() in the repair case
  10601. * @private
  10602. */
  10603. cleanup: function(repairing, origNode, rs, re, eof, addedCharCount, removedCharCount) {
  10604. var i, node, maybeRepairedNodes;
  10605. if (repairing) {
  10606. // The repair succeeded, so update stale begin/end indices by simple translation.
  10607. var delta = addedCharCount - removedCharCount;
  10608. // A repaired node's end can't exceed re, but it may exceed re-delta+1.
  10609. // TODO: find a way to guarantee disjoint intervals for repaired vs unrepaired, then stop using flag
  10610. var maybeUnrepairedNodes = this.getIntersecting(re-delta+1, eof);
  10611. maybeRepairedNodes = this.getIntersecting(rs, re);
  10612. // Handle unrepaired nodes. They are those intersecting [re-delta+1, eof] that don't have the flag
  10613. for (i=0; i < maybeUnrepairedNodes.length; i++) {
  10614. node = maybeUnrepairedNodes[i];
  10615. if (!node.repaired && node instanceof this.BeginEndNode) {
  10616. node.shiftEnd(delta);
  10617. node.shiftStart(delta);
  10618. }
  10619. }
  10620. // Translate 'end' index of repaired node whose 'end' was not matched in loop (>= re)
  10621. for (i=0; i < maybeRepairedNodes.length; i++) {
  10622. node = maybeRepairedNodes[i];
  10623. if (node.repaired && node.endNeedsUpdate) {
  10624. node.shiftEnd(delta);
  10625. }
  10626. delete node.endNeedsUpdate;
  10627. delete node.repaired;
  10628. }
  10629. } else {
  10630. // Clean up after ourself
  10631. maybeRepairedNodes = this.getIntersecting(rs, re);
  10632. for (i=0; i < maybeRepairedNodes.length; i++) {
  10633. delete maybeRepairedNodes[i].repaired;
  10634. }
  10635. }
  10636. },
  10637. /**
  10638. * @param model {orion.textview.TextModel}
  10639. * @param node {Node}
  10640. * @param pos {Number}
  10641. * @param [matchRulesOnly] {Boolean} Optional, if true only "match" subrules will be considered.
  10642. * @returns {Object} A match info object with properties:
  10643. * {Boolean} isEnd
  10644. * {Boolean} isSub
  10645. * {RegExp.match} match
  10646. * {(Match|BeginEnd)Rule} rule
  10647. * @private
  10648. */
  10649. getNextMatch: function(model, node, pos, matchRulesOnly) {
  10650. var lineIndex = model.getLineAtOffset(pos);
  10651. var lineEnd = model.getLineEnd(lineIndex);
  10652. var line = model.getText(pos, lineEnd);
  10653. var stack = [],
  10654. expandedContainers = [],
  10655. subMatches = [],
  10656. subrules = [];
  10657. this.push(stack, node.rule.subrules);
  10658. while (stack.length) {
  10659. var next = stack.length ? stack.pop() : null;
  10660. var subrule = next && next._resolvedRule._typedRule;
  10661. if (subrule instanceof this.ContainerRule && expandedContainers.indexOf(subrule) === -1) {
  10662. // Expand ContainerRule by pushing its subrules on
  10663. expandedContainers.push(subrule);
  10664. this.push(stack, subrule.subrules);
  10665. continue;
  10666. }
  10667. if (subrule && matchRulesOnly && !(subrule.matchRegex)) {
  10668. continue;
  10669. }
  10670. var subMatch = subrule && this.exec(subrule.matchRegex || subrule.beginRegex, line, pos);
  10671. if (subMatch) {
  10672. subMatches.push(subMatch);
  10673. subrules.push(subrule);
  10674. }
  10675. }
  10676. var bestSub = Number.MAX_VALUE,
  10677. bestSubIndex = -1;
  10678. for (var i=0; i < subMatches.length; i++) {
  10679. var match = subMatches[i];
  10680. if (match.index < bestSub) {
  10681. bestSub = match.index;
  10682. bestSubIndex = i;
  10683. }
  10684. }
  10685. if (!matchRulesOnly) {
  10686. // See if the "end" pattern of the active begin/end node matches.
  10687. // TODO: The active begin/end node may not be the same as the node that holds the subrules
  10688. var activeBENode = node;
  10689. var endMatch = this.getEndMatch(node, line, pos);
  10690. if (endMatch) {
  10691. var doEndLast = activeBENode.rule.applyEndPatternLast;
  10692. var endWins = bestSubIndex === -1 || (endMatch.index < bestSub) || (!doEndLast && endMatch.index === bestSub);
  10693. if (endWins) {
  10694. return {isEnd: true, rule: activeBENode.rule, match: endMatch};
  10695. }
  10696. }
  10697. }
  10698. return bestSubIndex === -1 ? null : {isSub: true, rule: subrules[bestSubIndex], match: subMatches[bestSubIndex]};
  10699. },
  10700. /**
  10701. * Gets the node corresponding to the first match we expect to see in the repair.
  10702. * @param {BeginEndNode|ContainerNode} node The node returned via getFirstDamaged(rs,rs) -- may be the root.
  10703. * @param {Number} rs See _onModelChanged()
  10704. * Note that because rs is a line end (or 0, a line start), it will intersect a beginMatch or
  10705. * endMatch either at their 0th character, or not at all. (begin/endMatches can't cross lines).
  10706. * This is the only time we rely on the start/end values from the pre-change tree. After this
  10707. * we only look at node ordering, never use the old indices.
  10708. * @returns {Node}
  10709. * @private
  10710. */
  10711. getInitialExpected: function(node, rs) {
  10712. // TODO: Kind of weird.. maybe ContainerNodes should have start & end set, like BeginEndNodes
  10713. var i, child;
  10714. if (node === this._tree) {
  10715. // get whichever of our children comes after rs
  10716. for (i=0; i < node.children.length; i++) {
  10717. child = node.children[i]; // BeginEndNode
  10718. if (child.start >= rs) {
  10719. return child;
  10720. }
  10721. }
  10722. } else if (node instanceof this.BeginEndNode) {
  10723. if (node.endMatch) {
  10724. // Which comes next after rs: our nodeEnd or one of our children?
  10725. var nodeEnd = node.endMatch.index;
  10726. for (i=0; i < node.children.length; i++) {
  10727. child = node.children[i]; // BeginEndNode
  10728. if (child.start >= rs) {
  10729. break;
  10730. }
  10731. }
  10732. if (child && child.start < nodeEnd) {
  10733. return child; // Expect child as the next match
  10734. }
  10735. } else {
  10736. // No endMatch => node goes until eof => it end should be the next match
  10737. }
  10738. }
  10739. return node; // We expect node to end, so it should be the next match
  10740. },
  10741. /**
  10742. * Helper for repair() to tell us what kind of event we expect next.
  10743. * @param {Node} expected Last value returned by this method.
  10744. * @param {String} event "begin" if the last value of expected was matched as "begin",
  10745. * or "end" if it was matched as an end.
  10746. * @returns {Node} The next expected node to match, or null.
  10747. * @private
  10748. */
  10749. getNextExpected: function(/**Node*/ expected, event) {
  10750. var node = expected;
  10751. if (event === "begin") {
  10752. var child = node.children[0];
  10753. if (child) {
  10754. return child;
  10755. } else {
  10756. return node;
  10757. }
  10758. } else if (event === "end") {
  10759. var parent = node.parent;
  10760. if (parent) {
  10761. var nextSibling = parent.children[parent.children.indexOf(node) + 1];
  10762. if (nextSibling) {
  10763. return nextSibling;
  10764. } else {
  10765. return parent;
  10766. }
  10767. }
  10768. }
  10769. return null;
  10770. },
  10771. /** Helper for parse() when repairing. Prunes out the unmatched nodes from the tree so we can continue parsing.
  10772. * @private
  10773. */
  10774. prune: function(/**BeginEndNode|ContainerNode*/ node, /**Node*/ expected) {
  10775. var expectedAChild = expected.parent === node;
  10776. if (expectedAChild) {
  10777. // Expected child wasn't matched; prune it and all siblings after it
  10778. node.children.length = expected.getIndexInParent();
  10779. } else if (node instanceof this.BeginEndNode) {
  10780. // Expected node to end but it didn't; set its end unknown and we'll match it eventually
  10781. node.endMatch = null;
  10782. node.end = null;
  10783. }
  10784. // Reparsing from node, so prune the successors outside of node's subtree
  10785. if (node.parent) {
  10786. node.parent.children.length = node.getIndexInParent() + 1;
  10787. }
  10788. },
  10789. onLineStyle: function(/**eclipse.LineStyleEvent*/ e) {
  10790. function byStart(r1, r2) {
  10791. return r1.start - r2.start;
  10792. }
  10793. if (!this._tree) {
  10794. // In some cases it seems onLineStyle is called before onModelChanged, so we need to parse here
  10795. this.initialParse();
  10796. }
  10797. var lineStart = e.lineStart,
  10798. model = this.textView.getModel(),
  10799. lineEnd = model.getLineEnd(e.lineIndex);
  10800. var rs = model.getLineEnd(model.getLineAtOffset(lineStart) - 1); // may be < 0
  10801. var node = this.getFirstDamaged(rs, rs);
  10802. var scopes = this.getLineScope(model, node, lineStart, lineEnd);
  10803. e.ranges = this.toStyleRanges(scopes);
  10804. // Editor requires StyleRanges must be in ascending order by 'start', or else some will be ignored
  10805. e.ranges.sort(byStart);
  10806. },
  10807. /** Runs parse algorithm on [start, end] in the context of node, assigning scope as we find matches.
  10808. * @private
  10809. */
  10810. getLineScope: function(model, node, start, end) {
  10811. var pos = start;
  10812. var expected = this.getInitialExpected(node, start);
  10813. var scopes = [],
  10814. gaps = [];
  10815. while (node && (pos < end)) {
  10816. var matchInfo = this.getNextMatch(model, node, pos);
  10817. if (!matchInfo) {
  10818. break; // line is over
  10819. }
  10820. var match = matchInfo && matchInfo.match,
  10821. rule = matchInfo && matchInfo.rule,
  10822. isSub = matchInfo && matchInfo.isSub,
  10823. isEnd = matchInfo && matchInfo.isEnd;
  10824. if (match.index !== pos) {
  10825. // gap [pos..match.index]
  10826. gaps.push({ start: pos, end: match.index, node: node});
  10827. }
  10828. if (isSub) {
  10829. pos = this.afterMatch(match);
  10830. if (rule instanceof this.BeginEndRule) {
  10831. // Matched a "begin", assign its scope and descend into it
  10832. this.addBeginScope(scopes, match, rule);
  10833. node = expected; // descend
  10834. expected = this.getNextExpected(expected, "begin");
  10835. } else {
  10836. // Matched a child MatchRule;
  10837. this.addMatchScope(scopes, match, rule);
  10838. }
  10839. } else if (isEnd) {
  10840. pos = this.afterMatch(match);
  10841. // Matched and "end", assign its end scope and go up
  10842. this.addEndScope(scopes, match, rule);
  10843. expected = this.getNextExpected(expected, "end");
  10844. node = node.parent; // ascend
  10845. }
  10846. }
  10847. if (pos < end) {
  10848. gaps.push({ start: pos, end: end, node: node });
  10849. }
  10850. var inherited = this.getInheritedLineScope(gaps, start, end);
  10851. return scopes.concat(inherited);
  10852. },
  10853. /** @private */
  10854. getInheritedLineScope: function(gaps, start, end) {
  10855. var scopes = [];
  10856. for (var i=0; i < gaps.length; i++) {
  10857. var gap = gaps[i];
  10858. var node = gap.node;
  10859. while (node) {
  10860. // if node defines a contentName or name, apply it
  10861. var rule = node.rule.rule;
  10862. var name = rule.name,
  10863. contentName = rule.contentName;
  10864. // TODO: if both are given, we don't resolve the conflict. contentName always wins
  10865. var scope = contentName || name;
  10866. if (scope) {
  10867. this.addScopeRange(scopes, gap.start, gap.end, scope);
  10868. break;
  10869. }
  10870. node = node.parent;
  10871. }
  10872. }
  10873. return scopes;
  10874. },
  10875. /** @private */
  10876. addBeginScope: function(scopes, match, typedRule) {
  10877. var rule = typedRule.rule;
  10878. this.addCapturesScope(scopes, match, (rule.beginCaptures || rule.captures), typedRule.isComplex, typedRule.beginOld2New, typedRule.beginConsuming);
  10879. },
  10880. /** @private */
  10881. addEndScope: function(scopes, match, typedRule) {
  10882. var rule = typedRule.rule;
  10883. this.addCapturesScope(scopes, match, (rule.endCaptures || rule.captures), typedRule.isComplex, typedRule.endOld2New, typedRule.endConsuming);
  10884. },
  10885. /** @private */
  10886. addMatchScope: function(scopes, match, typedRule) {
  10887. var rule = typedRule.rule,
  10888. name = rule.name,
  10889. captures = rule.captures;
  10890. if (captures) {
  10891. // captures takes priority over name
  10892. this.addCapturesScope(scopes, match, captures, typedRule.isComplex, typedRule.matchOld2New, typedRule.matchConsuming);
  10893. } else {
  10894. this.addScope(scopes, match, name);
  10895. }
  10896. },
  10897. /** @private */
  10898. addScope: function(scopes, match, name) {
  10899. if (!name) { return; }
  10900. scopes.push({start: match.index, end: this.afterMatch(match), scope: name });
  10901. },
  10902. /** @private */
  10903. addScopeRange: function(scopes, start, end, name) {
  10904. if (!name) { return; }
  10905. scopes.push({start: start, end: end, scope: name });
  10906. },
  10907. /** @private */
  10908. addCapturesScope: function(/**Array*/scopes, /*RegExp.match*/ match, /**Object*/captures, /**Boolean*/isComplex, /**Object*/old2New, /**Object*/consuming) {
  10909. if (!captures) { return; }
  10910. if (!isComplex) {
  10911. this.addScope(scopes, match, captures[0] && captures[0].name);
  10912. } else {
  10913. // apply scopes captures[1..n] to matching groups [1]..[n] of match
  10914. // Sum up the lengths of preceding consuming groups to get the start offset for each matched group.
  10915. var newGroupStarts = {1: 0};
  10916. var sum = 0;
  10917. for (var num = 1; match[num] !== undefined; num++) {
  10918. if (consuming[num] !== undefined) {
  10919. sum += match[num].length;
  10920. }
  10921. if (match[num+1] !== undefined) {
  10922. newGroupStarts[num + 1] = sum;
  10923. }
  10924. }
  10925. // Map the group numbers referred to in captures object to the new group numbers, and get the actual matched range.
  10926. var start = match.index;
  10927. for (var oldGroupNum = 1; captures[oldGroupNum]; oldGroupNum++) {
  10928. var scope = captures[oldGroupNum].name;
  10929. var newGroupNum = old2New[oldGroupNum];
  10930. var groupStart = start + newGroupStarts[newGroupNum];
  10931. // Not every capturing group defined in regex need match every time the regex is run.
  10932. // eg. (a)|b matches "b" but group 1 is undefined
  10933. if (typeof match[newGroupNum] !== "undefined") {
  10934. var groupEnd = groupStart + match[newGroupNum].length;
  10935. this.addScopeRange(scopes, groupStart, groupEnd, scope);
  10936. }
  10937. }
  10938. }
  10939. },
  10940. /** @returns {Node[]} In depth-first order
  10941. * @private
  10942. */
  10943. getIntersecting: function(start, end) {
  10944. var result = [];
  10945. var nodes = this._tree ? [this._tree] : [];
  10946. while (nodes.length) {
  10947. var n = nodes.pop();
  10948. var visitChildren = false;
  10949. if (n instanceof this.ContainerNode) {
  10950. visitChildren = true;
  10951. } else if (this.isDamaged(n, start, end)) {
  10952. visitChildren = true;
  10953. result.push(n);
  10954. }
  10955. if (visitChildren) {
  10956. var len = n.children.length;
  10957. // for (var i=len-1; i >= 0; i--) {
  10958. // nodes.push(n.children[i]);
  10959. // }
  10960. for (var i=0; i < len; i++) {
  10961. nodes.push(n.children[i]);
  10962. }
  10963. }
  10964. }
  10965. return result.reverse();
  10966. },
  10967. /**
  10968. * Applies the grammar to obtain the {@link eclipse.StyleRange[]} for the given line.
  10969. * @returns eclipse.StyleRange[]
  10970. * @private
  10971. */
  10972. toStyleRanges: function(/**ScopeRange[]*/ scopeRanges) {
  10973. var styleRanges = [];
  10974. for (var i=0; i < scopeRanges.length; i++) {
  10975. var scopeRange = scopeRanges[i];
  10976. var classNames = this._styles[scopeRange.scope];
  10977. if (!classNames) { throw new Error("styles not found for " + scopeRange.scope); }
  10978. var classNamesString = classNames.join(" ");
  10979. styleRanges.push({start: scopeRange.start, end: scopeRange.end, style: {styleClass: classNamesString}});
  10980. // console.debug("{start " + styleRanges[i].start + ", end " + styleRanges[i].end + ", style: " + styleRanges[i].style.styleClass + "}");
  10981. }
  10982. return styleRanges;
  10983. }
  10984. };
  10985. return {
  10986. RegexUtil: RegexUtil,
  10987. TextMateStyler: TextMateStyler
  10988. };
  10989. });
  10990. /*******************************************************************************
  10991. * @license
  10992. * Copyright (c) 2010, 2011 IBM Corporation and others.
  10993. * All rights reserved. This program and the accompanying materials are made
  10994. * available under the terms of the Eclipse Public License v1.0
  10995. * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
  10996. * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
  10997. *
  10998. * Contributors: IBM Corporation - initial API and implementation
  10999. * Alex Lakatos - fix for bug#369781
  11000. ******************************************************************************/
  11001. /*global document window navigator define */
  11002. define("examples/textview/textStyler", ['orion/textview/annotations'], function(mAnnotations) {
  11003. var JS_KEYWORDS =
  11004. ["break",
  11005. "case", "class", "catch", "continue", "const",
  11006. "debugger", "default", "delete", "do",
  11007. "else", "enum", "export", "extends",
  11008. "false", "finally", "for", "function",
  11009. "if", "implements", "import", "in", "instanceof", "interface",
  11010. "let",
  11011. "new", "null",
  11012. "package", "private", "protected", "public",
  11013. "return",
  11014. "static", "super", "switch",
  11015. "this", "throw", "true", "try", "typeof",
  11016. "undefined",
  11017. "var", "void",
  11018. "while", "with",
  11019. "yield"];
  11020. var JAVA_KEYWORDS =
  11021. ["abstract",
  11022. "boolean", "break", "byte",
  11023. "case", "catch", "char", "class", "continue",
  11024. "default", "do", "double",
  11025. "else", "extends",
  11026. "false", "final", "finally", "float", "for",
  11027. "if", "implements", "import", "instanceof", "int", "interface",
  11028. "long",
  11029. "native", "new", "null",
  11030. "package", "private", "protected", "public",
  11031. "return",
  11032. "short", "static", "super", "switch", "synchronized",
  11033. "this", "throw", "throws", "transient", "true", "try",
  11034. "void", "volatile",
  11035. "while"];
  11036. var CSS_KEYWORDS =
  11037. ["alignment-adjust", "alignment-baseline", "animation", "animation-delay", "animation-direction", "animation-duration",
  11038. "animation-iteration-count", "animation-name", "animation-play-state", "animation-timing-function", "appearance",
  11039. "azimuth", "backface-visibility", "background", "background-attachment", "background-clip", "background-color",
  11040. "background-image", "background-origin", "background-position", "background-repeat", "background-size", "baseline-shift",
  11041. "binding", "bleed", "bookmark-label", "bookmark-level", "bookmark-state", "bookmark-target", "border", "border-bottom",
  11042. "border-bottom-color", "border-bottom-left-radius", "border-bottom-right-radius", "border-bottom-style", "border-bottom-width",
  11043. "border-collapse", "border-color", "border-image", "border-image-outset", "border-image-repeat", "border-image-slice",
  11044. "border-image-source", "border-image-width", "border-left", "border-left-color", "border-left-style", "border-left-width",
  11045. "border-radius", "border-right", "border-right-color", "border-right-style", "border-right-width", "border-spacing", "border-style",
  11046. "border-top", "border-top-color", "border-top-left-radius", "border-top-right-radius", "border-top-style", "border-top-width",
  11047. "border-width", "bottom", "box-align", "box-decoration-break", "box-direction", "box-flex", "box-flex-group", "box-lines",
  11048. "box-ordinal-group", "box-orient", "box-pack", "box-shadow", "box-sizing", "break-after", "break-before", "break-inside",
  11049. "caption-side", "clear", "clip", "color", "color-profile", "column-count", "column-fill", "column-gap", "column-rule",
  11050. "column-rule-color", "column-rule-style", "column-rule-width", "column-span", "column-width", "columns", "content", "counter-increment",
  11051. "counter-reset", "crop", "cue", "cue-after", "cue-before", "cursor", "direction", "display", "dominant-baseline",
  11052. "drop-initial-after-adjust", "drop-initial-after-align", "drop-initial-before-adjust", "drop-initial-before-align", "drop-initial-size",
  11053. "drop-initial-value", "elevation", "empty-cells", "fit", "fit-position", "flex-align", "flex-flow", "flex-inline-pack", "flex-order",
  11054. "flex-pack", "float", "float-offset", "font", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style",
  11055. "font-variant", "font-weight", "grid-columns", "grid-rows", "hanging-punctuation", "height", "hyphenate-after",
  11056. "hyphenate-before", "hyphenate-character", "hyphenate-lines", "hyphenate-resource", "hyphens", "icon", "image-orientation",
  11057. "image-rendering", "image-resolution", "inline-box-align", "left", "letter-spacing", "line-height", "line-stacking",
  11058. "line-stacking-ruby", "line-stacking-shift", "line-stacking-strategy", "list-style", "list-style-image", "list-style-position",
  11059. "list-style-type", "margin", "margin-bottom", "margin-left", "margin-right", "margin-top", "mark", "mark-after", "mark-before",
  11060. "marker-offset", "marks", "marquee-direction", "marquee-loop", "marquee-play-count", "marquee-speed", "marquee-style", "max-height",
  11061. "max-width", "min-height", "min-width", "move-to", "nav-down", "nav-index", "nav-left", "nav-right", "nav-up", "opacity", "orphans",
  11062. "outline", "outline-color", "outline-offset", "outline-style", "outline-width", "overflow", "overflow-style", "overflow-x",
  11063. "overflow-y", "padding", "padding-bottom", "padding-left", "padding-right", "padding-top", "page", "page-break-after", "page-break-before",
  11064. "page-break-inside", "page-policy", "pause", "pause-after", "pause-before", "perspective", "perspective-origin", "phonemes", "pitch",
  11065. "pitch-range", "play-during", "position", "presentation-level", "punctuation-trim", "quotes", "rendering-intent", "resize",
  11066. "rest", "rest-after", "rest-before", "richness", "right", "rotation", "rotation-point", "ruby-align", "ruby-overhang", "ruby-position",
  11067. "ruby-span", "size", "speak", "speak-header", "speak-numeral", "speak-punctuation", "speech-rate", "stress", "string-set", "table-layout",
  11068. "target", "target-name", "target-new", "target-position", "text-align", "text-align-last", "text-decoration", "text-emphasis",
  11069. "text-height", "text-indent", "text-justify", "text-outline", "text-shadow", "text-transform", "text-wrap", "top", "transform",
  11070. "transform-origin", "transform-style", "transition", "transition-delay", "transition-duration", "transition-property",
  11071. "transition-timing-function", "unicode-bidi", "vertical-align", "visibility", "voice-balance", "voice-duration", "voice-family",
  11072. "voice-pitch", "voice-pitch-range", "voice-rate", "voice-stress", "voice-volume", "volume", "white-space", "white-space-collapse",
  11073. "widows", "width", "word-break", "word-spacing", "word-wrap", "z-index"
  11074. ];
  11075. // Scanner constants
  11076. var UNKOWN = 1;
  11077. var KEYWORD = 2;
  11078. var STRING = 3;
  11079. var SINGLELINE_COMMENT = 4;
  11080. var MULTILINE_COMMENT = 5;
  11081. var DOC_COMMENT = 6;
  11082. var WHITE = 7;
  11083. var WHITE_TAB = 8;
  11084. var WHITE_SPACE = 9;
  11085. var HTML_MARKUP = 10;
  11086. var DOC_TAG = 11;
  11087. var TASK_TAG = 12;
  11088. // Styles
  11089. var singleCommentStyle = {styleClass: "token_singleline_comment"};
  11090. var multiCommentStyle = {styleClass: "token_multiline_comment"};
  11091. var docCommentStyle = {styleClass: "token_doc_comment"};
  11092. var htmlMarkupStyle = {styleClass: "token_doc_html_markup"};
  11093. var tasktagStyle = {styleClass: "token_task_tag"};
  11094. var doctagStyle = {styleClass: "token_doc_tag"};
  11095. var stringStyle = {styleClass: "token_string"};
  11096. var keywordStyle = {styleClass: "token_keyword"};
  11097. var spaceStyle = {styleClass: "token_space"};
  11098. var tabStyle = {styleClass: "token_tab"};
  11099. var caretLineStyle = {styleClass: "line_caret"};
  11100. function Scanner (keywords, whitespacesVisible) {
  11101. this.keywords = keywords;
  11102. this.whitespacesVisible = whitespacesVisible;
  11103. this.setText("");
  11104. }
  11105. Scanner.prototype = {
  11106. getOffset: function() {
  11107. return this.offset;
  11108. },
  11109. getStartOffset: function() {
  11110. return this.startOffset;
  11111. },
  11112. getData: function() {
  11113. return this.text.substring(this.startOffset, this.offset);
  11114. },
  11115. getDataLength: function() {
  11116. return this.offset - this.startOffset;
  11117. },
  11118. _default: function(c) {
  11119. var keywords = this.keywords;
  11120. switch (c) {
  11121. case 32: // SPACE
  11122. case 9: // TAB
  11123. if (this.whitespacesVisible) {
  11124. return c === 32 ? WHITE_SPACE : WHITE_TAB;
  11125. }
  11126. do {
  11127. c = this._read();
  11128. } while(c === 32 || c === 9);
  11129. this._unread(c);
  11130. return WHITE;
  11131. case 123: // {
  11132. case 125: // }
  11133. case 40: // (
  11134. case 41: // )
  11135. case 91: // [
  11136. case 93: // ]
  11137. case 60: // <
  11138. case 62: // >
  11139. // BRACKETS
  11140. return c;
  11141. default:
  11142. var isCSS = this.isCSS;
  11143. if ((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57) || (0x2d === c && isCSS)) { //LETTER OR UNDERSCORE OR NUMBER
  11144. var off = this.offset - 1;
  11145. do {
  11146. c = this._read();
  11147. } while((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57) || (0x2d === c && isCSS)); //LETTER OR UNDERSCORE OR NUMBER
  11148. this._unread(c);
  11149. if (keywords.length > 0) {
  11150. var word = this.text.substring(off, this.offset);
  11151. //TODO slow
  11152. for (var i=0; i<keywords.length; i++) {
  11153. if (this.keywords[i] === word) { return KEYWORD; }
  11154. }
  11155. }
  11156. }
  11157. return UNKOWN;
  11158. }
  11159. },
  11160. _read: function() {
  11161. if (this.offset < this.text.length) {
  11162. return this.text.charCodeAt(this.offset++);
  11163. }
  11164. return -1;
  11165. },
  11166. _unread: function(c) {
  11167. if (c !== -1) { this.offset--; }
  11168. },
  11169. nextToken: function() {
  11170. this.startOffset = this.offset;
  11171. while (true) {
  11172. var c = this._read();
  11173. switch (c) {
  11174. case -1: return null;
  11175. case 47: // SLASH -> comment
  11176. c = this._read();
  11177. if (!this.isCSS) {
  11178. if (c === 47) { // SLASH -> single line
  11179. while (true) {
  11180. c = this._read();
  11181. if ((c === -1) || (c === 10) || (c === 13)) {
  11182. this._unread(c);
  11183. return SINGLELINE_COMMENT;
  11184. }
  11185. }
  11186. }
  11187. }
  11188. if (c === 42) { // STAR -> multi line
  11189. c = this._read();
  11190. var token = MULTILINE_COMMENT;
  11191. if (c === 42) {
  11192. token = DOC_COMMENT;
  11193. }
  11194. while (true) {
  11195. while (c === 42) {
  11196. c = this._read();
  11197. if (c === 47) {
  11198. return token;
  11199. }
  11200. }
  11201. if (c === -1) {
  11202. this._unread(c);
  11203. return token;
  11204. }
  11205. c = this._read();
  11206. }
  11207. }
  11208. this._unread(c);
  11209. return UNKOWN;
  11210. case 39: // SINGLE QUOTE -> char const
  11211. while(true) {
  11212. c = this._read();
  11213. switch (c) {
  11214. case 39:
  11215. return STRING;
  11216. case 13:
  11217. case 10:
  11218. case -1:
  11219. this._unread(c);
  11220. return STRING;
  11221. case 92: // BACKSLASH
  11222. c = this._read();
  11223. break;
  11224. }
  11225. }
  11226. break;
  11227. case 34: // DOUBLE QUOTE -> string
  11228. while(true) {
  11229. c = this._read();
  11230. switch (c) {
  11231. case 34: // DOUBLE QUOTE
  11232. return STRING;
  11233. case 13:
  11234. case 10:
  11235. case -1:
  11236. this._unread(c);
  11237. return STRING;
  11238. case 92: // BACKSLASH
  11239. c = this._read();
  11240. break;
  11241. }
  11242. }
  11243. break;
  11244. default:
  11245. return this._default(c);
  11246. }
  11247. }
  11248. },
  11249. setText: function(text) {
  11250. this.text = text;
  11251. this.offset = 0;
  11252. this.startOffset = 0;
  11253. }
  11254. };
  11255. function WhitespaceScanner () {
  11256. Scanner.call(this, null, true);
  11257. }
  11258. WhitespaceScanner.prototype = new Scanner(null);
  11259. WhitespaceScanner.prototype.nextToken = function() {
  11260. this.startOffset = this.offset;
  11261. while (true) {
  11262. var c = this._read();
  11263. switch (c) {
  11264. case -1: return null;
  11265. case 32: // SPACE
  11266. return WHITE_SPACE;
  11267. case 9: // TAB
  11268. return WHITE_TAB;
  11269. default:
  11270. do {
  11271. c = this._read();
  11272. } while(!(c === 32 || c === 9 || c === -1));
  11273. this._unread(c);
  11274. return UNKOWN;
  11275. }
  11276. }
  11277. };
  11278. function CommentScanner (whitespacesVisible) {
  11279. Scanner.call(this, null, whitespacesVisible);
  11280. }
  11281. CommentScanner.prototype = new Scanner(null);
  11282. CommentScanner.prototype.setType = function(type) {
  11283. this._type = type;
  11284. };
  11285. CommentScanner.prototype.nextToken = function() {
  11286. this.startOffset = this.offset;
  11287. while (true) {
  11288. var c = this._read();
  11289. switch (c) {
  11290. case -1: return null;
  11291. case 32: // SPACE
  11292. case 9: // TAB
  11293. if (this.whitespacesVisible) {
  11294. return c === 32 ? WHITE_SPACE : WHITE_TAB;
  11295. }
  11296. do {
  11297. c = this._read();
  11298. } while(c === 32 || c === 9);
  11299. this._unread(c);
  11300. return WHITE;
  11301. case 60: // <
  11302. if (this._type === DOC_COMMENT) {
  11303. do {
  11304. c = this._read();
  11305. } while(!(c === 62 || c === -1)); // >
  11306. if (c === 62) {
  11307. return HTML_MARKUP;
  11308. }
  11309. }
  11310. return UNKOWN;
  11311. case 64: // @
  11312. if (this._type === DOC_COMMENT) {
  11313. do {
  11314. c = this._read();
  11315. } while((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57)); //LETTER OR UNDERSCORE OR NUMBER
  11316. this._unread(c);
  11317. return DOC_TAG;
  11318. }
  11319. return UNKOWN;
  11320. case 84: // T
  11321. if ((c = this._read()) === 79) { // O
  11322. if ((c = this._read()) === 68) { // D
  11323. if ((c = this._read()) === 79) { // O
  11324. c = this._read();
  11325. if (!((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57))) {
  11326. this._unread(c);
  11327. return TASK_TAG;
  11328. }
  11329. this._unread(c);
  11330. } else {
  11331. this._unread(c);
  11332. }
  11333. } else {
  11334. this._unread(c);
  11335. }
  11336. } else {
  11337. this._unread(c);
  11338. }
  11339. //FALL THROUGH
  11340. default:
  11341. do {
  11342. c = this._read();
  11343. } while(!(c === 32 || c === 9 || c === -1 || c === 60 || c === 64 || c === 84));
  11344. this._unread(c);
  11345. return UNKOWN;
  11346. }
  11347. }
  11348. };
  11349. function FirstScanner () {
  11350. Scanner.call(this, null, false);
  11351. }
  11352. FirstScanner.prototype = new Scanner(null);
  11353. FirstScanner.prototype._default = function(c) {
  11354. while(true) {
  11355. c = this._read();
  11356. switch (c) {
  11357. case 47: // SLASH
  11358. case 34: // DOUBLE QUOTE
  11359. case 39: // SINGLE QUOTE
  11360. case -1:
  11361. this._unread(c);
  11362. return UNKOWN;
  11363. }
  11364. }
  11365. };
  11366. function TextStyler (view, lang, annotationModel) {
  11367. this.commentStart = "/*";
  11368. this.commentEnd = "*/";
  11369. var keywords = [];
  11370. switch (lang) {
  11371. case "java": keywords = JAVA_KEYWORDS; break;
  11372. case "js": keywords = JS_KEYWORDS; break;
  11373. case "css": keywords = CSS_KEYWORDS; break;
  11374. }
  11375. this.whitespacesVisible = false;
  11376. this.detectHyperlinks = true;
  11377. this.highlightCaretLine = false;
  11378. this.foldingEnabled = true;
  11379. this.detectTasks = true;
  11380. this._scanner = new Scanner(keywords, this.whitespacesVisible);
  11381. this._firstScanner = new FirstScanner();
  11382. this._commentScanner = new CommentScanner(this.whitespacesVisible);
  11383. this._whitespaceScanner = new WhitespaceScanner();
  11384. //TODO these scanners are not the best/correct way to parse CSS
  11385. if (lang === "css") {
  11386. this._scanner.isCSS = true;
  11387. this._firstScanner.isCSS = true;
  11388. }
  11389. this.view = view;
  11390. this.annotationModel = annotationModel;
  11391. this._bracketAnnotations = undefined;
  11392. var self = this;
  11393. this._listener = {
  11394. onChanged: function(e) {
  11395. self._onModelChanged(e);
  11396. },
  11397. onDestroy: function(e) {
  11398. self._onDestroy(e);
  11399. },
  11400. onLineStyle: function(e) {
  11401. self._onLineStyle(e);
  11402. },
  11403. onSelection: function(e) {
  11404. self._onSelection(e);
  11405. }
  11406. };
  11407. var model = view.getModel();
  11408. if (model.getBaseModel) {
  11409. model.getBaseModel().addEventListener("Changed", this._listener.onChanged);
  11410. } else {
  11411. //TODO still needed to keep the event order correct (styler before view)
  11412. view.addEventListener("ModelChanged", this._listener.onChanged);
  11413. }
  11414. view.addEventListener("Selection", this._listener.onSelection);
  11415. view.addEventListener("Destroy", this._listener.onDestroy);
  11416. view.addEventListener("LineStyle", this._listener.onLineStyle);
  11417. this._computeComments ();
  11418. this._computeFolding();
  11419. view.redrawLines();
  11420. }
  11421. TextStyler.prototype = {
  11422. getClassNameForToken: function(token) {
  11423. switch (token) {
  11424. case "singleLineComment": return singleCommentStyle.styleClass;
  11425. case "multiLineComment": return multiCommentStyle.styleClass;
  11426. case "docComment": return docCommentStyle.styleClass;
  11427. case "docHtmlComment": return htmlMarkupStyle.styleClass;
  11428. case "tasktag": return tasktagStyle.styleClass;
  11429. case "doctag": return doctagStyle.styleClass;
  11430. case "string": return stringStyle.styleClass;
  11431. case "keyword": return keywordStyle.styleClass;
  11432. case "space": return spaceStyle.styleClass;
  11433. case "tab": return tabStyle.styleClass;
  11434. case "caretLine": return caretLineStyle.styleClass;
  11435. }
  11436. return null;
  11437. },
  11438. destroy: function() {
  11439. var view = this.view;
  11440. if (view) {
  11441. var model = view.getModel();
  11442. if (model.getBaseModel) {
  11443. model.getBaseModel().removeEventListener("Changed", this._listener.onChanged);
  11444. } else {
  11445. view.removeEventListener("ModelChanged", this._listener.onChanged);
  11446. }
  11447. view.removeEventListener("Selection", this._listener.onSelection);
  11448. view.removeEventListener("Destroy", this._listener.onDestroy);
  11449. view.removeEventListener("LineStyle", this._listener.onLineStyle);
  11450. this.view = null;
  11451. }
  11452. },
  11453. setHighlightCaretLine: function(highlight) {
  11454. this.highlightCaretLine = highlight;
  11455. },
  11456. setWhitespacesVisible: function(visible) {
  11457. this.whitespacesVisible = visible;
  11458. this._scanner.whitespacesVisible = visible;
  11459. this._commentScanner.whitespacesVisible = visible;
  11460. },
  11461. setDetectHyperlinks: function(enabled) {
  11462. this.detectHyperlinks = enabled;
  11463. },
  11464. setFoldingEnabled: function(enabled) {
  11465. this.foldingEnabled = enabled;
  11466. },
  11467. setDetectTasks: function(enabled) {
  11468. this.detectTasks = enabled;
  11469. },
  11470. _binarySearch: function (array, offset, inclusive, low, high) {
  11471. var index;
  11472. if (low === undefined) { low = -1; }
  11473. if (high === undefined) { high = array.length; }
  11474. while (high - low > 1) {
  11475. index = Math.floor((high + low) / 2);
  11476. if (offset <= array[index].start) {
  11477. high = index;
  11478. } else if (inclusive && offset < array[index].end) {
  11479. high = index;
  11480. break;
  11481. } else {
  11482. low = index;
  11483. }
  11484. }
  11485. return high;
  11486. },
  11487. _computeComments: function() {
  11488. var model = this.view.getModel();
  11489. if (model.getBaseModel) { model = model.getBaseModel(); }
  11490. this.comments = this._findComments(model.getText());
  11491. },
  11492. _computeFolding: function() {
  11493. if (!this.foldingEnabled) { return; }
  11494. var view = this.view;
  11495. var viewModel = view.getModel();
  11496. if (!viewModel.getBaseModel) { return; }
  11497. var annotationModel = this.annotationModel;
  11498. if (!annotationModel) { return; }
  11499. annotationModel.removeAnnotations("orion.annotation.folding");
  11500. var add = [];
  11501. var baseModel = viewModel.getBaseModel();
  11502. var comments = this.comments;
  11503. for (var i=0; i<comments.length; i++) {
  11504. var comment = comments[i];
  11505. var annotation = this._createFoldingAnnotation(viewModel, baseModel, comment.start, comment.end);
  11506. if (annotation) {
  11507. add.push(annotation);
  11508. }
  11509. }
  11510. annotationModel.replaceAnnotations(null, add);
  11511. },
  11512. _createFoldingAnnotation: function(viewModel, baseModel, start, end) {
  11513. var startLine = baseModel.getLineAtOffset(start);
  11514. var endLine = baseModel.getLineAtOffset(end);
  11515. if (startLine === endLine) {
  11516. return null;
  11517. }
  11518. return new mAnnotations.FoldingAnnotation(viewModel, "orion.annotation.folding", start, end,
  11519. "<div class='annotationHTML expanded'></div>", {styleClass: "annotation expanded"},
  11520. "<div class='annotationHTML collapsed'></div>", {styleClass: "annotation collapsed"});
  11521. },
  11522. _computeTasks: function(type, commentStart, commentEnd) {
  11523. if (!this.detectTasks) { return; }
  11524. var annotationModel = this.annotationModel;
  11525. if (!annotationModel) { return; }
  11526. var view = this.view;
  11527. var viewModel = view.getModel(), baseModel = viewModel;
  11528. if (viewModel.getBaseModel) { baseModel = viewModel.getBaseModel(); }
  11529. var annotations = annotationModel.getAnnotations(commentStart, commentEnd);
  11530. var remove = [];
  11531. var annotationType = "orion.annotation.task";
  11532. while (annotations.hasNext()) {
  11533. var annotation = annotations.next();
  11534. if (annotation.type === annotationType) {
  11535. remove.push(annotation);
  11536. }
  11537. }
  11538. var add = [];
  11539. var scanner = this._commentScanner;
  11540. scanner.setText(baseModel.getText(commentStart, commentEnd));
  11541. var token;
  11542. while ((token = scanner.nextToken())) {
  11543. var tokenStart = scanner.getStartOffset() + commentStart;
  11544. if (token === TASK_TAG) {
  11545. var end = baseModel.getLineEnd(baseModel.getLineAtOffset(tokenStart));
  11546. if (type !== SINGLELINE_COMMENT) {
  11547. end = Math.min(end, commentEnd - this.commentEnd.length);
  11548. }
  11549. add.push({
  11550. start: tokenStart,
  11551. end: end,
  11552. type: annotationType,
  11553. title: baseModel.getText(tokenStart, end),
  11554. style: {styleClass: "annotation task"},
  11555. html: "<div class='annotationHTML task'></div>",
  11556. overviewStyle: {styleClass: "annotationOverview task"},
  11557. rangeStyle: {styleClass: "annotationRange task"}
  11558. });
  11559. }
  11560. }
  11561. annotationModel.replaceAnnotations(remove, add);
  11562. },
  11563. _getLineStyle: function(lineIndex) {
  11564. if (this.highlightCaretLine) {
  11565. var view = this.view;
  11566. var model = view.getModel();
  11567. var selection = view.getSelection();
  11568. if (selection.start === selection.end && model.getLineAtOffset(selection.start) === lineIndex) {
  11569. return caretLineStyle;
  11570. }
  11571. }
  11572. return null;
  11573. },
  11574. _getStyles: function(model, text, start) {
  11575. if (model.getBaseModel) {
  11576. start = model.mapOffset(start);
  11577. }
  11578. var end = start + text.length;
  11579. var styles = [];
  11580. // for any sub range that is not a comment, parse code generating tokens (keywords, numbers, brackets, line comments, etc)
  11581. var offset = start, comments = this.comments;
  11582. var startIndex = this._binarySearch(comments, start, true);
  11583. for (var i = startIndex; i < comments.length; i++) {
  11584. if (comments[i].start >= end) { break; }
  11585. var commentStart = comments[i].start;
  11586. var commentEnd = comments[i].end;
  11587. if (offset < commentStart) {
  11588. this._parse(text.substring(offset - start, commentStart - start), offset, styles);
  11589. }
  11590. var style = comments[i].type === DOC_COMMENT ? docCommentStyle : multiCommentStyle;
  11591. if (this.whitespacesVisible || this.detectHyperlinks) {
  11592. var s = Math.max(offset, commentStart);
  11593. var e = Math.min(end, commentEnd);
  11594. this._parseComment(text.substring(s - start, e - start), s, styles, style, comments[i].type);
  11595. } else {
  11596. styles.push({start: commentStart, end: commentEnd, style: style});
  11597. }
  11598. offset = commentEnd;
  11599. }
  11600. if (offset < end) {
  11601. this._parse(text.substring(offset - start, end - start), offset, styles);
  11602. }
  11603. if (model.getBaseModel) {
  11604. for (var j = 0; j < styles.length; j++) {
  11605. var length = styles[j].end - styles[j].start;
  11606. styles[j].start = model.mapOffset(styles[j].start, true);
  11607. styles[j].end = styles[j].start + length;
  11608. }
  11609. }
  11610. return styles;
  11611. },
  11612. _parse: function(text, offset, styles) {
  11613. var scanner = this._scanner;
  11614. scanner.setText(text);
  11615. var token;
  11616. while ((token = scanner.nextToken())) {
  11617. var tokenStart = scanner.getStartOffset() + offset;
  11618. var style = null;
  11619. switch (token) {
  11620. case KEYWORD: style = keywordStyle; break;
  11621. case STRING:
  11622. if (this.whitespacesVisible) {
  11623. this._parseString(scanner.getData(), tokenStart, styles, stringStyle);
  11624. continue;
  11625. } else {
  11626. style = stringStyle;
  11627. }
  11628. break;
  11629. case DOC_COMMENT:
  11630. this._parseComment(scanner.getData(), tokenStart, styles, docCommentStyle, token);
  11631. continue;
  11632. case SINGLELINE_COMMENT:
  11633. this._parseComment(scanner.getData(), tokenStart, styles, singleCommentStyle, token);
  11634. continue;
  11635. case MULTILINE_COMMENT:
  11636. this._parseComment(scanner.getData(), tokenStart, styles, multiCommentStyle, token);
  11637. continue;
  11638. case WHITE_TAB:
  11639. if (this.whitespacesVisible) {
  11640. style = tabStyle;
  11641. }
  11642. break;
  11643. case WHITE_SPACE:
  11644. if (this.whitespacesVisible) {
  11645. style = spaceStyle;
  11646. }
  11647. break;
  11648. }
  11649. styles.push({start: tokenStart, end: scanner.getOffset() + offset, style: style});
  11650. }
  11651. },
  11652. _parseComment: function(text, offset, styles, s, type) {
  11653. var scanner = this._commentScanner;
  11654. scanner.setText(text);
  11655. scanner.setType(type);
  11656. var token;
  11657. while ((token = scanner.nextToken())) {
  11658. var tokenStart = scanner.getStartOffset() + offset;
  11659. var style = s;
  11660. switch (token) {
  11661. case WHITE_TAB:
  11662. if (this.whitespacesVisible) {
  11663. style = tabStyle;
  11664. }
  11665. break;
  11666. case WHITE_SPACE:
  11667. if (this.whitespacesVisible) {
  11668. style = spaceStyle;
  11669. }
  11670. break;
  11671. case HTML_MARKUP:
  11672. style = htmlMarkupStyle;
  11673. break;
  11674. case DOC_TAG:
  11675. style = doctagStyle;
  11676. break;
  11677. case TASK_TAG:
  11678. style = tasktagStyle;
  11679. break;
  11680. default:
  11681. if (this.detectHyperlinks) {
  11682. style = this._detectHyperlinks(scanner.getData(), tokenStart, styles, style);
  11683. }
  11684. }
  11685. if (style) {
  11686. styles.push({start: tokenStart, end: scanner.getOffset() + offset, style: style});
  11687. }
  11688. }
  11689. },
  11690. _parseString: function(text, offset, styles, s) {
  11691. var scanner = this._whitespaceScanner;
  11692. scanner.setText(text);
  11693. var token;
  11694. while ((token = scanner.nextToken())) {
  11695. var tokenStart = scanner.getStartOffset() + offset;
  11696. var style = s;
  11697. switch (token) {
  11698. case WHITE_TAB:
  11699. if (this.whitespacesVisible) {
  11700. style = tabStyle;
  11701. }
  11702. break;
  11703. case WHITE_SPACE:
  11704. if (this.whitespacesVisible) {
  11705. style = spaceStyle;
  11706. }
  11707. break;
  11708. }
  11709. if (style) {
  11710. styles.push({start: tokenStart, end: scanner.getOffset() + offset, style: style});
  11711. }
  11712. }
  11713. },
  11714. _detectHyperlinks: function(text, offset, styles, s) {
  11715. var href = null, index, linkStyle;
  11716. if ((index = text.indexOf("://")) > 0) {
  11717. href = text;
  11718. var start = index;
  11719. while (start > 0) {
  11720. var c = href.charCodeAt(start - 1);
  11721. if (!((97 <= c && c <= 122) || (65 <= c && c <= 90) || 0x2d === c || (48 <= c && c <= 57))) { //LETTER OR DASH OR NUMBER
  11722. break;
  11723. }
  11724. start--;
  11725. }
  11726. if (start > 0) {
  11727. var brackets = "\"\"''(){}[]<>";
  11728. index = brackets.indexOf(href.substring(start - 1, start));
  11729. if (index !== -1 && (index & 1) === 0 && (index = href.lastIndexOf(brackets.substring(index + 1, index + 2))) !== -1) {
  11730. var end = index;
  11731. linkStyle = this._clone(s);
  11732. linkStyle.tagName = "A";
  11733. linkStyle.attributes = {href: href.substring(start, end)};
  11734. styles.push({start: offset, end: offset + start, style: s});
  11735. styles.push({start: offset + start, end: offset + end, style: linkStyle});
  11736. styles.push({start: offset + end, end: offset + text.length, style: s});
  11737. return null;
  11738. }
  11739. }
  11740. } else if (text.toLowerCase().indexOf("bug#") === 0) {
  11741. href = "https://bugs.eclipse.org/bugs/show_bug.cgi?id=" + parseInt(text.substring(4), 10);
  11742. }
  11743. if (href) {
  11744. linkStyle = this._clone(s);
  11745. linkStyle.tagName = "A";
  11746. linkStyle.attributes = {href: href};
  11747. return linkStyle;
  11748. }
  11749. return s;
  11750. },
  11751. _clone: function(obj) {
  11752. if (!obj) { return obj; }
  11753. var newObj = {};
  11754. for (var p in obj) {
  11755. if (obj.hasOwnProperty(p)) {
  11756. var value = obj[p];
  11757. newObj[p] = value;
  11758. }
  11759. }
  11760. return newObj;
  11761. },
  11762. _findComments: function(text, offset) {
  11763. offset = offset || 0;
  11764. var scanner = this._firstScanner, token;
  11765. scanner.setText(text);
  11766. var result = [];
  11767. while ((token = scanner.nextToken())) {
  11768. if (token === MULTILINE_COMMENT || token === DOC_COMMENT) {
  11769. var comment = {
  11770. start: scanner.getStartOffset() + offset,
  11771. end: scanner.getOffset() + offset,
  11772. type: token
  11773. };
  11774. result.push(comment);
  11775. //TODO can we avoid this work if edition does not overlap comment?
  11776. this._computeTasks(token, scanner.getStartOffset() + offset, scanner.getOffset() + offset);
  11777. }
  11778. if (token === SINGLELINE_COMMENT) {
  11779. //TODO can we avoid this work if edition does not overlap comment?
  11780. this._computeTasks(token, scanner.getStartOffset() + offset, scanner.getOffset() + offset);
  11781. }
  11782. }
  11783. return result;
  11784. },
  11785. _findMatchingBracket: function(model, offset) {
  11786. var brackets = "{}()[]<>";
  11787. var bracket = model.getText(offset, offset + 1);
  11788. var bracketIndex = brackets.indexOf(bracket, 0);
  11789. if (bracketIndex === -1) { return -1; }
  11790. var closingBracket;
  11791. if (bracketIndex & 1) {
  11792. closingBracket = brackets.substring(bracketIndex - 1, bracketIndex);
  11793. } else {
  11794. closingBracket = brackets.substring(bracketIndex + 1, bracketIndex + 2);
  11795. }
  11796. var lineIndex = model.getLineAtOffset(offset);
  11797. var lineText = model.getLine(lineIndex);
  11798. var lineStart = model.getLineStart(lineIndex);
  11799. var lineEnd = model.getLineEnd(lineIndex);
  11800. brackets = this._findBrackets(bracket, closingBracket, lineText, lineStart, lineStart, lineEnd);
  11801. for (var i=0; i<brackets.length; i++) {
  11802. var sign = brackets[i] >= 0 ? 1 : -1;
  11803. if (brackets[i] * sign === offset) {
  11804. var level = 1;
  11805. if (bracketIndex & 1) {
  11806. i--;
  11807. for (; i>=0; i--) {
  11808. sign = brackets[i] >= 0 ? 1 : -1;
  11809. level += sign;
  11810. if (level === 0) {
  11811. return brackets[i] * sign;
  11812. }
  11813. }
  11814. lineIndex -= 1;
  11815. while (lineIndex >= 0) {
  11816. lineText = model.getLine(lineIndex);
  11817. lineStart = model.getLineStart(lineIndex);
  11818. lineEnd = model.getLineEnd(lineIndex);
  11819. brackets = this._findBrackets(bracket, closingBracket, lineText, lineStart, lineStart, lineEnd);
  11820. for (var j=brackets.length - 1; j>=0; j--) {
  11821. sign = brackets[j] >= 0 ? 1 : -1;
  11822. level += sign;
  11823. if (level === 0) {
  11824. return brackets[j] * sign;
  11825. }
  11826. }
  11827. lineIndex--;
  11828. }
  11829. } else {
  11830. i++;
  11831. for (; i<brackets.length; i++) {
  11832. sign = brackets[i] >= 0 ? 1 : -1;
  11833. level += sign;
  11834. if (level === 0) {
  11835. return brackets[i] * sign;
  11836. }
  11837. }
  11838. lineIndex += 1;
  11839. var lineCount = model.getLineCount ();
  11840. while (lineIndex < lineCount) {
  11841. lineText = model.getLine(lineIndex);
  11842. lineStart = model.getLineStart(lineIndex);
  11843. lineEnd = model.getLineEnd(lineIndex);
  11844. brackets = this._findBrackets(bracket, closingBracket, lineText, lineStart, lineStart, lineEnd);
  11845. for (var k=0; k<brackets.length; k++) {
  11846. sign = brackets[k] >= 0 ? 1 : -1;
  11847. level += sign;
  11848. if (level === 0) {
  11849. return brackets[k] * sign;
  11850. }
  11851. }
  11852. lineIndex++;
  11853. }
  11854. }
  11855. break;
  11856. }
  11857. }
  11858. return -1;
  11859. },
  11860. _findBrackets: function(bracket, closingBracket, text, textOffset, start, end) {
  11861. var result = [];
  11862. var bracketToken = bracket.charCodeAt(0);
  11863. var closingBracketToken = closingBracket.charCodeAt(0);
  11864. // for any sub range that is not a comment, parse code generating tokens (keywords, numbers, brackets, line comments, etc)
  11865. var offset = start, scanner = this._scanner, token, comments = this.comments;
  11866. var startIndex = this._binarySearch(comments, start, true);
  11867. for (var i = startIndex; i < comments.length; i++) {
  11868. if (comments[i].start >= end) { break; }
  11869. var commentStart = comments[i].start;
  11870. var commentEnd = comments[i].end;
  11871. if (offset < commentStart) {
  11872. scanner.setText(text.substring(offset - start, commentStart - start));
  11873. while ((token = scanner.nextToken())) {
  11874. if (token === bracketToken) {
  11875. result.push(scanner.getStartOffset() + offset - start + textOffset);
  11876. } else if (token === closingBracketToken) {
  11877. result.push(-(scanner.getStartOffset() + offset - start + textOffset));
  11878. }
  11879. }
  11880. }
  11881. offset = commentEnd;
  11882. }
  11883. if (offset < end) {
  11884. scanner.setText(text.substring(offset - start, end - start));
  11885. while ((token = scanner.nextToken())) {
  11886. if (token === bracketToken) {
  11887. result.push(scanner.getStartOffset() + offset - start + textOffset);
  11888. } else if (token === closingBracketToken) {
  11889. result.push(-(scanner.getStartOffset() + offset - start + textOffset));
  11890. }
  11891. }
  11892. }
  11893. return result;
  11894. },
  11895. _onDestroy: function(e) {
  11896. this.destroy();
  11897. },
  11898. _onLineStyle: function (e) {
  11899. if (e.textView === this.view) {
  11900. e.style = this._getLineStyle(e.lineIndex);
  11901. }
  11902. e.ranges = this._getStyles(e.textView.getModel(), e.lineText, e.lineStart);
  11903. },
  11904. _onSelection: function(e) {
  11905. var oldSelection = e.oldValue;
  11906. var newSelection = e.newValue;
  11907. var view = this.view;
  11908. var model = view.getModel();
  11909. var lineIndex;
  11910. if (this.highlightCaretLine) {
  11911. var oldLineIndex = model.getLineAtOffset(oldSelection.start);
  11912. lineIndex = model.getLineAtOffset(newSelection.start);
  11913. var newEmpty = newSelection.start === newSelection.end;
  11914. var oldEmpty = oldSelection.start === oldSelection.end;
  11915. if (!(oldLineIndex === lineIndex && oldEmpty && newEmpty)) {
  11916. if (oldEmpty) {
  11917. view.redrawLines(oldLineIndex, oldLineIndex + 1);
  11918. }
  11919. if ((oldLineIndex !== lineIndex || !oldEmpty) && newEmpty) {
  11920. view.redrawLines(lineIndex, lineIndex + 1);
  11921. }
  11922. }
  11923. }
  11924. if (!this.annotationModel) { return; }
  11925. var remove = this._bracketAnnotations, add, caret;
  11926. if (newSelection.start === newSelection.end && (caret = view.getCaretOffset()) > 0) {
  11927. var mapCaret = caret - 1;
  11928. if (model.getBaseModel) {
  11929. mapCaret = model.mapOffset(mapCaret);
  11930. model = model.getBaseModel();
  11931. }
  11932. var bracket = this._findMatchingBracket(model, mapCaret);
  11933. if (bracket !== -1) {
  11934. add = [{
  11935. start: bracket,
  11936. end: bracket + 1,
  11937. type: "orion.annotation.matchingBracket",
  11938. title: "Matching Bracket",
  11939. html: "<div class='annotationHTML matchingBracket'></div>",
  11940. overviewStyle: {styleClass: "annotationOverview matchingBracket"},
  11941. rangeStyle: {styleClass: "annotationRange matchingBracket"}
  11942. },
  11943. {
  11944. start: mapCaret,
  11945. end: mapCaret + 1,
  11946. type: "orion.annotation.currentBracket",
  11947. title: "Current Bracket",
  11948. html: "<div class='annotationHTML currentBracket'></div>",
  11949. overviewStyle: {styleClass: "annotationOverview currentBracket"},
  11950. rangeStyle: {styleClass: "annotationRange currentBracket"}
  11951. }];
  11952. }
  11953. }
  11954. this._bracketAnnotations = add;
  11955. this.annotationModel.replaceAnnotations(remove, add);
  11956. },
  11957. _onModelChanged: function(e) {
  11958. var start = e.start;
  11959. var removedCharCount = e.removedCharCount;
  11960. var addedCharCount = e.addedCharCount;
  11961. var changeCount = addedCharCount - removedCharCount;
  11962. var view = this.view;
  11963. var viewModel = view.getModel();
  11964. var baseModel = viewModel.getBaseModel ? viewModel.getBaseModel() : viewModel;
  11965. var end = start + removedCharCount;
  11966. var charCount = baseModel.getCharCount();
  11967. var commentCount = this.comments.length;
  11968. var lineStart = baseModel.getLineStart(baseModel.getLineAtOffset(start));
  11969. var commentStart = this._binarySearch(this.comments, lineStart, true);
  11970. var commentEnd = this._binarySearch(this.comments, end, false, commentStart - 1, commentCount);
  11971. var ts;
  11972. if (commentStart < commentCount && this.comments[commentStart].start <= lineStart && lineStart < this.comments[commentStart].end) {
  11973. ts = this.comments[commentStart].start;
  11974. if (ts > start) { ts += changeCount; }
  11975. } else {
  11976. if (commentStart === commentCount && commentCount > 0 && charCount - changeCount === this.comments[commentCount - 1].end) {
  11977. ts = this.comments[commentCount - 1].start;
  11978. } else {
  11979. ts = lineStart;
  11980. }
  11981. }
  11982. var te;
  11983. if (commentEnd < commentCount) {
  11984. te = this.comments[commentEnd].end;
  11985. if (te > start) { te += changeCount; }
  11986. commentEnd += 1;
  11987. } else {
  11988. commentEnd = commentCount;
  11989. te = charCount;//TODO could it be smaller?
  11990. }
  11991. var text = baseModel.getText(ts, te), comment;
  11992. var newComments = this._findComments(text, ts), i;
  11993. for (i = commentStart; i < this.comments.length; i++) {
  11994. comment = this.comments[i];
  11995. if (comment.start > start) { comment.start += changeCount; }
  11996. if (comment.start > start) { comment.end += changeCount; }
  11997. }
  11998. var redraw = (commentEnd - commentStart) !== newComments.length;
  11999. if (!redraw) {
  12000. for (i=0; i<newComments.length; i++) {
  12001. comment = this.comments[commentStart + i];
  12002. var newComment = newComments[i];
  12003. if (comment.start !== newComment.start || comment.end !== newComment.end || comment.type !== newComment.type) {
  12004. redraw = true;
  12005. break;
  12006. }
  12007. }
  12008. }
  12009. var args = [commentStart, commentEnd - commentStart].concat(newComments);
  12010. Array.prototype.splice.apply(this.comments, args);
  12011. if (redraw) {
  12012. var redrawStart = ts;
  12013. var redrawEnd = te;
  12014. if (viewModel !== baseModel) {
  12015. redrawStart = viewModel.mapOffset(redrawStart, true);
  12016. redrawEnd = viewModel.mapOffset(redrawEnd, true);
  12017. }
  12018. view.redrawRange(redrawStart, redrawEnd);
  12019. }
  12020. if (this.foldingEnabled && baseModel !== viewModel && this.annotationModel) {
  12021. var annotationModel = this.annotationModel;
  12022. var iter = annotationModel.getAnnotations(ts, te);
  12023. var remove = [], all = [];
  12024. var annotation;
  12025. while (iter.hasNext()) {
  12026. annotation = iter.next();
  12027. if (annotation.type === "orion.annotation.folding") {
  12028. all.push(annotation);
  12029. for (i = 0; i < newComments.length; i++) {
  12030. if (annotation.start === newComments[i].start && annotation.end === newComments[i].end) {
  12031. break;
  12032. }
  12033. }
  12034. if (i === newComments.length) {
  12035. remove.push(annotation);
  12036. annotation.expand();
  12037. } else {
  12038. var annotationStart = annotation.start;
  12039. var annotationEnd = annotation.end;
  12040. if (annotationStart > start) {
  12041. annotationStart -= changeCount;
  12042. }
  12043. if (annotationEnd > start) {
  12044. annotationEnd -= changeCount;
  12045. }
  12046. if (annotationStart <= start && start < annotationEnd && annotationStart <= end && end < annotationEnd) {
  12047. var startLine = baseModel.getLineAtOffset(annotation.start);
  12048. var endLine = baseModel.getLineAtOffset(annotation.end);
  12049. if (startLine !== endLine) {
  12050. if (!annotation.expanded) {
  12051. annotation.expand();
  12052. annotationModel.modifyAnnotation(annotation);
  12053. }
  12054. } else {
  12055. annotationModel.removeAnnotation(annotation);
  12056. }
  12057. }
  12058. }
  12059. }
  12060. }
  12061. var add = [];
  12062. for (i = 0; i < newComments.length; i++) {
  12063. comment = newComments[i];
  12064. for (var j = 0; j < all.length; j++) {
  12065. if (all[j].start === comment.start && all[j].end === comment.end) {
  12066. break;
  12067. }
  12068. }
  12069. if (j === all.length) {
  12070. annotation = this._createFoldingAnnotation(viewModel, baseModel, comment.start, comment.end);
  12071. if (annotation) {
  12072. add.push(annotation);
  12073. }
  12074. }
  12075. }
  12076. annotationModel.replaceAnnotations(remove, add);
  12077. }
  12078. }
  12079. };
  12080. return {TextStyler: TextStyler};
  12081. });