/*global ot */ ot.CodeMirrorAdapter = (function (global) { 'use strict'; var TextOperation = ot.TextOperation; var Selection = ot.Selection; function CodeMirrorAdapter(cm) { this.cm = cm; this.ignoreNextChange = false; this.changeInProgress = false; this.selectionChanged = false; bind(this, 'onChanges'); bind(this, 'onChange'); bind(this, 'onCursorActivity'); bind(this, 'onFocus'); bind(this, 'onBlur'); cm.on('changes', this.onChanges); cm.on('change', this.onChange); cm.on('cursorActivity', this.onCursorActivity); cm.on('focus', this.onFocus); cm.on('blur', this.onBlur); } // Removes all event listeners from the CodeMirror instance. CodeMirrorAdapter.prototype.detach = function () { this.cm.off('changes', this.onChanges); this.cm.off('change', this.onChange); this.cm.off('cursorActivity', this.onCursorActivity); this.cm.off('focus', this.onFocus); this.cm.off('blur', this.onBlur); }; function cmpPos(a, b) { if (a.line < b.line) { return -1; } if (a.line > b.line) { return 1; } if (a.ch < b.ch) { return -1; } if (a.ch > b.ch) { return 1; } return 0; } function posEq(a, b) { return cmpPos(a, b) === 0; } function posLe(a, b) { return cmpPos(a, b) <= 0; } function minPos(a, b) { return posLe(a, b) ? a : b; } function maxPos(a, b) { return posLe(a, b) ? b : a; } function codemirrorDocLength(doc) { return doc.indexFromPos({ line: doc.lastLine(), ch: 0 }) + doc.getLine(doc.lastLine()).length; } // Converts a CodeMirror change array (as obtained from the 'changes' event // in CodeMirror v4) or single change or linked list of changes (as returned // by the 'change' event in CodeMirror prior to version 4) into a // TextOperation and its inverse and returns them as a two-element array. CodeMirrorAdapter.operationFromCodeMirrorChanges = function (changes, doc) { // Approach: Replay the changes, beginning with the most recent one, and // construct the operation and its inverse. We have to convert the position // in the pre-change coordinate system to an index. We have a method to // convert a position in the coordinate system after all changes to an index, // namely CodeMirror's `indexFromPos` method. We can use the information of // a single change object to convert a post-change coordinate system to a // pre-change coordinate system. We can now proceed inductively to get a // pre-change coordinate system for all changes in the linked list. // A disadvantage of this approach is its complexity `O(n^2)` in the length // of the linked list of changes. var docEndLength = codemirrorDocLength(doc); var operation = new TextOperation().retain(docEndLength); var inverse = new TextOperation().retain(docEndLength); var indexFromPos = function (pos) { return doc.indexFromPos(pos); }; function last(arr) { return arr[arr.length - 1]; } function sumLengths(strArr) { if (strArr.length === 0) { return 0; } var sum = 0; for (var i = 0; i < strArr.length; i++) { sum += strArr[i].length; } return sum + strArr.length - 1; } function updateIndexFromPos(indexFromPos, change) { return function (pos) { if (posLe(pos, change.from)) { return indexFromPos(pos); } if (posLe(change.to, pos)) { return indexFromPos({ line: pos.line + change.text.length - 1 - (change.to.line - change.from.line), ch: (change.to.line < pos.line) ? pos.ch : (change.text.length <= 1) ? pos.ch - (change.to.ch - change.from.ch) + sumLengths(change.text) : pos.ch - change.to.ch + last(change.text).length }) + sumLengths(change.removed) - sumLengths(change.text); } if (change.from.line === pos.line) { return indexFromPos(change.from) + pos.ch - change.from.ch; } return indexFromPos(change.from) + sumLengths(change.removed.slice(0, pos.line - change.from.line)) + 1 + pos.ch; }; } for (var i = changes.length - 1; i >= 0; i--) { var change = changes[i]; indexFromPos = updateIndexFromPos(indexFromPos, change); var fromIndex = indexFromPos(change.from); var restLength = docEndLength - fromIndex - sumLengths(change.text); operation = new TextOperation() .retain(fromIndex)['delete'](sumLengths(change.removed)) .insert(change.text.join('\n')) .retain(restLength) .compose(operation); inverse = inverse.compose(new TextOperation() .retain(fromIndex)['delete'](sumLengths(change.text)) .insert(change.removed.join('\n')) .retain(restLength) ); docEndLength += sumLengths(change.removed) - sumLengths(change.text); } return [operation, inverse]; }; // Singular form for backwards compatibility. CodeMirrorAdapter.operationFromCodeMirrorChange = CodeMirrorAdapter.operationFromCodeMirrorChanges; // Apply an operation to a CodeMirror instance. CodeMirrorAdapter.applyOperationToCodeMirror = function (operation, cm) { cm.operation(function () { var ops = operation.ops; var index = 0; // holds the current index into CodeMirror's content for (var i = 0, l = ops.length; i < l; i++) { var op = ops[i]; if (TextOperation.isRetain(op)) { index += op; } else if (TextOperation.isInsert(op)) { cm.replaceRange(op, cm.posFromIndex(index), null, 'ignoreHistory'); index += op.length; } else if (TextOperation.isDelete(op)) { var from = cm.posFromIndex(index); var to = cm.posFromIndex(index - op); cm.replaceRange('', from, to, 'ignoreHistory'); } } }); }; CodeMirrorAdapter.prototype.registerCallbacks = function (cb) { this.callbacks = cb; }; CodeMirrorAdapter.prototype.onChange = function () { // By default, CodeMirror's event order is the following: // 1. 'change', 2. 'cursorActivity', 3. 'changes'. // We want to fire the 'selectionChange' event after the 'change' event, // but need the information from the 'changes' event. Therefore, we detect // when a change is in progress by listening to the change event, setting // a flag that makes this adapter defer all 'cursorActivity' events. this.changeInProgress = true; }; CodeMirrorAdapter.prototype.onChanges = function (_, changes) { if (!this.ignoreNextChange) { var pair = CodeMirrorAdapter.operationFromCodeMirrorChanges(changes, this.cm); this.trigger('change', pair[0], pair[1]); } if (this.selectionChanged) { this.trigger('selectionChange'); } this.changeInProgress = false; this.ignoreNextChange = false; }; CodeMirrorAdapter.prototype.onCursorActivity = CodeMirrorAdapter.prototype.onFocus = function () { if (this.changeInProgress) { this.selectionChanged = true; } else { this.trigger('selectionChange'); } }; CodeMirrorAdapter.prototype.onBlur = function () { if (!this.cm.somethingSelected()) { this.trigger('blur'); } }; CodeMirrorAdapter.prototype.getValue = function () { return this.cm.getValue(); }; CodeMirrorAdapter.prototype.getSelection = function () { var cm = this.cm; var selectionList = cm.listSelections(); var ranges = []; for (var i = 0; i < selectionList.length; i++) { ranges[i] = new Selection.Range( cm.indexFromPos(selectionList[i].anchor), cm.indexFromPos(selectionList[i].head) ); } return new Selection(ranges); }; CodeMirrorAdapter.prototype.setSelection = function (selection) { var ranges = []; for (var i = 0; selection && i < selection.ranges.length; i++) { var range = selection.ranges[i]; ranges[i] = { anchor: this.cm.posFromIndex(range.anchor), head: this.cm.posFromIndex(range.head) }; } this.cm.setSelections(ranges); }; var addStyleRule = (function () { var added = {}; var styleElement = document.createElement('style'); document.documentElement.getElementsByTagName('head')[0].appendChild(styleElement); var styleSheet = styleElement.sheet; return function (css) { if (added[css]) { return; } added[css] = true; styleSheet.insertRule(css, (styleSheet.cssRules || styleSheet.rules).length); }; }()); CodeMirrorAdapter.prototype.setOtherCursor = function (position, color, clientId) { var cursorPos = this.cm.posFromIndex(position); var cursorCoords = this.cm.cursorCoords(cursorPos); var cursorEl = document.createElement('span'); cursorEl.className = 'other-client'; cursorEl.style.display = 'none'; /* cursorEl.style.padding = '0'; cursorEl.style.marginLeft = cursorEl.style.marginRight = '-1px'; cursorEl.style.borderLeftWidth = '2px'; cursorEl.style.borderLeftStyle = 'solid'; cursorEl.style.borderLeftColor = color; cursorEl.style.height = (cursorCoords.bottom - cursorCoords.top) * 0.9 + 'px'; cursorEl.style.zIndex = 0; */ cursorEl.setAttribute('data-clientid', clientId); return this.cm.setBookmark(cursorPos, { widget: cursorEl, insertLeft: true }); }; CodeMirrorAdapter.prototype.setOtherSelectionRange = function (range, color, clientId) { var match = /^#([0-9a-fA-F]{6})$/.exec(color); if (!match) { throw new Error("only six-digit hex colors are allowed."); } var selectionClassName = 'selection-' + match[1]; var rgbcolor = hex2rgb(color); var rule = '.' + selectionClassName + ' { background: rgba(' + rgbcolor.red + ',' + rgbcolor.green + ',' + rgbcolor.blue + ',0.2); }'; addStyleRule(rule); var anchorPos = this.cm.posFromIndex(range.anchor); var headPos = this.cm.posFromIndex(range.head); return this.cm.markText( minPos(anchorPos, headPos), maxPos(anchorPos, headPos), { className: selectionClassName } ); }; CodeMirrorAdapter.prototype.setOtherSelection = function (selection, color, clientId) { var selectionObjects = []; for (var i = 0; i < selection.ranges.length; i++) { var range = selection.ranges[i]; if (range.isEmpty()) { //selectionObjects[i] = this.setOtherCursor(range.head, color, clientId); } else { selectionObjects[i] = this.setOtherSelectionRange(range, color, clientId); } } return { clear: function () { for (var i = 0; i < selectionObjects.length; i++) { selectionObjects[i].clear(); } } }; }; CodeMirrorAdapter.prototype.trigger = function (event) { var args = Array.prototype.slice.call(arguments, 1); var action = this.callbacks && this.callbacks[event]; if (action) { action.apply(this, args); } }; CodeMirrorAdapter.prototype.applyOperation = function (operation) { this.ignoreNextChange = true; CodeMirrorAdapter.applyOperationToCodeMirror(operation, this.cm); }; CodeMirrorAdapter.prototype.registerUndo = function (undoFn) { this.cm.undo = undoFn; }; CodeMirrorAdapter.prototype.registerRedo = function (redoFn) { this.cm.redo = redoFn; }; // Throws an error if the first argument is falsy. Useful for debugging. function assert(b, msg) { if (!b) { throw new Error(msg || "assertion error"); } } // Bind a method to an object, so it doesn't matter whether you call // object.method() directly or pass object.method as a reference to another // function. function bind(obj, method) { var fn = obj[method]; obj[method] = function () { fn.apply(obj, arguments); }; } return CodeMirrorAdapter; }(this)); function hex2rgb(hex) { if (hex[0] == "#") hex = hex.substr(1); if (hex.length == 3) { var temp = hex; hex = ''; temp = /^([a-f0-9])([a-f0-9])([a-f0-9])$/i.exec(temp).slice(1); for (var i = 0; i < 3; i++) hex += temp[i] + temp[i]; } var triplets = /^([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(hex).slice(1); return { red: parseInt(triplets[0], 16), green: parseInt(triplets[1], 16), blue: parseInt(triplets[2], 16) } }