继续阅读codemirror1
2019-02-23 我初步的阅读的codemirror1的源码, 但是, 没有解决一个问题, 没有仔细看光标的处理, 当我对dom进行处理的时候, 怎么保持光标的正确呢?
阅读目标:
- 拿到每键处理dom.
- 每键处理dom时如何保持光标的正确性.
阅读方法:
- 直接看源代码
- chrome开调试跟踪代码
- git跟踪提交记录找到相应的提交
这里看上去第二种方法比较合理那么一大波问题袭来
-
如何调试动态加载的js代码
在需要跟踪的js的头部或者尾部加这个注释 //# sourceURL=随便起的名字.js 然后再chrome里面就能看到nodomain里面出现了对应的名字的js文件
-
pause后逐步执行js时, chrome给我看的是这个:
//tab显示VM1415 !!( (window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && Object.keys(window.__REACT_DEVTOOLS_GLOBAL_HOOK__._renderers).length) || window.React )
-
询问全知全能的stackoverflow, 然后发现是react devtools的影响. 卸载了, 依旧没用.
-
重启chrome试试.
-
Stack Overflow有另一个解决办法, 在页面中加上这个:
<script> window.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function () {} </script> 很遗憾, 这个方法并没有解决问题.
-
-
然后上面这个问题被我绕过去了, 在elements里面, 找到event listen, 选择blocking, 这样事件都会被跟踪.
-
跟踪的过程中发现select.js是关键, 作者在这里实现了光标的各种操作.
-
实际的dom操作在文件尾部的highlight方法中实际执行.
-
select.js -> selectionrange方法 和 selectiontopnode方法中大量使用range操作光标.
var range = window.getselection().getrangeat(0) //拿到range var node = start ? range.startContainer : range.endContainer; //拿到node, 就是正在处理的文本 var offset = start ? range.startOffset : range.endOffset; //拿到光标偏移的量
-
关键代码
editor 1459 highlight 1081 对highlight的调用 832 keydown 事件的响应函数 1169 highlightParens 1216 highlightAtCursor 以及周围的调用 1075 highlightAtCursor 函数 1081 对于highlight的调用时最关键的调用, 同时它的上下文用select操作了光标. 1579 highlight里面的foreach是核心处理代码. 993 keyup是最佳断点位置. 1637 处理了contianer, 就是外部包围的span. 1655 移除了多余的内容. 最终断点位置: 1637, 此时可以移除keyup的断点. 不过这个位置会错过1216周围的光标操作. select 521 selectionTopNode
-
需要注释掉的异常抛出, 这些异常如果不注释掉, 会严重影响调试, 除非设置为不跟踪的模式.
editor 1602 抛出异常需要注释掉, 不然会频繁打断调试. 1608 抛出异常, 也需要注释掉. 不然也会打断调试. until 16 抛出异常 最终结论: 这些异常抛出其实也是可以不注释掉的. 是因为勾选了pause on excptions(在source面板的右边执行按钮下面)
代码分析
核心代码在editor
// 1165行
// Take the token before the cursor. If it contains a character in
// '()[]{}', search for the matching paren/brace/bracket, and
// highlight them in green for a moment, or red if no proper match
// was found.
highlightParens: function(jump, fromKey) {
var self = this, mark = this.options.markParen;
if (typeof mark == "string") mark = [mark, mark];
// give the relevant nodes a colour.
function highlight(node, ok) {
if (!node) return;
if (!mark) {
node.style.fontWeight = "bold";
node.style.color = ok ? "#8F8" : "#F88";
}
else if (mark.call) mark(node, ok);
else node.className += " " + mark[ok ? 0 : 1];
}
function unhighlight(node) {
if (!node) return;
if (mark && !mark.call)
removeClass(removeClass(node, mark[0]), mark[1]);
else if (self.options.unmarkParen)
self.options.unmarkParen(node);
else {
node.style.fontWeight = "";
node.style.color = "";
}
}
if (!fromKey && self.highlighted) {
unhighlight(self.highlighted[0]);
unhighlight(self.highlighted[1]);
}
if (!window || !window.parent || !window.select) return;
// Clear the event property.
if (this.parenEvent) parent.clearTimeout(this.parenEvent);
this.parenEvent = null;
// Extract a 'paren' from a piece of text.
function paren(node) {
if (node.currentText) {
var match = node.currentText.match(/^[\s\u00a0]*([\(\)\[\]{}])[\s\u00a0]*$/);
return match && match[1];
}
}
// Determine the direction a paren is facing.
function forward(ch) {
return /[\(\[\{]/.test(ch);
}
// 这里开始是核心代码
//这一行处理的是光标
var ch, cursor = select.selectionTopNode(this.container, true);
//这一行处理了高亮, 就是span那种, 具体调用下面分析.
if (!cursor || !this.highlightAtCursor()) return;
//这里开始继续搞光标
cursor = select.selectionTopNode(this.container, true);
if (!(cursor && ((ch = paren(cursor)) || (cursor = cursor.nextSibling) && (ch = paren(cursor)))))
return;
// We only look for tokens with the same className.
var className = cursor.className, dir = forward(ch), match = matching[ch];
// Since parts of the document might not have been properly
// highlighted, and it is hard to know in advance which part we
// have to scan, we just try, and when we find dirty nodes we
// abort, parse them, and re-try.
function tryFindMatch() {
var stack = [], ch, ok = true;
for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) {
if (runner.className == className && isSpan(runner) && (ch = paren(runner))) {
if (forward(ch) == dir)
stack.push(ch);
else if (!stack.length)
ok = false;
else if (stack.pop() != matching[ch])
ok = false;
if (!stack.length) break;
}
else if (runner.dirty || !isSpan(runner) && !isBR(runner)) {
return {node: runner, status: "dirty"};
}
}
return {node: runner, status: runner && ok};
}
while (true) {
var found = tryFindMatch();
if (found.status == "dirty") {
this.highlight(found.node, endOfLine(found.node));
// Needed because in some corner cases a highlight does not
// reach a node.
found.node.dirty = false;
continue;
}
else {
highlight(cursor, found.status);
highlight(found.node, found.status);
if (fromKey)
parent.setTimeout(function() {unhighlight(cursor); unhighlight(found.node);}, 500);
else
self.highlighted = [cursor, found.node];
if (jump && found.node)
select.focusAfterNode(found.node.previousSibling, this.container);
break;
}
}
}
//1074
// highlightAtCursor是个比较简单的函数
// Re-highlight the selected part of the document.
highlightAtCursor: function() {
//这里是之前的处理select的覅方, 所有的光标都在select
var pos = select.selectionTopNode(this.container, true);
var to = select.selectionTopNode(this.container, false);
if (pos === false || to === false) return false;
select.markSelection();
//这里是实际处理span的地方
if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
return false;
//这里继续处理select. 所有的光标都在select
select.selectMarked();
return true;
}
//1528 这个parts就是移除元素所在的位置
// parts is an interface to make it possible to 'delay' fetching
// the next DOM node until we are completely done with the one
// before it. This is necessary because often the next node is
// not yet available when we want to proceed past the current
// one.
var parts = {
current: null,
// Fetch current node.
get: function(){
if (!this.current)
this.current = traversal.nodes.shift();
return this.current;
},
// Advance to the next part (do not fetch it yet).
next: function(){
this.current = null;
},
// Remove the current part from the DOM tree, and move to the
// next.
remove: function(){ //这里是移除
container.removeChild(this.get());
this.current = null;
},
// Advance to the next part that is not empty, discarding empty
// parts.
getNonEmpty: function(){
var part = this.get();
// Allow empty nodes when they are alone on a line, needed
// for the FF cursor bug workaround (see select.js,
// insertNewlineAtCursor).
while (part && isSpan(part) && part.currentText == "") {
// Leave empty nodes that are alone on a line alone in
// Opera, since that browsers doesn't deal well with
// having 2 BRs in a row.
if (window.opera && surroundedByBRs(part)) {
this.next();
part = this.get();
}
else {
var old = part;
this.remove();
part = this.get();
// Adjust selection information, if any. See select.js for details.
select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0);
}
}
return part;
}
};
var lineDirty = false, prevLineDirty = true, lineNodes = 0;
//1575
//highlight 这个函数很复杂, 我们只拿核心出来分析
// This forEach loops over the tokens from the parsed stream, and
// at the same time uses the parts object to proceed through the
// corresponding DOM nodes.
forEach(parsed, function(token){
var part = parts.getNonEmpty();
if (token.value == "\n"){
// The idea of the two streams actually staying synchronized
// is such a long shot that we explicitly check.
if (!isBR(part))
throw "Parser out of sync. Expected BR.";
if (part.dirty || !part.indentation) lineDirty = true;
maybeTouch(from);
from = part;
// Every <br> gets a copy of the parser state and a lexical
// context assigned to it. The first is used to be able to
// later resume parsing from this point, the second is used
// for indentation.
part.parserFromHere = parsed.copy();
part.indentation = token.indentation || alwaysZero;
part.dirty = false;
// If the target argument wasn't an integer, go at least
// until that node.
if (endTime == null && part == target) {
console.log('StopIteration')
return;
}
//fixm by m 为了调试注释这段, 替换为上面的大括号代码块. throw StopIteration;
// A clean line with more than one node means we are done.
// Throwing a StopIteration is the way to break out of a
// MochiKit forEach loop.
if ((endTime != null && time() >= endTime) || (!lineDirty && !prevLineDirty && lineNodes > 1 && !cleanLines)) {
console.log('StopIteration')
return;
}
//fixm by m 为了调试注释这段, 替换为上面的大括号代码块. throw StopIteration;
prevLineDirty = lineDirty; lineDirty = false; lineNodes = 0;
parts.next();
}
else {
if (!isSpan(part))
throw "Parser out of sync. Expected SPAN.";
if (part.dirty)
lineDirty = true;
lineNodes++;
// If the part matches the token, we can leave it alone.
if (correctPart(token, part)){
if (active && part.dirty) active(part, token, self);
part.dirty = false;
parts.next();
}
// Otherwise, we have to fix it.
else {
lineDirty = true;
// Insert the correct part.
//这里弄了一个新的span
var newPart = tokenPart(token);
//插入这个span
container.insertBefore(newPart, part);
if (active) active(newPart, token, self);
var tokensize = token.value.length;
var offset = 0;
// Eat up parts until the text for this token has been
// removed, adjusting the stored selection info (see
// select.js) in the process.
while (tokensize > 0) {
part = parts.get();
var partsize = part.currentText.length;
select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset);
if (partsize > tokensize){
shortenPart(part, tokensize);
tokensize = 0;
}
else {
tokensize -= partsize;
offset += partsize;
parts.remove(); //这里移除了原本的文本, 因为新的span格式好的插进去了.
}
}
}
}
};
//tokenPart是一个比较简单的函数, 制造span, 其实不是.
//1489
// Create a part corresponding to a given token.
function tokenPart(token){
var part = makePartSpan(token.value);
part.className = token.style;
return part;
};
//47
// Create a SPAN node with the expected properties for document part
// spans.
function makePartSpan(value) {
var text = value;
if (value.nodeType == 3) text = value.nodeValue;
else value = document.createTextNode(text);
var span = document.createElement("span");
span.isPart = true;
span.appendChild(value); //span里面放内容.
span.currentText = text;
return span;
}
光标代码在select
//这个里面两套代码, 确实难受, 一套针对ie, 一套针对w3c, 在if里面做函数定义, 确实难受.
//219 是ie不需要看
//select.markSelection(); 有两个
//450
// Store start and end nodes, and offsets within these, and refer
// back to the selection object from those nodes, so that this
// object can be updated when the nodes are replaced before the
// selection is restored.
select.markSelection = function () {
var selection = window.getSelection();
if (!selection || selection.rangeCount == 0)
return (currentSelection = null);
var range = selection.getRangeAt(0);
currentSelection = {
start: innerNode(range.startContainer, range.startOffset),
end: innerNode(range.endContainer, range.endOffset),
changed: false
};
};
//select.selectMarked(); 也有两个
//229 是ie不需要看
select.selectMarked = function() {
if (!currentSelection || !currentSelection.changed) return;
function makeRange(point) {
var range = document.body.createTextRange(),
node = point.node;
if (!node) {
range.moveToElementText(document.body);
range.collapse(false);
}
else if (node.nodeType == 3) {
range.moveToElementText(node.parentNode);
var offset = point.offset;
while (node.previousSibling) {
node = node.previousSibling;
offset += (node.innerText || "").length;
}
range.move("character", offset);
}
else {
range.moveToElementText(node);
range.collapse(true);
}
return range;
}
var start = makeRange(currentSelection.start), end = makeRange(currentSelection.end);
start.setEndPoint("StartToEnd", end);
start.select();
};
//467
select.selectMarked = function () {
var cs = currentSelection;
// on webkit-based browsers, it is apparently possible that the
// selection gets reset even when a node that is not one of the
// endpoints get messed with. the most common situation where
// this occurs is when a selection is deleted or overwitten. we
// check for that here.
function focusIssue() {
if (cs.start.node == cs.end.node && cs.start.offset == cs.end.offset) {
var selection = window.getSelection();
if (!selection || selection.rangeCount == 0) return true;
var range = selection.getRangeAt(0), point = innerNode(range.startContainer, range.startOffset);
return cs.start.node != point.node || cs.start.offset != point.offset;
}
}
if (!cs || !(cs.changed || (webkit && focusIssue()))) return;
var range = document.createRange();
function setPoint(point, which) {
if (point.node) {
// Some magic to generalize the setting of the start and end
// of a range.
if (point.offset == 0)
range["set" + which + "Before"](point.node);
else
range["set" + which](point.node, point.offset);
}
else {
range.setStartAfter(document.body.lastChild || document.body);
}
}
setPoint(cs.end, "End");
setPoint(cs.start, "Start");
selectRange(range);
};
// 521
//select.selectionTopNode 一个很大的函数
心得
- 格式化代码时延迟异步合并处理的.
- 延迟是为了让他处理的时候, 内容是完整的. 因为, keydown时虽然我们拿到了code, 但是, 这个字还没有上到文本里面.
- 合并是为了降低开销, 不影响用户输入.
- 单独维护一个文本内容. 不论什么方案都要维护文本内容. 然后记录是否dirty, 以行为线索记录.
- 格式化的总的顺序
- 采集光标
- 格式化一个span, 加入文本内容, 插入
- 删除原本的文本内容
- 更新光标
附录
- codemirror.js 676 lines
- editor.js 1674 lines
- highlight.js 70 lines
- mirrorframe.js 83 lines
- parsecss.js 163 lines
- parsejavascript.js 361 lines
- select.js 435 lines
- stringstream.js 160 lines
- Tokenize.js 58 lines
- tokenizejavascript.js 175 lines
- undo.js 414 lines
- Unittests.js 44 Lines
- Util.js 163 Lines