Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting CodeMirror to follow a TextArea

How can I get CodeMirror to sync with a TextArea, so that the cursor position, selection & data stay the same on the both of them?

I'm using CodeMirror with MobWrite. CodeMirror only uses a textArea to read input & MobWrite can handle the selection, etc over a given TextArea, the problem is to get CodeMirror to sync with a TextArea.

like image 958
S P Avatar asked Sep 21 '11 12:09

S P


People also ask

What is the use of CodeMirror?

CodeMirror is a code editor component for the web. It can be used in websites to implement a text input field with support for many editing features, and has a rich programming interface to allow further extension. console.log(`Hello, ${who}!`) This is a CodeMirror field, configured for editing JavaScript code.


1 Answers

Extended the ModWrite code to support CodeMirror & it works like a charm.

It can also be found on GitHub. Details on the CodeMirror Group.

Code dumped below for all who may tread this path:

// CODEMIRROR INPUTS : Aped from Neil's code
/*
Set a 'id' property for your codeMirror obj
NoteToSelf: mobwrite.shareCodeMirrorObj.prototype.captureCursor_ - implement hasFocus
import linech2n func
import this code after you import MobWrite.
*/


/**
 * Constructor of shared object representing a CodeMirror obj
 */
mobwrite.shareCodeMirrorObj = function(cmObj) {
  // Call our prototype's constructor.
  if(!"id" in cmObj)    cmObj.id="TEMP";
  mobwrite.shareObj.apply(this, [cmObj.id]);
  this.element = cmObj;
};


// The textarea shared object's parent is a shareObj.
mobwrite.shareCodeMirrorObj.prototype = new mobwrite.shareObj('');


/**
 * Retrieve the user's text.
 * @return {string} Plaintext content.
 */
mobwrite.shareCodeMirrorObj.prototype.getClientText = function() {
  var text = mobwrite.shareCodeMirrorObj.normalizeLinebreaks_(this.element.getValue());
  return text;
};


/**
 * Set the user's text.
 * @param {string} text New text
 */
mobwrite.shareCodeMirrorObj.prototype.setClientText = function(text) {
  this.element.setValue(text);
  this.fireChange(this.element.getInputField());
};


/**
 * Modify the user's plaintext by applying a series of patches against it.
 * @param {Array.<patch_obj>} patches Array of Patch objects.
 */
mobwrite.shareCodeMirrorObj.prototype.patchClientText = function(patches) {
  // Set some constants which tweak the matching behaviour.
  // Maximum distance to search from expected location.
  this.dmp.Match_Distance = 1000;
  // At what point is no match declared (0.0 = perfection, 1.0 = very loose)
  this.dmp.Match_Threshold = 0.6;

  var oldClientText = this.getClientText();
  var cursor = this.captureCursor_();
  // Pack the cursor offsets into an array to be adjusted.
  // See http://neil.fraser.name/writing/cursor/
  var offsets = [];
  if (cursor) {
    offsets[0] = cursor.startOffset;
    if ('endOffset' in cursor) {
      offsets[1] = cursor.endOffset;
    }
  }
  var newClientText = this.patch_apply_(patches, oldClientText, offsets);
  // Set the new text only if there is a change to be made.
  if (oldClientText != newClientText) {
    this.setClientText(newClientText);
    if (cursor) {
      // Unpack the offset array.
      cursor.startOffset = offsets[0];
      if (offsets.length > 1) {
        cursor.endOffset = offsets[1];
        if (cursor.startOffset >= cursor.endOffset) {
          cursor.collapsed = true;
        }
      }
      this.restoreCursor_(cursor);
    }
  }
};


/**
 * Merge a set of patches onto the text.  Return a patched text.
 * @param {Array.<patch_obj>} patches Array of patch objects.
 * @param {string} text Old text.
 * @param {Array.<number>} offsets Offset indices to adjust.
 * @return {string} New text.
 */
mobwrite.shareCodeMirrorObj.prototype.patch_apply_ =
    function(patches, text, offsets) {
  if (patches.length == 0) {
    return text;
  }

  // Deep copy the patches so that no changes are made to originals.
  patches = this.dmp.patch_deepCopy(patches);
  var nullPadding = this.dmp.patch_addPadding(patches);
  text = nullPadding + text + nullPadding;

  this.dmp.patch_splitMax(patches);
  // delta keeps track of the offset between the expected and actual location
  // of the previous patch.  If there are patches expected at positions 10 and
  // 20, but the first patch was found at 12, delta is 2 and the second patch
  // has an effective expected position of 22.
  var delta = 0;
  for (var x = 0; x < patches.length; x++) {
    var expected_loc = patches[x].start2 + delta;
    var text1 = this.dmp.diff_text1(patches[x].diffs);
    var start_loc;
    var end_loc = -1;
    if (text1.length > this.dmp.Match_MaxBits) {
      // patch_splitMax will only provide an oversized pattern in the case of
      // a monster delete.
      start_loc = this.dmp.match_main(text,
          text1.substring(0, this.dmp.Match_MaxBits), expected_loc);
      if (start_loc != -1) {
        end_loc = this.dmp.match_main(text,
            text1.substring(text1.length - this.dmp.Match_MaxBits),
            expected_loc + text1.length - this.dmp.Match_MaxBits);
        if (end_loc == -1 || start_loc >= end_loc) {
          // Can't find valid trailing context.  Drop this patch.
          start_loc = -1;
        }
      }
    } else {
      start_loc = this.dmp.match_main(text, text1, expected_loc);
    }
    if (start_loc == -1) {
      // No match found.  :(
      if (mobwrite.debug) {
        window.console.warn('Patch failed: ' + patches[x]);
      }
      // Subtract the delta for this failed patch from subsequent patches.
      delta -= patches[x].length2 - patches[x].length1;
    } else {
      // Found a match.  :)
      if (mobwrite.debug) {
        window.console.info('Patch OK.');
      }
      delta = start_loc - expected_loc;
      var text2;
      if (end_loc == -1) {
        text2 = text.substring(start_loc, start_loc + text1.length);
      } else {
        text2 = text.substring(start_loc, end_loc + this.dmp.Match_MaxBits);
      }
      // Run a diff to get a framework of equivalent indices.
      var diffs = this.dmp.diff_main(text1, text2, false);
      if (text1.length > this.dmp.Match_MaxBits &&
          this.dmp.diff_levenshtein(diffs) / text1.length >
          this.dmp.Patch_DeleteThreshold) {
        // The end points match, but the content is unacceptably bad.
        if (mobwrite.debug) {
          window.console.warn('Patch contents mismatch: ' + patches[x]);
        }
      } else {
        var index1 = 0;
        var index2;
        for (var y = 0; y < patches[x].diffs.length; y++) {
          var mod = patches[x].diffs[y];
          if (mod[0] !== DIFF_EQUAL) {
            index2 = this.dmp.diff_xIndex(diffs, index1);
          }
          if (mod[0] === DIFF_INSERT) {  // Insertion
            text = text.substring(0, start_loc + index2) + mod[1] +
                   text.substring(start_loc + index2);
            for (var i = 0; i < offsets.length; i++) {
              if (offsets[i] + nullPadding.length > start_loc + index2) {
                offsets[i] += mod[1].length;
              }
            }
          } else if (mod[0] === DIFF_DELETE) {  // Deletion
            var del_start = start_loc + index2;
            var del_end = start_loc + this.dmp.diff_xIndex(diffs,
                index1 + mod[1].length);
            text = text.substring(0, del_start) + text.substring(del_end);
            for (var i = 0; i < offsets.length; i++) {
              if (offsets[i] + nullPadding.length > del_start) {
                if (offsets[i] + nullPadding.length < del_end) {
                  offsets[i] = del_start - nullPadding.length;
                } else {
                  offsets[i] -= del_end - del_start;
                }
              }
            }
          }
          if (mod[0] !== DIFF_DELETE) {
            index1 += mod[1].length;
          }
        }
      }
    }
  }
  // Strip the padding off.
  text = text.substring(nullPadding.length, text.length - nullPadding.length);
  return text;
};


/**
 * Record information regarding the current cursor.
 * @return {Object?} Context information of the cursor.
 * @private
 */
mobwrite.shareCodeMirrorObj.prototype.captureCursor_ = function() {
  this.element.focus();//change to hasFocus()?Pass:return null;
  var padLength = this.dmp.Match_MaxBits / 2;  // Normally 16.
  var text = this.element.getValue();
  var cursor = {};
    var selectionStart = linech2n(this.element, this.element.getCursor(true));
    var selectionEnd = linech2n(this.element, this.element.getCursor(false));
    cursor.startPrefix = text.substring(selectionStart - padLength, selectionStart);
    cursor.startSuffix = text.substring(selectionStart, selectionStart + padLength);
    cursor.startOffset = selectionStart;
    cursor.collapsed = (selectionStart == selectionEnd);
    if (!cursor.collapsed) {
      cursor.endPrefix = text.substring(selectionEnd - padLength, selectionEnd);
      cursor.endSuffix = text.substring(selectionEnd, selectionEnd + padLength);
      cursor.endOffset = selectionEnd;
    }
  // Record scrollbar locations
  if ('scrollTop' in this.element.getScrollerElement()) {
    scroller = this.element.getScrollerElement();
    cursor.scrollTop = scroller.scrollTop / scroller.scrollHeight;
    cursor.scrollLeft = scroller.scrollLeft / scroller.scrollWidth;
  }

  // alert(cursor.startPrefix + '|' + cursor.startSuffix + ' ' +
  //     cursor.startOffset + '\n' + cursor.endPrefix + '|' +
  //     cursor.endSuffix + ' ' + cursor.endOffset + '\n' +
  //     cursor.scrollTop + ' x ' + cursor.scrollLeft);
  return cursor;
};


/**
 * Attempt to restore the cursor's location.
 * @param {Object} cursor Context information of the cursor.
 * @private
 */
mobwrite.shareCodeMirrorObj.prototype.restoreCursor_ = function(cursor) {
  // Set some constants which tweak the matching behaviour.
  // Maximum distance to search from expected location.
  this.dmp.Match_Distance = 1000;
  // At what point is no match declared (0.0 = perfection, 1.0 = very loose)
  this.dmp.Match_Threshold = 0.9;

  var padLength = this.dmp.Match_MaxBits / 2;  // Normally 16.
  var newText = this.element.getValue();

  // Find the start of the selection in the new text.
  var pattern1 = cursor.startPrefix + cursor.startSuffix;
  var pattern2, diff;
  var cursorStartPoint = this.dmp.match_main(newText, pattern1,
      cursor.startOffset - padLength);
  if (cursorStartPoint !== null) {
    pattern2 = newText.substring(cursorStartPoint,
                                 cursorStartPoint + pattern1.length);
    //alert(pattern1 + '\nvs\n' + pattern2);
    // Run a diff to get a framework of equivalent indicies.
    diff = this.dmp.diff_main(pattern1, pattern2, false);
    cursorStartPoint += this.dmp.diff_xIndex(diff, cursor.startPrefix.length);
  }

  var cursorEndPoint = null;
  if (!cursor.collapsed) {
    // Find the end of the selection in the new text.
    pattern1 = cursor.endPrefix + cursor.endSuffix;
    cursorEndPoint = this.dmp.match_main(newText, pattern1,
        cursor.endOffset - padLength);
    if (cursorEndPoint !== null) {
      pattern2 = newText.substring(cursorEndPoint,
                                   cursorEndPoint + pattern1.length);
      //alert(pattern1 + '\nvs\n' + pattern2);
      // Run a diff to get a framework of equivalent indicies.
      diff = this.dmp.diff_main(pattern1, pattern2, false);
      cursorEndPoint += this.dmp.diff_xIndex(diff, cursor.endPrefix.length);
    }
  }

  // Deal with loose ends
  if (cursorStartPoint === null && cursorEndPoint !== null) {
    // Lost the start point of the selection, but we have the end point.
    // Collapse to end point.
    cursorStartPoint = cursorEndPoint;
  } else if (cursorStartPoint === null && cursorEndPoint === null) {
    // Lost both start and end points.
    // Jump to the offset of start.
    cursorStartPoint = cursor.startOffset;
  }
  if (cursorEndPoint === null) {
    // End not known, collapse to start.
    cursorEndPoint = cursorStartPoint;
  }

  // Restore selection.
  this.element.setSelection(n2linech(this.element, cursorStartPoint), n2linech(this.element, cursorEndPoint));

  // Restore scrollbar locations
  if ('scrollTop' in cursor) {
    this.element.getScrollerElement().scrollTop = cursor.scrollTop * this.element.getScrollerElement().scrollHeight;
    this.element.getScrollerElement().scrollLeft = cursor.scrollLeft * this.element.getScrollerElement().scrollWidth;
  }
};


/**
 * Ensure that all linebreaks are LF
 * @param {string} text Text with unknown line breaks
 * @return {string} Text with normalized linebreaks
 * @private
 */
mobwrite.shareCodeMirrorObj.normalizeLinebreaks_ = function(text) {
  return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
};


/**
 * Handler to accept CodeMirror Objs
 * @param {*} cmObj CodeMirror Object.
 * @return {Object?} A sharing object or null.
 */
mobwrite.shareCodeMirrorObj.shareHandler = function(cmObj) {
  if ('lineCount' in cmObj) {
    return new mobwrite.shareCodeMirrorObj(cmObj);
  }
  return null;
};


// Register this shareHandler with MobWrite.
mobwrite.shareHandlers.push(mobwrite.shareCodeMirrorObj.shareHandler);

//functions for converting b/n index on the data string & the {line, ch} obj of codeMirror
function linech2n(ed, linech) {
    var line = linech.line;
    var ch = linech.ch;
    var n = line + ch; //for the \n s & chars in the line
    for(i=0;i<line;i++) {
        n += (ed.getLine(i)).length;//for the chars in all preceeding lines
    }
    return n;
}

function n2linech(ed, n) {
    var line=0, ch=0, index=0;
    for(i=0;i<ed.lineCount();i++) {
        len = (ed.getLine(i)).length;
        if(n < index+len) {
            //alert(len+","+index+","+(n-index));
            line = i;
            ch = n-index;
            return {line:line, ch:ch};
        }
        len++;//for \n char
        index += len;
    }
    return {line:line, ch:ch};
}
like image 172
S P Avatar answered Oct 19 '22 08:10

S P