Merge "Async load popular modes into CodeMirror"
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index 14008e6..c8f40b2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -85,8 +85,6 @@
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtorm.client.KeyUtil;
 
-import net.codemirror.lib.CodeMirror;
-
 import java.util.ArrayList;
 
 public class Gerrit implements EntryPoint {
@@ -368,7 +366,6 @@
 
     initHostname();
     Window.setTitle(M.windowTitle1(myHost));
-    CodeMirror.install();
 
     final HostPageDataService hpd = GWT.create(HostPageDataService.class);
     hpd.load(new GerritCallback<HostPageData>() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CodeMirrorDemo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CodeMirrorDemo.java
index d0398daa..af149ab 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CodeMirrorDemo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CodeMirrorDemo.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.diff;
 
+import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -25,6 +27,7 @@
 
 import net.codemirror.lib.CodeMirror;
 import net.codemirror.lib.Configuration;
+import net.codemirror.lib.ModeInjector;
 
 public class CodeMirrorDemo extends Screen {
   private static final int HEADER_FOOTER = 60 + 15 * 2 + 38;
@@ -55,16 +58,30 @@
   protected void onLoad() {
     super.onLoad();
 
+    CallbackGroup group = new CallbackGroup();
+    CodeMirror.initLibrary(group.add(new GerritCallback<Void>() {
+      @Override
+      public void onSuccess(Void result) {
+      }
+    }));
     DiffApi.diff(revision, path)
       .base(base)
       .wholeFile()
       .ignoreWhitespace(DiffApi.IgnoreWhitespace.NONE)
-      .get(new ScreenLoadCallback<DiffInfo>(this) {
+      .get(group.add(new GerritCallback<DiffInfo>() {
         @Override
-        protected void preDisplay(DiffInfo diff) {
-          display(diff);
+        public void onSuccess(final DiffInfo diff) {
+          new ModeInjector()
+            .add(getContentType(diff.meta_a()))
+            .add(getContentType(diff.meta_b()))
+            .inject(new ScreenLoadCallback<Void>(CodeMirrorDemo.this){
+              @Override
+              protected void preDisplay(Void result) {
+                display(diff);
+              }
+            });
         }
-      });
+      }));
   }
 
   @Override
@@ -92,10 +109,8 @@
       .set("readOnly", true)
       .set("lineNumbers", true)
       .set("tabSize", 2)
+      .set("mode", getContentType(diff.meta_b()))
       .set("value", diff.text_b());
-    if (diff.meta_b() != null && diff.meta_b().content_type() != null) {
-      cfg.set("mode", diff.meta_b().content_type());
-    }
 
     cm = CodeMirror.create(editorContainer.getElement(), cfg);
     cm.setWidth("100%");
@@ -108,4 +123,10 @@
       }
     });
   }
+
+  private static String getContentType(DiffInfo.FileMeta meta) {
+    return meta != null && meta.content_type() != null
+        ? ModeInjector.getContentType(meta.content_type())
+        : null;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/CodeMirror.gwt.xml b/gerrit-gwtui/src/main/java/net/codemirror/CodeMirror.gwt.xml
index 20e413c..64ac16d 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/CodeMirror.gwt.xml
+++ b/gerrit-gwtui/src/main/java/net/codemirror/CodeMirror.gwt.xml
@@ -17,4 +17,5 @@
   <inherits name='com.google.gwt.logging.Logging'/>
   <inherits name='com.google.gwt.resources.Resources'/>
   <source path='lib'/>
+  <source path='mode'/>
 </module>
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
index e365595..11d10df 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
@@ -15,18 +15,8 @@
 package net.codemirror.lib;
 
 import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.ScriptElement;
-import com.google.gwt.dom.client.StyleInjector;
-import com.google.gwt.resources.client.ExternalTextResource;
-import com.google.gwt.resources.client.ResourceCallback;
-import com.google.gwt.resources.client.ResourceException;
-import com.google.gwt.resources.client.TextResource;
-import com.google.gwt.safehtml.shared.SafeUri;
-
-import java.util.logging.Level;
-import java.util.logging.Logger;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 
 /**
  * Glue to connect CodeMirror to be callable from GWT.
@@ -34,6 +24,10 @@
  * @link http://codemirror.net/doc/manual.html#api
  */
 public class CodeMirror extends JavaScriptObject {
+  public static void initLibrary(AsyncCallback<Void> cb) {
+    Loader.initLibrary(cb);
+  }
+
   public static native CodeMirror create(Element parent, Configuration cfg) /*-{
     return $wnd.CodeMirror(parent, cfg);
   }-*/;
@@ -48,42 +42,6 @@
   public final native void refresh() /*-{ this.refresh(); }-*/;
   public final native Element getWrapperElement() /*-{ return this.getWrapperElement(); }-*/;
 
-  public static void install() {
-    asyncInjectCss(Lib.I.css());
-    asyncInjectScript(Lib.I.js().getSafeUri());
-  }
-
-  private static void asyncInjectCss(ExternalTextResource css) {
-    try {
-      css.getText(new ResourceCallback<TextResource>() {
-        @Override
-        public void onSuccess(TextResource resource) {
-          StyleInjector.inject(resource.getText());
-        }
-
-        @Override
-        public void onError(ResourceException e) {
-          error(e);
-        }
-      });
-    } catch (ResourceException e) {
-      error(e);
-    }
-  }
-
-  private static void asyncInjectScript(SafeUri uri) {
-    ScriptElement script = Document.get().createScriptElement();
-    script.setSrc(uri.asString());
-    script.setLang("javascript");
-    script.setType("text/javascript");
-    Document.get().getBody().appendChild(script);
-  }
-
-  private static void error(ResourceException e) {
-    Logger log = Logger.getLogger("net.codemirror");
-    log.log(Level.SEVERE, "Cannot fetch CSS", e);
-  }
-
   protected CodeMirror() {
   }
 }
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java
new file mode 100644
index 0000000..0637a1b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.lib;
+
+import com.google.gwt.core.client.Callback;
+import com.google.gwt.core.client.ScriptInjector;
+import com.google.gwt.dom.client.ScriptElement;
+import com.google.gwt.dom.client.StyleInjector;
+import com.google.gwt.resources.client.ExternalTextResource;
+import com.google.gwt.resources.client.ResourceCallback;
+import com.google.gwt.resources.client.ResourceException;
+import com.google.gwt.resources.client.TextResource;
+import com.google.gwt.safehtml.shared.SafeUri;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+class Loader {
+  private static native boolean isLibLoaded()
+  /*-{ return $wnd.hasOwnProperty('CodeMirror'); }-*/;
+
+  static void initLibrary(AsyncCallback<Void> cb) {
+    if (isLibLoaded()) {
+      cb.onSuccess(null);
+    } else {
+      injectCss(Lib.I.css());
+      injectScript(Lib.I.js().getSafeUri(), cb);
+    }
+  }
+
+  private static void injectCss(ExternalTextResource css) {
+    try {
+      css.getText(new ResourceCallback<TextResource>() {
+        @Override
+        public void onSuccess(TextResource resource) {
+          StyleInjector.inject(resource.getText());
+        }
+
+        @Override
+        public void onError(ResourceException e) {
+          error(e);
+        }
+      });
+    } catch (ResourceException e) {
+      error(e);
+    }
+  }
+
+  static void injectScript(SafeUri js, final AsyncCallback<Void> callback) {
+    final ScriptElement[] script = new ScriptElement[1];
+    script[0] = ScriptInjector.fromUrl(js.asString())
+      .setWindow(ScriptInjector.TOP_WINDOW)
+      .setCallback(new Callback<Void, Exception>() {
+        @Override
+        public void onSuccess(Void result) {
+          script[0].removeFromParent();
+          callback.onSuccess(result);
+        }
+
+        @Override
+        public void onFailure(Exception reason) {
+          error(reason);
+          callback.onFailure(reason);
+        }
+       })
+      .inject()
+      .cast();
+  }
+
+  private static void error(Exception e) {
+    Logger log = Logger.getLogger("net.codemirror");
+    log.log(Level.SEVERE, "Cannot load portions of CodeMirror", e);
+  }
+
+  private Loader() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java
new file mode 100644
index 0000000..86b3776
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java
@@ -0,0 +1,179 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.lib;
+
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.resources.client.DataResource;
+import com.google.gwt.safehtml.shared.SafeUri;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import net.codemirror.mode.Modes;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class ModeInjector {
+  /** Map of server content type to CodeMiror mode or content type. */
+  private static final Map<String, String> mimeAlias;
+
+  /** Map of content type "text/x-java" to mode name "clike". */
+  private static final Map<String, String> mimeModes;
+
+  /** Map of names such as "clike" to URI for code download. */
+  private static final Map<String, SafeUri> modeUris;
+
+  static {
+    DataResource[] all = {
+      Modes.I.clike(),
+      Modes.I.css(),
+      Modes.I.go(),
+      Modes.I.htmlmixed(),
+      Modes.I.javascript(),
+      Modes.I.properties(),
+      Modes.I.python(),
+      Modes.I.shell(),
+      Modes.I.sql(),
+      Modes.I.velocity(),
+      Modes.I.xml(),
+    };
+
+    mimeAlias = new HashMap<String, String>();
+    mimeModes = new HashMap<String, String>();
+    modeUris = new HashMap<String, SafeUri>();
+
+    for (DataResource m : all) {
+      modeUris.put(m.getName(), m.getSafeUri());
+    }
+    parseModeMap();
+  }
+
+  private static void parseModeMap() {
+    String mode = null;
+    for (String line : Modes.I.mode_map().getText().split("\n")) {
+      int eq = line.indexOf('=');
+      if (0 < eq) {
+        mimeAlias.put(
+          line.substring(0, eq).trim(),
+          line.substring(eq + 1).trim());
+      } else if (line.endsWith(":")) {
+        String n = line.substring(0, line.length() - 1);
+        if (modeUris.containsKey(n)) {
+          mode = n;
+        }
+      } else if (mode != null && line.contains("/")) {
+        mimeModes.put(line, mode);
+      } else {
+        mode = null;
+      }
+    }
+  }
+
+  public static String getContentType(String mode) {
+    String real = mode != null ? mimeAlias.get(mode) : null;
+    return real != null ? real : mode;
+  }
+
+  private static native boolean isModeLoaded(String n)
+  /*-{ return $wnd.CodeMirror.modes.hasOwnProperty(n); }-*/;
+
+  private static native boolean isMimeLoaded(String n)
+  /*-{ return $wnd.CodeMirror.mimeModes.hasOwnProperty(n); }-*/;
+
+  private static native JsArrayString getDependencies(String n)
+  /*-{ return $wnd.CodeMirror.modes[n].dependencies || []; }-*/;
+
+  private final Set<String> loading = new HashSet<String>(4);
+  private int pending;
+  private AsyncCallback<Void> appCallback;
+
+  public ModeInjector add(String name) {
+    if (name == null || isModeLoaded(name) || isMimeLoaded(name)) {
+      return this;
+    }
+
+    String mode = mimeModes.get(name);
+    if (mode == null) {
+      mode = name;
+    }
+
+    SafeUri uri = modeUris.get(mode);
+    if (uri == null) {
+      Logger.getLogger("net.codemirror").log(
+        Level.WARNING,
+        "CodeMirror mode " + mode + " not configured.");
+      return this;
+    }
+
+    loading.add(mode);
+    return this;
+  }
+
+  public void inject(AsyncCallback<Void> appCallback) {
+    this.appCallback = appCallback;
+    for (String mode : loading) {
+      beginLoading(mode);
+    }
+    if (pending == 0) {
+      appCallback.onSuccess(null);
+    }
+  }
+
+  private void beginLoading(final String mode) {
+    pending++;
+    Loader.injectScript(
+      modeUris.get(mode),
+      new AsyncCallback<Void>() {
+        @Override
+        public void onSuccess(Void result) {
+          pending--;
+          ensureDependenciesAreLoaded(mode);
+          if (pending == 0) {
+            appCallback.onSuccess(null);
+          }
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+          if (--pending == 0) {
+            appCallback.onFailure(caught);
+          }
+        }
+      });
+  }
+
+  private void ensureDependenciesAreLoaded(String mode) {
+    JsArrayString deps = getDependencies(mode);
+    for (int i = 0; i < deps.length(); i++) {
+      String d = deps.get(i);
+      if (loading.contains(d) || isModeLoaded(d)) {
+        continue;
+      }
+
+      SafeUri uri = modeUris.get(d);
+      if (uri == null) {
+        Logger.getLogger("net.codemirror").log(
+          Level.SEVERE,
+          "CodeMirror mode " + mode + " needs " + d);
+        continue;
+      }
+
+      beginLoading(d);
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
new file mode 100644
index 0000000..e2d3e3c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.mode;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.DataResource;
+import com.google.gwt.resources.client.DataResource.DoNotEmbed;
+import com.google.gwt.resources.client.TextResource;
+
+public interface Modes extends ClientBundle {
+  public static final Modes I = GWT.create(Modes.class);
+
+  @Source("mode_map") TextResource mode_map();
+  @Source("clike/clike.js") @DoNotEmbed DataResource clike();
+  @Source("css/css.js") @DoNotEmbed DataResource css();
+  @Source("go/go.js") @DoNotEmbed DataResource go();
+  @Source("htmlmixed/htmlmixed.js") @DoNotEmbed DataResource htmlmixed();
+  @Source("javascript/javascript.js") @DoNotEmbed DataResource javascript();
+  @Source("properties/properties.js") @DoNotEmbed DataResource properties();
+  @Source("python/python.js") @DoNotEmbed DataResource python();
+  @Source("shell/shell.js") @DoNotEmbed DataResource shell();
+  @Source("sql/sql.js") @DoNotEmbed DataResource sql();
+  @Source("velocity/velocity.js") @DoNotEmbed DataResource velocity();
+  @Source("xml/xml.js") @DoNotEmbed DataResource xml();
+
+  // When adding a resource, update static initializer in ModeInjector.
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map b/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map
new file mode 100644
index 0000000..bcb615a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map
@@ -0,0 +1,54 @@
+clike:
+text/x-csrc
+text/x-c
+text/x-chdr
+text/x-c++src
+text/x-c++hdr
+text/x-java
+text/x-csharp
+text/x-scala
+
+css:
+text/css
+text/x-scss
+
+go:
+text/x-go
+
+htmlmixed:
+text/html
+
+javascript:
+text/javascript
+text/ecmascript
+application/javascript
+application/ecmascript
+application/json
+application/x-json
+text/typescript
+application/typescript
+
+properties:
+text/x-ini
+text/x-properties
+
+python:
+text/x-python
+
+shell:
+text/x-sh
+
+sql:
+text/x-sql
+text/x-mariadb
+text/x-mysql
+text/x-plsql
+
+velocity:
+text/velocity
+
+xml:
+text/xml
+application/xml
+
+text/x-java-source = text/x-java
diff --git a/lib/LICENSE-codemirror b/lib/LICENSE-codemirror
index cdb16cd..7df9fec 100644
--- a/lib/LICENSE-codemirror
+++ b/lib/LICENSE-codemirror
@@ -17,3 +17,28 @@
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
+----
+
+codemirror Python mode:
+----
+The MIT License
+
+Copyright (c) 2010 Timothy Farrell
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.