Skip to content

Commit 51a7454

Browse files
authored
Stickylines (#2149)
* StickyLines implementation using AST - substitute for #1851
1 parent 905eb1c commit 51a7454

File tree

2 files changed

+367
-0
lines changed

2 files changed

+367
-0
lines changed

org.eclipse.jdt.ui/plugin.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7853,4 +7853,18 @@
78537853
file-extensions="class without source">
78547854
</file-association>
78557855
</extension>
7856+
<extension
7857+
point="org.eclipse.ui.editors.stickyLinesProviders">
7858+
<stickyLinesProvider
7859+
class="org.eclipse.jdt.internal.ui.javaeditor.JavaStickyLinesProvider"
7860+
id="org.eclipse.jdt.internal.ui.StickyLinesProviderJava">
7861+
<enabledWhen>
7862+
<and>
7863+
<with variable="editor">
7864+
<instanceof value="org.eclipse.jdt.internal.ui.javaeditor.JavaEditor"/>
7865+
</with>
7866+
</and>
7867+
</enabledWhen>
7868+
</stickyLinesProvider>
7869+
</extension>
78567870
</plugin>
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Red Hat Inc. 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+
* Red Hat Inc. - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.jdt.internal.ui.javaeditor;
15+
16+
import java.util.HashMap;
17+
import java.util.LinkedList;
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.regex.Matcher;
21+
import java.util.regex.Pattern;
22+
23+
import org.eclipse.swt.custom.StyledText;
24+
25+
import org.eclipse.jface.text.AbstractDocument;
26+
import org.eclipse.jface.text.IDocument;
27+
import org.eclipse.jface.text.ITextViewerExtension5;
28+
import org.eclipse.jface.text.source.ISourceViewer;
29+
30+
import org.eclipse.ui.IEditorInput;
31+
import org.eclipse.ui.IEditorPart;
32+
33+
import org.eclipse.ui.texteditor.stickyscroll.IStickyLine;
34+
import org.eclipse.ui.texteditor.stickyscroll.IStickyLinesProvider;
35+
import org.eclipse.ui.texteditor.stickyscroll.StickyLine;
36+
37+
import org.eclipse.jdt.core.ICompilationUnit;
38+
import org.eclipse.jdt.core.IJavaElement;
39+
import org.eclipse.jdt.core.ITypeRoot;
40+
import org.eclipse.jdt.core.JavaModelException;
41+
import org.eclipse.jdt.core.WorkingCopyOwner;
42+
import org.eclipse.jdt.core.dom.ASTNode;
43+
import org.eclipse.jdt.core.dom.ASTParser;
44+
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
45+
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
46+
import org.eclipse.jdt.core.dom.CompilationUnit;
47+
import org.eclipse.jdt.core.dom.DoStatement;
48+
import org.eclipse.jdt.core.dom.EnhancedForStatement;
49+
import org.eclipse.jdt.core.dom.ForStatement;
50+
import org.eclipse.jdt.core.dom.IfStatement;
51+
import org.eclipse.jdt.core.dom.LambdaExpression;
52+
import org.eclipse.jdt.core.dom.MethodDeclaration;
53+
import org.eclipse.jdt.core.dom.Modifier;
54+
import org.eclipse.jdt.core.dom.ModuleDeclaration;
55+
import org.eclipse.jdt.core.dom.NodeFinder;
56+
import org.eclipse.jdt.core.dom.RecordDeclaration;
57+
import org.eclipse.jdt.core.dom.Statement;
58+
import org.eclipse.jdt.core.dom.SwitchExpression;
59+
import org.eclipse.jdt.core.dom.SwitchStatement;
60+
import org.eclipse.jdt.core.dom.TryStatement;
61+
import org.eclipse.jdt.core.dom.TypeDeclarationStatement;
62+
import org.eclipse.jdt.core.dom.WhileStatement;
63+
64+
import org.eclipse.jdt.internal.corext.dom.IASTSharedValues;
65+
66+
import org.eclipse.jdt.ui.JavaUI;
67+
68+
69+
public class JavaStickyLinesProvider implements IStickyLinesProvider {
70+
71+
private final static int IGNORE_LINE_INDENTATION= -1;
72+
private final static Pattern ELSE_PATTERN= Pattern.compile("else[\\s,{]"); //$NON-NLS-1$
73+
private final static Pattern DO_PATTERN= Pattern.compile("do[\\s,{]"); //$NON-NLS-1$
74+
private final static Pattern WHILE_PATTERN= Pattern.compile("while[\\s,{]"); //$NON-NLS-1$
75+
private final static Pattern FOR_PATTERN= Pattern.compile("for[\\s,{]"); //$NON-NLS-1$
76+
private final static Pattern TRY_PATTERN= Pattern.compile("try[\\s,{]"); //$NON-NLS-1$
77+
private final static Pattern NEW_PATTERN= Pattern.compile("new[\\s,{]"); //$NON-NLS-1$
78+
79+
private static Map<ITypeRoot, CompilationUnit> cuMap= new HashMap<>();
80+
private static Map<ITypeRoot, Long> timeMap= new HashMap<>();
81+
82+
@Override
83+
public List<IStickyLine> getStickyLines(ISourceViewer sourceViewer, int lineNumber, StickyLinesProperties properties) {
84+
final LinkedList<IStickyLine> stickyLines= new LinkedList<>();
85+
JavaEditor javaEditor= (JavaEditor) properties.editor();
86+
StyledText textWidget= sourceViewer.getTextWidget();
87+
ICompilationUnit unit= null;
88+
int textWidgetLineNumber= mapLineNumberToWidget(sourceViewer, lineNumber);
89+
int startIndentation= 0;
90+
String line= textWidget.getLine(textWidgetLineNumber);
91+
try {
92+
startIndentation= getIndentation(line);
93+
while (startIndentation == IGNORE_LINE_INDENTATION) {
94+
textWidgetLineNumber--;
95+
if (textWidgetLineNumber <= 0) {
96+
break;
97+
}
98+
line= textWidget.getLine(textWidgetLineNumber);
99+
startIndentation= getIndentation(line);
100+
}
101+
} catch (IllegalArgumentException e) {
102+
stickyLines.clear();
103+
}
104+
105+
if (textWidgetLineNumber > 0) {
106+
ITypeRoot typeRoot= getJavaInput(javaEditor);
107+
ASTNode node= null;
108+
if (typeRoot != null) {
109+
WorkingCopyOwner workingCopyOwner= new WorkingCopyOwner() {
110+
};
111+
CompilationUnit cu= cuMap.get(typeRoot);
112+
IDocument document= sourceViewer.getDocument();
113+
long currTime= getDocumentTimestamp(document);
114+
if (cu != null && !typeRoot.isReadOnly()) {
115+
Long oldTime= timeMap.get(typeRoot);
116+
if (oldTime == null || currTime != oldTime.longValue()) {
117+
cu= null;
118+
}
119+
}
120+
try {
121+
if (cu == null) {
122+
unit= typeRoot.getWorkingCopy(workingCopyOwner, null);
123+
if (unit != null) {
124+
cu= convertICompilationUnitToCompilationUnit(unit);
125+
}
126+
}
127+
if (cu == null) {
128+
return stickyLines;
129+
}
130+
cuMap.put(typeRoot, cu);
131+
timeMap.put(typeRoot, Long.valueOf(currTime));
132+
node= getASTNode(cu, mapWidgetToLineNumber(sourceViewer, textWidgetLineNumber)+1, line);
133+
while (node == null && textWidgetLineNumber > 0) {
134+
line= textWidget.getLine(--textWidgetLineNumber);
135+
startIndentation= getIndentation(line);
136+
while (startIndentation == IGNORE_LINE_INDENTATION && textWidgetLineNumber > 0) {
137+
line= textWidget.getLine(--textWidgetLineNumber);
138+
startIndentation= getIndentation(line);
139+
}
140+
if (textWidgetLineNumber > 0) {
141+
int position= cu.getPosition(mapWidgetToLineNumber(sourceViewer, textWidgetLineNumber) + 1, startIndentation);
142+
if (position >= 0) {
143+
node= getASTNode(cu, mapWidgetToLineNumber(sourceViewer, textWidgetLineNumber)+1, line);
144+
}
145+
}
146+
}
147+
if (node != null) {
148+
boolean addStickyLine= false;
149+
int nodeLineNumber= 0;
150+
while (node != null) {
151+
addStickyLine= false;
152+
switch (node.getNodeType()) {
153+
case ASTNode.ANNOTATION_TYPE_DECLARATION:
154+
case ASTNode.TYPE_DECLARATION:
155+
case ASTNode.ENUM_DECLARATION:
156+
addStickyLine= true;
157+
ASTNode name= ((AbstractTypeDeclaration)node).getName();
158+
nodeLineNumber= cu.getLineNumber(name.getStartPosition());
159+
break;
160+
case ASTNode.TYPE_DECLARATION_STATEMENT:
161+
addStickyLine= true;
162+
ASTNode typeDeclStmtName= ((TypeDeclarationStatement)node).getDeclaration().getName();
163+
nodeLineNumber= cu.getLineNumber(typeDeclStmtName.getStartPosition());
164+
break;
165+
case ASTNode.METHOD_DECLARATION:
166+
addStickyLine= true;
167+
ASTNode methodName= ((MethodDeclaration)node).getName();
168+
nodeLineNumber= cu.getLineNumber(methodName.getStartPosition());
169+
break;
170+
case ASTNode.RECORD_DECLARATION:
171+
addStickyLine= true;
172+
ASTNode recordName= ((RecordDeclaration)node).getName();
173+
nodeLineNumber= cu.getLineNumber(recordName.getStartPosition());
174+
break;
175+
case ASTNode.MODULE_DECLARATION:
176+
addStickyLine= true;
177+
ASTNode moduleName= ((ModuleDeclaration)node).getName();
178+
nodeLineNumber= cu.getLineNumber(moduleName.getStartPosition());
179+
break;
180+
case ASTNode.LAMBDA_EXPRESSION:
181+
addStickyLine= true;
182+
ASTNode lambdaBody= ((LambdaExpression)node).getBody();
183+
nodeLineNumber= cu.getLineNumber(lambdaBody.getStartPosition());
184+
break;
185+
case ASTNode.IF_STATEMENT:
186+
addStickyLine= true;
187+
IfStatement ifStmt= (IfStatement)node;
188+
ASTNode ifExpression= ifStmt.getExpression();
189+
nodeLineNumber= cu.getLineNumber(ifExpression.getStartPosition());
190+
Statement elseStmt= ifStmt.getElseStatement();
191+
if (elseStmt != null) {
192+
int elseLine= cu.getLineNumber(elseStmt.getStartPosition());
193+
if (elseLine <= mapWidgetToLineNumber(sourceViewer, textWidgetLineNumber + 1)) {
194+
Pattern p= ELSE_PATTERN;
195+
nodeLineNumber= elseLine;
196+
String stmtLine= textWidget.getLine(mapLineNumberToWidget(sourceViewer, nodeLineNumber - 1));
197+
Matcher m= p.matcher(stmtLine);
198+
while (!m.find() && nodeLineNumber > 1) {
199+
nodeLineNumber--;
200+
stmtLine= textWidget.getLine(mapLineNumberToWidget(sourceViewer, nodeLineNumber - 1));
201+
m= p.matcher(stmtLine);
202+
}
203+
node= node.getParent();
204+
}
205+
}
206+
while (node.getLocationInParent() == IfStatement.ELSE_STATEMENT_PROPERTY) {
207+
node= node.getParent();
208+
}
209+
break;
210+
case ASTNode.ENHANCED_FOR_STATEMENT:
211+
addStickyLine= true;
212+
ASTNode enhancedForExpression= ((EnhancedForStatement)node).getExpression();
213+
nodeLineNumber= cu.getLineNumber(enhancedForExpression.getStartPosition());
214+
break;
215+
case ASTNode.SWITCH_EXPRESSION:
216+
addStickyLine= true;
217+
ASTNode switchExpExpression= ((SwitchExpression)node).getExpression();
218+
nodeLineNumber= cu.getLineNumber(switchExpExpression.getStartPosition());
219+
break;
220+
case ASTNode.SWITCH_STATEMENT:
221+
addStickyLine= true;
222+
ASTNode switchStmtExpression= ((SwitchStatement)node).getExpression();
223+
nodeLineNumber= cu.getLineNumber(switchStmtExpression.getStartPosition());
224+
break;
225+
case ASTNode.WHILE_STATEMENT:
226+
case ASTNode.DO_STATEMENT:
227+
case ASTNode.TRY_STATEMENT:
228+
case ASTNode.FOR_STATEMENT:
229+
case ASTNode.ANONYMOUS_CLASS_DECLARATION:
230+
addStickyLine= true;
231+
ASTNode bodyProperty= null;
232+
Pattern pattern= null;
233+
switch (node.getNodeType()) {
234+
case ASTNode.DO_STATEMENT:
235+
bodyProperty= ((DoStatement)node).getBody();
236+
pattern= DO_PATTERN;
237+
break;
238+
case ASTNode.FOR_STATEMENT:
239+
bodyProperty= ((ForStatement)node).getBody();
240+
pattern= FOR_PATTERN;
241+
break;
242+
case ASTNode.WHILE_STATEMENT:
243+
bodyProperty= ((WhileStatement)node).getBody();
244+
pattern= WHILE_PATTERN;
245+
break;
246+
case ASTNode.TRY_STATEMENT:
247+
bodyProperty= ((TryStatement)node).getBody();
248+
pattern= TRY_PATTERN;
249+
break;
250+
case ASTNode.ANONYMOUS_CLASS_DECLARATION:
251+
bodyProperty= (ASTNode) ((AnonymousClassDeclaration)node).bodyDeclarations().get(0);
252+
pattern= NEW_PATTERN;
253+
break;
254+
}
255+
if (bodyProperty != null && pattern != null) {
256+
nodeLineNumber= cu.getLineNumber(bodyProperty.getStartPosition());
257+
String stmtLine= textWidget.getLine(mapLineNumberToWidget(sourceViewer, nodeLineNumber - 1));
258+
Matcher m= pattern.matcher(stmtLine);
259+
while (!m.find() && nodeLineNumber > 1) {
260+
nodeLineNumber--;
261+
stmtLine= textWidget.getLine(mapLineNumberToWidget(sourceViewer, nodeLineNumber - 1));
262+
m= pattern.matcher(stmtLine);
263+
}
264+
}
265+
break;
266+
case ASTNode.SWITCH_CASE:
267+
case ASTNode.CASE_DEFAULT_EXPRESSION:
268+
case ASTNode.CATCH_CLAUSE:
269+
nodeLineNumber= cu.getLineNumber(node.getStartPosition());
270+
break;
271+
default:
272+
break;
273+
}
274+
if (addStickyLine && nodeLineNumber <= lineNumber) {
275+
stickyLines.addFirst(new StickyLine(nodeLineNumber - 1, sourceViewer));
276+
}
277+
if (node.getNodeType() == ASTNode.MODIFIER) {
278+
Modifier modifier= (Modifier)node;
279+
startIndentation+= modifier.getLength();
280+
node= getASTNode(cu, mapWidgetToLineNumber(sourceViewer, textWidgetLineNumber+1), line);
281+
} else {
282+
node= node.getParent();
283+
}
284+
}
285+
}
286+
if (unit != null && !typeRoot.isReadOnly()) {
287+
unit.discardWorkingCopy();
288+
}
289+
} catch (JavaModelException e) {
290+
// do nothing
291+
}
292+
}
293+
}
294+
return stickyLines;
295+
}
296+
297+
private long getDocumentTimestamp(IDocument document) {
298+
if (document instanceof AbstractDocument ad) {
299+
return ad.getModificationStamp();
300+
}
301+
return 0;
302+
}
303+
304+
private ASTNode getASTNode(CompilationUnit cu, int lineNum, String line) {
305+
int linePos= cu.getPosition(lineNum, 0);
306+
if (linePos >= 0) {
307+
NodeFinder finder= new NodeFinder(cu, linePos, line.length());
308+
return finder.getCoveringNode();
309+
}
310+
return null;
311+
}
312+
313+
public static ITypeRoot getJavaInput(IEditorPart part) {
314+
IEditorInput editorInput= part.getEditorInput();
315+
if (editorInput != null) {
316+
IJavaElement input= JavaUI.getEditorInputJavaElement(editorInput);
317+
if (input instanceof ITypeRoot) {
318+
return (ITypeRoot) input;
319+
}
320+
}
321+
return null;
322+
}
323+
324+
private int mapLineNumberToWidget(ISourceViewer sourceViewer, int line) {
325+
if (sourceViewer instanceof ITextViewerExtension5 extension) {
326+
return extension.modelLine2WidgetLine(line);
327+
}
328+
return line;
329+
}
330+
331+
private int mapWidgetToLineNumber(ISourceViewer sourceViewer, int line) {
332+
if (sourceViewer instanceof ITextViewerExtension5 extension) {
333+
return extension.widgetLine2ModelLine(line);
334+
}
335+
return line;
336+
}
337+
338+
private int getIndentation(String line) {
339+
if (line == null || line.isBlank()) {
340+
return IGNORE_LINE_INDENTATION;
341+
}
342+
return line.length() - line.stripLeading().length();
343+
}
344+
345+
private static CompilationUnit convertICompilationUnitToCompilationUnit(ICompilationUnit compilationUnit) {
346+
ASTParser parser= ASTParser.newParser(IASTSharedValues.SHARED_AST_LEVEL);
347+
parser.setKind(ASTParser.K_COMPILATION_UNIT);
348+
parser.setSource(compilationUnit);
349+
parser.setResolveBindings(false);
350+
return (CompilationUnit) parser.createAST(null);
351+
}
352+
353+
}

0 commit comments

Comments
 (0)