Skip to content

Commit 6e0562b

Browse files
authored
ADFA-3322: Signature help for Kotlin LSP (#1484)
Signed-off-by: Akash Yadav <akashyadav@appdevforall.org>
1 parent b01edf0 commit 6e0562b

21 files changed

Lines changed: 1382 additions & 440 deletions

File tree

‎editor/src/main/java/com/itsaky/androidide/editor/ui/SignatureHelpWindow.java‎

Lines changed: 155 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import com.itsaky.androidide.utils.ContextUtilsKt;
2929
import io.github.rosemoe.sora.event.SelectionChangeEvent;
3030
import io.github.rosemoe.sora.widget.base.EditorPopupWindow;
31+
import java.util.ArrayList;
32+
import java.util.List;
3133
import org.slf4j.Logger;
3234
import org.slf4j.LoggerFactory;
3335

@@ -38,132 +40,157 @@
3840
*/
3941
public class SignatureHelpWindow extends BaseEditorWindow {
4042

41-
private static final Logger LOG = LoggerFactory.getLogger(SignatureHelpWindow.class);
42-
43-
/**
44-
* Create a signature help popup window for editor
45-
*
46-
* @param editor The editor.
47-
*/
48-
public SignatureHelpWindow(@NonNull IDEEditor editor) {
49-
super(editor);
50-
51-
editor.subscribeEvent(
52-
SelectionChangeEvent.class,
53-
(event, unsubscribe) -> {
54-
if (isShowing()) {
55-
dismiss();
56-
}
57-
});
58-
}
59-
60-
public void setupAndDisplay(SignatureHelp signature) {
61-
if (signature == null || signature.getSignatures().isEmpty()) {
62-
if (isShowing()) {
63-
dismiss();
64-
}
65-
66-
return;
67-
}
68-
69-
final var signatureText = createSignatureText(signature);
70-
71-
if (signatureText == null) {
72-
return;
73-
}
74-
75-
this.text.setText(signatureText);
76-
displayWindow();
77-
}
78-
79-
@Nullable
80-
private CharSequence createSignatureText(@NonNull SignatureHelp signature) {
81-
final var signatures = signature.getSignatures();
82-
final var activeSignature = signature.getActiveSignature();
83-
final var activeParameter = signature.getActiveParameter();
84-
final SpannableStringBuilder sb = new SpannableStringBuilder();
85-
86-
if (activeSignature < 0 || activeParameter < 0) {
87-
LOG.debug("activeSignature: {}, activeParameter: {}", activeSignature, activeParameter);
88-
return null;
89-
}
90-
91-
var count = signatures.size();
92-
if (activeSignature >= count) {
93-
LOG.debug("Active signature is invalid. Size is {}", count);
94-
return null;
95-
}
96-
97-
// remove all with non-applicable signatures
98-
signatures.removeIf(
99-
info -> {
100-
final var remove = activeParameter >= info.getParameters().size();
101-
if (remove) {
102-
LOG.debug("Removing {} params={} active={}", info, info.getParameters().size(),
103-
activeParameter);
104-
}
105-
return remove;
106-
});
107-
108-
count = signatures.size();
109-
for (var i = 0; i < count; i++) {
110-
final var info = signatures.get(i);
111-
formatSignature(info, activeParameter, sb);
112-
if (i != count - 1) {
113-
sb.append('\n');
114-
}
115-
}
116-
117-
return sb;
118-
}
119-
120-
/**
121-
* Formats (highlights) a method signature
122-
*
123-
* @param signature Signature information
124-
* @param paramIndex Currently active parameter index
125-
* @param result The builder to append spanned text to.
126-
*/
127-
private void formatSignature(
128-
@NonNull SignatureInformation signature,
129-
int paramIndex,
130-
SpannableStringBuilder result) {
131-
132-
String name = signature.getLabel();
133-
name = name.substring(0, name.indexOf("("));
134-
135-
final var foreground = ContextUtilsKt.resolveAttr(getEditor().getContext(),
136-
attr.colorOnSecondaryContainer);
137-
final var paramSelected = 0xffff6060;
138-
final var operators = 0xff4fc3f7;
139-
140-
result.append(
141-
name, new ForegroundColorSpan(foreground), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
142-
result.append(
143-
"(", new ForegroundColorSpan(operators), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
144-
145-
var params = signature.getParameters();
146-
for (int i = 0; i < params.size(); i++) {
147-
int color = i == paramIndex ? paramSelected : foreground;
148-
final var info = params.get(i);
149-
if (i == params.size() - 1) {
150-
result.append(
151-
info.getLabel(),
152-
new ForegroundColorSpan(color),
153-
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
154-
} else {
155-
result.append(
156-
info.getLabel(),
157-
new ForegroundColorSpan(color),
158-
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
159-
result.append(
160-
",",
161-
new ForegroundColorSpan(operators),
162-
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
163-
result.append(" ");
164-
}
165-
}
166-
result.append(
167-
")", new ForegroundColorSpan(0xff4fc3f7), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
168-
}
43+
private static final Logger LOG = LoggerFactory.getLogger(SignatureHelpWindow.class);
44+
45+
/**
46+
* Returns a new list containing only the signatures that have enough parameters for the given active parameter index. Never mutates the input list.
47+
*/
48+
static List<SignatureInformation> applicableSignatures(
49+
List<SignatureInformation> signatures, int activeParameter) {
50+
final List<SignatureInformation> result = new ArrayList<>(signatures.size());
51+
for (final SignatureInformation info : signatures) {
52+
if (activeParameter < info.getParameters().size()) {
53+
result.add(info);
54+
}
55+
}
56+
return result;
57+
}
58+
59+
/**
60+
* Returns the function name portion of a signature label (text before the first '('), or the whole label if it contains no '('.
61+
*/
62+
static String signatureName(String label) {
63+
final int paren = label.indexOf('(');
64+
return paren < 0 ? label : label.substring(0, paren);
65+
}
66+
67+
/**
68+
* Create a signature help popup window for editor
69+
*
70+
* @param editor
71+
* The editor.
72+
*/
73+
public SignatureHelpWindow(@NonNull IDEEditor editor) {
74+
super(editor);
75+
76+
editor.subscribeEvent(
77+
SelectionChangeEvent.class,
78+
(event, unsubscribe) -> {
79+
if (isShowing()) {
80+
dismiss();
81+
}
82+
});
83+
}
84+
85+
public void setupAndDisplay(SignatureHelp signature) {
86+
if (signature == null || signature.getSignatures().isEmpty()) {
87+
if (isShowing()) {
88+
dismiss();
89+
}
90+
91+
return;
92+
}
93+
94+
final var signatureText = createSignatureText(signature);
95+
96+
if (signatureText == null) {
97+
if (isShowing()) {
98+
dismiss();
99+
}
100+
101+
return;
102+
}
103+
104+
this.text.setText(signatureText);
105+
displayWindow();
106+
}
107+
108+
@Nullable
109+
private CharSequence createSignatureText(@NonNull SignatureHelp signature) {
110+
final var signatures = signature.getSignatures();
111+
final var activeSignature = signature.getActiveSignature();
112+
final var activeParameter = signature.getActiveParameter();
113+
final SpannableStringBuilder sb = new SpannableStringBuilder();
114+
115+
if (activeSignature < 0 || activeParameter < 0) {
116+
LOG.debug("activeSignature: {}, activeParameter: {}", activeSignature, activeParameter);
117+
return null;
118+
}
119+
120+
var count = signatures.size();
121+
if (activeSignature >= count) {
122+
LOG.debug("Active signature is invalid. Size is {}", count);
123+
return null;
124+
}
125+
126+
// keep only applicable signatures (does not mutate the input list)
127+
final var applicable = applicableSignatures(signatures, activeParameter);
128+
129+
if (applicable.isEmpty()) {
130+
return null;
131+
}
132+
133+
count = applicable.size();
134+
for (var i = 0; i < count; i++) {
135+
final var info = applicable.get(i);
136+
formatSignature(info, activeParameter, sb);
137+
if (i != count - 1) {
138+
sb.append('\n');
139+
}
140+
}
141+
142+
return sb;
143+
}
144+
145+
/**
146+
* Formats (highlights) a method signature
147+
*
148+
* @param signature
149+
* Signature information
150+
* @param paramIndex
151+
* Currently active parameter index
152+
* @param result
153+
* The builder to append spanned text to.
154+
*/
155+
private void formatSignature(
156+
@NonNull SignatureInformation signature,
157+
int paramIndex,
158+
SpannableStringBuilder result) {
159+
160+
final String name = signatureName(signature.getLabel());
161+
162+
final var foreground = ContextUtilsKt.resolveAttr(getEditor().getContext(),
163+
attr.colorOnSecondaryContainer);
164+
final var paramSelected = 0xffff6060;
165+
final var operators = 0xff4fc3f7;
166+
167+
result.append(
168+
name, new ForegroundColorSpan(foreground), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
169+
result.append(
170+
"(", new ForegroundColorSpan(operators), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
171+
172+
var params = signature.getParameters();
173+
for (int i = 0; i < params.size(); i++) {
174+
int color = i == paramIndex ? paramSelected : foreground;
175+
final var info = params.get(i);
176+
if (i == params.size() - 1) {
177+
result.append(
178+
info.getLabel(),
179+
new ForegroundColorSpan(color),
180+
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
181+
} else {
182+
result.append(
183+
info.getLabel(),
184+
new ForegroundColorSpan(color),
185+
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
186+
result.append(
187+
",",
188+
new ForegroundColorSpan(operators),
189+
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
190+
result.append(" ");
191+
}
192+
}
193+
result.append(
194+
")", new ForegroundColorSpan(0xff4fc3f7), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
195+
}
169196
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.itsaky.androidide.editor.ui
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import com.itsaky.androidide.lsp.models.MarkupContent
5+
import com.itsaky.androidide.lsp.models.ParameterInformation
6+
import com.itsaky.androidide.lsp.models.SignatureInformation
7+
import org.junit.Test
8+
import org.junit.runner.RunWith
9+
import org.robolectric.RobolectricTestRunner
10+
11+
@RunWith(RobolectricTestRunner::class)
12+
class SignatureHelpWindowTest {
13+
private fun sig(
14+
label: String,
15+
paramCount: Int,
16+
): SignatureInformation {
17+
val params = (0 until paramCount).map { ParameterInformation("p$it: Int", MarkupContent()) }
18+
return SignatureInformation(label, MarkupContent(), params)
19+
}
20+
21+
@Test
22+
fun `signatureName returns text before the opening paren`() {
23+
assertThat(SignatureHelpWindow.signatureName("foo(p0: Int, p1: Int)")).isEqualTo("foo")
24+
}
25+
26+
@Test
27+
fun `signatureName returns whole label when there is no paren`() {
28+
assertThat(SignatureHelpWindow.signatureName("foo")).isEqualTo("foo")
29+
}
30+
31+
@Test
32+
fun `applicableSignatures drops overloads without enough parameters`() {
33+
val input = listOf(sig("a(p0: Int)", 1), sig("b(p0: Int, p1: Int)", 2))
34+
val result = SignatureHelpWindow.applicableSignatures(input, 1)
35+
assertThat(result).hasSize(1)
36+
assertThat(result[0].label).isEqualTo("b(p0: Int, p1: Int)")
37+
}
38+
39+
@Test
40+
fun `applicableSignatures does not mutate an immutable input list`() {
41+
val input = java.util.Collections.unmodifiableList(listOf(sig("a(p0: Int)", 1)))
42+
// Must not throw UnsupportedOperationException:
43+
val result = SignatureHelpWindow.applicableSignatures(input, 5)
44+
assertThat(result).isEmpty()
45+
assertThat(input).hasSize(1)
46+
}
47+
}

0 commit comments

Comments
 (0)