go-tour: add error highlighting

LGTM=adg
R=adg
CC=golang-codereviews
https://golang.org/cl/170040043
diff --git a/static/css/app.css b/static/css/app.css
index c63cc6c..2751f2f 100755
--- a/static/css/app.css
+++ b/static/css/app.css
@@ -259,6 +259,13 @@
     font-family:'Inconsolata', monospace;
     line-height: 1.2em;
 }
+.CodeMirror-code > .line-error {
+    background: #FF8080;
+}
+.CodeMirror-code > .line-error .CodeMirror-linenumber {
+    color: #FF5555;
+    font-weight: bolder;
+}
 #file-editor .CodeMirror-gutters {
     width: 32px;
 }
diff --git a/static/js/services.js b/static/js/services.js
index fa2e55a..233d876 100755
--- a/static/js/services.js
+++ b/static/js/services.js
@@ -31,14 +31,28 @@
 ]).
 
 // Running code
-factory('run', ['$window',
-    function(win) {
+factory('run', ['$window', 'editor',
+    function(win, editor) {
+        var writeInterceptor = function(writer) {
+            return function(write) {
+                if (write.Kind == 'stderr') {
+                    var lines = write.Body.split('\n');
+                    for (var i in lines) {
+                        var match = lines[i].match(/prog\.go:([0-9]+): ([^\n]*)/);
+                        if (match !== null) {
+                            editor.highlight(match[1], match[2]);
+                        }
+                    }
+                }
+                writer(write);
+            };
+        };
         return function(code, output, options) {
             // PlaygroundOutput is defined in playground.js which is prepended
             // to the generated script.js in gotour/tour.go.
             // The next line removes the jshint warning.
             // global PlaygroundOutput
-            win.transport.Run(code, PlaygroundOutput(output), options);
+            win.transport.Run(code, writeInterceptor(PlaygroundOutput(output)), options);
         };
     }
 ]).
@@ -108,7 +122,17 @@
                 };
                 set();
             },
+            highlight: function(line, message) {
+                $('.CodeMirror-code > div:nth-child(' + line + ')')
+                    .addClass('line-error').attr('title', message);
+            },
+            onChange: function() {
+                $('.line-error').removeClass('line-error').attr('title', null);
+            }
         };
+        // Set in the window so the onChange function in the codemirror config
+        // can call it.
+        win.codeChanged = ctx.onChange;
         return ctx;
     }
 ]).
@@ -171,7 +195,8 @@
                 }
                 moduleQ.resolve(modules);
                 lessonQ.resolve(lessons);
-            }, function(error) {
+            },
+            function(error) {
                 $log.error('error loading lessons : ', error);
                 moduleQ.reject(error);
                 lessonQ.reject(error);
diff --git a/static/js/values.js b/static/js/values.js
index 8ec323c..520f280 100644
--- a/static/js/values.js
+++ b/static/js/values.js
@@ -72,6 +72,11 @@
             'PageUp': function() {
                 return false;
             },
+        },
+        // TODO: is there a better way to do this?
+        // AngularJS values can't depend on factories.
+        onChange: function() {
+            if (window.codeChanged !== null) window.codeChanged();
         }
     }
 });