/* * Copyright 2000-2016 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.changes.patch.tool; import com.intellij.diff.comparison.ByWord; import com.intellij.diff.comparison.ComparisonPolicy; import com.intellij.diff.comparison.DiffTooBigException; import com.intellij.diff.fragments.DiffFragment; import com.intellij.diff.merge.MergeModelBase; import com.intellij.diff.util.*; import com.intellij.icons.AllIcons; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.diff.DiffBundle; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.ex.DocumentEx; import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.editor.ex.MarkupModelEx; import com.intellij.openapi.editor.markup.GutterIconRenderer; import com.intellij.openapi.editor.markup.HighlighterLayer; import com.intellij.openapi.editor.markup.HighlighterTargetArea; import com.intellij.openapi.editor.markup.RangeHighlighter; import com.intellij.openapi.progress.DumbProgressIndicator; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vcs.changes.patch.AppliedTextPatch.HunkStatus; import com.intellij.openapi.vcs.ex.LineStatusMarkerRenderer; import com.intellij.ui.ColorUtil; import com.intellij.ui.JBColor; import com.intellij.util.PairConsumer; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.List; class ApplyPatchChange { @NotNull private final ApplyPatchViewer myViewer; private final int myIndex; // index in myModelChanges @NotNull private final LineRange myPatchDeletionRange; @NotNull private final LineRange myPatchInsertionRange; @NotNull private final HunkStatus myStatus; @Nullable private final List<DiffFragment> myPatchInnerDifferences; @NotNull private final List<MyGutterOperation> myOperations = new ArrayList<>(); @NotNull private final List<RangeHighlighter> myHighlighters = new ArrayList<>(); private boolean myResolved; public ApplyPatchChange(@NotNull PatchChangeBuilder.Hunk hunk, int index, @NotNull ApplyPatchViewer viewer) { myIndex = index; myViewer = viewer; myPatchDeletionRange = hunk.getPatchDeletionRange(); myPatchInsertionRange = hunk.getPatchInsertionRange(); myStatus = hunk.getStatus(); myPatchInnerDifferences = calcPatchInnerDifferences(hunk, viewer); } @Nullable private static List<DiffFragment> calcPatchInnerDifferences(@NotNull PatchChangeBuilder.Hunk hunk, @NotNull ApplyPatchViewer viewer) { LineRange deletionRange = hunk.getPatchDeletionRange(); LineRange insertionRange = hunk.getPatchInsertionRange(); if (deletionRange.isEmpty() || insertionRange.isEmpty()) return null; try { DocumentEx patchDocument = viewer.getPatchEditor().getDocument(); CharSequence deleted = DiffUtil.getLinesContent(patchDocument, deletionRange.start, deletionRange.end); CharSequence inserted = DiffUtil.getLinesContent(patchDocument, insertionRange.start, insertionRange.end); return ByWord.compare(deleted, inserted, ComparisonPolicy.DEFAULT, DumbProgressIndicator.INSTANCE); } catch (DiffTooBigException ignore) { return null; } } public void reinstallHighlighters() { destroyHighlighters(); installHighlighters(); myViewer.repaintDivider(); } private void installHighlighters() { createResultHighlighters(); createPatchHighlighters(); createStatusHighlighter(); createOperations(); } private void createPatchHighlighters() { EditorEx patchEditor = myViewer.getPatchEditor(); myHighlighters.addAll(DiffDrawUtil.createUnifiedChunkHighlighters(patchEditor, myPatchDeletionRange, myPatchInsertionRange, myPatchInnerDifferences)); } private void createResultHighlighters() { LineRange resultRange = getResultRange(); if (resultRange == null) return; EditorEx editor = myViewer.getResultEditor(); int startLine = resultRange.start; int endLine = resultRange.end; TextDiffType type = getDiffType(); boolean resolved = isRangeApplied(); myHighlighters.addAll(DiffDrawUtil.createHighlighter(editor, startLine, endLine, type, false, resolved, false)); } private void createStatusHighlighter() { int line1 = myPatchDeletionRange.start; int line2 = myPatchInsertionRange.end; Color color = getStatusColor(); if (isResolved()) { color = ColorUtil.mix(color, myViewer.getPatchEditor().getGutterComponentEx().getBackground(), 0.6f); } String tooltip = getStatusText(); EditorEx patchEditor = myViewer.getPatchEditor(); Document document = patchEditor.getDocument(); MarkupModelEx markupModel = patchEditor.getMarkupModel(); TextRange textRange = DiffUtil.getLinesRange(document, line1, line2); RangeHighlighter highlighter = markupModel.addRangeHighlighter(textRange.getStartOffset(), textRange.getEndOffset(), HighlighterLayer.LAST, null, HighlighterTargetArea.LINES_IN_RANGE); PairConsumer<Editor, MouseEvent> clickHandler = getResultRange() != null ? (e, event) -> myViewer.scrollToChange(this, Side.RIGHT, false) : null; highlighter.setLineMarkerRenderer(LineStatusMarkerRenderer.createRenderer(line1, line2, color, tooltip, clickHandler)); myHighlighters.add(highlighter); } private void destroyHighlighters() { for (RangeHighlighter highlighter : myHighlighters) { highlighter.dispose(); } myHighlighters.clear(); for (MyGutterOperation operation : myOperations) { operation.dispose(); } myOperations.clear(); } // // Getters // public int getIndex() { return myIndex; } @NotNull public HunkStatus getStatus() { return myStatus; } @NotNull public LineRange getPatchRange() { return new LineRange(myPatchDeletionRange.start, myPatchInsertionRange.end); } @NotNull public LineRange getPatchAffectedRange() { return isRangeApplied() ? myPatchInsertionRange : myPatchDeletionRange; } @NotNull public LineRange getPatchDeletionRange() { return myPatchDeletionRange; } @NotNull public LineRange getPatchInsertionRange() { return myPatchInsertionRange; } @Nullable public LineRange getResultRange() { ApplyPatchViewer.MyModel model = myViewer.getModel(); int lineStart = model.getLineStart(myIndex); int lineEnd = model.getLineEnd(myIndex); if (lineStart != -1 || lineEnd != -1) return new LineRange(lineStart, lineEnd); return null; } public boolean isResolved() { return myResolved; } public void setResolved(boolean resolved) { myResolved = resolved; } @NotNull public TextDiffType getDiffType() { return DiffUtil.getDiffType(!myPatchDeletionRange.isEmpty(), !myPatchInsertionRange.isEmpty()); } public boolean isRangeApplied() { return myResolved || getStatus() == HunkStatus.ALREADY_APPLIED; } @NotNull private String getStatusText() { switch (myStatus) { case ALREADY_APPLIED: return "Already applied"; case EXACTLY_APPLIED: return "Automatically applied"; case NOT_APPLIED: return "Not applied"; default: throw new IllegalStateException(); } } @NotNull private Color getStatusColor() { switch (myStatus) { case ALREADY_APPLIED: return JBColor.YELLOW.darker(); case EXACTLY_APPLIED: return new JBColor(new Color(0, 180, 5), new Color(0, 147, 5)); case NOT_APPLIED: return JBColor.RED.darker(); default: throw new IllegalStateException(); } } // // Operations // private void createOperations() { if (myViewer.isReadOnly()) return; if (isResolved()) return; if (myStatus == HunkStatus.EXACTLY_APPLIED) { ContainerUtil.addIfNotNull(myOperations, createOperation(OperationType.APPLY)); } ContainerUtil.addIfNotNull(myOperations, createOperation(OperationType.IGNORE)); } @Nullable private MyGutterOperation createOperation(@NotNull OperationType type) { if (isResolved()) return null; EditorEx editor = myViewer.getPatchEditor(); Document document = editor.getDocument(); int line = getPatchRange().start; int offset = line == DiffUtil.getLineCount(document) ? document.getTextLength() : document.getLineStartOffset(line); RangeHighlighter highlighter = editor.getMarkupModel().addRangeHighlighter(offset, offset, HighlighterLayer.ADDITIONAL_SYNTAX, null, HighlighterTargetArea.LINES_IN_RANGE); return new MyGutterOperation(highlighter, type); } private class MyGutterOperation { @NotNull private final RangeHighlighter myHighlighter; @NotNull private final OperationType myType; private MyGutterOperation(@NotNull RangeHighlighter highlighter, @NotNull OperationType type) { myHighlighter = highlighter; myType = type; myHighlighter.setGutterIconRenderer(createRenderer()); } public void dispose() { myHighlighter.dispose(); } @Nullable public GutterIconRenderer createRenderer() { switch (myType) { case APPLY: return createApplyRenderer(); case IGNORE: return createIgnoreRenderer(); default: throw new IllegalArgumentException(myType.name()); } } } @Nullable private GutterIconRenderer createApplyRenderer() { return createIconRenderer(DiffBundle.message("merge.dialog.apply.change.action.name"), DiffUtil.getArrowIcon(Side.RIGHT), () -> { myViewer.executeCommand("Accept change", () -> { myViewer.replaceChange(this); }); }); } @Nullable private GutterIconRenderer createIgnoreRenderer() { return createIconRenderer(DiffBundle.message("merge.dialog.ignore.change.action.name"), AllIcons.Diff.Remove, () -> { myViewer.executeCommand("Ignore change", () -> { myViewer.markChangeResolved(this); }); }); } @Nullable private static GutterIconRenderer createIconRenderer(@NotNull final String text, @NotNull final Icon icon, @NotNull final Runnable perform) { final String tooltipText = DiffUtil.createTooltipText(text, null); return new DiffGutterRenderer(icon, tooltipText) { @Override protected void performAction(AnActionEvent e) { perform.run(); } }; } private enum OperationType { APPLY, IGNORE } // // State // @NotNull public State storeState() { LineRange resultRange = getResultRange(); return new State( myIndex, resultRange != null ? resultRange.start : -1, resultRange != null ? resultRange.end : -1, myResolved); } public void restoreState(@NotNull State state) { myResolved = state.myResolved; } public static class State extends MergeModelBase.State { private final boolean myResolved; public State(int index, int startLine, int endLine, boolean resolved) { super(index, startLine, endLine); myResolved = resolved; } } }