之前读了codemirror2的代码, 继续读3-5的代码.

v3.0beta1
  • 果然有重大修改, 已经没有charFromX了. 不过posFromMouse还在
function posFromMouse(cm, e, liberal) {
    var display = cm.display;//这个是显示区域
    if (!liberal) {
        var target = e_target(e);
        if (target == display.scrollbarH || target == display.scrollbarH.firstChild ||
            target == display.scrollbarV || target == display.scrollbarV.firstChild ||
            target == display.scrollbarFiller) return null;
    }
    var x, y, space = display.lineSpace.getBoundingClientRect(); //linespace 是初始值, 一个行div, 为了wrap用的.
    // Fails unpredictably on IE[67] when mouse is dragged around quickly.
    try { x = e.clientX; y = e.clientY; } catch (e) { return null; }
    return coordsChar(cm, x - space.left, y - space.top);//后面两个参数就是块内的偏移量
}

这里调用了coordsChar

// Coords must be lineSpace-local
function coordsChar(cm, x, y) {
    var display = cm.display, doc = cm.view.doc;
    var cw = charWidth(display), heightPos = display.viewOffset + y;
    if (heightPos < 0) return {line: 0, ch: 0};
    var lineNo = lineAtHeight(doc, heightPos);//一行一行的计算高度来找到行.
    if (lineNo >= doc.size) return {line: doc.size - 1, ch: getLine(doc, doc.size - 1).text.length};
    var lineObj = getLine(doc, lineNo);//拿到行dom对象
    if (!lineObj.text.length) return {line: lineNo, ch: 0};
    var tw = cm.options.lineWrapping, innerOff = tw ? heightPos - heightAtLine(doc, lineNo) : 0;//开了wrap就要一通算.
    if (x < 0) x = 0;
    var wrongLine = false;
    //这个函数变化了. 返回的是某个元素的left, 也是像素.
    function getX(ch) {
        //这个函数关键了, 拿到合适的元素.
        var sp = cursorCoords(cm, {line: lineNo, ch: ch}, "line", lineObj);
        if (tw) { //这个地方是自动换行wrap的修正.
            wrongLine = true;
            if (innerOff > sp.bottom) return Math.max(0, sp.left - display.wrapper.clientWidth);
            else if (innerOff < sp.top) return sp.left + display.wrapper.clientWidth;
            else wrongLine = false;
        }
        return sp.left;
    }
    var bidi = getOrder(lineObj), dist = lineObj.text.length;
    var from = lineLeft(lineObj), fromX = 0, to = lineRight(lineObj), toX;
    if (!bidi) {
        // Guess a suitable upper bound for our search.
        var estimated = Math.min(to, Math.ceil((x + Math.floor(innerOff / textHeight(display)) *
                                                display.wrapper.clientWidth * .9) / cw));
        for (;;) {
            var estX = getX(estimated);
            if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2));
            else {toX = estX; to = estimated; break;}
        }
        // Try to guess a suitable lower bound as well.
        estimated = Math.floor(to * 0.8); estX = getX(estimated);
        if (estX < x) {from = estimated; fromX = estX;}
        dist = to - from;
    } else toX = getX(to);
    if (x > toX) return {line: lineNo, ch: to};
    // Do a binary search between these bounds.
    for (;;) {
        if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) {
            var after = x - fromX < toX - x, ch = after ? from : to;
            while (isExtendingChar.test(lineObj.text.charAt(ch))) ++ch;
            return {line: lineNo, ch: ch, after: after}; //核心: 返回行数和字符数
        }
        var step = Math.ceil(dist / 2), middle = from + step;
        if (bidi) {
            middle = from;
            for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1);
        }
        var middleX = getX(middle);
        if (middleX > x) {to = middle; toX = middleX; if (wrongLine) toX += 1000; dist -= step;}
        else {from = middle; fromX = middleX; dist = step;}
    }
}

相关函数cursorcoords


function cursorCoords(cm, pos, context, lineObj) {
    lineObj = lineObj || getLine(cm.view.doc, pos.line);
    function get(ch, right) {
        var m = measureLine(cm, lineObj, ch);
        if (right) m.left = m.right; else m.right = m.left;
        return intoCoordSystem(cm, pos, m, context);
    }
    var order = getOrder(lineObj), ch = pos.ch;
    if (!order) return get(ch);
    var main, other, linedir = order[0].level;
    for (var i = 0; i < order.length; ++i) {
        var part = order[i], rtl = part.level % 2, nb, here;
        if (part.from < ch && part.to > ch) return get(ch, rtl);
        var left = rtl ? part.to : part.from, right = rtl ? part.from : part.to;
        if (left == ch) {
            // Opera and IE return bogus offsets and widths for edges
            // where the direction flips, but only for the side with the
            // lower level. So we try to use the side with the higher
            // level.
            if (i && part.level < (nb = order[i-1]).level) here = get(nb.level % 2 ? nb.from : nb.to - 1, true);
            else here = get(rtl && part.from != part.to ? ch - 1 : ch);
            if (rtl == linedir) main = here; else other = here;
        } else if (right == ch) {
            var nb = i < order.length - 1 && order[i+1];
            if (!rtl && nb && nb.from == nb.to) continue;
            if (nb && part.level < nb.level) here = get(nb.level % 2 ? nb.to - 1 : nb.from);
            else here = get(rtl ? ch : ch - 1, true);
            if (rtl == linedir) main = here; else other = here;
        }
    }
    if (linedir && !ch) other = get(order[0].to - 1);
    if (!main) return other;
    if (other) main.other = other;
    return main;
}
3.21.0 主线的最后一个3.x

简化了

// Coords must be lineSpace-local
function coordsChar(cm, x, y) {
    var doc = cm.doc;
    y += cm.display.viewOffset;
    if (y < 0) return PosWithInfo(doc.first, 0, true, -1);
    var lineNo = lineAtHeight(doc, y), last = doc.first + doc.size - 1;
    if (lineNo > last)
        return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1);
    if (x < 0) x = 0;

    for (;;) {
        var lineObj = getLine(doc, lineNo);
        var found = coordsCharInner(cm, lineObj, lineNo, x, y);
        var merged = collapsedSpanAtEnd(lineObj);
        var mergedPos = merged && merged.find();
        if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0))
            lineNo = mergedPos.to.line;
        else
            return found;
    }
}

其实不是了, 后面还有

function coordsCharInner(cm, lineObj, lineNo, x, y) {
    var innerOff = y - heightAtLine(cm, lineObj);
    var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth;
    var measurement = measureLine(cm, lineObj);

    function getX(ch) {
        var sp = cursorCoords(cm, Pos(lineNo, ch), "line",
                              lineObj, measurement);
        wrongLine = true;
        if (innerOff > sp.bottom) return sp.left - adjust;
        else if (innerOff < sp.top) return sp.left + adjust;
        else wrongLine = false;
        return sp.left;
    }

    var bidi = getOrder(lineObj), dist = lineObj.text.length;
    var from = lineLeft(lineObj), to = lineRight(lineObj);
    var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine;

    if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1);
    // Do a binary search between these bounds.
    for (;;) {
        if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) {
            var ch = x < fromX || x - fromX <= toX - x ? from : to;
            var xDiff = x - (ch == from ? fromX : toX);
            while (isExtendingChar(lineObj.text.charAt(ch))) ++ch;
            var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside,
                                  xDiff < 0 ? -1 : xDiff ? 1 : 0);
            return pos;
        }
        var step = Math.ceil(dist / 2), middle = from + step;
        if (bidi) {
            middle = from;
            for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1);
        }
        var middleX = getX(middle);
        if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;}
        else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;}
    }
}

v4-beta1

和v3没区别

4.13

没区别

5.0.0

依旧没变化

5.20.0

从这个版本开始依赖node了. codemirror.js是npm出来的,

在这个文件了: position_measurement

5.42.0

完全不知道这个是啥了, 需要跟踪一下才行

  • 两个关键元素
    • display就是显示的内容.
    • doc就是内部实际的文档.
// Compute the character position closest to the given coordinates.
// Input must be lineSpace-local ("div" coordinate system).
export function coordsChar(cm, x, y) {
    let doc = cm.doc
    y += cm.display.viewOffset
    if (y < 0) return PosWithInfo(doc.first, 0, null, true, -1)
    let lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1
    //lineatheight这个函数就是一行一行的减去高度, 减到高度小于行高然后拿到行数, 简单的说就是一行一行试出来的.
    if (lineN > last)
        return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, null, true, 1)
    if (x < 0) x = 0

    let lineObj = getLine(doc, lineN) //doc里面有全部的文档按照一行一行的存储再数组里面.
    for (;;) {
        let found = coordsCharInner(cm, lineObj, lineN, x, y) //就是这里拿的最终结果.
        let collapsed = collapsedSpanAround(lineObj, found.ch + (found.xRel > 0 ? 1 : 0))
        if (!collapsed) return found
        let rangeEnd = collapsed.find(1)
        if (rangeEnd.line == lineN) return rangeEnd
        lineObj = getLine(doc, lineN = rangeEnd.line)
    }
}

function coordsCharInner(cm, lineObj, lineNo, x, y) {
  // Move y into line-local coordinate space
  y -= heightAtLine(lineObj)  //lineobj是doc, 这里减掉了前面的行高.
  let preparedMeasure = prepareMeasureForLine(cm, lineObj) //这里拿到了当前行.
  // When directly calling `measureCharPrepared`, we have to adjust
  // for the widgets at this line.
  let widgetHeight = widgetTopHeight(lineObj)
  let begin = 0, end = lineObj.text.length, ltr = true //end就是line的长度

  let order = getOrder(lineObj, cm.doc.direction) //这个是判断文本方向, 从右往左....
  // If the line isn't plain left-to-right text, first figure out
  // which bidi section the coordinates fall into.
  if (order) { //难为marijn了, 幸亏他不知道中文和日本本来是从上往下写的.
    let part = (cm.options.lineWrapping ? coordsBidiPartWrapped : coordsBidiPart)
                 (cm, lineObj, lineNo, preparedMeasure, order, x, y)
    ltr = part.level != 1
    // The awkward -1 offsets are needed because findFirst (called
    // on these below) will treat its first bound as inclusive,
    // second as exclusive, but we want to actually address the
    // characters in the part's range
    begin = ltr ? part.from : part.to - 1
    end = ltr ? part.to : part.from - 1
  }

  // A binary search to find the first character whose bounding box
  // starts after the coordinates. If we run across any whose box wrap
  // the coordinates, store that.
  let chAround = null, boxAround = null
  let ch = findFirst(ch => { //这个地方是关键, findfirst就是持续的找到合适的位置的那个死循环. 他的第一个参数是一个函数.  这个函数的参数是一个位置值. 核心就在这里, 虽然隔了那么多个版本, 但是本质是一样的, 就是一个一个试过去的.
    let box = measureCharPrepared(cm, preparedMeasure, ch)
    box.top += widgetHeight; box.bottom += widgetHeight
    if (!boxIsAfter(box, x, y, false)) return false
    if (box.top <= y && box.left <= x) {
      chAround = ch
      boxAround = box
    }
    return true
  }, begin, end)

  //下面是修正, 是对计算结果的修正.
  let baseX, sticky, outside = false
  // If a box around the coordinates was found, use that
  if (boxAround) {
    // Distinguish coordinates nearer to the left or right side of the box
    let atLeft = x - boxAround.left < boxAround.right - x, atStart = atLeft == ltr
    ch = chAround + (atStart ? 0 : 1)
    sticky = atStart ? "after" : "before"
    baseX = atLeft ? boxAround.left : boxAround.right
  } else {
    // (Adjust for extended bound, if necessary.)
    if (!ltr && (ch == end || ch == begin)) ch++
    // To determine which side to associate with, get the box to the
    // left of the character and compare it's vertical position to the
    // coordinates
    sticky = ch == 0 ? "after" : ch == lineObj.text.length ? "before" :
      (measureCharPrepared(cm, preparedMeasure, ch - (ltr ? 1 : 0)).bottom + widgetHeight <= y) == ltr ?
      "after" : "before"
    // Now get accurate coordinates for this place, in order to get a
    // base X position
    let coords = cursorCoords(cm, Pos(lineNo, ch, sticky), "line", lineObj, preparedMeasure)
    baseX = coords.left
    outside = y < coords.top || y >= coords.bottom
  }

  ch = skipExtendingChars(lineObj.text, ch, 1)
  return PosWithInfo(lineNo, ch, sticky, outside, x - baseX)
}

结论: codemirror虽然从2一路开到了5, 但是, 在计算光标位置这件事上并没有进步, 都是一路算过去的. 或许, 我想的直接硬插的方式并不可行:

  • 我想的是用一个隐藏的textarea在dom节点位置硬插入内容,
  • 然后更新这个dom当前这一行的内容.
  • 保存的时候再保存整个dom的内容.