package org.jetbrains.idea.svn.difftool.properties;
import com.intellij.diff.DiffContentFactory;
import com.intellij.diff.DiffContext;
import com.intellij.diff.comparison.ComparisonManager;
import com.intellij.diff.comparison.ComparisonPolicy;
import com.intellij.diff.comparison.DiffTooBigException;
import com.intellij.diff.contents.DiffContent;
import com.intellij.diff.contents.DocumentContent;
import com.intellij.diff.fragments.DiffFragment;
import com.intellij.diff.fragments.LineFragment;
import com.intellij.diff.fragments.LineFragmentImpl;
import com.intellij.diff.requests.ContentDiffRequest;
import com.intellij.diff.tools.util.DiffSplitter;
import com.intellij.diff.tools.util.SyncScrollSupport;
import com.intellij.diff.tools.util.side.TwosideTextDiffViewer;
import com.intellij.diff.util.*;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.EditorSettings;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.impl.DocumentImpl;
import com.intellij.openapi.editor.markup.SeparatorPlacement;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.util.EmptyRunnable;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.FileStatus;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBLabel;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.hash.HashMap;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.properties.PropertyData;
import org.jetbrains.idea.svn.properties.PropertyValue;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class SvnPropertiesDiffViewer extends TwosideTextDiffViewer {
@NotNull private final WrapperRequest myWrapperRequest;
@NotNull private final List<DiffChange> myDiffChanges;
private boolean myFirstRediff = true;
@NotNull
public static SvnPropertiesDiffViewer create(@NotNull DiffContext context, @NotNull SvnPropertiesDiffRequest request) {
return create(context, request, false);
}
@NotNull
public static SvnPropertiesDiffViewer create(@NotNull DiffContext context, @NotNull SvnPropertiesDiffRequest request, boolean embedded) {
Pair<WrapperRequest, List<DiffChange>> pair = convertRequest(request, embedded);
return new SvnPropertiesDiffViewer(context, pair.first, pair.second);
}
private SvnPropertiesDiffViewer(@NotNull DiffContext context, @NotNull WrapperRequest request, @NotNull List<DiffChange> diffChanges) {
super(context, request);
myWrapperRequest = request;
myDiffChanges = diffChanges;
for (EditorEx editor : getEditors()) {
if (editor == null) continue;
EditorSettings settings = editor.getSettings();
settings.setAdditionalLinesCount(0);
settings.setAdditionalColumnsCount(1);
settings.setRightMarginShown(false);
settings.setFoldingOutlineShown(false);
settings.setLineNumbersShown(false);
settings.setLineMarkerAreaShown(false);
settings.setIndentGuidesShown(false);
settings.setVirtualSpace(false);
settings.setWheelFontChangeEnabled(false);
settings.setAdditionalPageAtBottom(false);
settings.setCaretRowShown(false);
settings.setUseSoftWraps(false);
editor.reinitSettings();
}
for (DiffChange change : myDiffChanges) {
DiffDrawUtil.createBorderLineMarker(getEditor(Side.LEFT), change.myEndLine1, SeparatorPlacement.TOP);
DiffDrawUtil.createBorderLineMarker(getEditor(Side.RIGHT), change.myEndLine2, SeparatorPlacement.TOP);
}
DiffSplitter splitter = myContentPanel.getSplitter();
splitter.setDividerWidth(120);
splitter.setShowDividerIcon(false);
}
@Override
protected void onInit() {
super.onInit();
myContentPanel.setPainter(new MyDividerPainter());
}
@NotNull
@Override
protected Runnable performRediff(@NotNull ProgressIndicator indicator) {
if (!myFirstRediff) return new EmptyRunnable();
myFirstRediff = false;
for (DiffChange change : myDiffChanges) {
PropertyRecord record = change.getRecord();
String before = record.getBefore();
String after = record.getAfter();
assert before != null || after != null;
if (before == null) {
change.setFragments(createEverythingChanged(0, after.length(), 0, StringUtil.countNewLines(after) + 1));
continue;
}
if (after == null) {
change.setFragments(createEverythingChanged(before.length(), 0, StringUtil.countNewLines(before) + 1, 0));
continue;
}
try {
ComparisonManager manager = ComparisonManager.getInstance();
change.setFragments(manager.squash(manager.compareLinesInner(before, after, ComparisonPolicy.DEFAULT, indicator)));
}
catch (DiffTooBigException e) {
change.setFragments(createEverythingChanged(before.length(), after.length(),
StringUtil.countNewLines(before) + 1, StringUtil.countNewLines(after) + 1));
}
}
return () -> {
for (DiffChange change : myDiffChanges) {
setupHighlighting(change, Side.LEFT);
setupHighlighting(change, Side.RIGHT);
}
};
}
private void setupHighlighting(@NotNull DiffChange change, @NotNull Side side) {
PropertyRecord record = change.getRecord();
List<? extends LineFragment> fragments = change.getFragments();
assert fragments != null;
EditorEx editor = getEditor(side);
DocumentEx document = editor.getDocument();
int changeStartLine = change.getStartLine(side);
for (LineFragment fragment : fragments) {
List<DiffFragment> innerFragments = fragment.getInnerFragments();
int startLine = side.getStartLine(fragment) + changeStartLine;
int endLine = side.getEndLine(fragment) + changeStartLine;
int start = document.getLineStartOffset(startLine);
TextDiffType type = DiffUtil.getLineDiffType(fragment);
DiffDrawUtil.createHighlighter(editor, startLine, endLine, type, innerFragments != null);
// TODO: we can paint LineMarker here, but it looks ugly for small editors
if (innerFragments != null) {
for (DiffFragment innerFragment : innerFragments) {
int innerStart = side.getStartOffset(innerFragment);
int innerEnd = side.getEndOffset(innerFragment);
TextDiffType innerType = DiffUtil.getDiffType(innerFragment);
innerStart += start;
innerEnd += start;
DiffDrawUtil.createInlineHighlighter(editor, innerStart, innerEnd, innerType);
}
}
}
}
@NotNull
private static List<? extends LineFragment> createEverythingChanged(int length1, int length2, int lines1, int lines2) {
return Collections.singletonList(new LineFragmentImpl(0, lines1, 0, lines2, 0, length1, 0, length2));
}
private class MyDividerPainter implements DiffSplitter.Painter, DiffDividerDrawUtil.DividerPaintable {
@NotNull private final JBLabel myLabel;
public MyDividerPainter() {
myLabel = new JBLabel();
myLabel.setFont(UIUtil.getLabelFont());
myLabel.setHorizontalAlignment(SwingConstants.CENTER);
myLabel.setVerticalAlignment(SwingConstants.TOP);
}
@Override
public void paint(@NotNull Graphics g, @NotNull JComponent divider) {
Graphics2D gg = DiffDividerDrawUtil.getDividerGraphics(g, divider, getEditor1().getComponent());
Rectangle clip = gg.getClipBounds();
if (clip == null) return;
gg.setColor(DiffDrawUtil.getDividerColor());
gg.fill(clip);
EditorEx editor1 = getEditor1();
EditorEx editor2 = getEditor2();
JComponent header1 = editor1.getHeaderComponent();
JComponent header2 = editor2.getHeaderComponent();
int headerOffset1 = header1 == null ? 0 : header1.getHeight();
int headerOffset2 = header2 == null ? 0 : header2.getHeight();
// TODO: painting is ugly if shift1 != shift2 (ex: search field is opened for one of editors)
int shift1 = editor1.getScrollingModel().getVerticalScrollOffset() - headerOffset1;
int shift2 = editor2.getScrollingModel().getVerticalScrollOffset() - headerOffset2;
double rotate = shift1 == shift2 ? 0 : Math.atan2(shift2 - shift1, clip.width);
DiffDividerDrawUtil.paintPolygons(gg, divider.getWidth(), rotate == 0, editor1, editor2, this);
for (DiffChange change : myDiffChanges) {
int y1 = editor1.logicalPositionToXY(new LogicalPosition(change.getStartLine(Side.LEFT), 0)).y - shift1;
int y2 = editor2.logicalPositionToXY(new LogicalPosition(change.getStartLine(Side.RIGHT), 0)).y - shift2;
int endY1 = editor1.logicalPositionToXY(new LogicalPosition(change.getEndLine(Side.LEFT), 0)).y - shift1;
int endY2 = editor2.logicalPositionToXY(new LogicalPosition(change.getEndLine(Side.RIGHT), 0)).y - shift2;
AffineTransform oldTransform = gg.getTransform();
gg.translate(0, y1);
if (rotate != 0) gg.rotate(-rotate);
myLabel.setText(change.getRecord().getName());
myLabel.setForeground(getRecordTitleColor(change));
myLabel.setBounds(clip);
myLabel.paint(gg);
gg.setTransform(oldTransform);
gg.setColor(JBColor.border());
gg.drawLine(0, y1 - 1, clip.width, y2 - 1);
gg.drawLine(0, endY1 - 1, clip.width, endY2 - 1);
}
gg.dispose();
}
@Override
public void process(@NotNull Handler handler) {
for (DiffChange diffChange : myDiffChanges) {
TextDiffType type = getDiffType(diffChange);
if (type == null) continue;
int shift1 = diffChange.getStartLine(Side.LEFT);
int shift2 = diffChange.getStartLine(Side.RIGHT);
List<? extends LineFragment> fragments = diffChange.getFragments();
if (fragments == null) continue;
for (LineFragment fragment : diffChange.getFragments()) {
if (!handler.process(Side.LEFT.getStartLine(fragment) + shift1, Side.LEFT.getEndLine(fragment) + shift1,
Side.RIGHT.getStartLine(fragment) + shift2, Side.RIGHT.getEndLine(fragment) + shift2,
DiffUtil.getLineDiffType(fragment).getColor(getEditor1()))) {
return;
}
}
}
}
@Nullable
private Color getRecordTitleColor(@NotNull DiffChange change) {
TextDiffType type = getDiffType(change);
if (type == TextDiffType.INSERTED) return FileStatus.ADDED.getColor();
if (type == TextDiffType.DELETED) return FileStatus.DELETED.getColor();
if (type == TextDiffType.MODIFIED) return FileStatus.MODIFIED.getColor();
return JBColor.black; // unchanged
}
@Nullable
public TextDiffType getDiffType(@NotNull DiffChange change) {
if (change.getRecord().getBefore() == null) return TextDiffType.INSERTED;
if (change.getRecord().getAfter() == null) return TextDiffType.DELETED;
if (change.getFragments() != null && !change.getFragments().isEmpty()) return TextDiffType.MODIFIED;
return null; // unchanged
}
}
@Override
protected void onDocumentChange(@NotNull DocumentEvent event) {
LOG.warn("Document changes are not supported");
}
//
// Impl
//
@Nullable
@Override
protected SyncScrollSupport.SyncScrollable getSyncScrollable() {
return new SyncScrollSupport.SyncScrollable() {
@Override
public boolean isSyncScrollEnabled() {
return true;
}
@Override
public int transfer(@NotNull Side side, int line) {
return line;
}
};
}
@Nullable
@Override
public Object getData(@NonNls String dataId) {
if (PlatformDataKeys.HELP_ID.is(dataId)) {
return "topicId758145";
}
return super.getData(dataId);
}
//
// Initial step
//
@NotNull
private static Pair<WrapperRequest, List<DiffChange>> convertRequest(@NotNull SvnPropertiesDiffRequest request, boolean embedded) {
List<PropertyRecord> records = collectRecords(request);
StringBuilder builder1 = new StringBuilder();
StringBuilder builder2 = new StringBuilder();
List<DiffChange> diffChanges = new ArrayList<>();
int totalLines = 0;
for (PropertyRecord record : records) {
int start = totalLines;
String before = StringUtil.notNullize(record.getBefore());
String after = StringUtil.notNullize(record.getAfter());
builder1.append(before);
builder2.append(after);
int lines1 = StringUtil.countNewLines(before);
int lines2 = StringUtil.countNewLines(after);
int appendedLines = Math.max(lines1, lines2) + 1;
totalLines += appendedLines;
for (int i = lines1; i < appendedLines; i++) {
builder1.append('\n');
}
for (int i = lines2; i < appendedLines; i++) {
builder2.append('\n');
}
diffChanges.add(new DiffChange(record, start, totalLines, start, totalLines));
}
Document document1 = new DocumentImpl(builder1);
Document document2 = new DocumentImpl(builder2);
return Pair.create(new WrapperRequest(request, document1, document2, embedded), diffChanges);
}
@NotNull
private static List<PropertyRecord> collectRecords(@NotNull SvnPropertiesDiffRequest request) {
List<DiffContent> originalContents = request.getContents();
List<PropertyData> properties1 = getProperties(originalContents.get(0));
List<PropertyData> properties2 = getProperties(originalContents.get(1));
Map<String, PropertyValue> before = new HashMap<>();
Map<String, PropertyValue> after = new HashMap<>();
if (properties1 != null) {
for (PropertyData data : properties1) {
before.put(data.getName(), data.getValue());
}
}
if (properties2 != null) {
for (PropertyData data : properties2) {
after.put(data.getName(), data.getValue());
}
}
List<PropertyRecord> records = new ArrayList<>();
for (String name : ContainerUtil.union(before.keySet(), after.keySet())) {
records.add(createRecord(name, before.get(name), after.get(name)));
}
ContainerUtil.sort(records, (o1, o2) -> StringUtil.naturalCompare(o1.getName(), o2.getName()));
return records;
}
@Nullable
private static List<PropertyData> getProperties(@NotNull DiffContent content) {
if (content instanceof SvnPropertiesDiffRequest.PropertyContent) {
return ((SvnPropertiesDiffRequest.PropertyContent)content).getProperties();
}
return null;
}
@Nullable
private static PropertyRecord createRecord(@NotNull String name, @Nullable PropertyValue value1, @Nullable PropertyValue value2) {
assert value1 != null || value2 != null;
String text1 = value1 != null ? value1.toString() : null;
String text2 = value2 != null ? value2.toString() : null;
// TODO: show differences in line separators ?
if (text1 != null) text1 = StringUtil.convertLineSeparators(text1);
if (text2 != null) text2 = StringUtil.convertLineSeparators(text2);
return new PropertyRecord(name, text1, text2);
}
//
// Helpers
//
private static class WrapperRequest extends ContentDiffRequest {
@NotNull SvnPropertiesDiffRequest myRequest;
@NotNull DocumentContent myContent1;
@NotNull DocumentContent myContent2;
private final boolean myEmbedded;
public WrapperRequest(@NotNull SvnPropertiesDiffRequest request,
@NotNull Document document1,
@NotNull Document document2,
boolean embedded) {
myRequest = request;
myContent1 = DiffContentFactory.getInstance().create(null, document1);
myContent2 = DiffContentFactory.getInstance().create(null, document2);
myEmbedded = embedded;
putUserData(DiffUserDataKeys.FORCE_READ_ONLY, true);
}
@NotNull
public SvnPropertiesDiffRequest getPropertiesRequest() {
return myRequest;
}
@NotNull
@Override
public List<DiffContent> getContents() {
return ContainerUtil.list(myContent1, myContent2);
}
@NotNull
@Override
public List<String> getContentTitles() {
return myEmbedded ? ContainerUtil.list(null, null) : myRequest.getContentTitles();
}
@Nullable
@Override
public String getTitle() {
return myRequest.getTitle();
}
@Override
public <T> T getUserData(@NotNull Key<T> key) {
return myRequest.getUserData(key);
}
@Override
public <T> void putUserData(@NotNull Key<T> key, @Nullable T value) {
myRequest.putUserData(key, value);
}
}
private static class PropertyRecord {
@NotNull private final String myName;
@Nullable private final String myBefore;
@Nullable private final String myAfter;
public PropertyRecord(@NotNull String name,
@Nullable String before,
@Nullable String after) {
assert before != null || after != null;
myName = name;
myBefore = before;
myAfter = after;
}
@NotNull
public String getName() {
return myName;
}
@Nullable
public String getBefore() {
return myBefore;
}
@Nullable
public String getAfter() {
return myAfter;
}
}
private static class DiffChange {
@NotNull private final PropertyRecord myRecord;
private final int myStartLine1;
private final int myEndLine1;
private final int myStartLine2;
private final int myEndLine2;
@Nullable private List<? extends LineFragment> myFragments;
public DiffChange(@NotNull PropertyRecord record, int startLine1, int endLine1, int startLine2, int endLine2) {
myRecord = record;
myStartLine1 = startLine1;
myEndLine1 = endLine1;
myStartLine2 = startLine2;
myEndLine2 = endLine2;
}
@NotNull
public PropertyRecord getRecord() {
return myRecord;
}
public int getStartLine(@NotNull Side side) {
return side.select(myStartLine1, myStartLine2);
}
public int getEndLine(@NotNull Side side) {
return side.select(myEndLine1, myEndLine2);
}
public void setFragments(@Nullable List<? extends LineFragment> fragments) {
myFragments = fragments;
}
@Nullable
public List<? extends LineFragment> getFragments() {
return myFragments;
}
}
}