bidihandler.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  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(ace_require, exports, module) {
  31. "use strict";
  32. var bidiUtil = ace_require("./lib/bidiutil");
  33. var lang = ace_require("./lib/lang");
  34. var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac\u202B]/;
  35. /**
  36. * This object is used to ensure Bi-Directional support (for languages with text flowing from right to left, like Arabic or Hebrew)
  37. * including correct caret positioning, text selection mouse and keyboard arrows functioning
  38. * @class BidiHandler
  39. **/
  40. /**
  41. * Creates a new `BidiHandler` object
  42. * @param {EditSession} session The session to use
  43. *
  44. * @constructor
  45. **/
  46. var BidiHandler = function(session) {
  47. this.session = session;
  48. this.bidiMap = {};
  49. /* current screen row */
  50. this.currentRow = null;
  51. this.bidiUtil = bidiUtil;
  52. /* Arabic/Hebrew character width differs from regular character width */
  53. this.charWidths = [];
  54. this.EOL = "\xAC";
  55. this.showInvisibles = true;
  56. this.isRtlDir = false;
  57. this.$isRtl = false;
  58. this.line = "";
  59. this.wrapIndent = 0;
  60. this.EOF = "\xB6";
  61. this.RLE = "\u202B";
  62. this.contentWidth = 0;
  63. this.fontMetrics = null;
  64. this.rtlLineOffset = 0;
  65. this.wrapOffset = 0;
  66. this.isMoveLeftOperation = false;
  67. this.seenBidi = bidiRE.test(session.getValue());
  68. };
  69. (function() {
  70. /**
  71. * Returns 'true' if row contains Bidi characters, in such case
  72. * creates Bidi map to be used in operations related to selection
  73. * (keyboard arrays, mouse click, select)
  74. * @param {Number} the screen row to be checked
  75. * @param {Number} the document row to be checked [optional]
  76. * @param {Number} the wrapped screen line index [ optional]
  77. **/
  78. this.isBidiRow = function(screenRow, docRow, splitIndex) {
  79. if (!this.seenBidi)
  80. return false;
  81. if (screenRow !== this.currentRow) {
  82. this.currentRow = screenRow;
  83. this.updateRowLine(docRow, splitIndex);
  84. this.updateBidiMap();
  85. }
  86. return this.bidiMap.bidiLevels;
  87. };
  88. this.onChange = function(delta) {
  89. if (!this.seenBidi) {
  90. if (delta.action == "insert" && bidiRE.test(delta.lines.join("\n"))) {
  91. this.seenBidi = true;
  92. this.currentRow = null;
  93. }
  94. }
  95. else {
  96. this.currentRow = null;
  97. }
  98. };
  99. this.getDocumentRow = function() {
  100. var docRow = 0;
  101. var rowCache = this.session.$screenRowCache;
  102. if (rowCache.length) {
  103. var index = this.session.$getRowCacheIndex(rowCache, this.currentRow);
  104. if (index >= 0)
  105. docRow = this.session.$docRowCache[index];
  106. }
  107. return docRow;
  108. };
  109. this.getSplitIndex = function() {
  110. var splitIndex = 0;
  111. var rowCache = this.session.$screenRowCache;
  112. if (rowCache.length) {
  113. var currentIndex, prevIndex = this.session.$getRowCacheIndex(rowCache, this.currentRow);
  114. while (this.currentRow - splitIndex > 0) {
  115. currentIndex = this.session.$getRowCacheIndex(rowCache, this.currentRow - splitIndex - 1);
  116. if (currentIndex !== prevIndex)
  117. break;
  118. prevIndex = currentIndex;
  119. splitIndex++;
  120. }
  121. } else {
  122. splitIndex = this.currentRow;
  123. }
  124. return splitIndex;
  125. };
  126. this.updateRowLine = function(docRow, splitIndex) {
  127. if (docRow === undefined)
  128. docRow = this.getDocumentRow();
  129. var isLastRow = (docRow === this.session.getLength() - 1),
  130. endOfLine = isLastRow ? this.EOF : this.EOL;
  131. this.wrapIndent = 0;
  132. this.line = this.session.getLine(docRow);
  133. this.isRtlDir = this.$isRtl || this.line.charAt(0) === this.RLE;
  134. if (this.session.$useWrapMode) {
  135. var splits = this.session.$wrapData[docRow];
  136. if (splits) {
  137. if (splitIndex === undefined)
  138. splitIndex = this.getSplitIndex();
  139. if(splitIndex > 0 && splits.length) {
  140. this.wrapIndent = splits.indent;
  141. this.wrapOffset = this.wrapIndent * this.charWidths[bidiUtil.L];
  142. this.line = (splitIndex < splits.length) ?
  143. this.line.substring(splits[splitIndex - 1], splits[splitIndex]) :
  144. this.line.substring(splits[splits.length - 1]);
  145. } else {
  146. this.line = this.line.substring(0, splits[splitIndex]);
  147. }
  148. }
  149. if (splitIndex == splits.length)
  150. this.line += (this.showInvisibles) ? endOfLine : bidiUtil.DOT;
  151. } else {
  152. this.line += this.showInvisibles ? endOfLine : bidiUtil.DOT;
  153. }
  154. /* replace tab and wide characters by commensurate spaces */
  155. var session = this.session, shift = 0, size;
  156. this.line = this.line.replace(/\t|[\u1100-\u2029, \u202F-\uFFE6]/g, function(ch, i){
  157. if (ch === '\t' || session.isFullWidth(ch.charCodeAt(0))) {
  158. size = (ch === '\t') ? session.getScreenTabSize(i + shift) : 2;
  159. shift += size - 1;
  160. return lang.stringRepeat(bidiUtil.DOT, size);
  161. }
  162. return ch;
  163. });
  164. if (this.isRtlDir) {
  165. this.fontMetrics.$main.textContent = (this.line.charAt(this.line.length - 1) == bidiUtil.DOT) ? this.line.substr(0, this.line.length - 1) : this.line;
  166. this.rtlLineOffset = this.contentWidth - this.fontMetrics.$main.getBoundingClientRect().width;
  167. }
  168. };
  169. this.updateBidiMap = function() {
  170. var textCharTypes = [];
  171. if (bidiUtil.hasBidiCharacters(this.line, textCharTypes) || this.isRtlDir) {
  172. this.bidiMap = bidiUtil.doBidiReorder(this.line, textCharTypes, this.isRtlDir);
  173. } else {
  174. this.bidiMap = {};
  175. }
  176. };
  177. /**
  178. * Resets stored info related to current screen row
  179. **/
  180. this.markAsDirty = function() {
  181. this.currentRow = null;
  182. };
  183. /**
  184. * Updates array of character widths
  185. * @param {Object} font metrics
  186. *
  187. **/
  188. this.updateCharacterWidths = function(fontMetrics) {
  189. if (this.characterWidth === fontMetrics.$characterSize.width)
  190. return;
  191. this.fontMetrics = fontMetrics;
  192. var characterWidth = this.characterWidth = fontMetrics.$characterSize.width;
  193. var bidiCharWidth = fontMetrics.$measureCharWidth("\u05d4");
  194. this.charWidths[bidiUtil.L] = this.charWidths[bidiUtil.EN] = this.charWidths[bidiUtil.ON_R] = characterWidth;
  195. this.charWidths[bidiUtil.R] = this.charWidths[bidiUtil.AN] = bidiCharWidth;
  196. this.charWidths[bidiUtil.R_H] = bidiCharWidth * 0.45;
  197. this.charWidths[bidiUtil.B] = this.charWidths[bidiUtil.RLE] = 0;
  198. this.currentRow = null;
  199. };
  200. this.setShowInvisibles = function(showInvisibles) {
  201. this.showInvisibles = showInvisibles;
  202. this.currentRow = null;
  203. };
  204. this.setEolChar = function(eolChar) {
  205. this.EOL = eolChar;
  206. };
  207. this.setContentWidth = function(width) {
  208. this.contentWidth = width;
  209. };
  210. this.isRtlLine = function(row) {
  211. if (this.$isRtl) return true;
  212. if (row != undefined)
  213. return (this.session.getLine(row).charAt(0) == this.RLE);
  214. else
  215. return this.isRtlDir;
  216. };
  217. this.setRtlDirection = function(editor, isRtlDir) {
  218. var cursor = editor.getCursorPosition();
  219. for (var row = editor.selection.getSelectionAnchor().row; row <= cursor.row; row++) {
  220. if (!isRtlDir && editor.session.getLine(row).charAt(0) === editor.session.$bidiHandler.RLE)
  221. editor.session.doc.removeInLine(row, 0, 1);
  222. else if (isRtlDir && editor.session.getLine(row).charAt(0) !== editor.session.$bidiHandler.RLE)
  223. editor.session.doc.insert({column: 0, row: row}, editor.session.$bidiHandler.RLE);
  224. }
  225. };
  226. /**
  227. * Returns offset of character at position defined by column.
  228. * @param {Number} the screen column position
  229. *
  230. * @return {int} horizontal pixel offset of given screen column
  231. **/
  232. this.getPosLeft = function(col) {
  233. col -= this.wrapIndent;
  234. var leftBoundary = (this.line.charAt(0) === this.RLE) ? 1 : 0;
  235. var logicalIdx = (col > leftBoundary) ? (this.session.getOverwrite() ? col : col - 1) : leftBoundary;
  236. var visualIdx = bidiUtil.getVisualFromLogicalIdx(logicalIdx, this.bidiMap),
  237. levels = this.bidiMap.bidiLevels, left = 0;
  238. if (!this.session.getOverwrite() && col <= leftBoundary && levels[visualIdx] % 2 !== 0)
  239. visualIdx++;
  240. for (var i = 0; i < visualIdx; i++) {
  241. left += this.charWidths[levels[i]];
  242. }
  243. if (!this.session.getOverwrite() && (col > leftBoundary) && (levels[visualIdx] % 2 === 0))
  244. left += this.charWidths[levels[visualIdx]];
  245. if (this.wrapIndent)
  246. left += this.isRtlDir ? (-1 * this.wrapOffset) : this.wrapOffset;
  247. if (this.isRtlDir)
  248. left += this.rtlLineOffset;
  249. return left;
  250. };
  251. /**
  252. * Returns 'selections' - array of objects defining set of selection rectangles
  253. * @param {Number} the start column position
  254. * @param {Number} the end column position
  255. *
  256. * @return {Array of Objects} Each object contains 'left' and 'width' values defining selection rectangle.
  257. **/
  258. this.getSelections = function(startCol, endCol) {
  259. var map = this.bidiMap, levels = map.bidiLevels, level, selections = [], offset = 0,
  260. selColMin = Math.min(startCol, endCol) - this.wrapIndent, selColMax = Math.max(startCol, endCol) - this.wrapIndent,
  261. isSelected = false, isSelectedPrev = false, selectionStart = 0;
  262. if (this.wrapIndent)
  263. offset += this.isRtlDir ? (-1 * this.wrapOffset) : this.wrapOffset;
  264. for (var logIdx, visIdx = 0; visIdx < levels.length; visIdx++) {
  265. logIdx = map.logicalFromVisual[visIdx];
  266. level = levels[visIdx];
  267. isSelected = (logIdx >= selColMin) && (logIdx < selColMax);
  268. if (isSelected && !isSelectedPrev) {
  269. selectionStart = offset;
  270. } else if (!isSelected && isSelectedPrev) {
  271. selections.push({left: selectionStart, width: offset - selectionStart});
  272. }
  273. offset += this.charWidths[level];
  274. isSelectedPrev = isSelected;
  275. }
  276. if (isSelected && (visIdx === levels.length)) {
  277. selections.push({left: selectionStart, width: offset - selectionStart});
  278. }
  279. if(this.isRtlDir) {
  280. for (var i = 0; i < selections.length; i++) {
  281. selections[i].left += this.rtlLineOffset;
  282. }
  283. }
  284. return selections;
  285. };
  286. /**
  287. * Converts character coordinates on the screen to respective document column number
  288. * @param {int} character horizontal offset
  289. *
  290. * @return {Number} screen column number corresponding to given pixel offset
  291. **/
  292. this.offsetToCol = function(posX) {
  293. if(this.isRtlDir)
  294. posX -= this.rtlLineOffset;
  295. var logicalIdx = 0, posX = Math.max(posX, 0),
  296. offset = 0, visualIdx = 0, levels = this.bidiMap.bidiLevels,
  297. charWidth = this.charWidths[levels[visualIdx]];
  298. if (this.wrapIndent)
  299. posX -= this.isRtlDir ? (-1 * this.wrapOffset) : this.wrapOffset;
  300. while(posX > offset + charWidth/2) {
  301. offset += charWidth;
  302. if(visualIdx === levels.length - 1) {
  303. /* quit when we on the right of the last character, flag this by charWidth = 0 */
  304. charWidth = 0;
  305. break;
  306. }
  307. charWidth = this.charWidths[levels[++visualIdx]];
  308. }
  309. if (visualIdx > 0 && (levels[visualIdx - 1] % 2 !== 0) && (levels[visualIdx] % 2 === 0)){
  310. /* Bidi character on the left and None Bidi character on the right */
  311. if(posX < offset)
  312. visualIdx--;
  313. logicalIdx = this.bidiMap.logicalFromVisual[visualIdx];
  314. } else if (visualIdx > 0 && (levels[visualIdx - 1] % 2 === 0) && (levels[visualIdx] % 2 !== 0)){
  315. /* None Bidi character on the left and Bidi character on the right */
  316. logicalIdx = 1 + ((posX > offset) ? this.bidiMap.logicalFromVisual[visualIdx]
  317. : this.bidiMap.logicalFromVisual[visualIdx - 1]);
  318. } else if ((this.isRtlDir && visualIdx === levels.length - 1 && charWidth === 0 && (levels[visualIdx - 1] % 2 === 0))
  319. || (!this.isRtlDir && visualIdx === 0 && (levels[visualIdx] % 2 !== 0))){
  320. /* To the right of last character, which is None Bidi, in RTL direction or */
  321. /* to the left of first Bidi character, in LTR direction */
  322. logicalIdx = 1 + this.bidiMap.logicalFromVisual[visualIdx];
  323. } else {
  324. /* Tweak visual position when Bidi character on the left in order to map it to corresponding logical position */
  325. if (visualIdx > 0 && (levels[visualIdx - 1] % 2 !== 0) && charWidth !== 0)
  326. visualIdx--;
  327. /* Regular case */
  328. logicalIdx = this.bidiMap.logicalFromVisual[visualIdx];
  329. }
  330. if (logicalIdx === 0 && this.isRtlDir)
  331. logicalIdx++;
  332. return (logicalIdx + this.wrapIndent);
  333. };
  334. }).call(BidiHandler.prototype);
  335. exports.BidiHandler = BidiHandler;
  336. });