Files
gerrit/gerrit-gwtui/src/main/java/net/codemirror/lib/Extras.java
Gabor Somossy b72d4c6d8f Allow file annotations (blame) in side-by-side diff
Using CodeMirror's lint addon on the UI to display the blame
annotations. It works on both sides of the side-by-side
diff and also supports the auto-merge commit. It requires manual
step to enable the annotations to avoid any unnecessary git
processing and network traffic between the server and client.

Introduces a new dependency on blame-cache in gerrit-server
to reuse BlameCache.

In a following change, the gutter showing the blame info will be
made clickable. Clicking on the gutter will open a new tab that
takes the user to the corresponding change in Gerrit.

The commit SHA-1 hashes are currently not selectable. Making it
so might require an upstream change in CodeMirror's lint addon.

Bug: Issue 1642
Change-Id: I6267d30cbee448f8137e11c7120959dc424eaeeb
2016-04-25 08:38:55 -04:00

223 lines
6.5 KiB
Java

// Copyright (C) 2015 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 static com.google.gwt.dom.client.Style.Display.INLINE_BLOCK;
import static com.google.gwt.dom.client.Style.Unit.PX;
import static net.codemirror.lib.CodeMirror.style;
import static net.codemirror.lib.CodeMirror.LineClassWhere.WRAP;
import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.RangeInfo;
import com.google.gerrit.client.blame.BlameInfo;
import com.google.gerrit.client.diff.DisplaySide;
import com.google.gerrit.client.rpc.Natives;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.dom.client.Element;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.user.client.DOM;
import net.codemirror.lib.CodeMirror.LineHandle;
import java.util.Date;
import java.util.Objects;
/** Additional features added to CodeMirror by Gerrit Code Review. */
public class Extras {
public static final String ANNOTATION_GUTTER_ID = "CodeMirror-lint-markers";
static final BlameConfig C = GWT.create(BlameConfig.class);
static final native Extras get(CodeMirror c) /*-{ return c.gerritExtras }-*/;
private static native void set(CodeMirror c, Extras e)
/*-{ c.gerritExtras = e }-*/;
static void attach(CodeMirror c) {
set(c, new Extras(c));
}
private final CodeMirror cm;
private Element margin;
private DisplaySide side;
private double charWidthPx;
private double lineHeightPx;
private LineHandle activeLine;
private boolean annotated;
private Extras(CodeMirror cm) {
this.cm = cm;
}
public DisplaySide side() {
return side;
}
public void side(DisplaySide s) {
side = s;
}
public double charWidthPx() {
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(INLINE_BLOCK);
e.setInnerText(s.toString());
cm.measure().appendChild(e);
charWidthPx = ((double) e.getOffsetWidth()) / len;
e.removeFromParent();
}
return charWidthPx;
}
public double lineHeightPx() {
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);
}
cm.measure().appendChild(p);
lineHeightPx = ((double) p.getOffsetHeight()) / lines;
p.removeFromParent();
}
return lineHeightPx;
}
public void lineLength(int columns) {
if (margin == null) {
margin = DOM.createDiv();
margin.setClassName(style().margin());
cm.mover().appendChild(margin);
}
margin.getStyle().setMarginLeft(columns * charWidthPx(), PX);
}
public void showTabs(boolean show) {
Element e = cm.getWrapperElement();
if (show) {
e.addClassName(style().showTabs());
} else {
e.removeClassName(style().showTabs());
}
}
public final boolean hasActiveLine() {
return activeLine != null;
}
public final LineHandle activeLine() {
return activeLine;
}
public final boolean activeLine(LineHandle line) {
if (Objects.equals(activeLine, line)) {
return false;
}
if (activeLine != null) {
cm.removeLineClass(activeLine, WRAP, style().activeLine());
}
activeLine = line;
cm.addLineClass(activeLine, WRAP, style().activeLine());
return true;
}
public final void clearActiveLine() {
if (activeLine != null) {
cm.removeLineClass(activeLine, WRAP, style().activeLine());
activeLine = null;
}
}
public boolean isAnnotated() {
return annotated;
}
public final void clearAnnotations() {
JsArrayString gutters = ((JsArrayString) JsArrayString.createArray());
cm.setOption("gutters", gutters);
annotated = false;
}
public final void setAnnotations(JsArray<BlameInfo> blameInfos) {
if (blameInfos.length() > 0) {
setBlameInfo(blameInfos);
JsArrayString gutters = ((JsArrayString) JsArrayString.createArray());
gutters.push(ANNOTATION_GUTTER_ID);
cm.setOption("gutters", gutters);
annotated = true;
DateTimeFormat format = DateTimeFormat.getFormat(
DateTimeFormat.PredefinedFormat.DATE_SHORT);
JsArray<LintLine> annotations = JsArray.createArray().cast();
for (BlameInfo blameInfo : Natives.asList(blameInfos)) {
for (RangeInfo range : Natives.asList(blameInfo.ranges())) {
Date commitTime = new Date(blameInfo.time() * 1000L);
String shortId = blameInfo.id().substring(0, 8);
String shortBlame = C.shortBlameMsg(
shortId, format.format(commitTime), blameInfo.author());
String detailedBlame = C.detailedBlameMsg(blameInfo.id(),
blameInfo.author(), FormatUtil.mediumFormat(commitTime),
blameInfo.commitMsg());
annotations.push(LintLine.create(shortBlame, detailedBlame, shortId,
Pos.create(range.start() - 1)));
}
}
cm.setOption("lint", getAnnotation(annotations));
}
}
private native JavaScriptObject getAnnotation(JsArray<LintLine> annotations) /*-{
return {
getAnnotations: function(text, options, cm) { return annotations; }
};
}-*/;
public final native JsArray<BlameInfo> getBlameInfo() /*-{
return this.blameInfos;
}-*/;
public final native void setBlameInfo(JsArray<BlameInfo> blameInfos) /*-{
this['blameInfos'] = blameInfos;
}-*/;
public final void toggleAnnotation() {
toggleAnnotation(getBlameInfo());
}
public final void toggleAnnotation(JsArray<BlameInfo> blameInfos) {
if (isAnnotated()) {
clearAnnotations();
} else {
setAnnotations(blameInfos);
}
}
}