/* * Copyright 2000-2009 JetBrains s.r.o. * * 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.intellij.openapi.vcs.annotate; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.localVcs.UpToDateLineNumberProvider; import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.VcsKey; import com.intellij.openapi.vcs.diff.DiffProvider; import com.intellij.openapi.vcs.history.VcsFileRevision; import com.intellij.openapi.vcs.history.VcsRevisionNumber; import com.intellij.openapi.vcs.vfs.VcsVirtualFile; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.Consumer; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.HashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; /** * Represents annotations ("vcs blame") for some file in a specific revision * @see AnnotationProvider */ public abstract class FileAnnotation { private static final Logger LOG = Logger.getInstance(FileAnnotation.class); @NotNull private final Project myProject; private Runnable myCloser; private Consumer<FileAnnotation> myReloader; protected FileAnnotation(@NotNull Project project) { myProject = project; } @NotNull public Project getProject() { return myProject; } @Nullable public VcsKey getVcsKey() { return null; } /** * @return annotated file * <p> * If annotations are called on a local file, it can be this file. * If annotations are called on a specific revision, it can be corresponding {@link VcsVirtualFile}. * Note: file content might differ from content in annotated revision {@link #getAnnotatedContent}. */ @Nullable public VirtualFile getFile() { return null; } /** * @return file content in the annotated revision * <p> * It might differ from {@code getFile()} content. Ex: annotations for a local file, that has non-committed changes. * In this case {@link UpToDateLineNumberProvider} will be used to transfer lines between local and annotated revisions. */ @Nullable public abstract String getAnnotatedContent(); /** * @return annotated revision * <p> * This information might be used to close annotations on local file if current revision was changed, * and invocation of AnnotationProvider on this file will produce different results - see {@link #isBaseRevisionChanged}. */ @Nullable public abstract VcsRevisionNumber getCurrentRevision(); /** * @param number current revision number {@link DiffProvider#getCurrentRevision} * @return whether annotations should be updated */ public boolean isBaseRevisionChanged(@NotNull VcsRevisionNumber number) { final VcsRevisionNumber currentRevision = getCurrentRevision(); return currentRevision != null && !currentRevision.equals(number); } /** * This method is invoked when the file annotation is no longer used. * NB: method might be invoked multiple times */ public abstract void dispose(); /** * Get annotation aspects. * The typical aspects are revision number, date, author. * The aspects are displayed each in own column in the returned order. */ @NotNull public abstract LineAnnotationAspect[] getAspects(); /** * @return number of lines in annotated content */ public abstract int getLineCount(); /** * The tooltip that is shown over annotation. * Typically, this is a detailed info about related revision. ex: long revision number, commit message */ @Nullable public abstract String getToolTip(int lineNumber); /** * @return last revision that modified this line. */ @Nullable public abstract VcsRevisionNumber getLineRevisionNumber(int lineNumber); /** * @return time of the last modification of this line. * Typically, this is a timestamp associated with {@link #getLineRevisionNumber} */ @Nullable public abstract Date getLineDate(int lineNumber); /** * @return revisions that are mentioned in the annotations, from newest to oldest * Can be used to sort revisions, if they can't be sorted by {@code Date} or show file modification number for a revision. */ @Nullable public abstract List<VcsFileRevision> getRevisions(); /** * Allows to switch between different representation modes. * <p> * Ex: in SVN it's possible to show revision that modified line - "svn blame -g", * or the commit that merged that change into current branch - "svn blame". * <p> * when "show merge sources" is turned on, {@link #getLineRevisionNumber} returns merge source revision, * while {@link #originalRevision} returns merge revision. */ @Nullable public AnnotationSourceSwitcher getAnnotationSourceSwitcher() { return null; } /** * @return last revision that modified this line in current branch. * @see #getAnnotationSourceSwitcher() * @see #getLineRevisionNumber(int) */ @Nullable public VcsRevisionNumber originalRevision(int lineNumber) { return getLineRevisionNumber(lineNumber); } /** * Notify that annotations should be closed */ public final void close() { myCloser.run(); } /** * Notify that annotation information has changed, and should be updated. * If `this` is visible, hide it and show new one instead. * If `this` is not visible, do nothing. * * @param newFileAnnotation annotations to be shown */ public final void reload(@NotNull FileAnnotation newFileAnnotation) { if (myReloader != null) myReloader.consume(newFileAnnotation); } /** * @see #close() */ public final void setCloser(@NotNull Runnable closer) { myCloser = closer; } /** * @see #reload() */ public final void setReloader(@Nullable Consumer<FileAnnotation> reloader) { myReloader = reloader; } @Deprecated public boolean revisionsNotEmpty() { return true; } @Nullable public CurrentFileRevisionProvider getCurrentFileRevisionProvider() { return createDefaultCurrentFileRevisionProvider(this); } @Nullable public PreviousFileRevisionProvider getPreviousFileRevisionProvider() { return createDefaultPreviousFileRevisionProvider(this); } @Nullable public AuthorsMappingProvider getAuthorsMappingProvider() { return createDefaultAuthorsMappingProvider(this); } @Nullable public RevisionsOrderProvider getRevisionsOrderProvider() { return createDefaultRevisionsOrderProvider(this); } public interface CurrentFileRevisionProvider { @Nullable VcsFileRevision getRevision(int lineNumber); } public interface PreviousFileRevisionProvider { @Nullable VcsFileRevision getPreviousRevision(int lineNumber); @Nullable VcsFileRevision getLastRevision(); } public interface AuthorsMappingProvider { @NotNull Map<VcsRevisionNumber, String> getAuthors(); } public interface RevisionsOrderProvider { @NotNull List<List<VcsRevisionNumber>> getOrderedRevisions(); } @Nullable private static CurrentFileRevisionProvider createDefaultCurrentFileRevisionProvider(@NotNull FileAnnotation annotation) { List<VcsFileRevision> revisions = annotation.getRevisions(); if (revisions == null) return null; Map<VcsRevisionNumber, VcsFileRevision> map = new HashMap<>(); for (VcsFileRevision revision : revisions) { map.put(revision.getRevisionNumber(), revision); } List<VcsFileRevision> lineToRevision = new ArrayList<>(annotation.getLineCount()); for (int i = 0; i < annotation.getLineCount(); i++) { lineToRevision.add(map.get(annotation.getLineRevisionNumber(i))); } return (lineNumber) -> { LOG.assertTrue(lineNumber >= 0 && lineNumber < lineToRevision.size()); return lineToRevision.get(lineNumber); }; } @Nullable private static PreviousFileRevisionProvider createDefaultPreviousFileRevisionProvider(@NotNull FileAnnotation annotation) { List<VcsFileRevision> revisions = annotation.getRevisions(); if (revisions == null) return null; Map<VcsRevisionNumber, VcsFileRevision> map = new HashMap<>(); for (int i = 0; i < revisions.size(); i++) { VcsFileRevision revision = revisions.get(i); VcsFileRevision previousRevision = i + 1 < revisions.size() ? revisions.get(i + 1) : null; map.put(revision.getRevisionNumber(), previousRevision); } List<VcsFileRevision> lineToRevision = new ArrayList<>(annotation.getLineCount()); for (int i = 0; i < annotation.getLineCount(); i++) { lineToRevision.add(map.get(annotation.getLineRevisionNumber(i))); } VcsFileRevision lastRevision = ContainerUtil.getFirstItem(revisions); return new PreviousFileRevisionProvider() { @Nullable @Override public VcsFileRevision getPreviousRevision(int lineNumber) { LOG.assertTrue(lineNumber >= 0 && lineNumber < lineToRevision.size()); return lineToRevision.get(lineNumber); } @Nullable @Override public VcsFileRevision getLastRevision() { return lastRevision; } }; } @Nullable private static AuthorsMappingProvider createDefaultAuthorsMappingProvider(@NotNull FileAnnotation annotation) { List<VcsFileRevision> revisions = annotation.getRevisions(); if (revisions == null) return null; Map<VcsRevisionNumber, String> authorsMapping = new HashMap<>(); for (VcsFileRevision revision : revisions) { String author = revision.getAuthor(); if (author != null) authorsMapping.put(revision.getRevisionNumber(), author); } return () -> authorsMapping; } @Nullable private static RevisionsOrderProvider createDefaultRevisionsOrderProvider(@NotNull FileAnnotation annotation) { List<VcsFileRevision> revisions = annotation.getRevisions(); if (revisions == null) return null; List<List<VcsRevisionNumber>> orderedRevisions = ContainerUtil.map(revisions, (revision) -> { return Collections.singletonList(revision.getRevisionNumber()); }); return () -> orderedRevisions; } }