Skip to content

Commit b9a9b57

Browse files
danthe1sttrancexpress
authored andcommitted
Allow highlighting instructions in ClassFileEditor
1 parent 85e6c77 commit b9a9b57

4 files changed

Lines changed: 304 additions & 2 deletions

File tree

org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/AutomatedSuite.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2000, 2025 IBM Corporation and others.
2+
* Copyright (c) 2000, 2026 IBM Corporation and others.
33
*
44
* This program and the accompanying materials
55
* are made available under the terms of the Eclipse Public License 2.0
@@ -34,6 +34,7 @@
3434
import org.eclipse.jdt.ui.tests.core.CoreTestSuite;
3535
import org.eclipse.jdt.ui.tests.core.CoreTests;
3636
import org.eclipse.jdt.ui.tests.dialogs.FilteredTypesSelectionDialogTests;
37+
import org.eclipse.jdt.ui.tests.editor.ClassFileEditorTests;
3738
import org.eclipse.jdt.ui.tests.editor.ClassFileInputTests;
3839
import org.eclipse.jdt.ui.tests.editor.MarkdownTypingTest;
3940
import org.eclipse.jdt.ui.tests.hover.JavadocHoverTests;
@@ -92,6 +93,7 @@
9293
MarkdownCommentTests.class,
9394
SmokeViewsTest.class,
9495
ClassFileInputTests.class,
96+
ClassFileEditorTests.class,
9597
FilteredTypesSelectionDialogTests.class,
9698
MarkdownTypingTest.class
9799
})
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026, Daniel Schmid and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Daniel Schmid - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.jdt.ui.tests.editor;
15+
16+
import static org.junit.jupiter.api.Assertions.assertEquals;
17+
import static org.junit.jupiter.api.Assertions.assertFalse;
18+
19+
import org.junit.jupiter.api.AfterEach;
20+
import org.junit.jupiter.api.BeforeEach;
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.eclipse.jdt.testplugin.JavaProjectHelper;
24+
25+
import org.eclipse.swt.custom.StyleRange;
26+
import org.eclipse.swt.custom.StyledText;
27+
import org.eclipse.swt.custom.StyledTextContent;
28+
29+
import org.eclipse.core.runtime.CoreException;
30+
import org.eclipse.core.runtime.NullProgressMonitor;
31+
32+
import org.eclipse.core.resources.IncrementalProjectBuilder;
33+
34+
import org.eclipse.jdt.core.IJavaProject;
35+
import org.eclipse.jdt.core.IPackageFragment;
36+
37+
import org.eclipse.jdt.internal.ui.javaeditor.ClassFileEditor;
38+
import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;
39+
import org.eclipse.jdt.internal.ui.util.CoreUtility;
40+
41+
public class ClassFileEditorTests {
42+
private static final String TYPE_NAME= "HelloWorld";
43+
44+
private IJavaProject javaProject;
45+
private boolean wasAutoBuilding;
46+
47+
@BeforeEach
48+
void setUp() throws Exception {
49+
javaProject = setUpProject();
50+
51+
wasAutoBuilding = CoreUtility.setAutoBuilding(false);
52+
}
53+
54+
@AfterEach
55+
void tearDown() throws Exception {
56+
if (javaProject != null) {
57+
JavaProjectHelper.delete(javaProject);
58+
}
59+
CoreUtility.setAutoBuilding(wasAutoBuilding);
60+
}
61+
62+
private IJavaProject setUpProject() throws Exception {
63+
javaProject= JavaProjectHelper.createJavaProject(ClassFileInputTests.class.getSimpleName(), "bin");
64+
JavaProjectHelper.addSourceContainer(javaProject, "src");
65+
JavaProjectHelper.addRTJar(javaProject);
66+
return javaProject;
67+
}
68+
69+
@Test
70+
void testHighlightRange() throws CoreException {
71+
createHelloWorldClass();
72+
ClassFileEditor classEditor= createClassFileEditor(TYPE_NAME);
73+
classEditor.highlightInstruction("main", "([Ljava/lang/String;)V", 5);
74+
assertEquals(" 5 invokevirtual java.io.PrintStream.println(java.lang.String) : void [24]", getSingleHighlightedRange(classEditor));
75+
}
76+
77+
@Test
78+
void testHighlightInConstructor() throws CoreException {
79+
createHelloWorldClass();
80+
ClassFileEditor classEditor= createClassFileEditor(TYPE_NAME);
81+
classEditor.highlightInstruction(TYPE_NAME, "()V", 1);
82+
assertEquals(" 1 invokespecial java.lang.Object() [8]", getSingleHighlightedRange(classEditor));
83+
}
84+
85+
@Test
86+
void testChangeHighlightRange() throws CoreException {
87+
createHelloWorldClass();
88+
ClassFileEditor classEditor= createClassFileEditor(TYPE_NAME);
89+
classEditor.highlightInstruction("main", "([Ljava/lang/String;)V", 5);
90+
classEditor.highlightInstruction("main", "([Ljava/lang/String;)V", 0);
91+
assertEquals(" 0 getstatic java.lang.System.out : java.io.PrintStream [16]", getSingleHighlightedRange(classEditor));
92+
}
93+
94+
@Test
95+
void testUnhighlight() throws CoreException {
96+
createHelloWorldClass();
97+
ClassFileEditor classEditor= createClassFileEditor(TYPE_NAME);
98+
classEditor.highlightInstruction("main", "([Ljava/lang/String;)V", 5);
99+
StyledText noSourceTextWidget= classEditor.getNoSourceTextWidget();
100+
assertEquals(1, noSourceTextWidget.getStyleRanges().length);
101+
classEditor.unhighlight();
102+
assertEquals(0, noSourceTextWidget.getStyleRanges().length);
103+
}
104+
105+
@Test
106+
void testHighlightNonexistentCodeIndex() throws CoreException {
107+
createHelloWorldClass();
108+
ClassFileEditor classEditor= createClassFileEditor(TYPE_NAME);
109+
classEditor.highlightInstruction("main", "([Ljava/lang/String;)V", 100);
110+
StyledText noSourceTextWidget= classEditor.getNoSourceTextWidget();
111+
assertEquals(0, noSourceTextWidget.getStyleRanges().length);
112+
}
113+
114+
@Test
115+
void testHighlightNonexistentMethod() throws CoreException {
116+
createHelloWorldClass();
117+
ClassFileEditor classEditor= createClassFileEditor(TYPE_NAME);
118+
classEditor.highlightInstruction("main", "a", 5);
119+
assertEquals(0, classEditor.getNoSourceTextWidget().getStyleRanges().length);
120+
}
121+
122+
@Test
123+
void testHighlightMultipleCompilationUnits() throws CoreException {
124+
createClassFromSource("A.java", """
125+
public class A {
126+
void a() {
127+
System.out.println("a");
128+
}
129+
}
130+
class B {
131+
void b() {
132+
System.out.println("b");
133+
}
134+
}
135+
""");
136+
ClassFileEditor classEditor= createClassFileEditor("A");
137+
classEditor.highlightInstruction("a", "()V", 3);
138+
assertEquals(" 3 ldc <String \"a\"> [21]", getSingleHighlightedRange(classEditor));
139+
140+
classEditor= createClassFileEditor("B");
141+
classEditor.highlightInstruction("b", "()V", 3);
142+
assertEquals(" 3 ldc <String \"b\"> [21]", getSingleHighlightedRange(classEditor));
143+
144+
}
145+
146+
private String getSingleHighlightedRange(ClassFileEditor classEditor) {
147+
StyledText noSourceTextWidget= classEditor.getNoSourceTextWidget();
148+
StyleRange[] ranges= noSourceTextWidget.getStyleRanges();
149+
assertEquals(1, ranges.length);
150+
StyledTextContent content= noSourceTextWidget.getContent();
151+
StyleRange range= ranges[0];
152+
return content.getTextRange(range.start, range.length);
153+
}
154+
155+
private void createHelloWorldClass() throws CoreException {
156+
createClassFromSource(TYPE_NAME + ".java", """
157+
public class HelloWorld {
158+
public static void main(String[] args) {
159+
System.out.println("Hello World");
160+
}
161+
}
162+
""");
163+
}
164+
165+
private void createClassFromSource(String fileName, String content) throws CoreException {
166+
IPackageFragment fragment= javaProject.findPackageFragment(javaProject.getProject().getFullPath().append("src"));
167+
fragment.createCompilationUnit(fileName, content, true, new NullProgressMonitor());
168+
javaProject.getProject().build(IncrementalProjectBuilder.FULL_BUILD, new NullProgressMonitor());
169+
javaProject.getProject().getFile("src/" + fileName).delete(true,new NullProgressMonitor());
170+
}
171+
172+
private ClassFileEditor createClassFileEditor(String typeName) throws CoreException {
173+
ClassFileEditor editor= (ClassFileEditor) EditorUtility.openInEditor(javaProject.getProject().getFile("bin/" + typeName + ".class"));
174+
assertFalse(editor.isEditable());
175+
return editor;
176+
}
177+
}

org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/IUIConstants.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,7 @@ public interface IUIConstants {
3030

3131
String DIALOGSTORE_TYPECOMMENT_DEPRECATED= JavaUI.ID_PLUGIN + ".typecomment.deprecated"; //$NON-NLS-1$
3232

33+
// The preference is defined in the org.eclipse.debug.ui plugin.xml file
34+
String INSTRUCTION_POINTER_COLOR_PREFERENCE_KEY = "currentIPColor"; //$NON-NLS-1$
35+
3336
}

org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/ClassFileEditor.java

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2000, 2016 IBM Corporation and others.
2+
* Copyright (c) 2000, 2026 IBM Corporation and others.
33
*
44
* This program and the accompanying materials
55
* are made available under the terms of the Eclipse Public License 2.0
@@ -16,11 +16,18 @@
1616

1717
import java.lang.reflect.InvocationTargetException;
1818
import java.util.ArrayList;
19+
import java.util.HashMap;
1920
import java.util.List;
21+
import java.util.Map;
22+
import java.util.Objects;
23+
import java.util.regex.Matcher;
24+
import java.util.regex.Pattern;
2025

2126
import org.eclipse.swt.SWT;
2227
import org.eclipse.swt.custom.StackLayout;
28+
import org.eclipse.swt.custom.StyleRange;
2329
import org.eclipse.swt.custom.StyledText;
30+
import org.eclipse.swt.custom.StyledTextContent;
2431
import org.eclipse.swt.events.SelectionEvent;
2532
import org.eclipse.swt.events.SelectionListener;
2633
import org.eclipse.swt.graphics.Color;
@@ -54,6 +61,7 @@
5461
import org.eclipse.jface.util.PropertyChangeEvent;
5562

5663
import org.eclipse.jface.text.IWidgetTokenKeeper;
64+
import org.eclipse.jface.text.Region;
5765
import org.eclipse.jface.text.source.IOverviewRuler;
5866
import org.eclipse.jface.text.source.ISourceViewer;
5967
import org.eclipse.jface.text.source.IVerticalRuler;
@@ -104,6 +112,7 @@
104112
import org.eclipse.jdt.ui.actions.RefactorActionGroup;
105113
import org.eclipse.jdt.ui.wizards.BuildPathDialogAccess;
106114

115+
import org.eclipse.jdt.internal.ui.IUIConstants;
107116
import org.eclipse.jdt.internal.ui.JavaPlugin;
108117
import org.eclipse.jdt.internal.ui.JavaUIStatus;
109118
import org.eclipse.jdt.internal.ui.actions.CompositeActionGroup;
@@ -557,6 +566,9 @@ private boolean isEqualInput(IEditorInput input1, IEditorInput input2) {
557566
}
558567
}
559568

569+
private record MethodIdentifier(String name, String signature) {}
570+
private record LineRegion(int startLine, int endLine) {}
571+
560572
private StackLayout fStackLayout;
561573
private Composite fParent;
562574

@@ -585,7 +597,9 @@ private boolean isEqualInput(IEditorInput input1, IEditorInput input2) {
585597
* @since 3.3
586598
*/
587599
private StyledText fNoSourceTextWidget;
600+
private StyleRange currentSelection;
588601
private volatile boolean disposed;
602+
private Color instructionPointerColor = new Color(0, 255, 0);
589603

590604
/**
591605
* Default constructor.
@@ -736,6 +750,7 @@ protected IEditorInput transformEditorInput(IEditorInput input) throws CoreExcep
736750
@Override
737751
public void init(IEditorSite site, IEditorInput input) throws PartInitException {
738752
JavaCore.runReadOnly(() -> super.init(site, input));
753+
initInstructionPointerColor();
739754
}
740755
/*
741756
* @see AbstractTextEditor#doSetInput(IEditorInput)
@@ -964,6 +979,104 @@ private static boolean hasSource(IClassFile file) {
964979
}
965980
}
966981

982+
public void unhighlight() {
983+
if (disposed) {
984+
return;
985+
}
986+
if (currentSelection != null) {
987+
fNoSourceTextWidget.replaceStyleRanges(currentSelection.start, currentSelection.length, new StyleRange[0]);
988+
currentSelection= null;
989+
}
990+
}
991+
992+
public void highlightInstruction(String methodName, String signature, long codeIndex) {
993+
if (disposed) {
994+
return;
995+
}
996+
unhighlight();
997+
StyledTextContent content= fNoSourceTextWidget.getContent();
998+
999+
LineRegion methodBounds= getMethodsToInstructionRegionCache().get(new MethodIdentifier(methodName, signature));
1000+
if (methodBounds == null) {
1001+
return;
1002+
}
1003+
Region instructionPosition= findInstruction(methodBounds, content, codeIndex);
1004+
if (instructionPosition == null) {
1005+
return;
1006+
}
1007+
currentSelection= new StyleRange(instructionPosition.getOffset(), instructionPosition.getLength(), fNoSourceTextWidget.getForeground(), instructionPointerColor);
1008+
fNoSourceTextWidget.setStyleRange(currentSelection);
1009+
1010+
// move cursor to ensure scrolling
1011+
fNoSourceTextWidget.setSelection(instructionPosition.getOffset());
1012+
}
1013+
1014+
private Map<MethodIdentifier, LineRegion> methodsToInstructionRegions;
1015+
private Map<MethodIdentifier, LineRegion> getMethodsToInstructionRegionCache() {
1016+
return methodsToInstructionRegions = Objects.requireNonNullElseGet(methodsToInstructionRegions, this::createCache);
1017+
}
1018+
1019+
private Map<MethodIdentifier, LineRegion> createCache() {
1020+
Pattern methodDescriptorPattern = Pattern.compile("^\\s+// Method descriptor #\\d+ (.+)$"); //$NON-NLS-1$
1021+
StyledTextContent content= fNoSourceTextWidget.getContent();
1022+
Map<MethodIdentifier, LineRegion> cache = new HashMap<>();
1023+
for(int currentLine = 0; currentLine < content.getLineCount(); currentLine++) {
1024+
String line= content.getLine(currentLine);
1025+
Matcher descriptorMatcher= methodDescriptorPattern.matcher(line);
1026+
if (descriptorMatcher.matches()) {
1027+
String methodSignature = descriptorMatcher.group(1);
1028+
for(; currentLine < content.getLineCount(); currentLine++) {
1029+
// skip lines with leading slashes
1030+
line = content.getLine(currentLine);
1031+
if (!line.trim().startsWith("//")) { //$NON-NLS-1$
1032+
break;
1033+
}
1034+
}
1035+
1036+
int methodNameEnd = line.indexOf('(');
1037+
int methodNameStart = line.lastIndexOf(' ', methodNameEnd);
1038+
if (methodNameStart != -1) {
1039+
String methodName= line.substring(methodNameStart + 1, methodNameEnd);
1040+
int indentation = getIndentation(line);
1041+
int startLine = ++currentLine;
1042+
for(; currentLine < content.getLineCount() && getIndentation(content.getLine(currentLine)) > indentation; currentLine++) {
1043+
// find end of method
1044+
}
1045+
int endLine = currentLine;
1046+
cache.put(new MethodIdentifier(methodName, methodSignature), new LineRegion(startLine, endLine));
1047+
}
1048+
}
1049+
}
1050+
return cache;
1051+
}
1052+
1053+
private int getIndentation(String line) {
1054+
return line.length() - line.stripLeading().length();
1055+
}
1056+
1057+
private Region findInstruction(LineRegion methodBounds, StyledTextContent content, long codeIndex) {
1058+
for(int currentLine = methodBounds.startLine(); currentLine < methodBounds.endLine(); currentLine++) {
1059+
String line= content.getLine(currentLine);
1060+
if (line.trim().startsWith(codeIndex + " ")) { //$NON-NLS-1$
1061+
return new Region(content.getOffsetAtLine(currentLine), line.length());
1062+
}
1063+
}
1064+
return null;
1065+
}
1066+
1067+
private void initInstructionPointerColor() {
1068+
String color= getPreferenceStore().getString(IUIConstants.INSTRUCTION_POINTER_COLOR_PREFERENCE_KEY);
1069+
String[] splitColor = color.split(","); //$NON-NLS-1$
1070+
if (splitColor.length != 3) {
1071+
return;
1072+
}
1073+
try {
1074+
instructionPointerColor = new Color(Integer.parseInt(splitColor[0]), Integer.parseInt(splitColor[1]), Integer.parseInt(splitColor[2]));
1075+
} catch (NumberFormatException e) {
1076+
// use default color
1077+
}
1078+
}
1079+
9671080
/*
9681081
* @see ClassFileDocumentProvider.InputChangeListener#inputChanged(IClassFileEditorInput)
9691082
*/
@@ -1032,4 +1145,11 @@ public void setFocus() {
10321145
fSourceAttachmentForm.setFocus();
10331146
}
10341147
}
1148+
1149+
/*
1150+
* Used for testing.
1151+
*/
1152+
public StyledText getNoSourceTextWidget() {
1153+
return fNoSourceTextWidget;
1154+
}
10351155
}

0 commit comments

Comments
 (0)