Skip to content

Commit 9d1384a

Browse files
committed
Allow highlighting instructions in ClassFileEditor
1 parent 7c9a92e commit 9d1384a

3 files changed

Lines changed: 216 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: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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+
import org.eclipse.swt.graphics.Color;
29+
30+
import org.eclipse.core.runtime.CoreException;
31+
import org.eclipse.core.runtime.NullProgressMonitor;
32+
33+
import org.eclipse.core.resources.IncrementalProjectBuilder;
34+
35+
import org.eclipse.jdt.core.IJavaProject;
36+
import org.eclipse.jdt.core.IPackageFragment;
37+
38+
import org.eclipse.jdt.internal.ui.javaeditor.ClassFileEditor;
39+
import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;
40+
41+
public class ClassFileEditorTests {
42+
private static final String TYPE_NAME= "HelloWorld";
43+
44+
private String fileName;
45+
private String className;
46+
private IJavaProject javaProject;
47+
48+
@BeforeEach
49+
void setUp() throws Exception {
50+
fileName= TYPE_NAME + ".java";
51+
className= TYPE_NAME + ".class";
52+
javaProject = setUpProject();
53+
}
54+
55+
@AfterEach
56+
void tearDown() throws Exception {
57+
if (javaProject != null) {
58+
JavaProjectHelper.delete(javaProject);
59+
}
60+
}
61+
62+
private IJavaProject setUpProject() throws Exception {
63+
javaProject= JavaProjectHelper.createJavaProject(ClassFileInputTests.class.getSimpleName(), "bin");
64+
JavaProjectHelper.addSourceContainer(javaProject, "src");
65+
IPackageFragment fragment= javaProject.findPackageFragment(javaProject.getProject().getFullPath().append("src"));
66+
String content= """
67+
public class HelloWorld {
68+
void main() {
69+
IO.println("Hello World");
70+
}
71+
}
72+
""";
73+
fragment.createCompilationUnit(fileName, content, true, new NullProgressMonitor());
74+
javaProject.getProject().build(IncrementalProjectBuilder.FULL_BUILD, new NullProgressMonitor());
75+
return javaProject;
76+
}
77+
78+
@Test
79+
void testHighlightRange() throws CoreException {
80+
javaProject.getProject().getFile("src/" + fileName).delete(true,new NullProgressMonitor());
81+
ClassFileEditor classEditor= (ClassFileEditor) EditorUtility.openInEditor(javaProject.getProject().getFile("bin/" + className));
82+
assertFalse(classEditor.isEditable());
83+
classEditor.highlightInstruction("main", "()V", 3, "doesnotexist");
84+
StyledText noSourceTextWidget= classEditor.getNoSourceTextWidget();
85+
StyledTextContent content= noSourceTextWidget.getContent();
86+
StyleRange[] ranges= noSourceTextWidget.getStyleRanges();
87+
assertEquals(1, ranges.length);
88+
StyleRange range= ranges[0];
89+
String highlighted= content.getTextRange(range.start, range.length);
90+
assertEquals(" 3 dup", highlighted);
91+
assertEquals(new Color(0,255,0), range.background);
92+
}
93+
}

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

Lines changed: 120 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;
@@ -557,6 +565,9 @@ private boolean isEqualInput(IEditorInput input1, IEditorInput input2) {
557565
}
558566
}
559567

568+
private record MethodIdentifier(String name, String signature) {}
569+
private record LineRegion(int startLine, int endLine) {}
570+
560571
private StackLayout fStackLayout;
561572
private Composite fParent;
562573

@@ -585,7 +596,9 @@ private boolean isEqualInput(IEditorInput input1, IEditorInput input2) {
585596
* @since 3.3
586597
*/
587598
private StyledText fNoSourceTextWidget;
599+
private StyleRange currentSelection;
588600
private volatile boolean disposed;
601+
private Color instructionPointerColor = new Color(0, 255, 0);
589602

590603
/**
591604
* Default constructor.
@@ -736,6 +749,7 @@ protected IEditorInput transformEditorInput(IEditorInput input) throws CoreExcep
736749
@Override
737750
public void init(IEditorSite site, IEditorInput input) throws PartInitException {
738751
JavaCore.runReadOnly(() -> super.init(site, input));
752+
initInstructionPointerColor();
739753
}
740754
/*
741755
* @see AbstractTextEditor#doSetInput(IEditorInput)
@@ -964,6 +978,104 @@ private static boolean hasSource(IClassFile file) {
964978
}
965979
}
966980

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

0 commit comments

Comments
 (0)