diff --git a/bundles/org.eclipse.orion.client.core/web/orion/bidiUtils.js b/bundles/org.eclipse.orion.client.core/web/orion/bidiUtils.js index 2583928041..5cbeff170a 100644 --- a/bundles/org.eclipse.orion.client.core/web/orion/bidiUtils.js +++ b/bundles/org.eclipse.orion.client.core/web/orion/bidiUtils.js @@ -12,12 +12,6 @@ define ([ ], function(util) { /* BDL */ - var bidiEnabledStorage = "/orion/preferences/bidi/bidiEnabled"; //$NON-NLS-0$ - var bidiLayoutStorage = "/orion/preferences/bidi/bidiLayout"; //$NON-NLS-0$ - var LRE = "\u202A"; //$NON-NLS-0$ - var PDF = "\u202C"; //$NON-NLS-0$ - var RLE = "\u202B"; //$NON-NLS-0$ - function setBrowserLangDirection() { var lang; @@ -29,7 +23,7 @@ function(util) { /* BDL */ } var isBidi = lang && "ar iw he".indexOf(lang.substring(0, 2)) !== - 1; - if (isBidi && isBidiEnabled()) { + if (isBidi) { var htmlElement = document.getElementsByTagName("html")[0]; if (htmlElement){ //should be always true htmlElement.setAttribute ("dir", "rtl"); @@ -39,6 +33,12 @@ function(util) { /* BDL */ setBrowserLangDirection(); + var bidiEnabledStorage = "/orion/preferences/bidi/bidiEnabled"; //$NON-NLS-0$ + var bidiLayoutStorage = "/orion/preferences/bidi/bidiLayout"; //$NON-NLS-0$ + var LRE = "\u202A"; //$NON-NLS-0$ + var PDF = "\u202C"; //$NON-NLS-0$ + var RLE = "\u202B"; //$NON-NLS-0$ + var bidiLayout = getBidiLayout(); /** @@ -146,7 +146,16 @@ function(util) { /* BDL */ } } - function enforceTextDir(range) { + function createNewStyle ( oldStyle, textDir ) { + var newStyle = oldStyle; + if (typeof newStyle.attributes === "undefined") { + newStyle.attributes = {}; + } + newStyle.attributes.dir = textDir; + return newStyle; + } + + function enforceTextDir( range, textDir ) { var comments = [{name:"comment block"}, {name:"comment line double-slash"}, {name:"comment block documentation"}, @@ -163,13 +172,17 @@ function(util) { /* BDL */ var newStyle = style; if (typeof newStyle.attributes === "undefined") { newStyle.attributes = {}; - } - newStyle.attributes.dir = getTextDirection(text); - range.style = newStyle; + } + range.style = createNewStyle( style, getTextDirection(text) ); return range; } } } + else if (style && textDir && textDir.length > 0) { + range.style = createNewStyle( style, textDir ); + return range; + } + return range; } diff --git a/bundles/org.eclipse.orion.client.core/web/orion/util.js b/bundles/org.eclipse.orion.client.core/web/orion/util.js index a186dd3253..f2e2b69b6f 100644 --- a/bundles/org.eclipse.orion.client.core/web/orion/util.js +++ b/bundles/org.eclipse.orion.client.core/web/orion/util.js @@ -30,6 +30,8 @@ define(function() { var isTouch = typeof document !== "undefined" && "ontouchstart" in document.createElement("input"); //$NON-NLS-1$ //$NON-NLS-0$ var platformDelimiter = isWindows ? "\r\n" : "\n"; //$NON-NLS-1$ //$NON-NLS-0$ + var LtrMarker = "\u202A\u202A\u202A\u202A\u202A"; //$NON-NLS-0$ + var RtlMarker = "\u202B\u202B\u202B\u202B\u202B"; //$NON-NLS-0$ function formatMessage(msg) { var args = arguments; @@ -85,6 +87,9 @@ define(function() { /** Capabilities */ isTouch: isTouch, - platformDelimiter: platformDelimiter + platformDelimiter: platformDelimiter, + LtrMarker: LtrMarker, + RtlMarker: RtlMarker + }; }); \ No newline at end of file diff --git a/bundles/org.eclipse.orion.client.editor/web/orion/editor/keyModes.js b/bundles/org.eclipse.orion.client.editor/web/orion/editor/keyModes.js index 5f2589b255..43a54f4d5d 100644 --- a/bundles/org.eclipse.orion.client.editor/web/orion/editor/keyModes.js +++ b/bundles/org.eclipse.orion.client.editor/web/orion/editor/keyModes.js @@ -231,6 +231,8 @@ define("orion/editor/keyModes", [ //$NON-NLS-0$ bindings.push({actionID: "escape", keyBinding: new KeyBinding(27), predefined: true}); //$NON-NLS-0$ bindings.push({actionID: "selectAll", keyBinding: new KeyBinding('a', true), predefined: true}); //$NON-NLS-1$ //$NON-NLS-0$ bindings.push({actionID: "toggleTabMode", keyBinding: new KeyBinding('m', true), predefined: true}); //$NON-NLS-1$ //$NON-NLS-0$ + bindings.push({actionID: "dirLTR", keyBinding: new KeyBinding(36, null, true, true), predefined: true}); //$NON-NLS-0$ + bindings.push({actionID: "dirRTL", keyBinding: new KeyBinding(35, null, true, true), predefined: true}); //$NON-NLS-0$ if (util.isMac) { bindings.push({actionID: "deleteNext", keyBinding: new KeyBinding(46, null, true), predefined: true}); //$NON-NLS-0$ bindings.push({actionID: "deleteWordPrevious", keyBinding: new KeyBinding(8, null, null, true), predefined: true}); //$NON-NLS-0$ diff --git a/bundles/org.eclipse.orion.client.editor/web/orion/editor/nls/root/messages.js b/bundles/org.eclipse.orion.client.editor/web/orion/editor/nls/root/messages.js index f21c3cd5b8..87fdfde9ab 100644 --- a/bundles/org.eclipse.orion.client.editor/web/orion/editor/nls/root/messages.js +++ b/bundles/org.eclipse.orion.client.editor/web/orion/editor/nls/root/messages.js @@ -86,6 +86,8 @@ define({//Default message bundle "toggleWrapMode": "Toggle Wrap Mode", //$NON-NLS-1$ //$NON-NLS-0$ "toggleTabMode": "Toggle Tab Mode", //$NON-NLS-1$ //$NON-NLS-0$ "toggleOverwriteMode": "Toggle Overwrite Mode", //$NON-NLS-1$ //$NON-NLS-0$ + "dirLTR": "Set Left to Right Text Direction", //$NON-NLS-1$ //$NON-NLS-0$ + "dirRTL": "Set Right to Left Text Direction", //$NON-NLS-1$ //$NON-NLS-0$ "committerOnTime": "${0} on ${1}", //$NON-NLS-1$ //$NON-NLS-0$ diff --git a/bundles/org.eclipse.orion.client.editor/web/orion/editor/projectionTextModel.js b/bundles/org.eclipse.orion.client.editor/web/orion/editor/projectionTextModel.js index b00af8dc5b..2a5e8ccd87 100644 --- a/bundles/org.eclipse.orion.client.editor/web/orion/editor/projectionTextModel.js +++ b/bundles/org.eclipse.orion.client.editor/web/orion/editor/projectionTextModel.js @@ -339,6 +339,18 @@ define("orion/editor/projectionTextModel", ['orion/editor/textModel', 'orion/edi getLineDelimiter: function() { return this._model.getLineDelimiter(); }, + /** + * @see orion.editor.TextModel#getLineTextDir + */ + getLineTextDir: function(lineIndex) { + return this._model.getLineTextDir(lineIndex); + }, + /** + * @see orion.editor.TextModel#getTextForSave + */ + getTextForSave: function() { + return this._model.getTextForSave(); + }, /** * @see orion.editor.TextModel#getLineEnd */ diff --git a/bundles/org.eclipse.orion.client.editor/web/orion/editor/textModel.js b/bundles/org.eclipse.orion.client.editor/web/orion/editor/textModel.js index a10a811913..751d52c30f 100644 --- a/bundles/org.eclipse.orion.client.editor/web/orion/editor/textModel.js +++ b/bundles/org.eclipse.orion.client.editor/web/orion/editor/textModel.js @@ -38,6 +38,7 @@ define("orion/editor/textModel", ['orion/editor/eventTarget', 'orion/regex', 'or this._lastLineIndex = -1; this._text = [""]; this._lineOffsets = [0]; + this._textDirs = [""]; this.setText(text); this.setLineDelimiter(lineDelimiter); } @@ -370,6 +371,23 @@ define("orion/editor/textModel", ['orion/editor/eventTarget', 'orion/regex', 'or } return this._lineOffsets[lineIndex]; }, + /** + * Returns the text direction of the given line. + *

+ * The valid indices are 0 to line count exclusive. Returns "" + * if the index is out of range. + *

+ * + * @param {Number} lineIndex the zero based index of the line. + * @return {String} the text direction of the line"" if out of range. + * + */ + getLineTextDir: function(lineIndex) { + if (!(0 <= lineIndex && lineIndex < this.getLineCount())) { + return ""; + } + return this._textDirs[lineIndex]; + }, /** * Returns the text for the given range. *

@@ -416,6 +434,23 @@ define("orion/editor/textModel", ['orion/editor/eventTarget', 'orion/regex', 'or var afterText = this._text[lastChunk].substring(0, end - lastOffset); return beforeText + this._text.slice(firstChunk+1, lastChunk).join("") + afterText; }, + /** + * Returns the whole text for save. We keep the text direction of each line by adding the ltr/rtl marker. + * + */ + getTextForSave: function() { + var text = ""; + for (var i = 0; i @@ -508,6 +543,8 @@ define("orion/editor/textModel", ['orion/editor/eventTarget', 'orion/regex', 'or if (start === undefined) { start = 0; } if (end === undefined) { end = this.getCharCount(); } if (start === end && text === "") { return; } + var ltrMarker = util.LtrMarker; + var rtlMarker = util.RtlMarker; var startLine = this.getLineAtOffset(start); var endLine = this.getLineAtOffset(end); var eventStart = start; @@ -517,12 +554,21 @@ define("orion/editor/textModel", ['orion/editor/eventTarget', 'orion/regex', 'or var addedLineCount = 0; var lineCount = this.getLineCount(); - var cr = 0, lf = 0, index = 0; + var cr = 0, lf = 0, index = 0, shift = 0; var newLineOffsets = []; - while (true) { + var newTextDirs = []; + var origText = text; + + + while (true) { if (cr !== -1 && cr <= index) { cr = text.indexOf("\r", index); } //$NON-NLS-0$ if (lf !== -1 && lf <= index) { lf = text.indexOf("\n", index); } //$NON-NLS-0$ if (lf === -1 && cr === -1) { break; } + if (text.substr(index, ltrMarker.length) == ltrMarker) { + shift += ltrMarker.length; + } else if (text.substr(index, rtlMarker.length) == rtlMarker) { + shift += rtlMarker.length; + } if (cr !== -1 && lf !== -1) { if (cr + 1 === lf) { index = lf + 1; @@ -534,8 +580,20 @@ define("orion/editor/textModel", ['orion/editor/eventTarget', 'orion/regex', 'or } else { index = lf + 1; } - newLineOffsets.push(start + index); + newLineOffsets.push(start + index - shift); addedLineCount++; + if (text.substr(index, ltrMarker.length) == ltrMarker) { + newTextDirs.push("ltr"); //$NON-NLS-0$ + } else if (text.substr(index, rtlMarker.length) == rtlMarker) { + newTextDirs.push("rtl"); //$NON-NLS-0$ + } else { + newTextDirs.push(""); //$NON-NLS-0$ + } + } + if (text !== ltrMarker && text !== rtlMarker) { + var re1 = new RegExp(ltrMarker, "g"), re2 = new RegExp(rtlMarker, "g"); + text = text.replace(re1, "").replace(re2, ""); + addedCharCount = text.length; } var modelChangingEvent = { @@ -582,16 +640,26 @@ define("orion/editor/textModel", ['orion/editor/eventTarget', 'orion/regex', 'or if (newLineOffsets.length < limit) { args = [startLine + 1, removedLineCount].concat(newLineOffsets); Array.prototype.splice.apply(this._lineOffsets, args); + args = [startLine + 1, removedLineCount].concat(newTextDirs); + Array.prototype.splice.apply(this._textDirs, args); } else { index = startLine + 1; this._lineOffsets.splice(index, removedLineCount); for (var k = 0; k < newLineOffsets.length; k += limit) { args = [index, 0].concat(newLineOffsets.slice(k, Math.min(newLineOffsets.length, k + limit))); Array.prototype.splice.apply(this._lineOffsets, args); + args = [index, 0].concat(newTextDirs.slice(k, Math.min(newTextDirs.length, k + limit))); + Array.prototype.splice.apply(this._textDirs, args); index += limit; } } + if (origText.indexOf(ltrMarker) == 0) { + this._textDirs[startLine] = "ltr"; //$NON-NLS-0$ + } else if (origText.indexOf(rtlMarker) == 0) { + this._textDirs[startLine] = "rtl"; //$NON-NLS-0$ + } + var offset = 0, chunk = 0, length; while (chunk end) { return; } if (ranges) { for (var i = 0; i < ranges.length; i++) { @@ -805,7 +806,7 @@ define("orion/editor/textView", [ //$NON-NLS-1$ styleStart = Math.max(start, styleStart); styleEnd = Math.min(end, styleEnd); if (start < styleStart) { - this._createRange(text, start, styleStart, null, data); + this._createRange(text, start, styleStart, null, textDir, data); } if (!range.style || !range.style.unmergeable) { while (i + 1 < ranges.length && ranges[i + 1].start - lineStart === styleEnd && compare(range.style, ranges[i + 1].style)) { @@ -814,16 +815,16 @@ define("orion/editor/textView", [ //$NON-NLS-1$ i++; } } - this._createRange(text, styleStart, styleEnd, range.style, data); + this._createRange(text, styleStart, styleEnd, range.style, textDir, data); start = styleEnd; } } } if (start < end) { - this._createRange(text, start, end, null, data); + this._createRange(text, start, end, null, textDir, data); } }, - _createRange: function(text, start, end, style, data) { + _createRange: function(text, start, end, style, textDir, data) { if (start > end) { return; } var tabSize = this.view._customTabSize, range; var bidiStyle = {tagName:"span", bidi:true, style:{unicodeBidi:"embed", direction:"ltr"}}; @@ -833,9 +834,9 @@ define("orion/editor/textView", [ //$NON-NLS-1$ while (tabIndex !== -1 && tabIndex < end) { if (start < tabIndex) { range = {text: text.substring(start, tabIndex), style: style}; - range = bidiUtils.enforceTextDir(range); + range = bidiUtils.enforceTextDir(range, textDir); data.ranges.push(range); - if (bidiUtils.isBidiEnabled()) { + if (bidiUtils.isBidiEnabled() && !this.view._isMarkdown) { data.ranges.push(bidiRange); } data.tabOffset += range.text.length; @@ -849,7 +850,7 @@ define("orion/editor/textView", [ //$NON-NLS-1$ } range = {text: spaces, style: style, ignoreChars: spacesCount - 1}; data.ranges.push(range); - if (bidiUtils.isBidiEnabled()) { + if (bidiUtils.isBidiEnabled() && !this.view._isMarkdown) { data.ranges.push(bidiRange); } data.tabOffset += range.text.length; @@ -863,9 +864,9 @@ define("orion/editor/textView", [ //$NON-NLS-1$ } if (start <= end) { range = {text: text.substring(start, end), style: style}; - range = bidiUtils.enforceTextDir(range); + range = bidiUtils.enforceTextDir(range, textDir); data.ranges.push(range); - if (bidiUtils.isBidiEnabled()) { + if (bidiUtils.isBidiEnabled() && !this.view._isMarkdown) { data.ranges.push(bidiRange); } data.tabOffset += range.text.length; @@ -5338,6 +5339,27 @@ define("orion/editor/textView", [ //$NON-NLS-1$ this.setOptions({wrapMode: !this.getOptions("wrapMode")}); //$NON-NLS-1$ return true; }, + _doSetDirection: function (args) { + if (!this._isMarkdown) { return; } + var model = this._model; + var selections = this._getSelections(); + var marker = ( args.type === "ltr" ? util.LtrMarker : util.RtlMarker ); + var that = this; + selections.forEach(function(current) { + var startLineIndex = model.getLineAtOffset(current.start); + var endLineIndex = model.getLineAtOffset(current.end); + for (var i = startLineIndex; i<=endLineIndex; i++) { + var startLine = model.getLineStart(i); + var selection = [current]; + selection[0].start = selection[0].end = startLine; + that._modifyContent({text: marker, selection: selection, _ignoreDOMSelection: true}, true); + selection[0].start = startLine; + selection[0].end = startLine + marker.length; + that._modifyContent({text: "", selection: selection, _ignoreDOMSelection: true}, true); //$NON-NLS-1$ + } + }); + return true; + }, /************************************ Internals ******************************************/ _autoScroll: function () { @@ -5661,7 +5683,9 @@ define("orion/editor/textView", [ //$NON-NLS-1$ "toggleOverwriteMode": {defaultHandler: function(data) {return that._doOverwriteMode(merge(data,{}));}, actionDescription: {name: messages.toggleOverwriteMode}}, //$NON-NLS-1$ "toggleTabMode": {defaultHandler: function(data) {return that._doTabMode(merge(data,{}));}, actionDescription: {name: messages.toggleTabMode}}, //$NON-NLS-1$ - "toggleWrapMode": {defaultHandler: function(data) {return that._doWrapMode(merge(data,{}));}, actionDescription: {name: messages.toggleWrapMode}} //$NON-NLS-1$ + "toggleWrapMode": {defaultHandler: function(data) {return that._doWrapMode(merge(data,{}));}, actionDescription: {name: messages.toggleWrapMode}}, //$NON-NLS-1$ + "dirLTR": {defaultHandler: function(data) {return that._doSetDirection(merge(data,{type: "ltr"}));}, actionDescription: {name: messages.dirLTR}}, //$NON-NLS-1$ + "dirRTL": {defaultHandler: function(data) {return that._doSetDirection(merge(data,{type: "rtl"}));}, actionDescription: {name: messages.dirRTL}}, //$NON-NLS-1$ }; }, _createRulerParent: function(doc, className) { @@ -6496,6 +6520,7 @@ define("orion/editor/textView", [ //$NON-NLS-1$ this._dragOffset = -1; this._isRangeRects = (!util.isIE || util.isIE >= 9) && typeof _parent.ownerDocument.createRange().getBoundingClientRect === "function"; //$NON-NLS-1$ this._isW3CEvents = _parent.addEventListener; + this._isMarkdown = (_parent.baseURI.indexOf("editor=orion.editor.markdown") != -1); /* Auto scroll */ this._autoScrollX = null; diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/inputManager.js b/bundles/org.eclipse.orion.client.ui/web/orion/inputManager.js index 693cd0b001..6a696ba825 100644 --- a/bundles/org.eclipse.orion.client.ui/web/orion/inputManager.js +++ b/bundles/org.eclipse.orion.client.ui/web/orion/inputManager.js @@ -20,8 +20,9 @@ define([ 'orion/objects', 'orion/PageUtil', 'orion/editor/textModelFactory', - 'orion/metrics' -], function(messages, mNavigatorRenderer, i18nUtil, Deferred, EventTarget, objects, PageUtil, mTextModelFactory, mMetrics) { + 'orion/metrics', + 'orion/util' +], function(messages, mNavigatorRenderer, i18nUtil, Deferred, EventTarget, objects, PageUtil, mTextModelFactory, mMetrics, util) { function Idle(options){ this._document = options.document || document; @@ -399,8 +400,9 @@ define([ function _save(that) { editor.markClean(); - var contents = editor.getText(); + var contents = editor.getModel().getTextForSave(); var data = contents; + var hasTextDir = (data.indexOf(util.LtrMarker) != -1 || data.indexOf(util.RtlMarker) != -1); if (that._getSaveDiffsEnabled() && !that._errorSaving) { var changes = that._getUnsavedChanges(); if (changes) { @@ -408,7 +410,7 @@ define([ for (var i = 0; i < changes.length; i++) { len += changes[i].text.length; } - if (contents.length > len) { + if (contents.length > len && !hasTextDir) { data = { diff: changes }; diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/markdownEditor.js b/bundles/org.eclipse.orion.client.ui/web/orion/markdownEditor.js index 1b4ab3e94e..7e45745095 100644 --- a/bundles/org.eclipse.orion.client.ui/web/orion/markdownEditor.js +++ b/bundles/org.eclipse.orion.client.ui/web/orion/markdownEditor.js @@ -791,6 +791,25 @@ define([ this._computeAnnotations(annotationModel, baseModel, current, start, end, _add); }.bind(this)); }, + _enforceTextDir: function(block, tokens) { + var lineIndex = this.model.getLineAtOffset(block.start); + var LRE = "\u202A"; //$NON-NLS-0$ + var RLE = "\u202B"; //$NON-NLS-0$ + var textDir = this.model.getLineTextDir(lineIndex); + var charToAdd = ""; + if (textDir === "ltr") { + charToAdd = LRE; + } else if (textDir === "rtl") { + charToAdd = RLE; + } + if (charToAdd.length > 0) { + tokens.forEach(function(current) { + if (current.text) { + current.text = charToAdd + current.text; + } + }); + } + }, _generateHTML: function(rootElement, block) { if (!block.startToken && !block.tokens) { return; /* likely a block with only tokens for its parent */ @@ -806,6 +825,7 @@ define([ * removes each token from the array as it is processed */ var parseTokens = block.tokens.slice(); + this._enforceTextDir(block, parseTokens); parseTokens.links = this._linkDefs; rootElement.innerHTML = marked.Parser.parse(parseTokens, markedOptions); return; @@ -818,6 +838,7 @@ define([ /* a container block (a list, list item or blockquote) */ parseTokens = [block.startToken, block.endToken]; + this._enforceTextDir(block, parseTokens); parseTokens.links = this._linkDefs; rootElement.innerHTML = marked.Parser.parse(parseTokens, markedOptions); @@ -827,6 +848,7 @@ define([ * context to marked) and the tokens being contributed from child blocks. */ parseTokens = [block.startToken].concat(parentTokens).concat(block.endToken); + this._enforceTextDir(block, parseTokens); parseTokens.links = this._linkDefs; var tempElement = document.createElement("div"); //$NON-NLS-0$ tempElement.innerHTML = marked.Parser.parse(parseTokens, markedOptions);