blob: dbe7e5d092079589608d6779d369042246f93f81 [file] [log] [blame]
// 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 com.google.gerrit.client.diff;
import static java.lang.Double.POSITIVE_INFINITY;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gerrit.client.projects.ConfigInfoCache;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.InlineHyperlink;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
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.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.ImageResourceRenderer;
import com.google.gwtexpui.globalkey.client.GlobalKey;
import com.google.gwtexpui.globalkey.client.KeyCommand;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.CodeMirror.LineHandle;
import net.codemirror.lib.Configuration;
import net.codemirror.lib.KeyMap;
import net.codemirror.lib.Pos;
import java.util.Collections;
import java.util.List;
public class SideBySide extends DiffScreen {
interface Binder extends UiBinder<FlowPanel, SideBySide> {}
private static final Binder uiBinder = GWT.create(Binder.class);
private static final String LINE_NUMBER_CLASSNAME = "CodeMirror-linenumber";
@UiField(provided = true)
SideBySideTable diffTable;
private CodeMirror cmA;
private CodeMirror cmB;
private ScrollSynchronizer scrollSynchronizer;
private SideBySideChunkManager chunkManager;
private SideBySideCommentManager commentManager;
public SideBySide(
PatchSet.Id base,
PatchSet.Id revision,
String path,
DisplaySide startSide,
int startLine) {
super(base, revision, path, startSide, startLine, DiffView.SIDE_BY_SIDE);
diffTable = new SideBySideTable(this, base, revision, path);
add(uiBinder.createAndBindUi(this));
addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
}
@Override
ScreenLoadCallback<ConfigInfoCache.Entry> getScreenLoadCallback(
final CommentsCollections comments) {
return new ScreenLoadCallback<ConfigInfoCache.Entry>(SideBySide.this) {
@Override
protected void preDisplay(ConfigInfoCache.Entry result) {
commentManager = new SideBySideCommentManager(
SideBySide.this,
base, revision, path,
result.getCommentLinkProcessor(),
getChangeStatus().isOpen());
setTheme(result.getTheme());
display(comments);
header.setupPrevNextFiles(comments);
}
};
}
@Override
public void onShowView() {
super.onShowView();
operation(new Runnable() {
@Override
public void run() {
resizeCodeMirror();
chunkManager.adjustPadding();
cmA.refresh();
cmB.refresh();
}
});
setLineLength(Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength());
diffTable.refresh();
if (getStartLine() == 0) {
DiffChunkInfo d = chunkManager.getFirst();
if (d != null) {
if (d.isEdit() && d.getSide() == DisplaySide.A) {
setStartSide(DisplaySide.B);
setStartLine(lineOnOther(d.getSide(), d.getStart()).getLine() + 1);
} else {
setStartSide(d.getSide());
setStartLine(d.getStart() + 1);
}
}
}
if (getStartSide() != null && getStartLine() > 0) {
CodeMirror cm = getCmFromSide(getStartSide());
cm.scrollToLine(getStartLine() - 1);
cm.focus();
} else {
cmA.setCursor(Pos.create(0));
cmA.focus();
}
if (Gerrit.isSignedIn() && prefs.autoReview()) {
header.autoReview();
}
prefetchNextFile();
}
@Override
void registerCmEvents(final CodeMirror cm) {
super.registerCmEvents(cm);
KeyMap keyMap = KeyMap.create()
.on("Shift-A", diffTable.toggleA())
.on("Shift-Left", moveCursorToSide(cm, DisplaySide.A))
.on("Shift-Right", moveCursorToSide(cm, DisplaySide.B));
cm.addKeyMap(keyMap);
maybeRegisterRenderEntireFileKeyMap(cm);
}
@Override
public void registerKeys() {
super.registerKeys();
getKeysNavigation().add(
new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_LEFT, PatchUtil.C.focusSideA()),
new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_RIGHT, PatchUtil.C.focusSideB()));
getKeysAction().add(new KeyCommand(
KeyCommand.M_SHIFT, 'a', PatchUtil.C.toggleSideA()) {
@Override
public void onKeyPress(KeyPressEvent event) {
diffTable.toggleA().run();
}
});
registerHandlers();
}
@Override
FocusHandler getFocusHandler() {
return new FocusHandler() {
@Override
public void onFocus(FocusEvent event) {
cmB.focus();
}
};
}
private void display(final CommentsCollections comments) {
final DiffInfo diff = getDiff();
setThemeStyles(prefs.theme().isDark());
setShowIntraline(prefs.intralineDifference());
if (prefs.showLineNumbers()) {
diffTable.addStyleName(Resources.I.diffTableStyle().showLineNumbers());
}
cmA = newCm(diff.metaA(), diff.textA(), diffTable.cmA);
cmB = newCm(diff.metaB(), diff.textB(), diffTable.cmB);
boolean reviewingBase = base == null;
getDiffTable().setUpBlameIconA(cmA, reviewingBase,
reviewingBase ? revision : base, path);
getDiffTable().setUpBlameIconB(cmB, revision, path);
cmA.extras().side(DisplaySide.A);
cmB.extras().side(DisplaySide.B);
setShowTabs(prefs.showTabs());
chunkManager = new SideBySideChunkManager(this, cmA, cmB, diffTable.scrollbar);
operation(new Runnable() {
@Override
public void run() {
// Estimate initial CodeMirror height, fixed up in onShowView.
int height = Window.getClientHeight()
- (Gerrit.getHeaderFooterHeight() + 18);
cmA.setHeight(height);
cmB.setHeight(height);
render(diff);
commentManager.render(comments, prefs.expandAllComments());
skipManager.render(prefs.context(), diff);
}
});
registerCmEvents(cmA);
registerCmEvents(cmB);
scrollSynchronizer = new ScrollSynchronizer(diffTable, cmA, cmB,
chunkManager.lineMapper);
setPrefsAction(new PreferencesAction(this, prefs));
header.init(getPrefsAction(), getUnifiedDiffLink(), diff.sideBySideWebLinks());
scrollSynchronizer.setAutoHideDiffTableHeader(prefs.autoHideDiffTableHeader());
setupSyntaxHighlighting();
}
private List<InlineHyperlink> getUnifiedDiffLink() {
InlineHyperlink toUnifiedDiffLink = new InlineHyperlink();
toUnifiedDiffLink.setHTML(
new ImageResourceRenderer().render(Gerrit.RESOURCES.unifiedDiff()));
toUnifiedDiffLink.setTargetHistoryToken(
Dispatcher.toUnified(base, revision, path));
toUnifiedDiffLink.setTitle(PatchUtil.C.unifiedDiff());
return Collections.singletonList(toUnifiedDiffLink);
}
@Override
CodeMirror newCm(
DiffInfo.FileMeta meta,
String contents,
Element parent) {
return CodeMirror.create(parent, Configuration.create()
.set("cursorBlinkRate", prefs.cursorBlinkRate())
.set("cursorHeight", 0.85)
.set("inputStyle", "textarea")
.set("keyMap", "vim_ro")
.set("lineNumbers", prefs.showLineNumbers())
.set("matchBrackets", prefs.matchBrackets())
.set("lineWrapping", prefs.lineWrapping())
.set("mode", getFileSize() == FileSize.SMALL ? getContentType(meta) : null)
.set("readOnly", true)
.set("scrollbarStyle", "overlay")
.set("showTrailingSpace", prefs.showWhitespaceErrors())
.set("styleSelectedText", true)
.set("tabSize", prefs.tabSize())
.set("theme", prefs.theme().name().toLowerCase())
.set("value", meta != null ? contents : "")
.set("viewportMargin", renderEntireFile() ? POSITIVE_INFINITY : 10));
}
@Override
void setShowLineNumbers(boolean b) {
super.setShowLineNumbers(b);
cmA.setOption("lineNumbers", b);
cmB.setOption("lineNumbers", b);
}
@Override
void setSyntaxHighlighting(boolean b) {
final DiffInfo diff = getDiff();
if (b) {
injectMode(diff, new AsyncCallback<Void>() {
@Override
public void onSuccess(Void result) {
if (prefs.syntaxHighlighting()) {
cmA.setOption("mode", getContentType(diff.metaA()));
cmB.setOption("mode", getContentType(diff.metaB()));
}
}
@Override
public void onFailure(Throwable caught) {
prefs.syntaxHighlighting(false);
}
});
} else {
cmA.setOption("mode", (String) null);
cmB.setOption("mode", (String) null);
}
}
@Override
void setAutoHideDiffHeader(boolean hide) {
scrollSynchronizer.setAutoHideDiffTableHeader(hide);
}
CodeMirror otherCm(CodeMirror me) {
return me == cmA ? cmB : cmA;
}
@Override
CodeMirror getCmFromSide(DisplaySide side) {
return side == DisplaySide.A ? cmA : cmB;
}
@Override
int getCmLine(int line, DisplaySide side) {
return line;
}
@Override
Runnable updateActiveLine(final CodeMirror cm) {
final CodeMirror other = otherCm(cm);
return new Runnable() {
@Override
public void run() {
// The rendering of active lines has to be deferred. Reflow
// caused by adding and removing styles chokes Firefox when arrow
// key (or j/k) is held down. Performance on Chrome is fine
// without the deferral.
//
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
operation(new Runnable() {
@Override
public void run() {
LineHandle handle =
cm.getLineHandleVisualStart(cm.getCursor("end").line());
if (!cm.extras().activeLine(handle)) {
return;
}
LineOnOtherInfo info =
lineOnOther(cm.side(), cm.getLineNumber(handle));
if (info.isAligned()) {
other.extras().activeLine(other.getLineHandle(info.getLine()));
} else {
other.extras().clearActiveLine();
}
}
});
}
});
}
};
}
private Runnable moveCursorToSide(final CodeMirror cmSrc, DisplaySide sideDst) {
final CodeMirror cmDst = getCmFromSide(sideDst);
if (cmDst == cmSrc) {
return new Runnable() {
@Override
public void run() {
}
};
}
final DisplaySide sideSrc = cmSrc.side();
return new Runnable() {
@Override
public void run() {
if (cmSrc.extras().hasActiveLine()) {
cmDst.setCursor(Pos.create(lineOnOther(
sideSrc,
cmSrc.getLineNumber(cmSrc.extras().activeLine())).getLine()));
}
cmDst.focus();
}
};
}
void syncScroll(DisplaySide masterSide) {
if (scrollSynchronizer != null) {
scrollSynchronizer.syncScroll(masterSide);
}
}
@Override
void operation(final Runnable apply) {
cmA.operation(new Runnable() {
@Override
public void run() {
cmB.operation(new Runnable() {
@Override
public void run() {
apply.run();
}
});
}
});
}
@Override
CodeMirror[] getCms() {
return new CodeMirror[]{cmA, cmB};
}
@Override
SideBySideTable getDiffTable() {
return diffTable;
}
@Override
SideBySideChunkManager getChunkManager() {
return chunkManager;
}
@Override
SideBySideCommentManager getCommentManager() {
return commentManager;
}
@Override
boolean isSideBySide() {
return true;
}
@Override
String getLineNumberClassName() {
return LINE_NUMBER_CLASSNAME;
}
}