cleanup (inspection "Java | Class structure | Utility class is not 'final'")
[idea/community.git] / platform / diff-impl / src / com / intellij / diff / tools / util / base / TextDiffViewerUtil.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.diff.tools.util.base;
3
4 import com.intellij.diff.DiffContext;
5 import com.intellij.diff.contents.DiffContent;
6 import com.intellij.diff.contents.DocumentContent;
7 import com.intellij.diff.contents.EmptyContent;
8 import com.intellij.diff.requests.ContentDiffRequest;
9 import com.intellij.diff.tools.util.FoldingModelSupport;
10 import com.intellij.diff.tools.util.base.TextDiffSettingsHolder.TextDiffSettings;
11 import com.intellij.diff.util.DiffUserDataKeys;
12 import com.intellij.diff.util.DiffUserDataKeysEx;
13 import com.intellij.icons.AllIcons;
14 import com.intellij.openapi.Disposable;
15 import com.intellij.openapi.actionSystem.*;
16 import com.intellij.openapi.actionSystem.ex.ActionUtil;
17 import com.intellij.openapi.actionSystem.ex.ComboBoxAction;
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.diff.DiffBundle;
20 import com.intellij.openapi.diff.impl.DiffUsageTriggerCollector;
21 import com.intellij.openapi.editor.Document;
22 import com.intellij.openapi.editor.EditorModificationUtil;
23 import com.intellij.openapi.editor.event.DocumentListener;
24 import com.intellij.openapi.editor.ex.EditorEx;
25 import com.intellij.openapi.editor.ex.EditorPopupHandler;
26 import com.intellij.openapi.editor.impl.ContextMenuPopupHandler;
27 import com.intellij.openapi.project.DumbAware;
28 import com.intellij.ui.ToggleActionButton;
29 import com.intellij.util.ArrayUtil;
30 import com.intellij.util.Function;
31 import com.intellij.util.containers.ContainerUtil;
32 import org.jetbrains.annotations.NotNull;
33
34 import javax.swing.*;
35 import javax.swing.event.HyperlinkEvent;
36 import java.beans.PropertyChangeEvent;
37 import java.beans.PropertyChangeListener;
38 import java.util.*;
39
40 import static com.intellij.diff.util.DiffUtil.isUserDataFlagSet;
41
42 public final class TextDiffViewerUtil {
43   private static final Logger LOG = Logger.getInstance(TextDiffViewerUtil.class);
44
45   @NotNull
46   public static List<AnAction> createEditorPopupActions() {
47     List<AnAction> result = new ArrayList<>();
48     result.add(ActionManager.getInstance().getAction("CompareClipboardWithSelection"));
49
50     result.add(Separator.getInstance());
51     ContainerUtil.addAll(result, ((ActionGroup)ActionManager.getInstance().getAction(IdeActions.GROUP_DIFF_EDITOR_POPUP)).getChildren(null));
52
53     return result;
54   }
55
56   @NotNull
57   public static FoldingModelSupport.Settings getFoldingModelSettings(@NotNull DiffContext context) {
58     TextDiffSettings settings = getTextSettings(context);
59     return new FoldingModelSupport.Settings(settings.getContextRange(), settings.isExpandByDefault());
60   }
61
62   @NotNull
63   public static TextDiffSettings getTextSettings(@NotNull DiffContext context) {
64     TextDiffSettings settings = context.getUserData(TextDiffSettings.KEY);
65     if (settings == null) {
66       settings = TextDiffSettings.getSettings(context.getUserData(DiffUserDataKeys.PLACE));
67       context.putUserData(TextDiffSettings.KEY, settings);
68       if (isUserDataFlagSet(DiffUserDataKeys.DO_NOT_IGNORE_WHITESPACES, context)) {
69         settings.setIgnorePolicy(IgnorePolicy.DEFAULT);
70       }
71     }
72     return settings;
73   }
74
75   public static boolean @NotNull [] checkForceReadOnly(@NotNull DiffContext context, @NotNull ContentDiffRequest request) {
76     List<DiffContent> contents = request.getContents();
77     int contentCount = contents.size();
78     boolean[] result = new boolean[contentCount];
79
80     boolean[] data = request.getUserData(DiffUserDataKeys.FORCE_READ_ONLY_CONTENTS);
81     if (data != null && data.length != contentCount) {
82       LOG.warn("Invalid FORCE_READ_ONLY_CONTENTS key value: " + request);
83       data = null;
84     }
85
86     for (int i = 0; i < contents.size(); i++) {
87       if (isUserDataFlagSet(DiffUserDataKeys.FORCE_READ_ONLY, contents.get(i), request, context) ||
88           data != null && data[i]) {
89         result[i] = true;
90       }
91     }
92
93     return result;
94   }
95
96   public static void installDocumentListeners(@NotNull DocumentListener listener,
97                                               @NotNull List<? extends Document> documents,
98                                               @NotNull Disposable disposable) {
99     for (Document document : new HashSet<>((Collection<? extends Document>)documents)) {
100       document.addDocumentListener(listener, disposable);
101     }
102   }
103
104   public static void checkDifferentDocuments(@NotNull ContentDiffRequest request) {
105     // Actually, this should be a valid case. But it has little practical sense and will require explicit checks everywhere.
106     // Some listeners will be processed once instead of 2 times, some listeners will cause illegal document modifications.
107     List<DiffContent> contents = request.getContents();
108
109     boolean sameDocuments = false;
110     for (int i = 0; i < contents.size(); i++) {
111       for (int j = i + 1; j < contents.size(); j++) {
112         DiffContent content1 = contents.get(i);
113         DiffContent content2 = contents.get(j);
114         if (!(content1 instanceof DocumentContent)) continue;
115         if (!(content2 instanceof DocumentContent)) continue;
116         sameDocuments |= ((DocumentContent)content1).getDocument() == ((DocumentContent)content2).getDocument();
117       }
118     }
119
120     if (sameDocuments) {
121       StringBuilder message = new StringBuilder();
122       message.append("DiffRequest with same documents detected\n");
123       message.append(request.toString()).append("\n");
124       for (DiffContent content : contents) {
125         message.append(content.toString()).append("\n");
126       }
127       LOG.warn(message.toString());
128     }
129   }
130
131   public static boolean areEqualLineSeparators(@NotNull List<? extends DiffContent> contents) {
132     return areEqualDocumentContentProperties(contents, DocumentContent::getLineSeparator);
133   }
134
135   public static boolean areEqualCharsets(@NotNull List<? extends DiffContent> contents) {
136     boolean sameCharset = areEqualDocumentContentProperties(contents, DocumentContent::getCharset);
137     boolean sameBOM = areEqualDocumentContentProperties(contents, DocumentContent::hasBom);
138     return sameCharset && sameBOM;
139   }
140
141   private static <T> boolean areEqualDocumentContentProperties(@NotNull List<? extends DiffContent> contents,
142                                                                @NotNull Function<? super DocumentContent, ? extends T> propertyGetter) {
143     List<T> properties = ContainerUtil.mapNotNull(contents, (content) -> {
144       if (content instanceof EmptyContent) return null;
145       return propertyGetter.fun((DocumentContent)content);
146     });
147
148     if (properties.size() < 2) return true;
149     return new HashSet<>(properties).size() == 1;
150   }
151
152   //
153   // Actions
154   //
155
156   public static abstract class ComboBoxSettingAction<T> extends ComboBoxAction implements DumbAware {
157     private DefaultActionGroup myActions;
158
159     @Override
160     public void update(@NotNull AnActionEvent e) {
161       Presentation presentation = e.getPresentation();
162       presentation.setText(getText(getValue()));
163     }
164
165     @NotNull
166     @Override
167     protected DefaultActionGroup createPopupActionGroup(JComponent button) {
168       return getActions();
169     }
170
171     @NotNull
172     public DefaultActionGroup getActions() {
173       if (myActions == null) {
174         myActions = new DefaultActionGroup();
175         for (T setting : getAvailableOptions()) {
176           myActions.add(new MyAction(setting));
177         }
178       }
179       return myActions;
180     }
181
182     @NotNull
183     protected abstract List<T> getAvailableOptions();
184
185     @NotNull
186     protected abstract T getValue();
187
188     protected abstract void setValue(@NotNull T option);
189
190     @NotNull
191     protected abstract String getText(@NotNull T option);
192
193     private class MyAction extends AnAction implements DumbAware {
194       @NotNull private final T myOption;
195
196       MyAction(@NotNull T option) {
197         super(getText(option));
198         myOption = option;
199       }
200
201       @Override
202       public void actionPerformed(@NotNull AnActionEvent e) {
203         setValue(myOption);
204       }
205     }
206   }
207
208   private static abstract class EnumPolicySettingAction<T extends Enum> extends TextDiffViewerUtil.ComboBoxSettingAction<T> {
209     private final T @NotNull [] myPolicies;
210
211     EnumPolicySettingAction(T @NotNull [] policies) {
212       assert policies.length > 0;
213       myPolicies = policies;
214     }
215
216     @Override
217     public void update(@NotNull AnActionEvent e) {
218       super.update(e);
219       e.getPresentation().setEnabledAndVisible(myPolicies.length > 1);
220     }
221
222     @NotNull
223     @Override
224     protected List<T> getAvailableOptions() {
225       //noinspection unchecked
226       return ContainerUtil.sorted(Arrays.asList(myPolicies));
227     }
228
229     @NotNull
230     @Override
231     public T getValue() {
232       T value = getStoredValue();
233       if (ArrayUtil.contains(value, myPolicies)) return value;
234
235       List<T> substitutes = getValueSubstitutes(value);
236       for (T substitute : substitutes) {
237         if (ArrayUtil.contains(substitute, myPolicies)) return substitute;
238       }
239
240       return myPolicies[0];
241     }
242
243     @NotNull
244     protected abstract T getStoredValue();
245
246     @NotNull
247     protected abstract List<T> getValueSubstitutes(@NotNull T value);
248   }
249
250   public static class HighlightPolicySettingAction extends EnumPolicySettingAction<HighlightPolicy> {
251     @NotNull protected final TextDiffSettings mySettings;
252
253     public HighlightPolicySettingAction(@NotNull TextDiffSettings settings,
254                                         HighlightPolicy @NotNull ... policies) {
255       super(policies);
256       mySettings = settings;
257     }
258
259     @Override
260     protected void setValue(@NotNull HighlightPolicy option) {
261       if (getValue() == option) return;
262       DiffUsageTriggerCollector.trigger("toggle.highlight.policy", option, mySettings.getPlace());
263       mySettings.setHighlightPolicy(option);
264     }
265
266     @NotNull
267     @Override
268     protected HighlightPolicy getStoredValue() {
269       return mySettings.getHighlightPolicy();
270     }
271
272     @NotNull
273     @Override
274     protected List<HighlightPolicy> getValueSubstitutes(@NotNull HighlightPolicy value) {
275       if (value == HighlightPolicy.BY_WORD_SPLIT) {
276         return Collections.singletonList(HighlightPolicy.BY_WORD);
277       }
278       if (value == HighlightPolicy.DO_NOT_HIGHLIGHT) {
279         return Collections.singletonList(HighlightPolicy.BY_LINE);
280       }
281       return Collections.singletonList(HighlightPolicy.BY_WORD);
282     }
283
284     @NotNull
285     @Override
286     protected String getText(@NotNull HighlightPolicy option) {
287       return option.getText();
288     }
289   }
290
291   public static class IgnorePolicySettingAction extends EnumPolicySettingAction<IgnorePolicy> {
292     @NotNull protected final TextDiffSettings mySettings;
293
294     public IgnorePolicySettingAction(@NotNull TextDiffSettings settings,
295                                      IgnorePolicy @NotNull ... policies) {
296       super(policies);
297       mySettings = settings;
298     }
299
300     @Override
301     protected void setValue(@NotNull IgnorePolicy option) {
302       if (getValue() == option) return;
303       DiffUsageTriggerCollector.trigger("toggle.ignore.policy", option, mySettings.getPlace());
304       mySettings.setIgnorePolicy(option);
305     }
306
307     @NotNull
308     @Override
309     protected IgnorePolicy getStoredValue() {
310       return mySettings.getIgnorePolicy();
311     }
312
313     @NotNull
314     @Override
315     protected List<IgnorePolicy> getValueSubstitutes(@NotNull IgnorePolicy value) {
316       if (value == IgnorePolicy.IGNORE_WHITESPACES_CHUNKS) {
317         return Collections.singletonList(IgnorePolicy.IGNORE_WHITESPACES);
318       }
319       if (value == IgnorePolicy.FORMATTING) {
320         return Collections.singletonList(IgnorePolicy.TRIM_WHITESPACES);
321       }
322       return Collections.singletonList(IgnorePolicy.DEFAULT);
323     }
324
325     @NotNull
326     @Override
327     protected String getText(@NotNull IgnorePolicy option) {
328       return option.getText();
329     }
330   }
331
332   public static class ToggleAutoScrollAction extends ToggleActionButton implements DumbAware {
333     @NotNull protected final TextDiffSettings mySettings;
334
335     public ToggleAutoScrollAction(@NotNull TextDiffSettings settings) {
336       super(DiffBundle.message("synchronize.scrolling"), AllIcons.Actions.SynchronizeScrolling);
337       mySettings = settings;
338     }
339
340     @Override
341     public boolean isSelected(AnActionEvent e) {
342       return mySettings.isEnableSyncScroll();
343     }
344
345     @Override
346     public void setSelected(AnActionEvent e, boolean state) {
347       mySettings.setEnableSyncScroll(state);
348     }
349   }
350
351   public static abstract class ToggleExpandByDefaultAction extends ToggleActionButton implements DumbAware {
352     @NotNull protected final TextDiffSettings mySettings;
353
354     public ToggleExpandByDefaultAction(@NotNull TextDiffSettings settings) {
355       super(DiffBundle.message("collapse.unchanged.fragments"), AllIcons.Actions.Collapseall);
356       mySettings = settings;
357     }
358
359     @Override
360     public boolean isVisible() {
361       return mySettings.getContextRange() != -1;
362     }
363
364     @Override
365     public boolean isSelected(AnActionEvent e) {
366       return !mySettings.isExpandByDefault();
367     }
368
369     @Override
370     public void setSelected(AnActionEvent e, boolean state) {
371       boolean expand = !state;
372       if (mySettings.isExpandByDefault() == expand) return;
373       mySettings.setExpandByDefault(expand);
374       expandAll(expand);
375     }
376
377     protected abstract void expandAll(boolean expand);
378   }
379
380   public static abstract class ReadOnlyLockAction extends ToggleAction implements DumbAware {
381     @NotNull protected final DiffContext myContext;
382     @NotNull protected final TextDiffSettings mySettings;
383
384     public ReadOnlyLockAction(@NotNull DiffContext context) {
385       super(DiffBundle.message("disable.editing"), null, AllIcons.Diff.Lock);
386       myContext = context;
387       mySettings = getTextSettings(context);
388     }
389
390     protected void applyDefaults() {
391       if (isVisible()) { // apply default state
392         setSelected(isSelected());
393       }
394     }
395
396     @Override
397     public void update(@NotNull AnActionEvent e) {
398       if (!isVisible()) {
399         e.getPresentation().setEnabledAndVisible(false);
400       }
401       else {
402         super.update(e);
403       }
404     }
405
406     @Override
407     public boolean isSelected(@NotNull AnActionEvent e) {
408       return isSelected();
409     }
410
411     boolean isSelected() {
412       return mySettings.isReadOnlyLock();
413     }
414
415     @Override
416     public void setSelected(@NotNull AnActionEvent e, boolean state) {
417       setSelected(state);
418     }
419
420     void setSelected(boolean state) {
421       mySettings.setReadOnlyLock(state);
422       doApply(state);
423     }
424
425     private boolean isVisible() {
426       return myContext.getUserData(DiffUserDataKeysEx.SHOW_READ_ONLY_LOCK) == Boolean.TRUE && canEdit();
427     }
428
429     protected abstract void doApply(boolean readOnly);
430
431     protected abstract boolean canEdit();
432
433     protected void putEditorHint(@NotNull EditorEx editor, boolean readOnly) {
434       if (readOnly) {
435         EditorModificationUtil.setReadOnlyHint(editor, DiffBundle.message("editing.viewer.hint.enable.editing.text"),
436                                                (e) -> {
437                                                  if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
438                                                    setSelected(false);
439                                                  }
440                                                });
441       }
442       else {
443         EditorModificationUtil.setReadOnlyHint(editor, null);
444       }
445     }
446   }
447
448   public static class EditorReadOnlyLockAction extends ReadOnlyLockAction {
449     private final List<? extends EditorEx> myEditableEditors;
450
451     public EditorReadOnlyLockAction(@NotNull DiffContext context, @NotNull List<? extends EditorEx> editableEditors) {
452       super(context);
453       myEditableEditors = editableEditors;
454       applyDefaults();
455     }
456
457     @Override
458     protected void doApply(boolean readOnly) {
459       for (EditorEx editor : myEditableEditors) {
460         editor.setViewer(readOnly);
461         putEditorHint(editor, readOnly);
462       }
463     }
464
465     @Override
466     protected boolean canEdit() {
467       return !myEditableEditors.isEmpty();
468     }
469   }
470
471   @NotNull
472   public static List<? extends EditorEx> getEditableEditors(@NotNull List<? extends EditorEx> editors) {
473     return ContainerUtil.filter(editors, editor -> !editor.isViewer());
474   }
475
476   public static class EditorFontSizeSynchronizer implements PropertyChangeListener {
477     @NotNull private final List<? extends EditorEx> myEditors;
478
479     private boolean myDuringUpdate = false;
480
481     public EditorFontSizeSynchronizer(@NotNull List<? extends EditorEx> editors) {
482       myEditors = editors;
483     }
484
485     public void install(@NotNull Disposable disposable) {
486       if (myEditors.size() < 2) return;
487       for (EditorEx editor : myEditors) {
488         editor.addPropertyChangeListener(this, disposable);
489       }
490     }
491
492     @Override
493     public void propertyChange(PropertyChangeEvent evt) {
494       if (myDuringUpdate) return;
495
496       if (!EditorEx.PROP_FONT_SIZE.equals(evt.getPropertyName())) return;
497       if (evt.getOldValue().equals(evt.getNewValue())) return;
498       int fontSize = ((Integer)evt.getNewValue()).intValue();
499
500       for (EditorEx editor : myEditors) {
501         if (evt.getSource() != editor) updateEditor(editor, fontSize);
502       }
503     }
504
505     public void updateEditor(@NotNull EditorEx editor, int fontSize) {
506       try {
507         myDuringUpdate = true;
508         editor.setFontSize(fontSize);
509       }
510       finally {
511         myDuringUpdate = false;
512       }
513     }
514   }
515
516   public static class EditorActionsPopup {
517     @NotNull private final List<? extends AnAction> myEditorPopupActions;
518
519     public EditorActionsPopup(@NotNull List<? extends AnAction> editorPopupActions) {
520       myEditorPopupActions = editorPopupActions;
521     }
522
523     public void install(@NotNull List<? extends EditorEx> editors, @NotNull JComponent component) {
524       ActionUtil.recursiveRegisterShortcutSet(new DefaultActionGroup(myEditorPopupActions), component, null);
525
526       EditorPopupHandler handler = new ContextMenuPopupHandler.Simple(
527         myEditorPopupActions.isEmpty() ? null : new DefaultActionGroup(myEditorPopupActions)
528       );
529       for (EditorEx editor : editors) {
530         editor.installPopupHandler(handler);
531       }
532     }
533   }
534 }