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
This commit is contained in:
Shawn Pearce
2013-05-21 10:51:37 -07:00
parent 75110c1628
commit 73f51d91ba
9 changed files with 422 additions and 57 deletions

View File

@@ -85,8 +85,6 @@ import com.google.gwtjsonrpc.client.impl.ResultDeserializer;
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 @@ public class Gerrit implements EntryPoint {
initHostname();
Window.setTitle(M.windowTitle1(myHost));
CodeMirror.install();
final HostPageDataService hpd = GWT.create(HostPageDataService.class);
hpd.load(new GerritCallback<HostPageData>() {

View File

@@ -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 com.google.gwt.user.client.ui.FlowPanel;
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 @@ public class CodeMirrorDemo extends Screen {
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 @@ public class CodeMirrorDemo extends Screen {
.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 @@ public class CodeMirrorDemo extends Screen {
}
});
}
private static String getContentType(DiffInfo.FileMeta meta) {
return meta != null && meta.content_type() != null
? ModeInjector.getContentType(meta.content_type())
: null;
}
}

View File

@@ -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>

View File

@@ -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 @@ import java.util.logging.Logger;
* @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 class CodeMirror extends JavaScriptObject {
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() {
}
}

View File

@@ -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() {
}
}

View File

@@ -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);
}
}
}

View File

@@ -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.
}

View File

@@ -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

View File

@@ -17,3 +17,28 @@ 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.
----
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.