123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- /* ***** BEGIN LICENSE BLOCK *****
- * Distributed under the BSD license:
- *
- * Copyright (c) 2010, Ajax.org B.V.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- * * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- * * Neither the name of Ajax.org B.V. nor the
- * names of its contributors may be used to endorse or promote products
- * derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * ***** END LICENSE BLOCK ***** */
- define(function(ace_require, exports, module) {
- "use strict";
- var bidiUtil = ace_require("./lib/bidiutil");
- var lang = ace_require("./lib/lang");
- var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac\u202B]/;
- /**
- * This object is used to ensure Bi-Directional support (for languages with text flowing from right to left, like Arabic or Hebrew)
- * including correct caret positioning, text selection mouse and keyboard arrows functioning
- * @class BidiHandler
- **/
- /**
- * Creates a new `BidiHandler` object
- * @param {EditSession} session The session to use
- *
- * @constructor
- **/
- var BidiHandler = function(session) {
- this.session = session;
- this.bidiMap = {};
- /* current screen row */
- this.currentRow = null;
- this.bidiUtil = bidiUtil;
- /* Arabic/Hebrew character width differs from regular character width */
- this.charWidths = [];
- this.EOL = "\xAC";
- this.showInvisibles = true;
- this.isRtlDir = false;
- this.$isRtl = false;
- this.line = "";
- this.wrapIndent = 0;
- this.EOF = "\xB6";
- this.RLE = "\u202B";
- this.contentWidth = 0;
- this.fontMetrics = null;
- this.rtlLineOffset = 0;
- this.wrapOffset = 0;
- this.isMoveLeftOperation = false;
- this.seenBidi = bidiRE.test(session.getValue());
- };
- (function() {
- /**
- * Returns 'true' if row contains Bidi characters, in such case
- * creates Bidi map to be used in operations related to selection
- * (keyboard arrays, mouse click, select)
- * @param {Number} the screen row to be checked
- * @param {Number} the document row to be checked [optional]
- * @param {Number} the wrapped screen line index [ optional]
- **/
- this.isBidiRow = function(screenRow, docRow, splitIndex) {
- if (!this.seenBidi)
- return false;
- if (screenRow !== this.currentRow) {
- this.currentRow = screenRow;
- this.updateRowLine(docRow, splitIndex);
- this.updateBidiMap();
- }
- return this.bidiMap.bidiLevels;
- };
- this.onChange = function(delta) {
- if (!this.seenBidi) {
- if (delta.action == "insert" && bidiRE.test(delta.lines.join("\n"))) {
- this.seenBidi = true;
- this.currentRow = null;
- }
- }
- else {
- this.currentRow = null;
- }
- };
- this.getDocumentRow = function() {
- var docRow = 0;
- var rowCache = this.session.$screenRowCache;
- if (rowCache.length) {
- var index = this.session.$getRowCacheIndex(rowCache, this.currentRow);
- if (index >= 0)
- docRow = this.session.$docRowCache[index];
- }
- return docRow;
- };
- this.getSplitIndex = function() {
- var splitIndex = 0;
- var rowCache = this.session.$screenRowCache;
- if (rowCache.length) {
- var currentIndex, prevIndex = this.session.$getRowCacheIndex(rowCache, this.currentRow);
- while (this.currentRow - splitIndex > 0) {
- currentIndex = this.session.$getRowCacheIndex(rowCache, this.currentRow - splitIndex - 1);
- if (currentIndex !== prevIndex)
- break;
- prevIndex = currentIndex;
- splitIndex++;
- }
- } else {
- splitIndex = this.currentRow;
- }
- return splitIndex;
- };
- this.updateRowLine = function(docRow, splitIndex) {
- if (docRow === undefined)
- docRow = this.getDocumentRow();
-
- var isLastRow = (docRow === this.session.getLength() - 1),
- endOfLine = isLastRow ? this.EOF : this.EOL;
- this.wrapIndent = 0;
- this.line = this.session.getLine(docRow);
- this.isRtlDir = this.$isRtl || this.line.charAt(0) === this.RLE;
- if (this.session.$useWrapMode) {
- var splits = this.session.$wrapData[docRow];
- if (splits) {
- if (splitIndex === undefined)
- splitIndex = this.getSplitIndex();
- if(splitIndex > 0 && splits.length) {
- this.wrapIndent = splits.indent;
- this.wrapOffset = this.wrapIndent * this.charWidths[bidiUtil.L];
- this.line = (splitIndex < splits.length) ?
- this.line.substring(splits[splitIndex - 1], splits[splitIndex]) :
- this.line.substring(splits[splits.length - 1]);
- } else {
- this.line = this.line.substring(0, splits[splitIndex]);
- }
- }
- if (splitIndex == splits.length)
- this.line += (this.showInvisibles) ? endOfLine : bidiUtil.DOT;
- } else {
- this.line += this.showInvisibles ? endOfLine : bidiUtil.DOT;
- }
-
- /* replace tab and wide characters by commensurate spaces */
- var session = this.session, shift = 0, size;
- this.line = this.line.replace(/\t|[\u1100-\u2029, \u202F-\uFFE6]/g, function(ch, i){
- if (ch === '\t' || session.isFullWidth(ch.charCodeAt(0))) {
- size = (ch === '\t') ? session.getScreenTabSize(i + shift) : 2;
- shift += size - 1;
- return lang.stringRepeat(bidiUtil.DOT, size);
- }
- return ch;
- });
- if (this.isRtlDir) {
- this.fontMetrics.$main.textContent = (this.line.charAt(this.line.length - 1) == bidiUtil.DOT) ? this.line.substr(0, this.line.length - 1) : this.line;
- this.rtlLineOffset = this.contentWidth - this.fontMetrics.$main.getBoundingClientRect().width;
- }
- };
-
- this.updateBidiMap = function() {
- var textCharTypes = [];
- if (bidiUtil.hasBidiCharacters(this.line, textCharTypes) || this.isRtlDir) {
- this.bidiMap = bidiUtil.doBidiReorder(this.line, textCharTypes, this.isRtlDir);
- } else {
- this.bidiMap = {};
- }
- };
- /**
- * Resets stored info related to current screen row
- **/
- this.markAsDirty = function() {
- this.currentRow = null;
- };
- /**
- * Updates array of character widths
- * @param {Object} font metrics
- *
- **/
- this.updateCharacterWidths = function(fontMetrics) {
- if (this.characterWidth === fontMetrics.$characterSize.width)
- return;
- this.fontMetrics = fontMetrics;
- var characterWidth = this.characterWidth = fontMetrics.$characterSize.width;
- var bidiCharWidth = fontMetrics.$measureCharWidth("\u05d4");
- this.charWidths[bidiUtil.L] = this.charWidths[bidiUtil.EN] = this.charWidths[bidiUtil.ON_R] = characterWidth;
- this.charWidths[bidiUtil.R] = this.charWidths[bidiUtil.AN] = bidiCharWidth;
- this.charWidths[bidiUtil.R_H] = bidiCharWidth * 0.45;
- this.charWidths[bidiUtil.B] = this.charWidths[bidiUtil.RLE] = 0;
- this.currentRow = null;
- };
- this.setShowInvisibles = function(showInvisibles) {
- this.showInvisibles = showInvisibles;
- this.currentRow = null;
- };
- this.setEolChar = function(eolChar) {
- this.EOL = eolChar;
- };
- this.setContentWidth = function(width) {
- this.contentWidth = width;
- };
- this.isRtlLine = function(row) {
- if (this.$isRtl) return true;
- if (row != undefined)
- return (this.session.getLine(row).charAt(0) == this.RLE);
- else
- return this.isRtlDir;
- };
- this.setRtlDirection = function(editor, isRtlDir) {
- var cursor = editor.getCursorPosition();
- for (var row = editor.selection.getSelectionAnchor().row; row <= cursor.row; row++) {
- if (!isRtlDir && editor.session.getLine(row).charAt(0) === editor.session.$bidiHandler.RLE)
- editor.session.doc.removeInLine(row, 0, 1);
- else if (isRtlDir && editor.session.getLine(row).charAt(0) !== editor.session.$bidiHandler.RLE)
- editor.session.doc.insert({column: 0, row: row}, editor.session.$bidiHandler.RLE);
- }
- };
- /**
- * Returns offset of character at position defined by column.
- * @param {Number} the screen column position
- *
- * @return {int} horizontal pixel offset of given screen column
- **/
- this.getPosLeft = function(col) {
- col -= this.wrapIndent;
- var leftBoundary = (this.line.charAt(0) === this.RLE) ? 1 : 0;
- var logicalIdx = (col > leftBoundary) ? (this.session.getOverwrite() ? col : col - 1) : leftBoundary;
- var visualIdx = bidiUtil.getVisualFromLogicalIdx(logicalIdx, this.bidiMap),
- levels = this.bidiMap.bidiLevels, left = 0;
- if (!this.session.getOverwrite() && col <= leftBoundary && levels[visualIdx] % 2 !== 0)
- visualIdx++;
-
- for (var i = 0; i < visualIdx; i++) {
- left += this.charWidths[levels[i]];
- }
- if (!this.session.getOverwrite() && (col > leftBoundary) && (levels[visualIdx] % 2 === 0))
- left += this.charWidths[levels[visualIdx]];
- if (this.wrapIndent)
- left += this.isRtlDir ? (-1 * this.wrapOffset) : this.wrapOffset;
- if (this.isRtlDir)
- left += this.rtlLineOffset;
- return left;
- };
- /**
- * Returns 'selections' - array of objects defining set of selection rectangles
- * @param {Number} the start column position
- * @param {Number} the end column position
- *
- * @return {Array of Objects} Each object contains 'left' and 'width' values defining selection rectangle.
- **/
- this.getSelections = function(startCol, endCol) {
- var map = this.bidiMap, levels = map.bidiLevels, level, selections = [], offset = 0,
- selColMin = Math.min(startCol, endCol) - this.wrapIndent, selColMax = Math.max(startCol, endCol) - this.wrapIndent,
- isSelected = false, isSelectedPrev = false, selectionStart = 0;
-
- if (this.wrapIndent)
- offset += this.isRtlDir ? (-1 * this.wrapOffset) : this.wrapOffset;
- for (var logIdx, visIdx = 0; visIdx < levels.length; visIdx++) {
- logIdx = map.logicalFromVisual[visIdx];
- level = levels[visIdx];
- isSelected = (logIdx >= selColMin) && (logIdx < selColMax);
- if (isSelected && !isSelectedPrev) {
- selectionStart = offset;
- } else if (!isSelected && isSelectedPrev) {
- selections.push({left: selectionStart, width: offset - selectionStart});
- }
- offset += this.charWidths[level];
- isSelectedPrev = isSelected;
- }
- if (isSelected && (visIdx === levels.length)) {
- selections.push({left: selectionStart, width: offset - selectionStart});
- }
- if(this.isRtlDir) {
- for (var i = 0; i < selections.length; i++) {
- selections[i].left += this.rtlLineOffset;
- }
- }
- return selections;
- };
- /**
- * Converts character coordinates on the screen to respective document column number
- * @param {int} character horizontal offset
- *
- * @return {Number} screen column number corresponding to given pixel offset
- **/
- this.offsetToCol = function(posX) {
- if(this.isRtlDir)
- posX -= this.rtlLineOffset;
- var logicalIdx = 0, posX = Math.max(posX, 0),
- offset = 0, visualIdx = 0, levels = this.bidiMap.bidiLevels,
- charWidth = this.charWidths[levels[visualIdx]];
- if (this.wrapIndent)
- posX -= this.isRtlDir ? (-1 * this.wrapOffset) : this.wrapOffset;
-
- while(posX > offset + charWidth/2) {
- offset += charWidth;
- if(visualIdx === levels.length - 1) {
- /* quit when we on the right of the last character, flag this by charWidth = 0 */
- charWidth = 0;
- break;
- }
- charWidth = this.charWidths[levels[++visualIdx]];
- }
-
- if (visualIdx > 0 && (levels[visualIdx - 1] % 2 !== 0) && (levels[visualIdx] % 2 === 0)){
- /* Bidi character on the left and None Bidi character on the right */
- if(posX < offset)
- visualIdx--;
- logicalIdx = this.bidiMap.logicalFromVisual[visualIdx];
- } else if (visualIdx > 0 && (levels[visualIdx - 1] % 2 === 0) && (levels[visualIdx] % 2 !== 0)){
- /* None Bidi character on the left and Bidi character on the right */
- logicalIdx = 1 + ((posX > offset) ? this.bidiMap.logicalFromVisual[visualIdx]
- : this.bidiMap.logicalFromVisual[visualIdx - 1]);
- } else if ((this.isRtlDir && visualIdx === levels.length - 1 && charWidth === 0 && (levels[visualIdx - 1] % 2 === 0))
- || (!this.isRtlDir && visualIdx === 0 && (levels[visualIdx] % 2 !== 0))){
- /* To the right of last character, which is None Bidi, in RTL direction or */
- /* to the left of first Bidi character, in LTR direction */
- logicalIdx = 1 + this.bidiMap.logicalFromVisual[visualIdx];
- } else {
- /* Tweak visual position when Bidi character on the left in order to map it to corresponding logical position */
- if (visualIdx > 0 && (levels[visualIdx - 1] % 2 !== 0) && charWidth !== 0)
- visualIdx--;
- /* Regular case */
- logicalIdx = this.bidiMap.logicalFromVisual[visualIdx];
- }
- if (logicalIdx === 0 && this.isRtlDir)
- logicalIdx++;
- return (logicalIdx + this.wrapIndent);
- };
- }).call(BidiHandler.prototype);
- exports.BidiHandler = BidiHandler;
- });
|