Skip to content
Browse files

Implement first-class docs and linked docs

 - Move a lot of methods from the CodeMirror to the Doc prototype
 - Allow getting, attaching, and copying of Doc objects
 - Add linkedDoc to create documents that stay in sync
 - Allow such linked docs to be sub-view on their parent
 - Rewrite history handling
 - Remove the concept of a view

Documentation still failing.
  • Loading branch information...
1 parent 5051dd2 commit 381d89d4eb0515184ef89f9bcd1136736a31d6da @marijnh marijnh committed
Showing with 1,449 additions and 751 deletions.
  1. +8 −3 addon/search/searchcursor.js
  2. +98 −0 demo/buffers.html
  3. +0 −3 keymap/vim.js
  4. +1,011 −734 lib/codemirror.js
  5. +290 −0 test/doc_test.js
  6. +3 −3 test/driver.js
  7. +3 −6 test/index.html
  8. +36 −2 test/test.js
View
11 addon/search/searchcursor.js
@@ -5,6 +5,7 @@
pos = pos ? cm.clipPos(pos) : {line: 0, ch: 0};
this.pos = {from: pos, to: pos};
+ if (!pos) debugger;
@bfrohs
bfrohs added a note

Leave this in by mistake?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
// The matches method is filled in based on the type of query.
// It takes a position and a direction, and returns an object
@@ -85,6 +86,7 @@
findPrevious: function() {return this.find(true);},
find: function(reverse) {
+ if (!(reverse ? this.pos.from : this.pos.to)) debugger;
@bfrohs
bfrohs added a note

Leave this in by mistake?

@marijnh CodeMirror member
marijnh added a note

Indeed. See 27c1795

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
var self = this, pos = this.cm.clipPos(reverse ? this.pos.from : this.pos.to);
function savePosAndFail(line) {
var pos = {line: line, ch: 0};
@@ -95,6 +97,7 @@
for (;;) {
if (this.pos = this.matches(reverse, pos)) {
+ if (!this.pos.from || !this.pos.to) { console.log(this.matches, this.pos); }
this.atOccurrence = true;
return this.pos.match || true;
}
@@ -114,9 +117,11 @@
to: function() {if (this.atOccurrence) return this.pos.to;},
replace: function(newText) {
- var self = this;
- if (this.atOccurrence)
- self.pos.to = this.cm.replaceRange(newText, self.pos.from, self.pos.to);
+ if (!this.atOccurrence) return;
+ var lines = CodeMirror.splitLines(newText);
+ this.cm.replaceRange(lines, this.pos.from, this.pos.to);
+ this.pos.to = {line: this.pos.from.line + lines.length - 1,
+ ch: lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0)};
}
};
View
98 demo/buffers.html
@@ -0,0 +1,98 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>CodeMirror: Multiple Buffer & Split View Demo</title>
+ <link rel="stylesheet" href="../lib/codemirror.css">
+ <script src="../lib/codemirror.js"></script>
+ <script src="../mode/javascript/javascript.js"></script>
+ <script src="../mode/css/css.js"></script>
+ <link rel="stylesheet" href="../doc/docs.css">
+
+ <style type="text/css" id=style>
+ .CodeMirror {border: 1px solid black; height: 250px;}
+ </style>
+ </head>
+ <body>
+ <h1>CodeMirror: Multiple Buffer & Split View Demo</h1>
+
+ <div id=code_top></div>
+ <div>
+ Select buffer: <select id=buffers_top></select>
+ &nbsp; &nbsp; <button onclick="newBuf('top')">New buffer</button>
+ </div>
+ <div id=code_bot></div>
+ <div>
+ Select buffer: <select id=buffers_bot></select>
+ &nbsp; &nbsp; <button onclick="newBuf('bot')">New buffer</button>
+ </div>
+
+ <script id=script>
+var sel_top = document.getElementById("buffers_top");
+CodeMirror.on(sel_top, "change", function() {
+ selectBuffer(ed_top, sel_top.options[sel_top.selectedIndex].value);
+});
+
+var sel_bot = document.getElementById("buffers_bot");
+CodeMirror.on(sel_bot, "change", function() {
+ selectBuffer(ed_bot, sel_bot.options[sel_bot.selectedIndex].value);
+});
+
+var buffers = {};
+
+function openBuffer(name, text, mode) {
+ buffers[name] = CodeMirror.Doc(text, mode);
+ var opt = document.createElement("option");
+ opt.appendChild(document.createTextNode(name));
+ sel_top.appendChild(opt);
+ sel_bot.appendChild(opt.cloneNode(true));
+}
+
+function newBuf(where) {
+ var name = prompt("Name for the buffer", "*scratch*");
+ if (name == null) return;
+ if (buffers.hasOwnProperty(name)) {
+ alert("There's already a buffer by that name.");
+ return;
+ }
+ openBuffer(name, "", "javascript");
+ selectBuffer(where == "top" ? ed_top : ed_bot, name);
+ var sel = where == "top" ? sel_top : sel_bot;
+ sel.value = name;
+}
+
+function selectBuffer(editor, name) {
+ var buf = buffers[name];
+ if (buf.getEditor()) buf = buf.linkedDoc({sharedHist: true});
+ var old = editor.swapDoc(buf);
+ var linked = old.iterLinkedDocs(function(doc) {linked = doc;});
+ if (linked) {
+ // Make sure the document in buffers is the one the other view is looking at
+ for (var name in buffers) if (buffers[name] == old) buffers[name] = linked;
+ old.unlinkDoc(linked);
+ }
+ editor.focus();
+}
+
+function nodeContent(id) {
+ var node = document.getElementById(id), val = node.textContent || node.innerText;
+ val = val.slice(val.match(/^\s*/)[0].length, val.length - val.match(/\s*$/)[0].length) + "\n";
+ return val;
+}
+openBuffer("js", nodeContent("script"), "javascript");
+openBuffer("css", nodeContent("style"), "css");
+
+var ed_top = CodeMirror(document.getElementById("code_top"), {lineNumbers: true});
+selectBuffer(ed_top, "js");
+var ed_bot = CodeMirror(document.getElementById("code_bot"), {lineNumbers: true});
+selectBuffer(ed_bot, "js");
+</script>
+
+ <p>Demonstration of
+ using <a href="../doc/manual.html#linkedDoc">linked documents</a>
+ to provide a split view on a document, and
+ using <a href="../doc/manual.html#swapDoc"><code>swapDoc</code></a>
+ to use a single editor to display multiple documents.</p>
+
+ </body>
+</html>
View
3 keymap/vim.js
@@ -810,9 +810,6 @@
selectionStart.ch = lineLength(cm, selectionStart.line);
}
}
- // Need to set the cursor to clear the selection. Otherwise,
- // CodeMirror can't figure out that we changed directions...
- cm.setCursor(selectionStart);
cm.setSelection(selectionStart, selectionEnd);
updateMark(cm, vim, '<',
cursorIsBefore(selectionStart, selectionEnd) ? selectionStart
View
1,745 lib/codemirror.js
1,011 additions, 734 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
View
290 test/doc_test.js
@@ -0,0 +1,290 @@
+(function() {
+ // A minilanguage for instantiating linked CodeMirror instances and Docs
+ function instantiateSpec(spec, place, opts) {
+ var names = {}, pos = 0, l = spec.length, editors = [];
+ while (spec) {
+ var m = spec.match(/^(\w+)(\*?)(?:='([^\']*)'|<(~?)(\w+)(?:\/(\d+)-(\d+))?)\s*/);
+ var name = m[1], isDoc = m[2], cur;
+ if (m[3]) {
+ cur = isDoc ? CodeMirror.Doc(m[3]) : CodeMirror(place, clone(opts, {value: m[3]}));
+ } else {
+ var other = m[5];
+ if (!names.hasOwnProperty(other)) {
+ names[other] = editors.length;
+ editors.push(CodeMirror(place, opts));
+ }
+ var doc = editors[names[other]].linkedDoc({
+ sharedHist: !m[4],
+ from: m[6] && Number(m[6]),
+ to: m[7] && Number(m[7])
+ });
+ cur = isDoc ? doc : CodeMirror(place, clone(opts, {value: doc}));
+ }
+ names[name] = editors.length;
+ editors.push(cur);
+ spec = spec.slice(m[0].length);
+ }
+ return editors;
+ }
+
+ function clone(obj, props) {
+ if (!obj) return;
+ clone.prototype = obj;
+ var inst = new clone();
+ if (props) for (var n in props) if (props.hasOwnProperty(n))
+ inst[n] = props[n];
+ return inst;
+ }
+
+ function eqAll(val) {
+ var end = arguments.length, msg = null;
+ if (typeof arguments[end-1] == "string")
+ msg = arguments[--end];
+ if (i == end) throw new Error("No editors provided to eqAll");
+ for (var i = 1; i < end; ++i)
+ eq(arguments[i].getValue(), val, msg)
+ }
+
+ function testDoc(name, spec, run, opts) {
+ if (!opts) opts = {};
+
+ return test("doc_" + name, function() {
+ var place = document.getElementById("testground");
+ var editors = instantiateSpec(spec, place, opts);
+ var successful = false;
+
+ try {
+ run.apply(null, editors);
+ successful = true;
+ } finally {
+ if ((debug && !successful) || verbose) {
+ place.style.visibility = "";
+ } else {
+ for (var i = 0; i < editors.length; ++i)
+ if (editors[i] instanceof CodeMirror)
+ place.removeChild(editors[i].getWrapperElement());
+ }
+ }
+ });
+ }
+
+ function testBasic(a, b) {
+ eqAll("x", a, b);
+ a.setValue("hey");
+ eqAll("hey", a, b);
+ b.setValue("wow");
+ eqAll("wow", a, b);
+ a.replaceRange("u\nv\nw", {line: 0, ch: 3});
+ b.replaceRange("i", {line: 0, ch: 4});
+ b.replaceRange("j", {line: 2, ch: 1});
+ eqAll("wowui\nv\nwj", a, b);
+ }
+
+ testDoc("basic", "A='x' B<A", testBasic);
+ testDoc("basicSeparate", "A='x' B<~A", testBasic);
+
+ testDoc("sharedHist", "A='ab\ncd\nef' B<A", function(a, b) {
+ a.replaceRange("x", {line: 0});
+ b.replaceRange("y", {line: 1});
+ a.replaceRange("z", {line: 2});
+ eqAll("abx\ncdy\nefz", a, b);
+ a.undo();
+ a.undo();
+ eqAll("abx\ncd\nef", a, b);
+ a.redo();
+ eqAll("abx\ncdy\nef", a, b);
+ b.redo();
+ eqAll("abx\ncdy\nefz", a, b);
+ a.undo(); b.undo(); a.undo(); a.undo();
+ eqAll("ab\ncd\nef", a, b);
+ });
+
+ testDoc("undoIntact", "A='ab\ncd\nef' B<~A", function(a, b) {
+ a.replaceRange("x", {line: 0});
+ b.replaceRange("y", {line: 1});
+ a.replaceRange("z", {line: 2});
+ a.replaceRange("q", {line: 0});
+ eqAll("abxq\ncdy\nefz", a, b);
+ a.undo();
+ a.undo();
+ eqAll("abx\ncdy\nef", a, b);
+ b.undo();
+ eqAll("abx\ncd\nef", a, b);
+ a.redo();
+ eqAll("abx\ncd\nefz", a, b);
+ a.redo();
+ eqAll("abxq\ncd\nefz", a, b);
+ a.undo(); a.undo(); a.undo(); a.undo();
+ eqAll("ab\ncd\nef", a, b);
+ b.redo();
+ eqAll("ab\ncdy\nef", a, b);
+ });
+
+ testDoc("undoConflict", "A='ab\ncd\nef' B<~A", function(a, b) {
+ a.replaceRange("x", {line: 0});
+ a.replaceRange("z", {line: 2});
+ // This should clear the first undo event in a, but not the second
+ b.replaceRange("y", {line: 0});
+ a.undo(); a.undo();
+ eqAll("abxy\ncd\nef", a, b);
+ a.replaceRange("u", {line: 2});
+ a.replaceRange("v", {line: 0});
+ // This should clear both events in a
+ b.replaceRange("w", {line: 0});
+ a.undo(); a.undo();
+ eqAll("abxyvw\ncd\nefu", a, b);
+ });
+
+ testDoc("undoUpdate", "A='ab\ncd\nef' B<~A", function(a, b) {
+ a.replaceRange("x", {line: 2});
+ b.replaceRange("u\nv\nw\n", {line: 0, ch: 0});
+ a.undo();
+ eqAll("u\nv\nw\nab\ncd\nef", a, b);
+ a.redo();
+ eqAll("u\nv\nw\nab\ncd\nefx", a, b);
+ a.undo();
+ eqAll("u\nv\nw\nab\ncd\nef", a, b);
+ b.undo();
+ a.redo();
+ eqAll("ab\ncd\nefx", a, b);
+ a.undo();
+ eqAll("ab\ncd\nef", a, b);
+ });
+
+ testDoc("undoKeepRanges", "A='abcdefg' B<A", function(a, b) {
+ var m = a.markText({line: 0, ch: 1}, {line: 0, ch: 3}, {className: "foo"});
+ b.replaceRange("x", {line: 0, ch: 0});
+ eqPos(m.find().from, {line: 0, ch: 2});
+ b.replaceRange("yzzy", {line: 0, ch: 1}, {line: 0});
+ eq(m.find(), null);
+ b.undo();
+ eqPos(m.find().from, {line: 0, ch: 2});
+ b.undo();
+ eqPos(m.find().from, {line: 0, ch: 1});
+ });
+
+ testDoc("longChain", "A='uv' B<A C<B D<C", function(a, b, c, d) {
+ a.replaceSelection("X");
+ eqAll("Xuv", a, b, c, d);
+ d.replaceRange("Y", {line: 0});
+ eqAll("XuvY", a, b, c, d);
+ });
+
+ testDoc("broadCast", "B<A C<A D<A E<A", function(a, b, c, d, e) {
+ b.setValue("uu");
+ eqAll("uu", a, b, c, d, e);
+ a.replaceRange("v", {line: 0, ch: 1});
+ eqAll("uvu", a, b, c, d, e);
+ });
+
+ // A and B share a history, C and D share a separate one
+ testDoc("islands", "A='x\ny\nz' B<A C<~A D<C", function(a, b, c, d) {
+ a.replaceRange("u", {line: 0});
+ d.replaceRange("v", {line: 2});
+ b.undo();
+ eqAll("x\ny\nzv", a, b, c, d);
+ c.undo();
+ eqAll("x\ny\nz", a, b, c, d);
+ a.redo();
+ eqAll("xu\ny\nz", a, b, c, d);
+ d.redo();
+ eqAll("xu\ny\nzv", a, b, c, d);
+ });
+
+ testDoc("unlink", "B<A C<A D<B", function(a, b, c, d) {
+ a.setValue("hi");
+ b.unlinkDoc(a);
+ d.setValue("aye");
+ eqAll("hi", a, c);
+ eqAll("aye", b, d);
+ a.setValue("oo");
+ eqAll("oo", a, c);
+ eqAll("aye", b, d);
+ });
+
+ testDoc("bareDoc", "A*='foo' B*<A C<B", function(a, b, c) {
+ is(a instanceof CodeMirror.Doc);
+ is(b instanceof CodeMirror.Doc);
+ is(c instanceof CodeMirror);
+ eqAll("foo", a, b, c);
+ a.replaceRange("hey", {line: 0, ch: 0}, {line: 0});
+ c.replaceRange("!", {line: 0});
+ eqAll("hey!", a, b, c);
+ b.unlinkDoc(a);
+ b.setValue("x");
+ eqAll("x", b, c);
+ eqAll("hey!", a);
+ });
+
+ testDoc("swapDoc", "A='a' B*='b' C<A", function(a, b, c) {
+ var d = a.swapDoc(b);
+ d.setValue("x");
+ eqAll("x", c, d);
+ eqAll("b", a, b);
+ });
+
+ testDoc("docKeepsScroll", "A='x' B*='y'", function(a, b) {
+ addDoc(a, 200, 200);
+ a.scrollIntoView({line: 199, ch: 200});
+ var c = a.swapDoc(b);
+ a.swapDoc(c);
+ var pos = a.getScrollInfo();
+ is(pos.left > 0, "not at left");
+ is(pos.top > 0, "not at top");
+ });
+
+ testDoc("copyDoc", "A='u'", function(a) {
+ var copy = a.getDoc().copy(true);
+ a.setValue("foo");
+ copy.setValue("bar");
+ var old = a.swapDoc(copy);
+ eq(a.getValue(), "bar");
+ a.undo();
+ eq(a.getValue(), "u");
+ a.swapDoc(old);
+ eq(a.getValue(), "foo");
+ eq(old.historySize().undo, 1);
+ eq(old.copy(false).historySize().undo, 0);
+ });
+
+ testDoc("docKeepsMode", "A='1+1'", function(a) {
+ var other = CodeMirror.Doc("hi", "text/x-markdown");
+ a.setOption("mode", "text/javascript");
+ var old = a.swapDoc(other);
+ eq(a.getOption("mode"), "text/x-markdown");
+ eq(a.getMode().name, "markdown");
+ a.swapDoc(old);
+ eq(a.getOption("mode"), "text/javascript");
+ eq(a.getMode().name, "javascript");
+ });
+
+ testDoc("subview", "A='1\n2\n3\n4\n5' B<~A/1-3", function(a, b) {
+ eq(b.getValue(), "2\n3");
+ eq(b.firstLine(), 1);
+ b.setCursor({line: 4});
+ eqPos(b.getCursor(), {line: 2, ch: 1});
+ a.replaceRange("-1\n0\n", {line: 0, ch: 0});
+ eq(b.firstLine(), 3);
+ eqPos(b.getCursor(), {line: 4, ch: 1});
+ a.undo();
+ eqPos(b.getCursor(), {line: 2, ch: 1});
+ b.replaceRange("oyoy\n", {line: 2, ch: 0});
+ eq(a.getValue(), "1\n2\noyoy\n3\n4\n5");
+ b.undo();
+ eq(a.getValue(), "1\n2\n3\n4\n5");
+ });
+
+ testDoc("subviewEditOnBoundary", "A='11\n22\n33\n44\n55' B<~A/1-4", function(a, b) {
+ a.replaceRange("x\nyy\nz", {line: 0, ch: 1}, {line: 2, ch: 1});
+ eq(b.firstLine(), 2);
+ eq(b.lineCount(), 2);
+ eq(b.getValue(), "z3\n44");
+ a.replaceRange("q\nrr\ns", {line: 3, ch: 1}, {line: 4, ch: 1});
+ eq(b.firstLine(), 2);
+ eq(b.getValue(), "z3\n4q");
+ eq(a.getValue(), "1x\nyy\nz3\n4q\nrr\ns5");
+ a.execCommand("selectAll");
+ a.replaceSelection("!");
+ eqAll("!", a, b);
+ });
+})();
View
6 test/driver.js
@@ -117,8 +117,8 @@ function eq(a, b, msg) {
function eqPos(a, b, msg) {
function str(p) { return "{line:" + p.line + ",ch:" + p.ch + "}"; }
if (a == b) return;
- if (a == null) throw new Failure(label("comparing null to " + str(b)));
- if (b == null) throw new Failure(label("comparing " + str(a) + " to null"));
+ if (a == null) throw new Failure(label("comparing null to " + str(b), msg));
+ if (b == null) throw new Failure(label("comparing " + str(a) + " to null", msg));
if (a.line != b.line || a.ch != b.ch) throw new Failure(label(str(a) + " != " + str(b), msg));
}
function is(a, msg) {
@@ -135,4 +135,4 @@ function countTests() {
++sum;
}
return sum;
-}
+}
View
9 test/index.html
@@ -52,6 +52,7 @@
<script src="driver.js"></script>
<script src="test.js"></script>
+ <script src="doc_test.js"></script>
<script src="mode_test.js"></script>
<script src="../mode/css/css.js"></script>
<script src="../mode/css/test.js"></script>
@@ -65,12 +66,8 @@
<script src="../mode/xquery/test.js"></script>
<script src="vim_test.js"></script>
<script>
- window.onload = function() {
- runHarness();
- };
- CodeMirror.on(window, 'hashchange', function(){
- runHarness();
- });
+ window.onload = runHarness;
+ CodeMirror.on(window, 'hashchange', runHarness);
function esc(str) {
return str.replace(/[<&]/, function(ch) { return ch == "<" ? "&lt;" : "&amp;"; });
View
38 test/test.js
@@ -269,6 +269,19 @@ testCM("undo", function(cm) {
eq(cm.getValue(), "1\n\n\n2");
}, {value: "abc"});
+testCM("undoDepth", function(cm) {
+ cm.replaceRange("d", {line: 0});
+ cm.replaceRange("e", {line: 0});
+ cm.replaceRange("f", {line: 0});
+ cm.undo(); cm.undo(); cm.undo();
+ eq(cm.getValue(), "abcd");
+}, {value: "abc", undoDepth: 2});
+
+testCM("undoDoesntClearValue", function(cm) {
+ cm.undo();
+ eq(cm.getValue(), "x");
+}, {value: "x"});
+
testCM("undoMultiLine", function(cm) {
cm.operation(function() {
cm.replaceRange("x", {line:0, ch: 0});
@@ -291,6 +304,21 @@ testCM("undoMultiLine", function(cm) {
eq(cm.getValue(), "abc\ndef\nghi", 3);
}, {value: "abc\ndef\nghi"});
+testCM("undoComposite", function(cm) {
+ cm.replaceRange("y", {line: 1});
+ cm.operation(function() {
+ cm.replaceRange("x", {line: 0});
+ cm.replaceRange("z", {line: 2});
+ });
+ eq(cm.getValue(), "ax\nby\ncz\n");
+ cm.undo();
+ eq(cm.getValue(), "a\nby\nc\n");
+ cm.undo();
+ eq(cm.getValue(), "a\nb\nc\n");
+ cm.redo(); cm.redo();
+ eq(cm.getValue(), "ax\nby\ncz\n");
+}, {value: "a\nb\nc\n"});
+
testCM("undoSelection", function(cm) {
cm.setSelection({line: 0, ch: 2}, {line: 0, ch: 4});
cm.replaceSelection("");
@@ -362,17 +390,23 @@ testCM("markTextUndo", function(cm) {
bookmark = cm.setBookmark({line: 1, ch: 5});
cm.operation(function(){
cm.replaceRange("foo", {line: 0, ch: 2});
- cm.replaceRange("bar\baz\bug\n", {line: 2, ch: 0}, {line: 3, ch: 0});
+ cm.replaceRange("bar\nbaz\nbug\n", {line: 2, ch: 0}, {line: 3, ch: 0});
});
+ var v1 = cm.getValue();
cm.setValue("");
eq(marker1.find(), null); eq(marker2.find(), null); eq(bookmark.find(), null);
cm.undo();
- eqPos(bookmark.find(), {line: 1, ch: 5});
+ eqPos(bookmark.find(), {line: 1, ch: 5}, "still there");
cm.undo();
var m1Pos = marker1.find(), m2Pos = marker2.find();
eqPos(m1Pos.from, {line: 0, ch: 1}); eqPos(m1Pos.to, {line: 0, ch: 3});
eqPos(m2Pos.from, {line: 0, ch: 0}); eqPos(m2Pos.to, {line: 2, ch: 1});
eqPos(bookmark.find(), {line: 1, ch: 5});
+ cm.redo(); cm.redo();
+ eq(bookmark.find(), null);
+ cm.undo();
+ eqPos(bookmark.find(), {line: 1, ch: 5});
+ eq(cm.getValue(), v1);
}, {value: "1234\n56789\n00\n"});
testCM("markTextStayGone", function(cm) {

0 comments on commit 381d89d

Please sign in to comment.
Something went wrong with that request. Please try again.