edit_session.js 84 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587
  1. /* ***** BEGIN LICENSE BLOCK *****
  2. * Distributed under the BSD license:
  3. *
  4. * Copyright (c) 2010, Ajax.org B.V.
  5. * All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions are met:
  9. * * Redistributions of source code must retain the above copyright
  10. * notice, this list of conditions and the following disclaimer.
  11. * * Redistributions in binary form must reproduce the above copyright
  12. * notice, this list of conditions and the following disclaimer in the
  13. * documentation and/or other materials provided with the distribution.
  14. * * Neither the name of Ajax.org B.V. nor the
  15. * names of its contributors may be used to endorse or promote products
  16. * derived from this software without specific prior written permission.
  17. *
  18. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  19. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  24. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  27. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *
  29. * ***** END LICENSE BLOCK ***** */
  30. define(function(require, exports, module) {
  31. "use strict";
  32. var oop = require("./lib/oop");
  33. var lang = require("./lib/lang");
  34. var config = require("./config");
  35. var EventEmitter = require("./lib/event_emitter").EventEmitter;
  36. var Selection = require("./selection").Selection;
  37. var TextMode = require("./mode/text").Mode;
  38. var Range = require("./range").Range;
  39. var Document = require("./document").Document;
  40. var BackgroundTokenizer = require("./background_tokenizer").BackgroundTokenizer;
  41. var SearchHighlight = require("./search_highlight").SearchHighlight;
  42. /**
  43. * Stores all the data about [[Editor `Editor`]] state providing easy way to change editors state.
  44. *
  45. * `EditSession` can be attached to only one [[Document `Document`]]. Same `Document` can be attached to several `EditSession`s.
  46. * @class EditSession
  47. **/
  48. //{ events
  49. /**
  50. *
  51. * Emitted when the document changes.
  52. * @event change
  53. * @param {Object} e An object containing a `delta` of information about the change.
  54. **/
  55. /**
  56. * Emitted when the tab size changes, via [[EditSession.setTabSize]].
  57. *
  58. * @event changeTabSize
  59. **/
  60. /**
  61. * Emitted when the ability to overwrite text changes, via [[EditSession.setOverwrite]].
  62. *
  63. * @event changeOverwrite
  64. **/
  65. /**
  66. * Emitted when the gutter changes, either by setting or removing breakpoints, or when the gutter decorations change.
  67. *
  68. * @event changeBreakpoint
  69. **/
  70. /**
  71. * Emitted when a front marker changes.
  72. *
  73. * @event changeFrontMarker
  74. **/
  75. /**
  76. * Emitted when a back marker changes.
  77. *
  78. * @event changeBackMarker
  79. **/
  80. /**
  81. * Emitted when an annotation changes, like through [[EditSession.setAnnotations]].
  82. *
  83. * @event changeAnnotation
  84. **/
  85. /**
  86. * Emitted when a background tokenizer asynchronously processes new rows.
  87. * @event tokenizerUpdate
  88. *
  89. * @param {Object} e An object containing one property, `"data"`, that contains information about the changing rows
  90. *
  91. **/
  92. /**
  93. * Emitted when the current mode changes.
  94. *
  95. * @event changeMode
  96. *
  97. **/
  98. /**
  99. * Emitted when the wrap mode changes.
  100. *
  101. * @event changeWrapMode
  102. *
  103. **/
  104. /**
  105. * Emitted when the wrapping limit changes.
  106. *
  107. * @event changeWrapLimit
  108. *
  109. **/
  110. /**
  111. * Emitted when a code fold is added or removed.
  112. *
  113. * @event changeFold
  114. *
  115. **/
  116. /**
  117. * Emitted when the scroll top changes.
  118. * @event changeScrollTop
  119. *
  120. * @param {Number} scrollTop The new scroll top value
  121. **/
  122. /**
  123. * Emitted when the scroll left changes.
  124. * @event changeScrollLeft
  125. *
  126. * @param {Number} scrollLeft The new scroll left value
  127. **/
  128. //}
  129. /**
  130. * Sets up a new `EditSession` and associates it with the given `Document` and `TextMode`.
  131. * @param {Document | String} text [If `text` is a `Document`, it associates the `EditSession` with it. Otherwise, a new `Document` is created, with the initial text]{: #textParam}
  132. * @param {TextMode} mode [The inital language mode to use for the document]{: #modeParam}
  133. *
  134. * @constructor
  135. **/
  136. var EditSession = function(text, mode) {
  137. this.$breakpoints = [];
  138. this.$decorations = [];
  139. this.$frontMarkers = {};
  140. this.$backMarkers = {};
  141. this.$markerId = 1;
  142. this.$undoSelect = true;
  143. this.$foldData = [];
  144. this.$foldData.toString = function() {
  145. return this.join("\n");
  146. };
  147. this.on("changeFold", this.onChangeFold.bind(this));
  148. this.$onChange = this.onChange.bind(this);
  149. if (typeof text != "object" || !text.getLine)
  150. text = new Document(text);
  151. this.setDocument(text);
  152. this.selection = new Selection(this);
  153. config.resetOptions(this);
  154. this.setMode(mode);
  155. config._signal("session", this);
  156. };
  157. (function() {
  158. oop.implement(this, EventEmitter);
  159. /**
  160. * Sets the `EditSession` to point to a new `Document`. If a `BackgroundTokenizer` exists, it also points to `doc`.
  161. *
  162. * @param {Document} doc The new `Document` to use
  163. *
  164. **/
  165. this.setDocument = function(doc) {
  166. if (this.doc)
  167. this.doc.removeListener("change", this.$onChange);
  168. this.doc = doc;
  169. doc.on("change", this.$onChange);
  170. if (this.bgTokenizer)
  171. this.bgTokenizer.setDocument(this.getDocument());
  172. this.resetCaches();
  173. };
  174. /**
  175. * Returns the `Document` associated with this session.
  176. * @return {Document}
  177. **/
  178. this.getDocument = function() {
  179. return this.doc;
  180. };
  181. /**
  182. * @param {Number} row The row to work with
  183. *
  184. **/
  185. this.$resetRowCache = function(docRow) {
  186. if (!docRow) {
  187. this.$docRowCache = [];
  188. this.$screenRowCache = [];
  189. return;
  190. }
  191. var l = this.$docRowCache.length;
  192. var i = this.$getRowCacheIndex(this.$docRowCache, docRow) + 1;
  193. if (l > i) {
  194. this.$docRowCache.splice(i, l);
  195. this.$screenRowCache.splice(i, l);
  196. }
  197. };
  198. this.$getRowCacheIndex = function(cacheArray, val) {
  199. var low = 0;
  200. var hi = cacheArray.length - 1;
  201. while (low <= hi) {
  202. var mid = (low + hi) >> 1;
  203. var c = cacheArray[mid];
  204. if (val > c)
  205. low = mid + 1;
  206. else if (val < c)
  207. hi = mid - 1;
  208. else
  209. return mid;
  210. }
  211. return low -1;
  212. };
  213. this.resetCaches = function() {
  214. this.$modified = true;
  215. this.$wrapData = [];
  216. this.$rowLengthCache = [];
  217. this.$resetRowCache(0);
  218. if (this.bgTokenizer)
  219. this.bgTokenizer.start(0);
  220. };
  221. this.onChangeFold = function(e) {
  222. var fold = e.data;
  223. this.$resetRowCache(fold.start.row);
  224. };
  225. this.onChange = function(delta) {
  226. this.$modified = true;
  227. this.$resetRowCache(delta.start.row);
  228. var removedFolds = this.$updateInternalDataOnChange(delta);
  229. if (!this.$fromUndo && this.$undoManager && !delta.ignore) {
  230. this.$deltasDoc.push(delta);
  231. if (removedFolds && removedFolds.length != 0) {
  232. this.$deltasFold.push({
  233. action: "removeFolds",
  234. folds: removedFolds
  235. });
  236. }
  237. this.$informUndoManager.schedule();
  238. }
  239. this.bgTokenizer && this.bgTokenizer.$updateOnChange(delta);
  240. this._signal("change", delta);
  241. };
  242. /**
  243. * Sets the session text.
  244. * @param {String} text The new text to place
  245. *
  246. **/
  247. this.setValue = function(text) {
  248. this.doc.setValue(text);
  249. this.selection.moveTo(0, 0);
  250. this.$resetRowCache(0);
  251. this.$deltas = [];
  252. this.$deltasDoc = [];
  253. this.$deltasFold = [];
  254. this.setUndoManager(this.$undoManager);
  255. this.getUndoManager().reset();
  256. };
  257. /**
  258. * Returns the current [[Document `Document`]] as a string.
  259. * @method toString
  260. * @returns {String}
  261. * @alias EditSession.getValue
  262. *
  263. **/
  264. /**
  265. * Returns the current [[Document `Document`]] as a string.
  266. * @method getValue
  267. * @returns {String}
  268. * @alias EditSession.toString
  269. **/
  270. this.getValue =
  271. this.toString = function() {
  272. return this.doc.getValue();
  273. };
  274. /**
  275. * Returns selection object.
  276. **/
  277. this.getSelection = function() {
  278. return this.selection;
  279. };
  280. /**
  281. * {:BackgroundTokenizer.getState}
  282. * @param {Number} row The row to start at
  283. *
  284. * @related BackgroundTokenizer.getState
  285. **/
  286. this.getState = function(row) {
  287. return this.bgTokenizer.getState(row);
  288. };
  289. /**
  290. * Starts tokenizing at the row indicated. Returns a list of objects of the tokenized rows.
  291. * @param {Number} row The row to start at
  292. *
  293. *
  294. *
  295. **/
  296. this.getTokens = function(row) {
  297. return this.bgTokenizer.getTokens(row);
  298. };
  299. /**
  300. * Returns an object indicating the token at the current row. The object has two properties: `index` and `start`.
  301. * @param {Number} row The row number to retrieve from
  302. * @param {Number} column The column number to retrieve from
  303. *
  304. *
  305. **/
  306. this.getTokenAt = function(row, column) {
  307. var tokens = this.bgTokenizer.getTokens(row);
  308. var token, c = 0;
  309. if (column == null) {
  310. i = tokens.length - 1;
  311. c = this.getLine(row).length;
  312. } else {
  313. for (var i = 0; i < tokens.length; i++) {
  314. c += tokens[i].value.length;
  315. if (c >= column)
  316. break;
  317. }
  318. }
  319. token = tokens[i];
  320. if (!token)
  321. return null;
  322. token.index = i;
  323. token.start = c - token.value.length;
  324. return token;
  325. };
  326. /**
  327. * Sets the undo manager.
  328. * @param {UndoManager} undoManager The new undo manager
  329. *
  330. *
  331. **/
  332. this.setUndoManager = function(undoManager) {
  333. this.$undoManager = undoManager;
  334. this.$deltas = [];
  335. this.$deltasDoc = [];
  336. this.$deltasFold = [];
  337. if (this.$informUndoManager)
  338. this.$informUndoManager.cancel();
  339. if (undoManager) {
  340. var self = this;
  341. this.$syncInformUndoManager = function() {
  342. self.$informUndoManager.cancel();
  343. if (self.$deltasFold.length) {
  344. self.$deltas.push({
  345. group: "fold",
  346. deltas: self.$deltasFold
  347. });
  348. self.$deltasFold = [];
  349. }
  350. if (self.$deltasDoc.length) {
  351. self.$deltas.push({
  352. group: "doc",
  353. deltas: self.$deltasDoc
  354. });
  355. self.$deltasDoc = [];
  356. }
  357. if (self.$deltas.length > 0) {
  358. undoManager.execute({
  359. action: "aceupdate",
  360. args: [self.$deltas, self],
  361. merge: self.mergeUndoDeltas
  362. });
  363. }
  364. self.mergeUndoDeltas = false;
  365. self.$deltas = [];
  366. };
  367. this.$informUndoManager = lang.delayedCall(this.$syncInformUndoManager);
  368. }
  369. };
  370. /**
  371. * starts a new group in undo history
  372. **/
  373. this.markUndoGroup = function() {
  374. if (this.$syncInformUndoManager)
  375. this.$syncInformUndoManager();
  376. };
  377. this.$defaultUndoManager = {
  378. undo: function() {},
  379. redo: function() {},
  380. reset: function() {}
  381. };
  382. /**
  383. * Returns the current undo manager.
  384. **/
  385. this.getUndoManager = function() {
  386. return this.$undoManager || this.$defaultUndoManager;
  387. };
  388. /**
  389. * Returns the current value for tabs. If the user is using soft tabs, this will be a series of spaces (defined by [[EditSession.getTabSize `getTabSize()`]]); otherwise it's simply `'\t'`.
  390. **/
  391. this.getTabString = function() {
  392. if (this.getUseSoftTabs()) {
  393. return lang.stringRepeat(" ", this.getTabSize());
  394. } else {
  395. return "\t";
  396. }
  397. };
  398. /**
  399. * Pass `true` to enable the use of soft tabs. Soft tabs means you're using spaces instead of the tab character (`'\t'`).
  400. * @param {Boolean} useSoftTabs Value indicating whether or not to use soft tabs
  401. **/
  402. this.setUseSoftTabs = function(val) {
  403. this.setOption("useSoftTabs", val);
  404. };
  405. /**
  406. * Returns `true` if soft tabs are being used, `false` otherwise.
  407. * @returns {Boolean}
  408. **/
  409. this.getUseSoftTabs = function() {
  410. // todo might need more general way for changing settings from mode, but this is ok for now
  411. return this.$useSoftTabs && !this.$mode.$indentWithTabs;
  412. };
  413. /**
  414. * Set the number of spaces that define a soft tab; for example, passing in `4` transforms the soft tabs to be equivalent to four spaces. This function also emits the `changeTabSize` event.
  415. * @param {Number} tabSize The new tab size
  416. **/
  417. this.setTabSize = function(tabSize) {
  418. this.setOption("tabSize", tabSize);
  419. };
  420. /**
  421. * Returns the current tab size.
  422. **/
  423. this.getTabSize = function() {
  424. return this.$tabSize;
  425. };
  426. /**
  427. * Returns `true` if the character at the position is a soft tab.
  428. * @param {Object} position The position to check
  429. *
  430. **/
  431. this.isTabStop = function(position) {
  432. return this.$useSoftTabs && (position.column % this.$tabSize === 0);
  433. };
  434. this.$overwrite = false;
  435. /**
  436. * Pass in `true` to enable overwrites in your session, or `false` to disable.
  437. *
  438. * If overwrites is enabled, any text you enter will type over any text after it. If the value of `overwrite` changes, this function also emites the `changeOverwrite` event.
  439. *
  440. * @param {Boolean} overwrite Defines wheter or not to set overwrites
  441. *
  442. *
  443. **/
  444. this.setOverwrite = function(overwrite) {
  445. this.setOption("overwrite", overwrite);
  446. };
  447. /**
  448. * Returns `true` if overwrites are enabled; `false` otherwise.
  449. **/
  450. this.getOverwrite = function() {
  451. return this.$overwrite;
  452. };
  453. /**
  454. * Sets the value of overwrite to the opposite of whatever it currently is.
  455. **/
  456. this.toggleOverwrite = function() {
  457. this.setOverwrite(!this.$overwrite);
  458. };
  459. /**
  460. * Adds `className` to the `row`, to be used for CSS stylings and whatnot.
  461. * @param {Number} row The row number
  462. * @param {String} className The class to add
  463. *
  464. **/
  465. this.addGutterDecoration = function(row, className) {
  466. if (!this.$decorations[row])
  467. this.$decorations[row] = "";
  468. this.$decorations[row] += " " + className;
  469. this._signal("changeBreakpoint", {});
  470. };
  471. /**
  472. * Removes `className` from the `row`.
  473. * @param {Number} row The row number
  474. * @param {String} className The class to add
  475. *
  476. **/
  477. this.removeGutterDecoration = function(row, className) {
  478. this.$decorations[row] = (this.$decorations[row] || "").replace(" " + className, "");
  479. this._signal("changeBreakpoint", {});
  480. };
  481. /**
  482. * Returns an array of numbers, indicating which rows have breakpoints.
  483. * @returns {[Number]}
  484. **/
  485. this.getBreakpoints = function() {
  486. return this.$breakpoints;
  487. };
  488. /**
  489. * Sets a breakpoint on every row number given by `rows`. This function also emites the `'changeBreakpoint'` event.
  490. * @param {Array} rows An array of row indices
  491. *
  492. **/
  493. this.setBreakpoints = function(rows) {
  494. this.$breakpoints = [];
  495. for (var i=0; i<rows.length; i++) {
  496. this.$breakpoints[rows[i]] = "ace_breakpoint";
  497. }
  498. this._signal("changeBreakpoint", {});
  499. };
  500. /**
  501. * Removes all breakpoints on the rows. This function also emites the `'changeBreakpoint'` event.
  502. **/
  503. this.clearBreakpoints = function() {
  504. this.$breakpoints = [];
  505. this._signal("changeBreakpoint", {});
  506. };
  507. /**
  508. * Sets a breakpoint on the row number given by `rows`. This function also emites the `'changeBreakpoint'` event.
  509. * @param {Number} row A row index
  510. * @param {String} className Class of the breakpoint
  511. *
  512. **/
  513. this.setBreakpoint = function(row, className) {
  514. if (className === undefined)
  515. className = "ace_breakpoint";
  516. if (className)
  517. this.$breakpoints[row] = className;
  518. else
  519. delete this.$breakpoints[row];
  520. this._signal("changeBreakpoint", {});
  521. };
  522. /**
  523. * Removes a breakpoint on the row number given by `rows`. This function also emites the `'changeBreakpoint'` event.
  524. * @param {Number} row A row index
  525. *
  526. **/
  527. this.clearBreakpoint = function(row) {
  528. delete this.$breakpoints[row];
  529. this._signal("changeBreakpoint", {});
  530. };
  531. /**
  532. * Adds a new marker to the given `Range`. If `inFront` is `true`, a front marker is defined, and the `'changeFrontMarker'` event fires; otherwise, the `'changeBackMarker'` event fires.
  533. * @param {Range} range Define the range of the marker
  534. * @param {String} clazz Set the CSS class for the marker
  535. * @param {Function | String} type Identify the type of the marker
  536. * @param {Boolean} inFront Set to `true` to establish a front marker
  537. *
  538. * @return {Number} The new marker id
  539. **/
  540. this.addMarker = function(range, clazz, type, inFront) {
  541. var id = this.$markerId++;
  542. var marker = {
  543. range : range,
  544. type : type || "line",
  545. renderer: typeof type == "function" ? type : null,
  546. clazz : clazz,
  547. inFront: !!inFront,
  548. id: id
  549. };
  550. if (inFront) {
  551. this.$frontMarkers[id] = marker;
  552. this._signal("changeFrontMarker");
  553. } else {
  554. this.$backMarkers[id] = marker;
  555. this._signal("changeBackMarker");
  556. }
  557. return id;
  558. };
  559. /**
  560. * Adds a dynamic marker to the session.
  561. * @param {Object} marker object with update method
  562. * @param {Boolean} inFront Set to `true` to establish a front marker
  563. *
  564. * @return {Object} The added marker
  565. **/
  566. this.addDynamicMarker = function(marker, inFront) {
  567. if (!marker.update)
  568. return;
  569. var id = this.$markerId++;
  570. marker.id = id;
  571. marker.inFront = !!inFront;
  572. if (inFront) {
  573. this.$frontMarkers[id] = marker;
  574. this._signal("changeFrontMarker");
  575. } else {
  576. this.$backMarkers[id] = marker;
  577. this._signal("changeBackMarker");
  578. }
  579. return marker;
  580. };
  581. /**
  582. * Removes the marker with the specified ID. If this marker was in front, the `'changeFrontMarker'` event is emitted. If the marker was in the back, the `'changeBackMarker'` event is emitted.
  583. * @param {Number} markerId A number representing a marker
  584. *
  585. **/
  586. this.removeMarker = function(markerId) {
  587. var marker = this.$frontMarkers[markerId] || this.$backMarkers[markerId];
  588. if (!marker)
  589. return;
  590. var markers = marker.inFront ? this.$frontMarkers : this.$backMarkers;
  591. if (marker) {
  592. delete (markers[markerId]);
  593. this._signal(marker.inFront ? "changeFrontMarker" : "changeBackMarker");
  594. }
  595. };
  596. /**
  597. * Returns an object containing all of the markers, either front or back.
  598. * @param {Boolean} inFront If `true`, indicates you only want front markers; `false` indicates only back markers
  599. *
  600. * @returns {Object}
  601. **/
  602. this.getMarkers = function(inFront) {
  603. return inFront ? this.$frontMarkers : this.$backMarkers;
  604. };
  605. this.highlight = function(re) {
  606. if (!this.$searchHighlight) {
  607. var highlight = new SearchHighlight(null, "ace_selected-word", "text");
  608. this.$searchHighlight = this.addDynamicMarker(highlight);
  609. }
  610. this.$searchHighlight.setRegexp(re);
  611. };
  612. // experimental
  613. this.highlightLines = function(startRow, endRow, clazz, inFront) {
  614. if (typeof endRow != "number") {
  615. clazz = endRow;
  616. endRow = startRow;
  617. }
  618. if (!clazz)
  619. clazz = "ace_step";
  620. var range = new Range(startRow, 0, endRow, Infinity);
  621. range.id = this.addMarker(range, clazz, "fullLine", inFront);
  622. return range;
  623. };
  624. /*
  625. * Error:
  626. * {
  627. * row: 12,
  628. * column: 2, //can be undefined
  629. * text: "Missing argument",
  630. * type: "error" // or "warning" or "info"
  631. * }
  632. */
  633. /**
  634. * Sets annotations for the `EditSession`. This functions emits the `'changeAnnotation'` event.
  635. * @param {Array} annotations A list of annotations
  636. *
  637. **/
  638. this.setAnnotations = function(annotations) {
  639. this.$annotations = annotations;
  640. this._signal("changeAnnotation", {});
  641. };
  642. /**
  643. * Returns the annotations for the `EditSession`.
  644. * @returns {Array}
  645. **/
  646. this.getAnnotations = function() {
  647. return this.$annotations || [];
  648. };
  649. /**
  650. * Clears all the annotations for this session. This function also triggers the `'changeAnnotation'` event.
  651. **/
  652. this.clearAnnotations = function() {
  653. this.setAnnotations([]);
  654. };
  655. /**
  656. * If `text` contains either the newline (`\n`) or carriage-return ('\r') characters, `$autoNewLine` stores that value.
  657. * @param {String} text A block of text
  658. *
  659. **/
  660. this.$detectNewLine = function(text) {
  661. var match = text.match(/^.*?(\r?\n)/m);
  662. if (match) {
  663. this.$autoNewLine = match[1];
  664. } else {
  665. this.$autoNewLine = "\n";
  666. }
  667. };
  668. /**
  669. * Given a starting row and column, this method returns the `Range` of the first word boundary it finds.
  670. * @param {Number} row The row to start at
  671. * @param {Number} column The column to start at
  672. *
  673. * @returns {Range}
  674. **/
  675. this.getWordRange = function(row, column) {
  676. var line = this.getLine(row);
  677. var inToken = false;
  678. if (column > 0)
  679. inToken = !!line.charAt(column - 1).match(this.tokenRe);
  680. if (!inToken)
  681. inToken = !!line.charAt(column).match(this.tokenRe);
  682. if (inToken)
  683. var re = this.tokenRe;
  684. else if (/^\s+$/.test(line.slice(column-1, column+1)))
  685. var re = /\s/;
  686. else
  687. var re = this.nonTokenRe;
  688. var start = column;
  689. if (start > 0) {
  690. do {
  691. start--;
  692. }
  693. while (start >= 0 && line.charAt(start).match(re));
  694. start++;
  695. }
  696. var end = column;
  697. while (end < line.length && line.charAt(end).match(re)) {
  698. end++;
  699. }
  700. return new Range(row, start, row, end);
  701. };
  702. /**
  703. * Gets the range of a word, including its right whitespace.
  704. * @param {Number} row The row number to start from
  705. * @param {Number} column The column number to start from
  706. *
  707. * @return {Range}
  708. **/
  709. this.getAWordRange = function(row, column) {
  710. var wordRange = this.getWordRange(row, column);
  711. var line = this.getLine(wordRange.end.row);
  712. while (line.charAt(wordRange.end.column).match(/[ \t]/)) {
  713. wordRange.end.column += 1;
  714. }
  715. return wordRange;
  716. };
  717. /**
  718. * {:Document.setNewLineMode.desc}
  719. * @param {String} newLineMode {:Document.setNewLineMode.param}
  720. *
  721. *
  722. * @related Document.setNewLineMode
  723. **/
  724. this.setNewLineMode = function(newLineMode) {
  725. this.doc.setNewLineMode(newLineMode);
  726. };
  727. /**
  728. *
  729. * Returns the current new line mode.
  730. * @returns {String}
  731. * @related Document.getNewLineMode
  732. **/
  733. this.getNewLineMode = function() {
  734. return this.doc.getNewLineMode();
  735. };
  736. /**
  737. * Identifies if you want to use a worker for the `EditSession`.
  738. * @param {Boolean} useWorker Set to `true` to use a worker
  739. *
  740. **/
  741. this.setUseWorker = function(useWorker) { this.setOption("useWorker", useWorker); };
  742. /**
  743. * Returns `true` if workers are being used.
  744. **/
  745. this.getUseWorker = function() { return this.$useWorker; };
  746. /**
  747. * Reloads all the tokens on the current session. This function calls [[BackgroundTokenizer.start `BackgroundTokenizer.start ()`]] to all the rows; it also emits the `'tokenizerUpdate'` event.
  748. **/
  749. this.onReloadTokenizer = function(e) {
  750. var rows = e.data;
  751. this.bgTokenizer.start(rows.first);
  752. this._signal("tokenizerUpdate", e);
  753. };
  754. this.$modes = {};
  755. /**
  756. * Sets a new text mode for the `EditSession`. This method also emits the `'changeMode'` event. If a [[BackgroundTokenizer `BackgroundTokenizer`]] is set, the `'tokenizerUpdate'` event is also emitted.
  757. * @param {TextMode} mode Set a new text mode
  758. * @param {cb} optional callback
  759. *
  760. **/
  761. this.$mode = null;
  762. this.$modeId = null;
  763. this.setMode = function(mode, cb) {
  764. if (mode && typeof mode === "object") {
  765. if (mode.getTokenizer)
  766. return this.$onChangeMode(mode);
  767. var options = mode;
  768. var path = options.path;
  769. } else {
  770. path = mode || "ace/mode/text";
  771. }
  772. // this is needed if ace isn't on require path (e.g tests in node)
  773. if (!this.$modes["ace/mode/text"])
  774. this.$modes["ace/mode/text"] = new TextMode();
  775. if (this.$modes[path] && !options) {
  776. this.$onChangeMode(this.$modes[path]);
  777. cb && cb();
  778. return;
  779. }
  780. // load on demand
  781. this.$modeId = path;
  782. config.loadModule(["mode", path], function(m) {
  783. if (this.$modeId !== path)
  784. return cb && cb();
  785. if (this.$modes[path] && !options) {
  786. this.$onChangeMode(this.$modes[path]);
  787. } else if (m && m.Mode) {
  788. m = new m.Mode(options);
  789. if (!options) {
  790. this.$modes[path] = m;
  791. m.$id = path;
  792. }
  793. this.$onChangeMode(m);
  794. }
  795. cb && cb();
  796. }.bind(this));
  797. // set mode to text until loading is finished
  798. if (!this.$mode)
  799. this.$onChangeMode(this.$modes["ace/mode/text"], true);
  800. };
  801. this.$onChangeMode = function(mode, $isPlaceholder) {
  802. if (!$isPlaceholder)
  803. this.$modeId = mode.$id;
  804. if (this.$mode === mode)
  805. return;
  806. this.$mode = mode;
  807. this.$stopWorker();
  808. if (this.$useWorker)
  809. this.$startWorker();
  810. var tokenizer = mode.getTokenizer();
  811. if(tokenizer.addEventListener !== undefined) {
  812. var onReloadTokenizer = this.onReloadTokenizer.bind(this);
  813. tokenizer.addEventListener("update", onReloadTokenizer);
  814. }
  815. if (!this.bgTokenizer) {
  816. this.bgTokenizer = new BackgroundTokenizer(tokenizer);
  817. var _self = this;
  818. this.bgTokenizer.addEventListener("update", function(e) {
  819. _self._signal("tokenizerUpdate", e);
  820. });
  821. } else {
  822. this.bgTokenizer.setTokenizer(tokenizer);
  823. }
  824. this.bgTokenizer.setDocument(this.getDocument());
  825. this.tokenRe = mode.tokenRe;
  826. this.nonTokenRe = mode.nonTokenRe;
  827. if (!$isPlaceholder) {
  828. // experimental method, used by c9 findiniles
  829. if (mode.attachToSession)
  830. mode.attachToSession(this);
  831. this.$options.wrapMethod.set.call(this, this.$wrapMethod);
  832. this.$setFolding(mode.foldingRules);
  833. this.bgTokenizer.start(0);
  834. this._emit("changeMode");
  835. }
  836. };
  837. this.$completers = [];
  838. /**
  839. * Sets the completers to be used for this session only. Each completer should provide a getCompletions function (see language_tools)
  840. * @param {Object[]} completers
  841. */
  842. this.setCompleters = function(completers) {
  843. this.$completers = completers;
  844. };
  845. this.getCompleters = function() {
  846. return this.$completers;
  847. };
  848. this.$stopWorker = function() {
  849. if (this.$worker) {
  850. this.$worker.terminate();
  851. this.$worker = null;
  852. }
  853. };
  854. this.$startWorker = function() {
  855. try {
  856. this.$worker = this.$mode.createWorker(this);
  857. } catch (e) {
  858. config.warn("Could not load worker", e);
  859. this.$worker = null;
  860. }
  861. };
  862. /**
  863. * Returns the current text mode.
  864. * @returns {TextMode} The current text mode
  865. **/
  866. this.getMode = function() {
  867. return this.$mode;
  868. };
  869. this.$scrollTop = 0;
  870. /**
  871. * This function sets the scroll top value. It also emits the `'changeScrollTop'` event.
  872. * @param {Number} scrollTop The new scroll top value
  873. *
  874. **/
  875. this.setScrollTop = function(scrollTop) {
  876. // TODO: should we force integer lineheight instead? scrollTop = Math.round(scrollTop);
  877. if (this.$scrollTop === scrollTop || isNaN(scrollTop))
  878. return;
  879. this.$scrollTop = scrollTop;
  880. this._signal("changeScrollTop", scrollTop);
  881. };
  882. /**
  883. * [Returns the value of the distance between the top of the editor and the topmost part of the visible content.]{: #EditSession.getScrollTop}
  884. * @returns {Number}
  885. **/
  886. this.getScrollTop = function() {
  887. return this.$scrollTop;
  888. };
  889. this.$scrollLeft = 0;
  890. /**
  891. * [Sets the value of the distance between the left of the editor and the leftmost part of the visible content.]{: #EditSession.setScrollLeft}
  892. **/
  893. this.setScrollLeft = function(scrollLeft) {
  894. // scrollLeft = Math.round(scrollLeft);
  895. if (this.$scrollLeft === scrollLeft || isNaN(scrollLeft))
  896. return;
  897. this.$scrollLeft = scrollLeft;
  898. this._signal("changeScrollLeft", scrollLeft);
  899. };
  900. /**
  901. * [Returns the value of the distance between the left of the editor and the leftmost part of the visible content.]{: #EditSession.getScrollLeft}
  902. * @returns {Number}
  903. **/
  904. this.getScrollLeft = function() {
  905. return this.$scrollLeft;
  906. };
  907. /**
  908. * Returns the width of the screen.
  909. * @returns {Number}
  910. **/
  911. this.getScreenWidth = function() {
  912. this.$computeWidth();
  913. if (this.lineWidgets)
  914. return Math.max(this.getLineWidgetMaxWidth(), this.screenWidth);
  915. return this.screenWidth;
  916. };
  917. this.getLineWidgetMaxWidth = function() {
  918. if (this.lineWidgetsWidth != null) return this.lineWidgetsWidth;
  919. var width = 0;
  920. this.lineWidgets.forEach(function(w) {
  921. if (w && w.screenWidth > width)
  922. width = w.screenWidth;
  923. });
  924. return this.lineWidgetWidth = width;
  925. };
  926. this.$computeWidth = function(force) {
  927. if (this.$modified || force) {
  928. this.$modified = false;
  929. if (this.$useWrapMode)
  930. return this.screenWidth = this.$wrapLimit;
  931. var lines = this.doc.getAllLines();
  932. var cache = this.$rowLengthCache;
  933. var longestScreenLine = 0;
  934. var foldIndex = 0;
  935. var foldLine = this.$foldData[foldIndex];
  936. var foldStart = foldLine ? foldLine.start.row : Infinity;
  937. var len = lines.length;
  938. for (var i = 0; i < len; i++) {
  939. if (i > foldStart) {
  940. i = foldLine.end.row + 1;
  941. if (i >= len)
  942. break;
  943. foldLine = this.$foldData[foldIndex++];
  944. foldStart = foldLine ? foldLine.start.row : Infinity;
  945. }
  946. if (cache[i] == null)
  947. cache[i] = this.$getStringScreenWidth(lines[i])[0];
  948. if (cache[i] > longestScreenLine)
  949. longestScreenLine = cache[i];
  950. }
  951. this.screenWidth = longestScreenLine;
  952. }
  953. };
  954. /**
  955. * Returns a verbatim copy of the given line as it is in the document
  956. * @param {Number} row The row to retrieve from
  957. *
  958. * @returns {String}
  959. **/
  960. this.getLine = function(row) {
  961. return this.doc.getLine(row);
  962. };
  963. /**
  964. * Returns an array of strings of the rows between `firstRow` and `lastRow`. This function is inclusive of `lastRow`.
  965. * @param {Number} firstRow The first row index to retrieve
  966. * @param {Number} lastRow The final row index to retrieve
  967. *
  968. * @returns {[String]}
  969. *
  970. **/
  971. this.getLines = function(firstRow, lastRow) {
  972. return this.doc.getLines(firstRow, lastRow);
  973. };
  974. /**
  975. * Returns the number of rows in the document.
  976. * @returns {Number}
  977. **/
  978. this.getLength = function() {
  979. return this.doc.getLength();
  980. };
  981. /**
  982. * {:Document.getTextRange.desc}
  983. * @param {Range} range The range to work with
  984. *
  985. * @returns {String}
  986. **/
  987. this.getTextRange = function(range) {
  988. return this.doc.getTextRange(range || this.selection.getRange());
  989. };
  990. /**
  991. * Inserts a block of `text` and the indicated `position`.
  992. * @param {Object} position The position {row, column} to start inserting at
  993. * @param {String} text A chunk of text to insert
  994. * @returns {Object} The position of the last line of `text`. If the length of `text` is 0, this function simply returns `position`.
  995. *
  996. *
  997. **/
  998. this.insert = function(position, text) {
  999. return this.doc.insert(position, text);
  1000. };
  1001. /**
  1002. * Removes the `range` from the document.
  1003. * @param {Range} range A specified Range to remove
  1004. * @returns {Object} The new `start` property of the range, which contains `startRow` and `startColumn`. If `range` is empty, this function returns the unmodified value of `range.start`.
  1005. *
  1006. * @related Document.remove
  1007. *
  1008. **/
  1009. this.remove = function(range) {
  1010. return this.doc.remove(range);
  1011. };
  1012. /**
  1013. * Removes a range of full lines. This method also triggers the `'change'` event.
  1014. * @param {Number} firstRow The first row to be removed
  1015. * @param {Number} lastRow The last row to be removed
  1016. * @returns {[String]} Returns all the removed lines.
  1017. *
  1018. * @related Document.removeFullLines
  1019. *
  1020. **/
  1021. this.removeFullLines = function(firstRow, lastRow){
  1022. return this.doc.removeFullLines(firstRow, lastRow);
  1023. };
  1024. /**
  1025. * Reverts previous changes to your document.
  1026. * @param {Array} deltas An array of previous changes
  1027. * @param {Boolean} dontSelect [If `true`, doesn't select the range of where the change occured]{: #dontSelect}
  1028. *
  1029. * @returns {Range}
  1030. **/
  1031. this.undoChanges = function(deltas, dontSelect) {
  1032. if (!deltas.length)
  1033. return;
  1034. this.$fromUndo = true;
  1035. var lastUndoRange = null;
  1036. for (var i = deltas.length - 1; i != -1; i--) {
  1037. var delta = deltas[i];
  1038. if (delta.group == "doc") {
  1039. this.doc.revertDeltas(delta.deltas);
  1040. lastUndoRange =
  1041. this.$getUndoSelection(delta.deltas, true, lastUndoRange);
  1042. } else {
  1043. delta.deltas.forEach(function(foldDelta) {
  1044. this.addFolds(foldDelta.folds);
  1045. }, this);
  1046. }
  1047. }
  1048. this.$fromUndo = false;
  1049. lastUndoRange &&
  1050. this.$undoSelect &&
  1051. !dontSelect &&
  1052. this.selection.setSelectionRange(lastUndoRange);
  1053. return lastUndoRange;
  1054. };
  1055. /**
  1056. * Re-implements a previously undone change to your document.
  1057. * @param {Array} deltas An array of previous changes
  1058. * @param {Boolean} dontSelect {:dontSelect}
  1059. *
  1060. * @returns {Range}
  1061. **/
  1062. this.redoChanges = function(deltas, dontSelect) {
  1063. if (!deltas.length)
  1064. return;
  1065. this.$fromUndo = true;
  1066. var lastUndoRange = null;
  1067. for (var i = 0; i < deltas.length; i++) {
  1068. var delta = deltas[i];
  1069. if (delta.group == "doc") {
  1070. this.doc.applyDeltas(delta.deltas);
  1071. lastUndoRange =
  1072. this.$getUndoSelection(delta.deltas, false, lastUndoRange);
  1073. }
  1074. }
  1075. this.$fromUndo = false;
  1076. lastUndoRange &&
  1077. this.$undoSelect &&
  1078. !dontSelect &&
  1079. this.selection.setSelectionRange(lastUndoRange);
  1080. return lastUndoRange;
  1081. };
  1082. /**
  1083. * Enables or disables highlighting of the range where an undo occured.
  1084. * @param {Boolean} enable If `true`, selects the range of the reinserted change
  1085. *
  1086. **/
  1087. this.setUndoSelect = function(enable) {
  1088. this.$undoSelect = enable;
  1089. };
  1090. this.$getUndoSelection = function(deltas, isUndo, lastUndoRange) {
  1091. function isInsert(delta) {
  1092. return isUndo ? delta.action !== "insert" : delta.action === "insert";
  1093. }
  1094. var delta = deltas[0];
  1095. var range, point;
  1096. var lastDeltaIsInsert = false;
  1097. if (isInsert(delta)) {
  1098. range = Range.fromPoints(delta.start, delta.end);
  1099. lastDeltaIsInsert = true;
  1100. } else {
  1101. range = Range.fromPoints(delta.start, delta.start);
  1102. lastDeltaIsInsert = false;
  1103. }
  1104. for (var i = 1; i < deltas.length; i++) {
  1105. delta = deltas[i];
  1106. if (isInsert(delta)) {
  1107. point = delta.start;
  1108. if (range.compare(point.row, point.column) == -1) {
  1109. range.setStart(point);
  1110. }
  1111. point = delta.end;
  1112. if (range.compare(point.row, point.column) == 1) {
  1113. range.setEnd(point);
  1114. }
  1115. lastDeltaIsInsert = true;
  1116. } else {
  1117. point = delta.start;
  1118. if (range.compare(point.row, point.column) == -1) {
  1119. range = Range.fromPoints(delta.start, delta.start);
  1120. }
  1121. lastDeltaIsInsert = false;
  1122. }
  1123. }
  1124. // Check if this range and the last undo range has something in common.
  1125. // If true, merge the ranges.
  1126. if (lastUndoRange != null) {
  1127. if (Range.comparePoints(lastUndoRange.start, range.start) === 0) {
  1128. lastUndoRange.start.column += range.end.column - range.start.column;
  1129. lastUndoRange.end.column += range.end.column - range.start.column;
  1130. }
  1131. var cmp = lastUndoRange.compareRange(range);
  1132. if (cmp == 1) {
  1133. range.setStart(lastUndoRange.start);
  1134. } else if (cmp == -1) {
  1135. range.setEnd(lastUndoRange.end);
  1136. }
  1137. }
  1138. return range;
  1139. };
  1140. /**
  1141. * Replaces a range in the document with the new `text`.
  1142. *
  1143. * @param {Range} range A specified Range to replace
  1144. * @param {String} text The new text to use as a replacement
  1145. * @returns {Object} An object containing the final row and column, like this:
  1146. * ```
  1147. * {row: endRow, column: 0}
  1148. * ```
  1149. * If the text and range are empty, this function returns an object containing the current `range.start` value.
  1150. * If the text is the exact same as what currently exists, this function returns an object containing the current `range.end` value.
  1151. *
  1152. * @related Document.replace
  1153. **/
  1154. this.replace = function(range, text) {
  1155. return this.doc.replace(range, text);
  1156. };
  1157. /**
  1158. * Moves a range of text from the given range to the given position. `toPosition` is an object that looks like this:
  1159. * ```json
  1160. * { row: newRowLocation, column: newColumnLocation }
  1161. * ```
  1162. * @param {Range} fromRange The range of text you want moved within the document
  1163. * @param {Object} toPosition The location (row and column) where you want to move the text to
  1164. * @returns {Range} The new range where the text was moved to.
  1165. **/
  1166. this.moveText = function(fromRange, toPosition, copy) {
  1167. var text = this.getTextRange(fromRange);
  1168. var folds = this.getFoldsInRange(fromRange);
  1169. var toRange = Range.fromPoints(toPosition, toPosition);
  1170. if (!copy) {
  1171. this.remove(fromRange);
  1172. var rowDiff = fromRange.start.row - fromRange.end.row;
  1173. var collDiff = rowDiff ? -fromRange.end.column : fromRange.start.column - fromRange.end.column;
  1174. if (collDiff) {
  1175. if (toRange.start.row == fromRange.end.row && toRange.start.column > fromRange.end.column)
  1176. toRange.start.column += collDiff;
  1177. if (toRange.end.row == fromRange.end.row && toRange.end.column > fromRange.end.column)
  1178. toRange.end.column += collDiff;
  1179. }
  1180. if (rowDiff && toRange.start.row >= fromRange.end.row) {
  1181. toRange.start.row += rowDiff;
  1182. toRange.end.row += rowDiff;
  1183. }
  1184. }
  1185. toRange.end = this.insert(toRange.start, text);
  1186. if (folds.length) {
  1187. var oldStart = fromRange.start;
  1188. var newStart = toRange.start;
  1189. var rowDiff = newStart.row - oldStart.row;
  1190. var collDiff = newStart.column - oldStart.column;
  1191. this.addFolds(folds.map(function(x) {
  1192. x = x.clone();
  1193. if (x.start.row == oldStart.row)
  1194. x.start.column += collDiff;
  1195. if (x.end.row == oldStart.row)
  1196. x.end.column += collDiff;
  1197. x.start.row += rowDiff;
  1198. x.end.row += rowDiff;
  1199. return x;
  1200. }));
  1201. }
  1202. return toRange;
  1203. };
  1204. /**
  1205. * Indents all the rows, from `startRow` to `endRow` (inclusive), by prefixing each row with the token in `indentString`.
  1206. *
  1207. * If `indentString` contains the `'\t'` character, it's replaced by whatever is defined by [[EditSession.getTabString `getTabString()`]].
  1208. * @param {Number} startRow Starting row
  1209. * @param {Number} endRow Ending row
  1210. * @param {String} indentString The indent token
  1211. *
  1212. *
  1213. **/
  1214. this.indentRows = function(startRow, endRow, indentString) {
  1215. indentString = indentString.replace(/\t/g, this.getTabString());
  1216. for (var row=startRow; row<=endRow; row++)
  1217. this.doc.insertInLine({row: row, column: 0}, indentString);
  1218. };
  1219. /**
  1220. * Outdents all the rows defined by the `start` and `end` properties of `range`.
  1221. * @param {Range} range A range of rows
  1222. *
  1223. **/
  1224. this.outdentRows = function (range) {
  1225. var rowRange = range.collapseRows();
  1226. var deleteRange = new Range(0, 0, 0, 0);
  1227. var size = this.getTabSize();
  1228. for (var i = rowRange.start.row; i <= rowRange.end.row; ++i) {
  1229. var line = this.getLine(i);
  1230. deleteRange.start.row = i;
  1231. deleteRange.end.row = i;
  1232. for (var j = 0; j < size; ++j)
  1233. if (line.charAt(j) != ' ')
  1234. break;
  1235. if (j < size && line.charAt(j) == '\t') {
  1236. deleteRange.start.column = j;
  1237. deleteRange.end.column = j + 1;
  1238. } else {
  1239. deleteRange.start.column = 0;
  1240. deleteRange.end.column = j;
  1241. }
  1242. this.remove(deleteRange);
  1243. }
  1244. };
  1245. this.$moveLines = function(firstRow, lastRow, dir) {
  1246. firstRow = this.getRowFoldStart(firstRow);
  1247. lastRow = this.getRowFoldEnd(lastRow);
  1248. if (dir < 0) {
  1249. var row = this.getRowFoldStart(firstRow + dir);
  1250. if (row < 0) return 0;
  1251. var diff = row-firstRow;
  1252. } else if (dir > 0) {
  1253. var row = this.getRowFoldEnd(lastRow + dir);
  1254. if (row > this.doc.getLength()-1) return 0;
  1255. var diff = row-lastRow;
  1256. } else {
  1257. firstRow = this.$clipRowToDocument(firstRow);
  1258. lastRow = this.$clipRowToDocument(lastRow);
  1259. var diff = lastRow - firstRow + 1;
  1260. }
  1261. var range = new Range(firstRow, 0, lastRow, Number.MAX_VALUE);
  1262. var folds = this.getFoldsInRange(range).map(function(x){
  1263. x = x.clone();
  1264. x.start.row += diff;
  1265. x.end.row += diff;
  1266. return x;
  1267. });
  1268. var lines = dir == 0
  1269. ? this.doc.getLines(firstRow, lastRow)
  1270. : this.doc.removeFullLines(firstRow, lastRow);
  1271. this.doc.insertFullLines(firstRow+diff, lines);
  1272. folds.length && this.addFolds(folds);
  1273. return diff;
  1274. };
  1275. /**
  1276. * Shifts all the lines in the document up one, starting from `firstRow` and ending at `lastRow`.
  1277. * @param {Number} firstRow The starting row to move up
  1278. * @param {Number} lastRow The final row to move up
  1279. * @returns {Number} If `firstRow` is less-than or equal to 0, this function returns 0. Otherwise, on success, it returns -1.
  1280. *
  1281. **/
  1282. this.moveLinesUp = function(firstRow, lastRow) {
  1283. return this.$moveLines(firstRow, lastRow, -1);
  1284. };
  1285. /**
  1286. * Shifts all the lines in the document down one, starting from `firstRow` and ending at `lastRow`.
  1287. * @param {Number} firstRow The starting row to move down
  1288. * @param {Number} lastRow The final row to move down
  1289. * @returns {Number} If `firstRow` is less-than or equal to 0, this function returns 0. Otherwise, on success, it returns -1.
  1290. **/
  1291. this.moveLinesDown = function(firstRow, lastRow) {
  1292. return this.$moveLines(firstRow, lastRow, 1);
  1293. };
  1294. /**
  1295. * Duplicates all the text between `firstRow` and `lastRow`.
  1296. * @param {Number} firstRow The starting row to duplicate
  1297. * @param {Number} lastRow The final row to duplicate
  1298. * @returns {Number} Returns the number of new rows added; in other words, `lastRow - firstRow + 1`.
  1299. *
  1300. *
  1301. **/
  1302. this.duplicateLines = function(firstRow, lastRow) {
  1303. return this.$moveLines(firstRow, lastRow, 0);
  1304. };
  1305. this.$clipRowToDocument = function(row) {
  1306. return Math.max(0, Math.min(row, this.doc.getLength()-1));
  1307. };
  1308. this.$clipColumnToRow = function(row, column) {
  1309. if (column < 0)
  1310. return 0;
  1311. return Math.min(this.doc.getLine(row).length, column);
  1312. };
  1313. this.$clipPositionToDocument = function(row, column) {
  1314. column = Math.max(0, column);
  1315. if (row < 0) {
  1316. row = 0;
  1317. column = 0;
  1318. } else {
  1319. var len = this.doc.getLength();
  1320. if (row >= len) {
  1321. row = len - 1;
  1322. column = this.doc.getLine(len-1).length;
  1323. } else {
  1324. column = Math.min(this.doc.getLine(row).length, column);
  1325. }
  1326. }
  1327. return {
  1328. row: row,
  1329. column: column
  1330. };
  1331. };
  1332. this.$clipRangeToDocument = function(range) {
  1333. if (range.start.row < 0) {
  1334. range.start.row = 0;
  1335. range.start.column = 0;
  1336. } else {
  1337. range.start.column = this.$clipColumnToRow(
  1338. range.start.row,
  1339. range.start.column
  1340. );
  1341. }
  1342. var len = this.doc.getLength() - 1;
  1343. if (range.end.row > len) {
  1344. range.end.row = len;
  1345. range.end.column = this.doc.getLine(len).length;
  1346. } else {
  1347. range.end.column = this.$clipColumnToRow(
  1348. range.end.row,
  1349. range.end.column
  1350. );
  1351. }
  1352. return range;
  1353. };
  1354. // WRAPMODE
  1355. this.$wrapLimit = 80;
  1356. this.$useWrapMode = false;
  1357. this.$wrapLimitRange = {
  1358. min : null,
  1359. max : null
  1360. };
  1361. /**
  1362. * Sets whether or not line wrapping is enabled. If `useWrapMode` is different than the current value, the `'changeWrapMode'` event is emitted.
  1363. * @param {Boolean} useWrapMode Enable (or disable) wrap mode
  1364. *
  1365. **/
  1366. this.setUseWrapMode = function(useWrapMode) {
  1367. if (useWrapMode != this.$useWrapMode) {
  1368. this.$useWrapMode = useWrapMode;
  1369. this.$modified = true;
  1370. this.$resetRowCache(0);
  1371. // If wrapMode is activaed, the wrapData array has to be initialized.
  1372. if (useWrapMode) {
  1373. var len = this.getLength();
  1374. this.$wrapData = Array(len);
  1375. this.$updateWrapData(0, len - 1);
  1376. }
  1377. this._signal("changeWrapMode");
  1378. }
  1379. };
  1380. /**
  1381. * Returns `true` if wrap mode is being used; `false` otherwise.
  1382. * @returns {Boolean}
  1383. **/
  1384. this.getUseWrapMode = function() {
  1385. return this.$useWrapMode;
  1386. };
  1387. // Allow the wrap limit to move freely between min and max. Either
  1388. // parameter can be null to allow the wrap limit to be unconstrained
  1389. // in that direction. Or set both parameters to the same number to pin
  1390. // the limit to that value.
  1391. /**
  1392. * Sets the boundaries of wrap. Either value can be `null` to have an unconstrained wrap, or, they can be the same number to pin the limit. If the wrap limits for `min` or `max` are different, this method also emits the `'changeWrapMode'` event.
  1393. * @param {Number} min The minimum wrap value (the left side wrap)
  1394. * @param {Number} max The maximum wrap value (the right side wrap)
  1395. *
  1396. **/
  1397. this.setWrapLimitRange = function(min, max) {
  1398. if (this.$wrapLimitRange.min !== min || this.$wrapLimitRange.max !== max) {
  1399. this.$wrapLimitRange = { min: min, max: max };
  1400. this.$modified = true;
  1401. // This will force a recalculation of the wrap limit
  1402. if (this.$useWrapMode)
  1403. this._signal("changeWrapMode");
  1404. }
  1405. };
  1406. /**
  1407. * This should generally only be called by the renderer when a resize is detected.
  1408. * @param {Number} desiredLimit The new wrap limit
  1409. * @returns {Boolean}
  1410. *
  1411. * @private
  1412. **/
  1413. this.adjustWrapLimit = function(desiredLimit, $printMargin) {
  1414. var limits = this.$wrapLimitRange;
  1415. if (limits.max < 0)
  1416. limits = {min: $printMargin, max: $printMargin};
  1417. var wrapLimit = this.$constrainWrapLimit(desiredLimit, limits.min, limits.max);
  1418. if (wrapLimit != this.$wrapLimit && wrapLimit > 1) {
  1419. this.$wrapLimit = wrapLimit;
  1420. this.$modified = true;
  1421. if (this.$useWrapMode) {
  1422. this.$updateWrapData(0, this.getLength() - 1);
  1423. this.$resetRowCache(0);
  1424. this._signal("changeWrapLimit");
  1425. }
  1426. return true;
  1427. }
  1428. return false;
  1429. };
  1430. this.$constrainWrapLimit = function(wrapLimit, min, max) {
  1431. if (min)
  1432. wrapLimit = Math.max(min, wrapLimit);
  1433. if (max)
  1434. wrapLimit = Math.min(max, wrapLimit);
  1435. return wrapLimit;
  1436. };
  1437. /**
  1438. * Returns the value of wrap limit.
  1439. * @returns {Number} The wrap limit.
  1440. **/
  1441. this.getWrapLimit = function() {
  1442. return this.$wrapLimit;
  1443. };
  1444. /**
  1445. * Sets the line length for soft wrap in the editor. Lines will break
  1446. * at a minimum of the given length minus 20 chars and at a maximum
  1447. * of the given number of chars.
  1448. * @param {number} limit The maximum line length in chars, for soft wrapping lines.
  1449. */
  1450. this.setWrapLimit = function (limit) {
  1451. this.setWrapLimitRange(limit, limit);
  1452. };
  1453. /**
  1454. * Returns an object that defines the minimum and maximum of the wrap limit; it looks something like this:
  1455. *
  1456. * { min: wrapLimitRange_min, max: wrapLimitRange_max }
  1457. *
  1458. * @returns {Object}
  1459. **/
  1460. this.getWrapLimitRange = function() {
  1461. // Avoid unexpected mutation by returning a copy
  1462. return {
  1463. min : this.$wrapLimitRange.min,
  1464. max : this.$wrapLimitRange.max
  1465. };
  1466. };
  1467. this.$updateInternalDataOnChange = function(delta) {
  1468. var useWrapMode = this.$useWrapMode;
  1469. var action = delta.action;
  1470. var start = delta.start;
  1471. var end = delta.end;
  1472. var firstRow = start.row;
  1473. var lastRow = end.row;
  1474. var len = lastRow - firstRow;
  1475. var removedFolds = null;
  1476. this.$updating = true;
  1477. if (len != 0) {
  1478. if (action === "remove") {
  1479. this[useWrapMode ? "$wrapData" : "$rowLengthCache"].splice(firstRow, len);
  1480. var foldLines = this.$foldData;
  1481. removedFolds = this.getFoldsInRange(delta);
  1482. this.removeFolds(removedFolds);
  1483. var foldLine = this.getFoldLine(end.row);
  1484. var idx = 0;
  1485. if (foldLine) {
  1486. foldLine.addRemoveChars(end.row, end.column, start.column - end.column);
  1487. foldLine.shiftRow(-len);
  1488. var foldLineBefore = this.getFoldLine(firstRow);
  1489. if (foldLineBefore && foldLineBefore !== foldLine) {
  1490. foldLineBefore.merge(foldLine);
  1491. foldLine = foldLineBefore;
  1492. }
  1493. idx = foldLines.indexOf(foldLine) + 1;
  1494. }
  1495. for (idx; idx < foldLines.length; idx++) {
  1496. var foldLine = foldLines[idx];
  1497. if (foldLine.start.row >= end.row) {
  1498. foldLine.shiftRow(-len);
  1499. }
  1500. }
  1501. lastRow = firstRow;
  1502. } else {
  1503. var args = Array(len);
  1504. args.unshift(firstRow, 0);
  1505. var arr = useWrapMode ? this.$wrapData : this.$rowLengthCache
  1506. arr.splice.apply(arr, args);
  1507. // If some new line is added inside of a foldLine, then split
  1508. // the fold line up.
  1509. var foldLines = this.$foldData;
  1510. var foldLine = this.getFoldLine(firstRow);
  1511. var idx = 0;
  1512. if (foldLine) {
  1513. var cmp = foldLine.range.compareInside(start.row, start.column);
  1514. // Inside of the foldLine range. Need to split stuff up.
  1515. if (cmp == 0) {
  1516. foldLine = foldLine.split(start.row, start.column);
  1517. if (foldLine) {
  1518. foldLine.shiftRow(len);
  1519. foldLine.addRemoveChars(lastRow, 0, end.column - start.column);
  1520. }
  1521. } else
  1522. // Infront of the foldLine but same row. Need to shift column.
  1523. if (cmp == -1) {
  1524. foldLine.addRemoveChars(firstRow, 0, end.column - start.column);
  1525. foldLine.shiftRow(len);
  1526. }
  1527. // Nothing to do if the insert is after the foldLine.
  1528. idx = foldLines.indexOf(foldLine) + 1;
  1529. }
  1530. for (idx; idx < foldLines.length; idx++) {
  1531. var foldLine = foldLines[idx];
  1532. if (foldLine.start.row >= firstRow) {
  1533. foldLine.shiftRow(len);
  1534. }
  1535. }
  1536. }
  1537. } else {
  1538. // Realign folds. E.g. if you add some new chars before a fold, the
  1539. // fold should "move" to the right.
  1540. len = Math.abs(delta.start.column - delta.end.column);
  1541. if (action === "remove") {
  1542. // Get all the folds in the change range and remove them.
  1543. removedFolds = this.getFoldsInRange(delta);
  1544. this.removeFolds(removedFolds);
  1545. len = -len;
  1546. }
  1547. var foldLine = this.getFoldLine(firstRow);
  1548. if (foldLine) {
  1549. foldLine.addRemoveChars(firstRow, start.column, len);
  1550. }
  1551. }
  1552. if (useWrapMode && this.$wrapData.length != this.doc.getLength()) {
  1553. console.error("doc.getLength() and $wrapData.length have to be the same!");
  1554. }
  1555. this.$updating = false;
  1556. if (useWrapMode)
  1557. this.$updateWrapData(firstRow, lastRow);
  1558. else
  1559. this.$updateRowLengthCache(firstRow, lastRow);
  1560. return removedFolds;
  1561. };
  1562. this.$updateRowLengthCache = function(firstRow, lastRow, b) {
  1563. this.$rowLengthCache[firstRow] = null;
  1564. this.$rowLengthCache[lastRow] = null;
  1565. };
  1566. this.$updateWrapData = function(firstRow, lastRow) {
  1567. var lines = this.doc.getAllLines();
  1568. var tabSize = this.getTabSize();
  1569. var wrapData = this.$wrapData;
  1570. var wrapLimit = this.$wrapLimit;
  1571. var tokens;
  1572. var foldLine;
  1573. var row = firstRow;
  1574. lastRow = Math.min(lastRow, lines.length - 1);
  1575. while (row <= lastRow) {
  1576. foldLine = this.getFoldLine(row, foldLine);
  1577. if (!foldLine) {
  1578. tokens = this.$getDisplayTokens(lines[row]);
  1579. wrapData[row] = this.$computeWrapSplits(tokens, wrapLimit, tabSize);
  1580. row ++;
  1581. } else {
  1582. tokens = [];
  1583. foldLine.walk(function(placeholder, row, column, lastColumn) {
  1584. var walkTokens;
  1585. if (placeholder != null) {
  1586. walkTokens = this.$getDisplayTokens(
  1587. placeholder, tokens.length);
  1588. walkTokens[0] = PLACEHOLDER_START;
  1589. for (var i = 1; i < walkTokens.length; i++) {
  1590. walkTokens[i] = PLACEHOLDER_BODY;
  1591. }
  1592. } else {
  1593. walkTokens = this.$getDisplayTokens(
  1594. lines[row].substring(lastColumn, column),
  1595. tokens.length);
  1596. }
  1597. tokens = tokens.concat(walkTokens);
  1598. }.bind(this),
  1599. foldLine.end.row,
  1600. lines[foldLine.end.row].length + 1
  1601. );
  1602. wrapData[foldLine.start.row] = this.$computeWrapSplits(tokens, wrapLimit, tabSize);
  1603. row = foldLine.end.row + 1;
  1604. }
  1605. }
  1606. };
  1607. // "Tokens"
  1608. var CHAR = 1,
  1609. CHAR_EXT = 2,
  1610. PLACEHOLDER_START = 3,
  1611. PLACEHOLDER_BODY = 4,
  1612. PUNCTUATION = 9,
  1613. SPACE = 10,
  1614. TAB = 11,
  1615. TAB_SPACE = 12;
  1616. this.$computeWrapSplits = function(tokens, wrapLimit, tabSize) {
  1617. if (tokens.length == 0) {
  1618. return [];
  1619. }
  1620. var splits = [];
  1621. var displayLength = tokens.length;
  1622. var lastSplit = 0, lastDocSplit = 0;
  1623. var isCode = this.$wrapAsCode;
  1624. var indentedSoftWrap = this.$indentedSoftWrap;
  1625. var maxIndent = wrapLimit <= Math.max(2 * tabSize, 8)
  1626. || indentedSoftWrap === false ? 0 : Math.floor(wrapLimit / 2);
  1627. function getWrapIndent() {
  1628. var indentation = 0;
  1629. if (maxIndent === 0)
  1630. return indentation;
  1631. if (indentedSoftWrap) {
  1632. for (var i = 0; i < tokens.length; i++) {
  1633. var token = tokens[i];
  1634. if (token == SPACE)
  1635. indentation += 1;
  1636. else if (token == TAB)
  1637. indentation += tabSize;
  1638. else if (token == TAB_SPACE)
  1639. continue;
  1640. else
  1641. break;
  1642. }
  1643. }
  1644. if (isCode && indentedSoftWrap !== false)
  1645. indentation += tabSize;
  1646. return Math.min(indentation, maxIndent);
  1647. }
  1648. function addSplit(screenPos) {
  1649. var displayed = tokens.slice(lastSplit, screenPos);
  1650. // The document size is the current size - the extra width for tabs
  1651. // and multipleWidth characters.
  1652. var len = displayed.length;
  1653. displayed.join("").
  1654. // Get all the TAB_SPACEs.
  1655. replace(/12/g, function() {
  1656. len -= 1;
  1657. }).
  1658. // Get all the CHAR_EXT/multipleWidth characters.
  1659. replace(/2/g, function() {
  1660. len -= 1;
  1661. });
  1662. if (!splits.length) {
  1663. indent = getWrapIndent();
  1664. splits.indent = indent;
  1665. }
  1666. lastDocSplit += len;
  1667. splits.push(lastDocSplit);
  1668. lastSplit = screenPos;
  1669. }
  1670. var indent = 0;
  1671. while (displayLength - lastSplit > wrapLimit - indent) {
  1672. // This is, where the split should be.
  1673. var split = lastSplit + wrapLimit - indent;
  1674. // If there is a space or tab at this split position, then making
  1675. // a split is simple.
  1676. if (tokens[split - 1] >= SPACE && tokens[split] >= SPACE) {
  1677. /* disabled see https://github.com/ajaxorg/ace/issues/1186
  1678. // Include all following spaces + tabs in this split as well.
  1679. while (tokens[split] >= SPACE) {
  1680. split ++;
  1681. } */
  1682. addSplit(split);
  1683. continue;
  1684. }
  1685. // === ELSE ===
  1686. // Check if split is inside of a placeholder. Placeholder are
  1687. // not splitable. Therefore, seek the beginning of the placeholder
  1688. // and try to place the split beofre the placeholder's start.
  1689. if (tokens[split] == PLACEHOLDER_START || tokens[split] == PLACEHOLDER_BODY) {
  1690. // Seek the start of the placeholder and do the split
  1691. // before the placeholder. By definition there always
  1692. // a PLACEHOLDER_START between split and lastSplit.
  1693. for (split; split != lastSplit - 1; split--) {
  1694. if (tokens[split] == PLACEHOLDER_START) {
  1695. // split++; << No incremental here as we want to
  1696. // have the position before the Placeholder.
  1697. break;
  1698. }
  1699. }
  1700. // If the PLACEHOLDER_START is not the index of the
  1701. // last split, then we can do the split
  1702. if (split > lastSplit) {
  1703. addSplit(split);
  1704. continue;
  1705. }
  1706. // If the PLACEHOLDER_START IS the index of the last
  1707. // split, then we have to place the split after the
  1708. // placeholder. So, let's seek for the end of the placeholder.
  1709. split = lastSplit + wrapLimit;
  1710. for (split; split < tokens.length; split++) {
  1711. if (tokens[split] != PLACEHOLDER_BODY) {
  1712. break;
  1713. }
  1714. }
  1715. // If spilt == tokens.length, then the placeholder is the last
  1716. // thing in the line and adding a new split doesn't make sense.
  1717. if (split == tokens.length) {
  1718. break; // Breaks the while-loop.
  1719. }
  1720. // Finally, add the split...
  1721. addSplit(split);
  1722. continue;
  1723. }
  1724. // === ELSE ===
  1725. // Search for the first non space/tab/placeholder/punctuation token backwards.
  1726. var minSplit = Math.max(split - (wrapLimit -(wrapLimit>>2)), lastSplit - 1);
  1727. while (split > minSplit && tokens[split] < PLACEHOLDER_START) {
  1728. split --;
  1729. }
  1730. if (isCode) {
  1731. while (split > minSplit && tokens[split] < PLACEHOLDER_START) {
  1732. split --;
  1733. }
  1734. while (split > minSplit && tokens[split] == PUNCTUATION) {
  1735. split --;
  1736. }
  1737. } else {
  1738. while (split > minSplit && tokens[split] < SPACE) {
  1739. split --;
  1740. }
  1741. }
  1742. // If we found one, then add the split.
  1743. if (split > minSplit) {
  1744. addSplit(++split);
  1745. continue;
  1746. }
  1747. // === ELSE ===
  1748. split = lastSplit + wrapLimit;
  1749. // The split is inside of a CHAR or CHAR_EXT token and no space
  1750. // around -> force a split.
  1751. if (tokens[split] == CHAR_EXT)
  1752. split--;
  1753. addSplit(split - indent);
  1754. }
  1755. return splits;
  1756. };
  1757. /**
  1758. * Given a string, returns an array of the display characters, including tabs and spaces.
  1759. * @param {String} str The string to check
  1760. * @param {Number} offset The value to start at
  1761. *
  1762. **/
  1763. this.$getDisplayTokens = function(str, offset) {
  1764. var arr = [];
  1765. var tabSize;
  1766. offset = offset || 0;
  1767. for (var i = 0; i < str.length; i++) {
  1768. var c = str.charCodeAt(i);
  1769. // Tab
  1770. if (c == 9) {
  1771. tabSize = this.getScreenTabSize(arr.length + offset);
  1772. arr.push(TAB);
  1773. for (var n = 1; n < tabSize; n++) {
  1774. arr.push(TAB_SPACE);
  1775. }
  1776. }
  1777. // Space
  1778. else if (c == 32) {
  1779. arr.push(SPACE);
  1780. } else if((c > 39 && c < 48) || (c > 57 && c < 64)) {
  1781. arr.push(PUNCTUATION);
  1782. }
  1783. // full width characters
  1784. else if (c >= 0x1100 && isFullWidth(c)) {
  1785. arr.push(CHAR, CHAR_EXT);
  1786. } else {
  1787. arr.push(CHAR);
  1788. }
  1789. }
  1790. return arr;
  1791. };
  1792. /**
  1793. * Calculates the width of the string `str` on the screen while assuming that the string starts at the first column on the screen.
  1794. * @param {String} str The string to calculate the screen width of
  1795. * @param {Number} maxScreenColumn
  1796. * @param {Number} screenColumn
  1797. * @returns {[Number]} Returns an `int[]` array with two elements:<br/>
  1798. * The first position indicates the number of columns for `str` on screen.<br/>
  1799. * The second value contains the position of the document column that this function read until.
  1800. *
  1801. **/
  1802. this.$getStringScreenWidth = function(str, maxScreenColumn, screenColumn) {
  1803. if (maxScreenColumn == 0)
  1804. return [0, 0];
  1805. if (maxScreenColumn == null)
  1806. maxScreenColumn = Infinity;
  1807. screenColumn = screenColumn || 0;
  1808. var c, column;
  1809. for (column = 0; column < str.length; column++) {
  1810. c = str.charCodeAt(column);
  1811. // tab
  1812. if (c == 9) {
  1813. screenColumn += this.getScreenTabSize(screenColumn);
  1814. }
  1815. // full width characters
  1816. else if (c >= 0x1100 && isFullWidth(c)) {
  1817. screenColumn += 2;
  1818. } else {
  1819. screenColumn += 1;
  1820. }
  1821. if (screenColumn > maxScreenColumn) {
  1822. break;
  1823. }
  1824. }
  1825. return [screenColumn, column];
  1826. };
  1827. this.lineWidgets = null;
  1828. /**
  1829. * Returns number of screenrows in a wrapped line.
  1830. * @param {Number} row The row number to check
  1831. *
  1832. * @returns {Number}
  1833. **/
  1834. this.getRowLength = function(row) {
  1835. if (this.lineWidgets)
  1836. var h = this.lineWidgets[row] && this.lineWidgets[row].rowCount || 0;
  1837. else
  1838. h = 0
  1839. if (!this.$useWrapMode || !this.$wrapData[row]) {
  1840. return 1 + h;
  1841. } else {
  1842. return this.$wrapData[row].length + 1 + h;
  1843. }
  1844. };
  1845. this.getRowLineCount = function(row) {
  1846. if (!this.$useWrapMode || !this.$wrapData[row]) {
  1847. return 1;
  1848. } else {
  1849. return this.$wrapData[row].length + 1;
  1850. }
  1851. };
  1852. this.getRowWrapIndent = function(screenRow) {
  1853. if (this.$useWrapMode) {
  1854. var pos = this.screenToDocumentPosition(screenRow, Number.MAX_VALUE);
  1855. var splits = this.$wrapData[pos.row];
  1856. return splits.length && splits[0] < pos.column ? splits.indent : 0;
  1857. } else {
  1858. return 0;
  1859. }
  1860. }
  1861. /**
  1862. * Returns the position (on screen) for the last character in the provided screen row.
  1863. * @param {Number} screenRow The screen row to check
  1864. * @returns {Number}
  1865. *
  1866. * @related EditSession.documentToScreenColumn
  1867. **/
  1868. this.getScreenLastRowColumn = function(screenRow) {
  1869. var pos = this.screenToDocumentPosition(screenRow, Number.MAX_VALUE);
  1870. return this.documentToScreenColumn(pos.row, pos.column);
  1871. };
  1872. /**
  1873. * For the given document row and column, this returns the column position of the last screen row.
  1874. * @param {Number} docRow
  1875. *
  1876. * @param {Number} docColumn
  1877. **/
  1878. this.getDocumentLastRowColumn = function(docRow, docColumn) {
  1879. var screenRow = this.documentToScreenRow(docRow, docColumn);
  1880. return this.getScreenLastRowColumn(screenRow);
  1881. };
  1882. /**
  1883. * For the given document row and column, this returns the document position of the last row.
  1884. * @param {Number} docRow
  1885. * @param {Number} docColumn
  1886. *
  1887. **/
  1888. this.getDocumentLastRowColumnPosition = function(docRow, docColumn) {
  1889. var screenRow = this.documentToScreenRow(docRow, docColumn);
  1890. return this.screenToDocumentPosition(screenRow, Number.MAX_VALUE / 10);
  1891. };
  1892. /**
  1893. * For the given row, this returns the split data.
  1894. * @returns {String}
  1895. **/
  1896. this.getRowSplitData = function(row) {
  1897. if (!this.$useWrapMode) {
  1898. return undefined;
  1899. } else {
  1900. return this.$wrapData[row];
  1901. }
  1902. };
  1903. /**
  1904. * The distance to the next tab stop at the specified screen column.
  1905. * @param {Number} screenColumn The screen column to check
  1906. *
  1907. * @returns {Number}
  1908. **/
  1909. this.getScreenTabSize = function(screenColumn) {
  1910. return this.$tabSize - screenColumn % this.$tabSize;
  1911. };
  1912. this.screenToDocumentRow = function(screenRow, screenColumn) {
  1913. return this.screenToDocumentPosition(screenRow, screenColumn).row;
  1914. };
  1915. this.screenToDocumentColumn = function(screenRow, screenColumn) {
  1916. return this.screenToDocumentPosition(screenRow, screenColumn).column;
  1917. };
  1918. /**
  1919. * Converts characters coordinates on the screen to characters coordinates within the document. [This takes into account code folding, word wrap, tab size, and any other visual modifications.]{: #conversionConsiderations}
  1920. * @param {Number} screenRow The screen row to check
  1921. * @param {Number} screenColumn The screen column to check
  1922. * @returns {Object} The object returned has two properties: `row` and `column`.
  1923. *
  1924. * @related EditSession.documentToScreenPosition
  1925. **/
  1926. this.screenToDocumentPosition = function(screenRow, screenColumn) {
  1927. if (screenRow < 0)
  1928. return {row: 0, column: 0};
  1929. var line;
  1930. var docRow = 0;
  1931. var docColumn = 0;
  1932. var column;
  1933. var row = 0;
  1934. var rowLength = 0;
  1935. var rowCache = this.$screenRowCache;
  1936. var i = this.$getRowCacheIndex(rowCache, screenRow);
  1937. var l = rowCache.length;
  1938. if (l && i >= 0) {
  1939. var row = rowCache[i];
  1940. var docRow = this.$docRowCache[i];
  1941. var doCache = screenRow > rowCache[l - 1];
  1942. } else {
  1943. var doCache = !l;
  1944. }
  1945. var maxRow = this.getLength() - 1;
  1946. var foldLine = this.getNextFoldLine(docRow);
  1947. var foldStart = foldLine ? foldLine.start.row : Infinity;
  1948. while (row <= screenRow) {
  1949. rowLength = this.getRowLength(docRow);
  1950. if (row + rowLength > screenRow || docRow >= maxRow) {
  1951. break;
  1952. } else {
  1953. row += rowLength;
  1954. docRow++;
  1955. if (docRow > foldStart) {
  1956. docRow = foldLine.end.row+1;
  1957. foldLine = this.getNextFoldLine(docRow, foldLine);
  1958. foldStart = foldLine ? foldLine.start.row : Infinity;
  1959. }
  1960. }
  1961. if (doCache) {
  1962. this.$docRowCache.push(docRow);
  1963. this.$screenRowCache.push(row);
  1964. }
  1965. }
  1966. if (foldLine && foldLine.start.row <= docRow) {
  1967. line = this.getFoldDisplayLine(foldLine);
  1968. docRow = foldLine.start.row;
  1969. } else if (row + rowLength <= screenRow || docRow > maxRow) {
  1970. // clip at the end of the document
  1971. return {
  1972. row: maxRow,
  1973. column: this.getLine(maxRow).length
  1974. };
  1975. } else {
  1976. line = this.getLine(docRow);
  1977. foldLine = null;
  1978. }
  1979. var wrapIndent = 0;
  1980. if (this.$useWrapMode) {
  1981. var splits = this.$wrapData[docRow];
  1982. if (splits) {
  1983. var splitIndex = Math.floor(screenRow - row);
  1984. column = splits[splitIndex];
  1985. if(splitIndex > 0 && splits.length) {
  1986. wrapIndent = splits.indent;
  1987. docColumn = splits[splitIndex - 1] || splits[splits.length - 1];
  1988. line = line.substring(docColumn);
  1989. }
  1990. }
  1991. }
  1992. docColumn += this.$getStringScreenWidth(line, screenColumn - wrapIndent)[1];
  1993. // We remove one character at the end so that the docColumn
  1994. // position returned is not associated to the next row on the screen.
  1995. if (this.$useWrapMode && docColumn >= column)
  1996. docColumn = column - 1;
  1997. if (foldLine)
  1998. return foldLine.idxToPosition(docColumn);
  1999. return {row: docRow, column: docColumn};
  2000. };
  2001. /**
  2002. * Converts document coordinates to screen coordinates. {:conversionConsiderations}
  2003. * @param {Number} docRow The document row to check
  2004. * @param {Number} docColumn The document column to check
  2005. * @returns {Object} The object returned by this method has two properties: `row` and `column`.
  2006. *
  2007. * @related EditSession.screenToDocumentPosition
  2008. **/
  2009. this.documentToScreenPosition = function(docRow, docColumn) {
  2010. // Normalize the passed in arguments.
  2011. if (typeof docColumn === "undefined")
  2012. var pos = this.$clipPositionToDocument(docRow.row, docRow.column);
  2013. else
  2014. pos = this.$clipPositionToDocument(docRow, docColumn);
  2015. docRow = pos.row;
  2016. docColumn = pos.column;
  2017. var screenRow = 0;
  2018. var foldStartRow = null;
  2019. var fold = null;
  2020. // Clamp the docRow position in case it's inside of a folded block.
  2021. fold = this.getFoldAt(docRow, docColumn, 1);
  2022. if (fold) {
  2023. docRow = fold.start.row;
  2024. docColumn = fold.start.column;
  2025. }
  2026. var rowEnd, row = 0;
  2027. var rowCache = this.$docRowCache;
  2028. var i = this.$getRowCacheIndex(rowCache, docRow);
  2029. var l = rowCache.length;
  2030. if (l && i >= 0) {
  2031. var row = rowCache[i];
  2032. var screenRow = this.$screenRowCache[i];
  2033. var doCache = docRow > rowCache[l - 1];
  2034. } else {
  2035. var doCache = !l;
  2036. }
  2037. var foldLine = this.getNextFoldLine(row);
  2038. var foldStart = foldLine ?foldLine.start.row :Infinity;
  2039. while (row < docRow) {
  2040. if (row >= foldStart) {
  2041. rowEnd = foldLine.end.row + 1;
  2042. if (rowEnd > docRow)
  2043. break;
  2044. foldLine = this.getNextFoldLine(rowEnd, foldLine);
  2045. foldStart = foldLine ?foldLine.start.row :Infinity;
  2046. }
  2047. else {
  2048. rowEnd = row + 1;
  2049. }
  2050. screenRow += this.getRowLength(row);
  2051. row = rowEnd;
  2052. if (doCache) {
  2053. this.$docRowCache.push(row);
  2054. this.$screenRowCache.push(screenRow);
  2055. }
  2056. }
  2057. // Calculate the text line that is displayed in docRow on the screen.
  2058. var textLine = "";
  2059. // Check if the final row we want to reach is inside of a fold.
  2060. if (foldLine && row >= foldStart) {
  2061. textLine = this.getFoldDisplayLine(foldLine, docRow, docColumn);
  2062. foldStartRow = foldLine.start.row;
  2063. } else {
  2064. textLine = this.getLine(docRow).substring(0, docColumn);
  2065. foldStartRow = docRow;
  2066. }
  2067. var wrapIndent = 0;
  2068. // Clamp textLine if in wrapMode.
  2069. if (this.$useWrapMode) {
  2070. var wrapRow = this.$wrapData[foldStartRow];
  2071. if (wrapRow) {
  2072. var screenRowOffset = 0;
  2073. while (textLine.length >= wrapRow[screenRowOffset]) {
  2074. screenRow ++;
  2075. screenRowOffset++;
  2076. }
  2077. textLine = textLine.substring(
  2078. wrapRow[screenRowOffset - 1] || 0, textLine.length
  2079. );
  2080. wrapIndent = screenRowOffset > 0 ? wrapRow.indent : 0;
  2081. }
  2082. }
  2083. return {
  2084. row: screenRow,
  2085. column: wrapIndent + this.$getStringScreenWidth(textLine)[0]
  2086. };
  2087. };
  2088. /**
  2089. * For the given document row and column, returns the screen column.
  2090. * @param {Number} row
  2091. * @param {Number} docColumn
  2092. * @returns {Number}
  2093. *
  2094. **/
  2095. this.documentToScreenColumn = function(row, docColumn) {
  2096. return this.documentToScreenPosition(row, docColumn).column;
  2097. };
  2098. /**
  2099. * For the given document row and column, returns the screen row.
  2100. * @param {Number} docRow
  2101. * @param {Number} docColumn
  2102. *
  2103. **/
  2104. this.documentToScreenRow = function(docRow, docColumn) {
  2105. return this.documentToScreenPosition(docRow, docColumn).row;
  2106. };
  2107. /**
  2108. * Returns the length of the screen.
  2109. * @returns {Number}
  2110. **/
  2111. this.getScreenLength = function() {
  2112. var screenRows = 0;
  2113. var fold = null;
  2114. if (!this.$useWrapMode) {
  2115. screenRows = this.getLength();
  2116. // Remove the folded lines again.
  2117. var foldData = this.$foldData;
  2118. for (var i = 0; i < foldData.length; i++) {
  2119. fold = foldData[i];
  2120. screenRows -= fold.end.row - fold.start.row;
  2121. }
  2122. } else {
  2123. var lastRow = this.$wrapData.length;
  2124. var row = 0, i = 0;
  2125. var fold = this.$foldData[i++];
  2126. var foldStart = fold ? fold.start.row :Infinity;
  2127. while (row < lastRow) {
  2128. var splits = this.$wrapData[row];
  2129. screenRows += splits ? splits.length + 1 : 1;
  2130. row ++;
  2131. if (row > foldStart) {
  2132. row = fold.end.row+1;
  2133. fold = this.$foldData[i++];
  2134. foldStart = fold ?fold.start.row :Infinity;
  2135. }
  2136. }
  2137. }
  2138. // todo
  2139. if (this.lineWidgets)
  2140. screenRows += this.$getWidgetScreenLength();
  2141. return screenRows;
  2142. };
  2143. /**
  2144. * @private
  2145. *
  2146. */
  2147. this.$setFontMetrics = function(fm) {
  2148. if (!this.$enableVarChar) return;
  2149. this.$getStringScreenWidth = function(str, maxScreenColumn, screenColumn) {
  2150. if (maxScreenColumn === 0)
  2151. return [0, 0];
  2152. if (!maxScreenColumn)
  2153. maxScreenColumn = Infinity;
  2154. screenColumn = screenColumn || 0;
  2155. var c, column;
  2156. for (column = 0; column < str.length; column++) {
  2157. c = str.charAt(column);
  2158. // tab
  2159. if (c === "\t") {
  2160. screenColumn += this.getScreenTabSize(screenColumn);
  2161. } else {
  2162. screenColumn += fm.getCharacterWidth(c);
  2163. }
  2164. if (screenColumn > maxScreenColumn) {
  2165. break;
  2166. }
  2167. }
  2168. return [screenColumn, column];
  2169. };
  2170. };
  2171. this.destroy = function() {
  2172. if (this.bgTokenizer) {
  2173. this.bgTokenizer.setDocument(null);
  2174. this.bgTokenizer = null;
  2175. }
  2176. this.$stopWorker();
  2177. };
  2178. // For every keystroke this gets called once per char in the whole doc!!
  2179. // Wouldn't hurt to make it a bit faster for c >= 0x1100
  2180. function isFullWidth(c) {
  2181. if (c < 0x1100)
  2182. return false;
  2183. return c >= 0x1100 && c <= 0x115F ||
  2184. c >= 0x11A3 && c <= 0x11A7 ||
  2185. c >= 0x11FA && c <= 0x11FF ||
  2186. c >= 0x2329 && c <= 0x232A ||
  2187. c >= 0x2E80 && c <= 0x2E99 ||
  2188. c >= 0x2E9B && c <= 0x2EF3 ||
  2189. c >= 0x2F00 && c <= 0x2FD5 ||
  2190. c >= 0x2FF0 && c <= 0x2FFB ||
  2191. c >= 0x3000 && c <= 0x303E ||
  2192. c >= 0x3041 && c <= 0x3096 ||
  2193. c >= 0x3099 && c <= 0x30FF ||
  2194. c >= 0x3105 && c <= 0x312D ||
  2195. c >= 0x3131 && c <= 0x318E ||
  2196. c >= 0x3190 && c <= 0x31BA ||
  2197. c >= 0x31C0 && c <= 0x31E3 ||
  2198. c >= 0x31F0 && c <= 0x321E ||
  2199. c >= 0x3220 && c <= 0x3247 ||
  2200. c >= 0x3250 && c <= 0x32FE ||
  2201. c >= 0x3300 && c <= 0x4DBF ||
  2202. c >= 0x4E00 && c <= 0xA48C ||
  2203. c >= 0xA490 && c <= 0xA4C6 ||
  2204. c >= 0xA960 && c <= 0xA97C ||
  2205. c >= 0xAC00 && c <= 0xD7A3 ||
  2206. c >= 0xD7B0 && c <= 0xD7C6 ||
  2207. c >= 0xD7CB && c <= 0xD7FB ||
  2208. c >= 0xF900 && c <= 0xFAFF ||
  2209. c >= 0xFE10 && c <= 0xFE19 ||
  2210. c >= 0xFE30 && c <= 0xFE52 ||
  2211. c >= 0xFE54 && c <= 0xFE66 ||
  2212. c >= 0xFE68 && c <= 0xFE6B ||
  2213. c >= 0xFF01 && c <= 0xFF60 ||
  2214. c >= 0xFFE0 && c <= 0xFFE6;
  2215. };
  2216. }).call(EditSession.prototype);
  2217. require("./edit_session/folding").Folding.call(EditSession.prototype);
  2218. require("./edit_session/bracket_match").BracketMatch.call(EditSession.prototype);
  2219. config.defineOptions(EditSession.prototype, "session", {
  2220. wrap: {
  2221. set: function(value) {
  2222. if (!value || value == "off")
  2223. value = false;
  2224. else if (value == "free")
  2225. value = true;
  2226. else if (value == "printMargin")
  2227. value = -1;
  2228. else if (typeof value == "string")
  2229. value = parseInt(value, 10) || false;
  2230. if (this.$wrap == value)
  2231. return;
  2232. this.$wrap = value;
  2233. if (!value) {
  2234. this.setUseWrapMode(false);
  2235. } else {
  2236. var col = typeof value == "number" ? value : null;
  2237. this.setWrapLimitRange(col, col);
  2238. this.setUseWrapMode(true);
  2239. }
  2240. },
  2241. get: function() {
  2242. if (this.getUseWrapMode()) {
  2243. if (this.$wrap == -1)
  2244. return "printMargin";
  2245. if (!this.getWrapLimitRange().min)
  2246. return "free";
  2247. return this.$wrap;
  2248. }
  2249. return "off";
  2250. },
  2251. handlesSet: true
  2252. },
  2253. wrapMethod: {
  2254. // code|text|auto
  2255. set: function(val) {
  2256. val = val == "auto"
  2257. ? this.$mode.type != "text"
  2258. : val != "text";
  2259. if (val != this.$wrapAsCode) {
  2260. this.$wrapAsCode = val;
  2261. if (this.$useWrapMode) {
  2262. this.$modified = true;
  2263. this.$resetRowCache(0);
  2264. this.$updateWrapData(0, this.getLength() - 1);
  2265. }
  2266. }
  2267. },
  2268. initialValue: "auto"
  2269. },
  2270. indentedSoftWrap: { initialValue: true },
  2271. firstLineNumber: {
  2272. set: function() {this._signal("changeBreakpoint");},
  2273. initialValue: 1
  2274. },
  2275. useWorker: {
  2276. set: function(useWorker) {
  2277. this.$useWorker = useWorker;
  2278. this.$stopWorker();
  2279. if (useWorker)
  2280. this.$startWorker();
  2281. },
  2282. initialValue: true
  2283. },
  2284. useSoftTabs: {initialValue: true},
  2285. tabSize: {
  2286. set: function(tabSize) {
  2287. if (isNaN(tabSize) || this.$tabSize === tabSize) return;
  2288. this.$modified = true;
  2289. this.$rowLengthCache = [];
  2290. this.$tabSize = tabSize;
  2291. this._signal("changeTabSize");
  2292. },
  2293. initialValue: 4,
  2294. handlesSet: true
  2295. },
  2296. overwrite: {
  2297. set: function(val) {this._signal("changeOverwrite");},
  2298. initialValue: false
  2299. },
  2300. newLineMode: {
  2301. set: function(val) {this.doc.setNewLineMode(val)},
  2302. get: function() {return this.doc.getNewLineMode()},
  2303. handlesSet: true
  2304. },
  2305. mode: {
  2306. set: function(val) { this.setMode(val) },
  2307. get: function() { return this.$modeId }
  2308. }
  2309. });
  2310. exports.EditSession = EditSession;
  2311. });