IDEA-286601 vcs: inline diff from annotation

* Add generic implementation for all vcses

GitOrigin-RevId: 4af117687a1d57fc586d4a094a6d37e3492f855c
This commit is contained in:
Aleksey Pivovarov
2021-12-22 18:57:05 +03:00
committed by intellij-monorepo-bot
parent 7ea6f25dad
commit 33b92c8d65
4 changed files with 246 additions and 105 deletions

View File

@@ -0,0 +1,223 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.vcs.annotate;
import com.intellij.diff.comparison.ComparisonManager;
import com.intellij.diff.comparison.ComparisonPolicy;
import com.intellij.diff.fragments.DiffFragment;
import com.intellij.diff.fragments.LineFragment;
import com.intellij.diff.tools.util.text.LineOffsets;
import com.intellij.diff.tools.util.text.LineOffsetsUtil;
import com.intellij.openapi.diff.DiffNavigationContext;
import com.intellij.openapi.progress.DumbProgressIndicator;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressIndicatorProvider;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.annotate.AnnotatedLineModificationDetails.InnerChange;
import com.intellij.openapi.vcs.annotate.AnnotatedLineModificationDetails.InnerChangeType;
import com.intellij.openapi.vcs.history.VcsFileRevision;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.vcsUtil.VcsImplUtil;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static java.util.Collections.singletonList;
public class DefaultLineModificationDetailsProvider implements FileAnnotation.LineModificationDetailsProvider {
@NotNull private final FileAnnotation myAnnotation;
@NotNull private final FilePath myFilePath;
@NotNull private final FileAnnotation.CurrentFileRevisionProvider myCurrentRevisionProvider;
@NotNull private final FileAnnotation.PreviousFileRevisionProvider myPreviousRevisionProvider;
private DefaultLineModificationDetailsProvider(@NotNull FileAnnotation annotation,
@NotNull FilePath filePath,
@NotNull FileAnnotation.CurrentFileRevisionProvider currentRevisionProvider,
@NotNull FileAnnotation.PreviousFileRevisionProvider previousRevisionProvider) {
myAnnotation = annotation;
myFilePath = filePath;
myCurrentRevisionProvider = currentRevisionProvider;
myPreviousRevisionProvider = previousRevisionProvider;
}
@Nullable
public static FileAnnotation.LineModificationDetailsProvider create(@NotNull FileAnnotation annotation) {
VirtualFile file = annotation.getFile();
if (file == null) return null;
FileAnnotation.CurrentFileRevisionProvider currentRevisionProvider = annotation.getCurrentFileRevisionProvider();
FileAnnotation.PreviousFileRevisionProvider previousRevisionProvider = annotation.getPreviousFileRevisionProvider();
if (currentRevisionProvider == null || previousRevisionProvider == null) return null;
FilePath filePath = VcsUtil.getFilePath(file);
return new DefaultLineModificationDetailsProvider(annotation, filePath, currentRevisionProvider, previousRevisionProvider);
}
@Override
public @Nullable AnnotatedLineModificationDetails getDetails(int lineNumber) throws VcsException {
String annotatedContent = myAnnotation.getAnnotatedContent();
if (annotatedContent == null) return null;
VcsFileRevision afterRevision = myCurrentRevisionProvider.getRevision(lineNumber);
String afterContent = loadRevision(myAnnotation.getProject(), afterRevision, myFilePath);
if (afterContent == null) return null;
VcsFileRevision beforeRevision = myPreviousRevisionProvider.getPreviousRevision(lineNumber);
String beforeContent = loadRevision(myAnnotation.getProject(), beforeRevision, myFilePath);
if (beforeContent == null) return null;
LineOffsets offsets = LineOffsetsUtil.create(annotatedContent);
String originalLine = getLine(annotatedContent, offsets, lineNumber);
return createDetailsFor(beforeContent, afterContent, originalLine);
}
@Nullable
public static String loadRevision(@Nullable Project project,
@Nullable VcsFileRevision revision,
@NotNull FilePath filePath) throws VcsException {
try {
if (revision == null) return null;
byte[] bytes = revision.loadContent();
if (bytes == null) return null;
String content = VcsImplUtil.loadTextFromBytes(project, bytes, filePath);
return StringUtil.convertLineSeparators(content);
}
catch (IOException e) {
throw new VcsException(e);
}
}
@Nullable
public static AnnotatedLineModificationDetails createDetailsFor(@NotNull String beforeContent,
@NotNull String afterContent,
@NotNull String originalLine) {
List<LineFragment> fragments = compareContents(beforeContent, afterContent);
LineOffsets afterLineOffsets = LineOffsetsUtil.create(afterContent);
int originalLineNumber = findOriginalLine(afterContent, afterLineOffsets, originalLine, fragments);
if (originalLineNumber == -1) {
return null; // line not found
}
String lineContentAfter = getLine(afterContent, afterLineOffsets, originalLineNumber);
if (StringUtil.isEmptyOrSpaces(lineContentAfter)) {
return createNewLineDetails(lineContentAfter); // empty lines are always new, for simplicity
}
return createFragmentDetails(lineContentAfter, afterLineOffsets, fragments, originalLineNumber);
}
@Nullable
public static AnnotatedLineModificationDetails createDetailsFor(@Nullable String beforeContent,
@NotNull String afterContent,
int originalLineNumber) {
LineOffsets afterLineOffsets = LineOffsetsUtil.create(afterContent);
String lineContentAfter = getLine(afterContent, afterLineOffsets, originalLineNumber);
if (beforeContent == null) {
return createNewLineDetails(lineContentAfter); // whole file is new
}
if (StringUtil.isEmptyOrSpaces(lineContentAfter)) {
return createNewLineDetails(lineContentAfter); // empty lines are always new, for simplicity
}
List<LineFragment> fragments = compareContents(beforeContent, afterContent);
return createFragmentDetails(lineContentAfter, afterLineOffsets, fragments, originalLineNumber);
}
@Nullable
private static AnnotatedLineModificationDetails createFragmentDetails(@NotNull String lineContentAfter,
@NotNull LineOffsets afterLineOffsets,
@NotNull List<LineFragment> fragments,
int originalLineNumber) {
LineFragment lineFragment = ContainerUtil.find(fragments.iterator(), fragment -> {
return fragment.getStartLine2() <= originalLineNumber && originalLineNumber < fragment.getEndLine2();
});
if (lineFragment == null) return null; // line unmodified
if (lineFragment.getStartLine1() == lineFragment.getEndLine1()) {
return createNewLineDetails(lineContentAfter); // whole line is new
}
List<DiffFragment> innerFragments = lineFragment.getInnerFragments();
if (innerFragments == null) {
return createModifiedLineDetails(lineContentAfter); // whole line is modified
}
int lineStart = afterLineOffsets.getLineStart(originalLineNumber);
int lineEnd = afterLineOffsets.getLineEnd(originalLineNumber);
int windowStart = lineStart - lineFragment.getStartOffset2();
int windowEnd = lineEnd - lineFragment.getStartOffset2();
int lineLength = lineEnd - lineStart;
List<InnerChange> changes = new ArrayList<>();
for (DiffFragment innerFragment : innerFragments) {
if (innerFragment.getEndOffset2() < windowStart || innerFragment.getStartOffset2() > windowEnd) continue;
int start = Math.max(0, innerFragment.getStartOffset2() - windowStart);
int end = Math.min(lineLength, innerFragment.getEndOffset2() - windowStart);
InnerChangeType type = start == end ? InnerChangeType.DELETED
: innerFragment.getStartOffset1() != innerFragment.getEndOffset1() ? InnerChangeType.MODIFIED
: InnerChangeType.INSERTED;
changes.add(new InnerChange(start, end, type));
}
return new AnnotatedLineModificationDetails(lineContentAfter, changes);
}
@NotNull
public static AnnotatedLineModificationDetails createNewLineDetails(@NotNull String lineContentAfter) {
InnerChange innerChange = new InnerChange(0, lineContentAfter.length(), InnerChangeType.INSERTED);
return new AnnotatedLineModificationDetails(lineContentAfter, singletonList(innerChange));
}
@NotNull
public static AnnotatedLineModificationDetails createModifiedLineDetails(@NotNull String lineContentAfter) {
InnerChange innerChange = new InnerChange(0, lineContentAfter.length(), InnerChangeType.MODIFIED);
return new AnnotatedLineModificationDetails(lineContentAfter, singletonList(innerChange));
}
@NotNull
private static List<LineFragment> compareContents(@NotNull String beforeContent, @NotNull String afterContent) {
ProgressIndicator indicator = ObjectUtils.chooseNotNull(ProgressIndicatorProvider.getGlobalProgressIndicator(),
DumbProgressIndicator.INSTANCE);
return ComparisonManager.getInstance().compareLinesInner(beforeContent, afterContent, ComparisonPolicy.DEFAULT, indicator);
}
/**
* Search for affected line in content after the commit.
*
* @see DiffNavigationContext
*/
private static int findOriginalLine(@NotNull String afterContent,
@NotNull LineOffsets afterLineOffsets,
@NotNull String originalLine,
@NotNull List<LineFragment> fragments) {
for (LineFragment fragment : fragments) {
for (int i = fragment.getStartLine2(); i < fragment.getEndLine2(); i++) {
String line = getLine(afterContent, afterLineOffsets, i);
if (StringUtil.equalsIgnoreWhitespaces(line, originalLine)) return i;
}
}
return -1; // line not found
}
@NotNull
private static String getLine(@NotNull String text, @NotNull LineOffsets lineOffsets, int line) {
int lineStart = lineOffsets.getLineStart(line);
int lineEnd = lineOffsets.getLineEnd(line);
return text.substring(lineStart, lineEnd);
}
}

View File

@@ -1,36 +1,23 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package git4idea.annotate;
import com.intellij.diff.comparison.ComparisonManager;
import com.intellij.diff.comparison.ComparisonPolicy;
import com.intellij.diff.fragments.DiffFragment;
import com.intellij.diff.fragments.LineFragment;
import com.intellij.diff.tools.util.text.LineOffsets;
import com.intellij.diff.tools.util.text.LineOffsetsUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.DumbProgressIndicator;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressIndicatorProvider;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.annotate.*;
import com.intellij.openapi.vcs.annotate.AnnotatedLineModificationDetails.InnerChange;
import com.intellij.openapi.vcs.annotate.AnnotatedLineModificationDetails.InnerChangeType;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vcs.history.VcsFileRevision;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vcs.impl.AbstractVcsHelperImpl;
import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ObjectUtils;
import com.intellij.util.concurrency.EdtExecutorService;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.DateFormatUtil;
@@ -38,7 +25,6 @@ import com.intellij.vcs.log.Hash;
import com.intellij.vcs.log.VcsUser;
import com.intellij.vcs.log.impl.*;
import com.intellij.vcs.log.util.VcsUserUtil;
import com.intellij.vcsUtil.VcsImplUtil;
import com.intellij.vcsUtil.VcsUtil;
import git4idea.GitContentRevision;
import git4idea.GitFileRevision;
@@ -56,12 +42,9 @@ import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import static java.util.Collections.singletonList;
public final class GitFileAnnotation extends FileAnnotation {
private static final Logger LOG = Logger.getInstance(GitFileAnnotation.class);
@@ -626,86 +609,13 @@ public final class GitFileAnnotation extends FileAnnotation {
LineInfo lineInfo = getLineInfo(lineNumber);
if (lineInfo == null) return null;
String afterContent = loadRevision(lineInfo.getFileRevision());
String afterContent = DefaultLineModificationDetailsProvider.loadRevision(myProject, lineInfo.getFileRevision(), myFilePath);
if (afterContent == null) return null;
int originalLineNumber = lineInfo.getOriginalLineNumber() - 1; // in 'afterContent'
LineOffsets afterLineOffsets = LineOffsetsUtil.create(afterContent);
int lineStart = afterLineOffsets.getLineStart(originalLineNumber);
int lineEnd = afterLineOffsets.getLineEnd(originalLineNumber);
String lineContentAfter = afterContent.substring(lineStart, lineEnd);
String beforeContent = DefaultLineModificationDetailsProvider.loadRevision(myProject, lineInfo.getPreviousFileRevision(), myFilePath);
if (StringUtil.isEmptyOrSpaces(lineContentAfter)) {
return createNewLineDetails(lineContentAfter); // empty lines are always new, for simplicity
}
String beforeContent = loadRevision(lineInfo.getPreviousFileRevision());
if (beforeContent == null) {
return createNewLineDetails(lineContentAfter); // whole file is new
}
ProgressIndicator indicator = ObjectUtils.chooseNotNull(ProgressIndicatorProvider.getGlobalProgressIndicator(),
DumbProgressIndicator.INSTANCE);
List<LineFragment> fragments = ComparisonManager.getInstance().compareLinesInner(beforeContent, afterContent,
ComparisonPolicy.DEFAULT, indicator);
LineFragment lineFragment = ContainerUtil.find(fragments.iterator(), fragment -> {
return fragment.getStartLine2() <= originalLineNumber && originalLineNumber < fragment.getEndLine2();
});
if (lineFragment == null) return null; // line unmodified
if (lineFragment.getStartLine1() == lineFragment.getEndLine1()) {
return createNewLineDetails(lineContentAfter); // whole line is new
}
List<DiffFragment> innerFragments = lineFragment.getInnerFragments();
if (innerFragments == null) {
return createModifiedLineDetails(lineContentAfter); // whole line is modified
}
int windowStart = lineStart - lineFragment.getStartOffset2();
int windowEnd = lineEnd - lineFragment.getStartOffset2();
int lineLength = lineEnd - lineStart;
List<InnerChange> changes = new ArrayList<>();
for (DiffFragment innerFragment : innerFragments) {
if (innerFragment.getEndOffset2() < windowStart || innerFragment.getStartOffset2() > windowEnd) continue;
int start = Math.max(0, innerFragment.getStartOffset2() - windowStart);
int end = Math.min(lineLength, innerFragment.getEndOffset2() - windowStart);
InnerChangeType type = start == end ? InnerChangeType.DELETED
: innerFragment.getStartOffset1() != innerFragment.getEndOffset1() ? InnerChangeType.MODIFIED
: InnerChangeType.INSERTED;
changes.add(new InnerChange(start, end, type));
}
return new AnnotatedLineModificationDetails(lineContentAfter, changes);
}
@NotNull
private AnnotatedLineModificationDetails createNewLineDetails(@NotNull String lineContentAfter) {
InnerChange innerChange = new InnerChange(0, lineContentAfter.length(), InnerChangeType.INSERTED);
return new AnnotatedLineModificationDetails(lineContentAfter, singletonList(innerChange));
}
@NotNull
private AnnotatedLineModificationDetails createModifiedLineDetails(@NotNull String lineContentAfter) {
InnerChange innerChange = new InnerChange(0, lineContentAfter.length(), InnerChangeType.MODIFIED);
return new AnnotatedLineModificationDetails(lineContentAfter, singletonList(innerChange));
}
@Nullable
private String loadRevision(@Nullable VcsFileRevision revision) throws VcsException {
try {
if (revision == null) return null;
byte[] bytes = revision.loadContent();
if (bytes == null) return null;
String content = VcsImplUtil.loadTextFromBytes(myProject, bytes, myFilePath);
return StringUtil.convertLineSeparators(content);
}
catch (IOException e) {
throw new VcsException(e);
}
int originalLineNumber = lineInfo.getOriginalLineNumber() - 1;
return DefaultLineModificationDetailsProvider.createDetailsFor(beforeContent, afterContent, originalLineNumber);
}
}
}

View File

@@ -5,16 +5,12 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.vcs.VcsBundle;
import com.intellij.openapi.vcs.VcsKey;
import com.intellij.openapi.vcs.annotate.FileAnnotation;
import com.intellij.openapi.vcs.annotate.LineAnnotationAspect;
import com.intellij.openapi.vcs.annotate.LineAnnotationAspectAdapter;
import com.intellij.openapi.vcs.annotate.ShowAllAffectedGenericAction;
import com.intellij.openapi.vcs.annotate.*;
import com.intellij.openapi.vcs.history.VcsFileRevision;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.openapi.vcs.annotate.AnnotationTooltipBuilder;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -66,7 +62,7 @@ public class HgAnnotation extends FileAnnotation {
@Override
public LineAnnotationAspect[] getAspects() {
return new LineAnnotationAspect[] {
return new LineAnnotationAspect[]{
revisionAnnotationAspect,
dateAnnotationAspect,
userAnnotationAspect
@@ -88,7 +84,7 @@ public class HgAnnotation extends FileAnnotation {
@Nls
@Nullable
private String getToolTip(int lineNumber, boolean asHtml) {
if ( myLines.size() <= lineNumber || lineNumber < 0 ) {
if (myLines.size() <= lineNumber || lineNumber < 0) {
return null;
}
HgAnnotationLine info = myLines.get(lineNumber);
@@ -194,8 +190,8 @@ public class HgAnnotation extends FileAnnotation {
}
HgAnnotationLine annotationLine = myLines.get(lineNumber);
return myAspectType == FIELD.REVISION
? annotationLine.getVcsRevisionNumber().asString()
: annotationLine.get(myAspectType).toString();
? annotationLine.getVcsRevisionNumber().asString()
: annotationLine.get(myAspectType).toString();
}
@Override
@@ -225,4 +221,10 @@ public class HgAnnotation extends FileAnnotation {
public VirtualFile getFile() {
return LocalFileSystem.getInstance().refreshAndFindFileByIoFile(myFile.getFile());
}
@Nullable
@Override
public LineModificationDetailsProvider getLineModificationDetailsProvider() {
return DefaultLineModificationDetailsProvider.create(this);
}
}

View File

@@ -26,8 +26,8 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import static com.intellij.xml.util.XmlStringUtil.escapeString;
import static com.intellij.openapi.vcs.annotate.AnnotationTooltipBuilder.buildSimpleTooltip;
import static com.intellij.xml.util.XmlStringUtil.escapeString;
public abstract class BaseSvnFileAnnotation extends FileAnnotation {
private final String myContents;
@@ -192,7 +192,7 @@ public abstract class BaseSvnFileAnnotation extends FileAnnotation {
@Override
@Nullable
public AnnotationSourceSwitcher getAnnotationSourceSwitcher() {
if (! myShowMergeSources) return null;
if (!myShowMergeSources) return null;
return new AnnotationSourceSwitcher() {
@Override
@NotNull
@@ -336,4 +336,10 @@ public abstract class BaseSvnFileAnnotation extends FileAnnotation {
public VcsRevisionNumber getCurrentRevision() {
return myBaseRevision;
}
@Nullable
@Override
public LineModificationDetailsProvider getLineModificationDetailsProvider() {
return DefaultLineModificationDetailsProvider.create(this);
}
}