| // 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.reviewdb.client.AccountDiffPreference.WHOLE_FILE_CONTEXT; |
| import static java.lang.Double.POSITIVE_INFINITY; |
| |
| 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.ChangeScreen2; |
| import com.google.gerrit.client.changes.ChangeApi; |
| import com.google.gerrit.client.changes.ChangeInfo; |
| import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo; |
| 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.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.Screen; |
| import com.google.gerrit.common.PageLinks; |
| import com.google.gerrit.extensions.common.ListChangesOption; |
| import com.google.gerrit.reviewdb.client.Change; |
| 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.dom.client.Style; |
| 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.DOM; |
| 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.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.LineClassWhere; |
| import net.codemirror.lib.CodeMirror.LineHandle; |
| import net.codemirror.lib.Configuration; |
| import net.codemirror.lib.KeyMap; |
| import net.codemirror.lib.LineCharacter; |
| import net.codemirror.lib.ModeInjector; |
| |
| import java.util.ArrayList; |
| import java.util.EnumSet; |
| import java.util.List; |
| |
| public class SideBySide2 extends Screen { |
| private static final KeyMap RENDER_ENTIRE_FILE_KEYMAP = KeyMap.create() |
| .on("Ctrl-F", false); |
| |
| interface Binder extends UiBinder<FlowPanel, SideBySide2> {} |
| 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 CodeMirror cmA; |
| private CodeMirror cmB; |
| private Element columnMarginA; |
| private Element columnMarginB; |
| private double charWidthPx; |
| private double lineHeightPx; |
| |
| private HandlerRegistration resizeHandler; |
| private ScrollSynchronizer scrollSynchronizer; |
| private DiffInfo diff; |
| private FileSize fileSize; |
| 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 SideBySide2( |
| 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.getAccountDiffPreference()); |
| 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 cmGroup = new CallbackGroup(); |
| CodeMirror.initLibrary(cmGroup.add(CallbackGroup.<Void> emptyCallback())); |
| final CallbackGroup group = new CallbackGroup(); |
| final AsyncCallback<Void> modeInjectorCb = |
| group.add(CallbackGroup.<Void> emptyCallback()); |
| |
| DiffApi.diff(revision, path) |
| .base(base) |
| .wholeFile() |
| .intraline(prefs.intralineDifference()) |
| .ignoreWhitespace(prefs.ignoreWhitespace()) |
| .get(cmGroup.addFinal(new GerritCallback<DiffInfo>() { |
| @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); |
| } |
| } |
| })); |
| |
| final CommentsCollections comments = new CommentsCollections(); |
| comments.load(base, revision, path, group); |
| |
| RestApi call = ChangeApi.detail(changeId.get()); |
| ChangeList.addOptions(call, EnumSet.of( |
| ListChangesOption.ALL_REVISIONS)); |
| call.get(group.add(new GerritCallback<ChangeInfo>() { |
| @Override |
| public void onSuccess(ChangeInfo info) { |
| info.revisions().copyKeysIntoChildren("name"); |
| JsArray<RevisionInfo> list = info.revisions().values(); |
| RevisionInfo.sortRevisionInfoByNumber(list); |
| diffTable.set(prefs, list, diff); |
| header.setChangeInfo(info); |
| }})); |
| |
| ConfigInfoCache.get(changeId, group.addFinal( |
| new ScreenLoadCallback<ConfigInfoCache.Entry>(SideBySide2.this) { |
| @Override |
| protected void preDisplay(ConfigInfoCache.Entry result) { |
| commentManager = new CommentManager( |
| SideBySide2.this, |
| base, revision, path, |
| result.getCommentLinkProcessor()); |
| 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(); |
| } |
| }); |
| |
| final int height = getCodeMirrorHeight(); |
| operation(new Runnable() { |
| @Override |
| public void run() { |
| cmA.setHeight(height); |
| cmB.setHeight(height); |
| chunkManager.adjustPadding(); |
| cmA.refresh(); |
| cmB.refresh(); |
| } |
| }); |
| setLineLength(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) { |
| int line = startLine - 1; |
| CodeMirror cm = getCmFromSide(startSide); |
| if (cm.lineAtHeight(height - 20) < line) { |
| cm.scrollToY(cm.heightAtLine(line, "local") - 0.5 * height); |
| } |
| cm.setCursor(LineCharacter.create(line)); |
| cm.focus(); |
| } else { |
| cmA.setCursor(LineCharacter.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("beforeSelectionChange", onSelectionChange(cm)); |
| cm.on("cursorActivity", updateActiveLine(cm)); |
| cm.on("gutterClick", onGutterClick(cm)); |
| cm.on("focus", updateActiveLine(cm)); |
| cm.addKeyMap(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("C", commentManager.insertNewDraft(cm)) |
| .on("N", maybeNextVimSearch(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() { |
| 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() { |
| CodeMirror.handleVimKey(cm, "<C-d>"); |
| } |
| }) |
| .on("Shift-Space", new Runnable() { |
| @Override |
| public void run() { |
| CodeMirror.handleVimKey(cm, "<C-u>"); |
| } |
| }) |
| .on("Ctrl-F", new Runnable() { |
| @Override |
| public void run() { |
| CodeMirror.handleVimKey(cm, "/"); |
| } |
| }) |
| .on("Ctrl-A", new Runnable() { |
| @Override |
| public void run() { |
| cm.execCommand("selectAll"); |
| } |
| })); |
| if (prefs.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, LineCharacter anchor, LineCharacter head) { |
| if (anchor == head |
| || (anchor.getLine() == head.getLine() |
| && anchor.getCh() == head.getCh())) { |
| 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(LineCharacter anchor) { |
| bubble = new InsertCommentBubble(commentManager, cm); |
| add(bubble); |
| cm.addWidget(anchor, bubble.getElement(), false); |
| } |
| }; |
| } |
| |
| @Override |
| public void registerKeys() { |
| super.registerKeys(); |
| |
| keysNavigation.add(new UpToChangeCommand2(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, 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()); |
| setShowTabs(prefs.showTabs()); |
| setShowIntraline(prefs.intralineDifference()); |
| if (prefs.showLineNumbers()) { |
| diffTable.addStyleName(DiffTable.style.showLineNumbers()); |
| } |
| |
| cmA = newCM(diff.meta_a(), diff.text_a(), DisplaySide.A, diffTable.cmA); |
| cmB = newCM(diff.meta_b(), diff.text_b(), DisplaySide.B, diffTable.cmB); |
| diffTable.overview.init(cmB); |
| chunkManager = new ChunkManager(this, cmA, cmB, diffTable.overview); |
| skipManager = new SkipManager(this, commentManager); |
| |
| columnMarginA = DOM.createDiv(); |
| columnMarginB = DOM.createDiv(); |
| columnMarginA.setClassName(DiffTable.style.columnMargin()); |
| columnMarginB.setClassName(DiffTable.style.columnMargin()); |
| cmA.getMoverElement().appendChild(columnMarginA); |
| cmB.getMoverElement().appendChild(columnMarginB); |
| |
| if (prefs.renderEntireFile() && !canEnableRenderEntireFile(prefs)) { |
| // CodeMirror is too slow to layout an entire huge file. |
| prefs.renderEntireFile(false); |
| } |
| |
| operation(new Runnable() { |
| 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); |
| |
| 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 CodeMirror newCM( |
| DiffInfo.FileMeta meta, |
| String contents, |
| DisplaySide side, |
| Element parent) { |
| String mode = fileSize == FileSize.SMALL |
| ? getContentType(meta) |
| : null; |
| return CodeMirror.create(side, parent, Configuration.create() |
| .set("readOnly", true) |
| .set("cursorBlinkRate", 0) |
| .set("cursorHeight", 0.85) |
| .set("lineNumbers", prefs.showLineNumbers()) |
| .set("tabSize", prefs.tabSize()) |
| .set("mode", mode) |
| .set("lineWrapping", false) |
| .set("styleSelectedText", true) |
| .set("showTrailingSpace", prefs.showWhitespaceErrors()) |
| .set("keyMap", "vim_ro") |
| .set("theme", prefs.theme().name().toLowerCase()) |
| .set("value", meta != null ? contents : "") |
| .set("viewportMargin", prefs.renderEntireFile() ? POSITIVE_INFINITY : 10)); |
| } |
| |
| DiffInfo.IntraLineStatus getIntraLineStatus() { |
| return diff.intraline_status(); |
| } |
| |
| boolean canEnableRenderEntireFile(DiffPreferences prefs) { |
| return fileSize.compareTo(FileSize.HUGE) < 0 |
| || (prefs.context() != WHOLE_FILE_CONTEXT && prefs.context() < 100); |
| } |
| |
| String getContentType() { |
| return getContentType(diff.meta_b()); |
| } |
| |
| void setThemeStyles(boolean d) { |
| if (d) { |
| diffTable.addStyleName(DiffTable.style.dark()); |
| } else { |
| diffTable.removeStyleName(DiffTable.style.dark()); |
| } |
| } |
| |
| void setShowTabs(boolean b) { |
| if (b) { |
| diffTable.addStyleName(DiffTable.style.showTabs()); |
| } else { |
| diffTable.removeStyleName(DiffTable.style.showTabs()); |
| } |
| } |
| |
| void setLineLength(int columns) { |
| double w = columns * getCharWidthPx(); |
| columnMarginA.getStyle().setMarginLeft(w, Style.Unit.PX); |
| columnMarginB.getStyle().setMarginLeft(w, Style.Unit.PX); |
| } |
| |
| double getLineHeightPx() { |
| if (lineHeightPx <= 1) { |
| Element p = DOM.createDiv(); |
| int lines = 1; |
| for (int i = 0; i < lines; i++) { |
| Element e = DOM.createDiv(); |
| p.appendChild(e); |
| |
| Element pre = DOM.createElement("pre"); |
| pre.setInnerText("gqyŚŻŹŃ"); |
| e.appendChild(pre); |
| } |
| |
| cmB.getMeasureElement().appendChild(p); |
| lineHeightPx = ((double) p.getOffsetHeight()) / lines; |
| p.removeFromParent(); |
| } |
| return lineHeightPx; |
| } |
| |
| private double getCharWidthPx() { |
| if (charWidthPx <= 1) { |
| int len = 100; |
| StringBuilder s = new StringBuilder(); |
| for (int i = 0; i < len; i++) { |
| s.append('m'); |
| } |
| Element e = DOM.createSpan(); |
| e.getStyle().setDisplay(Style.Display.INLINE_BLOCK); |
| e.setInnerText(s.toString()); |
| |
| cmA.getMeasureElement().appendChild(e); |
| double a = ((double) e.getOffsetWidth()) / len; |
| e.removeFromParent(); |
| |
| cmB.getMeasureElement().appendChild(e); |
| double b = ((double) e.getOffsetWidth()) / len; |
| e.removeFromParent(); |
| charWidthPx = Math.max(a, b); |
| } |
| return charWidthPx; |
| } |
| |
| 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.meta_a())); |
| cmB.setOption("mode", getContentType(diff.meta_b())); |
| } |
| } |
| |
| @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); |
| diffTable.overview.refresh(); |
| } |
| }); |
| } |
| |
| 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 void clearActiveLine(CodeMirror cm) { |
| if (cm.hasActiveLine()) { |
| LineHandle activeLine = cm.getActiveLine(); |
| cm.removeLineClass(activeLine, |
| LineClassWhere.WRAP, DiffTable.style.activeLine()); |
| cm.setActiveLine(null); |
| } |
| } |
| |
| private Runnable updateActiveLine(final CodeMirror cm) { |
| final CodeMirror other = otherCm(cm); |
| return new Runnable() { |
| 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() { |
| public void run() { |
| LineHandle handle = cm.getLineHandleVisualStart( |
| cm.getCursor("end").getLine()); |
| if (cm.hasActiveLine() && cm.getActiveLine().equals(handle)) { |
| return; |
| } |
| |
| clearActiveLine(cm); |
| clearActiveLine(other); |
| cm.setActiveLine(handle); |
| cm.addLineClass( |
| handle, LineClassWhere.WRAP, DiffTable.style.activeLine()); |
| LineOnOtherInfo info = |
| lineOnOther(cm.side(), cm.getLineNumber(handle)); |
| if (info.isAligned()) { |
| LineHandle oLineHandle = other.getLineHandle(info.getLine()); |
| other.setActiveLine(oLineHandle); |
| other.addLineClass(oLineHandle, LineClassWhere.WRAP, |
| DiffTable.style.activeLine()); |
| } |
| } |
| }); |
| } |
| }); |
| } |
| }; |
| } |
| |
| private GutterClickHandler onGutterClick(final CodeMirror cm) { |
| return new GutterClickHandler() { |
| @Override |
| public void handle(CodeMirror instance, int line, String gutter, |
| NativeEvent clickEvent) { |
| if (clickEvent.getButton() == NativeEvent.BUTTON_LEFT |
| && !clickEvent.getMetaKey() |
| && !clickEvent.getAltKey() |
| && !clickEvent.getCtrlKey() |
| && !clickEvent.getShiftKey()) { |
| if (!(cm.hasActiveLine() && |
| cm.getLineNumber(cm.getActiveLine()) == line)) { |
| cm.setCursor(LineCharacter.create(line)); |
| } |
| Scheduler.get().scheduleDeferred(new ScheduledCommand() { |
| @Override |
| public void execute() { |
| commentManager.insertNewDraft(cm).run(); |
| } |
| }); |
| } |
| } |
| }; |
| } |
| |
| private Runnable upToChange(final boolean openReplyBox) { |
| return new Runnable() { |
| 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 ? String.valueOf(base.get()) : null; |
| String rev = String.valueOf(revision.get()); |
| Gerrit.display( |
| PageLinks.toChange(changeId, b, rev), |
| new ChangeScreen2(changeId, b, rev, openReplyBox)); |
| } |
| }); |
| } |
| }; |
| } |
| |
| 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() { |
| public void run() { |
| if (cmSrc.hasActiveLine()) { |
| cmDst.setCursor(LineCharacter.create(lineOnOther( |
| sideSrc, |
| cmSrc.getLineNumber(cmSrc.getActiveLine())).getLine())); |
| } |
| cmDst.focus(); |
| } |
| }; |
| } |
| |
| private Runnable maybePrevVimSearch(final CodeMirror cm) { |
| return new Runnable() { |
| @Override |
| public void run() { |
| if (cm.hasVimSearchHighlight()) { |
| CodeMirror.handleVimKey(cm, "N"); |
| } else { |
| commentManager.commentNav(cm, Direction.NEXT).run(); |
| } |
| } |
| }; |
| } |
| |
| private Runnable maybeNextVimSearch(final CodeMirror cm) { |
| return new Runnable() { |
| @Override |
| public void run() { |
| if (cm.hasVimSearchHighlight()) { |
| CodeMirror.handleVimKey(cm, "n"); |
| } else { |
| chunkManager.diffChunkNav(cm, Direction.NEXT).run(); |
| } |
| } |
| }; |
| } |
| |
| void updateRenderEntireFile() { |
| cmA.removeKeyMap(RENDER_ENTIRE_FILE_KEYMAP); |
| cmB.removeKeyMap(RENDER_ENTIRE_FILE_KEYMAP); |
| if (prefs.renderEntireFile()) { |
| cmA.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP); |
| cmB.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP); |
| } |
| |
| cmA.setOption("viewportMargin", prefs.renderEntireFile() ? POSITIVE_INFINITY : 10); |
| cmB.setOption("viewportMargin", prefs.renderEntireFile() ? POSITIVE_INFINITY : 10); |
| } |
| |
| void resizeCodeMirror() { |
| int height = getCodeMirrorHeight(); |
| cmA.setHeight(height); |
| cmB.setHeight(height); |
| diffTable.overview.refresh(); |
| } |
| |
| private int getCodeMirrorHeight() { |
| int rest = Gerrit.getHeaderFooterHeight() |
| + header.getOffsetHeight() |
| + diffTable.getHeaderHeight() |
| + 5; // Estimate |
| return Window.getClientHeight() - rest; |
| } |
| |
| void syncScroll(DisplaySide masterSide) { |
| if (scrollSynchronizer != null) { |
| scrollSynchronizer.syncScroll(masterSide); |
| } |
| } |
| |
| private String getContentType(DiffInfo.FileMeta meta) { |
| return prefs.syntaxHighlighting() |
| && meta != null |
| && meta.content_type() != null |
| ? ModeInjector.getContentType(meta.content_type()) |
| : null; |
| } |
| |
| private void injectMode(DiffInfo diffInfo, AsyncCallback<Void> cb) { |
| new ModeInjector() |
| .add(getContentType(diffInfo.meta_a())) |
| .add(getContentType(diffInfo.meta_b())) |
| .inject(cb); |
| } |
| |
| 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.meta_a())) |
| .add(getContentType(info.meta_b())) |
| .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.overview.clearDiffMarkers(); |
| setShowIntraline(prefs.intralineDifference()); |
| render(diff); |
| chunkManager.adjustPadding(); |
| skipManager.render(prefs.context(), diff); |
| } |
| }); |
| } |
| } |
| }); |
| } |
| |
| private static FileSize bucketFileSize(DiffInfo diff) { |
| FileMeta a = diff.meta_a(); |
| FileMeta b = diff.meta_b(); |
| 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; |
| } |
| } |