/* * Copyright 2000-2015 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.diff.merge; import com.intellij.diff.DiffContext; import com.intellij.diff.FrameDiffTool; import com.intellij.diff.actions.ProxyUndoRedoAction; import com.intellij.diff.comparison.ComparisonManager; import com.intellij.diff.comparison.ComparisonMergeUtil; 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.MergeLineFragment; import com.intellij.diff.requests.ContentDiffRequest; import com.intellij.diff.requests.SimpleDiffRequest; import com.intellij.diff.tools.simple.MergeInnerDifferences; import com.intellij.diff.tools.simple.ThreesideTextDiffViewerEx; import com.intellij.diff.tools.util.DiffNotifications; import com.intellij.diff.tools.util.KeyboardModifierListener; import com.intellij.diff.tools.util.base.HighlightPolicy; import com.intellij.diff.tools.util.base.TextDiffViewerUtil; import com.intellij.diff.util.*; import com.intellij.icons.AllIcons; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.ex.ActionUtil; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.command.UndoConfirmationPolicy; import com.intellij.openapi.diff.DiffBundle; import com.intellij.openapi.editor.Caret; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.event.DocumentEvent; import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.progress.*; import com.intellij.openapi.progress.util.ProgressWindow; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.text.LineTokenizer; import com.intellij.openapi.util.text.StringUtil; import com.intellij.ui.awt.RelativePoint; import com.intellij.util.Alarm; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.ui.JBUI; import gnu.trove.TIntArrayList; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import consulo.annotations.RequiredDispatchThread; import consulo.annotations.RequiredWriteAction; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.util.*; import java.util.List; public class TextMergeViewer implements MergeTool.MergeViewer { @NotNull private final MergeContext myMergeContext; @NotNull private final TextMergeRequest myMergeRequest; @NotNull private final MyThreesideViewer myViewer; public TextMergeViewer(@NotNull MergeContext context, @NotNull TextMergeRequest request) { myMergeContext = context; myMergeRequest = request; DiffContext diffContext = new MergeUtil.ProxyDiffContext(myMergeContext); ContentDiffRequest diffRequest = new SimpleDiffRequest(myMergeRequest.getTitle(), getDiffContents(myMergeRequest), getDiffContentTitles(myMergeRequest)); diffRequest.putUserData(DiffUserDataKeys.FORCE_READ_ONLY_CONTENTS, new boolean[]{true, false, true}); myViewer = new MyThreesideViewer(diffContext, diffRequest); } @NotNull private static List<DiffContent> getDiffContents(@NotNull TextMergeRequest mergeRequest) { List<DocumentContent> contents = mergeRequest.getContents(); final DocumentContent left = ThreeSide.LEFT.select(contents); final DocumentContent right = ThreeSide.RIGHT.select(contents); final DocumentContent output = mergeRequest.getOutputContent(); return ContainerUtil.<DiffContent>list(left, output, right); } @NotNull private static List<String> getDiffContentTitles(@NotNull TextMergeRequest mergeRequest) { List<String> titles = MergeUtil.notNullizeContentTitles(mergeRequest.getContentTitles()); titles.set(ThreeSide.BASE.getIndex(), "Result"); return titles; } // // Impl // @NotNull @Override public JComponent getComponent() { return myViewer.getComponent(); } @Nullable @Override public JComponent getPreferredFocusedComponent() { return myViewer.getPreferredFocusedComponent(); } @NotNull @Override public MergeTool.ToolbarComponents init() { MergeTool.ToolbarComponents components = new MergeTool.ToolbarComponents(); FrameDiffTool.ToolbarComponents init = myViewer.init(); components.statusPanel = init.statusPanel; components.toolbarActions = init.toolbarActions; components.closeHandler = () -> MergeUtil.showExitWithoutApplyingChangesDialog(this, myMergeRequest, myMergeContext); return components; } @Nullable @Override public Action getResolveAction(@NotNull MergeResult result) { return myViewer.getResolveAction(result); } @Override public void dispose() { Disposer.dispose(myViewer); } // // Getters // @NotNull public MyThreesideViewer getViewer() { return myViewer; } // // Viewer // public class MyThreesideViewer extends ThreesideTextDiffViewerEx { @NotNull private final MergeModelBase myModel; @NotNull private final ModifierProvider myModifierProvider; @NotNull private final MyInnerDiffWorker myInnerDiffWorker; // all changes - both applied and unapplied ones @NotNull private final List<TextMergeChange> myAllMergeChanges = new ArrayList<>(); private boolean myInitialRediffStarted; private boolean myInitialRediffFinished; private boolean myContentModified; public MyThreesideViewer(@NotNull DiffContext context, @NotNull ContentDiffRequest request) { super(context, request); myModel = new MyMergeModel(getProject(), getEditor().getDocument()); myModifierProvider = new ModifierProvider(); myInnerDiffWorker = new MyInnerDiffWorker(); DiffUtil.registerAction(new ApplySelectedChangesAction(Side.LEFT, true), myPanel); DiffUtil.registerAction(new ApplySelectedChangesAction(Side.RIGHT, true), myPanel); DiffUtil.registerAction(new IgnoreSelectedChangesSideAction(Side.LEFT, true), myPanel); DiffUtil.registerAction(new IgnoreSelectedChangesSideAction(Side.RIGHT, true), myPanel); ProxyUndoRedoAction.register(getProject(), getEditor(), myContentPanel); } @Override protected void onInit() { super.onInit(); myModifierProvider.init(); } @Override protected void onDispose() { Disposer.dispose(myModel); super.onDispose(); } @NotNull @Override protected List<AnAction> createToolbarActions() { List<AnAction> group = new ArrayList<>(); group.add(new MyHighlightPolicySettingAction()); group.add(new MyToggleAutoScrollAction()); group.add(myEditorSettingsAction); group.add(AnSeparator.getInstance()); group.add(new TextShowPartialDiffAction(PartialDiffMode.LEFT_BASE)); group.add(new TextShowPartialDiffAction(PartialDiffMode.BASE_RIGHT)); group.add(new TextShowPartialDiffAction(PartialDiffMode.LEFT_RIGHT)); group.add(AnSeparator.getInstance()); group.add(new ApplyNonConflictsAction(ThreeSide.BASE)); group.add(new ApplyNonConflictsAction(ThreeSide.LEFT)); group.add(new ApplyNonConflictsAction(ThreeSide.RIGHT)); return group; } @NotNull @Override protected List<AnAction> createEditorPopupActions() { List<AnAction> group = new ArrayList<>(); group.add(new ApplySelectedChangesAction(Side.LEFT, false)); group.add(new ApplySelectedChangesAction(Side.RIGHT, false)); group.add(new ResolveSelectedChangesAction(Side.LEFT)); group.add(new ResolveSelectedChangesAction(Side.RIGHT)); group.add(new IgnoreSelectedChangesSideAction(Side.LEFT, false)); group.add(new IgnoreSelectedChangesSideAction(Side.RIGHT, false)); group.add(new IgnoreSelectedChangesAction()); group.add(AnSeparator.getInstance()); group.addAll(TextDiffViewerUtil.createEditorPopupActions()); return group; } @Nullable @Override protected List<AnAction> createPopupActions() { List<AnAction> group = new ArrayList<>(); group.add(AnSeparator.getInstance()); group.add(new MyToggleAutoScrollAction()); return group; } @Nullable public Action getResolveAction(@NotNull final MergeResult result) { String caption = MergeUtil.getResolveActionTitle(result, myMergeRequest, myMergeContext); return new AbstractAction(caption) { @Override public void actionPerformed(ActionEvent e) { if ((result == MergeResult.LEFT || result == MergeResult.RIGHT) && myContentModified && Messages.showYesNoDialog(myPanel.getRootPane(), DiffBundle.message("merge.dialog.resolve.side.with.discard.message", result == MergeResult.LEFT ? 0 : 1), DiffBundle.message("merge.dialog.resolve.side.with.discard.title"), Messages.getQuestionIcon()) != Messages.YES) { return; } if (result == MergeResult.RESOLVED) { if ((getChangesCount() != 0 || getConflictsCount() != 0) && Messages.showYesNoDialog(myPanel.getRootPane(), DiffBundle.message("merge.dialog.apply.partially.resolved.changes.confirmation.message", getChangesCount(), getConflictsCount()), DiffBundle.message("apply.partially.resolved.merge.dialog.title"), Messages.getQuestionIcon()) != Messages.YES) { return; } } if (result == MergeResult.CANCEL && !MergeUtil.showExitWithoutApplyingChangesDialog(TextMergeViewer.this, myMergeRequest, myMergeContext)) { return; } destroyChangedBlocks(); myMergeContext.finishMerge(result); } }; } // // Diff // private void setInitialOutputContent() { final Document baseDocument = ThreeSide.BASE.select(myMergeRequest.getContents()).getDocument(); final Document outputDocument = myMergeRequest.getOutputContent().getDocument(); DiffUtil.executeWriteCommand(outputDocument, getProject(), "Init merge content", () -> { outputDocument.setText(baseDocument.getCharsSequence()); DiffUtil.putNonundoableOperation(getProject(), outputDocument); }); } @Override @RequiredDispatchThread public void rediff(boolean trySync) { if (myInitialRediffStarted) return; myInitialRediffStarted = true; assert myAllMergeChanges.isEmpty(); doRediff(); } @NotNull @Override protected Runnable performRediff(@NotNull ProgressIndicator indicator) { throw new UnsupportedOperationException(); } @RequiredDispatchThread private void doRediff() { myStatusPanel.setBusy(true); // This is made to reduce unwanted modifications before rediff is finished. // It could happen between this init() EDT chunk and invokeLater(). getEditor().setViewer(true); // we need invokeLater() here because viewer is partially-initialized (ex: there are no toolbar or status panel) // user can see this state while we're showing progress indicator, so we want let init() to finish. ApplicationManager.getApplication().invokeLater(() -> { ProgressManager.getInstance().run(new Task.Modal(getProject(), "Computing Differences...", true) { private Runnable myCallback; @Override public void run(@NotNull ProgressIndicator indicator) { myCallback = doPerformRediff(indicator); } @Override public void onCancel() { myMergeContext.finishMerge(MergeResult.CANCEL); } @Override public void onError(@NotNull Exception error) { LOG.error(error); myMergeContext.finishMerge(MergeResult.CANCEL); } @Override public void onSuccess() { if (isDisposed()) return; myCallback.run(); } }); }); } @NotNull protected Runnable doPerformRediff(@NotNull ProgressIndicator indicator) { try { indicator.checkCanceled(); List<DocumentContent> contents = myMergeRequest.getContents(); List<Document> documents = ContainerUtil.map(contents, DocumentContent::getDocument); List<CharSequence> sequences = ReadAction.compute(() -> { return ContainerUtil.map(documents, Document::getImmutableCharSequence); }); ComparisonManager manager = ComparisonManager.getInstance(); List<MergeLineFragment> lineFragments = manager.compareLines(sequences.get(0), sequences.get(1), sequences.get(2), ComparisonPolicy.DEFAULT, indicator); List<MergeConflictType> conflictTypes = ReadAction.compute(() -> { indicator.checkCanceled(); return ContainerUtil.map(lineFragments, (fragment -> DiffUtil.getLineMergeType(fragment, documents, ComparisonPolicy.DEFAULT))); }); return apply(lineFragments, conflictTypes); } catch (DiffTooBigException e) { return applyNotification(DiffNotifications.createDiffTooBig()); } catch (ProcessCanceledException e) { throw e; } catch (Throwable e) { LOG.error(e); return () -> { clearDiffPresentation(); myPanel.setErrorContent(); }; } } @NotNull private Runnable apply(@NotNull final List<MergeLineFragment> fragments, @NotNull final List<MergeConflictType> conflictTypes) { return () -> { setInitialOutputContent(); clearDiffPresentation(); resetChangeCounters(); myModel.setChanges(ContainerUtil.map(fragments, f -> new LineRange(f.getStartLine(ThreeSide.BASE), f.getEndLine(ThreeSide.BASE)))); for (int index = 0; index < fragments.size(); index++) { MergeLineFragment fragment = fragments.get(index); MergeConflictType conflictType = conflictTypes.get(index); TextMergeChange change = new TextMergeChange(index, fragment, conflictType, TextMergeViewer.this); myAllMergeChanges.add(change); onChangeAdded(change); } myInitialScrollHelper.onRediff(); myContentPanel.repaintDividers(); myStatusPanel.update(); getEditor().setViewer(false); myInnerDiffWorker.onSettingsChanged(); myInitialRediffFinished = true; if (myViewer.getTextSettings().isAutoApplyNonConflictedChanges()) { if (getFirstUnresolvedChange(false, ThreeSide.BASE) != null) { applyNonConflictedChanges(ThreeSide.BASE); } } }; } @Override @RequiredDispatchThread protected void destroyChangedBlocks() { super.destroyChangedBlocks(); myInnerDiffWorker.stop(); for (TextMergeChange change : myAllMergeChanges) { change.destroy(); } myAllMergeChanges.clear(); myModel.setChanges(Collections.emptyList()); } // // By-word diff // private class MyInnerDiffWorker { @NotNull private final Set<TextMergeChange> myScheduled = ContainerUtil.newHashSet(); @NotNull private final Alarm myAlarm = new Alarm(MyThreesideViewer.this); @Nullable private ProgressIndicator myProgress; private boolean myEnabled = false; @RequiredDispatchThread public void scheduleRediff(@NotNull TextMergeChange change) { scheduleRediff(Collections.singletonList(change)); } @RequiredDispatchThread public void scheduleRediff(@NotNull Collection<TextMergeChange> changes) { if (!myEnabled) return; putChanges(changes); schedule(); } @RequiredDispatchThread public void onSettingsChanged() { boolean enabled = getHighlightPolicy() == HighlightPolicy.BY_WORD; if (myEnabled == enabled) return; myEnabled = enabled; if (myProgress != null) myProgress.cancel(); myProgress = null; if (myEnabled) { putChanges(myAllMergeChanges); launchRediff(); } else { myStatusPanel.setBusy(false); myScheduled.clear(); for (TextMergeChange change : myAllMergeChanges) { change.setInnerFragments(null); } } } @RequiredDispatchThread public void stop() { if (myProgress != null) myProgress.cancel(); myProgress = null; myScheduled.clear(); myAlarm.cancelAllRequests(); } @RequiredDispatchThread private void putChanges(@NotNull Collection<TextMergeChange> changes) { for (TextMergeChange change : changes) { if (change.isResolved()) continue; myScheduled.add(change); } } @RequiredDispatchThread private void schedule() { if (myProgress != null) return; if (myScheduled.isEmpty()) return; myAlarm.cancelAllRequests(); myAlarm.addRequest(this::launchRediff, ProgressWindow.DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS); } @RequiredDispatchThread private void launchRediff() { myStatusPanel.setBusy(true); myProgress = new EmptyProgressIndicator(); final List<TextMergeChange> scheduled = ContainerUtil.newArrayList(myScheduled); myScheduled.clear(); List<Document> documents = ThreeSide.map((side) -> getEditor(side).getDocument()); final List<InnerChunkData> data = ContainerUtil.map(scheduled, change -> new InnerChunkData(change, documents)); final ProgressIndicator indicator = myProgress; ApplicationManager.getApplication().executeOnPooledThread(() -> { performRediff(scheduled, data, indicator); }); } private void performRediff(@NotNull final List<TextMergeChange> scheduled, @NotNull final List<InnerChunkData> data, @NotNull final ProgressIndicator indicator) { final List<MergeInnerDifferences> result = new ArrayList<>(data.size()); for (InnerChunkData chunkData : data) { result.add(DiffUtil.compareThreesideInner(chunkData.text, ComparisonPolicy.DEFAULT, indicator)); } ApplicationManager.getApplication().invokeLater(() -> { if (!myEnabled || indicator.isCanceled()) return; myProgress = null; for (int i = 0; i < scheduled.size(); i++) { TextMergeChange change = scheduled.get(i); if (myScheduled.contains(change)) continue; change.setInnerFragments(result.get(i)); } myStatusPanel.setBusy(false); if (!myScheduled.isEmpty()) { launchRediff(); } }, ModalityState.any()); } } // // Impl // @Override @RequiredDispatchThread protected void onBeforeDocumentChange(@NotNull DocumentEvent e) { super.onBeforeDocumentChange(e); if (myInitialRediffFinished) myContentModified = true; } public void repaintDividers() { myContentPanel.repaintDividers(); } private void onChangeResolved(@NotNull TextMergeChange change) { if (change.isResolved()) { onChangeRemoved(change); } else { onChangeAdded(change); } if (getChangesCount() == 0 && getConflictsCount() == 0) { LOG.assertTrue(getFirstUnresolvedChange(true, ThreeSide.BASE) == null); ApplicationManager.getApplication().invokeLater(() -> { if (isDisposed()) return; JComponent component = getEditor().getComponent(); RelativePoint point = new RelativePoint(component, new Point(component.getWidth() / 2, JBUI.scale(5))); String message = DiffBundle.message("merge.all.changes.processed.message.text"); DiffUtil.showSuccessPopup(message, point, this, () -> { if (isDisposed()) return; destroyChangedBlocks(); myMergeContext.finishMerge(MergeResult.RESOLVED); }); }); } } @NotNull private HighlightPolicy getHighlightPolicy() { HighlightPolicy policy = getTextSettings().getHighlightPolicy(); if (policy == HighlightPolicy.BY_WORD_SPLIT) return HighlightPolicy.BY_WORD; if (policy == HighlightPolicy.DO_NOT_HIGHLIGHT) return HighlightPolicy.BY_LINE; return policy; } // // Getters // @NotNull public MergeModelBase getModel() { return myModel; } @NotNull @Override public List<TextMergeChange> getAllChanges() { return myAllMergeChanges; } @NotNull @Override public List<TextMergeChange> getChanges() { return ContainerUtil.filter(myAllMergeChanges, mergeChange -> !mergeChange.isResolved()); } @NotNull @Override protected DiffDividerDrawUtil.DividerPaintable getDividerPaintable(@NotNull Side side) { return new MyDividerPaintable(side); } @NotNull public ModifierProvider getModifierProvider() { return myModifierProvider; } @NotNull public EditorEx getEditor() { return getEditor(ThreeSide.BASE); } // // Modification operations // /* * affected changes should be sorted */ public void executeMergeCommand(@Nullable String commandName, boolean underBulkUpdate, @Nullable List<TextMergeChange> affected, @NotNull Runnable task) { myContentModified = true; TIntArrayList affectedIndexes = null; if (affected != null) { affectedIndexes = new TIntArrayList(affected.size()); for (TextMergeChange change : affected) { affectedIndexes.add(change.getIndex()); } } myModel.executeMergeCommand(commandName, null, UndoConfirmationPolicy.DEFAULT, underBulkUpdate, affectedIndexes, task); } public void executeMergeCommand(@Nullable String commandName, @Nullable List<TextMergeChange> affected, @NotNull Runnable task) { executeMergeCommand(commandName, false, affected, task); } @RequiredDispatchThread public void markChangeResolved(@NotNull TextMergeChange change) { if (change.isResolved()) return; change.setResolved(Side.LEFT, true); change.setResolved(Side.RIGHT, true); onChangeResolved(change); myModel.invalidateHighlighters(change.getIndex()); } @RequiredDispatchThread public void markChangeResolved(@NotNull TextMergeChange change, @NotNull Side side) { if (change.isResolved(side)) return; change.setResolved(side, true); if (change.isResolved()) onChangeResolved(change); myModel.invalidateHighlighters(change.getIndex()); } public void ignoreChange(@NotNull TextMergeChange change, @NotNull Side side, boolean resolveChange) { if (!change.isConflict() || resolveChange) { markChangeResolved(change); } else { markChangeResolved(change, side); } } @RequiredWriteAction public void replaceChange(@NotNull TextMergeChange change, @NotNull Side side, boolean resolveChange) { if (change.isResolved(side)) return; if (!change.isChange(side)) { markChangeResolved(change); return; } ThreeSide sourceSide = side.select(ThreeSide.LEFT, ThreeSide.RIGHT); ThreeSide oppositeSide = side.select(ThreeSide.RIGHT, ThreeSide.LEFT); Document sourceDocument = getContent(sourceSide).getDocument(); int sourceStartLine = change.getStartLine(sourceSide); int sourceEndLine = change.getEndLine(sourceSide); List<String> newContent = DiffUtil.getLines(sourceDocument, sourceStartLine, sourceEndLine); if (change.isConflict()) { boolean append = change.isOnesideAppliedConflict(); if (append) { myModel.appendChange(change.getIndex(), newContent); } else { myModel.replaceChange(change.getIndex(), newContent); } if (resolveChange || change.getStartLine(oppositeSide) == change.getEndLine(oppositeSide)) { markChangeResolved(change); } else { change.markOnesideAppliedConflict(); markChangeResolved(change, side); } } else { myModel.replaceChange(change.getIndex(), newContent); markChangeResolved(change); } } @Nullable public CharSequence resolveConflictUsingInnerDifferences(@NotNull TextMergeChange change) { if (!change.isConflict()) return null; if (change.isResolved(Side.LEFT) || change.isResolved(Side.RIGHT)) return null; MergeLineFragment changeFragment = change.getFragment(); if (changeFragment.getStartLine(ThreeSide.LEFT) == changeFragment.getEndLine(ThreeSide.LEFT)) return null; if (changeFragment.getStartLine(ThreeSide.BASE) == changeFragment.getEndLine(ThreeSide.BASE)) return null; if (changeFragment.getStartLine(ThreeSide.RIGHT) == changeFragment.getEndLine(ThreeSide.RIGHT)) return null; int baseStartLine = changeFragment.getStartLine(ThreeSide.BASE); int baseEndLine = changeFragment.getEndLine(ThreeSide.BASE); DiffContent baseDiffContent = ThreeSide.BASE.select(myMergeRequest.getContents()); Document baseDocument = ((DocumentContent)baseDiffContent).getDocument(); int resultStartLine = change.getStartLine(); int resultEndLine = change.getEndLine(); Document resultDocument = getEditor().getDocument(); CharSequence baseContent = DiffUtil.getLinesContent(baseDocument, baseStartLine, baseEndLine); CharSequence resultContent = DiffUtil.getLinesContent(resultDocument, resultStartLine, resultEndLine); if (!StringUtil.equals(baseContent, resultContent)) return null; List<CharSequence> texts = ThreeSide.map((side) -> { return DiffUtil.getLinesContent(getEditor(side).getDocument(), change.getStartLine(side), change.getEndLine(side)); }); return ComparisonMergeUtil.tryResolveConflict(texts.get(0), texts.get(1), texts.get(2)); } public void resolveConflictedChange(@NotNull TextMergeChange change) { CharSequence newContent = resolveConflictUsingInnerDifferences(change); if (newContent == null) return; String[] newContentLines = LineTokenizer.tokenize(newContent, false); myModel.replaceChange(change.getIndex(), Arrays.asList(newContentLines)); markChangeResolved(change); } private class MyMergeModel extends MergeModelBase<TextMergeChange.State> { public MyMergeModel(@Nullable Project project, @NotNull Document document) { super(project, document); } @Override protected void reinstallHighlighters(int index) { TextMergeChange change = myAllMergeChanges.get(index); change.reinstallHighlighters(); myInnerDiffWorker.scheduleRediff(change); } @NotNull @Override protected TextMergeChange.State storeChangeState(int index) { TextMergeChange change = myAllMergeChanges.get(index); return change.storeState(); } @Override protected void restoreChangeState(@NotNull TextMergeChange.State state) { super.restoreChangeState(state); TextMergeChange change = myAllMergeChanges.get(state.myIndex); boolean wasResolved = change.isResolved(); change.restoreState(state); if (wasResolved != change.isResolved()) onChangeResolved(change); } @Nullable @Override protected TextMergeChange.State processDocumentChange(int index, int oldLine1, int oldLine2, int shift) { TextMergeChange.State state = super.processDocumentChange(index, oldLine1, oldLine2, shift); TextMergeChange mergeChange = myAllMergeChanges.get(index); if (mergeChange.getStartLine() == mergeChange.getEndLine() && mergeChange.getDiffType() == TextDiffType.DELETED && !mergeChange.isResolved()) { myViewer.markChangeResolved(mergeChange); } return state; } } // // Actions // @Nullable private TextMergeChange getFirstUnresolvedChange(boolean acceptConflicts, @NotNull ThreeSide side) { for (TextMergeChange change : getAllChanges()) { if (change.isResolved()) continue; if (!acceptConflicts && change.isConflict()) continue; if (!change.isChange(side)) continue; return change; } return null; } private void applyNonConflictedChanges(@NotNull ThreeSide side) { executeMergeCommand("Apply Non Conflicted Changes", true, null, () -> { List<TextMergeChange> allChanges = ContainerUtil.newArrayList(getAllChanges()); for (TextMergeChange change : allChanges) { if (change.isConflict()) continue; if (change.isResolved(side)) continue; if (!change.isChange(side)) continue; Side masterSide = side.select(Side.LEFT, change.isChange(Side.LEFT) ? Side.LEFT : Side.RIGHT, Side.RIGHT); replaceChange(change, masterSide, false); } }); TextMergeChange firstConflict = getFirstUnresolvedChange(true, ThreeSide.BASE); if (firstConflict != null) doScrollToChange(firstConflict, true); } private class MyHighlightPolicySettingAction extends TextDiffViewerUtil.HighlightPolicySettingAction { public MyHighlightPolicySettingAction() { super(getTextSettings()); } @NotNull @Override protected HighlightPolicy getCurrentSetting() { return getHighlightPolicy(); } @NotNull @Override protected List<HighlightPolicy> getAvailableSettings() { return ContainerUtil.list(HighlightPolicy.BY_LINE, HighlightPolicy.BY_WORD); } @Override protected void onSettingsChanged() { myInnerDiffWorker.onSettingsChanged(); } } private abstract class ApplySelectedChangesActionBase extends AnAction implements DumbAware { private final boolean myShortcut; public ApplySelectedChangesActionBase(boolean shortcut) { myShortcut = shortcut; } @Override public void update(@NotNull AnActionEvent e) { if (myShortcut) { // consume shortcut even if there are nothing to do - avoid calling some other action e.getPresentation().setEnabledAndVisible(true); return; } Presentation presentation = e.getPresentation(); Editor editor = e.getData(CommonDataKeys.EDITOR); ThreeSide side = getEditorSide(editor); if (side == null) { presentation.setEnabledAndVisible(false); return; } if (!isVisible(side)) { presentation.setEnabledAndVisible(false); return; } presentation.setText(getText(side)); presentation.setVisible(true); presentation.setEnabled(isSomeChangeSelected(side)); } @Override public void actionPerformed(@NotNull final AnActionEvent e) { Editor editor = e.getData(CommonDataKeys.EDITOR); final ThreeSide side = getEditorSide(editor); if (editor == null || side == null) return; final List<TextMergeChange> selectedChanges = getSelectedChanges(side); if (selectedChanges.isEmpty()) return; String title = e.getPresentation().getText() + " in merge"; executeMergeCommand(title, selectedChanges.size() > 1, selectedChanges, () -> { apply(side, selectedChanges); }); } private boolean isSomeChangeSelected(@NotNull ThreeSide side) { EditorEx editor = getEditor(side); List<Caret> carets = editor.getCaretModel().getAllCarets(); if (carets.size() != 1) return true; Caret caret = carets.get(0); if (caret.hasSelection()) return true; int line = editor.getDocument().getLineNumber(editor.getExpectedCaretOffset()); List<TextMergeChange> changes = getAllChanges(); for (TextMergeChange change : changes) { if (!isEnabled(change)) continue; int line1 = change.getStartLine(side); int line2 = change.getEndLine(side); if (DiffUtil.isSelectedByLine(line, line1, line2)) return true; } return false; } @NotNull @RequiredDispatchThread private List<TextMergeChange> getSelectedChanges(@NotNull ThreeSide side) { final BitSet lines = DiffUtil.getSelectedLines(getEditor(side)); List<TextMergeChange> changes = getChanges(); List<TextMergeChange> affectedChanges = new ArrayList<>(); for (TextMergeChange change : changes) { if (!isEnabled(change)) continue; int line1 = change.getStartLine(side); int line2 = change.getEndLine(side); if (DiffUtil.isSelectedByLine(lines, line1, line2)) { affectedChanges.add(change); } } return affectedChanges; } protected abstract String getText(@NotNull ThreeSide side); protected abstract boolean isVisible(@NotNull ThreeSide side); protected abstract boolean isEnabled(@NotNull TextMergeChange change); @RequiredWriteAction protected abstract void apply(@NotNull ThreeSide side, @NotNull List<TextMergeChange> changes); } private class IgnoreSelectedChangesSideAction extends ApplySelectedChangesActionBase { @NotNull private final Side mySide; public IgnoreSelectedChangesSideAction(@NotNull Side side, boolean shortcut) { super(shortcut); mySide = side; ActionUtil.copyFrom(this, mySide.select("Diff.IgnoreLeftSide", "Diff.IgnoreRightSide")); } @Override protected String getText(@NotNull ThreeSide side) { return "Ignore"; } @Override protected boolean isVisible(@NotNull ThreeSide side) { return side == mySide.select(ThreeSide.LEFT, ThreeSide.RIGHT); } @Override protected boolean isEnabled(@NotNull TextMergeChange change) { return !change.isResolved(mySide); } @Override protected void apply(@NotNull ThreeSide side, @NotNull List<TextMergeChange> changes) { for (TextMergeChange change : changes) { ignoreChange(change, mySide, false); } } } private class IgnoreSelectedChangesAction extends ApplySelectedChangesActionBase { public IgnoreSelectedChangesAction() { super(false); getTemplatePresentation().setIcon(AllIcons.Diff.Remove); } @Override protected String getText(@NotNull ThreeSide side) { return "Ignore"; } @Override protected boolean isVisible(@NotNull ThreeSide side) { return side == ThreeSide.BASE; } @Override protected boolean isEnabled(@NotNull TextMergeChange change) { return !change.isResolved(); } @Override protected void apply(@NotNull ThreeSide side, @NotNull List<TextMergeChange> changes) { for (TextMergeChange change : changes) { markChangeResolved(change); } } } private class ApplySelectedChangesAction extends ApplySelectedChangesActionBase { @NotNull private final Side mySide; public ApplySelectedChangesAction(@NotNull Side side, boolean shortcut) { super(shortcut); mySide = side; ActionUtil.copyFrom(this, mySide.select("Diff.ApplyLeftSide", "Diff.ApplyRightSide")); } @Override protected String getText(@NotNull ThreeSide side) { return side != ThreeSide.BASE ? "Accept" : getTemplatePresentation().getText(); } @Override protected boolean isVisible(@NotNull ThreeSide side) { if (side == ThreeSide.BASE) return true; return side == mySide.select(ThreeSide.LEFT, ThreeSide.RIGHT); } @Override protected boolean isEnabled(@NotNull TextMergeChange change) { return !change.isResolved(mySide); } @Override protected void apply(@NotNull ThreeSide side, @NotNull List<TextMergeChange> changes) { for (int i = changes.size() - 1; i >= 0; i--) { replaceChange(changes.get(i), mySide, false); } } } private class ResolveSelectedChangesAction extends ApplySelectedChangesActionBase { @NotNull private final Side mySide; public ResolveSelectedChangesAction(@NotNull Side side) { super(false); mySide = side; } @Override protected String getText(@NotNull ThreeSide side) { return mySide.select("Resolve using Left", "Resolve using Right"); } @Override protected boolean isVisible(@NotNull ThreeSide side) { return side == ThreeSide.BASE; } @Override protected boolean isEnabled(@NotNull TextMergeChange change) { return !change.isResolved(mySide); } @Override protected void apply(@NotNull ThreeSide side, @NotNull List<TextMergeChange> changes) { for (int i = changes.size() - 1; i >= 0; i--) { replaceChange(changes.get(i), mySide, true); } } } public class ApplyNonConflictsAction extends DumbAwareAction { @NotNull private final ThreeSide mySide; public ApplyNonConflictsAction(@NotNull ThreeSide side) { String id = side.select("Diff.ApplyNonConflicts.Left", "Diff.ApplyNonConflicts", "Diff.ApplyNonConflicts.Right"); ActionUtil.copyFrom(this, id); mySide = side; } @Override public void update(AnActionEvent e) { e.getPresentation().setEnabled(getFirstUnresolvedChange(false, mySide) != null); } @Override public void actionPerformed(AnActionEvent e) { applyNonConflictedChanges(mySide); } } // // Helpers // private class MyDividerPaintable implements DiffDividerDrawUtil.DividerPaintable { @NotNull private final Side mySide; public MyDividerPaintable(@NotNull Side side) { mySide = side; } @Override public void process(@NotNull Handler handler) { ThreeSide left = mySide.select(ThreeSide.LEFT, ThreeSide.BASE); ThreeSide right = mySide.select(ThreeSide.BASE, ThreeSide.RIGHT); for (TextMergeChange mergeChange : myAllMergeChanges) { if (!mergeChange.isChange(mySide)) continue; Color color = mergeChange.getDiffType().getColor(getEditor()); boolean isResolved = mergeChange.isResolved(mySide); if (!handler.process(mergeChange.getStartLine(left), mergeChange.getEndLine(left), mergeChange.getStartLine(right), mergeChange.getEndLine(right), color, isResolved)) { return; } } } } public class ModifierProvider extends KeyboardModifierListener { public void init() { init(myPanel, TextMergeViewer.this); } @Override public void onModifiersChanged() { for (TextMergeChange change : myAllMergeChanges) { change.updateGutterActions(false); } } } } private static class InnerChunkData { @NotNull public final List<CharSequence> text; public InnerChunkData(@NotNull TextMergeChange change, @NotNull List<Document> documents) { text = ThreeSide.map(side -> { if (!change.isChange(side) || change.isResolved(side)) return null; return getChunkContent(change, documents, side); }); } @Nullable @RequiredWriteAction private static CharSequence getChunkContent(@NotNull TextMergeChange change, @NotNull List<Document> documents, @NotNull ThreeSide side) { int startLine = change.getStartLine(side); int endLine = change.getEndLine(side); return startLine != endLine ? DiffUtil.getLinesContent(side.select(documents), startLine, endLine) : null; } } }