blob: 0fdc5197fa7a82e69aa7895c2cfd68b1a4292633 [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 com.google.gerrit.extensions.client.DiffPreferencesInfo.WHOLE_FILE_CONTEXT;
import static java.lang.Double.POSITIVE_INFINITY;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.JumpKeys;
import com.google.gerrit.client.account.DiffPreferences;
import com.google.gerrit.client.change.ChangeScreen;
import com.google.gerrit.client.change.FileTable;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeList;
import com.google.gerrit.client.diff.DiffInfo.FileMeta;
import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
import com.google.gerrit.client.info.ChangeInfo;
import com.google.gerrit.client.info.ChangeInfo.EditInfo;
import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
import com.google.gerrit.client.info.FileInfo;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gerrit.client.projects.ConfigInfoCache;
import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
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.ListChangesOption;
import com.google.gerrit.reviewdb.client.Change;
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.JsArray;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
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.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
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 com.google.gwtexpui.globalkey.client.KeyCommandSet;
import com.google.gwtexpui.globalkey.client.ShowHelpCommand;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.CodeMirror.BeforeSelectionChangeHandler;
import net.codemirror.lib.CodeMirror.GutterClickHandler;
import net.codemirror.lib.CodeMirror.LineHandle;
import net.codemirror.lib.Configuration;
import net.codemirror.lib.KeyMap;
import net.codemirror.lib.Pos;
import net.codemirror.mode.ModeInfo;
import net.codemirror.mode.ModeInjector;
import net.codemirror.theme.ThemeLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
public class SideBySide extends Screen {
private static final KeyMap RENDER_ENTIRE_FILE_KEYMAP = KeyMap.create()
.propagate("Ctrl-F");
interface Binder extends UiBinder<FlowPanel, SideBySide> {}
private static final Binder uiBinder = GWT.create(Binder.class);
enum FileSize {
SMALL(0),
LARGE(500),
HUGE(4000);
final int lines;
FileSize(int n) {
this.lines = n;
}
}
@UiField(provided = true)
Header header;
@UiField(provided = true)
DiffTable diffTable;
private final Change.Id changeId;
private final PatchSet.Id base;
private final PatchSet.Id revision;
private final String path;
private DisplaySide startSide;
private int startLine;
private DiffPreferences prefs;
private Change.Status changeStatus;
private CodeMirror cmA;
private CodeMirror cmB;
private HandlerRegistration resizeHandler;
private ScrollSynchronizer scrollSynchronizer;
private DiffInfo diff;
private FileSize fileSize;
private EditInfo edit;
private ChunkManager chunkManager;
private CommentManager commentManager;
private SkipManager skipManager;
private KeyCommandSet keysNavigation;
private KeyCommandSet keysAction;
private KeyCommandSet keysComment;
private List<HandlerRegistration> handlers;
private PreferencesAction prefsAction;
private int reloadVersionId;
public SideBySide(
PatchSet.Id base,
PatchSet.Id revision,
String path,
DisplaySide startSide,
int startLine) {
this.base = base;
this.revision = revision;
this.changeId = revision.getParentKey();
this.path = path;
this.startSide = startSide;
this.startLine = startLine;
prefs = DiffPreferences.create(Gerrit.getDiffPreferences());
handlers = new ArrayList<>(6);
keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
header = new Header(keysNavigation, base, revision, path);
diffTable = new DiffTable(this, base, revision, path);
add(uiBinder.createAndBindUi(this));
addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
}
@Override
protected void onInitUI() {
super.onInitUI();
setHeaderVisible(false);
setWindowTitle(FileInfo.getFileName(path));
}
@Override
protected void onLoad() {
super.onLoad();
CallbackGroup group1 = new CallbackGroup();
final CallbackGroup group2 = new CallbackGroup();
CodeMirror.initLibrary(group1.add(new AsyncCallback<Void>() {
final AsyncCallback<Void> themeCallback = group2.addEmpty();
@Override
public void onSuccess(Void result) {
// Load theme after CM library to ensure theme can override CSS.
ThemeLoader.loadTheme(prefs.theme(), themeCallback);
}
@Override
public void onFailure(Throwable caught) {
}
}));
DiffApi.diff(revision, path)
.base(base)
.wholeFile()
.intraline(prefs.intralineDifference())
.ignoreWhitespace(prefs.ignoreWhitespace())
.get(group1.addFinal(new GerritCallback<DiffInfo>() {
final AsyncCallback<Void> modeInjectorCb = group2.addEmpty();
@Override
public void onSuccess(DiffInfo diffInfo) {
diff = diffInfo;
fileSize = bucketFileSize(diffInfo);
if (prefs.syntaxHighlighting()) {
if (fileSize.compareTo(FileSize.SMALL) > 0) {
modeInjectorCb.onSuccess(null);
} else {
injectMode(diffInfo, modeInjectorCb);
}
} else {
modeInjectorCb.onSuccess(null);
}
}
}));
if (Gerrit.isSignedIn()) {
ChangeApi.edit(changeId.get(), group2.add(
new AsyncCallback<EditInfo>() {
@Override
public void onSuccess(EditInfo result) {
edit = result;
}
@Override
public void onFailure(Throwable caught) {
}
}));
}
final CommentsCollections comments = new CommentsCollections();
comments.load(base, revision, path, group2);
RestApi call = ChangeApi.detail(changeId.get());
ChangeList.addOptions(call, EnumSet.of(
ListChangesOption.ALL_REVISIONS));
call.get(group2.add(new AsyncCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo info) {
changeStatus = info.status();
info.revisions().copyKeysIntoChildren("name");
if (edit != null) {
edit.setName(edit.commit().commit());
info.setEdit(edit);
info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
}
String currentRevision = info.currentRevision();
boolean current = currentRevision != null &&
revision.get() == info.revision(currentRevision)._number();
JsArray<RevisionInfo> list = info.revisions().values();
RevisionInfo.sortRevisionInfoByNumber(list);
diffTable.set(prefs, list, diff, edit != null, current,
changeStatus.isOpen(), diff.binary());
header.setChangeInfo(info);
}
@Override
public void onFailure(Throwable caught) {
}
}));
ConfigInfoCache.get(changeId, group2.addFinal(
new ScreenLoadCallback<ConfigInfoCache.Entry>(SideBySide.this) {
@Override
protected void preDisplay(ConfigInfoCache.Entry result) {
commentManager = new CommentManager(
SideBySide.this,
base, revision, path,
result.getCommentLinkProcessor(),
changeStatus.isOpen());
setTheme(result.getTheme());
display(comments);
}
}));
}
@Override
public void onShowView() {
super.onShowView();
Window.enableScrolling(false);
JumpKeys.enable(false);
if (prefs.hideTopMenu()) {
Gerrit.setHeaderVisible(false);
}
resizeHandler = Window.addResizeHandler(new ResizeHandler() {
@Override
public void onResize(ResizeEvent event) {
resizeCodeMirror();
}
});
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 (startLine == 0) {
DiffChunkInfo d = chunkManager.getFirst();
if (d != null) {
if (d.isEdit() && d.getSide() == DisplaySide.A) {
startSide = DisplaySide.B;
startLine = lineOnOther(d.getSide(), d.getStart()).getLine() + 1;
} else {
startSide = d.getSide();
startLine = d.getStart() + 1;
}
}
}
if (startSide != null && startLine > 0) {
CodeMirror cm = getCmFromSide(startSide);
cm.scrollToLine(startLine - 1);
cm.focus();
} else {
cmA.setCursor(Pos.create(0));
cmA.focus();
}
if (Gerrit.isSignedIn() && prefs.autoReview()) {
header.autoReview();
}
prefetchNextFile();
}
@Override
protected void onUnload() {
super.onUnload();
removeKeyHandlerRegistrations();
if (commentManager != null) {
CallbackGroup group = new CallbackGroup();
commentManager.saveAllDrafts(group);
group.done();
}
if (resizeHandler != null) {
resizeHandler.removeHandler();
resizeHandler = null;
}
if (cmA != null) {
cmA.getWrapperElement().removeFromParent();
}
if (cmB != null) {
cmB.getWrapperElement().removeFromParent();
}
if (prefsAction != null) {
prefsAction.hide();
}
Window.enableScrolling(true);
Gerrit.setHeaderVisible(true);
JumpKeys.enable(true);
}
private void removeKeyHandlerRegistrations() {
for (HandlerRegistration h : handlers) {
h.removeHandler();
}
handlers.clear();
}
private void registerCmEvents(final CodeMirror cm) {
cm.on("cursorActivity", updateActiveLine(cm));
cm.on("focus", updateActiveLine(cm));
KeyMap keyMap = KeyMap.create()
.on("A", upToChange(true))
.on("U", upToChange(false))
.on("'['", header.navigate(Direction.PREV))
.on("']'", header.navigate(Direction.NEXT))
.on("R", header.toggleReviewed())
.on("O", commentManager.toggleOpenBox(cm))
.on("Enter", commentManager.toggleOpenBox(cm))
.on("N", maybeNextVimSearch(cm))
.on("E", openEditScreen(cm))
.on("P", chunkManager.diffChunkNav(cm, Direction.PREV))
.on("Shift-A", diffTable.toggleA())
.on("Shift-M", header.reviewedAndNext())
.on("Shift-N", maybePrevVimSearch(cm))
.on("Shift-P", commentManager.commentNav(cm, Direction.PREV))
.on("Shift-O", commentManager.openCloseAll(cm))
.on("Shift-Left", moveCursorToSide(cm, DisplaySide.A))
.on("Shift-Right", moveCursorToSide(cm, DisplaySide.B))
.on("I", new Runnable() {
@Override
public void run() {
switch (getIntraLineStatus()) {
case OFF:
case OK:
toggleShowIntraline();
break;
default:
break;
}
}
})
.on("','", new Runnable() {
@Override
public void run() {
prefsAction.show();
}
})
.on("Shift-/", new Runnable() {
@Override
public void run() {
new ShowHelpCommand().onKeyPress(null);
}
})
.on("Space", new Runnable() {
@Override
public void run() {
cm.vim().handleKey("<C-d>");
}
})
.on("Shift-Space", new Runnable() {
@Override
public void run() {
cm.vim().handleKey("<C-u>");
}
})
.on("Ctrl-F", new Runnable() {
@Override
public void run() {
cm.vim().handleKey("/");
}
})
.on("Ctrl-A", new Runnable() {
@Override
public void run() {
cm.execCommand("selectAll");
}
});
if (revision.get() != 0) {
cm.on("beforeSelectionChange", onSelectionChange(cm));
cm.on("gutterClick", onGutterClick(cm));
keyMap.on("C", commentManager.insertNewDraft(cm));
}
cm.addKeyMap(keyMap);
if (renderEntireFile()) {
cm.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
}
}
private BeforeSelectionChangeHandler onSelectionChange(final CodeMirror cm) {
return new BeforeSelectionChangeHandler() {
private InsertCommentBubble bubble;
@Override
public void handle(CodeMirror cm, Pos anchor, Pos head) {
if (anchor.equals(head)) {
if (bubble != null) {
bubble.setVisible(false);
}
return;
} else if (bubble == null) {
init(anchor);
} else {
bubble.setVisible(true);
}
bubble.position(cm.charCoords(head, "local"));
}
private void init(Pos anchor) {
bubble = new InsertCommentBubble(commentManager, cm);
add(bubble);
cm.addWidget(anchor, bubble.getElement());
}
};
}
@Override
public void registerKeys() {
super.registerKeys();
keysNavigation.add(new UpToChangeCommand(revision, 0, 'u'));
keysNavigation.add(
new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_LEFT, PatchUtil.C.focusSideA()),
new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_RIGHT, PatchUtil.C.focusSideB()));
keysNavigation.add(
new NoOpKeyCommand(0, 'j', PatchUtil.C.lineNext()),
new NoOpKeyCommand(0, 'k', PatchUtil.C.linePrev()));
keysNavigation.add(
new NoOpKeyCommand(0, 'n', PatchUtil.C.chunkNext2()),
new NoOpKeyCommand(0, 'p', PatchUtil.C.chunkPrev2()));
keysNavigation.add(
new NoOpKeyCommand(KeyCommand.M_SHIFT, 'n', PatchUtil.C.commentNext()),
new NoOpKeyCommand(KeyCommand.M_SHIFT, 'p', PatchUtil.C.commentPrev()));
keysNavigation.add(
new NoOpKeyCommand(KeyCommand.M_CTRL, 'f', Gerrit.C.keySearch()));
keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
keysAction.add(new NoOpKeyCommand(0, 'e', PatchUtil.C.openEditScreen()));
keysAction.add(new NoOpKeyCommand(0, KeyCodes.KEY_ENTER,
PatchUtil.C.expandComment()));
keysAction.add(new NoOpKeyCommand(0, 'o', PatchUtil.C.expandComment()));
keysAction.add(new NoOpKeyCommand(
KeyCommand.M_SHIFT, 'o', PatchUtil.C.expandAllCommentsOnCurrentLine()));
if (Gerrit.isSignedIn()) {
keysAction.add(new KeyCommand(0, 'r', PatchUtil.C.toggleReviewed()) {
@Override
public void onKeyPress(KeyPressEvent event) {
header.toggleReviewed().run();
}
});
}
keysAction.add(new KeyCommand(
KeyCommand.M_SHIFT, 'm', PatchUtil.C.markAsReviewedAndGoToNext()) {
@Override
public void onKeyPress(KeyPressEvent event) {
header.reviewedAndNext().run();
}
});
keysAction.add(new KeyCommand(0, 'a', PatchUtil.C.openReply()) {
@Override
public void onKeyPress(KeyPressEvent event) {
upToChange(true).run();
}
});
keysAction.add(new KeyCommand(
KeyCommand.M_SHIFT, 'a', PatchUtil.C.toggleSideA()) {
@Override
public void onKeyPress(KeyPressEvent event) {
diffTable.toggleA().run();
}
});
keysAction.add(new KeyCommand(0, ',', PatchUtil.C.showPreferences()) {
@Override
public void onKeyPress(KeyPressEvent event) {
prefsAction.show();
}
});
if (getIntraLineStatus() == DiffInfo.IntraLineStatus.OFF
|| getIntraLineStatus() == DiffInfo.IntraLineStatus.OK) {
keysAction.add(new KeyCommand(0, 'i', PatchUtil.C.toggleIntraline()) {
@Override
public void onKeyPress(KeyPressEvent event) {
toggleShowIntraline();
}
});
}
if (Gerrit.isSignedIn()) {
keysAction.add(new NoOpKeyCommand(0, 'c', PatchUtil.C.commentInsert()));
keysComment = new KeyCommandSet(PatchUtil.C.commentEditorSet());
keysComment.add(new NoOpKeyCommand(KeyCommand.M_CTRL, 's',
PatchUtil.C.commentSaveDraft()));
keysComment.add(new NoOpKeyCommand(0, KeyCodes.KEY_ESCAPE,
PatchUtil.C.commentCancelEdit()));
} else {
keysComment = null;
}
removeKeyHandlerRegistrations();
handlers.add(GlobalKey.add(this, keysAction));
handlers.add(GlobalKey.add(this, keysNavigation));
if (keysComment != null) {
handlers.add(GlobalKey.add(this, keysComment));
}
handlers.add(ShowHelpCommand.addFocusHandler(new FocusHandler() {
@Override
public void onFocus(FocusEvent event) {
cmB.focus();
}
}));
}
private void display(final CommentsCollections comments) {
setThemeStyles(prefs.theme().isDark());
setShowIntraline(prefs.intralineDifference());
if (prefs.showLineNumbers()) {
diffTable.addStyleName(DiffTable.style.showLineNumbers());
}
cmA = newCM(diff.metaA(), diff.textA(), diffTable.cmA);
cmB = newCM(diff.metaB(), diff.textB(), diffTable.cmB);
cmA.extras().side(DisplaySide.A);
cmB.extras().side(DisplaySide.B);
setShowTabs(prefs.showTabs());
chunkManager = new ChunkManager(this, cmA, cmB, diffTable.scrollbar);
skipManager = new SkipManager(this, commentManager);
operation(new Runnable() {
@Override
public void run() {
// Estimate initial CM3 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.getLineMapper());
prefsAction = new PreferencesAction(this, prefs);
header.init(prefsAction, getLinks(), diff.sideBySideWebLinks());
scrollSynchronizer.setAutoHideDiffTableHeader(prefs.autoHideDiffTableHeader());
if (prefs.syntaxHighlighting() && fileSize.compareTo(FileSize.SMALL) > 0) {
Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
@Override
public boolean execute() {
if (prefs.syntaxHighlighting() && isAttached()) {
setSyntaxHighlighting(prefs.syntaxHighlighting());
}
return false;
}
}, 250);
}
}
private List<InlineHyperlink> getLinks() {
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);
}
private CodeMirror newCM(
DiffInfo.FileMeta meta,
String contents,
Element parent) {
return CodeMirror.create(parent, Configuration.create()
.set("readOnly", true)
.set("cursorBlinkRate", prefs.cursorBlinkRate())
.set("cursorHeight", 0.85)
.set("lineNumbers", prefs.showLineNumbers())
.set("tabSize", prefs.tabSize())
.set("mode", fileSize == FileSize.SMALL ? getContentType(meta) : null)
.set("lineWrapping", prefs.lineWrapping())
.set("scrollbarStyle", "overlay")
.set("styleSelectedText", true)
.set("showTrailingSpace", prefs.showWhitespaceErrors())
.set("keyMap", "vim_ro")
.set("theme", prefs.theme().name().toLowerCase())
.set("value", meta != null ? contents : "")
.set("viewportMargin", renderEntireFile() ? POSITIVE_INFINITY : 10));
}
DiffInfo.IntraLineStatus getIntraLineStatus() {
return diff.intralineStatus();
}
boolean renderEntireFile() {
return prefs.renderEntireFile() && canRenderEntireFile(prefs);
}
boolean canRenderEntireFile(DiffPreferences prefs) {
// CodeMirror is too slow to layout an entire huge file.
return fileSize.compareTo(FileSize.HUGE) < 0
|| (prefs.context() != WHOLE_FILE_CONTEXT && prefs.context() < 100);
}
String getContentType() {
return getContentType(diff.metaB());
}
void setThemeStyles(boolean d) {
if (d) {
diffTable.addStyleName(DiffTable.style.dark());
} else {
diffTable.removeStyleName(DiffTable.style.dark());
}
}
void setShowTabs(boolean show) {
cmA.extras().showTabs(show);
cmB.extras().showTabs(show);
}
void setLineLength(int columns) {
cmA.extras().lineLength(columns);
cmB.extras().lineLength(columns);
}
void setShowLineNumbers(boolean b) {
cmA.setOption("lineNumbers", b);
cmB.setOption("lineNumbers", b);
if (b) {
diffTable.addStyleName(DiffTable.style.showLineNumbers());
} else {
diffTable.removeStyleName(DiffTable.style.showLineNumbers());
}
}
void setShowIntraline(boolean b) {
if (b && getIntraLineStatus() == DiffInfo.IntraLineStatus.OFF) {
reloadDiffInfo();
} else if (b) {
diffTable.removeStyleName(DiffTable.style.noIntraline());
} else {
diffTable.addStyleName(DiffTable.style.noIntraline());
}
}
private void toggleShowIntraline() {
prefs.intralineDifference(!prefs.intralineDifference());
setShowIntraline(prefs.intralineDifference());
prefsAction.update();
}
void setSyntaxHighlighting(boolean b) {
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);
}
}
void setContext(final int context) {
operation(new Runnable() {
@Override
public void run() {
skipManager.removeAll();
skipManager.render(context, diff);
updateRenderEntireFile();
}
});
}
void setAutoHideDiffHeader(boolean hide) {
scrollSynchronizer.setAutoHideDiffTableHeader(hide);
}
private void render(DiffInfo diff) {
header.setNoDiff(diff);
chunkManager.render(diff);
}
CodeMirror otherCm(CodeMirror me) {
return me == cmA ? cmB : cmA;
}
CodeMirror getCmFromSide(DisplaySide side) {
return side == DisplaySide.A ? cmA : cmB;
}
LineOnOtherInfo lineOnOther(DisplaySide side, int line) {
return chunkManager.getLineMapper().lineOnOther(side, line);
}
private 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 GutterClickHandler onGutterClick(final CodeMirror cm) {
return new GutterClickHandler() {
@Override
public void handle(CodeMirror instance, final int line, String gutter,
NativeEvent clickEvent) {
if (clickEvent.getButton() == NativeEvent.BUTTON_LEFT
&& !clickEvent.getMetaKey()
&& !clickEvent.getAltKey()
&& !clickEvent.getCtrlKey()
&& !clickEvent.getShiftKey()) {
cm.setCursor(Pos.create(line));
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
commentManager.newDraft(cm, line + 1);
}
});
}
}
};
}
private Runnable upToChange(final boolean openReplyBox) {
return new Runnable() {
@Override
public void run() {
CallbackGroup group = new CallbackGroup();
commentManager.saveAllDrafts(group);
group.done();
group.addListener(new GerritCallback<Void>() {
@Override
public void onSuccess(Void result) {
String b = base != null ? base.getId() : null;
String rev = revision.getId();
Gerrit.display(
PageLinks.toChange(changeId, b, rev),
new ChangeScreen(changeId, b, rev, openReplyBox,
FileTable.Mode.REVIEW));
}
});
}
};
}
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();
}
};
}
private Runnable maybePrevVimSearch(final CodeMirror cm) {
return new Runnable() {
@Override
public void run() {
if (cm.vim().hasSearchHighlight()) {
cm.vim().handleKey("N");
} else {
commentManager.commentNav(cm, Direction.NEXT).run();
}
}
};
}
private Runnable maybeNextVimSearch(final CodeMirror cm) {
return new Runnable() {
@Override
public void run() {
if (cm.vim().hasSearchHighlight()) {
cm.vim().handleKey("n");
} else {
chunkManager.diffChunkNav(cm, Direction.NEXT).run();
}
}
};
}
private int adjustCommitMessageLine(int line) {
/* When commit messages are shown in the side-by-side screen they include
a header block that looks like this:
1 Parent: deadbeef (Parent commit title)
2 Author: A. U. Thor <author@example.com>
3 AuthorDate: 2015-02-27 19:20:52 +0900
4 Commit: A. U. Thor <author@example.com>
5 CommitDate: 2015-02-27 19:20:52 +0900
6 [blank line]
7 Commit message title
8
9 Commit message body
10 ...
11 ...
If the commit is a merge commit, both parent commits are listed in the
first two lines instead of a 'Parent' line:
1 Merge Of: deadbeef (Parent 1 commit title)
2 beefdead (Parent 2 commit title)
*/
// Offset to compensate for header lines until the blank line
// after 'CommitDate'
int offset = 6;
// Adjust for merge commits, which have two parent lines
if (diff.textB().startsWith("Merge")) {
offset += 1;
}
// If the cursor is inside the header line, reset to the first line of the
// commit message. Otherwise if the cursor is on an actual line of the commit
// message, adjust the line number to compensate for the header lines, so the
// focus is on the correct line.
if (line <= offset) {
return 1;
} else {
return line - offset;
}
}
private Runnable openEditScreen(final CodeMirror cm) {
return new Runnable() {
@Override
public void run() {
LineHandle handle = cm.extras().activeLine();
int line = cm.getLineNumber(handle) + 1;
if (Patch.COMMIT_MSG.equals(path)) {
line = adjustCommitMessageLine(line);
}
String token = Dispatcher.toEditScreen(revision, path, line);
if (!Gerrit.isSignedIn()) {
Gerrit.doSignIn(token);
} else {
Gerrit.display(token);
}
}
};
}
void updateRenderEntireFile() {
cmA.removeKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
cmB.removeKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
boolean entireFile = renderEntireFile();
if (entireFile) {
cmA.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
cmB.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
}
cmA.setOption("viewportMargin", entireFile ? POSITIVE_INFINITY : 10);
cmB.setOption("viewportMargin", entireFile ? POSITIVE_INFINITY : 10);
}
void resizeCodeMirror() {
int hdr = header.getOffsetHeight() + diffTable.getHeaderHeight();
cmA.adjustHeight(hdr);
cmB.adjustHeight(hdr);
}
void syncScroll(DisplaySide masterSide) {
if (scrollSynchronizer != null) {
scrollSynchronizer.syncScroll(masterSide);
}
}
private String getContentType(DiffInfo.FileMeta meta) {
if (prefs.syntaxHighlighting() && meta != null
&& meta.contentType() != null) {
ModeInfo m = ModeInfo.findMode(meta.contentType(), path);
return m != null ? m.mime() : null;
}
return null;
}
private void injectMode(DiffInfo diffInfo, AsyncCallback<Void> cb) {
new ModeInjector()
.add(getContentType(diffInfo.metaA()))
.add(getContentType(diffInfo.metaB()))
.inject(cb);
}
String getPath() {
return path;
}
DiffPreferences getPrefs() {
return prefs;
}
ChunkManager getChunkManager() {
return chunkManager;
}
CommentManager getCommentManager() {
return commentManager;
}
SkipManager getSkipManager() {
return skipManager;
}
void operation(final Runnable apply) {
cmA.operation(new Runnable() {
@Override
public void run() {
cmB.operation(new Runnable() {
@Override
public void run() {
apply.run();
}
});
}
});
}
private void prefetchNextFile() {
String nextPath = header.getNextPath();
if (nextPath != null) {
DiffApi.diff(revision, nextPath)
.base(base)
.wholeFile()
.intraline(prefs.intralineDifference())
.ignoreWhitespace(prefs.ignoreWhitespace())
.get(new AsyncCallback<DiffInfo>() {
@Override
public void onSuccess(DiffInfo info) {
new ModeInjector()
.add(getContentType(info.metaA()))
.add(getContentType(info.metaB()))
.inject(CallbackGroup.<Void> emptyCallback());
}
@Override
public void onFailure(Throwable caught) {
}
});
}
}
void reloadDiffInfo() {
final int id = ++reloadVersionId;
DiffApi.diff(revision, path)
.base(base)
.wholeFile()
.intraline(prefs.intralineDifference())
.ignoreWhitespace(prefs.ignoreWhitespace())
.get(new GerritCallback<DiffInfo>() {
@Override
public void onSuccess(DiffInfo diffInfo) {
if (id == reloadVersionId && isAttached()) {
diff = diffInfo;
operation(new Runnable() {
@Override
public void run() {
skipManager.removeAll();
chunkManager.reset();
diffTable.scrollbar.removeDiffAnnotations();
setShowIntraline(prefs.intralineDifference());
render(diff);
chunkManager.adjustPadding();
skipManager.render(prefs.context(), diff);
}
});
}
}
});
}
private static FileSize bucketFileSize(DiffInfo diff) {
FileMeta a = diff.metaA();
FileMeta b = diff.metaB();
FileSize[] sizes = FileSize.values();
for (int i = sizes.length - 1; 0 <= i; i--) {
FileSize s = sizes[i];
if ((a != null && s.lines <= a.lines())
|| (b != null && s.lines <= b.lines())) {
return s;
}
}
return FileSize.SMALL;
}
}