2 * Copyright 2000-2015 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.intellij.diff.tools.fragmented;
18 import com.intellij.diff.DiffContext;
19 import com.intellij.diff.actions.BufferedLineIterator;
20 import com.intellij.diff.actions.NavigationContextChecker;
21 import com.intellij.diff.actions.impl.OpenInEditorWithMouseAction;
22 import com.intellij.diff.actions.impl.SetEditorSettingsAction;
23 import com.intellij.diff.comparison.DiffTooBigException;
24 import com.intellij.diff.contents.DocumentContent;
25 import com.intellij.diff.fragments.LineFragment;
26 import com.intellij.diff.requests.ContentDiffRequest;
27 import com.intellij.diff.requests.DiffRequest;
28 import com.intellij.diff.tools.util.*;
29 import com.intellij.diff.tools.util.base.*;
30 import com.intellij.diff.tools.util.side.TwosideTextDiffViewer;
31 import com.intellij.diff.util.*;
32 import com.intellij.diff.util.DiffUserDataKeysEx.ScrollToPolicy;
33 import com.intellij.icons.AllIcons;
34 import com.intellij.openapi.Disposable;
35 import com.intellij.openapi.actionSystem.*;
36 import com.intellij.openapi.application.ApplicationManager;
37 import com.intellij.openapi.command.undo.UndoManager;
38 import com.intellij.openapi.diagnostic.Logger;
39 import com.intellij.openapi.diff.LineTokenizer;
40 import com.intellij.openapi.editor.*;
41 import com.intellij.openapi.editor.actionSystem.EditorActionManager;
42 import com.intellij.openapi.editor.actionSystem.ReadonlyFragmentModificationHandler;
43 import com.intellij.openapi.editor.colors.EditorColors;
44 import com.intellij.openapi.editor.event.DocumentAdapter;
45 import com.intellij.openapi.editor.event.DocumentEvent;
46 import com.intellij.openapi.editor.ex.EditorEx;
47 import com.intellij.openapi.editor.highlighter.EditorHighlighter;
48 import com.intellij.openapi.fileEditor.OpenFileDescriptor;
49 import com.intellij.openapi.fileTypes.FileType;
50 import com.intellij.openapi.progress.ProcessCanceledException;
51 import com.intellij.openapi.progress.ProgressIndicator;
52 import com.intellij.openapi.project.DumbAware;
53 import com.intellij.openapi.project.Project;
54 import com.intellij.openapi.util.Computable;
55 import com.intellij.openapi.util.Pair;
56 import com.intellij.openapi.util.UserDataHolder;
57 import com.intellij.util.Function;
58 import com.intellij.util.containers.ContainerUtil;
59 import gnu.trove.TIntFunction;
60 import org.jetbrains.annotations.*;
65 import static com.intellij.diff.util.DiffUtil.getLineCount;
67 public class UnifiedDiffViewer extends ListenerDiffViewerBase {
68 public static final Logger LOG = Logger.getInstance(UnifiedDiffViewer.class);
70 @NotNull protected final EditorEx myEditor;
71 @NotNull protected final Document myDocument;
72 @NotNull private final UnifiedDiffPanel myPanel;
74 @NotNull private final SetEditorSettingsAction myEditorSettingsAction;
75 @NotNull private final PrevNextDifferenceIterable myPrevNextDifferenceIterable;
76 @NotNull private final MyStatusPanel myStatusPanel;
78 @NotNull private final MyInitialScrollHelper myInitialScrollHelper = new MyInitialScrollHelper();
79 @NotNull private final MyFoldingModel myFoldingModel;
81 @NotNull protected Side myMasterSide = Side.RIGHT;
83 @Nullable private ChangedBlockData myChangedBlockData;
85 private final boolean[] myForceReadOnlyFlags;
86 private boolean myReadOnlyLockSet = false;
88 private boolean myDuringOnesideDocumentModification;
89 private boolean myDuringTwosideDocumentModification;
91 private boolean myStateIsOutOfDate; // whether something was changed since last rediff
92 private boolean mySuppressEditorTyping; // our state is inconsistent. No typing can be handled correctly
94 public UnifiedDiffViewer(@NotNull DiffContext context, @NotNull DiffRequest request) {
95 super(context, (ContentDiffRequest)request);
97 myPrevNextDifferenceIterable = new MyPrevNextDifferenceIterable();
98 myStatusPanel = new MyStatusPanel();
100 myForceReadOnlyFlags = TextDiffViewerUtil.checkForceReadOnly(myContext, myRequest);
102 boolean leftEditable = isEditable(Side.LEFT, false);
103 boolean rightEditable = isEditable(Side.RIGHT, false);
104 if (leftEditable && !rightEditable) myMasterSide = Side.LEFT;
105 if (!leftEditable && rightEditable) myMasterSide = Side.RIGHT;
108 myDocument = EditorFactory.getInstance().createDocument("");
109 myEditor = DiffUtil.createEditor(myDocument, myProject, true, true);
111 List<JComponent> titles = DiffUtil.createTextTitles(myRequest, ContainerUtil.list(myEditor, myEditor));
112 UnifiedContentPanel contentPanel = new UnifiedContentPanel(titles, myEditor);
114 myPanel = new UnifiedDiffPanel(myProject, contentPanel, this, myContext);
116 myFoldingModel = new MyFoldingModel(myEditor, this);
118 myEditorSettingsAction = new SetEditorSettingsAction(getTextSettings(), getEditors());
119 myEditorSettingsAction.applyDefaults();
121 new MyOpenInEditorWithMouseAction().register(getEditors());
123 TextDiffViewerUtil.checkDifferentDocuments(myRequest);
125 DiffUtil.registerAction(new ReplaceSelectedChangesAction(Side.LEFT, true), myPanel);
126 DiffUtil.registerAction(new AppendSelectedChangesAction(Side.LEFT, true), myPanel);
127 DiffUtil.registerAction(new ReplaceSelectedChangesAction(Side.RIGHT, true), myPanel);
128 DiffUtil.registerAction(new AppendSelectedChangesAction(Side.RIGHT, true), myPanel);
133 protected void onInit() {
135 installEditorListeners();
136 installTypingSupport();
137 myPanel.setLoadingContent(); // We need loading panel only for initial rediff()
138 myPanel.setPersistentNotifications(DiffUtil.getCustomNotifications(myContext, myRequest));
143 protected void onDispose() {
145 EditorFactory.getInstance().releaseEditor(myEditor);
150 protected void processContextHints() {
151 super.processContextHints();
152 Side side = DiffUtil.getUserData(myRequest, myContext, DiffUserDataKeys.MASTER_SIDE);
153 if (side != null) myMasterSide = side;
155 myInitialScrollHelper.processContext(myRequest);
160 protected void updateContextHints() {
161 super.updateContextHints();
162 myInitialScrollHelper.updateContext(myRequest);
163 myFoldingModel.updateContext(myRequest, getFoldingModelSettings());
167 protected void updateEditorCanBeTyped() {
168 myEditor.setViewer(mySuppressEditorTyping || !isEditable(myMasterSide, true));
171 private void installTypingSupport() {
172 if (!isEditable(myMasterSide, false)) return;
174 updateEditorCanBeTyped();
175 myEditor.getColorsScheme().setColor(EditorColors.READONLY_FRAGMENT_BACKGROUND_COLOR, null); // guarded blocks
176 EditorActionManager.getInstance().setReadonlyFragmentModificationHandler(myDocument, new MyReadonlyFragmentModificationHandler());
177 myDocument.putUserData(UndoManager.ORIGINAL_DOCUMENT, getDocument(myMasterSide)); // use undo of master document
179 myDocument.addDocumentListener(new MyOnesideDocumentListener());
184 public List<AnAction> createToolbarActions() {
185 List<AnAction> group = new ArrayList<AnAction>();
187 // TODO: allow to choose myMasterSide
188 group.add(new MyIgnorePolicySettingAction());
189 group.add(new MyHighlightPolicySettingAction());
190 group.add(new MyToggleExpandByDefaultAction());
191 group.add(new MyReadOnlyLockAction());
192 group.add(myEditorSettingsAction);
194 group.add(Separator.getInstance());
195 group.addAll(super.createToolbarActions());
202 public List<AnAction> createPopupActions() {
203 List<AnAction> group = new ArrayList<AnAction>();
205 group.add(Separator.getInstance());
206 group.add(new MyIgnorePolicySettingAction().getPopupGroup());
207 group.add(Separator.getInstance());
208 group.add(new MyHighlightPolicySettingAction().getPopupGroup());
209 group.add(Separator.getInstance());
210 group.add(new MyToggleExpandByDefaultAction());
212 group.add(Separator.getInstance());
213 group.addAll(super.createPopupActions());
219 protected List<AnAction> createEditorPopupActions() {
220 List<AnAction> group = new ArrayList<AnAction>();
222 group.add(new ReplaceSelectedChangesAction(Side.LEFT, false));
223 group.add(new AppendSelectedChangesAction(Side.LEFT, false));
224 group.add(new ReplaceSelectedChangesAction(Side.RIGHT, false));
225 group.add(new AppendSelectedChangesAction(Side.RIGHT, false));
226 group.add(new RevertSelectedChangesAction(Side.LEFT));
227 group.add(new RevertSelectedChangesAction(Side.RIGHT));
229 group.add(Separator.getInstance());
230 group.addAll(TextDiffViewerUtil.createEditorPopupActions());
236 protected void installEditorListeners() {
237 new TextDiffViewerUtil.EditorActionsPopup(createEditorPopupActions()).install(getEditors());
246 protected void onSlowRediff() {
247 super.onSlowRediff();
248 myStatusPanel.setBusy(true);
253 protected Runnable performRediff(@NotNull final ProgressIndicator indicator) {
255 indicator.checkCanceled();
257 final Document document1 = getContent1().getDocument();
258 final Document document2 = getContent2().getDocument();
260 final CharSequence[] texts = ApplicationManager.getApplication().runReadAction(new Computable<CharSequence[]>() {
262 public CharSequence[] compute() {
263 return new CharSequence[]{document1.getImmutableCharSequence(), document2.getImmutableCharSequence()};
267 final boolean innerFragments = getDiffConfig().innerFragments;
268 final List<LineFragment> fragments = DiffUtil.compare(texts[0], texts[1], getDiffConfig(), indicator);
270 final DocumentContent content1 = getContent1();
271 final DocumentContent content2 = getContent2();
273 indicator.checkCanceled();
274 TwosideDocumentData data = ApplicationManager.getApplication().runReadAction(new Computable<TwosideDocumentData>() {
276 public TwosideDocumentData compute() {
277 indicator.checkCanceled();
278 UnifiedFragmentBuilder builder = new UnifiedFragmentBuilder(fragments, document1, document2, myMasterSide);
281 indicator.checkCanceled();
283 EditorHighlighter highlighter = buildHighlighter(myProject, content1, content2,
284 texts[0], texts[1], builder.getRanges(),
285 builder.getText().length());
287 UnifiedEditorRangeHighlighter rangeHighlighter = new UnifiedEditorRangeHighlighter(myProject, document1, document2,
288 builder.getRanges());
290 return new TwosideDocumentData(builder, highlighter, rangeHighlighter);
293 UnifiedFragmentBuilder builder = data.getBuilder();
295 FileType fileType = content2.getContentType() == null ? content1.getContentType() : content2.getContentType();
297 LineNumberConvertor convertor = builder.getConvertor();
298 List<LineRange> changedLines = builder.getChangedLines();
299 boolean isEqual = builder.isEqual();
301 CombinedEditorData editorData = new CombinedEditorData(builder.getText(), data.getHighlighter(), data.getRangeHighlighter(), fileType,
302 convertor.createConvertor1(), convertor.createConvertor2());
304 return apply(editorData, builder.getBlocks(), convertor, changedLines, isEqual, innerFragments);
306 catch (DiffTooBigException e) {
307 return new Runnable() {
310 clearDiffPresentation();
311 myPanel.setTooBigContent();
315 catch (ProcessCanceledException e) {
318 catch (Throwable e) {
320 return new Runnable() {
323 clearDiffPresentation();
324 myPanel.setErrorContent();
330 private void clearDiffPresentation() {
331 myPanel.resetNotifications();
332 myStatusPanel.setBusy(false);
333 destroyChangedBlockData();
335 myStateIsOutOfDate = false;
336 mySuppressEditorTyping = false;
337 updateEditorCanBeTyped();
341 protected void markSuppressEditorTyping() {
342 mySuppressEditorTyping = true;
343 updateEditorCanBeTyped();
347 protected void markStateIsOutOfDate() {
348 myStateIsOutOfDate = true;
349 if (myChangedBlockData != null) {
350 for (UnifiedDiffChange diffChange : myChangedBlockData.getDiffChanges()) {
351 diffChange.updateGutterActions();
357 private EditorHighlighter buildHighlighter(@Nullable Project project,
358 @NotNull DocumentContent content1,
359 @NotNull DocumentContent content2,
360 @NotNull CharSequence text1,
361 @NotNull CharSequence text2,
362 @NotNull List<HighlightRange> ranges,
364 EditorHighlighter highlighter1 = DiffUtil.initEditorHighlighter(project, content1, text1);
365 EditorHighlighter highlighter2 = DiffUtil.initEditorHighlighter(project, content2, text2);
367 if (highlighter1 == null && highlighter2 == null) return null;
368 if (highlighter1 == null) highlighter1 = DiffUtil.initEmptyEditorHighlighter(text1);
369 if (highlighter2 == null) highlighter2 = DiffUtil.initEmptyEditorHighlighter(text2);
371 return new UnifiedEditorHighlighter(myDocument, highlighter1, highlighter2, ranges, textLength);
375 private Runnable apply(@NotNull final CombinedEditorData data,
376 @NotNull final List<ChangedBlock> blocks,
377 @NotNull final LineNumberConvertor convertor,
378 @NotNull final List<LineRange> changedLines,
379 final boolean isEqual, final boolean innerFragments) {
380 return new Runnable() {
383 myFoldingModel.updateContext(myRequest, getFoldingModelSettings());
385 clearDiffPresentation();
386 if (isEqual) myPanel.addNotification(DiffNotifications.EQUAL_CONTENTS);
388 TIntFunction separatorLines = myFoldingModel.getLineNumberConvertor();
389 myEditor.getGutterComponentEx().setLineNumberConvertor(mergeConverters(data.getLineConvertor1(), separatorLines),
390 mergeConverters(data.getLineConvertor2(), separatorLines));
392 ApplicationManager.getApplication().runWriteAction(new Runnable() {
395 myDuringOnesideDocumentModification = true;
397 myDocument.setText(data.getText());
400 myDuringOnesideDocumentModification = false;
405 if (data.getHighlighter() != null) myEditor.setHighlighter(data.getHighlighter());
406 DiffUtil.setEditorCodeStyle(myProject, myEditor, data.getFileType());
408 if (data.getRangeHighlighter() != null) data.getRangeHighlighter().apply(myProject, myDocument);
411 ArrayList<UnifiedDiffChange> diffChanges = new ArrayList<UnifiedDiffChange>(blocks.size());
412 for (ChangedBlock block : blocks) {
413 diffChanges.add(new UnifiedDiffChange(UnifiedDiffViewer.this, block, innerFragments));
416 List<RangeMarker> guarderRangeBlocks = new ArrayList<RangeMarker>();
417 if (!myEditor.isViewer()) {
418 for (ChangedBlock block : blocks) {
419 int start = myMasterSide.select(block.getStartOffset2(), block.getStartOffset1());
420 int end = myMasterSide.select(block.getEndOffset2() - 1, block.getEndOffset1() - 1);
421 if (start >= end) continue;
422 guarderRangeBlocks.add(createGuardedBlock(start, end));
424 int textLength = myDocument.getTextLength(); // there are 'fake' newline at the very end
425 guarderRangeBlocks.add(createGuardedBlock(textLength, textLength));
428 myChangedBlockData = new ChangedBlockData(diffChanges, guarderRangeBlocks, convertor);
430 myFoldingModel.install(changedLines, myRequest, getFoldingModelSettings());
432 myInitialScrollHelper.onRediff();
434 myStatusPanel.update();
435 myPanel.setGoodContent();
441 private RangeMarker createGuardedBlock(int start, int end) {
442 RangeMarker block = myDocument.createGuardedBlock(start, end);
443 block.setGreedyToLeft(true);
444 block.setGreedyToRight(true);
448 @Contract("!null, _ -> !null")
449 private static TIntFunction mergeConverters(@NotNull final TIntFunction convertor, @NotNull final TIntFunction separatorLines) {
450 return new TIntFunction() {
452 public int execute(int value) {
453 return convertor.execute(separatorLines.execute(value));
459 * This convertor returns -1 if exact matching is impossible
462 protected int transferLineToOnesideStrict(@NotNull Side side, int line) {
463 if (myChangedBlockData == null) return -1;
465 LineNumberConvertor lineConvertor = myChangedBlockData.getLineNumberConvertor();
466 return side.isLeft() ? lineConvertor.convertInv1(line) : lineConvertor.convertInv2(line);
470 * This convertor returns -1 if exact matching is impossible
473 protected int transferLineFromOnesideStrict(@NotNull Side side, int line) {
474 if (myChangedBlockData == null) return -1;
476 LineNumberConvertor lineConvertor = myChangedBlockData.getLineNumberConvertor();
477 return side.isLeft() ? lineConvertor.convert1(line) : lineConvertor.convert2(line);
481 * This convertor returns 'good enough' position, even if exact matching is impossible
484 protected int transferLineToOneside(@NotNull Side side, int line) {
485 if (myChangedBlockData == null) return line;
487 LineNumberConvertor lineConvertor = myChangedBlockData.getLineNumberConvertor();
488 return side.isLeft() ? lineConvertor.convertApproximateInv1(line) : lineConvertor.convertApproximateInv2(line);
492 * This convertor returns 'good enough' position, even if exact matching is impossible
496 protected Pair<int[], Side> transferLineFromOneside(int line) {
497 int[] lines = new int[2];
499 if (myChangedBlockData == null) {
502 return Pair.create(lines, myMasterSide);
505 LineNumberConvertor lineConvertor = myChangedBlockData.getLineNumberConvertor();
507 Side side = myMasterSide;
508 lines[0] = lineConvertor.convert1(line);
509 lines[1] = lineConvertor.convert2(line);
511 if (lines[0] == -1 && lines[1] == -1) {
512 lines[0] = lineConvertor.convertApproximate1(line);
513 lines[1] = lineConvertor.convertApproximate2(line);
515 else if (lines[0] == -1) {
516 lines[0] = lineConvertor.convertApproximate1(line);
519 else if (lines[1] == -1) {
520 lines[1] = lineConvertor.convertApproximate2(line);
524 return Pair.create(lines, side);
528 private void destroyChangedBlockData() {
529 if (myChangedBlockData == null) return;
531 for (UnifiedDiffChange change : myChangedBlockData.getDiffChanges()) {
532 change.destroyHighlighter();
534 for (RangeMarker block : myChangedBlockData.getGuardedRangeBlocks()) {
535 myDocument.removeGuardedBlock(block);
537 myChangedBlockData = null;
539 UnifiedEditorRangeHighlighter.erase(myProject, myDocument);
541 myFoldingModel.destroy();
543 myStatusPanel.update();
550 private class MyOnesideDocumentListener extends DocumentAdapter {
552 public void beforeDocumentChange(DocumentEvent e) {
553 if (myDuringOnesideDocumentModification) return;
554 if (myChangedBlockData == null) {
555 LOG.warn("oneside beforeDocumentChange - myChangedBlockData == null");
558 // TODO: modify Document guard range logic - we can handle case, when whole read-only block is modified (ex: my replacing selection).
561 myDuringTwosideDocumentModification = true;
563 Document twosideDocument = getDocument(myMasterSide);
565 LineCol onesideStartPosition = LineCol.fromOffset(myDocument, e.getOffset());
566 LineCol onesideEndPosition = LineCol.fromOffset(myDocument, e.getOffset() + e.getOldLength());
568 int line1 = onesideStartPosition.line;
569 int line2 = onesideEndPosition.line + 1;
570 int shift = DiffUtil.countLinesShift(e);
572 int twosideStartLine = transferLineFromOnesideStrict(myMasterSide, onesideStartPosition.line);
573 int twosideEndLine = transferLineFromOnesideStrict(myMasterSide, onesideEndPosition.line);
574 if (twosideStartLine == -1 || twosideEndLine == -1) {
575 // this should never happen
576 logDebugInfo(e, onesideStartPosition, onesideEndPosition, twosideStartLine, twosideEndLine);
577 markSuppressEditorTyping();
581 int twosideStartOffset = twosideDocument.getLineStartOffset(twosideStartLine) + onesideStartPosition.column;
582 int twosideEndOffset = twosideDocument.getLineStartOffset(twosideEndLine) + onesideEndPosition.column;
583 twosideDocument.replaceString(twosideStartOffset, twosideEndOffset, e.getNewFragment());
585 for (UnifiedDiffChange change : myChangedBlockData.getDiffChanges()) {
586 change.processChange(line1, line2, shift);
589 LineNumberConvertor lineNumberConvertor = myChangedBlockData.getLineNumberConvertor();
590 lineNumberConvertor.handleOnesideChange(line1, line2, shift, myMasterSide);
593 // TODO: we can avoid marking state out-of-date in some simple cases (like in SimpleDiffViewer)
594 // but this will greatly increase complexity, so let's wait if it's actually required by users
595 markStateIsOutOfDate();
597 myFoldingModel.onDocumentChanged(e);
600 myDuringTwosideDocumentModification = false;
604 private void logDebugInfo(DocumentEvent e,
605 LineCol onesideStartPosition, LineCol onesideEndPosition,
606 int twosideStartLine, int twosideEndLine) {
607 StringBuilder info = new StringBuilder();
608 Document document1 = getDocument(Side.LEFT);
609 Document document2 = getDocument(Side.RIGHT);
610 info.append("==== UnifiedDiffViewer Debug Info ====");
611 info.append("myMasterSide - ").append(myMasterSide).append('\n');
612 info.append("myLeftDocument.length() - ").append(document1.getTextLength()).append('\n');
613 info.append("myRightDocument.length() - ").append(document2.getTextLength()).append('\n');
614 info.append("myDocument.length() - ").append(myDocument.getTextLength()).append('\n');
615 info.append("e.getOffset() - ").append(e.getOffset()).append('\n');
616 info.append("e.getNewLength() - ").append(e.getNewLength()).append('\n');
617 info.append("e.getOldLength() - ").append(e.getOldLength()).append('\n');
618 info.append("onesideStartPosition - ").append(onesideStartPosition).append('\n');
619 info.append("onesideEndPosition - ").append(onesideEndPosition).append('\n');
620 info.append("twosideStartLine - ").append(twosideStartLine).append('\n');
621 info.append("twosideEndLine - ").append(twosideEndLine).append('\n');
622 Pair<int[], Side> pair1 = transferLineFromOneside(onesideStartPosition.line);
623 Pair<int[], Side> pair2 = transferLineFromOneside(onesideEndPosition.line);
624 info.append("non-strict transferStartLine - ").append(pair1.first[0]).append("-").append(pair1.first[1])
625 .append(":").append(pair1.second).append('\n');
626 info.append("non-strict transferEndLine - ").append(pair2.first[0]).append("-").append(pair2.first[1])
627 .append(":").append(pair2.second).append('\n');
628 info.append("---- UnifiedDiffViewer Debug Info ----");
630 LOG.warn(info.toString());
635 protected void onDocumentChange(@NotNull DocumentEvent e) {
636 if (myDuringTwosideDocumentModification) return;
638 markStateIsOutOfDate();
639 markSuppressEditorTyping();
641 myFoldingModel.onDocumentChanged(e);
646 // Modification operations
649 private abstract class ApplySelectedChangesActionBase extends AnAction implements DumbAware {
650 @NotNull protected final Side myModifiedSide;
651 private final boolean myShortcut;
653 public ApplySelectedChangesActionBase(@NotNull Side modifiedSide, boolean shortcut) {
654 myModifiedSide = modifiedSide;
655 myShortcut = shortcut;
659 public void update(@NotNull AnActionEvent e) {
661 // consume shortcut even if there are nothing to do - avoid calling some other action
662 e.getPresentation().setEnabledAndVisible(true);
666 Editor editor = e.getData(CommonDataKeys.EDITOR);
667 if (editor != getEditor()) {
668 e.getPresentation().setEnabledAndVisible(false);
672 if (!isEditable(myModifiedSide, true) || isStateIsOutOfDate()) {
673 e.getPresentation().setEnabledAndVisible(false);
677 e.getPresentation().setVisible(true);
678 e.getPresentation().setEnabled(isSomeChangeSelected());
682 public void actionPerformed(@NotNull final AnActionEvent e) {
683 final List<UnifiedDiffChange> selectedChanges = getSelectedChanges();
684 if (selectedChanges.isEmpty()) return;
686 if (!isEditable(myModifiedSide, true)) return;
687 if (isStateIsOutOfDate()) return;
689 String title = e.getPresentation().getText() + " selected changes";
690 DiffUtil.executeWriteCommand(getDocument(myModifiedSide), e.getProject(), title, new Runnable() {
693 // state is invalidated during apply(), but changes are in reverse order, so they should not conflict with each other
694 apply(selectedChanges);
700 protected boolean isSomeChangeSelected() {
701 if (myChangedBlockData == null) return false;
702 List<UnifiedDiffChange> changes = myChangedBlockData.getDiffChanges();
703 if (changes.isEmpty()) return false;
705 List<Caret> carets = getEditor().getCaretModel().getAllCarets();
706 if (carets.size() != 1) return true;
707 Caret caret = carets.get(0);
708 if (caret.hasSelection()) return true;
709 int line = getEditor().getDocument().getLineNumber(getEditor().getExpectedCaretOffset());
711 for (UnifiedDiffChange change : changes) {
712 if (DiffUtil.isSelectedByLine(line, change.getLine1(), change.getLine2())) return true;
718 protected abstract void apply(@NotNull List<UnifiedDiffChange> changes);
721 private class ReplaceSelectedChangesAction extends ApplySelectedChangesActionBase {
722 public ReplaceSelectedChangesAction(@NotNull Side focusedSide, boolean shortcut) {
723 super(focusedSide.other(), shortcut);
725 setShortcutSet(ActionManager.getInstance().getAction(focusedSide.select("Diff.ApplyLeftSide", "Diff.ApplyRightSide")).getShortcutSet());
726 getTemplatePresentation().setText("Replace");
727 getTemplatePresentation().setIcon(focusedSide.select(AllIcons.Diff.ArrowRight, AllIcons.Diff.Arrow));
731 protected void apply(@NotNull List<UnifiedDiffChange> changes) {
732 for (UnifiedDiffChange change : changes) {
733 replaceChange(change, myModifiedSide.other());
738 private class AppendSelectedChangesAction extends ApplySelectedChangesActionBase {
739 public AppendSelectedChangesAction(@NotNull Side focusedSide, boolean shortcut) {
740 super(focusedSide.other(), shortcut);
742 setShortcutSet(ActionManager.getInstance().getAction(focusedSide.select("Diff.AppendLeftSide", "Diff.AppendRightSide")).getShortcutSet());
743 getTemplatePresentation().setText("Insert");
744 getTemplatePresentation().setIcon(focusedSide.select(AllIcons.Diff.ArrowRightDown, AllIcons.Diff.ArrowLeftDown));
748 protected void apply(@NotNull List<UnifiedDiffChange> changes) {
749 for (UnifiedDiffChange change : changes) {
750 appendChange(change, myModifiedSide.other());
755 private class RevertSelectedChangesAction extends ApplySelectedChangesActionBase {
756 public RevertSelectedChangesAction(@NotNull Side focusedSide) {
757 super(focusedSide, false);
758 getTemplatePresentation().setText("Revert");
759 getTemplatePresentation().setIcon(AllIcons.Diff.Remove);
763 protected void apply(@NotNull List<UnifiedDiffChange> changes) {
764 for (UnifiedDiffChange change : changes) {
765 replaceChange(change, myModifiedSide.other());
771 public void replaceChange(@NotNull UnifiedDiffChange change, @NotNull Side sourceSide) {
772 Side outputSide = sourceSide.other();
774 Document document1 = getDocument(Side.LEFT);
775 Document document2 = getDocument(Side.RIGHT);
777 LineFragment lineFragment = change.getLineFragment();
779 DiffUtil.applyModification(outputSide.select(document1, document2),
780 outputSide.getStartLine(lineFragment), outputSide.getEndLine(lineFragment),
781 sourceSide.select(document1, document2),
782 sourceSide.getStartLine(lineFragment), sourceSide.getEndLine(lineFragment));
784 // no need to mark myStateIsOutOfDate - it will be made by DocumentListener
785 // TODO: we can apply change manually, without marking state out-of-date. But we'll have to schedule rediff anyway.
789 public void appendChange(@NotNull UnifiedDiffChange change, @NotNull final Side sourceSide) {
790 Side outputSide = sourceSide.other();
792 Document document1 = getDocument(Side.LEFT);
793 Document document2 = getDocument(Side.RIGHT);
795 LineFragment lineFragment = change.getLineFragment();
796 if (sourceSide.getStartLine(lineFragment) == sourceSide.getEndLine(lineFragment)) return;
798 DiffUtil.applyModification(outputSide.select(document1, document2),
799 outputSide.getEndLine(lineFragment), outputSide.getEndLine(lineFragment),
800 sourceSide.select(document1, document2),
801 sourceSide.getStartLine(lineFragment), sourceSide.getEndLine(lineFragment));
810 public TextDiffSettingsHolder.TextDiffSettings getTextSettings() {
811 return TextDiffViewerUtil.getTextSettings(myContext);
815 public FoldingModelSupport.Settings getFoldingModelSettings() {
816 return TextDiffViewerUtil.getFoldingModelSettings(myContext);
820 private DiffUtil.DiffConfig getDiffConfig() {
821 return new DiffUtil.DiffConfig(getIgnorePolicy(), getHighlightPolicy());
825 private HighlightPolicy getHighlightPolicy() {
826 HighlightPolicy policy = getTextSettings().getHighlightPolicy();
827 if (policy == HighlightPolicy.DO_NOT_HIGHLIGHT) return HighlightPolicy.BY_LINE;
832 private IgnorePolicy getIgnorePolicy() {
833 IgnorePolicy policy = getTextSettings().getIgnorePolicy();
834 if (policy == IgnorePolicy.IGNORE_WHITESPACES_CHUNKS) return IgnorePolicy.IGNORE_WHITESPACES;
843 public EditorEx getEditor() {
848 protected List<? extends EditorEx> getEditors() {
849 return Collections.singletonList(myEditor);
853 protected List<? extends DocumentContent> getContents() {
854 //noinspection unchecked
855 return (List<? extends DocumentContent>)(List)myRequest.getContents();
859 protected DocumentContent getContent(@NotNull Side side) {
860 return side.select(getContents());
864 protected DocumentContent getContent1() {
865 return getContent(Side.LEFT);
869 protected DocumentContent getContent2() {
870 return getContent(Side.RIGHT);
875 protected List<UnifiedDiffChange> getDiffChanges() {
876 return myChangedBlockData == null ? null : myChangedBlockData.getDiffChanges();
881 public JComponent getComponent() {
887 public JComponent getPreferredFocusedComponent() {
888 if (!myPanel.isGoodContent()) return null;
889 return myEditor.getContentComponent();
894 protected JComponent getStatusPanel() {
895 return myStatusPanel;
899 public boolean isEditable(@NotNull Side side, boolean respectReadOnlyLock) {
900 if (myReadOnlyLockSet && respectReadOnlyLock) return false;
901 if (side.select(myForceReadOnlyFlags)) return false;
902 return DiffUtil.canMakeWritable(getDocument(side));
906 public Document getDocument(@NotNull Side side) {
907 return getContent(side).getDocument();
910 protected boolean isStateIsOutOfDate() {
911 return myStateIsOutOfDate;
920 protected OpenFileDescriptor getOpenFileDescriptor() {
921 return getOpenFileDescriptor(myEditor.getCaretModel().getOffset());
926 protected UnifiedDiffChange getCurrentChange() {
927 if (myChangedBlockData == null) return null;
928 int caretLine = myEditor.getCaretModel().getLogicalPosition().line;
930 for (UnifiedDiffChange change : myChangedBlockData.getDiffChanges()) {
931 if (DiffUtil.isSelectedByLine(caretLine, change.getLine1(), change.getLine2())) return change;
938 private List<UnifiedDiffChange> getSelectedChanges() {
939 if (myChangedBlockData == null) return Collections.emptyList();
940 final BitSet lines = DiffUtil.getSelectedLines(myEditor);
941 List<UnifiedDiffChange> changes = myChangedBlockData.getDiffChanges();
943 List<UnifiedDiffChange> affectedChanges = new ArrayList<UnifiedDiffChange>();
944 for (int i = changes.size() - 1; i >= 0; i--) {
945 UnifiedDiffChange change = changes.get(i);
946 int line1 = change.getLine1();
947 int line2 = change.getLine2();
949 if (DiffUtil.isSelectedByLine(lines, line1, line2)) {
950 affectedChanges.add(change);
953 return affectedChanges;
958 protected OpenFileDescriptor getOpenFileDescriptor(int offset) {
959 LogicalPosition position = myEditor.offsetToLogicalPosition(offset);
960 Pair<int[], Side> pair = transferLineFromOneside(position.line);
961 int offset1 = DiffUtil.getOffset(getContent1().getDocument(), pair.first[0], position.column);
962 int offset2 = DiffUtil.getOffset(getContent2().getDocument(), pair.first[1], position.column);
964 // TODO: issue: non-optimal GoToSource position with caret on deleted block for "Compare with local"
965 // we should transfer using calculated diff, not jump to "somehow related" position from old content's descriptor
967 OpenFileDescriptor descriptor1 = getContent1().getOpenFileDescriptor(offset1);
968 OpenFileDescriptor descriptor2 = getContent2().getOpenFileDescriptor(offset2);
969 if (descriptor1 == null) return descriptor2;
970 if (descriptor2 == null) return descriptor1;
971 return pair.second.select(descriptor1, descriptor2);
974 public static boolean canShowRequest(@NotNull DiffContext context, @NotNull DiffRequest request) {
975 return TwosideTextDiffViewer.canShowRequest(context, request);
982 private class MyPrevNextDifferenceIterable extends PrevNextDifferenceIterableBase<UnifiedDiffChange> {
985 protected List<UnifiedDiffChange> getChanges() {
986 return ContainerUtil.notNullize(getDiffChanges());
991 protected EditorEx getEditor() {
996 protected int getStartLine(@NotNull UnifiedDiffChange change) {
997 return change.getLine1();
1001 protected int getEndLine(@NotNull UnifiedDiffChange change) {
1002 return change.getLine2();
1006 protected void scrollToChange(@NotNull UnifiedDiffChange change) {
1007 DiffUtil.scrollEditor(myEditor, change.getLine1(), true);
1011 private class MyOpenInEditorWithMouseAction extends OpenInEditorWithMouseAction {
1013 protected OpenFileDescriptor getDescriptor(@NotNull Editor editor, int line) {
1014 if (editor != myEditor) return null;
1016 return getOpenFileDescriptor(myEditor.logicalPositionToOffset(new LogicalPosition(line, 0)));
1020 private class MyToggleExpandByDefaultAction extends TextDiffViewerUtil.ToggleExpandByDefaultAction {
1021 public MyToggleExpandByDefaultAction() {
1022 super(getTextSettings());
1026 protected void expandAll(boolean expand) {
1027 myFoldingModel.expandAll(expand);
1031 private class MyHighlightPolicySettingAction extends TextDiffViewerUtil.HighlightPolicySettingAction {
1032 public MyHighlightPolicySettingAction() {
1033 super(getTextSettings());
1038 protected HighlightPolicy getCurrentSetting() {
1039 return getHighlightPolicy();
1044 protected List<HighlightPolicy> getAvailableSettings() {
1045 ArrayList<HighlightPolicy> settings = ContainerUtil.newArrayList(HighlightPolicy.values());
1046 settings.remove(HighlightPolicy.DO_NOT_HIGHLIGHT);
1051 protected void onSettingsChanged() {
1056 private class MyIgnorePolicySettingAction extends TextDiffViewerUtil.IgnorePolicySettingAction {
1057 public MyIgnorePolicySettingAction() {
1058 super(getTextSettings());
1063 protected IgnorePolicy getCurrentSetting() {
1064 return getIgnorePolicy();
1069 protected List<IgnorePolicy> getAvailableSettings() {
1070 ArrayList<IgnorePolicy> settings = ContainerUtil.newArrayList(IgnorePolicy.values());
1071 settings.remove(IgnorePolicy.IGNORE_WHITESPACES_CHUNKS);
1076 protected void onSettingsChanged() {
1081 private class MyReadOnlyLockAction extends TextDiffViewerUtil.ReadOnlyLockAction {
1082 public MyReadOnlyLockAction() {
1083 super(getContext());
1088 protected void doApply(boolean readOnly) {
1089 myReadOnlyLockSet = readOnly;
1090 if (myChangedBlockData != null) {
1091 for (UnifiedDiffChange unifiedDiffChange : myChangedBlockData.getDiffChanges()) {
1092 unifiedDiffChange.updateGutterActions();
1095 updateEditorCanBeTyped();
1099 protected boolean canEdit() {
1100 return !myForceReadOnlyFlags[0] && DiffUtil.canMakeWritable(getContent1().getDocument()) ||
1101 !myForceReadOnlyFlags[1] && DiffUtil.canMakeWritable(getContent2().getDocument());
1106 // Scroll from annotate
1109 private class AllLinesIterator implements Iterator<Pair<Integer, CharSequence>> {
1110 @NotNull private final Side mySide;
1111 @NotNull private final Document myDocument;
1112 private int myLine = 0;
1114 private AllLinesIterator(@NotNull Side side) {
1117 myDocument = getContent(mySide).getDocument();
1121 public boolean hasNext() {
1122 return myLine < getLineCount(myDocument);
1126 public Pair<Integer, CharSequence> next() {
1127 int offset1 = myDocument.getLineStartOffset(myLine);
1128 int offset2 = myDocument.getLineEndOffset(myLine);
1130 CharSequence text = myDocument.getImmutableCharSequence().subSequence(offset1, offset2);
1132 Pair<Integer, CharSequence> pair = new Pair<Integer, CharSequence>(myLine, text);
1139 public void remove() {
1140 throw new UnsupportedOperationException();
1144 private class ChangedLinesIterator extends BufferedLineIterator {
1145 @NotNull private final Side mySide;
1146 @NotNull private final List<UnifiedDiffChange> myChanges;
1148 private int myIndex = 0;
1150 private ChangedLinesIterator(@NotNull Side side, @NotNull List<UnifiedDiffChange> changes) {
1152 myChanges = changes;
1157 public boolean hasNextBlock() {
1158 return myIndex < myChanges.size();
1162 public void loadNextBlock() {
1163 LOG.assertTrue(!myStateIsOutOfDate);
1165 UnifiedDiffChange change = myChanges.get(myIndex);
1168 LineFragment lineFragment = change.getLineFragment();
1170 int insertedStart = lineFragment.getStartOffset2();
1171 int insertedEnd = lineFragment.getEndOffset2();
1172 CharSequence insertedText = getContent(mySide).getDocument().getCharsSequence().subSequence(insertedStart, insertedEnd);
1174 int lineNumber = lineFragment.getStartLine2();
1176 LineTokenizer tokenizer = new LineTokenizer(insertedText.toString());
1177 for (String line : tokenizer.execute()) {
1178 addLine(lineNumber, line);
1190 public Object getData(@NonNls String dataId) {
1191 if (DiffDataKeys.PREV_NEXT_DIFFERENCE_ITERABLE.is(dataId)) {
1192 return myPrevNextDifferenceIterable;
1194 else if (CommonDataKeys.VIRTUAL_FILE.is(dataId)) {
1195 return DiffUtil.getVirtualFile(myRequest, myMasterSide);
1197 else if (DiffDataKeys.CURRENT_EDITOR.is(dataId)) {
1200 else if (DiffDataKeys.CURRENT_CHANGE_RANGE.is(dataId)) {
1201 UnifiedDiffChange change = getCurrentChange();
1202 if (change != null) {
1203 return new LineRange(change.getLine1(), change.getLine2());
1206 return super.getData(dataId);
1209 private class MyStatusPanel extends StatusPanel {
1211 protected int getChangesCount() {
1212 return myChangedBlockData == null ? 0 : myChangedBlockData.getDiffChanges().size();
1216 private static class TwosideDocumentData {
1217 @NotNull private final UnifiedFragmentBuilder myBuilder;
1218 @Nullable private final EditorHighlighter myHighlighter;
1219 @Nullable private final UnifiedEditorRangeHighlighter myRangeHighlighter;
1221 public TwosideDocumentData(@NotNull UnifiedFragmentBuilder builder,
1222 @Nullable EditorHighlighter highlighter,
1223 @Nullable UnifiedEditorRangeHighlighter rangeHighlighter) {
1224 myBuilder = builder;
1225 myHighlighter = highlighter;
1226 myRangeHighlighter = rangeHighlighter;
1230 public UnifiedFragmentBuilder getBuilder() {
1235 public EditorHighlighter getHighlighter() {
1236 return myHighlighter;
1240 public UnifiedEditorRangeHighlighter getRangeHighlighter() {
1241 return myRangeHighlighter;
1245 private static class ChangedBlockData {
1246 @NotNull private final List<UnifiedDiffChange> myDiffChanges;
1247 @NotNull private final List<RangeMarker> myGuardedRangeBlocks;
1248 @NotNull private final LineNumberConvertor myLineNumberConvertor;
1250 public ChangedBlockData(@NotNull List<UnifiedDiffChange> diffChanges,
1251 @NotNull List<RangeMarker> guarderRangeBlocks,
1252 @NotNull LineNumberConvertor lineNumberConvertor) {
1253 myDiffChanges = diffChanges;
1254 myGuardedRangeBlocks = guarderRangeBlocks;
1255 myLineNumberConvertor = lineNumberConvertor;
1259 public List<UnifiedDiffChange> getDiffChanges() {
1260 return myDiffChanges;
1264 public List<RangeMarker> getGuardedRangeBlocks() {
1265 return myGuardedRangeBlocks;
1269 public LineNumberConvertor getLineNumberConvertor() {
1270 return myLineNumberConvertor;
1274 private static class CombinedEditorData {
1275 @NotNull private final CharSequence myText;
1276 @Nullable private final EditorHighlighter myHighlighter;
1277 @Nullable private final UnifiedEditorRangeHighlighter myRangeHighlighter;
1278 @Nullable private final FileType myFileType;
1279 @NotNull private final TIntFunction myLineConvertor1;
1280 @NotNull private final TIntFunction myLineConvertor2;
1282 public CombinedEditorData(@NotNull CharSequence text,
1283 @Nullable EditorHighlighter highlighter,
1284 @Nullable UnifiedEditorRangeHighlighter rangeHighlighter,
1285 @Nullable FileType fileType,
1286 @NotNull TIntFunction convertor1,
1287 @NotNull TIntFunction convertor2) {
1289 myHighlighter = highlighter;
1290 myRangeHighlighter = rangeHighlighter;
1291 myFileType = fileType;
1292 myLineConvertor1 = convertor1;
1293 myLineConvertor2 = convertor2;
1297 public CharSequence getText() {
1302 public EditorHighlighter getHighlighter() {
1303 return myHighlighter;
1307 public UnifiedEditorRangeHighlighter getRangeHighlighter() {
1308 return myRangeHighlighter;
1312 public FileType getFileType() {
1317 public TIntFunction getLineConvertor1() {
1318 return myLineConvertor1;
1322 public TIntFunction getLineConvertor2() {
1323 return myLineConvertor2;
1327 private class MyInitialScrollHelper extends InitialScrollPositionSupport.TwosideInitialScrollHelper {
1330 protected List<? extends Editor> getEditors() {
1331 return UnifiedDiffViewer.this.getEditors();
1335 protected void disableSyncScroll(boolean value) {
1339 public void onSlowRediff() {
1340 // Will not happen for initial rediff
1345 protected LogicalPosition[] getCaretPositions() {
1346 LogicalPosition position = myEditor.getCaretModel().getLogicalPosition();
1347 Pair<int[], Side> pair = transferLineFromOneside(position.line);
1348 LogicalPosition[] carets = new LogicalPosition[2];
1349 carets[0] = getPosition(pair.first[0], position.column);
1350 carets[1] = getPosition(pair.first[1], position.column);
1355 protected boolean doScrollToPosition() {
1356 if (myCaretPosition == null) return false;
1358 LogicalPosition twosidePosition = myMasterSide.selectNotNull(myCaretPosition);
1359 int onesideLine = transferLineToOneside(myMasterSide, twosidePosition.line);
1360 LogicalPosition position = new LogicalPosition(onesideLine, twosidePosition.column);
1362 myEditor.getCaretModel().moveToLogicalPosition(position);
1364 if (myEditorsPosition != null && myEditorsPosition.isSame(position)) {
1365 DiffUtil.scrollToPoint(myEditor, myEditorsPosition.myPoints[0], false);
1368 DiffUtil.scrollToCaret(myEditor, false);
1374 private LogicalPosition getPosition(int line, int column) {
1375 if (line == -1) return new LogicalPosition(0, 0);
1376 return new LogicalPosition(line, column);
1379 private void doScrollToLine(@NotNull Side side, @NotNull LogicalPosition position) {
1380 int onesideLine = transferLineToOneside(side, position.line);
1381 DiffUtil.scrollEditor(myEditor, onesideLine, position.column, false);
1385 protected boolean doScrollToLine() {
1386 if (myScrollToLine == null) return false;
1387 doScrollToLine(myScrollToLine.first, new LogicalPosition(myScrollToLine.second, 0));
1391 private boolean doScrollToChange(@NotNull ScrollToPolicy scrollToChangePolicy) {
1392 if (myChangedBlockData == null) return false;
1393 List<UnifiedDiffChange> changes = myChangedBlockData.getDiffChanges();
1395 UnifiedDiffChange targetChange = scrollToChangePolicy.select(changes);
1396 if (targetChange == null) return false;
1398 DiffUtil.scrollEditor(myEditor, targetChange.getLine1(), false);
1403 protected boolean doScrollToChange() {
1404 if (myScrollToChange == null) return false;
1405 return doScrollToChange(myScrollToChange);
1409 protected boolean doScrollToFirstChange() {
1410 return doScrollToChange(ScrollToPolicy.FIRST_CHANGE);
1414 protected boolean doScrollToContext() {
1415 if (myNavigationContext == null) return false;
1416 if (myChangedBlockData == null) return false;
1418 ChangedLinesIterator changedLinesIterator = new ChangedLinesIterator(Side.RIGHT, myChangedBlockData.getDiffChanges());
1419 NavigationContextChecker checker = new NavigationContextChecker(changedLinesIterator, myNavigationContext);
1420 int line = checker.contextMatchCheck();
1422 // this will work for the case, when spaces changes are ignored, and corresponding fragments are not reported as changed
1423 // just try to find target line -> +-
1424 AllLinesIterator allLinesIterator = new AllLinesIterator(Side.RIGHT);
1425 NavigationContextChecker checker2 = new NavigationContextChecker(allLinesIterator, myNavigationContext);
1426 line = checker2.contextMatchCheck();
1428 if (line == -1) return false;
1430 doScrollToLine(Side.RIGHT, new LogicalPosition(line, 0));
1435 private static class MyFoldingModel extends FoldingModelSupport {
1436 public MyFoldingModel(@NotNull EditorEx editor, @NotNull Disposable disposable) {
1437 super(new EditorEx[]{editor}, disposable);
1440 public void install(@Nullable List<LineRange> changedLines,
1441 @NotNull UserDataHolder context,
1442 @NotNull FoldingModelSupport.Settings settings) {
1443 Iterator<int[]> it = map(changedLines, new Function<LineRange, int[]>() {
1445 public int[] fun(LineRange line) {
1451 install(it, context, settings);
1455 public TIntFunction getLineNumberConvertor() {
1456 return getLineConvertor(0);
1460 private static class MyReadonlyFragmentModificationHandler implements ReadonlyFragmentModificationHandler {
1462 public void handle(ReadOnlyFragmentModificationException e) {