Support for 'leftRightIntoCmdGoes' config in the matrix element (#59)

* Add direction-sensitive implementation of leftRightIntoCmdGoes for matrix.

This lets the user cross over the top or bottom of the matrix naturally
without getting caught inside it, using the same config option as for
fractions.
diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js
index 3b347de..ae35e48 100644
--- a/src/commands/math/commands.js
+++ b/src/commands/math/commands.js
@@ -1007,18 +1007,59 @@
     table.toggleClass('mq-rows-1', table.find('tr').length === 1);
     this.relink();
   };
+  // Enter the matrix at the top or bottom row if updown is configured.
+  _.getEntryPoint = function(dir, cursor, updown) {
+    if (updown === 'up') {
+      if (dir === L) {
+        return this.blocks[this.rowSize - 1];
+      } else {
+        return this.blocks[0];
+      }
+    } else { // updown === 'down'
+      if (dir === L) {
+        return this.blocks[this.blocks.length - 1];
+      } else {
+        return this.blocks[this.blocks.length - this.rowSize];
+      }
+    }
+  };
+  // Exit the matrix at the first and last columns if updown is configured.
+  _.atExitPoint = function(dir, cursor) {
+      // Which block are we in?
+      var i = this.blocks.indexOf(cursor.parent);
+      if (dir === L) {
+        // If we're on the left edge and moving left, we should exit.
+        return i % this.rowSize === 0;
+      } else {
+        // If we're on the right edge and moving right, we should exit.
+        return (i + 1) % this.rowSize === 0;
+      }
+  };
+  _.moveTowards = function(dir, cursor, updown) {
+    var entryPoint = updown && this.getEntryPoint(dir, cursor, updown);
+    cursor.insAtDirEnd(-dir, entryPoint || this.ends[-dir]);
+  };
+
   // Set up directional pointers between cells
   _.relink = function() {
     var blocks = this.blocks;
     var rows = [];
     var row, column, cell;
 
+    // The row size will be used by other functions down the track.
+    // Begin by assuming we're a one-row matrix, and we'll overwrite this if we find another row.
+    this.rowSize = blocks.length;
+
     // Use a for loop rather than eachChild
     // as we're still making sure children()
     // is set up properly
     for (var i=0; i<blocks.length; i+=1) {
       cell = blocks[i];
       if (row !== cell.row) {
+        if (cell.row === 1) {
+          // We've just finished iterating the first row.
+          this.rowSize = column;
+        }
         row = cell.row;
         rows[row] = [];
         column = 0;
@@ -1296,5 +1337,11 @@
       // called when last cell gets deleted
       return super_.deleteOutOf.apply(self, args);
     });
-  }
+  };
+  _.moveOutOf = function(dir, cursor, updown) {
+    var atExitPoint = updown && this.parent.atExitPoint(dir, cursor);
+    // Step out of the matrix if we've moved past an edge column
+    if (!atExitPoint && this[dir]) cursor.insAtDirEnd(-dir, this[dir]);
+    else cursor.insDirOf(dir, this.parent);
+  };
 });
diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js
index 33d4163..7e8a400 100644
--- a/test/unit/typing.test.js
+++ b/test/unit/typing.test.js
@@ -1119,6 +1119,62 @@
       assertLatex('\\begin{matrix}&&\\\\b&c&\\\\a&d&\\end{matrix}');
     });
 
+    test('passes over matrices when leftRightIntoCmdGoes is set to up', function() {
+      mq.config({ leftRightIntoCmdGoes: 'up' });
+
+      // 1 2 3
+      // 4 5 6
+      // 7 8 9
+      mq.write('\\begin{matrix}1&2&3\\\\4&5&6\\\\7&8&9\\end{matrix}');
+
+      mq.keystroke('Left Left Left Left Left Left Left').typedText('a')
+        .keystroke('Right Right Right Right Right Right Right').typedText('b')
+        .keystroke('Left Left Left Left').typedText('c');
+
+      // It should've entered the top of the matrix and exited at either end, leading to
+      //   1  2c 3
+      // a 4  5  6 b
+      //   7  8  9
+      assertLatex('a\\begin{matrix}1&2c&3\\\\4&5&6\\\\7&8&9\\end{matrix}b');
+    });
+
+    test('passes under matrices when leftRightIntoCmdGoes is set to down', function() {
+      mq.config({ leftRightIntoCmdGoes: 'down' });
+
+      // 1 2 3
+      // 4 5 6
+      // 7 8 9
+      mq.write('\\begin{matrix}1&2&3\\\\4&5&6\\\\7&8&9\\end{matrix}');
+
+      mq.keystroke('Left Left Left Left Left Left Left').typedText('a')
+        .keystroke('Right Right Right Right Right Right Right').typedText('b')
+        .keystroke('Left Left Left Left').typedText('c');
+
+      // It should've entered the bottom of the matrix and exited at either end, leading to
+      //   1  2  3
+      // a 4  5  6 b
+      //   7  8c 9
+      assertLatex('a\\begin{matrix}1&2&3\\\\4&5&6\\\\7&8c&9\\end{matrix}b');
+    });
+
+    test('exits out of matrices on their edges when leftRightIntoCmdGoes is set', function() {
+      mq.config({ leftRightIntoCmdGoes: 'up' });
+
+      // 1 2 3
+      // 4 5 6
+      // 7 8 9
+      mq.write('\\begin{matrix}1&2&3\\\\4&5&6\\\\7&8&9\\end{matrix}');
+
+      mq.keystroke('Left Left Left Down').typedText('a')
+        .keystroke('Right Right Right').typedText('b')
+
+      // It should've entered the top of the matrix and exited out the side, leading to
+      // 1  2  3
+      // 4  5a 6 b
+      // 7  8  9
+      assertLatex('\\begin{matrix}1&2&3\\\\4&5a&6\\\\7&8&9\\end{matrix}b');
+    });
+
     test('delete key removes empty matrix row/column', function() {
       mq.write('\\begin{matrix}a&&b\\\\&c&d\\\\&e&f\\end{matrix}');