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;
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;
35 import javax.swing.event.HyperlinkEvent;
36 import java.beans.PropertyChangeEvent;
37 import java.beans.PropertyChangeListener;
40 import static com.intellij.diff.util.DiffUtil.isUserDataFlagSet;
42 public final class TextDiffViewerUtil {
43 private static final Logger LOG = Logger.getInstance(TextDiffViewerUtil.class);
46 public static List<AnAction> createEditorPopupActions() {
47 List<AnAction> result = new ArrayList<>();
48 result.add(ActionManager.getInstance().getAction("CompareClipboardWithSelection"));
50 result.add(Separator.getInstance());
51 ContainerUtil.addAll(result, ((ActionGroup)ActionManager.getInstance().getAction(IdeActions.GROUP_DIFF_EDITOR_POPUP)).getChildren(null));
57 public static FoldingModelSupport.Settings getFoldingModelSettings(@NotNull DiffContext context) {
58 TextDiffSettings settings = getTextSettings(context);
59 return new FoldingModelSupport.Settings(settings.getContextRange(), settings.isExpandByDefault());
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);
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];
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);
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]) {
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);
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();
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();
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");
127 LOG.warn(message.toString());
131 public static boolean areEqualLineSeparators(@NotNull List<? extends DiffContent> contents) {
132 return areEqualDocumentContentProperties(contents, DocumentContent::getLineSeparator);
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;
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);
148 if (properties.size() < 2) return true;
149 return new HashSet<>(properties).size() == 1;
156 public static abstract class ComboBoxSettingAction<T> extends ComboBoxAction implements DumbAware {
157 private DefaultActionGroup myActions;
160 public void update(@NotNull AnActionEvent e) {
161 Presentation presentation = e.getPresentation();
162 presentation.setText(getText(getValue()));
167 protected DefaultActionGroup createPopupActionGroup(JComponent button) {
172 public DefaultActionGroup getActions() {
173 if (myActions == null) {
174 myActions = new DefaultActionGroup();
175 for (T setting : getAvailableOptions()) {
176 myActions.add(new MyAction(setting));
183 protected abstract List<T> getAvailableOptions();
186 protected abstract T getValue();
188 protected abstract void setValue(@NotNull T option);
191 protected abstract String getText(@NotNull T option);
193 private class MyAction extends AnAction implements DumbAware {
194 @NotNull private final T myOption;
196 MyAction(@NotNull T option) {
197 super(getText(option));
202 public void actionPerformed(@NotNull AnActionEvent e) {
208 private static abstract class EnumPolicySettingAction<T extends Enum> extends TextDiffViewerUtil.ComboBoxSettingAction<T> {
209 private final T @NotNull [] myPolicies;
211 EnumPolicySettingAction(T @NotNull [] policies) {
212 assert policies.length > 0;
213 myPolicies = policies;
217 public void update(@NotNull AnActionEvent e) {
219 e.getPresentation().setEnabledAndVisible(myPolicies.length > 1);
224 protected List<T> getAvailableOptions() {
225 //noinspection unchecked
226 return ContainerUtil.sorted(Arrays.asList(myPolicies));
231 public T getValue() {
232 T value = getStoredValue();
233 if (ArrayUtil.contains(value, myPolicies)) return value;
235 List<T> substitutes = getValueSubstitutes(value);
236 for (T substitute : substitutes) {
237 if (ArrayUtil.contains(substitute, myPolicies)) return substitute;
240 return myPolicies[0];
244 protected abstract T getStoredValue();
247 protected abstract List<T> getValueSubstitutes(@NotNull T value);
250 public static class HighlightPolicySettingAction extends EnumPolicySettingAction<HighlightPolicy> {
251 @NotNull protected final TextDiffSettings mySettings;
253 public HighlightPolicySettingAction(@NotNull TextDiffSettings settings,
254 HighlightPolicy @NotNull ... policies) {
256 mySettings = settings;
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);
268 protected HighlightPolicy getStoredValue() {
269 return mySettings.getHighlightPolicy();
274 protected List<HighlightPolicy> getValueSubstitutes(@NotNull HighlightPolicy value) {
275 if (value == HighlightPolicy.BY_WORD_SPLIT) {
276 return Collections.singletonList(HighlightPolicy.BY_WORD);
278 if (value == HighlightPolicy.DO_NOT_HIGHLIGHT) {
279 return Collections.singletonList(HighlightPolicy.BY_LINE);
281 return Collections.singletonList(HighlightPolicy.BY_WORD);
286 protected String getText(@NotNull HighlightPolicy option) {
287 return option.getText();
291 public static class IgnorePolicySettingAction extends EnumPolicySettingAction<IgnorePolicy> {
292 @NotNull protected final TextDiffSettings mySettings;
294 public IgnorePolicySettingAction(@NotNull TextDiffSettings settings,
295 IgnorePolicy @NotNull ... policies) {
297 mySettings = settings;
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);
309 protected IgnorePolicy getStoredValue() {
310 return mySettings.getIgnorePolicy();
315 protected List<IgnorePolicy> getValueSubstitutes(@NotNull IgnorePolicy value) {
316 if (value == IgnorePolicy.IGNORE_WHITESPACES_CHUNKS) {
317 return Collections.singletonList(IgnorePolicy.IGNORE_WHITESPACES);
319 if (value == IgnorePolicy.FORMATTING) {
320 return Collections.singletonList(IgnorePolicy.TRIM_WHITESPACES);
322 return Collections.singletonList(IgnorePolicy.DEFAULT);
327 protected String getText(@NotNull IgnorePolicy option) {
328 return option.getText();
332 public static class ToggleAutoScrollAction extends ToggleActionButton implements DumbAware {
333 @NotNull protected final TextDiffSettings mySettings;
335 public ToggleAutoScrollAction(@NotNull TextDiffSettings settings) {
336 super(DiffBundle.message("synchronize.scrolling"), AllIcons.Actions.SynchronizeScrolling);
337 mySettings = settings;
341 public boolean isSelected(AnActionEvent e) {
342 return mySettings.isEnableSyncScroll();
346 public void setSelected(AnActionEvent e, boolean state) {
347 mySettings.setEnableSyncScroll(state);
351 public static abstract class ToggleExpandByDefaultAction extends ToggleActionButton implements DumbAware {
352 @NotNull protected final TextDiffSettings mySettings;
354 public ToggleExpandByDefaultAction(@NotNull TextDiffSettings settings) {
355 super(DiffBundle.message("collapse.unchanged.fragments"), AllIcons.Actions.Collapseall);
356 mySettings = settings;
360 public boolean isVisible() {
361 return mySettings.getContextRange() != -1;
365 public boolean isSelected(AnActionEvent e) {
366 return !mySettings.isExpandByDefault();
370 public void setSelected(AnActionEvent e, boolean state) {
371 boolean expand = !state;
372 if (mySettings.isExpandByDefault() == expand) return;
373 mySettings.setExpandByDefault(expand);
377 protected abstract void expandAll(boolean expand);
380 public static abstract class ReadOnlyLockAction extends ToggleAction implements DumbAware {
381 @NotNull protected final DiffContext myContext;
382 @NotNull protected final TextDiffSettings mySettings;
384 public ReadOnlyLockAction(@NotNull DiffContext context) {
385 super(DiffBundle.message("disable.editing"), null, AllIcons.Diff.Lock);
387 mySettings = getTextSettings(context);
390 protected void applyDefaults() {
391 if (isVisible()) { // apply default state
392 setSelected(isSelected());
397 public void update(@NotNull AnActionEvent e) {
399 e.getPresentation().setEnabledAndVisible(false);
407 public boolean isSelected(@NotNull AnActionEvent e) {
411 boolean isSelected() {
412 return mySettings.isReadOnlyLock();
416 public void setSelected(@NotNull AnActionEvent e, boolean state) {
420 void setSelected(boolean state) {
421 mySettings.setReadOnlyLock(state);
425 private boolean isVisible() {
426 return myContext.getUserData(DiffUserDataKeysEx.SHOW_READ_ONLY_LOCK) == Boolean.TRUE && canEdit();
429 protected abstract void doApply(boolean readOnly);
431 protected abstract boolean canEdit();
433 protected void putEditorHint(@NotNull EditorEx editor, boolean readOnly) {
435 EditorModificationUtil.setReadOnlyHint(editor, DiffBundle.message("editing.viewer.hint.enable.editing.text"),
437 if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
443 EditorModificationUtil.setReadOnlyHint(editor, null);
448 public static class EditorReadOnlyLockAction extends ReadOnlyLockAction {
449 private final List<? extends EditorEx> myEditableEditors;
451 public EditorReadOnlyLockAction(@NotNull DiffContext context, @NotNull List<? extends EditorEx> editableEditors) {
453 myEditableEditors = editableEditors;
458 protected void doApply(boolean readOnly) {
459 for (EditorEx editor : myEditableEditors) {
460 editor.setViewer(readOnly);
461 putEditorHint(editor, readOnly);
466 protected boolean canEdit() {
467 return !myEditableEditors.isEmpty();
472 public static List<? extends EditorEx> getEditableEditors(@NotNull List<? extends EditorEx> editors) {
473 return ContainerUtil.filter(editors, editor -> !editor.isViewer());
476 public static class EditorFontSizeSynchronizer implements PropertyChangeListener {
477 @NotNull private final List<? extends EditorEx> myEditors;
479 private boolean myDuringUpdate = false;
481 public EditorFontSizeSynchronizer(@NotNull List<? extends EditorEx> editors) {
485 public void install(@NotNull Disposable disposable) {
486 if (myEditors.size() < 2) return;
487 for (EditorEx editor : myEditors) {
488 editor.addPropertyChangeListener(this, disposable);
493 public void propertyChange(PropertyChangeEvent evt) {
494 if (myDuringUpdate) return;
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();
500 for (EditorEx editor : myEditors) {
501 if (evt.getSource() != editor) updateEditor(editor, fontSize);
505 public void updateEditor(@NotNull EditorEx editor, int fontSize) {
507 myDuringUpdate = true;
508 editor.setFontSize(fontSize);
511 myDuringUpdate = false;
516 public static class EditorActionsPopup {
517 @NotNull private final List<? extends AnAction> myEditorPopupActions;
519 public EditorActionsPopup(@NotNull List<? extends AnAction> editorPopupActions) {
520 myEditorPopupActions = editorPopupActions;
523 public void install(@NotNull List<? extends EditorEx> editors, @NotNull JComponent component) {
524 ActionUtil.recursiveRegisterShortcutSet(new DefaultActionGroup(myEditorPopupActions), component, null);
526 EditorPopupHandler handler = new ContextMenuPopupHandler.Simple(
527 myEditorPopupActions.isEmpty() ? null : new DefaultActionGroup(myEditorPopupActions)
529 for (EditorEx editor : editors) {
530 editor.installPopupHandler(handler);