From 006f65743ea9c3aa3e8f79219e595c4498e459f2 Mon Sep 17 00:00:00 2001 From: sebthom Date: Sun, 26 Oct 2025 22:18:16 +0100 Subject: [PATCH 1/5] Allow reuse of last opened editor via Project Explorer --- .../ui/actions/OpenFileWithReuseAction.java | 193 ++++++++++++++++++ .../resources/actions/OpenActionProvider.java | 3 +- .../ui/internal/IPreferenceConstants.java | 6 + .../ui/internal/WorkbenchMessages.java | 1 + .../WorkbenchPreferenceInitializer.java | 1 + .../dialogs/WorkbenchPreferencePage.java | 14 ++ .../eclipse/ui/internal/messages.properties | 3 +- .../tests/navigator/NavigatorTestSuite.java | 5 +- .../OpenFileWithReuseActionTest.java | 147 +++++++++++++ 9 files changed, 368 insertions(+), 5 deletions(-) create mode 100644 bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenFileWithReuseAction.java create mode 100644 tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/OpenFileWithReuseActionTest.java diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenFileWithReuseAction.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenFileWithReuseAction.java new file mode 100644 index 00000000000..4e9cb4e64e2 --- /dev/null +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenFileWithReuseAction.java @@ -0,0 +1,193 @@ +/******************************************************************************* + * Copyright (c) 2025 Vegard IT GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Sebastian Thomschke (Vegard IT GmbH) - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.actions; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IResource; +import org.eclipse.jface.util.OpenStrategy; +import org.eclipse.ui.IEditorDescriptor; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorReference; +import org.eclipse.ui.IEditorRegistry; +import org.eclipse.ui.IPageLayout; +import org.eclipse.ui.IReusableEditor; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchPartReference; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.internal.IPreferenceConstants; +import org.eclipse.ui.internal.ide.DialogUtil; +import org.eclipse.ui.internal.ide.IDEWorkbenchMessages; +import org.eclipse.ui.internal.util.PrefUtil; +import org.eclipse.ui.part.FileEditorInput; + +/** + * Open action that optionally reuses a single editor for opens originating from + * the Project Explorer, similar to the Search view behavior. When the feature + * is disabled or outside Project Explorer, behavior falls back to + * {@link OpenFileAction}. + * + * Scope is currently limited to Project Explorer via the creating part id + * passed in the constructor. Reuse stops when the editor becomes dirty, is + * pinned, closed, or when the resolved editor id differs from the currently + * reused editor id. + * + * This class is intentionally package-public in org.eclipse.ui.actions to be a + * drop-in replacement for {@link OpenFileAction} in CNF wiring. + */ +public class OpenFileWithReuseAction extends OpenFileAction { + + private final String hostPartId; + private IEditorReference reusedRef; + private boolean disableReuseForRun; + + /** + * @param page workbench page + * @param hostPartId id of the part creating this action (used to scope to + * Project Explorer) + */ + public OpenFileWithReuseAction(final IWorkbenchPage page, final String hostPartId) { + this(page, null, hostPartId); + } + + /** + * @param page workbench page + * @param descriptor editor descriptor to use (or null for default) + * @param hostPartId id of the part creating this action (used to scope to + * Project Explorer) + */ + public OpenFileWithReuseAction(final IWorkbenchPage page, final IEditorDescriptor descriptor, + final String hostPartId) { + super(page, descriptor); + this.hostPartId = hostPartId; + } + + @Override + public void run() { + // Disable reuse for multi-file selection to preserve baseline behavior + int fileCount = 0; + for (final IResource res : getSelectedResources()) { + if (res instanceof IFile) { + fileCount++; + if (fileCount > 1) { + break; + } + } + } + disableReuseForRun = fileCount > 1; + try { + super.run(); + } finally { + disableReuseForRun = false; + } + } + + @Override + void openFile(final IFile file) { + if (disableReuseForRun || !shouldApplyReuse()) { + // Delegate to baseline behavior + super.openFile(file); + return; + } + + final IWorkbenchPage page = getWorkbenchPage(); + final boolean activate = OpenStrategy.activateOnOpen(); + + // If already open, just bring to top/activate + final IEditorPart existing = page.findEditor(new FileEditorInput(file)); + if (existing != null) { + if (activate) { + page.activate(existing); + } else { + page.bringToTop(existing); + } + return; + } + + // Determine target editor id (explicit), + // falling back to external system editor id + String editorId = IEditorRegistry.SYSTEM_EXTERNAL_EDITOR_ID; + IEditorDescriptor desc = null; + try { + desc = IDE.getEditorDescriptor(file, true, true); + } catch (PartInitException e) { + // ignore here; will fall back to system external editor id + } + if (desc != null) { + editorId = desc.getId(); + } + + // Do not attempt reuse for external editors + if (IEditorRegistry.SYSTEM_EXTERNAL_EDITOR_ID.equals(editorId)) { + super.openFile(file); + return; + } + + // Try reuse if we have a valid reference + if (reusedRef != null) { + final boolean open = reusedRef.getEditor(false) != null; + final boolean valid = open && !reusedRef.isDirty() && !reusedRef.isPinned(); + if (valid) { + if (!editorId.equals(reusedRef.getId())) { + // Different editor type needed; close old reusable editor + page.closeEditors(new IEditorReference[] { reusedRef }, false); + reusedRef = null; + } else { + final IEditorPart part = reusedRef.getEditor(true); + if (part instanceof IReusableEditor reusableEditor) { + reusableEditor.setInput(new FileEditorInput(file)); + if (activate) { + page.activate(part); + } else { + page.bringToTop(part); + } + return; + } + // Not reusable after all + reusedRef = null; + } + } else { + // Reference no longer valid + reusedRef = null; + } + } + + // Open a new editor and remember it if reusable + try { + final IEditorPart opened = IDE.openEditor(page, file, editorId, activate); + if (opened instanceof IReusableEditor) { + final IWorkbenchPartReference ref = page.getReference(opened); + if (ref instanceof IEditorReference editorRef) { + reusedRef = editorRef; + } else { + reusedRef = null; + } + } else { + reusedRef = null; + } + } catch (final PartInitException ex) { + DialogUtil.openError(page.getWorkbenchWindow().getShell(), + IDEWorkbenchMessages.OpenFileAction_openFileShellTitle, ex.getMessage(), ex); + } + } + + private boolean shouldApplyReuse() { + // Strictly scope to Project Explorer and opt-in preference + if (hostPartId == null || !hostPartId.startsWith(IPageLayout.ID_PROJECT_EXPLORER)) { + return false; + } + + return PrefUtil.getInternalPreferenceStore().getBoolean(IPreferenceConstants.REUSE_LAST_OPENED_EDITOR); + } +} diff --git a/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/OpenActionProvider.java b/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/OpenActionProvider.java index eae3801edba..5c9b59daacd 100644 --- a/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/OpenActionProvider.java +++ b/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/OpenActionProvider.java @@ -26,6 +26,7 @@ import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.ui.IActionBars; import org.eclipse.ui.actions.OpenFileAction; +import org.eclipse.ui.actions.OpenFileWithReuseAction; import org.eclipse.ui.actions.OpenWithMenu; import org.eclipse.ui.internal.navigator.resources.plugin.WorkbenchNavigatorMessages; import org.eclipse.ui.navigator.CommonActionProvider; @@ -51,7 +52,7 @@ public class OpenActionProvider extends CommonActionProvider { public void init(ICommonActionExtensionSite aConfig) { if (aConfig.getViewSite() instanceof ICommonViewerWorkbenchSite) { viewSite = (ICommonViewerWorkbenchSite) aConfig.getViewSite(); - openFileAction = new OpenFileAction(viewSite.getPage()); + openFileAction = new OpenFileWithReuseAction(viewSite.getPage(), viewSite.getSite().getId()); contribute = true; } } diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/IPreferenceConstants.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/IPreferenceConstants.java index b7c23d2676d..804cbd37389 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/IPreferenceConstants.java +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/IPreferenceConstants.java @@ -327,4 +327,10 @@ public interface IPreferenceConstants { */ String SHOW_KEYS_TIME_TO_CLOSE = "showCommandKeys_timeToClose"; //$NON-NLS-1$ + /** + * Preference to enable editor reuse for opens from (currently only) Project + * Explorer. Applies to both single- and double-click. Default false. + */ + String REUSE_LAST_OPENED_EDITOR = "REUSE_LAST_OPENED_EDITOR"; //$NON-NLS-1$ + } diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbenchMessages.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbenchMessages.java index 7f1f1435b4f..224bcb7abb7 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbenchMessages.java +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbenchMessages.java @@ -545,6 +545,7 @@ public class WorkbenchMessages extends NLS { public static String WorkbenchPreference_singleClick; public static String WorkbenchPreference_singleClick_SelectOnHover; public static String WorkbenchPreference_singleClick_OpenAfterDelay; + public static String WorkbenchPreference_reuseLastOpenedEditor; public static String WorkbenchPreference_noEffectOnAllViews; public static String WorkbenchPreference_HeapStatusButton; public static String WorkbenchPreference_HeapStatusButtonToolTip; diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbenchPreferenceInitializer.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbenchPreferenceInitializer.java index 349dc1e744b..ff7770c06be 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbenchPreferenceInitializer.java +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbenchPreferenceInitializer.java @@ -59,6 +59,7 @@ public void initializeDefaultPreferences() { node.putBoolean(IPreferenceConstants.STICKY_CYCLE, false); node.putBoolean(IPreferenceConstants.REUSE_EDITORS_BOOLEAN, true); node.putInt(IPreferenceConstants.REUSE_EDITORS, 99); + node.putBoolean(IPreferenceConstants.REUSE_LAST_OPENED_EDITOR, false); node.putBoolean(IPreferenceConstants.OPEN_ON_SINGLE_CLICK, false); node.putBoolean(IPreferenceConstants.SELECT_ON_HOVER, false); node.putBoolean(IPreferenceConstants.OPEN_AFTER_DELAY, false); diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/dialogs/WorkbenchPreferencePage.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/dialogs/WorkbenchPreferencePage.java index 1c6cd5b371c..10e047b30b2 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/dialogs/WorkbenchPreferencePage.java +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/dialogs/WorkbenchPreferencePage.java @@ -80,6 +80,8 @@ public class WorkbenchPreferencePage extends PreferencePage implements IWorkbenc private Button showInlineRenameButton; + private Button reuseLastOpenedEditorButton; + protected static int MAX_SAVE_INTERVAL = 9999; protected static int MAX_VIEW_LIMIT = 1_000_000; @@ -295,6 +297,14 @@ protected void createOpenModeGroup(Composite composite) { data.horizontalIndent = LayoutConstants.getIndent(); openAfterDelayButton.setLayoutData(data); + // reuse last opened editor when opening a file (currently only in Project + // Explorer) + reuseLastOpenedEditorButton = new Button(buttonComposite, SWT.CHECK | SWT.LEFT); + reuseLastOpenedEditorButton + .setText(WorkbenchMessages.WorkbenchPreference_reuseLastOpenedEditor); + reuseLastOpenedEditorButton.setSelection( + getPreferenceStore().getBoolean(IPreferenceConstants.REUSE_LAST_OPENED_EDITOR)); + createNoteComposite(font, buttonComposite, WorkbenchMessages.Preference_note, WorkbenchMessages.WorkbenchPreference_noEffectOnAllViews); } @@ -419,6 +429,8 @@ protected void performDefaults() { openAfterDelayButton.setSelection(openAfterDelay); selectOnHoverButton.setEnabled(openOnSingleClick); openAfterDelayButton.setEnabled(openOnSingleClick); + reuseLastOpenedEditorButton + .setSelection(store.getDefaultBoolean(IPreferenceConstants.REUSE_LAST_OPENED_EDITOR)); stickyCycleButton.setSelection(store.getDefaultBoolean(IPreferenceConstants.STICKY_CYCLE)); showUserDialogButton.setSelection(store.getDefaultBoolean(IPreferenceConstants.RUN_IN_BACKGROUND)); showHeapStatusButton.setSelection( @@ -443,6 +455,8 @@ public boolean performOk() { store.setValue(IPreferenceConstants.OPEN_ON_SINGLE_CLICK, openOnSingleClick); store.setValue(IPreferenceConstants.SELECT_ON_HOVER, selectOnHover); store.setValue(IPreferenceConstants.OPEN_AFTER_DELAY, openAfterDelay); + store.setValue(IPreferenceConstants.REUSE_LAST_OPENED_EDITOR, + reuseLastOpenedEditorButton.getSelection()); store.setValue(IPreferenceConstants.RUN_IN_BACKGROUND, showUserDialogButton.getSelection()); store.setValue(IPreferenceConstants.WORKBENCH_SAVE_INTERVAL, saveInterval.getIntValue()); store.setValue(IWorkbenchPreferenceConstants.LARGE_VIEW_LIMIT, largeViewLimit.getIntValue()); diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/messages.properties b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/messages.properties index aa3e02edf00..43085cc5f9b 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/messages.properties +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/messages.properties @@ -496,7 +496,7 @@ OpenPerspectiveDialogAction_tooltip=Open Perspective #---- General Preferences---- PreferencePage_noDescription = (No description available) -PreferencePageParameterValues_pageLabelSeparator = \ >\ +PreferencePageParameterValues_pageLabelSeparator = \ >\ ThemingEnabled = E&nable theming ThemeChangeWarningText = Restart for the theme changes to take full effect ThemeChangeWarningTitle = Theme Changed @@ -511,6 +511,7 @@ WorkbenchPreference_doubleClick=D&ouble click WorkbenchPreference_singleClick=&Single click WorkbenchPreference_singleClick_SelectOnHover=Select on &hover WorkbenchPreference_singleClick_OpenAfterDelay=Open when using arrow &keys +WorkbenchPreference_reuseLastOpenedEditor=Reuse last opened editor when opening files via Project Explorer WorkbenchPreference_noEffectOnAllViews=This preference may not take effect on all views WorkbenchPreference_inlineRename=&Rename resource inline if available diff --git a/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/NavigatorTestSuite.java b/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/NavigatorTestSuite.java index a67e92eb561..0021a747363 100644 --- a/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/NavigatorTestSuite.java +++ b/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/NavigatorTestSuite.java @@ -25,9 +25,8 @@ import org.eclipse.ui.tests.navigator.resources.NestedResourcesTests; import org.eclipse.ui.tests.navigator.resources.PathComparatorTest; import org.eclipse.ui.tests.navigator.resources.ResourceMgmtActionProviderTests; - -import org.junit.platform.suite.api.Suite; import org.junit.platform.suite.api.SelectClasses; +import org.junit.platform.suite.api.Suite; @Suite @SelectClasses({ InitialActivationTest.class, ActionProviderTest.class, ExtensionsTest.class, FilterTest.class, @@ -37,7 +36,7 @@ FirstClassM1Tests.class, LinkHelperTest.class, ShowInTest.class, ResourceTransferTest.class, EvaluationCacheTest.class, ResourceMgmtActionProviderTests.class, NestedResourcesTests.class, PathComparatorTest.class, FoldersAsProjectsContributionTest.class, - GoBackForwardsTest.class + GoBackForwardsTest.class, OpenFileWithReuseActionTest.class, // DnDTest.class, // DnDTest.testSetDragOperation() fails // PerformanceTest.class // Does not pass on all platforms see bug 264449 }) diff --git a/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/OpenFileWithReuseActionTest.java b/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/OpenFileWithReuseActionTest.java new file mode 100644 index 00000000000..bdc2ac8e825 --- /dev/null +++ b/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/OpenFileWithReuseActionTest.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * Copyright (c) 2025 Vegard IT GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Sebastian Thomschke (Vegard IT GmbH) - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.tests.navigator; + +import static org.junit.Assert.*; + +import java.nio.charset.StandardCharsets; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorReference; +import org.eclipse.ui.IPageLayout; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.actions.OpenFileWithReuseAction; +import org.eclipse.ui.internal.IPreferenceConstants; +import org.eclipse.ui.part.FileEditorInput; +import org.eclipse.ui.tests.harness.util.EditorTestHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Verifies Project Explorer open behavior when the "reuse like Search" option + * is enabled/disabled. + */ +public class OpenFileWithReuseActionTest extends NavigatorTestBase { + + public OpenFileWithReuseActionTest() { + _navigatorInstanceId = org.eclipse.ui.navigator.resources.ProjectExplorer.VIEW_ID; + } + + @Before + public void before() { + EditorTestHelper.closeAllEditors(); + } + + @After + public void after() { + EditorTestHelper.closeAllEditors(); + } + + private IFile createFile(IProject project, String name, String content) throws CoreException { + IFile f = project.getFile(name); + byte[] bytes = content.getBytes(StandardCharsets.UTF_8); + if (f.exists()) { + f.setContents(bytes, true, false, null); + } else { + f.create(bytes, true, false, null); + } + return f; + } + + @Test + public void testReuseEnabled_singleSelectionsReuseSameEditor() throws Exception { + // Enable preference + IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode("org.eclipse.ui.workbench"); + prefs.putBoolean(IPreferenceConstants.REUSE_LAST_OPENED_EDITOR, true); + try { + prefs.flush(); + } catch (Exception e) { /* ignore */ } + + // Create two simple files + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + IProject project = root.getProject("p1"); + IFile a = createFile(project, "a.txt", "a"); + IFile b = createFile(project, "b.txt", "b"); + + showNavigator(); + + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + var action = new OpenFileWithReuseAction(page, IPageLayout.ID_PROJECT_EXPLORER); + + // Open first file + action.selectionChanged(new StructuredSelection(a)); + action.run(); + EditorTestHelper.runEventQueue(200); + + IEditorReference[] refs1 = page.getEditorReferences(); + assertEquals(1, refs1.length); + IEditorReference ref1 = refs1[0]; + IEditorPart part1 = ref1.getEditor(false); + assertNotNull(part1); + // Sanity: editor shows first file + assertNotNull(page.findEditor(new FileEditorInput(a))); + + // Open second file + action.selectionChanged(new StructuredSelection(b)); + action.run(); + EditorTestHelper.runEventQueue(200); + + IEditorReference[] refs2 = page.getEditorReferences(); + assertEquals("Reused editor expected", 1, refs2.length); + assertSame("Editor reference should be the same instance", ref1, refs2[0]); + // Input replaced with second file + assertNotNull(page.findEditor(new FileEditorInput(b))); + } + + @Test + public void testReuseDisabled_opensTwoEditors() throws Exception { + // Disable preference + IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode("org.eclipse.ui.workbench"); + prefs.putBoolean(IPreferenceConstants.REUSE_LAST_OPENED_EDITOR, false); + try { + prefs.flush(); + } catch (Exception e) { /* ignore */ } + + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + IProject project = root.getProject("p1"); + IFile a = createFile(project, "c.txt", "c"); + IFile b = createFile(project, "d.txt", "d"); + + showNavigator(); + + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + var action = new OpenFileWithReuseAction(page, IPageLayout.ID_PROJECT_EXPLORER); + + action.selectionChanged(new StructuredSelection(a)); + action.run(); + EditorTestHelper.runEventQueue(200); + + action.selectionChanged(new StructuredSelection(b)); + action.run(); + EditorTestHelper.runEventQueue(200); + + IEditorReference[] refs = page.getEditorReferences(); + assertEquals("Two editors expected when reuse is disabled", 2, refs.length); + } +} From 7b0069f6ad0ac8577626d958370e1c49e9181355 Mon Sep 17 00:00:00 2001 From: sebthom Date: Sun, 26 Oct 2025 22:52:28 +0100 Subject: [PATCH 2/5] Add `@since` javadoc annotation to OpenFileWithReuseAction --- .../org/eclipse/ui/actions/OpenFileWithReuseAction.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenFileWithReuseAction.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenFileWithReuseAction.java index 4e9cb4e64e2..027b400ce70 100644 --- a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenFileWithReuseAction.java +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenFileWithReuseAction.java @@ -45,6 +45,8 @@ * * This class is intentionally package-public in org.eclipse.ui.actions to be a * drop-in replacement for {@link OpenFileAction} in CNF wiring. + * + * @since 3.22 */ public class OpenFileWithReuseAction extends OpenFileAction { From 9abfd8accc932e2680963ae98ba1f6488fb567cb Mon Sep 17 00:00:00 2001 From: sebthom Date: Sun, 26 Oct 2025 23:36:50 +0100 Subject: [PATCH 3/5] Bump bundle version to 3.23 --- bundles/org.eclipse.ui.ide/META-INF/MANIFEST.MF | 2 +- .../org/eclipse/ui/actions/OpenFileWithReuseAction.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.eclipse.ui.ide/META-INF/MANIFEST.MF b/bundles/org.eclipse.ui.ide/META-INF/MANIFEST.MF index c115a47b5f6..7818ea8f4e9 100644 --- a/bundles/org.eclipse.ui.ide/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.ui.ide/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %Plugin.name Bundle-SymbolicName: org.eclipse.ui.ide; singleton:=true -Bundle-Version: 3.22.800.qualifier +Bundle-Version: 3.23.000.qualifier Bundle-Activator: org.eclipse.ui.internal.ide.IDEWorkbenchPlugin Bundle-ActivationPolicy: lazy Bundle-Vendor: %Plugin.providerName diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenFileWithReuseAction.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenFileWithReuseAction.java index 027b400ce70..ea26a1348b2 100644 --- a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenFileWithReuseAction.java +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenFileWithReuseAction.java @@ -46,7 +46,7 @@ * This class is intentionally package-public in org.eclipse.ui.actions to be a * drop-in replacement for {@link OpenFileAction} in CNF wiring. * - * @since 3.22 + * @since 3.23 */ public class OpenFileWithReuseAction extends OpenFileAction { From 613436db74ec3a08a9a72904a2691e8b27b61ab8 Mon Sep 17 00:00:00 2001 From: sebthom Date: Sun, 26 Oct 2025 23:43:01 +0100 Subject: [PATCH 4/5] Support Reuse of Last Opened Editor when JDT Plugin is present Adds `OpenFileWithReuseActionProvider` that wraps the current OPEN handler (dependsOn org.eclipse.jdt.ui.navigator.actions.OpenActions) and installs a wrapper OPEN action in Project Explorer. For single IFile selections (pref enabled), reuse the last editor via `OpenFileWithReuseAction`. For all other selections (e.g., IJavaElement), delegate to the previously installed OPEN handler (JDT's OpenAction), preserving Java behavior. --- .../plugin.xml | 11 ++ .../OpenFileWithReuseActionProvider.java | 121 ++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/OpenFileWithReuseActionProvider.java diff --git a/bundles/org.eclipse.ui.navigator.resources/plugin.xml b/bundles/org.eclipse.ui.navigator.resources/plugin.xml index 7eec1464661..e423a2800dc 100644 --- a/bundles/org.eclipse.ui.navigator.resources/plugin.xml +++ b/bundles/org.eclipse.ui.navigator.resources/plugin.xml @@ -233,6 +233,17 @@ + + + + + + + + diff --git a/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/OpenFileWithReuseActionProvider.java b/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/OpenFileWithReuseActionProvider.java new file mode 100644 index 00000000000..fdff2d88109 --- /dev/null +++ b/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/OpenFileWithReuseActionProvider.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) 2025 Vegard IT GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Sebastian Thomschke (Vegard IT GmbH) - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.internal.navigator.resources.actions; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.Adapters; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.IPageLayout; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.actions.ActionContext; +import org.eclipse.ui.actions.OpenFileWithReuseAction; +import org.eclipse.ui.navigator.CommonActionProvider; +import org.eclipse.ui.navigator.ICommonActionConstants; +import org.eclipse.ui.navigator.ICommonActionExtensionSite; +import org.eclipse.ui.navigator.ICommonViewerWorkbenchSite; + +/** + * Installs a wrapper OPEN handler in Project Explorer that applies last opened + * editor reuse for single {@link IFile} selections, and delegates to the + * previous OPEN handler otherwise (e.g., JDT OpenAction for Java elements). + */ +public class OpenFileWithReuseActionProvider extends CommonActionProvider { + + private ICommonViewerWorkbenchSite viewSite; + private OpenFileWithReuseAction reuseAction; + private IWorkbenchPage reusePage; + + @Override + public void init(ICommonActionExtensionSite aConfig) { + if (aConfig.getViewSite() instanceof ICommonViewerWorkbenchSite) { + viewSite = (ICommonViewerWorkbenchSite) aConfig.getViewSite(); + } + } + + @Override + public void fillActionBars(IActionBars actionBars) { + if (viewSite == null) { + return; + } + // Scope strictly to Project Explorer (including secondary ids) + String partId = viewSite.getSite().getId(); + if (partId == null || !partId.startsWith(IPageLayout.ID_PROJECT_EXPLORER)) { + return; + } + + // Capture current OPEN delegate (e.g., JDT's OpenAction) to call when reuse + // doesn't apply + IAction delegate = actionBars.getGlobalActionHandler(ICommonActionConstants.OPEN); + + IAction wrapper = new Action() { + @Override + public void run() { + ISelection sel = getSelection(); + if (!(sel instanceof IStructuredSelection)) { + if (delegate != null) { + delegate.run(); + } + return; + } + IStructuredSelection ss = (IStructuredSelection) sel; + if (ss.size() == 1) { + Object element = ss.getFirstElement(); + IFile file = Adapters.adapt(element, IFile.class); + if (file != null) { + IWorkbenchPage page = viewSite.getPage(); + if (reuseAction == null || reusePage != page) { + reuseAction = new OpenFileWithReuseAction(page, IPageLayout.ID_PROJECT_EXPLORER); + reusePage = page; + } + reuseAction.selectionChanged(new StructuredSelection(file)); + reuseAction.run(); + return; + } + } + // Fallback: delegate to prior handler (e.g., JDT) + if (delegate != null) { + delegate.run(); + } + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + if (delegate != null) { + delegate.setEnabled(enabled); + } + } + }; + + // Install synchronously; plugin.xml dependsOn ensures we run after JDT's + // provider + actionBars.setGlobalActionHandler(ICommonActionConstants.OPEN, wrapper); + actionBars.updateActionBars(); + } + + private ISelection getSelection() { + ActionContext ctx = getContext(); + if (ctx != null && ctx.getSelection() != null) { + return ctx.getSelection(); + } + return viewSite.getSelectionProvider() != null // + ? viewSite.getSelectionProvider().getSelection() + : StructuredSelection.EMPTY; + } +} From 0859be011a57cc4e79b8e7f828cd7209d60950f6 Mon Sep 17 00:00:00 2001 From: sebthom Date: Sun, 26 Oct 2025 23:54:23 +0100 Subject: [PATCH 5/5] Fix bundle version --- bundles/org.eclipse.ui.ide/META-INF/MANIFEST.MF | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.eclipse.ui.ide/META-INF/MANIFEST.MF b/bundles/org.eclipse.ui.ide/META-INF/MANIFEST.MF index 7818ea8f4e9..aaa47a4c675 100644 --- a/bundles/org.eclipse.ui.ide/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.ui.ide/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %Plugin.name Bundle-SymbolicName: org.eclipse.ui.ide; singleton:=true -Bundle-Version: 3.23.000.qualifier +Bundle-Version: 3.23.0.qualifier Bundle-Activator: org.eclipse.ui.internal.ide.IDEWorkbenchPlugin Bundle-ActivationPolicy: lazy Bundle-Vendor: %Plugin.providerName