Skip to content

Commit 006f657

Browse files
committed
Allow reuse of last opened editor via Project Explorer
1 parent 4acc3e3 commit 006f657

File tree

9 files changed

+368
-5
lines changed

9 files changed

+368
-5
lines changed
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Vegard IT GmbH and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Sebastian Thomschke (Vegard IT GmbH) - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.ui.actions;
15+
16+
import org.eclipse.core.resources.IFile;
17+
import org.eclipse.core.resources.IResource;
18+
import org.eclipse.jface.util.OpenStrategy;
19+
import org.eclipse.ui.IEditorDescriptor;
20+
import org.eclipse.ui.IEditorPart;
21+
import org.eclipse.ui.IEditorReference;
22+
import org.eclipse.ui.IEditorRegistry;
23+
import org.eclipse.ui.IPageLayout;
24+
import org.eclipse.ui.IReusableEditor;
25+
import org.eclipse.ui.IWorkbenchPage;
26+
import org.eclipse.ui.IWorkbenchPartReference;
27+
import org.eclipse.ui.PartInitException;
28+
import org.eclipse.ui.ide.IDE;
29+
import org.eclipse.ui.internal.IPreferenceConstants;
30+
import org.eclipse.ui.internal.ide.DialogUtil;
31+
import org.eclipse.ui.internal.ide.IDEWorkbenchMessages;
32+
import org.eclipse.ui.internal.util.PrefUtil;
33+
import org.eclipse.ui.part.FileEditorInput;
34+
35+
/**
36+
* Open action that optionally reuses a single editor for opens originating from
37+
* the Project Explorer, similar to the Search view behavior. When the feature
38+
* is disabled or outside Project Explorer, behavior falls back to
39+
* {@link OpenFileAction}.
40+
*
41+
* Scope is currently limited to Project Explorer via the creating part id
42+
* passed in the constructor. Reuse stops when the editor becomes dirty, is
43+
* pinned, closed, or when the resolved editor id differs from the currently
44+
* reused editor id.
45+
*
46+
* This class is intentionally package-public in org.eclipse.ui.actions to be a
47+
* drop-in replacement for {@link OpenFileAction} in CNF wiring.
48+
*/
49+
public class OpenFileWithReuseAction extends OpenFileAction {
50+
51+
private final String hostPartId;
52+
private IEditorReference reusedRef;
53+
private boolean disableReuseForRun;
54+
55+
/**
56+
* @param page workbench page
57+
* @param hostPartId id of the part creating this action (used to scope to
58+
* Project Explorer)
59+
*/
60+
public OpenFileWithReuseAction(final IWorkbenchPage page, final String hostPartId) {
61+
this(page, null, hostPartId);
62+
}
63+
64+
/**
65+
* @param page workbench page
66+
* @param descriptor editor descriptor to use (or null for default)
67+
* @param hostPartId id of the part creating this action (used to scope to
68+
* Project Explorer)
69+
*/
70+
public OpenFileWithReuseAction(final IWorkbenchPage page, final IEditorDescriptor descriptor,
71+
final String hostPartId) {
72+
super(page, descriptor);
73+
this.hostPartId = hostPartId;
74+
}
75+
76+
@Override
77+
public void run() {
78+
// Disable reuse for multi-file selection to preserve baseline behavior
79+
int fileCount = 0;
80+
for (final IResource res : getSelectedResources()) {
81+
if (res instanceof IFile) {
82+
fileCount++;
83+
if (fileCount > 1) {
84+
break;
85+
}
86+
}
87+
}
88+
disableReuseForRun = fileCount > 1;
89+
try {
90+
super.run();
91+
} finally {
92+
disableReuseForRun = false;
93+
}
94+
}
95+
96+
@Override
97+
void openFile(final IFile file) {
98+
if (disableReuseForRun || !shouldApplyReuse()) {
99+
// Delegate to baseline behavior
100+
super.openFile(file);
101+
return;
102+
}
103+
104+
final IWorkbenchPage page = getWorkbenchPage();
105+
final boolean activate = OpenStrategy.activateOnOpen();
106+
107+
// If already open, just bring to top/activate
108+
final IEditorPart existing = page.findEditor(new FileEditorInput(file));
109+
if (existing != null) {
110+
if (activate) {
111+
page.activate(existing);
112+
} else {
113+
page.bringToTop(existing);
114+
}
115+
return;
116+
}
117+
118+
// Determine target editor id (explicit),
119+
// falling back to external system editor id
120+
String editorId = IEditorRegistry.SYSTEM_EXTERNAL_EDITOR_ID;
121+
IEditorDescriptor desc = null;
122+
try {
123+
desc = IDE.getEditorDescriptor(file, true, true);
124+
} catch (PartInitException e) {
125+
// ignore here; will fall back to system external editor id
126+
}
127+
if (desc != null) {
128+
editorId = desc.getId();
129+
}
130+
131+
// Do not attempt reuse for external editors
132+
if (IEditorRegistry.SYSTEM_EXTERNAL_EDITOR_ID.equals(editorId)) {
133+
super.openFile(file);
134+
return;
135+
}
136+
137+
// Try reuse if we have a valid reference
138+
if (reusedRef != null) {
139+
final boolean open = reusedRef.getEditor(false) != null;
140+
final boolean valid = open && !reusedRef.isDirty() && !reusedRef.isPinned();
141+
if (valid) {
142+
if (!editorId.equals(reusedRef.getId())) {
143+
// Different editor type needed; close old reusable editor
144+
page.closeEditors(new IEditorReference[] { reusedRef }, false);
145+
reusedRef = null;
146+
} else {
147+
final IEditorPart part = reusedRef.getEditor(true);
148+
if (part instanceof IReusableEditor reusableEditor) {
149+
reusableEditor.setInput(new FileEditorInput(file));
150+
if (activate) {
151+
page.activate(part);
152+
} else {
153+
page.bringToTop(part);
154+
}
155+
return;
156+
}
157+
// Not reusable after all
158+
reusedRef = null;
159+
}
160+
} else {
161+
// Reference no longer valid
162+
reusedRef = null;
163+
}
164+
}
165+
166+
// Open a new editor and remember it if reusable
167+
try {
168+
final IEditorPart opened = IDE.openEditor(page, file, editorId, activate);
169+
if (opened instanceof IReusableEditor) {
170+
final IWorkbenchPartReference ref = page.getReference(opened);
171+
if (ref instanceof IEditorReference editorRef) {
172+
reusedRef = editorRef;
173+
} else {
174+
reusedRef = null;
175+
}
176+
} else {
177+
reusedRef = null;
178+
}
179+
} catch (final PartInitException ex) {
180+
DialogUtil.openError(page.getWorkbenchWindow().getShell(),
181+
IDEWorkbenchMessages.OpenFileAction_openFileShellTitle, ex.getMessage(), ex);
182+
}
183+
}
184+
185+
private boolean shouldApplyReuse() {
186+
// Strictly scope to Project Explorer and opt-in preference
187+
if (hostPartId == null || !hostPartId.startsWith(IPageLayout.ID_PROJECT_EXPLORER)) {
188+
return false;
189+
}
190+
191+
return PrefUtil.getInternalPreferenceStore().getBoolean(IPreferenceConstants.REUSE_LAST_OPENED_EDITOR);
192+
}
193+
}

bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/OpenActionProvider.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.eclipse.jface.viewers.IStructuredSelection;
2727
import org.eclipse.ui.IActionBars;
2828
import org.eclipse.ui.actions.OpenFileAction;
29+
import org.eclipse.ui.actions.OpenFileWithReuseAction;
2930
import org.eclipse.ui.actions.OpenWithMenu;
3031
import org.eclipse.ui.internal.navigator.resources.plugin.WorkbenchNavigatorMessages;
3132
import org.eclipse.ui.navigator.CommonActionProvider;
@@ -51,7 +52,7 @@ public class OpenActionProvider extends CommonActionProvider {
5152
public void init(ICommonActionExtensionSite aConfig) {
5253
if (aConfig.getViewSite() instanceof ICommonViewerWorkbenchSite) {
5354
viewSite = (ICommonViewerWorkbenchSite) aConfig.getViewSite();
54-
openFileAction = new OpenFileAction(viewSite.getPage());
55+
openFileAction = new OpenFileWithReuseAction(viewSite.getPage(), viewSite.getSite().getId());
5556
contribute = true;
5657
}
5758
}

bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/IPreferenceConstants.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,4 +327,10 @@ public interface IPreferenceConstants {
327327
*/
328328
String SHOW_KEYS_TIME_TO_CLOSE = "showCommandKeys_timeToClose"; //$NON-NLS-1$
329329

330+
/**
331+
* Preference to enable editor reuse for opens from (currently only) Project
332+
* Explorer. Applies to both single- and double-click. Default false.
333+
*/
334+
String REUSE_LAST_OPENED_EDITOR = "REUSE_LAST_OPENED_EDITOR"; //$NON-NLS-1$
335+
330336
}

bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbenchMessages.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,7 @@ public class WorkbenchMessages extends NLS {
545545
public static String WorkbenchPreference_singleClick;
546546
public static String WorkbenchPreference_singleClick_SelectOnHover;
547547
public static String WorkbenchPreference_singleClick_OpenAfterDelay;
548+
public static String WorkbenchPreference_reuseLastOpenedEditor;
548549
public static String WorkbenchPreference_noEffectOnAllViews;
549550
public static String WorkbenchPreference_HeapStatusButton;
550551
public static String WorkbenchPreference_HeapStatusButtonToolTip;

bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbenchPreferenceInitializer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public void initializeDefaultPreferences() {
5959
node.putBoolean(IPreferenceConstants.STICKY_CYCLE, false);
6060
node.putBoolean(IPreferenceConstants.REUSE_EDITORS_BOOLEAN, true);
6161
node.putInt(IPreferenceConstants.REUSE_EDITORS, 99);
62+
node.putBoolean(IPreferenceConstants.REUSE_LAST_OPENED_EDITOR, false);
6263
node.putBoolean(IPreferenceConstants.OPEN_ON_SINGLE_CLICK, false);
6364
node.putBoolean(IPreferenceConstants.SELECT_ON_HOVER, false);
6465
node.putBoolean(IPreferenceConstants.OPEN_AFTER_DELAY, false);

bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/dialogs/WorkbenchPreferencePage.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ public class WorkbenchPreferencePage extends PreferencePage implements IWorkbenc
8080

8181
private Button showInlineRenameButton;
8282

83+
private Button reuseLastOpenedEditorButton;
84+
8385
protected static int MAX_SAVE_INTERVAL = 9999;
8486
protected static int MAX_VIEW_LIMIT = 1_000_000;
8587

@@ -295,6 +297,14 @@ protected void createOpenModeGroup(Composite composite) {
295297
data.horizontalIndent = LayoutConstants.getIndent();
296298
openAfterDelayButton.setLayoutData(data);
297299

300+
// reuse last opened editor when opening a file (currently only in Project
301+
// Explorer)
302+
reuseLastOpenedEditorButton = new Button(buttonComposite, SWT.CHECK | SWT.LEFT);
303+
reuseLastOpenedEditorButton
304+
.setText(WorkbenchMessages.WorkbenchPreference_reuseLastOpenedEditor);
305+
reuseLastOpenedEditorButton.setSelection(
306+
getPreferenceStore().getBoolean(IPreferenceConstants.REUSE_LAST_OPENED_EDITOR));
307+
298308
createNoteComposite(font, buttonComposite, WorkbenchMessages.Preference_note,
299309
WorkbenchMessages.WorkbenchPreference_noEffectOnAllViews);
300310
}
@@ -419,6 +429,8 @@ protected void performDefaults() {
419429
openAfterDelayButton.setSelection(openAfterDelay);
420430
selectOnHoverButton.setEnabled(openOnSingleClick);
421431
openAfterDelayButton.setEnabled(openOnSingleClick);
432+
reuseLastOpenedEditorButton
433+
.setSelection(store.getDefaultBoolean(IPreferenceConstants.REUSE_LAST_OPENED_EDITOR));
422434
stickyCycleButton.setSelection(store.getDefaultBoolean(IPreferenceConstants.STICKY_CYCLE));
423435
showUserDialogButton.setSelection(store.getDefaultBoolean(IPreferenceConstants.RUN_IN_BACKGROUND));
424436
showHeapStatusButton.setSelection(
@@ -443,6 +455,8 @@ public boolean performOk() {
443455
store.setValue(IPreferenceConstants.OPEN_ON_SINGLE_CLICK, openOnSingleClick);
444456
store.setValue(IPreferenceConstants.SELECT_ON_HOVER, selectOnHover);
445457
store.setValue(IPreferenceConstants.OPEN_AFTER_DELAY, openAfterDelay);
458+
store.setValue(IPreferenceConstants.REUSE_LAST_OPENED_EDITOR,
459+
reuseLastOpenedEditorButton.getSelection());
446460
store.setValue(IPreferenceConstants.RUN_IN_BACKGROUND, showUserDialogButton.getSelection());
447461
store.setValue(IPreferenceConstants.WORKBENCH_SAVE_INTERVAL, saveInterval.getIntValue());
448462
store.setValue(IWorkbenchPreferenceConstants.LARGE_VIEW_LIMIT, largeViewLimit.getIntValue());

bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/messages.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ OpenPerspectiveDialogAction_tooltip=Open Perspective
496496

497497
#---- General Preferences----
498498
PreferencePage_noDescription = (No description available)
499-
PreferencePageParameterValues_pageLabelSeparator = \ >\
499+
PreferencePageParameterValues_pageLabelSeparator = \ >\
500500
ThemingEnabled = E&nable theming
501501
ThemeChangeWarningText = Restart for the theme changes to take full effect
502502
ThemeChangeWarningTitle = Theme Changed
@@ -511,6 +511,7 @@ WorkbenchPreference_doubleClick=D&ouble click
511511
WorkbenchPreference_singleClick=&Single click
512512
WorkbenchPreference_singleClick_SelectOnHover=Select on &hover
513513
WorkbenchPreference_singleClick_OpenAfterDelay=Open when using arrow &keys
514+
WorkbenchPreference_reuseLastOpenedEditor=Reuse last opened editor when opening files via Project Explorer
514515
WorkbenchPreference_noEffectOnAllViews=This preference may not take effect on all views
515516
WorkbenchPreference_inlineRename=&Rename resource inline if available
516517

tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/NavigatorTestSuite.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@
2525
import org.eclipse.ui.tests.navigator.resources.NestedResourcesTests;
2626
import org.eclipse.ui.tests.navigator.resources.PathComparatorTest;
2727
import org.eclipse.ui.tests.navigator.resources.ResourceMgmtActionProviderTests;
28-
29-
import org.junit.platform.suite.api.Suite;
3028
import org.junit.platform.suite.api.SelectClasses;
29+
import org.junit.platform.suite.api.Suite;
3130

3231
@Suite
3332
@SelectClasses({ InitialActivationTest.class, ActionProviderTest.class, ExtensionsTest.class, FilterTest.class,
@@ -37,7 +36,7 @@
3736
FirstClassM1Tests.class, LinkHelperTest.class, ShowInTest.class, ResourceTransferTest.class,
3837
EvaluationCacheTest.class, ResourceMgmtActionProviderTests.class,
3938
NestedResourcesTests.class, PathComparatorTest.class, FoldersAsProjectsContributionTest.class,
40-
GoBackForwardsTest.class
39+
GoBackForwardsTest.class, OpenFileWithReuseActionTest.class,
4140
// DnDTest.class, // DnDTest.testSetDragOperation() fails
4241
// PerformanceTest.class // Does not pass on all platforms see bug 264449
4342
})

0 commit comments

Comments
 (0)