From 8f038cb4ec95fccf9ebb3f24cc20904c9f1eba61 Mon Sep 17 00:00:00 2001 From: danthe1st Date: Mon, 22 Dec 2025 13:15:07 +0100 Subject: [PATCH] Allow stepping through disassembly This change adjusts JavaStackFrameEditorPresenter for ClassFileEditor, so that bytecode instructions are highlighted during debug stepping. --- .../jdt/debug/tests/AbstractDebugTest.java | 15 +- .../jdt/debug/tests/AutomatedSuite.java | 4 + .../ClassFileEditorHighlightingTest.java | 312 ++++++++++++++++++ .../ui/JavaStackFrameEditorPresenter.java | 101 +++++- .../debug/core/model/JDIStackFrame.java | 5 + 5 files changed, 432 insertions(+), 5 deletions(-) create mode 100644 org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/sourcelookup/ClassFileEditorHighlightingTest.java diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java index be9bb57186..3931aee8b4 100644 --- a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java +++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java @@ -2228,6 +2228,20 @@ protected IJavaMethodBreakpoint createMethodBreakpoint(IJavaProject project, Str protected IJavaMethodBreakpoint createMethodBreakpoint(String packageName, String cuName, String typeName, String methodName, String methodSignature, boolean entry, boolean exit) throws Exception { IType type = getType(packageName, cuName, typeName); assertNotNull("did not find type to install breakpoint in", type); //$NON-NLS-1$ + return createMethodBreakpoint(type, methodName, methodSignature, entry, exit); + } + + /** + * Creates a method breakpoint in a specified type. + * + * @param type the type in which the method breakpoint is set + * @param methodName method or null for all methods + * @param methodSignature JLS method signature or null for all methods with the given name + * @param entry whether to break on entry + * @param exit whether to break on exit + * @return method breakpoint + */ + protected IJavaMethodBreakpoint createMethodBreakpoint(IType type, String methodName, String methodSignature, boolean entry, boolean exit) throws Exception { IMethod method= null; if (methodSignature != null && methodName != null) { if (type != null ) { @@ -2240,7 +2254,6 @@ protected IJavaMethodBreakpoint createMethodBreakpoint(String packageName, Strin return bp; } - /** * Creates a MethodBreakPoint on the method specified at the given path. * Syntax: diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java index 0ac74834e0..3e8baac8f5 100644 --- a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java +++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java @@ -135,6 +135,7 @@ import org.eclipse.jdt.debug.tests.refactoring.RenamePublicTypeUnitTests; import org.eclipse.jdt.debug.tests.sourcelookup.ArchiveSourceLookupTests; import org.eclipse.jdt.debug.tests.sourcelookup.Bug565462Tests; +import org.eclipse.jdt.debug.tests.sourcelookup.ClassFileEditorHighlightingTest; import org.eclipse.jdt.debug.tests.sourcelookup.DefaultSourceContainerTests; import org.eclipse.jdt.debug.tests.sourcelookup.DirectorySourceContainerTests; import org.eclipse.jdt.debug.tests.sourcelookup.DirectorySourceLookupTests; @@ -148,6 +149,7 @@ import org.eclipse.jdt.debug.tests.sourcelookup.TypeResolutionTests; import org.eclipse.jdt.debug.tests.state.RefreshStateTests; import org.eclipse.jdt.debug.tests.ui.DebugHoverTests; +import org.eclipse.jdt.debug.tests.ui.DebugSelectionTests; import org.eclipse.jdt.debug.tests.ui.DebugViewTests; import org.eclipse.jdt.debug.tests.ui.DetailPaneManagerTests; import org.eclipse.jdt.debug.tests.ui.HotCodeReplaceErrorDialogTest; @@ -229,6 +231,8 @@ public AutomatedSuite() { addTest(new TestSuite(TypeResolutionTests.class)); addTest(new TestSuite(JarSourceLookupTests.class)); addTest(new TestSuite(Bug565462Tests.class)); + addTest(new TestSuite(DebugSelectionTests.class)); + addTest(new TestSuite(ClassFileEditorHighlightingTest.class)); // Variable tests addTest(new TestSuite(InstanceVariableTests.class)); diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/sourcelookup/ClassFileEditorHighlightingTest.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/sourcelookup/ClassFileEditorHighlightingTest.java new file mode 100644 index 0000000000..8b52a9bb90 --- /dev/null +++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/sourcelookup/ClassFileEditorHighlightingTest.java @@ -0,0 +1,312 @@ +/******************************************************************************* + * Copyright (c) 2026, Daniel Schmid 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: + * Daniel Schmid - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.debug.tests.sourcelookup; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.debug.core.IJavaStackFrame; +import org.eclipse.jdt.debug.core.IJavaThread; +import org.eclipse.jdt.debug.testplugin.JavaProjectHelper; +import org.eclipse.jdt.debug.tests.TestUtil; +import org.eclipse.jdt.debug.tests.ui.AbstractDebugUiTests; +import org.eclipse.jdt.internal.ui.javaeditor.ClassFileEditor; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.eclipse.jdt.ui.jarpackager.IJarExportRunnable; +import org.eclipse.jdt.ui.jarpackager.JarPackageData; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.custom.StyledTextContent; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.junit.Assume; + +public class ClassFileEditorHighlightingTest extends AbstractDebugUiTests { + + private static final long TIMEOUT = 10_000L; + + private static final String CLASS_NAME = "NoSources"; + private static final String CLASS_CONTENTS = """ + public class NoSources { + private static int i = 0; + public static void main(String[] args) { + i++; + System.out.println(i); + } + } + """; + + public ClassFileEditorHighlightingTest(String name) { + super(name); + } + + @Override + protected boolean enableUIEventLoopProcessingInWaiter() { + return true; + } + + @Override + public void tearDown() throws Exception { + try { + closeAllEditors(); + } finally { + super.tearDown(); + } + } + + public void testDisplaySourceWithClassFileEditorHighlightsLine() throws Exception { + IJavaProject project = createProjectWithNoSources(CLASS_NAME, CLASS_CONTENTS, true); + IJavaThread thread = null; + try { + ILaunchConfiguration config = createLaunchConfiguration(project, CLASS_NAME); + createLineBreakpoint(project.findType(CLASS_NAME), 4); + thread = launchToBreakpoint(config); + + stepOver((IJavaStackFrame) thread.getTopStackFrame()); + expectHighlightedText(" 8 getstatic java.lang.System.out : java.io.PrintStream [13]", TIMEOUT); + + stepOver((IJavaStackFrame) thread.getTopStackFrame()); + expectHighlightedText(" 17 return", TIMEOUT); + } finally { + terminateAndRemove(thread); + removeAllBreakpoints(); + project.getProject().delete(true, null); + } + } + + public void testConstructorInPackage() throws Exception { + IJavaProject project = createProjectWithNoSources("ClassOne", """ + public class ClassOne { + + public static void main(String[] args) { + ClassOne co = new ClassOne(); + co.method1(); + } + + public void method1() { + method2(); + } + + public void method2() { + method3(); + } + + public void method3() { + System.out.println("ClassOne, method3"); + } + } + """); + createMethodBreakpoint(project.findType("ClassOne"), "", "()V", true, false); + IJavaThread thread = null; + try { + ILaunchConfiguration config = createLaunchConfiguration(project, "ClassOne"); + thread = launchToBreakpoint(config); + expectHighlightedText(" 0 aload_0 [this]", TIMEOUT); + } finally { + terminateAndRemove(thread); + removeAllBreakpoints(); + project.getProject().delete(true, null); + } + } + + public void testDisplaySourceWithClassFileEditorHighlightsLineInConstructor() throws Exception { + IJavaProject project = createProjectWithNoSources("MethodCall", """ + public class MethodCall { + + private int i; + private int sum = 0; + + public static void main(String[] args) { + MethodCall mc = new MethodCall(); + mc.go(); + } + + public void go() { + calculateSum(); + } + + protected void calculateSum() { + sum += i; + } + } + """); + createMethodBreakpoint(project.findType("MethodCall"), "", "()V", true, false); + IJavaThread thread = null; + try { + ILaunchConfiguration config = createLaunchConfiguration(project, "MethodCall"); + thread = launchToBreakpoint(config); + expectHighlightedText(" 0 aload_0 [this]", TIMEOUT); + } finally { + terminateAndRemove(thread); + removeAllBreakpoints(); + project.getProject().delete(true, null); + } + } + + public void testClassFileWithoutDebuggingInformation() throws Exception { + IJavaProject project = createProjectWithNoSources(CLASS_NAME, CLASS_CONTENTS); + IJavaThread thread = null; + try { + ILaunchConfiguration config = createLaunchConfiguration(project, CLASS_NAME); + ILaunchConfigurationWorkingCopy workingCopy = config.getWorkingCopy(); + workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_STOP_IN_MAIN, true); + config = workingCopy.doSave(); + thread = launchToBreakpoint(config); + + String[] expectedHighlights = """ + 0 getstatic NoSources.i : int [7] + 3 iconst_1 + 4 iadd + 5 putstatic NoSources.i : int [7] + 8 getstatic java.lang.System.out : java.io.PrintStream [13] + 11 getstatic NoSources.i : int [7] + 14 invokevirtual java.io.PrintStream.println(int) : void [19] + 17 return + """.split(System.lineSeparator()); + + for (int i = 0; i < expectedHighlights.length; i++) { + expectHighlightedText(expectedHighlights[i], TIMEOUT); + + if (i < expectedHighlights.length - 1) { + stepOver((IJavaStackFrame) thread.getTopStackFrame()); + } + } + } finally { + terminateAndRemove(thread); + removeAllBreakpoints(); + project.getProject().delete(true, null); + } + } + + private IJavaProject createProjectWithNoSources(String className, String contents) throws Exception { + return createProjectWithNoSources(className, contents, false); + } + + private IJavaProject createProjectWithNoSources(String className, String contents, boolean generateLineInfo) throws Exception { + IJavaProject javaProject = JavaProjectHelper.createJavaProject("ClassFileEditorHighlightingTest", JavaProjectHelper.BIN_DIR); + IProject project = javaProject.getProject(); + project.getFolder(LAUNCHCONFIGURATIONS).create(true, true, null); + + IFile classFile = project.getFile(className + ".class"); + + String debug = generateLineInfo ? "lines" : "none"; + compileWithJavac(className, contents, List.of("-g:" + debug, "-d", project.getLocation().toString(), "--release", "8")); + classFile.refreshLocal(IResource.DEPTH_ONE, null); + + IFile lib = project.getFile("lib.jar"); + jarClassFile(lib, classFile); + + JavaProjectHelper.addLibrary(javaProject, lib.getFullPath()); + waitForBuild(); + return javaProject; + } + + private static void jarClassFile(IFile jar, IFile classFile) { + JarPackageData data = new JarPackageData(); + data.setJarLocation(jar.getLocation()); + data.setBuildIfNeeded(true); + data.setOverwrite(true); + data.setElements(new Object[] { classFile }); + data.setExportClassFiles(true); + + IStatus status = callInUi(() -> { + IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + IJarExportRunnable op = data.createJarExportRunnable(window.getShell()); + window.run(false, false, op); + return op.getStatus(); + }); + if (status.getSeverity() == IStatus.ERROR) { + fail("Creating jar failed: " + status.getMessage()); + } + } + + private static void expectHighlightedText(String expectedHighlightedText, long timeout) throws InterruptedException { + long s = System.currentTimeMillis(); + while (System.currentTimeMillis() - s < timeout) { + List highlighted = callInUi(ClassFileEditorHighlightingTest::getClassEditorHighlightedText); + if (Arrays.asList(expectedHighlightedText).equals(highlighted)) { + break; + } + TestUtil.runEventLoop(); + Thread.sleep(50L); + } + List highlighted = callInUi(ClassFileEditorHighlightingTest::getClassEditorHighlightedText); + assertEquals("Timed out while waiting on highlighting", Arrays.asList(expectedHighlightedText), highlighted); + } + + private static List getClassEditorHighlightedText() { + List highlighted = new ArrayList<>(); + StyledText noSourceTextWidget = null; + ClassFileEditor editor = (ClassFileEditor) getActivePage().getActiveEditor(); + if (editor != null) { + noSourceTextWidget = editor.getNoSourceTextWidget(); + } + if (noSourceTextWidget != null) { + StyleRange[] styleRanges = noSourceTextWidget.getStyleRanges(); + StyledTextContent content = noSourceTextWidget.getContent(); + for (int i = 0; i < styleRanges.length; ++i) { + String highlightedText = content.getTextRange(styleRanges[0].start, styleRanges[0].length); + highlighted.add(highlightedText); + } + } + return highlighted; + } + + private static void compileWithJavac(String className, String source, List compilerOptions) { + JavaFileObject fileObject = new SourceJavaFileObject(className, source); + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + Assume.assumeNotNull(compiler); + + StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, Locale.ROOT, StandardCharsets.UTF_8); + DiagnosticCollector collector = new DiagnosticCollector<>(); + CompilationTask task = compiler.getTask(null, fileManager, collector, compilerOptions, null, List.of(fileObject)); + Boolean result = task.call(); + assertTrue(String.valueOf(collector.getDiagnostics()), result); + } + + private static class SourceJavaFileObject extends SimpleJavaFileObject { + + private final String code; + + protected SourceJavaFileObject(String name, String code) { + super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); + this.code = code; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + return code; + } + } +} diff --git a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/JavaStackFrameEditorPresenter.java b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/JavaStackFrameEditorPresenter.java index 1c7dc4a6d0..b8325066bd 100644 --- a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/JavaStackFrameEditorPresenter.java +++ b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/JavaStackFrameEditorPresenter.java @@ -16,20 +16,31 @@ import java.util.List; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; import org.eclipse.debug.core.model.IStackFrame; import org.eclipse.debug.core.model.IThread; +import org.eclipse.debug.ui.DebugUITools; import org.eclipse.debug.ui.IDebugEditorPresentation; +import org.eclipse.debug.ui.contexts.DebugContextEvent; +import org.eclipse.debug.ui.contexts.IDebugContextListener; +import org.eclipse.debug.ui.contexts.IDebugContextService; import org.eclipse.jdt.core.dom.IMethodBinding; import org.eclipse.jdt.core.dom.LambdaExpression; import org.eclipse.jdt.internal.debug.core.JDIDebugPlugin; import org.eclipse.jdt.internal.debug.core.model.JDIStackFrame; import org.eclipse.jdt.internal.debug.ui.actions.ToggleBreakpointAdapter; +import org.eclipse.jdt.internal.ui.javaeditor.ClassFileEditor; import org.eclipse.jdt.ui.JavaUI; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.progress.UIJob; import org.eclipse.ui.texteditor.IDocumentProvider; import org.eclipse.ui.texteditor.ITextEditor; @@ -38,12 +49,71 @@ */ public class JavaStackFrameEditorPresenter implements IDebugEditorPresentation { + private class ClassFileUnhighlightingListener implements IDebugContextListener { + + private final IDebugContextService service; + + public ClassFileUnhighlightingListener(IDebugContextService service) { + this.service = service; + service.addDebugContextListener(this); + } + + @Override + public void debugContextChanged(DebugContextEvent event) { + if ((event.getFlags() & DebugContextEvent.STATE) > 0) { + if (event.getContext() instanceof IStructuredSelection selection && selection.getFirstElement() instanceof JDIStackFrame frame + && frame.equals(currentClassFileFrame)) { + return; + } + unhighlightJob.cancel(); + unhighlightJob.schedule(); + } + } + + private void unregister() { + service.removeDebugContextListener(this); + } + } + + private class UnhighlightJob extends UIJob { + + public UnhighlightJob() { + super("Unhighlight class file editor"); //$NON-NLS-1$ + setSystem(true); + } + + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + if (monitor.isCanceled()) { + return Status.CANCEL_STATUS; + } + if (currentClassFileEditor != null) { + currentClassFileEditor.unhighlight(); + currentClassFileEditor = null; + currentClassFileFrame = null; + unhighlightingListener.unregister(); + unhighlightingListener = null; + } + return Status.OK_STATUS; + } + + @Override + public boolean belongsTo(Object family) { + return JavaStackFrameEditorPresenter.class == family; + } + } + + private final UnhighlightJob unhighlightJob = new UnhighlightJob(); + private ClassFileUnhighlightingListener unhighlightingListener; + private JDIStackFrame currentClassFileFrame; + private ClassFileEditor currentClassFileEditor; + @Override - public boolean addAnnotations(IEditorPart editor, IStackFrame frame) { + public boolean addAnnotations(IEditorPart editorPart, IStackFrame frame) { try { - if (editor instanceof ITextEditor textEditor && frame instanceof JDIStackFrame jdiFrame + if (editorPart instanceof ITextEditor textEditor && frame instanceof JDIStackFrame jdiFrame && org.eclipse.jdt.internal.debug.core.model.LambdaUtils.isLambdaFrame(jdiFrame)) { - IEditorInput editorInput = editor.getEditorInput(); + IEditorInput editorInput = editorPart.getEditorInput(); IDocumentProvider provider = textEditor.getDocumentProvider(); IDocument document = provider.getDocument(editorInput); if (document != null && JavaUI.getEditorInputJavaElement(editorInput) != null) { @@ -58,6 +128,19 @@ public boolean addAnnotations(IEditorPart editor, IStackFrame frame) { } } } + if (editorPart instanceof ClassFileEditor classFileEditor && frame instanceof JDIStackFrame jdiFrame + && classFileEditor.getDocumentProvider().getDocument(editorPart.getEditorInput()).getLength() == 0) { + String methodName = jdiFrame.getMethodName(); + if (jdiFrame.isConstructor()) { + methodName = jdiFrame.getDeclaringTypeName(); + methodName = methodName.substring(methodName.lastIndexOf('.') + 1); + } + + classFileEditor.highlightInstruction(methodName, jdiFrame.getSignature(), jdiFrame.getCodeIndex()); + currentClassFileEditor = classFileEditor; + currentClassFileFrame = jdiFrame; + ensureListenerRegistered(classFileEditor.getSite().getPage()); + } } catch (CoreException | BadLocationException e) { JDIDebugPlugin.log(e); } @@ -66,7 +149,10 @@ public boolean addAnnotations(IEditorPart editor, IStackFrame frame) { @Override public void removeAnnotations(IEditorPart editorPart, IThread thread) { - // nothing to clean up + if (editorPart instanceof ClassFileEditor classFileEditor + && classFileEditor.getDocumentProvider().getDocument(editorPart.getEditorInput()).getLength() == 0) { + classFileEditor.unhighlight(); + } } private static String getMethodBindingKey(LambdaExpression exp) { @@ -77,4 +163,11 @@ private static String getMethodBindingKey(LambdaExpression exp) { } return key; } + + private void ensureListenerRegistered(IWorkbenchPage page) { + if (unhighlightingListener == null) { + IDebugContextService service = DebugUITools.getDebugContextManager().getContextService(page.getWorkbenchWindow()); + unhighlightingListener = new ClassFileUnhighlightingListener(service); + } + } } diff --git a/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIStackFrame.java b/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIStackFrame.java index 107e2159b3..f3c175edd8 100644 --- a/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIStackFrame.java +++ b/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIStackFrame.java @@ -1756,6 +1756,11 @@ private boolean isDifferentLambdaContext(IStackFrame frame) throws DebugExceptio return true; } return false; + } + public long getCodeIndex() { + synchronized (fThread) { + return fLocation.codeIndex(); + } } }