From 933f1581738149322a90b6870774f39076839401 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Sun, 11 Oct 2026 16:03:05 +0200 Subject: [PATCH] Defer activation reparse while parsing and rethrow UI failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Activating the editor while a parse was running started a nested parse that disposed the outer parse's model. The outer parse then failed with a NullPointerException in UndoManager.setRoot, which hid the real error. UndoManager.activate() now waits for the running parse, as the buffer-change refresh already does. ExecutionUtils.runRethrowUI() did not rethrow in the IDE, because SWT hands a syncExec runnable's exception to the display's exception handler, which the workbench uses to log the exception and continue. The exception is now caught in the runnable and rethrown in the caller. Assisted-by: multiple AI agents and layers of automated tooling 🤖 --- .../wb/internal/core/editor/UndoManager.java | 21 ++++++++++-- .../core/utils/execution/ExecutionUtils.java | 17 +++++++--- .../util/execution/ExecutionUtilsTest.java | 34 +++++++++++++++++++ 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/org.eclipse.wb.core.java/src/org/eclipse/wb/internal/core/editor/UndoManager.java b/org.eclipse.wb.core.java/src/org/eclipse/wb/internal/core/editor/UndoManager.java index 7b1a5b120f..77ada4b763 100644 --- a/org.eclipse.wb.core.java/src/org/eclipse/wb/internal/core/editor/UndoManager.java +++ b/org.eclipse.wb.core.java/src/org/eclipse/wb/internal/core/editor/UndoManager.java @@ -84,13 +84,28 @@ public UndoManager(DesignPage designPage, ICompilationUnit unit) throws Exceptio public void activate() { if (!m_active) { m_active = true; - if (!Objects.equals(m_currentSource, m_buffer.getContents())) { - refreshDesignerEditor(); - } + refreshIfSourceChanged(); addBufferListener(); } } + /** + * Reparses if the buffer no longer matches the parsed source, waiting for a running parse first. + */ + private void refreshIfSourceChanged() { + if (!m_active) { + return; + } + // the progress dialog of a running parse spins the event loop, so wait for it to finish + if (m_designPage.getDesignerState() == DesignerState.Parsing) { + Display.getDefault().timerExec(1, this::refreshIfSourceChanged); + return; + } + if (!Objects.equals(m_currentSource, m_buffer.getContents())) { + refreshDesignerEditor(); + } + } + /** * Deactivates {@link UndoManager}, so it stops listening for buffer changes. */ diff --git a/org.eclipse.wb.core/src/org/eclipse/wb/internal/core/utils/execution/ExecutionUtils.java b/org.eclipse.wb.core/src/org/eclipse/wb/internal/core/utils/execution/ExecutionUtils.java index 7398f2260e..20517f8fac 100644 --- a/org.eclipse.wb.core/src/org/eclipse/wb/internal/core/utils/execution/ExecutionUtils.java +++ b/org.eclipse.wb.core/src/org/eclipse/wb/internal/core/utils/execution/ExecutionUtils.java @@ -184,15 +184,22 @@ public void run() { } /** - * Runs given {@link RunnableEx} inside of UI thread, using {@link Display#syncExec(Runnable)}. + * Runs given {@link RunnableEx} inside of UI thread, using {@link Display#syncExec(Runnable)}, + * and re-throws its exceptions in the calling thread. */ public static void runRethrowUI(final RunnableEx runnable) { - Display.getDefault().syncExec(new Runnable() { - @Override - public void run() { - ExecutionUtils.runRethrow(runnable); + // the workbench's display exception handler swallows what a syncExec runnable throws + Throwable[] failure = new Throwable[1]; + Display.getDefault().syncExec(() -> { + try { + runnable.run(); + } catch (Throwable e) { + failure[0] = e; } }); + if (failure[0] != null) { + throw ReflectionUtils.propagate(failure[0]); + } } /** diff --git a/org.eclipse.wb.tests/src/org/eclipse/wb/tests/designer/core/util/execution/ExecutionUtilsTest.java b/org.eclipse.wb.tests/src/org/eclipse/wb/tests/designer/core/util/execution/ExecutionUtilsTest.java index 8ee72cd24b..f05f9e9191 100644 --- a/org.eclipse.wb.tests/src/org/eclipse/wb/tests/designer/core/util/execution/ExecutionUtilsTest.java +++ b/org.eclipse.wb.tests/src/org/eclipse/wb/tests/designer/core/util/execution/ExecutionUtilsTest.java @@ -31,6 +31,8 @@ import java.beans.Beans; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; /** * Tests for {@link ExecutionUtils} and {@link RunnableEx}. @@ -458,6 +460,38 @@ public void test_runRethrowUI() throws Exception { } } + /** + * Test for {@link ExecutionUtils#runRethrowUI(RunnableEx)} when the display's exception handler + * swallows exceptions, as the workbench's does. + */ + @Test + public void test_runRethrowUI_exception() throws Exception { + Display display = Display.getCurrent(); + Consumer oldHandler = display.getRuntimeExceptionHandler(); + display.setRuntimeExceptionHandler(e -> { + }); + try { + IllegalStateException exception = new IllegalStateException(); + AtomicReference caught = new AtomicReference<>(); + Thread thread = new Thread(() -> { + try { + ExecutionUtils.runRethrowUI(() -> { + throw exception; + }); + } catch (Throwable e) { + caught.set(e); + } + }); + thread.start(); + while (thread.isAlive()) { + waitEventLoop(0); + } + assertSame(exception, caught.get()); + } finally { + display.setRuntimeExceptionHandler(oldHandler); + } + } + /** * Test for {@link ExecutionUtils#runAsync(RunnableEx)}. */