From 33b92c8d655ddf72e0548dfa4ba82e43eeec3c1c Mon Sep 17 00:00:00 2001 From: Aleksey Pivovarov Date: Wed, 22 Dec 2021 18:57:05 +0300 Subject: [PATCH] IDEA-286601 vcs: inline diff from annotation * Add generic implementation for all vcses GitOrigin-RevId: 4af117687a1d57fc586d4a094a6d37e3492f855c --- ...efaultLineModificationDetailsProvider.java | 223 ++++++++++++++++++ .../git4idea/annotate/GitFileAnnotation.java | 98 +------- .../provider/annotate/HgAnnotation.java | 20 +- .../svn/annotate/BaseSvnFileAnnotation.java | 10 +- 4 files changed, 246 insertions(+), 105 deletions(-) create mode 100644 platform/vcs-impl/src/com/intellij/openapi/vcs/annotate/DefaultLineModificationDetailsProvider.java diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/annotate/DefaultLineModificationDetailsProvider.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/annotate/DefaultLineModificationDetailsProvider.java new file mode 100644 index 000000000000..743726adb8cd --- /dev/null +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/annotate/DefaultLineModificationDetailsProvider.java @@ -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 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 fragments = compareContents(beforeContent, afterContent); + return createFragmentDetails(lineContentAfter, afterLineOffsets, fragments, originalLineNumber); + } + + @Nullable + private static AnnotatedLineModificationDetails createFragmentDetails(@NotNull String lineContentAfter, + @NotNull LineOffsets afterLineOffsets, + @NotNull List 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 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 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 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 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); + } +} diff --git a/plugins/git4idea/src/git4idea/annotate/GitFileAnnotation.java b/plugins/git4idea/src/git4idea/annotate/GitFileAnnotation.java index 1e14b0006a04..358b3617cbaa 100644 --- a/plugins/git4idea/src/git4idea/annotate/GitFileAnnotation.java +++ b/plugins/git4idea/src/git4idea/annotate/GitFileAnnotation.java @@ -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 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 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 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); } } } diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/provider/annotate/HgAnnotation.java b/plugins/hg4idea/src/org/zmlx/hg4idea/provider/annotate/HgAnnotation.java index 3e486fc083fb..409eea959154 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/provider/annotate/HgAnnotation.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/provider/annotate/HgAnnotation.java @@ -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); + } } diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/annotate/BaseSvnFileAnnotation.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/annotate/BaseSvnFileAnnotation.java index 1f50f61c7ffa..7b431398fc0c 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/annotate/BaseSvnFileAnnotation.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/annotate/BaseSvnFileAnnotation.java @@ -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); + } }