Skip to content

Commit 12d6f45

Browse files
committed
GH-1874: show spring data aot codelens directly on the method, not above the javadoc
1 parent 0fad3f3 commit 12d6f45

2 files changed

Lines changed: 146 additions & 1 deletion

File tree

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataCodeLensProvider.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
import java.util.stream.Collectors;
2020

2121
import org.apache.commons.text.StringEscapeUtils;
22+
import org.eclipse.jdt.core.dom.ASTNode;
2223
import org.eclipse.jdt.core.dom.ASTVisitor;
2324
import org.eclipse.jdt.core.dom.CompilationUnit;
2425
import org.eclipse.jdt.core.dom.IMethodBinding;
26+
import org.eclipse.jdt.core.dom.IExtendedModifier;
2527
import org.eclipse.jdt.core.dom.MethodDeclaration;
2628
import org.eclipse.lsp4j.CodeLens;
2729
import org.eclipse.lsp4j.Command;
@@ -145,7 +147,8 @@ private List<CodeLens> createCodeLenses(IJavaProject project, MethodDeclaration
145147

146148
try {
147149
IMethodBinding mb = node.resolveBinding();
148-
Position startPos = document.toPosition(node.getStartPosition());
150+
int anchorOffset = methodHeaderAnchorOffset(node);
151+
Position startPos = document.toPosition(anchorOffset);
149152
Position endPos = document.toPosition(node.getName().getStartPosition() + node.getName().getLength());
150153
Range range = new Range(startPos, endPos);
151154
AnnotationHierarchies hierarchyAnnot = AnnotationHierarchies.get(node);
@@ -192,6 +195,21 @@ private List<CodeLens> createCodeLenses(IJavaProject project, MethodDeclaration
192195
}
193196
return codeLenses;
194197
}
198+
199+
/**
200+
* Offset for the code lens range start so the client shows lenses below Javadoc but above
201+
* method annotations (same attachment style as annotation-targeted lenses).
202+
*/
203+
static int methodHeaderAnchorOffset(MethodDeclaration node) {
204+
List<IExtendedModifier> modifiers = node.modifiers();
205+
if (!modifiers.isEmpty() && modifiers.get(0) instanceof ASTNode first) {
206+
return first.getStartPosition();
207+
}
208+
if (node.getReturnType2() != null) {
209+
return node.getReturnType2().getStartPosition();
210+
}
211+
return node.getName().getStartPosition();
212+
}
195213

196214
private Optional<CodeLens> createRefreshCodeLens(IJavaProject project, String title, Range range) {
197215
return repositoryMetadataService.regenerateMetadataCommand(project).map(refreshCmd -> {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Broadcom, Inc.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.data;
12+
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
import static org.junit.jupiter.api.Assertions.assertNotNull;
15+
import static org.junit.jupiter.api.Assertions.assertNull;
16+
import static org.junit.jupiter.api.Assertions.assertTrue;
17+
18+
import java.util.List;
19+
import java.util.Map;
20+
21+
import org.eclipse.jdt.core.JavaCore;
22+
import org.eclipse.jdt.core.dom.AST;
23+
import org.eclipse.jdt.core.dom.ASTNode;
24+
import org.eclipse.jdt.core.dom.ASTParser;
25+
import org.eclipse.jdt.core.dom.CompilationUnit;
26+
import org.eclipse.jdt.core.dom.IExtendedModifier;
27+
import org.eclipse.jdt.core.dom.MethodDeclaration;
28+
import org.eclipse.jdt.core.dom.TypeDeclaration;
29+
import org.junit.jupiter.api.Test;
30+
31+
/**
32+
* Covers {@link DataRepositoryAotMetadataCodeLensProvider#methodHeaderAnchorOffset(MethodDeclaration)}:
33+
* code lens ranges must start after Javadoc and on the first modifier or return type so clients
34+
* render lenses between Javadoc and {@code @Query} (or other annotations).
35+
*/
36+
class DataRepositoryAotMetadataCodeLensProviderMethodHeaderAnchorTest {
37+
38+
private static CompilationUnit parse(String source) {
39+
ASTParser parser = ASTParser.newParser(AST.JLS25);
40+
parser.setSource(source.toCharArray());
41+
parser.setKind(ASTParser.K_COMPILATION_UNIT);
42+
parser.setResolveBindings(false);
43+
Map<String, String> options = JavaCore.getOptions();
44+
JavaCore.setComplianceOptions(JavaCore.VERSION_21, options);
45+
parser.setCompilerOptions(options);
46+
return (CompilationUnit) parser.createAST(null);
47+
}
48+
49+
private static MethodDeclaration firstMethod(CompilationUnit cu) {
50+
TypeDeclaration type = (TypeDeclaration) cu.types().get(0);
51+
MethodDeclaration[] methods = type.getMethods();
52+
assertTrue(methods.length > 0, "fixture should declare at least one method");
53+
return methods[0];
54+
}
55+
56+
@Test
57+
void anchorIsReturnTypeWhenMethodHasJavadocButNoAnnotations() {
58+
String source = """
59+
package p;
60+
61+
import java.util.List;
62+
63+
public interface Repo {
64+
/**
65+
* Some javadoc.
66+
*/
67+
List<String> findCustomers(String id);
68+
}
69+
""";
70+
CompilationUnit cu = parse(source);
71+
MethodDeclaration method = firstMethod(cu);
72+
assertNotNull(method.getJavadoc());
73+
74+
int anchor = DataRepositoryAotMetadataCodeLensProvider.methodHeaderAnchorOffset(method);
75+
int javadocEnd = method.getJavadoc().getStartPosition() + method.getJavadoc().getLength();
76+
assertTrue(anchor >= javadocEnd, "code lens start must be at or after the closing */");
77+
assertEquals(method.getReturnType2().getStartPosition(), anchor);
78+
}
79+
80+
@Test
81+
void anchorIsFirstAnnotationWhenJavadocPrecedesAnnotations() {
82+
String source = """
83+
package p;
84+
85+
import java.util.List;
86+
87+
public interface Repo {
88+
/**
89+
* Some javadoc.
90+
*/
91+
@Deprecated
92+
List<String> findCustomers(String id);
93+
}
94+
""";
95+
CompilationUnit cu = parse(source);
96+
MethodDeclaration method = firstMethod(cu);
97+
assertNotNull(method.getJavadoc());
98+
99+
int anchor = DataRepositoryAotMetadataCodeLensProvider.methodHeaderAnchorOffset(method);
100+
int javadocEnd = method.getJavadoc().getStartPosition() + method.getJavadoc().getLength();
101+
assertTrue(anchor >= javadocEnd, "code lens start must be at or after the closing */");
102+
103+
List<IExtendedModifier> modifiers = method.modifiers();
104+
IExtendedModifier first = modifiers.get(0);
105+
assertTrue(first instanceof ASTNode, "first modifier should be an AST node");
106+
assertEquals(((ASTNode) first).getStartPosition(), anchor);
107+
}
108+
109+
@Test
110+
void anchorMatchesReturnTypeWhenNoJavadoc() {
111+
String source = """
112+
package p;
113+
114+
import java.util.List;
115+
116+
public interface Repo {
117+
List<String> findCustomers(String id);
118+
}
119+
""";
120+
CompilationUnit cu = parse(source);
121+
MethodDeclaration method = firstMethod(cu);
122+
assertNull(method.getJavadoc());
123+
124+
int anchor = DataRepositoryAotMetadataCodeLensProvider.methodHeaderAnchorOffset(method);
125+
assertEquals(method.getReturnType2().getStartPosition(), anchor);
126+
}
127+
}

0 commit comments

Comments
 (0)