EditScreen: Show the base version of the file in a CodeMirror merge view
This adds a merge view to the inline editing screen so that users can diff against the patch set the change edit is based on while editing. This new functionality makes use of CodeMirror's merge view addon, which requires Google's diff-match-patch library to be loaded. The library implements an algorithm based on the Myers diff in JavaScript on the client side, so there is no extra usage of Gerrit's DiffApi. The minified diff-match-patch library is bundled with CodeMirror's merge view addon during the build process. By default, the merge view is disabled. This can be changed via EditPreferencesBox. Also added a checkbox to the EditScreen header to toggle the view. The loading of the content of the base version is lazy - it is only loaded when the merge view is actually used. The documentation and tutorial on this new feature will be added in later changes. Screenshot: https://i.imgur.com/uNMNzjM.png Change-Id: I5eb4af9b67bbfcb0b149965b3c818c1f6118e6de
This commit is contained in:
parent
551ad0c20d
commit
ecaf9b075c
gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts
gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client
gerrit-gwtui/src/main/java
com/google/gerrit/client
account
editor
net/codemirror
lib/codemirror
@ -43,6 +43,7 @@ public class EditPreferencesIT extends AbstractDaemonTest {
|
||||
assertThat(out.hideLineNumbers).isNull();
|
||||
assertThat(out.matchBrackets).isTrue();
|
||||
assertThat(out.autoCloseBrackets).isNull();
|
||||
assertThat(out.showBase).isNull();
|
||||
assertThat(out.theme).isEqualTo(Theme.DEFAULT);
|
||||
assertThat(out.keyMapType).isEqualTo(KeyMapType.DEFAULT);
|
||||
|
||||
@ -58,6 +59,7 @@ public class EditPreferencesIT extends AbstractDaemonTest {
|
||||
out.hideLineNumbers = true;
|
||||
out.matchBrackets = false;
|
||||
out.autoCloseBrackets = true;
|
||||
out.showBase = true;
|
||||
out.theme = Theme.TWILIGHT;
|
||||
out.keyMapType = KeyMapType.EMACS;
|
||||
|
||||
@ -92,6 +94,7 @@ public class EditPreferencesIT extends AbstractDaemonTest {
|
||||
assertThat(out.hideLineNumbers).isEqualTo(in.hideLineNumbers);
|
||||
assertThat(out.matchBrackets).isNull();
|
||||
assertThat(out.autoCloseBrackets).isEqualTo(in.autoCloseBrackets);
|
||||
assertThat(out.showBase).isEqualTo(in.showBase);
|
||||
assertThat(out.theme).isEqualTo(in.theme);
|
||||
assertThat(out.keyMapType).isEqualTo(in.keyMapType);
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ public class EditPreferencesInfo {
|
||||
public Boolean hideLineNumbers;
|
||||
public Boolean matchBrackets;
|
||||
public Boolean autoCloseBrackets;
|
||||
public Boolean showBase;
|
||||
public Theme theme;
|
||||
public KeyMapType keyMapType;
|
||||
|
||||
@ -43,6 +44,7 @@ public class EditPreferencesInfo {
|
||||
i.hideLineNumbers = false;
|
||||
i.matchBrackets = true;
|
||||
i.autoCloseBrackets = false;
|
||||
i.showBase = false;
|
||||
i.theme = Theme.DEFAULT;
|
||||
i.keyMapType = KeyMapType.DEFAULT;
|
||||
return i;
|
||||
|
@ -33,6 +33,7 @@ public class EditPreferences extends JavaScriptObject {
|
||||
p.hideLineNumbers(in.hideLineNumbers);
|
||||
p.matchBrackets(in.matchBrackets);
|
||||
p.autoCloseBrackets(in.autoCloseBrackets);
|
||||
p.showBase(in.showBase);
|
||||
p.theme(in.theme);
|
||||
p.keyMapType(in.keyMapType);
|
||||
return p;
|
||||
@ -50,6 +51,7 @@ public class EditPreferences extends JavaScriptObject {
|
||||
p.hideLineNumbers = hideLineNumbers();
|
||||
p.matchBrackets = matchBrackets();
|
||||
p.autoCloseBrackets = autoCloseBrackets();
|
||||
p.showBase = showBase();
|
||||
p.theme = theme();
|
||||
p.keyMapType = keyMapType();
|
||||
return p;
|
||||
@ -76,6 +78,7 @@ public class EditPreferences extends JavaScriptObject {
|
||||
public final native void hideLineNumbers(boolean s) /*-{ this.hide_line_numbers = s }-*/;
|
||||
public final native void matchBrackets(boolean m) /*-{ this.match_brackets = m }-*/;
|
||||
public final native void autoCloseBrackets(boolean c) /*-{ this.auto_close_brackets = c }-*/;
|
||||
public final native void showBase(boolean s) /*-{ this.show_base = s }-*/;
|
||||
|
||||
public final Theme theme() {
|
||||
String s = themeRaw();
|
||||
@ -112,6 +115,7 @@ public class EditPreferences extends JavaScriptObject {
|
||||
public final native boolean hideLineNumbers() /*-{ return this.hide_line_numbers || false }-*/;
|
||||
public final native boolean matchBrackets() /*-{ return this.match_brackets || false }-*/;
|
||||
public final native boolean autoCloseBrackets() /*-{ return this.auto_close_brackets || false }-*/;
|
||||
public final native boolean showBase() /*-{ return this.show_base || false }-*/;
|
||||
private native int get(String n, int d) /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
|
||||
|
||||
protected EditPreferences() {
|
||||
|
@ -69,6 +69,7 @@ public class EditPreferencesBox extends Composite {
|
||||
@UiField ToggleButton lineNumbers;
|
||||
@UiField ToggleButton matchBrackets;
|
||||
@UiField ToggleButton autoCloseBrackets;
|
||||
@UiField ToggleButton showBase;
|
||||
@UiField ListBox theme;
|
||||
@UiField ListBox keyMap;
|
||||
@UiField Button apply;
|
||||
@ -104,6 +105,7 @@ public class EditPreferencesBox extends Composite {
|
||||
lineNumbers.setValue(prefs.hideLineNumbers());
|
||||
matchBrackets.setValue(prefs.matchBrackets());
|
||||
autoCloseBrackets.setValue(prefs.autoCloseBrackets());
|
||||
showBase.setValue(prefs.showBase());
|
||||
setTheme(prefs.theme());
|
||||
setKeyMapType(prefs.keyMapType());
|
||||
}
|
||||
@ -114,7 +116,7 @@ public class EditPreferencesBox extends Composite {
|
||||
if (v != null && v.length() > 0) {
|
||||
prefs.tabSize(Math.max(1, Integer.parseInt(v)));
|
||||
if (view != null) {
|
||||
view.getEditor().setOption("tabSize", v);
|
||||
view.setOption("tabSize", v);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -149,7 +151,7 @@ public class EditPreferencesBox extends Composite {
|
||||
// don't let user shoot himself in the foot.
|
||||
prefs.cursorBlinkRate(Math.max(0, Integer.parseInt(v)));
|
||||
if (view != null) {
|
||||
view.getEditor().setOption("cursorBlinkRate", prefs.cursorBlinkRate());
|
||||
view.setOption("cursorBlinkRate", prefs.cursorBlinkRate());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -159,7 +161,7 @@ public class EditPreferencesBox extends Composite {
|
||||
prefs.hideTopMenu(!e.getValue());
|
||||
if (view != null) {
|
||||
Gerrit.setHeaderVisible(!prefs.hideTopMenu());
|
||||
view.resizeCodeMirror();
|
||||
view.adjustHeight();
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,7 +201,7 @@ public class EditPreferencesBox extends Composite {
|
||||
void onMatchBrackets(ValueChangeEvent<Boolean> e) {
|
||||
prefs.matchBrackets(e.getValue());
|
||||
if (view != null) {
|
||||
view.getEditor().setOption("matchBrackets", prefs.matchBrackets());
|
||||
view.setOption("matchBrackets", prefs.matchBrackets());
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,6 +213,15 @@ public class EditPreferencesBox extends Composite {
|
||||
}
|
||||
}
|
||||
|
||||
@UiHandler("showBase")
|
||||
void onShowBase(ValueChangeEvent<Boolean> e) {
|
||||
Boolean value = e.getValue();
|
||||
prefs.showBase(value);
|
||||
if (view != null) {
|
||||
view.showBase.setValue(value, true);
|
||||
}
|
||||
}
|
||||
|
||||
@UiHandler("theme")
|
||||
void onTheme(@SuppressWarnings("unused") ChangeEvent e) {
|
||||
final Theme newTheme = Theme.valueOf(theme.getValue(theme.getSelectedIndex()));
|
||||
@ -219,13 +230,7 @@ public class EditPreferencesBox extends Composite {
|
||||
ThemeLoader.loadTheme(newTheme, new GerritCallback<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void result) {
|
||||
view.getEditor().operation(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
String t = newTheme.name().toLowerCase();
|
||||
view.getEditor().setOption("theme", t);
|
||||
}
|
||||
});
|
||||
view.setTheme(newTheme);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -237,7 +242,7 @@ public class EditPreferencesBox extends Composite {
|
||||
keyMap.getValue(keyMap.getSelectedIndex()));
|
||||
prefs.keyMapType(keyMapType);
|
||||
if (view != null) {
|
||||
view.getEditor().setOption("keyMap", keyMapType.name().toLowerCase());
|
||||
view.setOption("keyMap", keyMapType.name().toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -244,6 +244,13 @@ limitations under the License.
|
||||
<g:downFace><ui:msg>On</ui:msg></g:downFace>
|
||||
</g:ToggleButton></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><ui:msg>Show Base Version</ui:msg></th>
|
||||
<td><g:ToggleButton ui:field='showBase'>
|
||||
<g:upFace><ui:msg>Hide</ui:msg></g:upFace>
|
||||
<g:downFace><ui:msg>Show</ui:msg></g:downFace>
|
||||
</g:ToggleButton></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
|
@ -43,6 +43,7 @@ import com.google.gerrit.client.ui.InlineHyperlink;
|
||||
import com.google.gerrit.client.ui.Screen;
|
||||
import com.google.gerrit.common.PageLinks;
|
||||
import com.google.gerrit.extensions.client.KeyMapType;
|
||||
import com.google.gerrit.extensions.client.Theme;
|
||||
import com.google.gerrit.reviewdb.client.Patch;
|
||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||
import com.google.gwt.core.client.GWT;
|
||||
@ -50,11 +51,14 @@ import com.google.gwt.core.client.JsArray;
|
||||
import com.google.gwt.core.client.Scheduler;
|
||||
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
|
||||
import com.google.gwt.dom.client.Element;
|
||||
import com.google.gwt.dom.client.Style.Unit;
|
||||
import com.google.gwt.event.dom.client.ClickEvent;
|
||||
import com.google.gwt.event.dom.client.KeyPressEvent;
|
||||
import com.google.gwt.event.logical.shared.ResizeEvent;
|
||||
import com.google.gwt.event.logical.shared.ResizeHandler;
|
||||
import com.google.gwt.event.logical.shared.ValueChangeEvent;
|
||||
import com.google.gwt.event.shared.HandlerRegistration;
|
||||
import com.google.gwt.resources.client.CssResource;
|
||||
import com.google.gwt.uibinder.client.UiBinder;
|
||||
import com.google.gwt.uibinder.client.UiField;
|
||||
import com.google.gwt.uibinder.client.UiHandler;
|
||||
@ -63,17 +67,21 @@ import com.google.gwt.user.client.Window.ClosingEvent;
|
||||
import com.google.gwt.user.client.Window.ClosingHandler;
|
||||
import com.google.gwt.user.client.rpc.AsyncCallback;
|
||||
import com.google.gwt.user.client.ui.Button;
|
||||
import com.google.gwt.user.client.ui.CheckBox;
|
||||
import com.google.gwt.user.client.ui.FlowPanel;
|
||||
import com.google.gwt.user.client.ui.HTMLPanel;
|
||||
import com.google.gwt.user.client.ui.ImageResourceRenderer;
|
||||
import com.google.gwtexpui.globalkey.client.GlobalKey;
|
||||
import com.google.gwtexpui.safehtml.client.SafeHtml;
|
||||
|
||||
import net.codemirror.addon.AddonInjector;
|
||||
import net.codemirror.addon.Addons;
|
||||
import net.codemirror.lib.CodeMirror;
|
||||
import net.codemirror.lib.CodeMirror.ChangesHandler;
|
||||
import net.codemirror.lib.CodeMirror.CommandRunner;
|
||||
import net.codemirror.lib.Configuration;
|
||||
import net.codemirror.lib.KeyMap;
|
||||
import net.codemirror.lib.MergeView;
|
||||
import net.codemirror.lib.Pos;
|
||||
import net.codemirror.mode.ModeInfo;
|
||||
import net.codemirror.mode.ModeInjector;
|
||||
@ -85,14 +93,23 @@ public class EditScreen extends Screen {
|
||||
interface Binder extends UiBinder<HTMLPanel, EditScreen> {}
|
||||
private static final Binder uiBinder = GWT.create(Binder.class);
|
||||
|
||||
interface Style extends CssResource {
|
||||
String fullWidth();
|
||||
String base();
|
||||
String hideBase();
|
||||
}
|
||||
|
||||
private final PatchSet.Id base;
|
||||
private final PatchSet.Id revision;
|
||||
private final String path;
|
||||
private final int startLine;
|
||||
private EditPreferences prefs;
|
||||
private EditPreferencesAction editPrefsAction;
|
||||
private CodeMirror cm;
|
||||
private MergeView mv;
|
||||
private CodeMirror cmBase;
|
||||
private CodeMirror cmEdit;
|
||||
private HttpResponse<NativeString> content;
|
||||
private HttpResponse<NativeString> baseContent;
|
||||
private EditFileInfo editFileInfo;
|
||||
private JsArray<DiffWebLinkInfo> diffLinks;
|
||||
|
||||
@ -103,9 +120,11 @@ public class EditScreen extends Screen {
|
||||
@UiField Element cursLine;
|
||||
@UiField Element cursCol;
|
||||
@UiField Element dirty;
|
||||
@UiField CheckBox showBase;
|
||||
@UiField Button close;
|
||||
@UiField Button save;
|
||||
@UiField Element editor;
|
||||
@UiField Style style;
|
||||
|
||||
private HandlerRegistration resizeHandler;
|
||||
private HandlerRegistration closeHandler;
|
||||
@ -145,9 +164,21 @@ public class EditScreen extends Screen {
|
||||
public void onSuccess(Void result) {
|
||||
// Load theme after CM library to ensure theme can override CSS.
|
||||
ThemeLoader.loadTheme(prefs.theme(), themeCallback);
|
||||
|
||||
group2.done();
|
||||
group3.done();
|
||||
|
||||
new AddonInjector().add(Addons.I.merge_bundled().getName()).inject(
|
||||
new AsyncCallback<Void>() {
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(Void result) {
|
||||
if (!prefs.showBase() || revision.get() > 0) {
|
||||
group3.done();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -180,13 +211,30 @@ public class EditScreen extends Screen {
|
||||
public void onFailure(Throwable e) {
|
||||
}
|
||||
}));
|
||||
|
||||
if (prefs.showBase()) {
|
||||
ChangeEditApi.get(revision, path, true /* base */,
|
||||
group1.addFinal(new HttpCallback<NativeString>() {
|
||||
@Override
|
||||
public void onSuccess(HttpResponse<NativeString> fc) {
|
||||
baseContent = fc;
|
||||
group3.done();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable e) {
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
group1.done();
|
||||
}
|
||||
} else {
|
||||
// TODO(davido): We probably want to create dedicated GET EditScreenMeta
|
||||
// REST endpoint. Abuse GET diff for now, as it retrieves links we need.
|
||||
DiffApi.diff(revision, path)
|
||||
.base(base)
|
||||
.webLinksOnly()
|
||||
.get(group1.add(new AsyncCallback<DiffInfo>() {
|
||||
.get(group1.addFinal(new AsyncCallback<DiffInfo>() {
|
||||
@Override
|
||||
public void onSuccess(DiffInfo diffInfo) {
|
||||
diffLinks = diffInfo.webLinks();
|
||||
@ -205,6 +253,10 @@ public class EditScreen extends Screen {
|
||||
@Override
|
||||
public void onSuccess(HttpResponse<NativeString> fc) {
|
||||
content = fc;
|
||||
if (revision.get() > 0) {
|
||||
baseContent = fc;
|
||||
}
|
||||
|
||||
if (prefs.syntaxHighlighting()) {
|
||||
injectMode(fc.getContentType(), modeCallback);
|
||||
} else {
|
||||
@ -227,14 +279,16 @@ public class EditScreen extends Screen {
|
||||
group3.addListener(new ScreenLoadCallback<Void>(this) {
|
||||
@Override
|
||||
protected void preDisplay(Void result) {
|
||||
initEditor(content);
|
||||
initEditor();
|
||||
|
||||
renderLinks(editFileInfo, diffLinks);
|
||||
editFileInfo = null;
|
||||
diffLinks = null;
|
||||
|
||||
showBase.setValue(prefs.showBase(), true);
|
||||
cmBase.refresh();
|
||||
}
|
||||
});
|
||||
group1.done();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -251,14 +305,15 @@ public class EditScreen extends Screen {
|
||||
localKeyMap.on("Ctrl-S", save());
|
||||
}
|
||||
|
||||
cm.addKeyMap(localKeyMap);
|
||||
cmBase.addKeyMap(localKeyMap);
|
||||
cmEdit.addKeyMap(localKeyMap);
|
||||
}
|
||||
|
||||
private Runnable gotoLine() {
|
||||
return new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
cm.execCommand("jumpToLine");
|
||||
cmEdit.execCommand("jumpToLine");
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -274,36 +329,36 @@ public class EditScreen extends Screen {
|
||||
resizeHandler = Window.addResizeHandler(new ResizeHandler() {
|
||||
@Override
|
||||
public void onResize(ResizeEvent event) {
|
||||
cm.adjustHeight(header.getOffsetHeight());
|
||||
adjustHeight();
|
||||
}
|
||||
});
|
||||
closeHandler = Window.addWindowClosingHandler(new ClosingHandler() {
|
||||
@Override
|
||||
public void onWindowClosing(ClosingEvent event) {
|
||||
if (!cm.isClean(generation)) {
|
||||
if (!cmEdit.isClean(generation)) {
|
||||
event.setMessage(EditConstants.I.closeUnsavedChanges());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
generation = cm.changeGeneration(true);
|
||||
generation = cmEdit.changeGeneration(true);
|
||||
setClean(true);
|
||||
cm.on(new ChangesHandler() {
|
||||
cmEdit.on(new ChangesHandler() {
|
||||
@Override
|
||||
public void handle(CodeMirror cm) {
|
||||
setClean(cm.isClean(generation));
|
||||
}
|
||||
});
|
||||
|
||||
cm.adjustHeight(header.getOffsetHeight());
|
||||
cm.on("cursorActivity", updateCursorPosition());
|
||||
adjustHeight();
|
||||
cmEdit.on("cursorActivity", updateCursorPosition());
|
||||
setShowTabs(prefs.showTabs());
|
||||
setLineLength(prefs.lineLength());
|
||||
cm.refresh();
|
||||
cm.focus();
|
||||
cmEdit.refresh();
|
||||
cmEdit.focus();
|
||||
|
||||
if (startLine > 0) {
|
||||
cm.scrollToLine(startLine);
|
||||
cmEdit.scrollToLine(startLine);
|
||||
}
|
||||
updateActiveLine();
|
||||
editPrefsAction = new EditPreferencesAction(this, prefs);
|
||||
@ -312,8 +367,11 @@ public class EditScreen extends Screen {
|
||||
@Override
|
||||
protected void onUnload() {
|
||||
super.onUnload();
|
||||
if (cm != null) {
|
||||
cm.getWrapperElement().removeFromParent();
|
||||
if (cmBase != null) {
|
||||
cmBase.getWrapperElement().removeFromParent();
|
||||
}
|
||||
if (cmEdit != null) {
|
||||
cmEdit.getWrapperElement().removeFromParent();
|
||||
}
|
||||
if (resizeHandler != null) {
|
||||
resizeHandler.removeHandler();
|
||||
@ -327,7 +385,7 @@ public class EditScreen extends Screen {
|
||||
}
|
||||
|
||||
CodeMirror getEditor() {
|
||||
return cm;
|
||||
return cmEdit;
|
||||
}
|
||||
|
||||
@UiHandler("editSettings")
|
||||
@ -342,41 +400,127 @@ public class EditScreen extends Screen {
|
||||
|
||||
@UiHandler("close")
|
||||
void onClose(@SuppressWarnings("unused") ClickEvent e) {
|
||||
if (cm.isClean(generation)
|
||||
if (cmEdit.isClean(generation)
|
||||
|| Window.confirm(EditConstants.I.cancelUnsavedChanges())) {
|
||||
upToChange();
|
||||
}
|
||||
}
|
||||
|
||||
private void displayBase() {
|
||||
cmBase.getWrapperElement().getParentElement()
|
||||
.removeClassName(style.hideBase());
|
||||
cmEdit.getWrapperElement().getParentElement()
|
||||
.removeClassName(style.fullWidth());
|
||||
mv.getGapElement().removeClassName(style.hideBase());
|
||||
setCmBaseValue();
|
||||
setLineLength(prefs.lineLength());
|
||||
cmBase.refresh();
|
||||
}
|
||||
|
||||
@UiHandler("showBase")
|
||||
void onShowBase(ValueChangeEvent<Boolean> e) {
|
||||
boolean shouldShow = e.getValue();
|
||||
if (shouldShow) {
|
||||
if (baseContent == null) {
|
||||
ChangeEditApi.get(revision, path, true /* base */,
|
||||
new HttpCallback<NativeString>() {
|
||||
@Override
|
||||
public void onSuccess(HttpResponse<NativeString> fc) {
|
||||
baseContent = fc;
|
||||
displayBase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable e) {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
displayBase();
|
||||
}
|
||||
} else {
|
||||
cmBase.getWrapperElement().getParentElement()
|
||||
.addClassName(style.hideBase());
|
||||
cmEdit.getWrapperElement().getParentElement()
|
||||
.addClassName(style.fullWidth());
|
||||
mv.getGapElement().addClassName(style.hideBase());
|
||||
}
|
||||
mv.setShowDifferences(shouldShow);
|
||||
}
|
||||
|
||||
void setOption(String option, String value) {
|
||||
cmBase.setOption(option, value);
|
||||
cmEdit.setOption(option, value);
|
||||
}
|
||||
|
||||
void setOption(String option, boolean value) {
|
||||
cmBase.setOption(option, value);
|
||||
cmEdit.setOption(option, value);
|
||||
}
|
||||
|
||||
void setOption(String option, double value) {
|
||||
cmBase.setOption(option, value);
|
||||
cmEdit.setOption(option, value);
|
||||
}
|
||||
|
||||
void setTheme(final Theme newTheme) {
|
||||
cmBase.operation(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
cmBase.setOption("theme", newTheme.name().toLowerCase());
|
||||
}
|
||||
});
|
||||
cmEdit.operation(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
cmEdit.setOption("theme", newTheme.name().toLowerCase());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void setLineLength(int length) {
|
||||
cm.extras().lineLength(
|
||||
Patch.COMMIT_MSG.equals(path) ? 72 : length);
|
||||
int adjustedLength = Patch.COMMIT_MSG.equals(path) ? 72 : length;
|
||||
cmBase.extras().lineLength(adjustedLength);
|
||||
cmEdit.extras().lineLength(adjustedLength);
|
||||
}
|
||||
|
||||
void setIndentUnit(int indent) {
|
||||
cm.setOption("indentUnit",
|
||||
Patch.COMMIT_MSG.equals(path) ? 2 : indent);
|
||||
cmEdit.setOption("indentUnit", Patch.COMMIT_MSG.equals(path) ? 2 : indent);
|
||||
}
|
||||
|
||||
void setShowLineNumbers(boolean show) {
|
||||
cm.setOption("lineNumbers", show);
|
||||
cmBase.setOption("lineNumbers", show);
|
||||
cmEdit.setOption("lineNumbers", show);
|
||||
}
|
||||
|
||||
void setShowWhitespaceErrors(final boolean show) {
|
||||
cm.operation(new Runnable() {
|
||||
cmBase.operation(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
cm.setOption("showTrailingSpace", show);
|
||||
cmBase.setOption("showTrailingSpace", show);
|
||||
}
|
||||
});
|
||||
cmEdit.operation(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
cmEdit.setOption("showTrailingSpace", show);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void setShowTabs(boolean show) {
|
||||
cm.extras().showTabs(show);
|
||||
cmBase.extras().showTabs(show);
|
||||
cmEdit.extras().showTabs(show);
|
||||
}
|
||||
|
||||
void resizeCodeMirror() {
|
||||
cm.adjustHeight(header.getOffsetHeight());
|
||||
void adjustHeight() {
|
||||
int height = header.getOffsetHeight();
|
||||
int rest = Gerrit.getHeaderFooterHeight()
|
||||
+ height
|
||||
+ 5; // Estimate
|
||||
mv.getGapElement().getStyle().setHeight(
|
||||
Window.getClientHeight() - rest, Unit.PX);
|
||||
cmBase.adjustHeight(height);
|
||||
cmEdit.adjustHeight(height);
|
||||
}
|
||||
|
||||
void setSyntaxHighlighting(boolean b) {
|
||||
@ -386,7 +530,8 @@ public class EditScreen extends Screen {
|
||||
injectMode(mode, new AsyncCallback<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void result) {
|
||||
cm.setOption("mode", mode);
|
||||
cmBase.setOption("mode", mode);
|
||||
cmEdit.setOption("mode", mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -395,7 +540,8 @@ public class EditScreen extends Screen {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cm.setOption("mode", (String) null);
|
||||
cmBase.setOption("mode", (String) null);
|
||||
cmEdit.setOption("mode", (String) null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -403,16 +549,16 @@ public class EditScreen extends Screen {
|
||||
Gerrit.display(PageLinks.toChangeInEditMode(revision.getParentKey()));
|
||||
}
|
||||
|
||||
private void initEditor(HttpResponse<NativeString> file) {
|
||||
private void initEditor() {
|
||||
ModeInfo mode = null;
|
||||
String content = "";
|
||||
if (file != null && file.getResult() != null) {
|
||||
content = file.getResult().asString();
|
||||
String editContent = "";
|
||||
if (content != null && content.getResult() != null) {
|
||||
editContent = content.getResult().asString();
|
||||
if (prefs.syntaxHighlighting()) {
|
||||
mode = ModeInfo.findMode(file.getContentType(), path);
|
||||
mode = ModeInfo.findMode(content.getContentType(), path);
|
||||
}
|
||||
}
|
||||
cm = CodeMirror.create(editor, Configuration.create()
|
||||
mv = MergeView.create(editor, Configuration.create()
|
||||
.set("autoCloseBrackets", prefs.autoCloseBrackets())
|
||||
.set("cursorBlinkRate", prefs.cursorBlinkRate())
|
||||
.set("cursorHeight", 0.85)
|
||||
@ -422,13 +568,19 @@ public class EditScreen extends Screen {
|
||||
.set("lineWrapping", false)
|
||||
.set("matchBrackets", prefs.matchBrackets())
|
||||
.set("mode", mode != null ? mode.mime() : null)
|
||||
.set("readOnly", false)
|
||||
.set("origLeft", editContent)
|
||||
.set("scrollbarStyle", "overlay")
|
||||
.set("showTrailingSpace", prefs.showWhitespaceErrors())
|
||||
.set("styleSelectedText", true)
|
||||
.set("tabSize", prefs.tabSize())
|
||||
.set("theme", prefs.theme().name().toLowerCase())
|
||||
.set("value", content));
|
||||
.set("value", ""));
|
||||
|
||||
cmBase = mv.leftOriginal();
|
||||
cmBase.getWrapperElement().addClassName(style.base());
|
||||
cmEdit = mv.editor();
|
||||
setCmBaseValue();
|
||||
cmEdit.setValue(editContent);
|
||||
|
||||
CodeMirror.addCommand("save", new CommandRunner() {
|
||||
@Override
|
||||
@ -487,7 +639,7 @@ public class EditScreen extends Screen {
|
||||
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
|
||||
@Override
|
||||
public void execute() {
|
||||
cm.operation(new Runnable() {
|
||||
cmEdit.operation(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateActiveLine();
|
||||
@ -500,10 +652,10 @@ public class EditScreen extends Screen {
|
||||
}
|
||||
|
||||
private void updateActiveLine() {
|
||||
Pos p = cm.getCursor("end");
|
||||
Pos p = cmEdit.getCursor("end");
|
||||
cursLine.setInnerText(Integer.toString(p.line() + 1));
|
||||
cursCol.setInnerText(Integer.toString(p.ch() + 1));
|
||||
cm.extras().activeLine(cm.getLineHandleVisualStart(p.line()));
|
||||
cmEdit.extras().activeLine(cmEdit.getLineHandleVisualStart(p.line()));
|
||||
}
|
||||
|
||||
private void setClean(boolean clean) {
|
||||
@ -516,23 +668,23 @@ public class EditScreen extends Screen {
|
||||
return new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!cm.isClean(generation)) {
|
||||
if (!cmEdit.isClean(generation)) {
|
||||
close.setEnabled(false);
|
||||
String text = cm.getValue();
|
||||
String text = cmEdit.getValue();
|
||||
if (Patch.COMMIT_MSG.equals(path)) {
|
||||
String trimmed = text.trim() + "\r";
|
||||
if (!trimmed.equals(text)) {
|
||||
text = trimmed;
|
||||
cm.setValue(text);
|
||||
cmEdit.setValue(text);
|
||||
}
|
||||
}
|
||||
final int g = cm.changeGeneration(false);
|
||||
final int g = cmEdit.changeGeneration(false);
|
||||
ChangeEditApi.put(revision.getParentKey().get(), path, text,
|
||||
new GerritCallback<VoidResult>() {
|
||||
@Override
|
||||
public void onSuccess(VoidResult result) {
|
||||
generation = g;
|
||||
setClean(cm.isClean(g));
|
||||
setClean(cmEdit.isClean(g));
|
||||
}
|
||||
@Override
|
||||
public void onFailure(final Throwable caught) {
|
||||
@ -547,4 +699,10 @@ public class EditScreen extends Screen {
|
||||
private void injectMode(String type, AsyncCallback<Void> cb) {
|
||||
new ModeInjector().add(type).inject(cb);
|
||||
}
|
||||
|
||||
private void setCmBaseValue() {
|
||||
cmBase.setValue(baseContent != null && baseContent.getResult() != null
|
||||
? baseContent.getResult().asString()
|
||||
: "");
|
||||
}
|
||||
}
|
||||
|
@ -17,8 +17,11 @@ limitations under the License.
|
||||
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
|
||||
xmlns:g='urn:import:com.google.gwt.user.client.ui'>
|
||||
<ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
|
||||
<ui:style gss='false'>
|
||||
<ui:style gss='false' type='com.google.gerrit.client.editor.EditScreen.Style'>
|
||||
@external .CodeMirror, .CodeMirror-cursor;
|
||||
@external .CodeMirror-merge-2pane, .CodeMirror-merge-pane;
|
||||
@external .CodeMirror-merge-gap;
|
||||
@external .CodeMirror-scroll, .CodeMirror-overlayscroll-vertical;
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
@ -123,12 +126,28 @@ limitations under the License.
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.hideBase.CodeMirror-merge-pane {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hideBase.CodeMirror-merge-gap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.CodeMirror-merge-2pane .fullWidth.CodeMirror-merge-pane {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Hide the vertical scrollbar on the base side. The edit side controls
|
||||
both views */
|
||||
.base .CodeMirror-scroll { margin-right: -42px; }
|
||||
.base .CodeMirror-overlayscroll-vertical { display: none !important; }
|
||||
</ui:style>
|
||||
<g:HTMLPanel styleName='{style.header}'>
|
||||
<div class='{style.headerLine}' ui:field='header'>
|
||||
<div class='{style.headerButtons}'>
|
||||
<g:Button ui:field='close'
|
||||
styleName=''
|
||||
title='Close file and return to change'>
|
||||
<ui:attribute name='title'/>
|
||||
<div><ui:msg>Close</ui:msg></div>
|
||||
@ -142,6 +161,11 @@ limitations under the License.
|
||||
</div>
|
||||
<span class='{style.path}'><span ui:field='project'/> / <span ui:field='filePath'/></span>
|
||||
<div class='{style.navigation}'>
|
||||
<g:Label text='Show Base' styleName='{style.linkPanel}'></g:Label>
|
||||
<g:CheckBox ui:field='showBase' checked='true' styleName='{style.linkPanel}'
|
||||
title='Show Base Version'>
|
||||
<ui:attribute name='title'/>
|
||||
</g:CheckBox>
|
||||
<g:FlowPanel ui:field='linkPanel' styleName='{style.linkPanel}'/>
|
||||
<g:Image
|
||||
ui:field='editSettings'
|
||||
|
95
gerrit-gwtui/src/main/java/net/codemirror/addon/AddonInjector.java
vendored
Normal file
95
gerrit-gwtui/src/main/java/net/codemirror/addon/AddonInjector.java
vendored
Normal file
@ -0,0 +1,95 @@
|
||||
// Copyright (C) 2016 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.addon;
|
||||
|
||||
import com.google.gwt.safehtml.shared.SafeUri;
|
||||
import com.google.gwt.user.client.rpc.AsyncCallback;
|
||||
|
||||
import net.codemirror.lib.Loader;
|
||||
|
||||
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 AddonInjector {
|
||||
private static final Map<String, SafeUri> addonUris = new HashMap<>();
|
||||
static {
|
||||
addonUris.put(Addons.I.merge_bundled().getName(),
|
||||
Addons.I.merge_bundled().getSafeUri());
|
||||
}
|
||||
|
||||
public static SafeUri getAddonScriptUri(String addon) {
|
||||
return addonUris.get(addon);
|
||||
}
|
||||
|
||||
private static boolean canLoad(String addon) {
|
||||
return getAddonScriptUri(addon) != null;
|
||||
}
|
||||
|
||||
private final Set<String> loading = new HashSet<>();
|
||||
private int pending;
|
||||
private AsyncCallback<Void> appCallback;
|
||||
|
||||
public AddonInjector add(String name) {
|
||||
if (name == null) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!canLoad(name)) {
|
||||
Logger.getLogger("net.codemirror").log(
|
||||
Level.WARNING,
|
||||
"CodeMirror addon " + name + " not configured.");
|
||||
return this;
|
||||
}
|
||||
|
||||
loading.add(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void inject(AsyncCallback<Void> appCallback) {
|
||||
this.appCallback = appCallback;
|
||||
for (String addon : loading) {
|
||||
beginLoading(addon);
|
||||
}
|
||||
if (pending == 0) {
|
||||
appCallback.onSuccess(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void beginLoading(final String addon) {
|
||||
pending++;
|
||||
Loader.injectScript(
|
||||
getAddonScriptUri(addon),
|
||||
new AsyncCallback<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void result) {
|
||||
pending--;
|
||||
if (pending == 0) {
|
||||
appCallback.onSuccess(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
if (--pending == 0) {
|
||||
appCallback.onFailure(caught);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
26
gerrit-gwtui/src/main/java/net/codemirror/addon/Addons.java
vendored
Normal file
26
gerrit-gwtui/src/main/java/net/codemirror/addon/Addons.java
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright (C) 2016 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.addon;
|
||||
|
||||
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;
|
||||
|
||||
public interface Addons extends ClientBundle {
|
||||
public static final Addons I = GWT.create(Addons.class);
|
||||
|
||||
@Source("merge_bundled.js") @DoNotEmbed DataResource merge_bundled();
|
||||
}
|
51
gerrit-gwtui/src/main/java/net/codemirror/lib/MergeView.java
vendored
Normal file
51
gerrit-gwtui/src/main/java/net/codemirror/lib/MergeView.java
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright (C) 2016 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.JavaScriptObject;
|
||||
import com.google.gwt.dom.client.Element;
|
||||
|
||||
/** Object that represents a text marker within CodeMirror */
|
||||
public class MergeView extends JavaScriptObject {
|
||||
public static MergeView create(Element p, Configuration cfg) {
|
||||
MergeView mv = newMergeView(p, cfg);
|
||||
Extras.attach(mv.leftOriginal());
|
||||
Extras.attach(mv.editor());
|
||||
return mv;
|
||||
}
|
||||
|
||||
private static native MergeView newMergeView(Element p, Configuration cfg) /*-{
|
||||
return $wnd.CodeMirror.MergeView(p, cfg);
|
||||
}-*/;
|
||||
|
||||
public final native CodeMirror leftOriginal() /*-{
|
||||
return this.leftOriginal();
|
||||
}-*/;
|
||||
|
||||
public final native CodeMirror editor() /*-{
|
||||
return this.editor();
|
||||
}-*/;
|
||||
|
||||
public final native void setShowDifferences(boolean b) /*-{
|
||||
this.setShowDifferences(b);
|
||||
}-*/;
|
||||
|
||||
public final native Element getGapElement() /*-{
|
||||
return $doc.getElementsByClassName("CodeMirror-merge-gap")[0];
|
||||
}-*/;
|
||||
|
||||
protected MergeView() {
|
||||
}
|
||||
}
|
@ -23,6 +23,18 @@ maven_jar(
|
||||
visibility = [],
|
||||
)
|
||||
|
||||
DIFF_MATCH_PATCH_VERSION = '20121119-1'
|
||||
DIFF_MATCH_PATCH_TOP = ('META-INF/resources/webjars/google-diff-match-patch/%s'
|
||||
% DIFF_MATCH_PATCH_VERSION)
|
||||
|
||||
maven_jar(
|
||||
name = 'diff-match-patch',
|
||||
id = 'org.webjars:google-diff-match-patch:' + DIFF_MATCH_PATCH_VERSION,
|
||||
sha1 = '0cf1782dbcb8359d95070da9176059a5a9d37709',
|
||||
license = 'Apache2.0',
|
||||
attach_source = False,
|
||||
)
|
||||
|
||||
for archive, suffix, top in [('codemirror-original', '', TOP), ('codemirror-minified', '_r', TOP_MINIFIED)]:
|
||||
# Main JavaScript and addons
|
||||
genrule(
|
||||
@ -58,11 +70,12 @@ for archive, suffix, top in [('codemirror-original', '', TOP), ('codemirror-mini
|
||||
genrule (
|
||||
name = 'mode_%s%s' % (n, suffix),
|
||||
cmd = ';'.join([
|
||||
"echo '/** @license' >$OUT",
|
||||
'unzip -p $(location :%s) %s/LICENSE >>$OUT' % (archive, top),
|
||||
"echo '*/' >>$OUT",
|
||||
'unzip -p $(location :%s) %s/mode/%s/%s.js >>$OUT' % (archive, top, n, n),
|
||||
]),
|
||||
"echo '/** @license' >$OUT",
|
||||
'unzip -p $(location :%s) %s/LICENSE >>$OUT' % (archive, top),
|
||||
"echo '*/' >>$OUT",
|
||||
'unzip -p $(location :%s) %s/mode/%s/%s.js >>$OUT' % (archive, top, n, n),
|
||||
]
|
||||
),
|
||||
out = 'mode_%s%s.js' % (n, suffix),
|
||||
)
|
||||
|
||||
@ -80,19 +93,39 @@ for archive, suffix, top in [('codemirror-original', '', TOP), ('codemirror-mini
|
||||
out = 'theme_%s%s.css' % (n, suffix),
|
||||
)
|
||||
|
||||
# Merge Addon bundled with diff-match-patch
|
||||
genrule(
|
||||
name = 'addon_merge%s' % suffix,
|
||||
cmd = ';'.join([
|
||||
"echo '/** @license' >$OUT",
|
||||
'unzip -p $(location :%s) %s/LICENSE >>$OUT' % (archive, top),
|
||||
"echo '*/\n' >>$OUT",
|
||||
"echo '// The google-diff-match-patch library is from https://google-diff-match-patch.googlecode.com/svn-history/r106/trunk/javascript/diff_match_patch.js\n' >> $OUT",
|
||||
"echo '/** @license' >>$OUT",
|
||||
'cat $(location //lib:LICENSE-Apache2.0) >>$OUT',
|
||||
"echo '*/' >>$OUT",
|
||||
'unzip -p $(location :diff-match-patch) %s/diff_match_patch.js >>$OUT' % DIFF_MATCH_PATCH_TOP,
|
||||
"echo ';' >> $OUT",
|
||||
'unzip -p $(location :%s) %s/addon/merge/merge.js >>$OUT' % (archive, top)
|
||||
]
|
||||
),
|
||||
out = 'addon_merge%s.js' % suffix,
|
||||
)
|
||||
|
||||
# Jar packaging
|
||||
genrule(
|
||||
name = 'jar' + suffix,
|
||||
cmd = ';'.join([
|
||||
'cd $TMP',
|
||||
'mkdir -p net/codemirror/{lib,mode,theme}',
|
||||
'mkdir -p net/codemirror/{addon,lib,mode,theme}',
|
||||
'cp $(location :css%s) net/codemirror/lib/cm.css' % suffix,
|
||||
'cp $(location :cm%s) net/codemirror/lib/cm.js' % suffix]
|
||||
+ ['cp $(location :mode_%s%s) net/codemirror/mode/%s.js' % (n, suffix, n)
|
||||
for n in CM_MODES]
|
||||
+ ['cp $(location :theme_%s%s) net/codemirror/theme/%s.css' % (n, suffix, n)
|
||||
for n in CM_THEMES]
|
||||
+ ['zip -qr $OUT net/codemirror/{lib,mode,theme}']),
|
||||
+ ['cp $(location :addon_merge%s) net/codemirror/addon/merge_bundled.js' % suffix]
|
||||
+ ['zip -qr $OUT net/codemirror/{addon,lib,mode,theme}']),
|
||||
out = 'codemirror%s.jar' % suffix,
|
||||
)
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
CM_CSS = [
|
||||
'lib/codemirror.css',
|
||||
'addon/dialog/dialog.css',
|
||||
'addon/merge/merge.css',
|
||||
'addon/scroll/simplescrollbars.css',
|
||||
'addon/search/matchesonscrollbar.css',
|
||||
'addon/lint/lint.css',
|
||||
|
Loading…
x
Reference in New Issue
Block a user