edit_session.js 83 KB

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