From 046082390fbe35c734a69b71e52aa234c0b82840 Mon Sep 17 00:00:00 2001 From: Sougandh S Date: Mon, 8 Jun 2026 09:56:33 +0530 Subject: [PATCH] Show relative termination time in Launch History tooltip Display the relative termination time (for example, "a moment ago", "5 mins ago", or "1 hour ago") in launch history tooltips using the stored termination timestamp. This provides a quick indication of when a launch was last terminated --- .../core/org/eclipse/debug/core/Launch.java | 25 ++++- .../launching/LaunchConfigurationTests.java | 98 ++++++++++++++++++- .../internal/ui/actions/ActionMessages.java | 7 ++ .../ui/actions/ActionMessages.properties | 13 ++- .../actions/AbstractLaunchHistoryAction.java | 76 +++++++++++++- 5 files changed, 214 insertions(+), 5 deletions(-) diff --git a/debug/org.eclipse.debug.core/core/org/eclipse/debug/core/Launch.java b/debug/org.eclipse.debug.core/core/org/eclipse/debug/core/Launch.java index ad366baf37a..e63150a30a9 100644 --- a/debug/org.eclipse.debug.core/core/org/eclipse/debug/core/Launch.java +++ b/debug/org.eclipse.debug.core/core/org/eclipse/debug/core/Launch.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2018 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -25,15 +25,19 @@ import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.MultiStatus; import org.eclipse.core.runtime.PlatformObject; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.debug.core.model.IDebugTarget; import org.eclipse.debug.core.model.IDisconnect; import org.eclipse.debug.core.model.IProcess; import org.eclipse.debug.core.model.ISourceLocator; import org.eclipse.debug.internal.core.DebugCoreMessages; import org.eclipse.debug.internal.core.LaunchManager; +import org.osgi.service.prefs.BackingStoreException; /** * A launch is the result of launching a debug session @@ -475,7 +479,24 @@ protected void fireChanged() { * properly created/initialized. */ protected void fireTerminate() { - setAttribute(DebugPlugin.ATTR_TERMINATE_TIMESTAMP, Long.toString(System.currentTimeMillis())); + String timeStamp = Long.toString(System.currentTimeMillis()); + setAttribute(DebugPlugin.ATTR_TERMINATE_TIMESTAMP, timeStamp); + ILaunchConfiguration launchConfig = getLaunchConfiguration(); + if (launchConfig != null) { + try { + if (launchConfig.isLocal()) { + ILaunchConfigurationWorkingCopy launchCopy = launchConfig.getWorkingCopy(); + launchCopy.setAttribute(DebugPlugin.ATTR_TERMINATE_TIMESTAMP, timeStamp); + launchCopy.doSave(); + } else { + IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(DebugPlugin.getUniqueIdentifier()); + prefs.put(launchConfig.getName(), timeStamp); + prefs.flush(); + } + } catch (CoreException | BackingStoreException e) { + DebugPlugin.log(e); + } + } if (!fSuppressChange) { ((LaunchManager)getLaunchManager()).fireUpdate(this, LaunchManager.TERMINATE); ((LaunchManager)getLaunchManager()).fireUpdate(new ILaunch[] {this}, LaunchManager.TERMINATE); diff --git a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/launching/LaunchConfigurationTests.java b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/launching/LaunchConfigurationTests.java index d675e66742b..fc724831f33 100644 --- a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/launching/LaunchConfigurationTests.java +++ b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/launching/LaunchConfigurationTests.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2025 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -2039,4 +2039,100 @@ public void launchConfigurationAdded(ILaunchConfiguration configuration) { } + @Test + public void testSharedTerminateTimeStampPersistence() throws Exception { + IProject project = getProject(); + ILaunchConfigurationWorkingCopy workingCopy = newConfiguration(project, "testSharedTerminateTimestamp"); + Set terminatedLaunches = Collections.synchronizedSet(new HashSet<>()); + + ILaunchesListener2 listener = new ILaunchesListener2() { + @Override + public void launchesRemoved(ILaunch[] launches) { + } + + @Override + public void launchesChanged(ILaunch[] launches) { + } + + @Override + public void launchesAdded(ILaunch[] launches) { + } + + @Override + public void launchesTerminated(ILaunch[] launches) { + terminatedLaunches.addAll(Arrays.asList(launches)); + } + }; + + DebugPlugin.getDefault().getLaunchManager().addLaunchListener(listener); + ILaunch launch = workingCopy.launch(ILaunchManager.DEBUG_MODE, null); + IProcess process = null; + + try { + process = DebugPlugin.newProcess(launch, new MockProcess(0), "test"); + waitWhile(() -> !terminatedLaunches.contains(launch), () -> "Launch termination event did not occur"); + IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(DebugPlugin.getUniqueIdentifier()); + String stamp = prefs.get(workingCopy.getName(), null); + assertNotNull(stamp, "Missing persisted terminate timestamp"); + long timestamp = Long.parseLong(stamp); + assertTrue(timestamp <= System.currentTimeMillis(), "Terminate timestamp should be before current time"); + } finally { + DebugPlugin.getDefault().getLaunchManager().removeLaunchListener(listener); + if (launch != null) { + getLaunchManager().removeLaunch(launch); + } + if (process != null) { + process.terminate(); + } + } + } + + @Test + public void testLocalTerminateTimeStampPersistence() throws Exception { + ILaunchConfigurationWorkingCopy workingCopy = newConfiguration(null, "testLocalTerminateTimestamp"); + Set terminatedLaunches = Collections.synchronizedSet(new HashSet<>()); + + ILaunchesListener2 listener = new ILaunchesListener2() { + @Override + public void launchesRemoved(ILaunch[] launches) { + } + + @Override + public void launchesChanged(ILaunch[] launches) { + } + + @Override + public void launchesAdded(ILaunch[] launches) { + } + + @Override + public void launchesTerminated(ILaunch[] launches) { + terminatedLaunches.addAll(Arrays.asList(launches)); + } + }; + DebugPlugin.getDefault().getLaunchManager().addLaunchListener(listener); + ILaunch launch = workingCopy.launch(ILaunchManager.DEBUG_MODE, null); + IProcess process = null; + + try { + process = DebugPlugin.newProcess(launch, new MockProcess(0), "test"); + waitWhile(() -> !terminatedLaunches.contains(launch), () -> "Launch termination event did not occur"); + ILaunchConfiguration config = launch.getLaunchConfiguration(); + assertNotNull(config); + assertTrue(config.isLocal()); + String timeStamp = config.getAttribute(DebugPlugin.ATTR_TERMINATE_TIMESTAMP, (String) null); + assertNotNull(timeStamp, "Terminate timestamp was not persisted"); + long timestamp = Long.parseLong(timeStamp); + assertTrue(timestamp <= System.currentTimeMillis(), "Terminate timestamp should be before current time"); + } finally { + DebugPlugin.getDefault().getLaunchManager().removeLaunchListener(listener); + if (launch != null) { + getLaunchManager().removeLaunch(launch); + } + if (process != null) { + process.terminate(); + } + } + } + } diff --git a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/actions/ActionMessages.java b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/actions/ActionMessages.java index 606467dee19..8942b51694a 100644 --- a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/actions/ActionMessages.java +++ b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/actions/ActionMessages.java @@ -261,5 +261,12 @@ public class ActionMessages extends NLS { public static String ExpressionPasteDialog; public static String ExpressionPasteRemember; public static String ExpressionPastePromptButton; + public static String LaunchActionToolTip_Seconds; + public static String LaunchActionToolTip_Minutes; + public static String LaunchActionToolTip_OneMinute; + public static String LaunchActionToolTip_OneHour; + public static String LaunchActionToolTip_Hours; + public static String LaunchActionToolTip_OneDay; + public static String LaunchActionToolTip_Days; } \ No newline at end of file diff --git a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/actions/ActionMessages.properties b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/actions/ActionMessages.properties index 24c158b608b..e16e1bb711c 100644 --- a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/actions/ActionMessages.properties +++ b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/actions/ActionMessages.properties @@ -245,4 +245,15 @@ ExpressionPasteSingleButton=Single Expression ExpressionPastePromptButton=Prompt ExpressionPasteTitle=Paste Multiline Expression ExpressionPasteDialog=You are pasting a multiline expression. Choose whether to paste it as a single combined expression or as separate multiple expressions. -ExpressionPasteRemember=Remember my preference next time \ No newline at end of file +ExpressionPasteRemember=Remember my preference next time + +LaunchActionToolTip_Seconds = a moment ago +LaunchActionToolTip_OneMinute = 1 min ago +LaunchActionToolTip_Minutes = {0} mins ago +LaunchActionToolTip_OneHour = 1 hour ago +LaunchActionToolTip_Hours = {0} hours ago +LaunchActionToolTip_OneDay = 1 day ago +LaunchActionToolTip_Days = {0} days ago + + + diff --git a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/ui/actions/AbstractLaunchHistoryAction.java b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/ui/actions/AbstractLaunchHistoryAction.java index 0a5d52c57b8..66c65de56b1 100644 --- a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/ui/actions/AbstractLaunchHistoryAction.java +++ b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/ui/actions/AbstractLaunchHistoryAction.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2016 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -22,6 +22,8 @@ import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; @@ -50,6 +52,7 @@ import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.events.MenuAdapter; import org.eclipse.swt.events.MenuEvent; @@ -353,6 +356,8 @@ protected void fillMenu(Menu menu) { LaunchAction action= new LaunchAction(launch, getMode()); if (checkIfLaunchActive(launch, launches)) { action.setText(action.getText() + " \u2699"); //$NON-NLS-1$ + } else { + addRecentLaunchTimeTooltip(launch, action); } addToMenu(menu, action, accelerator); accelerator++; @@ -368,6 +373,8 @@ protected void fillMenu(Menu menu) { LaunchAction action= new LaunchAction(launch, getMode()); if (checkIfLaunchActive(launch, launches)) { action.setText(action.getText() + " \u2699"); //$NON-NLS-1$ + } else { + addRecentLaunchTimeTooltip(launch, action); } addToMenu(menu, action, accelerator); accelerator++; @@ -627,4 +634,71 @@ private LaunchConfigurationManager getLaunchConfigurationManager() { protected String getLaunchGroupIdentifier() { return fLaunchGroup.getIdentifier(); } + + /** + * Adds a tooltip showing how long ago the given launch was terminated if + * there's a valid terminate timestamp attribute. + * + * @param launch launch configuration + * @param launchAction launch action to update + */ + private void addRecentLaunchTimeTooltip(ILaunchConfiguration launch, LaunchAction launchAction) { + try { + String timeStamp; + if (launch.isLocal()) { + timeStamp = launch.getAttribute(DebugPlugin.ATTR_TERMINATE_TIMESTAMP, ""); //$NON-NLS-1$ + } else { + IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(DebugPlugin.getUniqueIdentifier()); + timeStamp = prefs.get(launch.getName(), ""); //$NON-NLS-1$ + } + + if (!timeStamp.isEmpty()) { + long timestamp = Long.parseLong(timeStamp); + launchAction.setToolTipText(getRelativeTime(timestamp)); + } + } catch (CoreException | NumberFormatException e) { + DebugUIPlugin.log(e); + } + } + + /** + * Returns a human-readable relative time string for the given timestamp. + * + * @param timestamp timestamp in milliseconds since the epoch + * @return relative time string + */ + private String getRelativeTime(long timestamp) { + long diffMillis = Math.max(0L, System.currentTimeMillis() - timestamp); + long seconds = diffMillis / 1000; + + if (seconds < 60) { + return ActionMessages.LaunchActionToolTip_Seconds; + } + + long minutes = seconds / 60; + + if (minutes == 1) { + return ActionMessages.LaunchActionToolTip_OneMinute; + } + if (minutes < 60) { + return NLS.bind(ActionMessages.LaunchActionToolTip_Minutes, minutes); + } + + long hours = minutes / 60; + + if (hours == 1) { + return ActionMessages.LaunchActionToolTip_OneHour; + } + if (hours < 24) { + return NLS.bind(ActionMessages.LaunchActionToolTip_Hours, hours); + } + + long days = hours / 24; + + if (days == 1) { + return ActionMessages.LaunchActionToolTip_OneDay; + } + return NLS.bind(ActionMessages.LaunchActionToolTip_Days, days); + } + }