/* * 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.history.impl; import com.intellij.diff.Block; import com.intellij.diff.DiffContentFactory; import com.intellij.diff.DiffManager; import com.intellij.diff.DiffRequestPanel; import com.intellij.diff.contents.DiffContent; import com.intellij.diff.requests.LoadingDiffRequest; import com.intellij.diff.requests.MessageDiffRequest; import com.intellij.diff.requests.NoDiffRequest; import com.intellij.diff.requests.SimpleDiffRequest; import com.intellij.diff.util.IntPair; import com.intellij.icons.AllIcons; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.progress.util.BackgroundTaskUtil; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.FrameWrapper; import com.intellij.openapi.ui.MessageType; import com.intellij.openapi.ui.Splitter; import com.intellij.openapi.ui.popup.util.PopupUtil; import com.intellij.openapi.util.Conditions; import com.intellij.openapi.vcs.*; import com.intellij.openapi.vcs.annotate.ShowAllAffectedGenericAction; import com.intellij.openapi.vcs.changes.issueLinks.IssueLinkHtmlRenderer; import com.intellij.openapi.vcs.changes.issueLinks.TableLinkMouseListener; import com.intellij.openapi.vcs.history.*; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.BrowserHyperlinkListener; import com.intellij.ui.JBSplitter; import com.intellij.ui.PopupHandler; import com.intellij.ui.ScrollPaneFactory; import com.intellij.ui.components.JBLabel; import com.intellij.ui.table.TableView; import com.intellij.util.ArrayUtil; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.ui.*; import com.intellij.util.ui.update.MergingUpdateQueue; import com.intellij.util.ui.update.Update; import com.intellij.vcsUtil.VcsUtil; import org.jetbrains.annotations.CalledInBackground; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; import static com.intellij.util.ObjectUtils.notNull; public class VcsSelectionHistoryDialog extends FrameWrapper implements DataProvider { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.history.impl.VcsHistoryDialog"); private static final VcsRevisionNumber LOCAL_REVISION_NUMBER = new VcsRevisionNumber() { @Override public String asString() { return "Local Changes"; } @Override public int compareTo(@NotNull VcsRevisionNumber vcsRevisionNumber) { return 0; } @Override public String toString() { return asString(); } }; private static final float DIFF_SPLITTER_PROPORTION = 0.5f; private static final float COMMENTS_SPLITTER_PROPORTION = 0.8f; private static final String DIFF_SPLITTER_PROPORTION_KEY = "file.history.selection.diff.splitter.proportion"; private static final String COMMENTS_SPLITTER_PROPORTION_KEY = "file.history.selection.comments.splitter.proportion"; private static final Block EMPTY_BLOCK = new Block("", 0, 0); @NotNull private final Project myProject; @NotNull private final VirtualFile myFile; @NotNull private final AbstractVcs myActiveVcs; @NonNls private final String myHelpId; private final List<VcsFileRevision> myRevisions = new ArrayList<>(); private final CurrentRevision myLocalRevision; private final ListTableModel<VcsFileRevision> myListModel; private final TableView<VcsFileRevision> myList; private final Splitter mySplitter; private final DiffRequestPanel myDiffPanel; private final JCheckBox myChangesOnlyCheckBox = new JCheckBox(VcsBundle.message("checkbox.show.changed.revisions.only")); private final JLabel myStatusLabel = new JBLabel(); private final AnimatedIcon myStatusSpinner = new AsyncProcessIcon("VcsSelectionHistoryDialog"); private final JEditorPane myComments; @NotNull private final MergingUpdateQueue myUpdateQueue; @NotNull private final BlockLoader myBlockLoader; private boolean myIsDuringUpdate = false; private boolean myIsDisposed = false; public VcsSelectionHistoryDialog(@NotNull Project project, @NotNull VirtualFile file, @NotNull Document document, @NotNull VcsHistoryProvider vcsHistoryProvider, @NotNull VcsHistorySession session, @NotNull AbstractVcs vcs, int selectionStart, int selectionEnd, @NotNull String title) { super(project); myProject = project; myFile = file; myActiveVcs = vcs; myHelpId = notNull(vcsHistoryProvider.getHelpId(), "reference.dialogs.vcs.selection.history"); myComments = new JEditorPane(UIUtil.HTML_MIME, ""); myComments.setPreferredSize(new Dimension(150, 100)); myComments.setEditable(false); myComments.addHyperlinkListener(BrowserHyperlinkListener.INSTANCE); JRootPane rootPane = ((RootPaneContainer)getFrame()).getRootPane(); final VcsDependentHistoryComponents components = vcsHistoryProvider.getUICustomization(session, rootPane); ColumnInfo[] defaultColumns = new ColumnInfo[]{ new FileHistoryPanelImpl.RevisionColumnInfo(null), new FileHistoryPanelImpl.DateColumnInfo(), new FileHistoryPanelImpl.AuthorColumnInfo(), new FileHistoryPanelImpl.MessageColumnInfo(project)}; ColumnInfo[] additionalColumns = notNull(components.getColumns(), ColumnInfo.EMPTY_ARRAY); myListModel = new ListTableModel<>(ArrayUtil.mergeArrays(defaultColumns, additionalColumns)); myListModel.setSortable(false); myList = new TableView<>(myListModel); new TableLinkMouseListener().installOn(myList); myList.getEmptyText().setText(VcsBundle.message("history.empty")); myDiffPanel = DiffManager.getInstance().createRequestPanel(myProject, this, getFrame()); myUpdateQueue = new MergingUpdateQueue("VcsSelectionHistoryDialog", 300, true, myList, this); myLocalRevision = new CurrentRevision(file, LOCAL_REVISION_NUMBER); myRevisions.add(myLocalRevision); myRevisions.addAll(session.getRevisionList()); mySplitter = new JBSplitter(true, DIFF_SPLITTER_PROPORTION_KEY, DIFF_SPLITTER_PROPORTION); mySplitter.setFirstComponent(myDiffPanel.getComponent()); mySplitter.setSecondComponent(createBottomPanel(components.getDetailsComponent())); final ListSelectionListener selectionListener = new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { final VcsFileRevision revision; if (myList.getSelectedRowCount() == 1 && !myList.isEmpty()) { revision = myList.getItems().get(myList.getSelectedRow()); String message = IssueLinkHtmlRenderer.formatTextIntoHtml(myProject, revision.getCommitMessage()); myComments.setText(message); myComments.setCaretPosition(0); } else { revision = null; myComments.setText(""); } if (components.getRevisionListener() != null) { components.getRevisionListener().consume(revision); } updateDiff(); } }; myList.getSelectionModel().addListSelectionListener(selectionListener); final VcsConfiguration configuration = VcsConfiguration.getInstance(myProject); myChangesOnlyCheckBox.setSelected(configuration.SHOW_ONLY_CHANGED_IN_SELECTION_DIFF); myChangesOnlyCheckBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { configuration.SHOW_ONLY_CHANGED_IN_SELECTION_DIFF = myChangesOnlyCheckBox.isSelected(); updateRevisionsList(); } }); final DefaultActionGroup popupActions = new DefaultActionGroup(); popupActions.add(new MyDiffAction()); popupActions.add(new MyDiffLocalAction()); popupActions.add(ShowAllAffectedGenericAction.getInstance()); popupActions.add(ActionManager.getInstance().getAction(VcsActions.ACTION_COPY_REVISION_NUMBER)); PopupHandler.installPopupHandler(myList, popupActions, ActionPlaces.UPDATE_POPUP, ActionManager.getInstance()); for (AnAction action : popupActions.getChildren(null)) { action.registerCustomShortcutSet(action.getShortcutSet(), mySplitter); } setTitle(title); setComponent(mySplitter); setPreferredFocusedComponent(myList); setDimensionKey("VCS.FileHistoryDialog"); closeOnEsc(); myBlockLoader = new BlockLoader(myRevisions, myFile, document, selectionStart, selectionEnd) { @Override protected void notifyError(@NotNull VcsException e) { SwingUtilities.invokeLater(() -> { VcsSelectionHistoryDialog dialog = VcsSelectionHistoryDialog.this; if (dialog.isDisposed() || !dialog.getFrame().isShowing()) return; PopupUtil.showBalloonForComponent(mySplitter, canNoLoadMessage(e), MessageType.ERROR, true, myProject); }); } @Override protected void notifyUpdate() { myUpdateQueue.queue(new Update(this) { @Override public void run() { updateStatusPanel(); updateRevisionsList(); } }); } }; myBlockLoader.start(this); updateRevisionsList(); myList.getSelectionModel().setSelectionInterval(0, 0); } @NotNull private static String canNoLoadMessage(@Nullable VcsException e) { return "Can not load revision contents" + (e != null ? ": " + e.getMessage() : ""); } private void updateRevisionsList() { if (myIsDuringUpdate) return; try { myIsDuringUpdate = true; List<VcsFileRevision> newItems; if (myChangesOnlyCheckBox.isSelected()) { newItems = filteredRevisions(); } else { newItems = myRevisions; } IntPair range = getSelectedRevisionsRange(); List<VcsFileRevision> oldSelection = myRevisions.subList(range.val1, range.val2); myListModel.setItems(newItems); myList.setSelection(oldSelection); if (myList.getSelectedRowCount() == 0) { int index = getNearestVisibleRevision(ContainerUtil.getFirstItem(oldSelection)); myList.getSelectionModel().setSelectionInterval(index, index); } } finally { myIsDuringUpdate = false; } updateDiff(); } private void updateStatusPanel() { BlockData data = myBlockLoader.getLoadedData(); if (data.isLoading()) { VcsFileRevision revision = data.getCurrentLoadingRevision(); if (revision != null) { myStatusLabel.setText("<html>Loading revision <tt>" + revision.getRevisionNumber() + "</tt></html>"); } else { myStatusLabel.setText("Loading..."); } myStatusSpinner.resume(); myStatusSpinner.setVisible(true); } else { myStatusLabel.setText(""); myStatusSpinner.suspend(); myStatusSpinner.setVisible(false); } } @NotNull private IntPair getSelectedRevisionsRange() { List<VcsFileRevision> selection = myList.getSelectedObjects(); if (selection.isEmpty()) return new IntPair(0, 0); int startIndex = myRevisions.indexOf(ContainerUtil.getFirstItem(selection)); int endIndex = myRevisions.indexOf(ContainerUtil.getLastItem(selection)); return new IntPair(startIndex, endIndex + 1); } private int getNearestVisibleRevision(@Nullable VcsFileRevision anchor) { int anchorIndex = myRevisions.indexOf(anchor); if (anchorIndex == -1) return 0; for (int i = anchorIndex - 1; i > 0; i--) { int index = myListModel.indexOf(myRevisions.get(i)); if (index != -1) return index; } return 0; } private List<VcsFileRevision> filteredRevisions() { ArrayList<VcsFileRevision> result = new ArrayList<>(); BlockData data = myBlockLoader.getLoadedData(); int firstRevision; boolean foundInitialRevision = false; for (firstRevision = myRevisions.size() - 1; firstRevision > 0; firstRevision--) { Block block = data.getBlock(firstRevision); if (block == EMPTY_BLOCK) foundInitialRevision = true; if (block != null && block != EMPTY_BLOCK) break; } if (!foundInitialRevision && data.isLoading()) firstRevision = myRevisions.size() - 1; result.add(myRevisions.get(firstRevision)); for (int i = firstRevision - 1; i >= 0; i--) { Block block1 = data.getBlock(i + 1); Block block2 = data.getBlock(i); if (block1 == null || block2 == null) continue; if (block1.getLines().equals(block2.getLines())) continue; result.add(myRevisions.get(i)); } Collections.reverse(result); return result; } private void updateDiff() { if (myIsDisposed || myIsDuringUpdate) return; if (myList.getSelectedRowCount() == 0) { myDiffPanel.setRequest(NoDiffRequest.INSTANCE); return; } int count = myRevisions.size(); IntPair range = getSelectedRevisionsRange(); int revIndex1 = range.val2; int revIndex2 = range.val1; if (revIndex1 == count && revIndex2 == count) { myDiffPanel.setRequest(NoDiffRequest.INSTANCE); return; } BlockData blockData = myBlockLoader.getLoadedData(); DiffContent content1 = createDiffContent(revIndex1, blockData); DiffContent content2 = createDiffContent(revIndex2, blockData); String title1 = createDiffContentTitle(revIndex1); String title2 = createDiffContentTitle(revIndex2); if (content1 != null && content2 != null) { myDiffPanel.setRequest(new SimpleDiffRequest(null, content1, content2, title1, title2), new IntPair(revIndex1, revIndex2)); return; } if (blockData.isLoading()) { myDiffPanel.setRequest(new LoadingDiffRequest()); } else { myDiffPanel.setRequest(new MessageDiffRequest(canNoLoadMessage(blockData.getException()))); } } @Nullable private String createDiffContentTitle(int index) { if (index >= myRevisions.size()) return null; return VcsBundle.message("diff.content.title.revision.number", myRevisions.get(index).getRevisionNumber()); } @Nullable private DiffContent createDiffContent(int index, @NotNull BlockData data) { if (index >= myRevisions.size()) return DiffContentFactory.getInstance().createEmpty(); Block block = data.getBlock(index); if (block == null) return null; if (block == EMPTY_BLOCK) return DiffContentFactory.getInstance().createEmpty(); return DiffContentFactory.getInstance().create(block.getBlockContent(), myFile.getFileType()); } @Override public void dispose() { myIsDisposed = true; super.dispose(); } private JComponent createBottomPanel(final JComponent addComp) { JBSplitter splitter = new JBSplitter(true, COMMENTS_SPLITTER_PROPORTION_KEY, COMMENTS_SPLITTER_PROPORTION); splitter.setDividerWidth(4); JPanel tablePanel = new JPanel(new BorderLayout()); tablePanel.add(ScrollPaneFactory.createScrollPane(myList), BorderLayout.CENTER); JPanel statusPanel = new JPanel(new FlowLayout()); statusPanel.add(myStatusSpinner); statusPanel.add(myStatusLabel); JPanel separatorPanel = new JPanel(new BorderLayout()); separatorPanel.add(myChangesOnlyCheckBox, BorderLayout.WEST); separatorPanel.add(statusPanel, BorderLayout.EAST); tablePanel.add(separatorPanel, BorderLayout.NORTH); splitter.setFirstComponent(tablePanel); splitter.setSecondComponent(createComments(addComp)); return splitter; } private JComponent createComments(final JComponent addComp) { JPanel panel = new JPanel(new BorderLayout(4, 4)); panel.add(new JLabel("Commit Message:"), BorderLayout.NORTH); panel.add(ScrollPaneFactory.createScrollPane(myComments), BorderLayout.CENTER); final Splitter splitter = new Splitter(false); splitter.setFirstComponent(panel); splitter.setSecondComponent(addComp); return splitter; } @Override public Object getData(@NonNls String dataId) { if (CommonDataKeys.PROJECT.is(dataId)) { return myProject; } else if (VcsDataKeys.VCS_VIRTUAL_FILE.is(dataId)) { return myFile; } else if (VcsDataKeys.VCS_FILE_REVISION.is(dataId)) { VcsFileRevision selectedObject = myList.getSelectedObject(); return selectedObject instanceof CurrentRevision ? null : selectedObject; } else if (VcsDataKeys.VCS_FILE_REVISIONS.is(dataId)) { List<VcsFileRevision> revisions = ContainerUtil.filter(myList.getSelectedObjects(), Conditions.notEqualTo(myLocalRevision)); return ArrayUtil.toObjectArray(revisions, VcsFileRevision.class); } else if (VcsDataKeys.VCS.is(dataId)) { return myActiveVcs.getKeyInstanceMethod(); } else if (PlatformDataKeys.HELP_ID.is(dataId)) { return myHelpId; } return null; } private class MyDiffAction extends DumbAwareAction { public MyDiffAction() { super(VcsBundle.message("action.name.compare"), VcsBundle.message("action.description.compare"), AllIcons.Actions.Diff); setShortcutSet(CommonShortcuts.getDiff()); } public void update(final AnActionEvent e) { e.getPresentation().setEnabled(myList.getSelectedRowCount() > 1 || myList.getSelectedRowCount() == 1 && myList.getSelectedObject() != myLocalRevision); } public void actionPerformed(AnActionEvent e) { IntPair range = getSelectedRevisionsRange(); VcsFileRevision beforeRevision = range.val2 < myRevisions.size() ? myRevisions.get(range.val2) : VcsFileRevision.NULL; VcsFileRevision afterRevision = myRevisions.get(range.val1); FilePath filePath = VcsUtil.getFilePath(myFile); if (range.val2 - range.val1 > 1) { getDiffHandler().showDiffForTwo(myProject, filePath, beforeRevision, afterRevision); } else { getDiffHandler().showDiffForOne(e, myProject, filePath, beforeRevision, afterRevision); } } } private class MyDiffLocalAction extends DumbAwareAction { public MyDiffLocalAction() { super(VcsBundle.message("show.diff.with.local.action.text"), VcsBundle.message("show.diff.with.local.action.description"), AllIcons.Actions.DiffWithCurrent); setShortcutSet(ActionManager.getInstance().getAction("Vcs.ShowDiffWithLocal").getShortcutSet()); } public void update(final AnActionEvent e) { e.getPresentation().setEnabled(myList.getSelectedRowCount() == 1 && myList.getSelectedObject() != myLocalRevision); } public void actionPerformed(AnActionEvent e) { VcsFileRevision revision = myList.getSelectedObject(); if (revision == null) return; FilePath filePath = VcsUtil.getFilePath(myFile); getDiffHandler().showDiffForTwo(myProject, filePath, revision, myLocalRevision); } } @NotNull private DiffFromHistoryHandler getDiffHandler() { VcsHistoryProvider historyProvider = myActiveVcs.getVcsHistoryProvider(); DiffFromHistoryHandler handler = historyProvider != null ? historyProvider.getHistoryDiffHandler() : null; return handler != null ? handler : new StandardDiffFromHistoryHandler(); } private abstract static class BlockLoader { @NotNull private final Object LOCK = new Object(); @NotNull private final List<VcsFileRevision> myRevisions; @NotNull private final Charset myCharset; @NotNull private final List<Block> myBlocks = new ArrayList<>(); @Nullable private VcsException myException; private boolean myIsLoading = true; private VcsFileRevision myCurrentLoadingRevision; public BlockLoader(@NotNull List<VcsFileRevision> revisions, @NotNull VirtualFile file, @NotNull Document document, int selectionStart, int selectionEnd) { myRevisions = revisions; myCharset = file.getCharset(); String[] lastContent = Block.tokenize(document.getText()); myBlocks.add(new Block(lastContent, selectionStart, selectionEnd + 1)); } @NotNull public BlockData getLoadedData() { synchronized (LOCK) { return new BlockData(myIsLoading, new ArrayList<>(myBlocks), myException, myCurrentLoadingRevision); } } public void start(@NotNull Disposable disposable) { BackgroundTaskUtil.executeOnPooledThread((indicator) -> { try { // first block is loaded in constructor for (int index = 1; index < myRevisions.size(); index++) { indicator.checkCanceled(); Block block = myBlocks.get(index - 1); VcsFileRevision revision = myRevisions.get(index); synchronized (LOCK) { myCurrentLoadingRevision = revision; } notifyUpdate(); Block previousBlock = createBlock(block, revision); synchronized (LOCK) { myBlocks.add(previousBlock); } notifyUpdate(); } } catch (VcsException e) { synchronized (LOCK) { myException = e; } notifyError(e); } finally { synchronized (LOCK) { myIsLoading = false; myCurrentLoadingRevision = null; } notifyUpdate(); } }, disposable); } @CalledInBackground protected abstract void notifyError(@NotNull VcsException e); @CalledInBackground protected abstract void notifyUpdate(); @NotNull private Block createBlock(@NotNull Block block, @NotNull VcsFileRevision revision) throws VcsException { if (block == EMPTY_BLOCK) return EMPTY_BLOCK; String revisionContent = loadContents(revision); Block newBlock = block.createPreviousBlock(revisionContent); return newBlock.getStart() != newBlock.getEnd() ? newBlock : EMPTY_BLOCK; } @NotNull private String loadContents(@NotNull VcsFileRevision revision) throws VcsException { try { byte[] bytes = revision.loadContent(); return new String(bytes, myCharset); } catch (IOException e) { throw new VcsException(e); } } } private static class BlockData { private final boolean myIsLoading; @NotNull private final List<Block> myBlocks; @Nullable private final VcsException myException; @Nullable private final VcsFileRevision myCurrentLoadingRevision; public BlockData(boolean isLoading, @NotNull List<Block> blocks, @Nullable VcsException exception, @Nullable VcsFileRevision currentLoadingRevision) { myIsLoading = isLoading; myBlocks = blocks; myException = exception; myCurrentLoadingRevision = currentLoadingRevision; } public boolean isLoading() { return myIsLoading; } @Nullable public VcsException getException() { return myException; } @Nullable public VcsFileRevision getCurrentLoadingRevision() { return myCurrentLoadingRevision; } @Nullable public Block getBlock(int index) { if (myBlocks.size() <= index) return null; return myBlocks.get(index); } } }