From 1de591c65829e6b2380fbf411bc2e7df793f6fbf Mon Sep 17 00:00:00 2001 From: Andrey Loskutov Date: Tue, 24 Feb 2026 11:11:35 +0100 Subject: [PATCH 1/8] Manual code cleanup & fixes in ConsoleManager - Extracted common code from ShowConsoleViewJob to AbstractConsoleJob - Renamed RepaintJob to RedrawJob, switched to use AbstractConsoleJob - Extracted anonymous UI job to WarnAboutContentChangedJob, based on AbstractConsoleJob - fWarnQueued is not needed anymore for WarnAboutContentChangedJob - This above fixes the race condition where multiple consoles changes could have triggered the warnOfContentChange(), but the job only scheduled for the first one - Moved final field inits to constructor - Fixed potential NP in RedrawJob and ShowConsoleViewJob See https://github.com/eclipse-platform/eclipse.platform/issues/2477 --- .../ui/internal/console/ConsoleManager.java | 189 +++++++++--------- 1 file changed, 96 insertions(+), 93 deletions(-) diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleManager.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleManager.java index 69ead9b2015..708bd2ea14a 100644 --- a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleManager.java +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleManager.java @@ -15,7 +15,6 @@ package org.eclipse.ui.internal.console; import java.util.ArrayList; -import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -36,6 +35,7 @@ import org.eclipse.swt.widgets.Control; import org.eclipse.ui.IViewPart; import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchPartSite; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; @@ -48,7 +48,7 @@ import org.eclipse.ui.console.IConsoleView; import org.eclipse.ui.console.IPatternMatchListener; import org.eclipse.ui.console.TextConsole; -import org.eclipse.ui.progress.UIJob; +import org.eclipse.ui.part.IPage; import org.eclipse.ui.progress.WorkbenchJob; /** @@ -62,12 +62,12 @@ public class ConsoleManager implements IConsoleManager { /** * Console listeners */ - private final ListenerList fListeners = new ListenerList<>(); + private final ListenerList fListeners; /** * List of registered consoles */ - private final List fConsoles = new ArrayList<>(10); + private final List fConsoles; // change notification constants @@ -80,60 +80,60 @@ public class ConsoleManager implements IConsoleManager { private List fConsoleFactoryExtensions; - private final List fConsoleViews = new ArrayList<>(); + private final List fConsoleViews; - private boolean fWarnQueued = false; + /** Used to trigger redrawing of console pages when links changed */ + private final RedrawJob redrawConsoleJob; - private final RepaintJob fRepaintJob = new RepaintJob(); + /** Used to show console view in active window */ + private final ShowConsoleViewJob showConsoleJob; - private class RepaintJob extends WorkbenchJob { - private final Set list = new HashSet<>(); + /** Show console change indication in all views if console is not visible */ + private final WarnAboutContentChangedJob warnAboutContentChangeJob; - public RepaintJob() { - super("schedule redraw() of viewers"); //$NON-NLS-1$ - setSystem(true); - } + public ConsoleManager() { + fListeners = new ListenerList<>(); + fConsoles = new ArrayList<>(10); + fConsoleViews = new ArrayList<>(); + redrawConsoleJob = new RedrawJob(); + showConsoleJob = new ShowConsoleViewJob(); + warnAboutContentChangeJob = new WarnAboutContentChangedJob(); + } - @Override - public boolean belongsTo(Object family) { - return family == ConsoleManager.CONSOLE_JOB_FAMILY; - } + private class RedrawJob extends AbstractConsoleJob { - void addConsole(IConsole console) { - synchronized (list) { - list.add(console); - } + public RedrawJob() { + super("Schedule console redraw"); //$NON-NLS-1$ + setSystem(true); } @Override - public IStatus runInUIThread(IProgressMonitor monitor) { - synchronized (list) { - if (list.isEmpty()) { - return Status.OK_STATUS; - } - - IWorkbenchWindow[] workbenchWindows = PlatformUI.getWorkbench().getWorkbenchWindows(); - for (IWorkbenchWindow window : workbenchWindows) { - if (window != null) { - IWorkbenchPage page = window.getActivePage(); - if (page != null) { - IViewPart part = page.findView(IConsoleConstants.ID_CONSOLE_VIEW); - if (part != null && part instanceof IConsoleView) { - ConsoleView view = (ConsoleView) part; - if (list.contains(view.getConsole())) { - Control control = view.getCurrentPage().getControl(); - if (!control.isDisposed()) { - control.redraw(); - } + protected void workWith(IConsole console, IProgressMonitor monitor) { + IWorkbenchWindow[] workbenchWindows = PlatformUI.getWorkbench().getWorkbenchWindows(); + for (IWorkbenchWindow window : workbenchWindows) { + if (window != null) { + IWorkbenchPage page = window.getActivePage(); + if (page != null) { + IViewPart part = page.findView(IConsoleConstants.ID_CONSOLE_VIEW); + if (part != null && part instanceof IConsoleView) { + ConsoleView view = (ConsoleView) part; + if (console.equals(view.getConsole())) { + IPage currentPage = view.getCurrentPage(); + if (currentPage == null) { + continue; + } + Control control = currentPage.getControl(); + if (control != null && !control.isDisposed()) { + control.redraw(); } } - } } } - list.clear(); + if (monitor.isCanceled()) { + return; + } } - return Status.OK_STATUS; } } @@ -256,12 +256,11 @@ private void fireUpdate(IConsole[] consoles, int type) { new ConsoleNotifier().notify(consoles, type); } - - private class ShowConsoleViewJob extends WorkbenchJob { + private abstract static class AbstractConsoleJob extends WorkbenchJob { private final Set queue = new LinkedHashSet<>(); - ShowConsoleViewJob() { - super("Show Console View"); //$NON-NLS-1$ + AbstractConsoleJob(String name) { + super(name); setSystem(true); setPriority(Job.SHORT); } @@ -271,7 +270,7 @@ public boolean belongsTo(Object family) { return family == ConsoleManager.CONSOLE_JOB_FAMILY; } - void addConsole(IConsole console) { + protected void addConsole(IConsole console) { synchronized (queue) { queue.add(console); } @@ -279,13 +278,16 @@ void addConsole(IConsole console) { @Override public IStatus runInUIThread(IProgressMonitor monitor) { - Set consolesToShow; + Set consolesToWorkWith; synchronized (queue) { - consolesToShow = new LinkedHashSet<>(queue); + consolesToWorkWith = new LinkedHashSet<>(queue); queue.clear(); } - for (IConsole c : consolesToShow) { - showConsole(c); + for (IConsole c : consolesToWorkWith) { + workWith(c, monitor); + if (monitor.isCanceled()) { + return Status.CANCEL_STATUS; + } } synchronized (queue) { if (!queue.isEmpty()) { @@ -295,7 +297,17 @@ public IStatus runInUIThread(IProgressMonitor monitor) { return Status.OK_STATUS; } - private void showConsole(IConsole c) { + abstract protected void workWith(IConsole console, IProgressMonitor monitor); + } + + private class ShowConsoleViewJob extends AbstractConsoleJob { + + ShowConsoleViewJob() { + super("Show Console View"); //$NON-NLS-1$ + } + + @Override + protected void workWith(IConsole c, IProgressMonitor monitor) { boolean consoleFound = false; IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); if (window != null && c != null) { @@ -303,7 +315,11 @@ private void showConsole(IConsole c) { if (page != null) { synchronized (fConsoleViews) { for (IConsoleView consoleView : fConsoleViews) { - if (consoleView.getSite().getPage().equals(page)) { + IWorkbenchPartSite site = consoleView.getSite(); + if (site == null) { + continue; + } + if (site.getPage().equals(page)) { boolean consoleVisible = page.isPartVisible(consoleView); if (consoleVisible) { consoleFound = true; @@ -314,6 +330,9 @@ private void showConsole(IConsole c) { consoleView.display(c); } } + if (monitor.isCanceled()) { + return; + } } } @@ -334,14 +353,10 @@ private void showConsole(IConsole c) { } } - private final ShowConsoleViewJob showJob = new ShowConsoleViewJob(); - /** - * @see IConsoleManager#showConsoleView(IConsole) - */ @Override public void showConsoleView(final IConsole console) { - showJob.addConsole(console); - showJob.schedule(100); + showConsoleJob.addConsole(console); + showConsoleJob.schedule(100); } /** @@ -366,32 +381,28 @@ private boolean shouldBringToTop(IConsole console, IViewPart consoleView) { @Override public void warnOfContentChange(final IConsole console) { - if (!fWarnQueued) { - fWarnQueued = true; - Job job = new UIJob(ConsolePlugin.getStandardDisplay(), ConsoleMessages.ConsoleManager_consoleContentChangeJob) { - @Override - public boolean belongsTo(Object family) { - return family == ConsoleManager.CONSOLE_JOB_FAMILY; - } + warnAboutContentChangeJob.addConsole(console); + warnAboutContentChangeJob.schedule(50); + } - @Override - public IStatus runInUIThread(IProgressMonitor monitor) { - IWorkbenchWindow window= PlatformUI.getWorkbench().getActiveWorkbenchWindow(); - if (window != null) { - IWorkbenchPage page= window.getActivePage(); - if (page != null) { - IConsoleView consoleView= (IConsoleView)page.findView(IConsoleConstants.ID_CONSOLE_VIEW); - if (consoleView != null) { - consoleView.warnOfContentChange(console); - } - } + private final class WarnAboutContentChangedJob extends AbstractConsoleJob { + + private WarnAboutContentChangedJob() { + super(ConsoleMessages.ConsoleManager_consoleContentChangeJob); + } + + @Override + protected void workWith(IConsole console, IProgressMonitor monitor) { + IWorkbenchWindow window= PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (window != null) { + IWorkbenchPage page= window.getActivePage(); + if (page != null) { + IConsoleView consoleView= (IConsoleView)page.findView(IConsoleConstants.ID_CONSOLE_VIEW); + if (consoleView != null) { + consoleView.warnOfContentChange(console); } - fWarnQueued = false; - return Status.OK_STATUS; } - }; - job.setSystem(true); - job.schedule(); + } } } @@ -432,11 +443,6 @@ public IPatternMatchListener[] createPatternMatchListeners(IConsole console) { return list.toArray(new PatternMatchListener[0]); } - /* - * @see - * org.eclipse.ui.console.IConsoleManager#getPageParticipants(org.eclipse.ui. - * console.IConsole) - */ public IConsolePageParticipant[] getPageParticipants(IConsole console) { if(fPageParticipants == null) { fPageParticipants = new ArrayList<>(); @@ -460,9 +466,6 @@ public IConsolePageParticipant[] getPageParticipants(IConsole console) { return list.toArray(new IConsolePageParticipant[0]); } - /* - * @see org.eclipse.ui.console.IConsoleManager#getConsoleFactories() - */ public ConsoleFactoryExtension[] getConsoleFactoryExtensions() { if (fConsoleFactoryExtensions == null) { fConsoleFactoryExtensions = new ArrayList<>(); @@ -478,8 +481,8 @@ public ConsoleFactoryExtension[] getConsoleFactoryExtensions() { @Override public void refresh(final IConsole console) { - fRepaintJob.addConsole(console); - fRepaintJob.schedule(50); + redrawConsoleJob.addConsole(console); + redrawConsoleJob.schedule(50); } } From 4477d7ed75a9780fd05403109846bf15ea7d04a9 Mon Sep 17 00:00:00 2001 From: Andrey Loskutov Date: Tue, 24 Feb 2026 15:14:04 +0100 Subject: [PATCH 2/8] Fix link redraw issues with secondary consoles `page.findView(IConsoleConstants.ID_CONSOLE_VIEW)` API doesn't return console views opened in same page with secondary id's (== any Console views opened next to the first one in same page). So all calls to `ConsoleManager.refresh(IConsole)` were not working for such views. In SDK, this would only affect `TextConsole.addHyperlink()` code and is visible if opening Java Stack Trace view as secondary view - it will not show hyperlinks. Worse, since view id's are persisted across sessions, such views would not work properly as long as they present in the perspective. For the fix, make use of views registered in ConsoleManager and iterate over them, similar to ShowConsoleViewJob code. See https://github.com/eclipse-platform/eclipse.platform/issues/2477 --- .../ui/internal/console/ConsoleManager.java | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleManager.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleManager.java index 708bd2ea14a..9413c5e17ff 100644 --- a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleManager.java +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleManager.java @@ -109,29 +109,21 @@ public RedrawJob() { @Override protected void workWith(IConsole console, IProgressMonitor monitor) { - IWorkbenchWindow[] workbenchWindows = PlatformUI.getWorkbench().getWorkbenchWindows(); - for (IWorkbenchWindow window : workbenchWindows) { - if (window != null) { - IWorkbenchPage page = window.getActivePage(); - if (page != null) { - IViewPart part = page.findView(IConsoleConstants.ID_CONSOLE_VIEW); - if (part != null && part instanceof IConsoleView) { - ConsoleView view = (ConsoleView) part; - if (console.equals(view.getConsole())) { - IPage currentPage = view.getCurrentPage(); - if (currentPage == null) { - continue; - } - Control control = currentPage.getControl(); - if (control != null && !control.isDisposed()) { - control.redraw(); - } - } + synchronized (fConsoleViews) { + for (ConsoleView view : fConsoleViews) { + if (console.equals(view.getConsole())) { + IPage currentPage = view.getCurrentPage(); + if (currentPage == null) { + continue; + } + Control control = currentPage.getControl(); + if (control != null && !control.isDisposed()) { + control.redraw(); } } - } - if (monitor.isCanceled()) { - return; + if (monitor.isCanceled()) { + return; + } } } } From e22266f1c5ffff9538292064e59f68ecf5781e22 Mon Sep 17 00:00:00 2001 From: Andrey Loskutov Date: Tue, 24 Feb 2026 11:14:50 +0100 Subject: [PATCH 3/8] Fix console content notifications Original code had multiple issues: it didn't notified about console content changes in all opened windows, it didn't notified for the console changes in all secondary console views but it notified even if the console with changes was visible. As noticed already before, `page.findView(IConsoleConstants.ID_CONSOLE_VIEW)` API doesn't return console views opened in same page with secondary id's (== any Console views opened next to the first one in same page). So WarnAboutContentChangedJob only supported Console views without secondary ids and only in the currently active window. - Notify console changes in all windows, not only in the active one - Notify also for secondary consoles - Don't notify about console changes if changed console page is visible (== top of the stack) in the current Console view and Console view is also visible. Fixes https://github.com/eclipse-platform/eclipse.platform/issues/2477 --- .../ui/internal/console/ConsoleManager.java | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleManager.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleManager.java index 9413c5e17ff..5e92f56c1f2 100644 --- a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleManager.java +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleManager.java @@ -385,16 +385,27 @@ private WarnAboutContentChangedJob() { @Override protected void workWith(IConsole console, IProgressMonitor monitor) { - IWorkbenchWindow window= PlatformUI.getWorkbench().getActiveWorkbenchWindow(); - if (window != null) { - IWorkbenchPage page= window.getActivePage(); - if (page != null) { - IConsoleView consoleView= (IConsoleView)page.findView(IConsoleConstants.ID_CONSOLE_VIEW); - if (consoleView != null) { - consoleView.warnOfContentChange(console); + List viewsToUpdate = new ArrayList<>(); + synchronized (fConsoleViews) { + for (ConsoleView view : fConsoleViews) { + IWorkbenchPartSite site = view.getSite(); + if (site == null) { + continue; } + boolean viewVisible = site.getPage().isPartVisible(view); + if (viewVisible && view.getConsole() == console) { + // No need to update the UI if the console is already on top, since + // user will already see content changes. This also prevents unnecessary + // redraws which can cause flickering. + viewsToUpdate.clear(); + break; + } + viewsToUpdate.add(view); } } + for (ConsoleView view : viewsToUpdate) { + view.warnOfContentChange(console); + } } } From 46309240d59f6c2a2a386aebeb4e8b6432ff977c Mon Sep 17 00:00:00 2001 From: Andrey Loskutov Date: Tue, 24 Feb 2026 14:52:51 +0100 Subject: [PATCH 4/8] Manual code cleanup ConsoleView - Moved final field inits to constructor - Get rid of unneeded or duplicated code, obsoleted comments - Use instanceof variables - Fix spelling mistakes - Changed all access to fActiveConsole via get/set methods --- .../ui/internal/console/ConsoleView.java | 135 +++++++++--------- 1 file changed, 71 insertions(+), 64 deletions(-) diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleView.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleView.java index 5850ed01813..7b66dc4746f 100644 --- a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleView.java +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleView.java @@ -80,17 +80,17 @@ public class ConsoleView extends PageBookView implements IConsoleView, IConsoleL /** * Whether this console is pinned. */ - private boolean fPinned = false; + private boolean fPinned; /** * Stack of consoles in MRU order */ - private final List fStack = new ArrayList<>(); + private final List fStack; /** * The console being displayed, or null if none */ - private IConsole fActiveConsole = null; + private IConsole fActiveConsole; /** * Map of consoles to dummy console parts (used to close pages) @@ -110,7 +110,7 @@ public class ConsoleView extends PageBookView implements IConsoleView, IConsoleL /** * Whether this view is active */ - private boolean fActive = false; + private boolean fActive; /** * 'In Console View' context @@ -118,10 +118,10 @@ public class ConsoleView extends PageBookView implements IConsoleView, IConsoleL private IContextActivation fActivatedContext; // actions - private PinConsoleAction fPinAction = null; - private ConsoleDropDownAction fDisplayConsoleAction = null; + private PinConsoleAction fPinAction; + private ConsoleDropDownAction fDisplayConsoleAction; - private OpenConsoleAction fOpenConsoleAction = null; + private OpenConsoleAction fOpenConsoleAction; private boolean fScrollLock; private boolean fWordWrap; @@ -132,7 +132,8 @@ public class ConsoleView extends PageBookView implements IConsoleView, IConsoleL private boolean updateConsoleIcon; private boolean isAvailable() { - return getPageBook() != null && !getPageBook().isDisposed(); + PageBook pageBook = getPageBook(); + return pageBook != null && !pageBook.isDisposed(); } @Override @@ -165,11 +166,16 @@ public IConsole getConsole() { return fActiveConsole; } + private void setConsole(IConsole recConsole) { + fActiveConsole = recConsole; + } + @Override protected void showPageRec(PageRec pageRec) { + IConsole oldActiveConsole = getConsole(); // don't show the page when pinned, unless this is the first console to be added // or its the default page - if (fActiveConsole != null && pageRec.page != getDefaultPage() && fPinned && fConsoleToPart.size() > 1) { + if (oldActiveConsole != null && pageRec.page != getDefaultPage() && fPinned && fConsoleToPart.size() > 1) { IConsole console = fPartToConsole.get(pageRec.part); if (!fStack.contains(console)) { fStack.add(console); @@ -178,25 +184,25 @@ protected void showPageRec(PageRec pageRec) { } IConsole recConsole = fPartToConsole.get(pageRec.part); - if (recConsole!=null && recConsole.equals(fActiveConsole)) { + if (recConsole != null && recConsole.equals(oldActiveConsole)) { return; } super.showPageRec(pageRec); - if (fActiveConsole != recConsole) { - if (fActive && fActiveConsole != null) { - deactivateParticipants(fActiveConsole); + if (oldActiveConsole != recConsole) { + if (fActive && oldActiveConsole != null) { + deactivateParticipants(oldActiveConsole); } if (recConsole != null) { activateParticipants(recConsole); } } - fActiveConsole = recConsole; + setConsole(recConsole); // bring active console on top of stack - if (fActiveConsole != null && !fStack.isEmpty() && !fActiveConsole.equals(fStack.get(0))) { - fStack.remove(fActiveConsole); - fStack.add(0, fActiveConsole); + if (recConsole != null && !fStack.isEmpty() && !recConsole.equals(fStack.get(0))) { + fStack.remove(recConsole); + fStack.add(0, recConsole); } updateTitle(); updateHelp(); @@ -206,8 +212,8 @@ protected void showPageRec(PageRec pageRec) { fPinAction.update(); } IPage page = getCurrentPage(); - if (page instanceof IOConsolePage) { - ((IOConsolePage) page).setWordWrap(fWordWrap); + if (page instanceof IOConsolePage consolePage) { + consolePage.setWordWrap(fWordWrap); } /* * Bug 268608: cannot invoke find/replace after opening console @@ -215,8 +221,8 @@ protected void showPageRec(PageRec pageRec) { * Global actions of TextConsolePage must be updated here, * but they are only updated on a selection change. */ - if (page instanceof TextConsolePage) { - TextConsoleViewer viewer = ((TextConsolePage) page).getViewer(); + if (page instanceof TextConsolePage consolePage) { + TextConsoleViewer viewer = consolePage.getViewer(); viewer.setSelection(viewer.getSelection()); } } @@ -268,7 +274,7 @@ protected void updateTitle() { } else { String newName = console.getName(); String oldName = getContentDescription(); - if (newName!=null && !(newName.equals(oldName))) { + if (newName != null && !(newName.equals(oldName))) { setContentDescription(console.getName()); } } @@ -349,7 +355,7 @@ public void handleException(Throwable exception) { fPartToConsole.remove(part); fConsoleToPart.remove(console); if (fPartToConsole.isEmpty()) { - fActiveConsole = null; + setConsole(null); } // update console actions @@ -377,7 +383,7 @@ protected PageRec doCreatePage(IWorkbenchPart dummyPart) { console.addPropertyChangeListener(this); // initialize page participants - IConsolePageParticipant[] consoleParticipants = ((ConsoleManager)getConsoleManager()).getPageParticipants(console); + IConsolePageParticipant[] consoleParticipants = getConsoleManager().getPageParticipants(console); final ListenerList participants = new ListenerList<>(); for (IConsolePageParticipant consoleParticipant : consoleParticipants) { participants.add(consoleParticipant); @@ -414,7 +420,7 @@ public void dispose() { site.getPage().removePartListener((IPartListener2)this); } super.dispose(); - ConsoleManager consoleManager = (ConsoleManager) ConsolePlugin.getDefault().getConsoleManager(); + ConsoleManager consoleManager = getConsoleManager(); consoleManager.removeConsoleListener(this); consoleManager.unregisterConsoleView(this); if (fDisplayConsoleAction != null) { @@ -429,16 +435,18 @@ public void dispose() { localResManager.dispose(); localResManager = null; } + fConsoleToPageParticipants.clear(); + fStack.clear(); + fConsoleToPart.clear(); + fPartToConsole.clear(); ConsolePlugin.getDefault().getPreferenceStore().removePropertyChangeListener(this); } /** - * Returns the console manager. - * * @return the console manager */ - private IConsoleManager getConsoleManager() { - return ConsolePlugin.getDefault().getConsoleManager(); + private ConsoleManager getConsoleManager() { + return (ConsoleManager) ConsolePlugin.getDefault().getConsoleManager(); } @Override @@ -502,18 +510,17 @@ public void consolesRemoved(final IConsole[] consoles) { */ public ConsoleView() { super(); + fStack = new ArrayList<>(); fConsoleToPart = new HashMap<>(); fPartToConsole = new HashMap<>(); fConsoleToPageParticipants = new HashMap<>(); - - ConsoleManager consoleManager = (ConsoleManager) ConsolePlugin.getDefault().getConsoleManager(); - consoleManager.registerConsoleView(this); + getConsoleManager().registerConsoleView(this); } protected void createActions() { fPinAction = new PinConsoleAction(this); fDisplayConsoleAction = new ConsoleDropDownAction(this); - ConsoleFactoryExtension[] extensions = ((ConsoleManager)ConsolePlugin.getDefault().getConsoleManager()).getConsoleFactoryExtensions(); + ConsoleFactoryExtension[] extensions = getConsoleManager().getConsoleFactoryExtensions(); if (extensions.length > 0) { fOpenConsoleAction = new OpenConsoleAction(this); } @@ -534,8 +541,7 @@ protected void configureToolBar(IToolBarManager mgr) { public void mouseDown(MouseEvent e) { ToolItem ti= tb.getItem(new Point(e.x, e.y)); if (ti != null) { - if (ti.getData() instanceof ActionContributionItem) { - ActionContributionItem actionContributionItem= (ActionContributionItem) ti.getData(); + if (ti.getData() instanceof ActionContributionItem actionContributionItem) { IAction action= actionContributionItem.getAction(); if (action == fOpenConsoleAction) { Event event= new Event(); @@ -554,10 +560,11 @@ public void mouseDown(MouseEvent e) { @Override public void display(IConsole console) { - if (fPinned && fActiveConsole != null) { + IConsole activeConsole = getConsole(); + if (fPinned && activeConsole != null) { return; } - if (console.equals(fActiveConsole)) { + if (console.equals(activeConsole)) { return; } ConsoleWorkbenchPart part = fConsoleToPart.get(console); @@ -590,8 +597,8 @@ protected IWorkbenchPart getBootstrapPart() { } /** - * Registers the given runnable with the display associated with this view's - * control, if any. + * Runs the given runnable with the display associated with this view's control, + * if any. * * @param r the runnable * @see org.eclipse.swt.widgets.Display#asyncExec(java.lang.Runnable) @@ -622,12 +629,12 @@ public void asyncExec(Runnable r) { public void createPartControl(Composite parent) { super.createPartControl(parent); createActions(); - IToolBarManager tbm= getViewSite().getActionBars().getToolBarManager(); - configureToolBar(tbm); + IViewSite viewSite = getViewSite(); + configureToolBar(viewSite.getActionBars().getToolBarManager()); updateForExistingConsoles(); - getViewSite().getActionBars().updateActionBars(); + viewSite.getActionBars().updateActionBars(); PlatformUI.getWorkbench().getHelpSystem().setHelp(parent, IConsoleHelpContextIds.CONSOLE_VIEW); - getViewSite().getPage().addPartListener((IPartListener2)this); + viewSite.getPage().addPartListener((IPartListener2) this); initPageSwitcher(); localResManager = new LocalResourceManager(JFaceResources.getResources(), parent); updateConsoleIcon = shouldUpdateConsoleIcon(); @@ -701,8 +708,8 @@ public void warnOfContentChange(IConsole console) { @SuppressWarnings("unchecked") @Override public T getAdapter(Class key) { - Object adpater = super.getAdapter(key); - if (adpater == null) { + Object adapter = super.getAdapter(key); + if (adapter == null) { IConsole console = getConsole(); if (console != null) { ListenerList listeners = getParticipants(console); @@ -710,15 +717,15 @@ public T getAdapter(Class key) { if (listeners != null) { for (IConsolePageParticipant iConsolePageParticipant : listeners) { IConsolePageParticipant participant = iConsolePageParticipant; - adpater = participant.getAdapter(key); - if (adpater != null) { - return (T) adpater; + adapter = participant.getAdapter(key); + if (adapter != null) { + return (T) adapter; } } } } } - return (T) adpater; + return (T) adapter; } @Override @@ -728,7 +735,7 @@ public void partActivated(IWorkbenchPartReference partRef) { IContextService contextService = getSite().getService(IContextService.class); if(contextService != null) { fActivatedContext = contextService.activateContext(IConsoleConstants.ID_CONSOLE_VIEW); - activateParticipants(fActiveConsole); + activateParticipants(getConsole()); } } } @@ -748,7 +755,7 @@ public void partDeactivated(IWorkbenchPartReference partRef) { IContextService contextService = getSite().getService(IContextService.class); if(contextService != null) { contextService.deactivateContext(fActivatedContext); - deactivateParticipants(fActiveConsole); + deactivateParticipants(getConsole()); } } } @@ -765,8 +772,8 @@ protected boolean isThisPart(IWorkbenchPartReference partRef) { if (getViewSite() != null && viewRef.getId().equals(getViewSite().getId())) { String secId = viewRef.getSecondaryId(); String mySec = null; - if (getSite() instanceof IViewSite) { - mySec = ((IViewSite)getSite()).getSecondaryId(); + if (getSite() instanceof IViewSite site) { + mySec = site.getSecondaryId(); } if (mySec == null) { return secId == null; @@ -826,8 +833,8 @@ public void setScrollLock(boolean scrollLock) { fScrollLock = scrollLock; IPage page = getCurrentPage(); - if (page instanceof IOConsolePage) { - ((IOConsolePage)page).setAutoScroll(!scrollLock); + if (page instanceof IOConsolePage consolePage) { + consolePage.setAutoScroll(!scrollLock); } } @@ -841,10 +848,10 @@ public void setWordWrap(boolean wordWrap) { fWordWrap = wordWrap; IWorkbenchPart part = getSite().getPart(); - if (part instanceof PageBookView) { - Control control = ((PageBookView) part).getCurrentPage().getControl(); - if (control instanceof StyledText) { - ((StyledText) control).setWordWrap(wordWrap); + if (part instanceof PageBookView pagebookView) { + Control control = pagebookView.getCurrentPage().getControl(); + if (control instanceof StyledText styledText) { + styledText.setWordWrap(wordWrap); } } } @@ -870,8 +877,8 @@ public void pin(IConsole console) { @Override public void setAutoScrollLock(boolean scrollLock) { IPage page = getCurrentPage(); - if (page instanceof IOConsolePage) { - ((IOConsolePage) page).setAutoScroll(!scrollLock); + if (page instanceof IOConsolePage consolePage) { + consolePage.setAutoScroll(!scrollLock); } } @@ -879,9 +886,9 @@ public void setAutoScrollLock(boolean scrollLock) { @Override public boolean getAutoScrollLock() { IPage page = getCurrentPage(); - if (page instanceof IOConsolePage) { - return !((IOConsolePage) page).isAutoScroll(); + if (page instanceof IOConsolePage consolePage) { + return !consolePage.isAutoScroll(); } return fScrollLock; } -} +} \ No newline at end of file From 4accaf991f949020ff4bbe34a741b66e71bf77cd Mon Sep 17 00:00:00 2001 From: Andrey Loskutov Date: Tue, 24 Feb 2026 18:11:30 +0100 Subject: [PATCH 5/8] Manual code cleanup for base Console classes - Moved final field inits to constructor - Added trivial toString() implementation to the AbstractConsole - Fixed possible NP in IOConsole --- .../org/eclipse/ui/console/AbstractConsole.java | 11 ++++++++--- .../src/org/eclipse/ui/console/IOConsole.java | 16 ++++++++-------- .../src/org/eclipse/ui/console/TextConsole.java | 17 ++++++++++------- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/AbstractConsole.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/AbstractConsole.java index 3539baa6a07..49f7ffcae50 100644 --- a/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/AbstractConsole.java +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/AbstractConsole.java @@ -39,17 +39,17 @@ public abstract class AbstractConsole implements IConsole { /** * Console name */ - private String fName = null; + private String fName; /** * Console image descriptor */ - private ImageDescriptor fImageDescriptor = null; + private ImageDescriptor fImageDescriptor; /** * Console type identifier */ - private String fType = null; + private String fType; /** * Used to notify this console of lifecycle methods init() @@ -316,4 +316,9 @@ public String getHelpContextId() { return null; } + @Override + public String toString() { + return "Console [" + fName + "]"; //$NON-NLS-1$ //$NON-NLS-2$ + } + } diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsole.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsole.java index c09f2111a4c..7a03913cea2 100644 --- a/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsole.java +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsole.java @@ -54,7 +54,7 @@ public class IOConsole extends TextConsole { /** * A collection of open streams connected to this console. */ - private final List openStreams = Collections.synchronizedList(new ArrayList<>()); + private final List openStreams; /** * The encoding used to for displaying console output. @@ -113,10 +113,9 @@ public IOConsole(String name, String consoleType, ImageDescriptor imageDescripto public IOConsole(String name, String consoleType, ImageDescriptor imageDescriptor, Charset charset, boolean autoLifecycle) { super(name, consoleType, imageDescriptor, autoLifecycle); this.charset = charset; - synchronized (openStreams) { - inputStream = new IOConsoleInputStream(this); - openStreams.add(inputStream); - } + openStreams = Collections.synchronizedList(new ArrayList<>()); + inputStream = new IOConsoleInputStream(this); + openStreams.add(inputStream); partitioner = new IOConsolePartitioner(this); final IDocument document = getDocument(); if (document != null) { @@ -313,7 +312,7 @@ protected void dispose() { try { closable.close(); } catch (IOException e) { - // e.printStackTrace(); + // ignore } } inputStream = null; @@ -321,8 +320,9 @@ protected void dispose() { final IDocument document = partitioner.getDocument(); partitioner.disconnect(); - document.setDocumentPartitioner(null); - + if (document != null) { + document.setDocumentPartitioner(null); + } super.dispose(); } diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/TextConsole.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/TextConsole.java index 6e3158efcfa..9c61e61dc9b 100644 --- a/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/TextConsole.java +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/TextConsole.java @@ -83,28 +83,27 @@ public abstract class TextConsole extends AbstractConsole { /** * indication that the console's partitioner is not expecting more input */ - private volatile boolean fPartitionerFinished = false; + private volatile boolean fPartitionerFinished; /** * Indication that the console's pattern matcher has finished. * (all matches have been found and all listeners notified) */ - private volatile boolean fMatcherFinished = false; + private volatile boolean fMatcherFinished; /** * indication that the console output complete property has been fired */ - private boolean fCompleteFired = false; - - private volatile boolean fConsoleAutoScrollLock = true; + private boolean fCompleteFired; + private volatile boolean fConsoleAutoScrollLock; /** * Map of client defined attributes */ - private final HashMap fAttributes = new HashMap<>(); + private final HashMap fAttributes; - private final IConsoleManager fConsoleManager = ConsolePlugin.getDefault().getConsoleManager(); + private final IConsoleManager fConsoleManager; private ScopedPreferenceStore store; private IPropertyChangeListener propListener; @@ -119,6 +118,7 @@ protected void dispose() { store.removePropertyChangeListener(propListener); } } + /** * Constructs a console with the given name, image descriptor, and lifecycle * @@ -130,6 +130,9 @@ protected void dispose() { */ public TextConsole(String name, String consoleType, ImageDescriptor imageDescriptor, boolean autoLifecycle) { super(name, consoleType, imageDescriptor, autoLifecycle); + fConsoleAutoScrollLock = true; + fAttributes = new HashMap<>(); + fConsoleManager = ConsolePlugin.getDefault().getConsoleManager(); fDocument = new ConsoleDocument(); fDocument.addPositionCategory(ConsoleHyperlinkPosition.HYPER_LINK_CATEGORY); fPatternMatcher = new ConsolePatternMatcher(this); From 174da5c0684b9ef211c88329ed22a34c3f977c57 Mon Sep 17 00:00:00 2001 From: Andrey Loskutov Date: Tue, 24 Feb 2026 18:27:23 +0100 Subject: [PATCH 6/8] ProcessConsole.computeName() refactoring and smaller fixes - Moved final field inits to constructor - Removed useless javadocs - Use instanceof variables where possible - Added "disposed" field and checks before each asyncExec() for that - Refactored computeName() to readable code -- split into multiple logical parts -- made things static which are static -- don't do anything if view is already disposed - Use imports in ProcessConsoleTests --- .../tests/console/ProcessConsoleTests.java | 17 +- .../ui/views/console/ProcessConsole.java | 271 +++++++++--------- 2 files changed, 150 insertions(+), 138 deletions(-) diff --git a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ProcessConsoleTests.java b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ProcessConsoleTests.java index 5a707283a91..4a637331115 100644 --- a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ProcessConsoleTests.java +++ b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ProcessConsoleTests.java @@ -54,6 +54,7 @@ import org.eclipse.debug.core.Launch; import org.eclipse.debug.core.model.IProcess; import org.eclipse.debug.internal.ui.DebugUIPlugin; +import org.eclipse.debug.internal.ui.views.console.ConsoleMessages; import org.eclipse.debug.internal.ui.views.console.ProcessConsole; import org.eclipse.debug.tests.DebugTestExtension; import org.eclipse.debug.tests.TestUtil; @@ -230,10 +231,10 @@ public void testInputReadJobCancel() throws Exception { final MockProcess mockProcess = new MockProcess(MockProcess.RUN_FOREVER); try { final IProcess process = mockProcess.toRuntimeProcess("testInputReadJobCancel"); - final org.eclipse.debug.internal.ui.views.console.ProcessConsole console = new org.eclipse.debug.internal.ui.views.console.ProcessConsole(process, new ConsoleColorProvider()); + final ProcessConsole console = new ProcessConsole(process, new ConsoleColorProvider()); try { console.initialize(); - final Class jobFamily = org.eclipse.debug.internal.ui.views.console.ProcessConsole.class; + final Class jobFamily = ProcessConsole.class; assertThat(Job.getJobManager().find(jobFamily)).as("check input read job started").hasSizeGreaterThan(0); Job.getJobManager().cancel(jobFamily); TestUtil.waitForJobs(testInfo.getDisplayName(), ProcessConsole.class, 0, 1000); @@ -292,7 +293,7 @@ public void processTerminationTest(ILaunchConfiguration launchConfig, boolean te final AtomicBoolean terminationSignaled = new AtomicBoolean(false); final Process mockProcess = new MockProcess(null, null, terminateBeforeConsoleInitialization ? 0 : -1); final IProcess process = DebugPlugin.newProcess(new Launch(launchConfig, ILaunchManager.RUN_MODE, null), mockProcess, testInfo.getDisplayName()); - final org.eclipse.debug.internal.ui.views.console.ProcessConsole console = new org.eclipse.debug.internal.ui.views.console.ProcessConsole(process, new ConsoleColorProvider()); + final ProcessConsole console = new ProcessConsole(process, new ConsoleColorProvider()); console.addPropertyChangeListener(event -> { if (event.getSource() == console && IConsoleConstants.P_CONSOLE_OUTPUT_COMPLETE.equals(event.getProperty())) { terminationSignaled.set(true); @@ -393,7 +394,7 @@ private IOConsole doConsoleOutputTest(byte[] testContent, Map la final IProcess process = mockProcess.toRuntimeProcess("Output Redirect", launchConfigAttributes); final String encoding = launchConfigAttributes != null ? (String) launchConfigAttributes.get(DebugPlugin.ATTR_CONSOLE_ENCODING) : null; final AtomicBoolean consoleFinished = new AtomicBoolean(false); - final org.eclipse.debug.internal.ui.views.console.ProcessConsole console = new org.eclipse.debug.internal.ui.views.console.ProcessConsole(process, new ConsoleColorProvider(), encoding); + final ProcessConsole console = new ProcessConsole(process, new ConsoleColorProvider(), encoding); console.addPropertyChangeListener((PropertyChangeEvent event) -> { if (event.getSource() == console && IConsoleConstants.P_CONSOLE_OUTPUT_COMPLETE.equals(event.getProperty())) { consoleFinished.set(true); @@ -412,7 +413,7 @@ private IOConsole doConsoleOutputTest(byte[] testContent, Map la final IDocument doc = console.getDocument(); if (outFile != null) { - String expectedPathMsg = MessageFormat.format(org.eclipse.debug.internal.ui.views.console.ConsoleMessages.ProcessConsole_1, outFile.getAbsolutePath()); + String expectedPathMsg = MessageFormat.format(ConsoleMessages.ProcessConsole_1, outFile.getAbsolutePath()); assertEquals(expectedPathMsg, doc.get(doc.getLineOffset(0), doc.getLineLength(0)), "No or wrong output of redirect file path in console."); assertThat(console.getHyperlinks()).as("check redirect file path is linked").hasSize(1); } @@ -451,7 +452,7 @@ public void testOutput() throws Exception { launchConfigAttributes.put(DebugPlugin.ATTR_CONSOLE_ENCODING, consoleEncoding); final IProcess process = mockProcess.toRuntimeProcess("simpleOutput", launchConfigAttributes); sysout.println(lines[1]); - final org.eclipse.debug.internal.ui.views.console.ProcessConsole console = new org.eclipse.debug.internal.ui.views.console.ProcessConsole(process, new ConsoleColorProvider(), consoleEncoding); + final ProcessConsole console = new ProcessConsole(process, new ConsoleColorProvider(), consoleEncoding); sysout.println(lines[2]); try { console.initialize(); @@ -507,7 +508,7 @@ public void testBinaryOutputToFile() throws Exception { launchConfigAttributes.put(IDebugUIConstants.ATTR_CAPTURE_IN_FILE, outFile.getCanonicalPath()); launchConfigAttributes.put(IDebugUIConstants.ATTR_CAPTURE_IN_CONSOLE, false); final IProcess process = mockProcess.toRuntimeProcess("redirectBinaryOutput", launchConfigAttributes); - final org.eclipse.debug.internal.ui.views.console.ProcessConsole console = new org.eclipse.debug.internal.ui.views.console.ProcessConsole(process, new ConsoleColorProvider(), consoleEncoding); + final ProcessConsole console = new ProcessConsole(process, new ConsoleColorProvider(), consoleEncoding); try { console.initialize(); @@ -561,7 +562,7 @@ public void testBinaryInputFromFile() throws Exception { launchConfigAttributes.put(IDebugUIConstants.ATTR_CAPTURE_STDIN_FILE, inFile.getCanonicalPath()); launchConfigAttributes.put(IDebugUIConstants.ATTR_CAPTURE_IN_CONSOLE, false); final IProcess process = mockProcess.toRuntimeProcess("redirectBinaryInput", launchConfigAttributes); - final org.eclipse.debug.internal.ui.views.console.ProcessConsole console = new org.eclipse.debug.internal.ui.views.console.ProcessConsole(process, new ConsoleColorProvider(), consoleEncoding); + final ProcessConsole console = new ProcessConsole(process, new ConsoleColorProvider(), consoleEncoding); try { console.initialize(); mockProcess.waitFor(TestUtil.DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS); diff --git a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java index e2fd398e51d..6c94887f078 100644 --- a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java +++ b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java @@ -116,7 +116,7 @@ public class ProcessConsole extends IOConsole implements IConsole, IDebugEventSetListener, IPropertyChangeListener { private final IProcess fProcess; - private final List fStreamListeners = new ArrayList<>(); + private final List fStreamListeners; private final IConsoleColorProvider fColorProvider; @@ -134,10 +134,12 @@ public class ProcessConsole extends IOConsole implements IConsole, IDebugEventSe private FileOutputStream fFileOutputStream; - private boolean fAllocateConsole = true; + private boolean fAllocateConsole; private String fStdInFile; private volatile boolean fStreamsClosed; + private volatile boolean disposed; + /** * Create process console with default encoding. @@ -158,6 +160,8 @@ public ProcessConsole(IProcess process, IConsoleColorProvider colorProvider) { */ public ProcessConsole(IProcess process, IConsoleColorProvider colorProvider, String encoding) { super(IInternalDebugCoreConstants.EMPTY_STRING, IDebugUIConstants.ID_PROCESS_CONSOLE_TYPE, null, encoding, true); + fStreamListeners = new ArrayList<>(); + fAllocateConsole = true; fProcess = process; fUserInput = getInputStream(); @@ -250,12 +254,11 @@ public ProcessConsole(IProcess process, IConsoleColorProvider colorProvider, Str fInput = getInputStream(); } - colorProvider.connect(fProcess, this); Color color = fColorProvider.getColor(IDebugUIConstants.ID_STANDARD_INPUT_STREAM); - if (fInput instanceof IOConsoleInputStream) { - ((IOConsoleInputStream)fInput).setColor(color); + if (fInput instanceof IOConsoleInputStream ioStream) { + ioStream.setColor(color); } IConsoleLineTracker[] lineTrackers = DebugUIPlugin.getDefault().getProcessConsoleManager().getLineTrackers(process); @@ -298,119 +301,145 @@ public IPageBookViewPage createPage(IConsoleView view) { * @return a name for this console */ protected String computeName() { - String label = null; + if (disposed) { + return getName(); + } IProcess process = getProcess(); - ILaunchConfiguration config = process.getLaunch().getLaunchConfiguration(); - - label = process.getAttribute(IProcess.ATTR_PROCESS_LABEL); + String label = process.getAttribute(IProcess.ATTR_PROCESS_LABEL); if (label == null) { - if (config == null) { + ILaunchConfiguration config = process.getLaunch().getLaunchConfiguration(); + if (config != null && DebugUITools.isPrivate(config)) { label = process.getLabel(); } else { - // check if PRIVATE config - if (DebugUITools.isPrivate(config)) { - label = process.getLabel(); - } else { - String type = null; - try { - type = config.getType().getName(); - } catch (CoreException e) { - } - StringBuilder buffer = new StringBuilder(); - buffer.append(config.getName()); - if (type != null) { - buffer.append(" ["); //$NON-NLS-1$ - buffer.append(type); - buffer.append("] "); //$NON-NLS-1$ - } + label = computeLabel(process, config); + } + } - Date launchTime = parseTimestamp(process.getAttribute(DebugPlugin.ATTR_LAUNCH_TIMESTAMP)); - Date terminateTime = parseTimestamp(process.getAttribute(DebugPlugin.ATTR_TERMINATE_TIMESTAMP)); - - String procLabel = process.getLabel(); - if (launchTime != null) { - // FIXME workaround to remove start time from process label added from jdt for - // java launches - int idx = procLabel.lastIndexOf('('); - if (idx >= 0) { - int end = procLabel.lastIndexOf(')'); - if (end > idx) { - String jdtTime = procLabel.substring(idx + 1, end); - try { - DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).parse(jdtTime); - procLabel = procLabel.substring(0, idx); - } catch (ParseException pe) { - // not a date. Label just contains parentheses - } - } - } - } + if (process.isTerminated()) { + return MessageFormat.format(ConsoleMessages.ProcessConsole_0, label); + } - DateFormat dateTimeFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM); - String elapsedFormat = "%d:%02d:%02d.%03d"; //$NON-NLS-1$ - if (terminateTime == null) { - // refresh every second: - DebugUIPlugin.getStandardDisplay().asyncExec( - () -> DebugUIPlugin.getStandardDisplay().timerExec(1000, () -> resetName(false))); - // pointless to update milliseconds: - elapsedFormat = "%d:%02d:%02d"; //$NON-NLS-1$ - } + // Process is still running, so trigger async update of console name every + // second to keep elapsed time updated. + triggerAsyncConsoleNameUpdate(); + return label; + } - IPreferenceStore store = DebugUIPlugin.getDefault().getPreferenceStore(); - - String elapsedTimeFormat = store.getString(IDebugPreferenceConstants.CONSOLE_ELAPSED_FORMAT); - String elapsedString = "";//$NON-NLS-1$ - if (!elapsedTimeFormat.equals(DebugPreferencesMessages.ConsoleDisableElapsedTime)) { - Duration elapsedTime = Duration.between( - launchTime != null ? launchTime.toInstant() : Instant.now(), - terminateTime != null ? terminateTime.toInstant() : Instant.now()); - elapsedFormat = "elapsed " + convertElapsedFormat(elapsedTimeFormat); //$NON-NLS-1$ - elapsedString = String.format(elapsedFormat, elapsedTime.toHours(), elapsedTime.toMinutesPart(), - elapsedTime.toSecondsPart(), elapsedTime.toMillisPart()); - } + private static String computeLabel(IProcess process, ILaunchConfiguration config) { + StringBuilder buffer = new StringBuilder(); + if (config != null) { + buffer.append(config.getName()); + String type = getConfigTypeName(config); + if (type != null) { + buffer.append(" ["); //$NON-NLS-1$ + buffer.append(type); + buffer.append("] "); //$NON-NLS-1$ + } + } - if (launchTime != null && terminateTime != null) { - String launchTimeStr = dateTimeFormat.format(launchTime); - // Check if process started and terminated at same day. If so only print the - // time part of termination time and omit the date part. - LocalDateTime launchDate = LocalDateTime.ofInstant(launchTime.toInstant(), - ZoneId.systemDefault()); - LocalDateTime terminateDate = LocalDateTime.ofInstant(terminateTime.toInstant(), - ZoneId.systemDefault()); - LocalDateTime launchDay = launchDate.truncatedTo(ChronoUnit.DAYS); - LocalDateTime terminateDay = terminateDate.truncatedTo(ChronoUnit.DAYS); - String terminateTimeStr; - if (launchDay.equals(terminateDay)) { - terminateTimeStr = DateFormat.getTimeInstance(DateFormat.MEDIUM).format(terminateTime); - } else { - terminateTimeStr = dateTimeFormat.format(terminateTime); - } + Date launchTime = parseTimestamp(process.getAttribute(DebugPlugin.ATTR_LAUNCH_TIMESTAMP)); + Date terminateTime = parseTimestamp(process.getAttribute(DebugPlugin.ATTR_TERMINATE_TIMESTAMP)); + DateFormat dateTimeFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM); + String elapsedString = computeElapsedTimeLabel(launchTime, terminateTime); + String procLabel = computeProcessLabel(process, launchTime); + if (launchTime != null && terminateTime != null) { + String launchTimeStr = dateTimeFormat.format(launchTime); + String terminateTimeStr = computeTerminatedTimeLabel(launchTime, terminateTime, dateTimeFormat); + buffer.append(MessageFormat.format(ConsoleMessages.ProcessConsole_commandLabel_withStartEnd, procLabel, + launchTimeStr, terminateTimeStr, elapsedString)); + } else if (launchTime != null) { + buffer.append(MessageFormat.format(ConsoleMessages.ProcessConsole_commandLabel_withStart, procLabel, + dateTimeFormat.format(launchTime), elapsedString)); + } else if (terminateTime != null) { + buffer.append(MessageFormat.format(ConsoleMessages.ProcessConsole_commandLabel_withEnd, procLabel, + dateTimeFormat.format(terminateTime))); + } else { + buffer.append(procLabel); + } + + String pid = process.getAttribute(IProcess.ATTR_PROCESS_ID); + if (pid != null && !pid.isBlank()) { + buffer.append(" [pid: "); //$NON-NLS-1$ + buffer.append(pid); + buffer.append("]"); //$NON-NLS-1$ + } + return buffer.toString(); + } - buffer.append(MessageFormat.format(ConsoleMessages.ProcessConsole_commandLabel_withStartEnd, - procLabel, launchTimeStr, terminateTimeStr, elapsedString)); - } else if (launchTime != null) { - buffer.append(MessageFormat.format(ConsoleMessages.ProcessConsole_commandLabel_withStart, - procLabel, dateTimeFormat.format(launchTime), elapsedString)); - } else if (terminateTime != null) { - buffer.append(MessageFormat.format(ConsoleMessages.ProcessConsole_commandLabel_withEnd, - procLabel, dateTimeFormat.format(terminateTime))); - } + private static String getConfigTypeName(ILaunchConfiguration config) { + String type = null; + try { + type = config.getType().getName(); + } catch (CoreException e) { + } + return type; + } - String pid = process.getAttribute(IProcess.ATTR_PROCESS_ID); - if (pid != null && !pid.isBlank()) { - buffer.append(" [pid: "); //$NON-NLS-1$ - buffer.append(pid); - buffer.append("]"); //$NON-NLS-1$ + private static String computeProcessLabel(IProcess process, Date launchTime) { + String procLabel = process.getLabel(); + if (launchTime != null) { + // FIXME workaround to remove start time from process label added from jdt for + // java launches + int idx = procLabel.lastIndexOf('('); + if (idx >= 0) { + int end = procLabel.lastIndexOf(')'); + if (end > idx) { + String jdtTime = procLabel.substring(idx + 1, end); + try { + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).parse(jdtTime); + procLabel = procLabel.substring(0, idx); + } catch (ParseException pe) { + // not a date. Label just contains parentheses } - label = buffer.toString(); } } } + return procLabel; + } - if (process.isTerminated()) { - return MessageFormat.format(ConsoleMessages.ProcessConsole_0, label); + private void triggerAsyncConsoleNameUpdate() { + if (disposed) { + return; } - return label; + DebugUIPlugin.getStandardDisplay().asyncExec(() -> { + if (disposed) { + return; + } + // refresh every second: + DebugUIPlugin.getStandardDisplay().timerExec(1000, () -> resetName(false)); + }); + } + + private static String computeElapsedTimeLabel(Date launchTime, Date terminateTime) { + String elapsedString; + IPreferenceStore store = DebugUIPlugin.getDefault().getPreferenceStore(); + String elapsedTimeFormat = store.getString(IDebugPreferenceConstants.CONSOLE_ELAPSED_FORMAT); + if (!elapsedTimeFormat.equals(DebugPreferencesMessages.ConsoleDisableElapsedTime)) { + Duration elapsedTime = Duration.between(launchTime != null ? launchTime.toInstant() : Instant.now(), + terminateTime != null ? terminateTime.toInstant() : Instant.now()); + String elapsedFormat = "elapsed " + convertElapsedFormat(elapsedTimeFormat); //$NON-NLS-1$ + elapsedString = String.format(elapsedFormat, elapsedTime.toHours(), elapsedTime.toMinutesPart(), + elapsedTime.toSecondsPart(), elapsedTime.toMillisPart()); + } else { + elapsedString = ""; //$NON-NLS-1$ + } + return elapsedString; + } + + private static String computeTerminatedTimeLabel(Date launchTime, Date terminateTime, DateFormat dateTimeFormat) { + // Check if process started and terminated at same day. If so only print the + // time part of termination time and omit the date part. + LocalDateTime launchDate = LocalDateTime.ofInstant(launchTime.toInstant(), ZoneId.systemDefault()); + LocalDateTime terminateDate = LocalDateTime.ofInstant(terminateTime.toInstant(), ZoneId.systemDefault()); + LocalDateTime launchDay = launchDate.truncatedTo(ChronoUnit.DAYS); + LocalDateTime terminateDay = terminateDate.truncatedTo(ChronoUnit.DAYS); + String terminateTimeStr; + if (launchDay.equals(terminateDay)) { + terminateTimeStr = DateFormat.getTimeInstance(DateFormat.MEDIUM).format(terminateTime); + } else { + terminateTimeStr = dateTimeFormat.format(terminateTime); + } + return terminateTimeStr; } /** @@ -433,9 +462,6 @@ private static Date parseTimestamp(String timestamp) { } } - /** - * @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(org.eclipse.jface.util.PropertyChangeEvent) - */ @Override public void propertyChange(PropertyChangeEvent evt) { String property = evt.getProperty(); @@ -516,17 +542,11 @@ public IOConsoleOutputStream getStream(String streamIdentifier) { return null; } - /** - * @see org.eclipse.debug.ui.console.IConsole#getProcess() - */ @Override public IProcess getProcess() { return fProcess; } - /** - * @see org.eclipse.ui.console.IOConsole#dispose() - */ @Override protected void dispose() { super.dispose(); @@ -536,6 +556,7 @@ protected void dispose() { JFaceResources.getFontRegistry().removeListener(this); closeStreams(); disposeStreams(); + disposed = true; } /** @@ -583,9 +604,6 @@ private synchronized void disposeStreams() { fUserInput = null; } - /** - * @see org.eclipse.ui.console.AbstractConsole#init() - */ @Override protected void init() { super.init(); @@ -616,6 +634,9 @@ protected void init() { setCarriageReturnAsControlCharacter(store.getBoolean(IDebugPreferenceConstants.CONSOLE_INTERPRET_CR_AS_CONTROL_CHARACTER)); DebugUIPlugin.getStandardDisplay().asyncExec(() -> { + if (disposed) { + return; + } setFont(JFaceResources.getFont(IDebugUIConstants.PREF_CONSOLE_FONT)); setBackground(DebugUIPlugin.getPreferenceColor(IDebugPreferenceConstants.CONSOLE_BAKGROUND_COLOR)); }); @@ -642,13 +663,18 @@ public void handleDebugEvents(DebugEvent[] events) { } /** - * resets the name of this console to the original computed name + * Compute & update console name, notify listeners if content has changed. Note, + * the {@link #computeName()} method may call this method again asynchronously + * if process is till running to update elapsed time display */ private synchronized void resetName(boolean changed) { final String newName = computeName(); String name = getName(); if (!name.equals(newName)) { DebugUIPlugin.getStandardDisplay().execute(() -> { + if (disposed) { + return; + } setName(newName); if (changed) { warnOfContentChange(); @@ -664,9 +690,6 @@ private void warnOfContentChange() { ConsolePlugin.getDefault().getConsoleManager().warnOfContentChange(DebugUITools.getConsole(fProcess)); } - /** - * @see org.eclipse.debug.ui.console.IConsole#connect(org.eclipse.debug.core.model.IStreamsProxy) - */ @Override public void connect(IStreamsProxy streamsProxy) { IPreferenceStore store = DebugUIPlugin.getDefault().getPreferenceStore(); @@ -685,9 +708,6 @@ public void connect(IStreamsProxy streamsProxy) { readJob.schedule(); } - /** - * @see org.eclipse.debug.ui.console.IConsole#connect(org.eclipse.debug.core.model.IStreamMonitor, java.lang.String) - */ @Override public void connect(IStreamMonitor streamMonitor, String streamIdentifier) { connect(streamMonitor, streamIdentifier, false); @@ -716,9 +736,6 @@ private void connect(IStreamMonitor streamMonitor, String streamIdentifier, bool } } - /** - * @see org.eclipse.debug.ui.console.IConsole#addLink(org.eclipse.debug.ui.console.IConsoleHyperlink, int, int) - */ @Override public void addLink(IConsoleHyperlink link, int offset, int length) { try { @@ -728,9 +745,6 @@ public void addLink(IConsoleHyperlink link, int offset, int length) { } } - /** - * @see org.eclipse.debug.ui.console.IConsole#addLink(org.eclipse.ui.console.IHyperlink, int, int) - */ @Override public void addLink(IHyperlink link, int offset, int length) { try { @@ -740,9 +754,6 @@ public void addLink(IHyperlink link, int offset, int length) { } } - /** - * @see org.eclipse.debug.ui.console.IConsole#getRegion(org.eclipse.debug.ui.console.IConsoleHyperlink) - */ @Override public IRegion getRegion(IConsoleHyperlink link) { return super.getRegion(link); From 280174fe59df976124648befe465844a46ef5408 Mon Sep 17 00:00:00 2001 From: Andrey Loskutov Date: Wed, 25 Feb 2026 08:42:54 +0100 Subject: [PATCH 7/8] Don't use display thread for scheduling console update tasks This fixes regression from 7e1f60c, where main thread (which owns lock on UI operations) waits for the lock on ProcessConsole to update console name but never gets it because console code is blocked internally waiting for a QueueProcessingJob being executed on UI thread. The solution is to avoid direct calls from UI to ProcessConsole.resetName(). Fixes https://github.com/eclipse-platform/eclipse.platform/issues/2460 --- .../ui/views/console/ProcessConsole.java | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java index 6c94887f078..ee6d114d87c 100644 --- a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java +++ b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java @@ -37,6 +37,10 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import org.eclipse.core.resources.IFile; @@ -140,6 +144,8 @@ public class ProcessConsole extends IOConsole implements IConsole, IDebugEventSe private volatile boolean fStreamsClosed; private volatile boolean disposed; + private final ScheduledExecutorService consoleNameUpdateExecutor; + private volatile ScheduledFuture pendingNameUpdate; /** * Create process console with default encoding. @@ -164,6 +170,11 @@ public ProcessConsole(IProcess process, IConsoleColorProvider colorProvider, Str fAllocateConsole = true; fProcess = process; fUserInput = getInputStream(); + consoleNameUpdateExecutor = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "Console name updater"); //$NON-NLS-1$ + t.setDaemon(true); + return t; + }); ILaunchConfiguration configuration = process.getLaunch().getLaunchConfiguration(); String file = null; @@ -401,13 +412,18 @@ private void triggerAsyncConsoleNameUpdate() { if (disposed) { return; } - DebugUIPlugin.getStandardDisplay().asyncExec(() -> { - if (disposed) { - return; - } - // refresh every second: - DebugUIPlugin.getStandardDisplay().timerExec(1000, () -> resetName(false)); - }); + + // refresh every second, but only if not already scheduled: + ScheduledFuture currentPending = pendingNameUpdate; + if (currentPending == null || currentPending.isDone()) { + currentPending = consoleNameUpdateExecutor.schedule(() -> { + pendingNameUpdate = null; + if (disposed) { + return; + } + resetName(false); + }, 1, TimeUnit.SECONDS); + } } private static String computeElapsedTimeLabel(Date launchTime, Date terminateTime) { @@ -550,6 +566,7 @@ public IProcess getProcess() { @Override protected void dispose() { super.dispose(); + consoleNameUpdateExecutor.shutdownNow(); fColorProvider.disconnect(); DebugPlugin.getDefault().removeDebugEventListener(this); DebugUIPlugin.getDefault().getPreferenceStore().removePropertyChangeListener(this); From 8db728662ac5a9606e2dbf5388c1c69670bd7746 Mon Sep 17 00:00:00 2001 From: Andrey Loskutov Date: Tue, 24 Feb 2026 11:16:52 +0100 Subject: [PATCH 8/8] Don't update console name every second if not needed - Added new API to allow console pages knew whether they are visible or not. - Use new IConsole API to schedule console updates only if console page is visible (top level) in the console view. This prevents multiple opened consoles in same console view run update tasks every second, even if no one can see the updated console name. - Don't update console label if the elapsed time format is set to "None" - Update console elapsed time format for running processes if preference changes - Added tests Fixes https://github.com/eclipse-platform/eclipse.platform/issues/2478 --- .../META-INF/MANIFEST.MF | 20 +- .../eclipse/debug/tests/AutomatedSuite.java | 2 + .../tests/console/ConsoleManagerTests.java | 89 +----- .../debug/tests/console/ConsoleMock.java | 108 +++++++ .../tests/console/ConsoleShowHideTests.java | 283 ++++++++++++++++++ .../tests/console/ProcessConsoleTests.java | 139 +++++++++ .../org.eclipse.debug.ui/META-INF/MANIFEST.MF | 24 +- .../ui/views/console/ProcessConsole.java | 114 +++++-- .../.settings/.api_filters | 17 ++ .../META-INF/MANIFEST.MF | 2 +- .../src/org/eclipse/ui/console/IConsole.java | 36 +++ .../eclipse/ui/console/IConsoleManager.java | 33 ++ .../ui/internal/console/ConsoleManager.java | 50 ++++ .../ui/internal/console/ConsoleView.java | 20 +- .../console/ConsoleViewConsoleFactory.java | 4 +- 15 files changed, 803 insertions(+), 138 deletions(-) create mode 100644 debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleMock.java create mode 100644 debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleShowHideTests.java create mode 100644 debug/org.eclipse.ui.console/.settings/.api_filters diff --git a/debug/org.eclipse.debug.tests/META-INF/MANIFEST.MF b/debug/org.eclipse.debug.tests/META-INF/MANIFEST.MF index 9423ff5b0a1..7016e231f40 100644 --- a/debug/org.eclipse.debug.tests/META-INF/MANIFEST.MF +++ b/debug/org.eclipse.debug.tests/META-INF/MANIFEST.MF @@ -2,17 +2,17 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.debug.tests;singleton:=true -Bundle-Version: 3.15.300.qualifier +Bundle-Version: 3.15.400.qualifier Bundle-Localization: plugin -Require-Bundle: org.eclipse.ui;bundle-version="[3.6.0,4.0.0)", - org.eclipse.core.runtime;bundle-version="[3.29.0,4.0.0)", - org.eclipse.debug.ui;bundle-version="[3.10.0,4.0.0)", - org.eclipse.core.filesystem;bundle-version="[1.3.0,2.0.0)", - org.eclipse.test.performance;bundle-version="3.6.0", - org.eclipse.ui.externaltools;bundle-version="[3.3.0,4.0.0)", - org.eclipse.ui.console;bundle-version="[3.7.0,4.0.0)", - org.eclipse.jface.text;bundle-version="[3.5.0,4.0.0)", - org.eclipse.ui.workbench.texteditor;bundle-version="[3.15.100,4.0.0)" +Require-Bundle: org.eclipse.ui;bundle-version="[3.208.0,4.0.0)", + org.eclipse.core.runtime;bundle-version="[3.34.0,4.0.0)", + org.eclipse.debug.ui;bundle-version="[3.21.0,4.0.0)", + org.eclipse.core.filesystem;bundle-version="[1.11.0,2.0.0)", + org.eclipse.test.performance;bundle-version="[3.21.0,4.0.0)", + org.eclipse.ui.externaltools;bundle-version="[3.7.0,4.0.0)", + org.eclipse.ui.console;bundle-version="[3.17.0,4.0.0)", + org.eclipse.jface.text;bundle-version="[3.30.0,4.0.0)", + org.eclipse.ui.workbench.texteditor;bundle-version="[3.20.0,4.0.0)" Bundle-ActivationPolicy: lazy Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-Vendor: %providerName diff --git a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/AutomatedSuite.java b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/AutomatedSuite.java index df91f40b224..52be8447f02 100644 --- a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/AutomatedSuite.java +++ b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/AutomatedSuite.java @@ -19,6 +19,7 @@ import org.eclipse.debug.tests.breakpoint.BreakpointTests; import org.eclipse.debug.tests.breakpoint.SerialExecutorTest; import org.eclipse.debug.tests.console.ConsoleDocumentAdapterTests; +import org.eclipse.debug.tests.console.ConsoleShowHideTests; import org.eclipse.debug.tests.console.ConsoleManagerTests; import org.eclipse.debug.tests.console.ConsoleTests; import org.eclipse.debug.tests.console.FileLinkTests; @@ -117,6 +118,7 @@ // Console view ConsoleDocumentAdapterTests.class, // + ConsoleShowHideTests.class, // ConsoleManagerTests.class, // ConsoleTests.class, // IOConsoleTests.class, // diff --git a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleManagerTests.java b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleManagerTests.java index 61e7dd4c76f..b241684aa61 100644 --- a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleManagerTests.java +++ b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleManagerTests.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2017 Andrey Loskutov and others. + * Copyright (c) 2016, 2026 Andrey Loskutov and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -21,14 +21,9 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.debug.tests.DebugTestExtension; import org.eclipse.debug.tests.TestUtil; -import org.eclipse.jface.resource.ImageDescriptor; -import org.eclipse.jface.util.IPropertyChangeListener; -import org.eclipse.swt.SWT; -import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IViewPart; import org.eclipse.ui.IWorkbenchPage; @@ -36,10 +31,7 @@ import org.eclipse.ui.console.ConsolePlugin; import org.eclipse.ui.console.IConsole; import org.eclipse.ui.console.IConsoleManager; -import org.eclipse.ui.console.IConsoleView; import org.eclipse.ui.internal.console.ConsoleManager; -import org.eclipse.ui.part.IPageBookViewPage; -import org.eclipse.ui.part.MessagePage; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -47,7 +39,8 @@ import org.junit.jupiter.api.extension.ExtendWith; /** - * Tests console manager + * Tests console manager behavior when multiple consoles are shown at the same + * time. */ @ExtendWith(DebugTestExtension.class) public class ConsoleManagerTests { @@ -58,6 +51,8 @@ public class ConsoleManagerTests { private CountDownLatch latch; private ConsoleMock[] consoles; ConsoleMock firstConsole; + private IViewPart consoleView; + private IWorkbenchPage activePage; @BeforeEach public void setUp(TestInfo testInfo) throws Exception { @@ -66,7 +61,7 @@ public void setUp(TestInfo testInfo) throws Exception { latch = new CountDownLatch(count); executorService = Executors.newFixedThreadPool(count); manager = ConsolePlugin.getDefault().getConsoleManager(); - IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); TestUtil.processUIEvents(100); consoles = new ConsoleMock[count]; for (int i = 0; i < count; i++) { @@ -76,7 +71,7 @@ public void setUp(TestInfo testInfo) throws Exception { // register consoles (this does *not* show anything) manager.addConsoles(consoles); - IViewPart consoleView = activePage.showView("org.eclipse.ui.console.ConsoleView"); //$NON-NLS-1$ + consoleView = activePage.showView("org.eclipse.ui.console.ConsoleView"); activePage.activate(consoleView); TestUtil.processUIEvents(100); @@ -96,6 +91,7 @@ public void tearDown() throws Exception { executorService.shutdownNow(); manager.removeConsoles(consoles); manager.removeConsoles(new ConsoleMock[] { firstConsole }); + activePage.hideView(consoleView); TestUtil.processUIEvents(100); } @@ -168,73 +164,4 @@ private void showConsole(final ConsoleMock console, String testName) { }); } - /** - * Dummy console page showing mock number and counting the numbers its - * control was shown in the console view. - */ - static final class ConsoleMock implements IConsole { - MessagePage page; - final AtomicInteger showCalled; - final int number; - final static AtomicInteger allShownConsoles = new AtomicInteger(); - - public ConsoleMock(int number) { - this.number = number; - showCalled = new AtomicInteger(); - } - - @Override - public void removePropertyChangeListener(IPropertyChangeListener listener) { - } - - @Override - public String getType() { - return null; - } - - @Override - public String getName() { - return toString(); - } - - @Override - public ImageDescriptor getImageDescriptor() { - return null; - } - - @Override - public void addPropertyChangeListener(IPropertyChangeListener listener) { - } - - /** - * Just a page showing the mock console name - */ - @Override - public IPageBookViewPage createPage(IConsoleView view) { - page = new MessagePage() { - @Override - public void createControl(Composite parent) { - super.createControl(parent); - // This listener is get called if the page is really shown - // in the console view - getControl().addListener(SWT.Show, event -> { - int count = showCalled.incrementAndGet(); - if (count == 1) { - count = allShownConsoles.incrementAndGet(); - System.out.println("Shown: " + ConsoleMock.this + ", overall: " + count); //$NON-NLS-1$ //$NON-NLS-2$ - } - }); - } - - }; - page.setMessage(toString()); - return page; - } - - @Override - public String toString() { - return "mock #" + number; //$NON-NLS-1$ - } - } - } diff --git a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleMock.java b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleMock.java new file mode 100644 index 00000000000..b5b866504a1 --- /dev/null +++ b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleMock.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * Copyright (c) 2026 Andrey Loskutov 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: + * Andrey Loskutov - initial API and implementation + *******************************************************************************/ +package org.eclipse.debug.tests.console; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.console.IConsole; +import org.eclipse.ui.console.IConsoleView; +import org.eclipse.ui.part.IPageBookViewPage; +import org.eclipse.ui.part.MessagePage; + +/** + * Dummy console page showing mock number and counting the numbers its + * control was shown in the console view. + */ +final class ConsoleMock implements IConsole { + MessagePage page; + final AtomicInteger showCalled; + final AtomicInteger pageShownCalled; + final AtomicInteger pageHiddenCalled; + final int number; + final static AtomicInteger allShownConsoles = new AtomicInteger(); + + public ConsoleMock(int number) { + this.number = number; + showCalled = new AtomicInteger(); + pageShownCalled = new AtomicInteger(); + pageHiddenCalled = new AtomicInteger(); + } + + @Override + public void pageShown() { + pageShownCalled.incrementAndGet(); + } + + @Override + public void pageHidden() { + pageHiddenCalled.incrementAndGet(); + } + + @Override + public void removePropertyChangeListener(IPropertyChangeListener listener) { + } + + @Override + public String getType() { + return null; + } + + @Override + public String getName() { + return toString(); + } + + @Override + public ImageDescriptor getImageDescriptor() { + return null; + } + + @Override + public void addPropertyChangeListener(IPropertyChangeListener listener) { + } + + /** + * Just a page showing the mock console name + */ + @Override + public IPageBookViewPage createPage(IConsoleView view) { + page = new MessagePage() { + @Override + public void createControl(Composite parent) { + super.createControl(parent); + // This listener is get called if the page is really shown + // in the console view + getControl().addListener(SWT.Show, event -> { + int count = showCalled.incrementAndGet(); + if (count == 1) { + count = allShownConsoles.incrementAndGet(); + System.out.println("Shown: " + ConsoleMock.this + ", overall: " + count); //$NON-NLS-1$ //$NON-NLS-2$ + } + }); + } + + }; + page.setMessage(toString()); + return page; + } + + @Override + public String toString() { + return "mock #" + number; //$NON-NLS-1$ + } +} \ No newline at end of file diff --git a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleShowHideTests.java b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleShowHideTests.java new file mode 100644 index 00000000000..e60bd9a95b6 --- /dev/null +++ b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleShowHideTests.java @@ -0,0 +1,283 @@ +/******************************************************************************* + * Copyright (c) 2026 Andrey Loskutov 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: + * Andrey Loskutov - initial API and implementation + *******************************************************************************/ +package org.eclipse.debug.tests.console; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.eclipse.debug.tests.DebugTestExtension; +import org.eclipse.debug.tests.TestUtil; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.console.ConsolePlugin; +import org.eclipse.ui.console.IConsole; +import org.eclipse.ui.console.IConsoleManager; +import org.eclipse.ui.internal.console.ConsoleManager; +import org.eclipse.ui.internal.console.ConsoleView; +import org.eclipse.ui.internal.console.ConsoleViewConsoleFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Tests console manager's show/hide behavior when multiple consoles are shown + * in the console view. + */ +@ExtendWith(DebugTestExtension.class) +public class ConsoleShowHideTests { + + private IConsoleManager manager; + private int count; + private ConsoleMock[] consoles; + private ConsoleView consoleView; + private ConsoleView consoleView2; + private IWorkbenchPage activePage; + + @BeforeEach + public void setUp() throws Exception { + assertNotNull(Display.getCurrent(), "Must run in UI thread, but was in: " + Thread.currentThread().getName()); + count = 3; + manager = ConsolePlugin.getDefault().getConsoleManager(); + activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + TestUtil.processUIEvents(100); + consoles = new ConsoleMock[count]; + for (int i = 0; i < count; i++) { + consoles[i] = new ConsoleMock(i + 1); + } + consoleView = (ConsoleView) activePage.showView("org.eclipse.ui.console.ConsoleView"); + activePage.activate(consoleView); + TestUtil.processUIEvents(100); + } + + @AfterEach + public void tearDown() throws Exception { + manager.removeConsoles(consoles); + activePage.hideView(consoleView); + if (consoleView2 != null) { + activePage.hideView(consoleView2); + } + TestUtil.processUIEvents(100); + } + + /** + * The test triggers {@link #count} sequential calls to the + * {@link IConsoleManager#showConsoleView(IConsole)} and checks if + * {@link IConsole#pageShown()} and {@link IConsole#pageHidden()} were + * properly called for each console. + */ + @Test + public void testShowHideConsoles(TestInfo testInfo) throws Exception { + // First time adding & showing consoles should trigger pageShown for + // each console + for (ConsoleMock console : consoles) { + addConsole(console, testInfo.getDisplayName()); + assertEquals(1, console.pageShownCalled.get(), console + " was shown unexpected number of times: " + console.pageShownCalled.get()); + } + + // Page hidden should be called for all but the last console + for (int i = 0; i < consoles.length; i++) { + ConsoleMock console = consoles[i]; + assertEquals(1, console.pageShownCalled.get(), console + " was shown unexpected number of times: " + console.pageShownCalled.get()); + if (i == consoles.length - 1) { + // last console should not be hidden + assertEquals(0, console.pageHiddenCalled.get(), console + " was hidden unexpected number of times: " + console.pageHiddenCalled.get()); + } else { + assertEquals(1, console.pageHiddenCalled.get(), console + " was hidden unexpected number of times: " + console.pageHiddenCalled.get()); + } + } + + clearConsoleCounts(); + + // Second time showing consoles should trigger pageShown for each + // console + for (ConsoleMock console : consoles) { + showConsole(console, testInfo.getDisplayName()); + assertEquals(1, console.showCalled.get()); + assertEquals(1, console.pageShownCalled.get(), console + " was shown unexpected number of times: " + console.pageShownCalled.get()); + } + + // Page hidden should be called for all consoles + for (ConsoleMock console : consoles) { + assertEquals(1, console.showCalled.get()); + assertEquals(1, console.pageShownCalled.get(), console + " was shown unexpected number of times: " + console.pageShownCalled.get()); + assertEquals(1, console.pageHiddenCalled.get(), console + " was hidden unexpected number of times: " + console.pageHiddenCalled.get()); + } + + clearConsoleCounts(); + + // Last console should be shown now. + // Close consoles one by one in reverse order. + for (int i = consoles.length - 1; i >= 0; i--) { + ConsoleMock console = consoles[i]; + removeConsole(console, testInfo.getDisplayName()); + if (i == consoles.length - 1) { + // last console should not be shown again + assertEquals(0, console.pageShownCalled.get(), console + " was shown unexpected number of times: " + console.pageShownCalled.get()); + } else { + // Other consoles should be shown again when the previous one is + // closed + assertEquals(1, console.pageShownCalled.get(), console + " was shown unexpected number of times: " + console.pageShownCalled.get()); + } + assertEquals(1, console.pageHiddenCalled.get(), console + " was hidden unexpected number of times: " + console.pageHiddenCalled.get()); + } + } + + /** + * The test triggers {@link #count} sequential calls to the + * {@link IConsoleManager#showConsoleView(IConsole)} and checks if + * {@link IConsole#pageShown()} and {@link IConsole#pageHidden()} were + * properly called for each console, if there are two console views showing + * the same consoles. + */ + @Test + public void testShowHideConsolesWithTwoViews(TestInfo testInfo) throws Exception { + assertSame(consoleView, activePage.getActivePart()); + + ConsoleViewConsoleFactoryForTest factory = new ConsoleViewConsoleFactoryForTest(); + factory.setConsoleView(consoleView); + factory.openConsole(); + TestUtil.processUIEvents(100); + + // Second console view should be opened and active + consoleView2 = (ConsoleView) activePage.getActivePart(); + assertNotSame(consoleView, consoleView2); + + // First time adding & showing consoles should trigger pageShown for + // each console (on creation) + for (ConsoleMock console : consoles) { + addConsole(console, testInfo.getDisplayName()); + assertEquals(1, console.pageShownCalled.get(), console + " was shown unexpected number of times: " + console.pageShownCalled.get()); + } + + // Page hidden should be called for all but the last console + for (int i = 0; i < consoles.length; i++) { + ConsoleMock console = consoles[i]; + assertEquals(1, console.pageShownCalled.get(), console + " was shown unexpected number of times: " + console.pageShownCalled.get()); + if (i == consoles.length - 1) { + // last console should not be hidden + assertEquals(0, console.pageHiddenCalled.get(), console + " was hidden unexpected number of times: " + console.pageHiddenCalled.get()); + } else { + assertEquals(1, console.pageHiddenCalled.get(), console + " was hidden unexpected number of times: " + console.pageHiddenCalled.get()); + } + } + + // Activate first console view again + activePage.activate(consoleView); + TestUtil.processUIEvents(100); + assertSame(consoleView, activePage.getActivePart()); + + clearConsoleCounts(); + + // Second time showing consoles should trigger pageShown for each + // console + for (ConsoleMock console : consoles) { + showConsole(console, testInfo.getDisplayName()); + assertEquals(1, console.showCalled.get()); + assertEquals(1, console.pageShownCalled.get(), console + " was shown unexpected number of times: " + console.pageShownCalled.get()); + } + + // Page hidden should be called for all consoles (last one should be + // hidden now, as it was shown on top) + for (ConsoleMock console : consoles) { + assertEquals(1, console.showCalled.get()); + assertEquals(1, console.pageShownCalled.get(), console + " was shown unexpected number of times: " + console.pageShownCalled.get()); + assertEquals(1, console.pageHiddenCalled.get(), console + " was hidden unexpected number of times: " + console.pageHiddenCalled.get()); + } + + // Activate second console view again + activePage.activate(consoleView2); + TestUtil.processUIEvents(100); + assertSame(consoleView2, activePage.getActivePart()); + + clearConsoleCounts(); + + // Second time showing consoles should trigger pageShown for each + // console (last one should be shown on top again) + for (ConsoleMock console : consoles) { + showConsole(console, testInfo.getDisplayName()); + assertEquals(1, console.showCalled.get()); + assertEquals(1, console.pageShownCalled.get(), console + " was shown unexpected number of times: " + console.pageShownCalled.get()); + } + + // Page hidden should be called for all consoles (last one should be + // hidden now, as it was shown on top) + for (ConsoleMock console : consoles) { + assertEquals(1, console.showCalled.get()); + assertEquals(1, console.pageShownCalled.get(), console + " was shown unexpected number of times: " + console.pageShownCalled.get()); + assertEquals(1, console.pageHiddenCalled.get(), console + " was hidden unexpected number of times: " + console.pageHiddenCalled.get()); + } + + clearConsoleCounts(); + + // Last console should be shown now. + // Close consoles one by one in reverse order. + for (int i = consoles.length - 1; i >= 0; i--) { + ConsoleMock console = consoles[i]; + removeConsole(console, testInfo.getDisplayName()); + if (i == consoles.length - 1) { + // last console should not be shown again + assertEquals(0, console.pageShownCalled.get(), console + " was shown unexpected number of times: " + console.pageShownCalled.get()); + } else { + // Other consoles should be shown again when the previous one is + // closed + assertEquals(1, console.pageShownCalled.get(), console + " was shown unexpected number of times: " + console.pageShownCalled.get()); + } + assertEquals(1, console.pageHiddenCalled.get(), console + " was hidden unexpected number of times: " + console.pageHiddenCalled.get()); + } + } + + private void clearConsoleCounts() { + for (ConsoleMock console : consoles) { + console.showCalled.set(0); + console.pageShownCalled.set(0); + console.pageHiddenCalled.set(0); + } + } + + private void addConsole(final ConsoleMock console, String testName) { + System.out.println("Requesting to add: " + console); //$NON-NLS-1$ + manager.addConsoles(new IConsole[] { console }); + TestUtil.waitForJobs(testName, ConsoleManager.CONSOLE_JOB_FAMILY, 200, 5000); + } + + private void removeConsole(final ConsoleMock console, String testName) { + System.out.println("Requesting to remove: " + console); //$NON-NLS-1$ + manager.removeConsoles(new IConsole[] { console }); + TestUtil.waitForJobs(testName, ConsoleManager.CONSOLE_JOB_FAMILY, 200, 5000); + } + + private void showConsole(final ConsoleMock console, String testName) { + System.out.println("Requesting to show: " + console); //$NON-NLS-1$ + manager.showConsoleView(console); + TestUtil.waitForJobs(testName, ConsoleManager.CONSOLE_JOB_FAMILY, 200, 5000); + } + + class ConsoleViewConsoleFactoryForTest extends ConsoleViewConsoleFactory { + + @Override + protected boolean handleAutoPin() { + // Override super to avoid dialogs about pinning consoles in the + // view, which would require user interaction and thus fail the + // test. + // No pinning required + return false; + } + } + +} diff --git a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ProcessConsoleTests.java b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ProcessConsoleTests.java index 4a637331115..69e3a9e542d 100644 --- a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ProcessConsoleTests.java +++ b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ProcessConsoleTests.java @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.debug.tests.TestUtil.waitWhile; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; @@ -64,6 +65,9 @@ import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.ui.IPerspectiveDescriptor; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PlatformUI; import org.eclipse.ui.console.ConsolePlugin; import org.eclipse.ui.console.IConsole; import org.eclipse.ui.console.IConsoleConstants; @@ -71,6 +75,7 @@ import org.eclipse.ui.console.IOConsole; import org.eclipse.ui.console.IOConsoleInputStream; import org.eclipse.ui.internal.console.ConsoleManager; +import org.eclipse.ui.internal.console.ConsoleView; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -576,4 +581,138 @@ public void testBinaryInputFromFile() throws Exception { byte[] receivedInput = mockProcess.getReceivedInput(); assertThat(receivedInput).as("received input").isEqualTo(input); } + + /** + * Test that console name updates (elapsed time) only happen for visible + * consoles. Hidden consoles should not update their name. When a hidden + * console is brought to front, it should start updating. When the console + * view is hidden/minimized, no console should update its name. When the + * view is shown again, the visible console should resume updates. + */ + @Test + public void testConsoleNameUpdateForVisibleAndHiddenConsoles() throws Exception { + final IConsoleManager consoleManager = ConsolePlugin.getDefault().getConsoleManager(); + final IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + + // Make sure we only see exact one console view during the test, + // otherwise it may interfere with visibility and name update checks. + IPerspectiveDescriptor currentPerspective = activePage.getPerspective(); + activePage.closeAllPerspectives(false, false); + TestUtil.processUIEvents(); + activePage.setPerspective(currentPerspective); + TestUtil.processUIEvents(); + + // Create two silent mock processes (no output, run forever) + final MockProcess mockProcess1 = new MockProcess(MockProcess.RUN_FOREVER); + final MockProcess mockProcess2 = new MockProcess(MockProcess.RUN_FOREVER); + try { + final IProcess process1 = mockProcess1.toRuntimeProcess("Process1"); + final IProcess process2 = mockProcess2.toRuntimeProcess("Process2"); + process1.setAttribute(DebugPlugin.ATTR_LAUNCH_TIMESTAMP, Long.toString(System.currentTimeMillis())); + process2.setAttribute(DebugPlugin.ATTR_LAUNCH_TIMESTAMP, Long.toString(System.currentTimeMillis())); + final ProcessConsole console1 = new ProcessConsole(process1, new ConsoleColorProvider()); + final ProcessConsole console2 = new ProcessConsole(process2, new ConsoleColorProvider()); + ConsoleView consoleView = (ConsoleView) activePage.showView(IConsoleConstants.ID_CONSOLE_VIEW); + try { + // Open console view and add both consoles + activePage.activate(consoleView); + TestUtil.processUIEvents(); + + consoleManager.addConsoles(new IConsole[] { console1, console2 }); + + // Display console2 (visible) - console1 is hidden + consoleView.display(console2); + TestUtil.waitForJobs(testInfo.getDisplayName(), ConsoleManager.CONSOLE_JOB_FAMILY, 200, 10000); + + // Record initial names + String console2NameBefore = console2.getName(); + String console1NameBefore = console1.getName(); + + // Wait >1 second for the elapsed time update to trigger + TestUtil.processUIEvents(1500); + + // Visible console (console2) should have updated name + String console2NameAfter = console2.getName(); + assertNotEquals(console2NameBefore, console2NameAfter, "Visible console name should have been updated (elapsed time changed)"); + + // Hidden console (console1) should NOT have updated name + String console1NameAfter = console1.getName(); + assertEquals(console1NameBefore, console1NameAfter, "Hidden console name should not be updated"); + + // Bring hidden console1 to front (visible) - console2 becomes + // hidden + consoleView.display(console1); + TestUtil.processUIEvents(200); + + // Record names after switch + String console1NameAfterSwitch = console1.getName(); + String console2NameAfterSwitch = console2.getName(); + + // Wait >1 second for the elapsed time update + TestUtil.processUIEvents(2000); + + // Now console1 (visible) should update + String console1NameAfterWait = console1.getName(); + assertNotEquals(console1NameAfterSwitch, console1NameAfterWait, "Console brought to front should start updating its name"); + + // console2 (now hidden) should stop updating + String console2NameAfterWait = console2.getName(); + assertEquals(console2NameAfterSwitch, console2NameAfterWait, "Console moved to background should stop updating its name"); + + // Minimize the console view - neither console should update + activePage.setPartState(activePage.getReference(consoleView), IWorkbenchPage.STATE_MINIMIZED); + TestUtil.processUIEvents(200); + + // Record names after minimizing + String console1NameBeforeHide = console1.getName(); + String console2NameBeforeHide = console2.getName(); + + // Wait >1 second + TestUtil.processUIEvents(2000); + + // Neither console should update when view is minimized + assertEquals(console1NameBeforeHide, console1.getName(), "Console name should not update when console view is minimized"); + assertEquals(console2NameBeforeHide, console2.getName(), "Console name should not update when console view is minimized"); + + // Restore the console view - console1 should resume + // updating, console2 should still not update because it's + // hidden in the view + activePage.setPartState(activePage.getReference(consoleView), IWorkbenchPage.STATE_RESTORED); + activePage.activate(consoleView); + TestUtil.processUIEvents(200); + + String console1NameAfterReshow = console1.getName(); + String console2NameAfterReshow = console2.getName(); + + // Wait >1 second for update + TestUtil.processUIEvents(2000); + + // Visible console should resume updating + assertNotEquals(console1NameAfterReshow, console1.getName(), "Visible console should resume name updates after view is shown again"); + assertEquals(console2NameAfterReshow, console2.getName(), "Hidden console name should not update after view is restored"); + + console1NameAfterReshow = console1.getName(); + console2NameAfterReshow = console2.getName(); + + activePage.hideView(consoleView); + + // Wait >1 second for update + TestUtil.processUIEvents(2000); + assertEquals(console1NameAfterReshow, console1.getName(), "Console name should not update when console view is minimized"); + assertEquals(console2NameAfterReshow, console2.getName(), "Console name should not update when console view is minimized"); + + mockProcess1.destroy(); + mockProcess2.destroy(); + } finally { + activePage.hideView(consoleView); + consoleManager.removeConsoles(List.of(console1, console2).toArray(new IConsole[0])); + waitForConsoleRelatedJobs(); + console1.destroy(); + console2.destroy(); + } + } finally { + mockProcess1.destroy(); + mockProcess2.destroy(); + } + } } diff --git a/debug/org.eclipse.debug.ui/META-INF/MANIFEST.MF b/debug/org.eclipse.debug.ui/META-INF/MANIFEST.MF index b4cbecb0922..8c8a0b29b7f 100644 --- a/debug/org.eclipse.debug.ui/META-INF/MANIFEST.MF +++ b/debug/org.eclipse.debug.ui/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.debug.ui; singleton:=true -Bundle-Version: 3.20.0.qualifier +Bundle-Version: 3.21.0.qualifier Bundle-Activator: org.eclipse.debug.internal.ui.DebugUIPlugin Bundle-Vendor: %providerName Bundle-Localization: plugin @@ -79,18 +79,18 @@ Export-Package: org.eclipse.debug.internal.ui; org.eclipse.debug.ui.memory, org.eclipse.debug.ui.sourcelookup, org.eclipse.debug.ui.stringsubstitution -Require-Bundle: org.eclipse.core.variables;bundle-version="[3.2.800,4.0.0)", +Require-Bundle: org.eclipse.core.variables;bundle-version="[3.6.0,4.0.0)", org.eclipse.ui;bundle-version="[3.208.0,4.0.0)", - org.eclipse.ui.console;bundle-version="[3.13.0,4.0.0)", - org.eclipse.help;bundle-version="[3.4.0,4.0.0)", - org.eclipse.debug.core;bundle-version="[3.9.0,4.0.0)";visibility:=reexport, - org.eclipse.jface.text;bundle-version="[3.5.0,4.0.0)", - org.eclipse.ui.workbench.texteditor;bundle-version="[3.5.0,4.0.0)", - org.eclipse.ui.ide;bundle-version="[3.5.0,4.0.0)", - org.eclipse.ui.editors;bundle-version="[3.5.0,4.0.0)", - org.eclipse.core.runtime;bundle-version="[3.29.0,4.0.0)", - org.eclipse.core.filesystem;bundle-version="[1.2.0,2.0.0)", - org.eclipse.e4.ui.services;bundle-version="[1.3.700,2.0.0)" + org.eclipse.ui.console;bundle-version="[3.17.0,4.0.0)", + org.eclipse.help;bundle-version="[3.11.0,4.0.0)", + org.eclipse.debug.core;bundle-version="[3.23.0,4.0.0)";visibility:=reexport, + org.eclipse.jface.text;bundle-version="[3.30.0,4.0.0)", + org.eclipse.ui.workbench.texteditor;bundle-version="[3.20.0,4.0.0)", + org.eclipse.ui.ide;bundle-version="[3.23.0,4.0.0)", + org.eclipse.ui.editors;bundle-version="[3.21.0,4.0.0)", + org.eclipse.core.runtime;bundle-version="[3.34.0,4.0.0)", + org.eclipse.core.filesystem;bundle-version="[1.11.0,2.0.0)", + org.eclipse.e4.ui.services;bundle-version="[1.6.0,2.0.0)" Bundle-ActivationPolicy: lazy Import-Package: org.eclipse.ui.forms.widgets Bundle-RequiredExecutionEnvironment: JavaSE-21 diff --git a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java index ee6d114d87c..7b11e31a96b 100644 --- a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java +++ b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java @@ -37,10 +37,9 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Pattern; import org.eclipse.core.resources.IFile; @@ -143,9 +142,11 @@ public class ProcessConsole extends IOConsole implements IConsole, IDebugEventSe private volatile boolean fStreamsClosed; private volatile boolean disposed; + private volatile boolean isVisible; + private volatile boolean isPeriodicNameUpdateRequired; - private final ScheduledExecutorService consoleNameUpdateExecutor; - private volatile ScheduledFuture pendingNameUpdate; + private final ExecutorService consoleNameUpdateExecutor; + private final AtomicBoolean updateRunning; /** * Create process console with default encoding. @@ -166,11 +167,12 @@ public ProcessConsole(IProcess process, IConsoleColorProvider colorProvider) { */ public ProcessConsole(IProcess process, IConsoleColorProvider colorProvider, String encoding) { super(IInternalDebugCoreConstants.EMPTY_STRING, IDebugUIConstants.ID_PROCESS_CONSOLE_TYPE, null, encoding, true); + updateRunning = new AtomicBoolean(); fStreamListeners = new ArrayList<>(); fAllocateConsole = true; fProcess = process; fUserInput = getInputStream(); - consoleNameUpdateExecutor = Executors.newSingleThreadScheduledExecutor(r -> { + consoleNameUpdateExecutor = Executors.newSingleThreadExecutor(r -> { Thread t = new Thread(r, "Console name updater"); //$NON-NLS-1$ t.setDaemon(true); return t; @@ -309,9 +311,14 @@ public IPageBookViewPage createPage(IConsoleView view) { /** * Computes and returns the current name of this console. * + * @param triggerAsyncUpdate if true and process is still running, + * triggers asynchronous update of console name every + * second to update elapsed time display in console + * name + * * @return a name for this console */ - protected String computeName() { + protected String computeName(boolean triggerAsyncUpdate) { if (disposed) { return getName(); } @@ -332,7 +339,9 @@ protected String computeName() { // Process is still running, so trigger async update of console name every // second to keep elapsed time updated. - triggerAsyncConsoleNameUpdate(); + if (triggerAsyncUpdate) { + triggerAsyncConsoleNameUpdate(label); + } return label; } @@ -408,29 +417,45 @@ private static String computeProcessLabel(IProcess process, Date launchTime) { return procLabel; } - private void triggerAsyncConsoleNameUpdate() { - if (disposed) { + private void triggerAsyncConsoleNameUpdate(String newName) { + if (!canUpdateConsoleName()) { return; } + // update console name immediately to show elapsed time right after launch and + // then start async update every second + showName(false, newName); - // refresh every second, but only if not already scheduled: - ScheduledFuture currentPending = pendingNameUpdate; - if (currentPending == null || currentPending.isDone()) { - currentPending = consoleNameUpdateExecutor.schedule(() -> { - pendingNameUpdate = null; - if (disposed) { - return; - } - resetName(false); - }, 1, TimeUnit.SECONDS); + if (!updateRunning.compareAndSet(false, true)) { + return; } + consoleNameUpdateExecutor.submit(() -> { + try { + while (!disposed) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // ignore and continue to update name + } + if (!canUpdateConsoleName()) { + break; + } + showName(false, computeName(false)); + } + } finally { + updateRunning.set(false); + } + }); + } + + private boolean canUpdateConsoleName() { + return !disposed && isVisible && isPeriodicNameUpdateRequired && !fProcess.isTerminated(); } private static String computeElapsedTimeLabel(Date launchTime, Date terminateTime) { String elapsedString; IPreferenceStore store = DebugUIPlugin.getDefault().getPreferenceStore(); String elapsedTimeFormat = store.getString(IDebugPreferenceConstants.CONSOLE_ELAPSED_FORMAT); - if (!elapsedTimeFormat.equals(DebugPreferencesMessages.ConsoleDisableElapsedTime)) { + if (!isElapsedTimeDisabled(elapsedTimeFormat)) { Duration elapsedTime = Duration.between(launchTime != null ? launchTime.toInstant() : Instant.now(), terminateTime != null ? terminateTime.toInstant() : Instant.now()); String elapsedFormat = "elapsed " + convertElapsedFormat(elapsedTimeFormat); //$NON-NLS-1$ @@ -442,6 +467,10 @@ private static String computeElapsedTimeLabel(Date launchTime, Date terminateTim return elapsedString; } + private static boolean isElapsedTimeDisabled(String elapsedTimeFormatValue) { + return DebugPreferencesMessages.ConsoleDisableElapsedTime.equals(elapsedTimeFormatValue); + } + private static String computeTerminatedTimeLabel(Date launchTime, Date terminateTime, DateFormat dateTimeFormat) { // Check if process started and terminated at same day. If so only print the // time part of termination time and omit the date part. @@ -542,6 +571,10 @@ public void propertyChange(PropertyChangeEvent evt) { setHandleControlCharacters(store.getBoolean(IDebugPreferenceConstants.CONSOLE_INTERPRET_CONTROL_CHARACTERS)); } else if (property.equals(IDebugPreferenceConstants.CONSOLE_INTERPRET_CR_AS_CONTROL_CHARACTER)) { setCarriageReturnAsControlCharacter(store.getBoolean(IDebugPreferenceConstants.CONSOLE_INTERPRET_CR_AS_CONTROL_CHARACTER)); + } else if (property.equals(IDebugPreferenceConstants.CONSOLE_ELAPSED_FORMAT)) { + String elapsedTimeFormat = store.getString(IDebugPreferenceConstants.CONSOLE_ELAPSED_FORMAT); + isPeriodicNameUpdateRequired = !isElapsedTimeDisabled(elapsedTimeFormat); + showName(false, computeName(isPeriodicNameUpdateRequired)); } } @@ -566,6 +599,7 @@ public IProcess getProcess() { @Override protected void dispose() { super.dispose(); + disposed = true; consoleNameUpdateExecutor.shutdownNow(); fColorProvider.disconnect(); DebugPlugin.getDefault().removeDebugEventListener(this); @@ -573,7 +607,6 @@ protected void dispose() { JFaceResources.getFontRegistry().removeListener(this); closeStreams(); disposeStreams(); - disposed = true; } /** @@ -624,16 +657,19 @@ private synchronized void disposeStreams() { @Override protected void init() { super.init(); + IPreferenceStore store = DebugUIPlugin.getDefault().getPreferenceStore(); + String elapsedTimeFormat = store.getString(IDebugPreferenceConstants.CONSOLE_ELAPSED_FORMAT); + isPeriodicNameUpdateRequired = !isElapsedTimeDisabled(elapsedTimeFormat); + DebugPlugin.getDefault().addDebugEventListener(this); // computeName() after addDebugEventListener() // see https://github.com/eclipse-jdt/eclipse.jdt.debug/issues/390 - setName(computeName()); + setName(computeName(false)); if (fProcess.isTerminated()) { closeStreams(); resetName(true); DebugPlugin.getDefault().removeDebugEventListener(this); } - IPreferenceStore store = DebugUIPlugin.getDefault().getPreferenceStore(); store.addPropertyChangeListener(this); JFaceResources.getFontRegistry().addListener(this); if (store.getBoolean(IDebugPreferenceConstants.CONSOLE_WRAP)) { @@ -680,12 +716,17 @@ public void handleDebugEvents(DebugEvent[] events) { } /** - * Compute & update console name, notify listeners if content has changed. Note, - * the {@link #computeName()} method may call this method again asynchronously - * if process is till running to update elapsed time display + * Compute & update console name, notify listeners if content has changed. + * + * @param contentChanged whether the content of console has changed since last + * name update. */ - private synchronized void resetName(boolean changed) { - final String newName = computeName(); + private synchronized void resetName(boolean contentChanged) { + final String newName = computeName(false); + showName(contentChanged, newName); + } + + private void showName(boolean contentChanged, final String newName) { String name = getName(); if (!name.equals(newName)) { DebugUIPlugin.getStandardDisplay().execute(() -> { @@ -693,7 +734,7 @@ private synchronized void resetName(boolean changed) { return; } setName(newName); - if (changed) { + if (contentChanged) { warnOfContentChange(); } }); @@ -1214,4 +1255,15 @@ public static String convertElapsedFormat(String humanReadable) { return format.toString(); } -} + + @Override + public void pageShown() { + isVisible = true; + computeName(true); + } + + @Override + public void pageHidden() { + isVisible = false; + } +} \ No newline at end of file diff --git a/debug/org.eclipse.ui.console/.settings/.api_filters b/debug/org.eclipse.ui.console/.settings/.api_filters new file mode 100644 index 00000000000..d5cc8304011 --- /dev/null +++ b/debug/org.eclipse.ui.console/.settings/.api_filters @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/debug/org.eclipse.ui.console/META-INF/MANIFEST.MF b/debug/org.eclipse.ui.console/META-INF/MANIFEST.MF index d66f083c3a4..e187457d024 100644 --- a/debug/org.eclipse.ui.console/META-INF/MANIFEST.MF +++ b/debug/org.eclipse.ui.console/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.ui.console; singleton:=true -Bundle-Version: 3.16.0.qualifier +Bundle-Version: 3.17.0.qualifier Bundle-Activator: org.eclipse.ui.console.ConsolePlugin Bundle-Vendor: %providerName Bundle-Localization: plugin diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/IConsole.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/IConsole.java index 0a56b91da49..2b66f2e2235 100644 --- a/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/IConsole.java +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/IConsole.java @@ -97,4 +97,40 @@ public interface IConsole { */ String getType(); + /** + * Notifies this console that its page has been shown in the UI. This method is + * called when this console page is shown on top of other console pages in at + * least one visible console view. + *

+ * Default implementation does nothing. + *

+ *

+ * Subclasses may override this method to be notified when the console page for + * this console is visible to user. + *

+ * + * @since 3.17 + */ + default void pageShown() { + // Subclasses may override + } + + /** + * Notifies this console that its page has been hidden in the UI. This method is + * called when this console page is not shown on top of other console pages in + * any of visible console views. + *

+ * Default implementation does nothing. + *

+ *

+ * Subclasses may override this method to be notified when the console page for + * this console is not longer visible to user. + *

+ * + * @since 3.17 + */ + default void pageHidden() { + // Subclasses may override + } + } diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/IConsoleManager.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/IConsoleManager.java index 2f539c36935..dce4ae1161b 100644 --- a/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/IConsoleManager.java +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/IConsoleManager.java @@ -107,4 +107,37 @@ public interface IConsoleManager { */ void refresh(IConsole console); + /** + * Notifies that given console has been shown in the UI (either the view with + * given console on top has been shown or the console has been switched to the + * top console in at least one view). + * + *

+ * Note, there could be multiple views showing the same console, so the page + * with given console can be still hidden in some views. + *

+ * + * @param console the top console from view that has been shown + * + * @since 3.17 + */ + default void consoleShown(IConsole console) { + } + + /** + * Notifies that given console that was on top of at least one view has been + * hidden in the UI (either the console is not the top page in the view or the + * view with given console on top has been hidden). + *

+ * Note, there could be multiple views showing the same console, so the page + * with given console can be still shown in some views. + *

+ * + * @param console the console that has been hidden + * + * @since 3.17 + */ + default void consoleHidden(IConsole console) { + } + } diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleManager.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleManager.java index 5e92f56c1f2..3e8de2d787b 100644 --- a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleManager.java +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleManager.java @@ -488,4 +488,54 @@ public void refresh(final IConsole console) { redrawConsoleJob.schedule(50); } + @Override + public void consoleShown(IConsole console) { + if (isConsoleViewNotVisibleAnywhere(console)) { + // if no view with console is visible, ignore + return; + } + console.pageShown(); + } + + @Override + public void consoleHidden(IConsole console) { + if (isConsoleVisibleSomewhere(console)) { + // if console is still shown in some other view, then there is no need to call + // pageHidden() on the console, since user can still see the console + return; + } + console.pageHidden(); + } + + private boolean isConsoleVisibleSomewhere(IConsole console) { + synchronized (fConsoleViews) { + for (ConsoleView view : fConsoleViews) { + IWorkbenchPartSite site = view.getSite(); + if (site == null) { + continue; + } + boolean viewVisible = site.getPage().isPartVisible(view); + if (viewVisible && view.getConsole() == console) { + return true; + } + } + } + return false; + } + + private boolean isConsoleViewNotVisibleAnywhere(IConsole console) { + synchronized (fConsoleViews) { + for (ConsoleView view : fConsoleViews) { + IWorkbenchPartSite site = view.getSite(); + if (site == null) { + continue; + } + boolean viewVisible = site.getPage().isPartVisible(view); + if (viewVisible && view.getConsole() == console) { + return false; + } + } + } + return true; + } } diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleView.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleView.java index 7b66dc4746f..e063e9e68f4 100644 --- a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleView.java +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleView.java @@ -194,11 +194,17 @@ protected void showPageRec(PageRec pageRec) { if (fActive && oldActiveConsole != null) { deactivateParticipants(oldActiveConsole); } + + setConsole(recConsole); + + if (oldActiveConsole != null) { + getConsoleManager().consoleHidden(oldActiveConsole); + } if (recConsole != null) { activateParticipants(recConsole); + getConsoleManager().consoleShown(recConsole); } } - setConsole(recConsole); // bring active console on top of stack if (recConsole != null && !fStack.isEmpty() && !recConsole.equals(fStack.get(0))) { fStack.remove(recConsole); @@ -818,10 +824,22 @@ public void partOpened(IWorkbenchPartReference partRef) { @Override public void partHidden(IWorkbenchPartReference partRef) { + if (isThisPart(partRef)) { + IConsole console = getConsole(); + if (console != null) { + getConsoleManager().consoleHidden(console); + } + } } @Override public void partVisible(IWorkbenchPartReference partRef) { + if (isThisPart(partRef)) { + IConsole console = getConsole(); + if (console != null) { + getConsoleManager().consoleShown(console); + } + } } @Override diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleViewConsoleFactory.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleViewConsoleFactory.java index 7ebab677345..f186c1f8613 100644 --- a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleViewConsoleFactory.java +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleViewConsoleFactory.java @@ -64,7 +64,7 @@ public void openConsole() { * If the remember auto-pin decision state is true it gathers the auto * pin preference value and sets this to the current view. */ - private boolean handleAutoPin() { + protected boolean handleAutoPin() { if (currentConsoleView == null) { return false; } @@ -92,7 +92,7 @@ private boolean handleAutoPin() { /** * Sets the console view, on which the open new console action was called. */ - void setConsoleView(ConsoleView consoleView) { + public void setConsoleView(ConsoleView consoleView) { this.currentConsoleView = consoleView; }