Skip to content

Commit b4ff980

Browse files
committed
update to spel reference impl
1 parent 29b7828 commit b4ff980

13 files changed

Lines changed: 349 additions & 57 deletions

plugin/src/main/java/com/intellij/spring/impl/ide/inject/SpELPropertyReferenceContributor.java

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import consulo.language.util.ProcessingContext;
2626
import consulo.spring.spel.language.SpELLanguage;
2727
import consulo.spring.spel.language.impl.psi.SpELPlaceholderKeyImpl;
28-
import consulo.spring.spel.language.impl.psi.SpELPropertyPlaceholderImpl;
2928

3029
import java.util.ArrayList;
3130
import java.util.List;
@@ -36,28 +35,23 @@
3635
public class SpELPropertyReferenceContributor extends PsiReferenceContributor {
3736
@Override
3837
public void registerReferenceProviders(PsiReferenceRegistrar registrar) {
38+
// register on PLACEHOLDER_KEY element directly - works for both
39+
// standalone ${key:default} and ${key:default} inside string literals
3940
registrar.registerReferenceProvider(
40-
psiElement(SpELPropertyPlaceholderImpl.class),
41+
psiElement(SpELPlaceholderKeyImpl.class),
4142
new PsiReferenceProvider() {
4243
@Override
4344
public PsiReference[] getReferencesByElement(PsiElement element, ProcessingContext context) {
44-
if (!(element instanceof SpELPropertyPlaceholderImpl placeholder)) {
45+
if (!(element instanceof SpELPlaceholderKeyImpl keyElement)) {
4546
return PsiReference.EMPTY_ARRAY;
4647
}
4748

48-
String key = placeholder.getPropertyKey();
49+
String key = keyElement.getText();
4950
if (key == null || key.isEmpty()) {
5051
return PsiReference.EMPTY_ARRAY;
5152
}
5253

53-
SpELPlaceholderKeyImpl keyElement = placeholder.getKeyElement();
54-
if (keyElement == null) {
55-
return PsiReference.EMPTY_ARRAY;
56-
}
57-
58-
int startOffset = keyElement.getStartOffsetInParent();
59-
TextRange range = new TextRange(startOffset, startOffset + keyElement.getTextLength());
60-
54+
TextRange range = TextRange.from(0, key.length());
6155
return new PsiReference[]{new SpELPropertyReference(element, range, key)};
6256
}
6357
}

plugin/src/main/java/com/intellij/spring/impl/ide/inject/SpELValueInjector.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,11 @@ else if (c == '}') {
7676
}
7777

7878
if (depth == 0) {
79-
// +1 for the opening quote of the string literal
79+
// After loop: i points past the closing }
80+
// Content to inject: content[spelStart+2 .. i-2] (between #{ and })
81+
// Text offset: +1 for the opening quote of the Java string literal
8082
int rangeStart = spelStart + 2 + 1;
81-
int rangeEnd = i - 1 + 1;
83+
int rangeEnd = i - 1 + 1; // i-1 is } in content, +1 for quote offset, exclusive end lands on }
8284

8385
if (rangeStart < rangeEnd) {
8486
if (!hasInjection) {
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
* Copyright 2013-2026 consulo.io
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package consulo.spring.spel.language.impl.highlight;
18+
19+
import com.intellij.java.language.psi.PsiField;
20+
import com.intellij.java.language.psi.PsiMethod;
21+
import consulo.annotation.access.RequiredReadAction;
22+
import consulo.codeEditor.DefaultLanguageHighlighterColors;
23+
import consulo.colorScheme.TextAttributesKey;
24+
import consulo.language.ast.ASTNode;
25+
import consulo.language.editor.annotation.AnnotationHolder;
26+
import consulo.language.editor.annotation.Annotator;
27+
import consulo.language.editor.annotation.HighlightSeverity;
28+
import consulo.language.psi.PsiElement;
29+
import consulo.spring.spel.language.SpELElementTypes;
30+
import consulo.spring.spel.language.SpELTokenTypes;
31+
import consulo.spring.spel.language.impl.psi.*;
32+
33+
public class SpELSemanticHighlighter implements Annotator {
34+
private static final TextAttributesKey PROPERTY_ACCESS = TextAttributesKey.createTextAttributesKey(
35+
"SPEL_PROPERTY_ACCESS", DefaultLanguageHighlighterColors.INSTANCE_FIELD
36+
);
37+
private static final TextAttributesKey METHOD_CALL = TextAttributesKey.createTextAttributesKey(
38+
"SPEL_METHOD_CALL", DefaultLanguageHighlighterColors.FUNCTION_CALL
39+
);
40+
private static final TextAttributesKey BEAN_REFERENCE = TextAttributesKey.createTextAttributesKey(
41+
"SPEL_BEAN_REFERENCE", DefaultLanguageHighlighterColors.INSTANCE_FIELD
42+
);
43+
private static final TextAttributesKey VARIABLE_REFERENCE = TextAttributesKey.createTextAttributesKey(
44+
"SPEL_VARIABLE_REFERENCE", DefaultLanguageHighlighterColors.LOCAL_VARIABLE
45+
);
46+
private static final TextAttributesKey TYPE_REFERENCE = TextAttributesKey.createTextAttributesKey(
47+
"SPEL_TYPE_REFERENCE", DefaultLanguageHighlighterColors.CLASS_NAME
48+
);
49+
private static final TextAttributesKey PLACEHOLDER_KEY_ATTR = TextAttributesKey.createTextAttributesKey(
50+
"SPEL_PLACEHOLDER_KEY", DefaultLanguageHighlighterColors.INSTANCE_FIELD
51+
);
52+
private static final TextAttributesKey STATIC_METHOD_CALL = TextAttributesKey.createTextAttributesKey(
53+
"SPEL_STATIC_METHOD_CALL", DefaultLanguageHighlighterColors.STATIC_METHOD
54+
);
55+
private static final TextAttributesKey STATIC_FIELD_ACCESS = TextAttributesKey.createTextAttributesKey(
56+
"SPEL_STATIC_FIELD_ACCESS", DefaultLanguageHighlighterColors.STATIC_FIELD
57+
);
58+
59+
@Override
60+
@RequiredReadAction
61+
public void annotate(PsiElement element, AnnotationHolder holder) {
62+
if (element instanceof SpELReferenceExpressionImpl refExpr) {
63+
highlightReferenceExpression(refExpr, holder);
64+
}
65+
else if (element instanceof SpELMethodCallExpressionImpl methodCall) {
66+
highlightMethodCall(methodCall, holder);
67+
}
68+
else if (element instanceof SpELBeanReferenceImpl beanRef) {
69+
highlightBeanReference(beanRef, holder);
70+
}
71+
else if (element instanceof SpELVariableReferenceImpl varRef) {
72+
highlightVariableReference(varRef, holder);
73+
}
74+
else if (element instanceof SpELTypeReferenceImpl typeRef) {
75+
highlightTypeReference(typeRef, holder);
76+
}
77+
else if (element instanceof SpELPlaceholderKeyImpl placeholderKey) {
78+
highlightPlaceholderKey(placeholderKey, holder);
79+
}
80+
}
81+
82+
@RequiredReadAction
83+
private void highlightReferenceExpression(SpELReferenceExpressionImpl refExpr, AnnotationHolder holder) {
84+
ASTNode idNode = findLastIdentifier(refExpr);
85+
if (idNode == null) {
86+
return;
87+
}
88+
89+
PsiElement resolved = refExpr.resolve();
90+
if (resolved instanceof PsiMethod method) {
91+
TextAttributesKey key = method.hasModifierProperty("static") ? STATIC_METHOD_CALL : METHOD_CALL;
92+
holder.newSilentAnnotation(HighlightSeverity.INFORMATION)
93+
.range(idNode)
94+
.textAttributes(key)
95+
.create();
96+
}
97+
else if (resolved instanceof PsiField field) {
98+
TextAttributesKey key = field.hasModifierProperty("static") ? STATIC_FIELD_ACCESS : PROPERTY_ACCESS;
99+
holder.newSilentAnnotation(HighlightSeverity.INFORMATION)
100+
.range(idNode)
101+
.textAttributes(key)
102+
.create();
103+
}
104+
else if (refExpr.getQualifier() == null) {
105+
// unqualified reference = bean name, highlight like instance field
106+
holder.newSilentAnnotation(HighlightSeverity.INFORMATION)
107+
.range(idNode)
108+
.textAttributes(BEAN_REFERENCE)
109+
.create();
110+
}
111+
else {
112+
// qualified but unresolved - still highlight as property
113+
holder.newSilentAnnotation(HighlightSeverity.INFORMATION)
114+
.range(idNode)
115+
.textAttributes(PROPERTY_ACCESS)
116+
.create();
117+
}
118+
}
119+
120+
@RequiredReadAction
121+
private void highlightMethodCall(SpELMethodCallExpressionImpl methodCall, AnnotationHolder holder) {
122+
// the method name is inside the REFERENCE_EXPRESSION child - handled there
123+
}
124+
125+
@RequiredReadAction
126+
private void highlightBeanReference(SpELBeanReferenceImpl beanRef, AnnotationHolder holder) {
127+
ASTNode idNode = beanRef.getNode().findChildByType(SpELTokenTypes.IDENTIFIER);
128+
if (idNode != null) {
129+
holder.newSilentAnnotation(HighlightSeverity.INFORMATION)
130+
.range(idNode)
131+
.textAttributes(BEAN_REFERENCE)
132+
.create();
133+
}
134+
}
135+
136+
@RequiredReadAction
137+
private void highlightVariableReference(SpELVariableReferenceImpl varRef, AnnotationHolder holder) {
138+
ASTNode idNode = varRef.getNode().findChildByType(SpELTokenTypes.IDENTIFIER);
139+
if (idNode != null) {
140+
holder.newSilentAnnotation(HighlightSeverity.INFORMATION)
141+
.range(idNode)
142+
.textAttributes(VARIABLE_REFERENCE)
143+
.create();
144+
}
145+
}
146+
147+
@RequiredReadAction
148+
private void highlightTypeReference(SpELTypeReferenceImpl typeRef, AnnotationHolder holder) {
149+
ASTNode qualifiedName = typeRef.getNode().findChildByType(SpELElementTypes.QUALIFIED_NAME);
150+
if (qualifiedName != null) {
151+
// highlight each identifier segment in the qualified name as class name
152+
for (ASTNode child = qualifiedName.getFirstChildNode(); child != null; child = child.getTreeNext()) {
153+
if (child.getElementType() == SpELTokenTypes.IDENTIFIER) {
154+
holder.newSilentAnnotation(HighlightSeverity.INFORMATION)
155+
.range(child)
156+
.textAttributes(TYPE_REFERENCE)
157+
.create();
158+
}
159+
}
160+
}
161+
}
162+
163+
@RequiredReadAction
164+
private void highlightPlaceholderKey(SpELPlaceholderKeyImpl placeholderKey, AnnotationHolder holder) {
165+
holder.newSilentAnnotation(HighlightSeverity.INFORMATION)
166+
.range(placeholderKey)
167+
.textAttributes(PLACEHOLDER_KEY_ATTR)
168+
.create();
169+
}
170+
171+
private ASTNode findLastIdentifier(SpELReferenceExpressionImpl refExpr) {
172+
ASTNode lastId = null;
173+
for (ASTNode child = refExpr.getNode().getFirstChildNode(); child != null; child = child.getTreeNext()) {
174+
if (child.getElementType() == SpELTokenTypes.IDENTIFIER) {
175+
lastId = child;
176+
}
177+
}
178+
return lastId;
179+
}
180+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2013-2026 consulo.io
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package consulo.spring.spel.language.impl.highlight;
18+
19+
import consulo.annotation.component.ExtensionImpl;
20+
import consulo.language.Language;
21+
import consulo.language.editor.annotation.Annotator;
22+
import consulo.language.editor.annotation.AnnotatorFactory;
23+
import consulo.spring.spel.language.SpELLanguage;
24+
import org.jspecify.annotations.Nullable;
25+
26+
@ExtensionImpl
27+
public class SpELSemanticHighlighterFactory implements AnnotatorFactory {
28+
@Override
29+
public @Nullable Annotator createAnnotator() {
30+
return new SpELSemanticHighlighter();
31+
}
32+
33+
@Override
34+
public Language getLanguage() {
35+
return SpELLanguage.INSTANCE;
36+
}
37+
}

spel-lang-impl/src/main/java/consulo/spring/spel/language/impl/lexer/_SpELLexer.flex

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import consulo.spring.spel.language.SpELTokenTypes;
3535
%state STRING
3636
%state PLACEHOLDER
3737
%state PLACEHOLDER_DEFAULT
38+
%state STR_PLACEHOLDER
39+
%state STR_PLACEHOLDER_DEFAULT
3840

3941
WHITE_SPACE=[ \t\r\n]+
4042
DIGIT=[0-9]
@@ -106,12 +108,16 @@ IDENTIFIER_PART=[a-zA-Z0-9_$]
106108
[^] { return TokenType.BAD_CHARACTER; }
107109
}
108110

111+
// String state: recognizes ${...} inside strings
109112
<STRING> {
110113
"''" { return SpELTokenTypes.STRING_LITERAL; }
111114
"'" { yybegin(YYINITIAL); return SpELTokenTypes.STRING_LITERAL; }
112-
[^']+ { return SpELTokenTypes.STRING_LITERAL; }
115+
"${" { yybegin(STR_PLACEHOLDER); return SpELTokenTypes.DOLLAR_LBRACE; }
116+
[^'$]+ { return SpELTokenTypes.STRING_LITERAL; }
117+
"$" { return SpELTokenTypes.STRING_LITERAL; }
113118
}
114119

120+
// Standalone placeholder: ${key.name:default} at top level
115121
<PLACEHOLDER> {
116122
"}" { yybegin(YYINITIAL); return SpELTokenTypes.RBRACE; }
117123
":" { yybegin(PLACEHOLDER_DEFAULT); return SpELTokenTypes.COLON; }
@@ -124,3 +130,17 @@ IDENTIFIER_PART=[a-zA-Z0-9_$]
124130
"}" { yybegin(YYINITIAL); return SpELTokenTypes.RBRACE; }
125131
[^}]+ { return SpELTokenTypes.PLACEHOLDER_CONTENT; }
126132
}
133+
134+
// Placeholder inside a string: ${key.name:default} returns to STRING on }
135+
<STR_PLACEHOLDER> {
136+
"}" { yybegin(STRING); return SpELTokenTypes.RBRACE; }
137+
":" { yybegin(STR_PLACEHOLDER_DEFAULT); return SpELTokenTypes.COLON; }
138+
"." { return SpELTokenTypes.DOT; }
139+
{LETTER} {IDENTIFIER_PART}* { return SpELTokenTypes.IDENTIFIER; }
140+
[^}:.a-zA-Z_$]+ { return SpELTokenTypes.PLACEHOLDER_CONTENT; }
141+
}
142+
143+
<STR_PLACEHOLDER_DEFAULT> {
144+
"}" { yybegin(STRING); return SpELTokenTypes.RBRACE; }
145+
[^}]+ { return SpELTokenTypes.PLACEHOLDER_CONTENT; }
146+
}

0 commit comments

Comments
 (0)