Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bundles/org.eclipse.ui.ide/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -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.0.qualifier
Bundle-Activator: org.eclipse.ui.internal.ide.IDEWorkbenchPlugin
Bundle-ActivationPolicy: lazy
Bundle-Vendor: %Plugin.providerName
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/*******************************************************************************
* 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.
*
* @since 3.23
*/
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);
}
}
11 changes: 11 additions & 0 deletions bundles/org.eclipse.ui.navigator.resources/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,17 @@
</enablement>
</actionProvider>

<!-- Wrapper OPEN handler for Project Explorer: applies reuse-last-open-editor for IFile, delegates otherwise -->
<actionProvider
class="org.eclipse.ui.internal.navigator.resources.actions.OpenFileWithReuseActionProvider"
id="org.eclipse.ui.navigator.resources.ReuseOpenActions"
dependsOn="org.eclipse.jdt.ui.navigator.actions.OpenActions">
<!-- true: always enabled so we can wrap any OPEN handler and delegate when needed -->
<enablement>
<or/>
</enablement>
</actionProvider>

<actionProvider
class="org.eclipse.ui.internal.navigator.resources.actions.ResourceMgmtActionProvider"
id="org.eclipse.ui.navigator.resources.ResourceMgmtActions">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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$

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading