Skip to content

Commit 8d25e19

Browse files
Add Unified Diff Display in Text Editor as Alternative to Classic 2-Way
Compare This contribution introduces a new Unified Diff viewing mode for org.eclipse.compare that displays differences in a single editor pane, similar to git diff or GitHub's pull request view. When enabled, it replaces the traditional side-by-side 2-way compare editor for read-only comparisons, providing a more compact and familiar diff experience.
1 parent 3b9b910 commit 8d25e19

22 files changed

Lines changed: 3567 additions & 10 deletions

team/bundles/org.eclipse.compare/META-INF/MANIFEST.MF

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ Bundle-Vendor: %providerName
88
Bundle-Localization: plugin
99
Export-Package: org.eclipse.compare,
1010
org.eclipse.compare.contentmergeviewer,
11-
org.eclipse.compare.internal;x-friends:="org.eclipse.team.ui, org.eclipse.team.tests.core",
11+
org.eclipse.compare.internal;x-friends:="org.eclipse.team.ui,org.eclipse.team.tests.core",
1212
org.eclipse.compare.internal.merge;x-internal:=true,
1313
org.eclipse.compare.internal.patch;x-friends:="org.eclipse.team.ui",
1414
org.eclipse.compare.patch,
15-
org.eclipse.compare.structuremergeviewer
15+
org.eclipse.compare.structuremergeviewer,
16+
org.eclipse.compare.unifieddiff;x-internal:=true
1617
Require-Bundle: org.eclipse.ui;bundle-version="[3.206.0,4.0.0)",
1718
org.eclipse.core.resources;bundle-version="[3.4.0,4.0.0)",
1819
org.eclipse.jface.text;bundle-version="[3.8.0,4.0.0)",

team/bundles/org.eclipse.compare/build.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ bin.includes = icons/,\
1818
.,\
1919
plugin.properties,\
2020
about.html,\
21-
META-INF/
21+
META-INF/,\
22+
resources/
2223
src.includes = about.html,\
2324
schema/

team/bundles/org.eclipse.compare/compare/org/eclipse/compare/contentmergeviewer/TextMergeViewer.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2131,9 +2131,15 @@ protected void handleDispose(DisposeEvent event) {
21312131
fBirdsEyeCanvas= null;
21322132
fSummaryHeader= null;
21332133

2134-
fAncestorContributor.unsetDocument(fAncestor);
2135-
fLeftContributor.unsetDocument(fLeft);
2136-
fRightContributor.unsetDocument(fRight);
2134+
if (fAncestorContributor != null) {
2135+
fAncestorContributor.unsetDocument(fAncestor);
2136+
}
2137+
if (fLeftContributor != null) {
2138+
fLeftContributor.unsetDocument(fLeft);
2139+
}
2140+
if (fRightContributor != null) {
2141+
fRightContributor.unsetDocument(fRight);
2142+
}
21372143

21382144
disconnect(fLeftContributor);
21392145
disconnect(fRightContributor);
@@ -5638,6 +5644,12 @@ public int getChangesCount() {
56385644
}
56395645
};
56405646
}
5647+
if (adapter == IDocumentMergerInput.class) {
5648+
if (fMerger == null) {
5649+
return null;
5650+
}
5651+
return (T) fMerger.getInput();
5652+
}
56415653
if (adapter == OutlineViewerCreator.class) {
56425654
if (fOutlineViewerCreator == null) {
56435655
fOutlineViewerCreator = new InternalOutlineViewerCreator();

team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/ComparePreferencePage.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ private String loadPreviewContentFromFile(String key) {
123123
public static final String ADDED_LINES_REGEX= PREFIX + "AddedLinesRegex"; //$NON-NLS-1$
124124
public static final String REMOVED_LINES_REGEX= PREFIX + "RemovedLinesRegex"; //$NON-NLS-1$
125125
public static final String SWAPPED = PREFIX + "Swapped"; //$NON-NLS-1$
126+
public static final String UNIFIED_DIFF = PREFIX + "UnitifedDiff"; //$NON-NLS-1$
126127

127128

128129
private IPropertyChangeListener fPreferenceChangeListener;
@@ -154,6 +155,7 @@ private String loadPreviewContentFromFile(String key) {
154155
new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.STRING, ICompareUIConstants.PREF_NAVIGATION_END_ACTION),
155156
new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.STRING, ICompareUIConstants.PREF_NAVIGATION_END_ACTION_LOCAL),
156157
new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, SWAPPED),
158+
new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, UNIFIED_DIFF),
157159
};
158160
private final List<FieldEditor> editors = new ArrayList<>();
159161
private CTabItem fTextCompareTab;
@@ -177,6 +179,7 @@ public static void initDefaults(IPreferenceStore store) {
177179
store.setDefault(ICompareUIConstants.PREF_NAVIGATION_END_ACTION, ICompareUIConstants.PREF_VALUE_PROMPT);
178180
store.setDefault(ICompareUIConstants.PREF_NAVIGATION_END_ACTION_LOCAL, ICompareUIConstants.PREF_VALUE_LOOP);
179181
store.setDefault(SWAPPED, true);
182+
store.setDefault(UNIFIED_DIFF, false);
180183
}
181184

182185
public ComparePreferencePage() {
@@ -286,6 +289,7 @@ private Control createGeneralPage(Composite parent) {
286289
addCheckBox(composite, "ComparePreferencePage.structureCompare.label", OPEN_STRUCTURE_COMPARE, 0); //$NON-NLS-1$
287290
addCheckBox(composite, "ComparePreferencePage.structureOutline.label", USE_OUTLINE_VIEW, 0); //$NON-NLS-1$
288291
addCheckBox(composite, "ComparePreferencePage.ignoreWhitespace.label", IGNORE_WHITESPACE, 0); //$NON-NLS-1$
292+
addCheckBox(composite, "ComparePreferencePage.unifiedDiff.label", UNIFIED_DIFF, 0); //$NON-NLS-1$
289293

290294
// a spacer
291295
new Label(composite, SWT.NONE);

team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/CompareUIPlugin.java

Lines changed: 211 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
import java.io.InputStream;
2222
import java.lang.reflect.InvocationTargetException;
2323
import java.net.URL;
24+
import java.nio.charset.StandardCharsets;
2425
import java.util.ArrayList;
26+
import java.util.Arrays;
2527
import java.util.Collection;
2628
import java.util.Collections;
2729
import java.util.Comparator;
@@ -43,13 +45,19 @@
4345
import org.eclipse.compare.CompareConfiguration;
4446
import org.eclipse.compare.CompareEditorInput;
4547
import org.eclipse.compare.IResourceProvider;
48+
import org.eclipse.compare.ISharedDocumentAdapter;
4649
import org.eclipse.compare.IStreamContentAccessor;
4750
import org.eclipse.compare.IStreamMerger;
4851
import org.eclipse.compare.ITypedElement;
4952
import org.eclipse.compare.internal.core.CompareSettings;
53+
import org.eclipse.compare.internal.merge.DocumentMerger.IDocumentMergerInput;
5054
import org.eclipse.compare.structuremergeviewer.ICompareInput;
5155
import org.eclipse.compare.structuremergeviewer.IStructureCreator;
56+
import org.eclipse.compare.structuremergeviewer.SharedDocumentAdapterWrapper;
5257
import org.eclipse.compare.structuremergeviewer.StructureDiffViewer;
58+
import org.eclipse.compare.unifieddiff.UnifiedDiff;
59+
import org.eclipse.compare.unifieddiff.UnifiedDiffMode;
60+
import org.eclipse.compare.unifieddiff.internal.CancelAllRunnable;
5361
import org.eclipse.core.resources.IFile;
5462
import org.eclipse.core.resources.IResource;
5563
import org.eclipse.core.runtime.Adapters;
@@ -62,12 +70,14 @@
6270
import org.eclipse.core.runtime.IPath;
6371
import org.eclipse.core.runtime.IProgressMonitor;
6472
import org.eclipse.core.runtime.IStatus;
73+
import org.eclipse.core.runtime.NullProgressMonitor;
6574
import org.eclipse.core.runtime.OperationCanceledException;
6675
import org.eclipse.core.runtime.Platform;
6776
import org.eclipse.core.runtime.Status;
6877
import org.eclipse.core.runtime.content.IContentDescription;
6978
import org.eclipse.core.runtime.content.IContentType;
7079
import org.eclipse.core.runtime.content.IContentTypeManager;
80+
import org.eclipse.jface.action.Action;
7181
import org.eclipse.jface.dialogs.MessageDialog;
7282
import org.eclipse.jface.operation.IRunnableContext;
7383
import org.eclipse.jface.preference.IPreferenceStore;
@@ -84,15 +94,19 @@
8494
import org.eclipse.ui.IEditorInput;
8595
import org.eclipse.ui.IEditorPart;
8696
import org.eclipse.ui.IEditorRegistry;
97+
import org.eclipse.ui.IFileEditorInput;
8798
import org.eclipse.ui.IReusableEditor;
8899
import org.eclipse.ui.ISharedImages;
89100
import org.eclipse.ui.IWorkbench;
90101
import org.eclipse.ui.IWorkbenchPage;
91102
import org.eclipse.ui.IWorkbenchWindow;
92103
import org.eclipse.ui.PartInitException;
93104
import org.eclipse.ui.PlatformUI;
105+
import org.eclipse.ui.ide.IDE;
94106
import org.eclipse.ui.model.IWorkbenchAdapter;
107+
import org.eclipse.ui.part.MultiPageEditorPart;
95108
import org.eclipse.ui.plugin.AbstractUIPlugin;
109+
import org.eclipse.ui.texteditor.ITextEditor;
96110
import org.osgi.framework.BundleContext;
97111
import org.osgi.framework.ServiceRegistration;
98112

@@ -560,6 +574,10 @@ public void openCompareEditor(final CompareEditorInput input,
560574
CompareConfiguration configuration = input.getCompareConfiguration();
561575
if (configuration != null) {
562576
IPreferenceStore ps= configuration.getPreferenceStore();
577+
boolean unifiedDiffEnabled = ps.getBoolean(ComparePreferencePage.UNIFIED_DIFF);
578+
if (unifiedDiffEnabled && openUnifiedDiffInEditor(input, page, editor, activate)) {
579+
return;
580+
}
563581
if (ps != null) {
564582
configuration.setProperty(
565583
CompareConfiguration.USE_OUTLINE_VIEW,
@@ -575,9 +593,199 @@ public void openCompareEditor(final CompareEditorInput input,
575593
}
576594
}
577595

578-
private void openEditorInBackground(final CompareEditorInput input,
579-
final IWorkbenchPage page, final IReusableEditor editor,
580-
final boolean activate) {
596+
private boolean openUnifiedDiffInEditor(final CompareEditorInput input, final IWorkbenchPage page,
597+
IReusableEditor editor, boolean activate) {
598+
var unifiedDiffInput = canShowInUnifiedDiff(input);
599+
if (unifiedDiffInput!=null) {
600+
try {
601+
IWorkbenchPage wpage = page != null ? page : getActivePage();
602+
if (unifiedDiffInput.left instanceof IFileEditorInput) {
603+
IEditorPart leftEditor = wpage.openEditor(unifiedDiffInput.left,
604+
getEditorId(unifiedDiffInput.left, unifiedDiffInput.leftElement));
605+
if (leftEditor instanceof MultiPageEditorPart mpe) {
606+
Object p = mpe.getSelectedPage();
607+
if (p instanceof IEditorPart) {
608+
leftEditor = (IEditorPart) p;
609+
}
610+
}
611+
if (leftEditor instanceof ITextEditor leftTextEditor) {
612+
Action openTwoWayCompare = createOpenTwoWayCompareAction(input, page, editor, activate,
613+
leftTextEditor);
614+
IStatus status = UnifiedDiff
615+
.create(leftTextEditor, getSourceOf(unifiedDiffInput.rightAcessor()),
616+
UnifiedDiffMode.REVERT_MODE)
617+
.additionalActions(Arrays.asList(openTwoWayCompare))
618+
.ignoreWhitespaceContributorFactory(t -> unifiedDiffInput.documentMergerInput != null
619+
? unifiedDiffInput.documentMergerInput.createIgnoreWhitespaceContributor(t)
620+
: Optional.empty())
621+
.tokenComparatorFactory(t -> unifiedDiffInput.documentMergerInput != null
622+
? unifiedDiffInput.documentMergerInput.createTokenComparator(t)
623+
: null)
624+
.ignoreWhiteSpace(Utilities.getBoolean(input.getCompareConfiguration(),
625+
CompareConfiguration.IGNORE_WHITESPACE, false))
626+
.open();
627+
return status.isOK();
628+
}
629+
} else {
630+
IEditorPart rightEditor = wpage.openEditor(unifiedDiffInput.right,
631+
getEditorId(unifiedDiffInput.right, unifiedDiffInput.rightElement));
632+
if (rightEditor instanceof MultiPageEditorPart mpe) {
633+
Object p = mpe.getSelectedPage();
634+
if (p instanceof IEditorPart) {
635+
rightEditor = (IEditorPart) p;
636+
}
637+
}
638+
if (rightEditor instanceof ITextEditor rightTextEditor) {
639+
Action openTwoWayCompare = createOpenTwoWayCompareAction(input, page, editor, activate,
640+
rightTextEditor);
641+
IStatus status = UnifiedDiff
642+
.create(rightTextEditor, getSourceOf(unifiedDiffInput.leftAcessor()),
643+
UnifiedDiffMode.OVERLAY_READ_ONLY_MODE)
644+
.additionalActions(Arrays.asList(openTwoWayCompare))
645+
.ignoreWhitespaceContributorFactory(t -> unifiedDiffInput.documentMergerInput != null
646+
? unifiedDiffInput.documentMergerInput.createIgnoreWhitespaceContributor(t)
647+
: Optional.empty())
648+
.tokenComparatorFactory(t -> unifiedDiffInput.documentMergerInput != null
649+
? unifiedDiffInput.documentMergerInput.createTokenComparator(t)
650+
: null)
651+
.ignoreWhiteSpace(Utilities.getBoolean(input.getCompareConfiguration(),
652+
CompareConfiguration.IGNORE_WHITESPACE, false))
653+
.open();
654+
return status.isOK();
655+
}
656+
}
657+
} catch (PartInitException e) {
658+
CompareUIPlugin.log(e);
659+
}
660+
}
661+
return false;
662+
}
663+
664+
private Action createOpenTwoWayCompareAction(final CompareEditorInput input, final IWorkbenchPage page,
665+
IReusableEditor editor, boolean activate, ITextEditor textEditor) {
666+
ImageDescriptor image = CompareUIPlugin.getImageDescriptor(ICompareUIConstants.TWO_WAY_COMPARE);
667+
Action openTwoWayCompare = new Action(null, image) {
668+
@Override
669+
public void run() {
670+
new CancelAllRunnable(textEditor).run();
671+
if (input.canRunAsJob()) {
672+
openEditorInBackground(input, page, editor, activate);
673+
} else {
674+
if (compareResultOK(input, null)) {
675+
internalOpenEditor(input, page, editor, activate);
676+
}
677+
}
678+
}
679+
};
680+
openTwoWayCompare.setToolTipText("Open in 2-way Compare Editor");
681+
return openTwoWayCompare;
682+
}
683+
684+
private String getSourceOf(IStreamContentAccessor right) {
685+
try {
686+
if (right == null) {
687+
return ""; //$NON-NLS-1$
688+
}
689+
String result = toString(right.getContents());
690+
return result;
691+
} catch (CoreException | IOException e) {
692+
CompareUIPlugin.log(e);
693+
}
694+
return null;
695+
}
696+
697+
private static String toString(InputStream inputStream) throws IOException {
698+
if (inputStream == null) {
699+
return ""; //$NON-NLS-1$
700+
}
701+
try (inputStream) {
702+
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
703+
}
704+
}
705+
706+
private static String getEditorId(IEditorInput editorInput, ITypedElement element) {
707+
String fileName = editorInput.getName();
708+
IEditorRegistry registry = PlatformUI.getWorkbench().getEditorRegistry();
709+
IContentType type = getContentType(element);
710+
IEditorDescriptor descriptor = registry.getDefaultEditor(fileName, type);
711+
IDE.overrideDefaultEditorAssociation(editorInput, type, descriptor);
712+
String id;
713+
if (descriptor == null || descriptor.isOpenExternal()) {
714+
id = "org.eclipse.ui.DefaultTextEditor"; //$NON-NLS-1$
715+
} else {
716+
id = descriptor.getId();
717+
}
718+
return id;
719+
}
720+
721+
private static record LeftEditorInputAndRightStreamContentAccessor(IEditorInput left, ITypedElement leftElement,
722+
IStreamContentAccessor leftAcessor, IEditorInput right, ITypedElement rightElement,
723+
IStreamContentAccessor rightAcessor, IDocumentMergerInput documentMergerInput) {
724+
}
725+
726+
private LeftEditorInputAndRightStreamContentAccessor canShowInUnifiedDiff(CompareEditorInput input) {
727+
IDocumentMergerInput documentMergerInput = null;
728+
try {
729+
input.run(new NullProgressMonitor());
730+
Object res = input.getCompareResult();
731+
if (!(res instanceof ICompareInput compareInput)) {
732+
return null;
733+
}
734+
ITypedElement ancestor = compareInput.getAncestor();
735+
if (ancestor != null) {
736+
return null;
737+
}
738+
// TODO (tm) do not support if one side is editable?
739+
ITypedElement left = compareInput.getLeft();
740+
if (left == null) {
741+
return null;
742+
}
743+
ISharedDocumentAdapter leftSda = SharedDocumentAdapterWrapper.getAdapter(left);
744+
if (leftSda == null) {
745+
return null;
746+
}
747+
IEditorInput leftEditorInput = leftSda.getDocumentKey(left);
748+
if (leftEditorInput == null) {
749+
return null;
750+
}
751+
if (!(left instanceof IStreamContentAccessor leftSa)) {
752+
return null;
753+
}
754+
ITypedElement right = compareInput.getRight();
755+
if (right == null) {
756+
return null;
757+
}
758+
ISharedDocumentAdapter rightSda = SharedDocumentAdapterWrapper.getAdapter(right);
759+
if (rightSda == null) {
760+
return null;
761+
}
762+
IEditorInput rightEditorInput = rightSda.getDocumentKey(right);
763+
if (rightEditorInput == null) {
764+
return null;
765+
}
766+
if (!(right instanceof IStreamContentAccessor rightSa)) {
767+
return null;
768+
}
769+
var invisibleParent = new Shell();
770+
try {
771+
invisibleParent.setVisible(false);
772+
var viewer = input.findContentViewer(new NullViewer(getShell()), compareInput, invisibleParent);
773+
if (viewer instanceof IAdaptable adaptable) {
774+
documentMergerInput = adaptable.getAdapter(IDocumentMergerInput.class);
775+
}
776+
} finally {
777+
invisibleParent.dispose();
778+
}
779+
return new LeftEditorInputAndRightStreamContentAccessor(leftEditorInput, left, leftSa, rightEditorInput,
780+
right, rightSa, documentMergerInput);
781+
} catch (InvocationTargetException | InterruptedException e) {
782+
CompareUIPlugin.log(e);
783+
}
784+
return null;
785+
}
786+
787+
private void openEditorInBackground(final CompareEditorInput input, final IWorkbenchPage page,
788+
final IReusableEditor editor, final boolean activate) {
581789
internalOpenEditor(input, page, editor, activate);
582790
}
583791

team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/ICompareUIConstants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public interface ICompareUIConstants {
2323
public static final String ETOOL_PREV = "elcl16/prev_nav.svg"; //$NON-NLS-1$
2424
public static final String CTOOL_PREV= ETOOL_PREV;
2525

26+
public static final String TWO_WAY_COMPARE = "elcl16/twowaycompare_co.svg"; //$NON-NLS-1$
27+
2628
public static final String HUNK_OBJ = "obj16/hunk_obj.svg"; //$NON-NLS-1$
2729

2830
public static final String ERROR_OVERLAY = "ovr16/error_ov.svg"; //$NON-NLS-1$

team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/merge/DocumentMerger.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1403,4 +1403,7 @@ private Diff findPrev(char contributor, List<Diff> v, int start, int end, boolea
14031403
return null;
14041404
}
14051405

1406+
public IDocumentMergerInput getInput() {
1407+
return fInput;
1408+
}
14061409
}

0 commit comments

Comments
 (0)