Async load popular modes into CodeMirror Dynamically load modes after the type of a file has been discovered from the server. This allows browers to download only the segments of CodeMirror they need to render the current file(s) being viewed. Change-Id: I34c037199cd01bf65b051400999d141eb0524cd8
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