Skip to content

Commit 61b2752

Browse files
committed
Narrow autocomplete options as user types
1 parent 8d4f5d8 commit 61b2752

3 files changed

Lines changed: 138 additions & 14 deletions

File tree

sierra-previewer/src/main/java/org/httprpc/sierra/previewer/SierraXMLCompletionProvider.java

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,20 @@ public List<Completion> getCompletions(JTextComponent comp) {
119119
var context = getCompletionContext(comp, offset);
120120

121121
if (context.type == CompletionType.TAG_NAME) {
122-
return new ArrayList<>(tagCompletions);
122+
// Narrow tag suggestions by the already-entered prefix (case-insensitive)
123+
var prefix = getAlreadyEnteredText(comp);
124+
var lowerPrefix = prefix == null ? "" : prefix.toLowerCase();
125+
126+
List<Completion> filtered = new ArrayList<>();
127+
for (var c : tagCompletions) {
128+
var text = c.getInputText();
129+
if (!lowerPrefix.isEmpty() && !text.toLowerCase().startsWith(lowerPrefix)) {
130+
continue;
131+
}
132+
filtered.add(c);
133+
}
134+
135+
return filtered;
123136
}
124137

125138
if (context.type == CompletionType.ATTRIBUTE_NAME && context.tagName != null) {
@@ -145,8 +158,17 @@ public List<Completion> getCompletions(JTextComponent comp) {
145158

146159
Collections.sort(availableAttributeNames);
147160

161+
// Determine the already-entered text (prefix) so we can narrow suggestions
162+
var prefix = getAlreadyEnteredText(comp);
163+
var lowerPrefix = prefix == null ? "" : prefix.toLowerCase();
164+
148165
List<Completion> suggestions = new ArrayList<>();
149166
for (var attr : availableAttributeNames) {
167+
// If there's a prefix, filter attributes that don't start with it (case-insensitive)
168+
if (!lowerPrefix.isEmpty() && !attr.toLowerCase().startsWith(lowerPrefix)) {
169+
continue;
170+
}
171+
150172
var description = allAttributes.get(attr);
151173

152174
// Create a richer HTML tooltip using the full description
@@ -198,12 +220,17 @@ private CompletionContext getCompletionContext(JTextComponent comp, int offset)
198220
}
199221

200222
var textSinceOpen = text.substring(lastOpenAngle);
223+
// If there is no space after the tag name, we're still typing the tag name
201224
if (!textSinceOpen.contains(" ") && !textSinceOpen.contains("\n")) {
202225
return new CompletionContext(CompletionType.TAG_NAME, tagName, lastOpenAngle);
203226
}
204227

205-
var lastChar = text.charAt(offset - 1);
206-
if (Character.isWhitespace(lastChar)) {
228+
// If we have reached the attribute area (there is a space/newline after '<...'),
229+
// consider this attribute name completion context even if the last character
230+
// isn't whitespace (so typing 'na' after '<button ' still triggers attribute
231+
// suggestions). We already checked for being inside a quoted attribute value
232+
// above.
233+
if (textSinceOpen.contains(" ") || textSinceOpen.contains("\n")) {
207234
return new CompletionContext(CompletionType.ATTRIBUTE_NAME, tagName, lastOpenAngle);
208235
}
209236

@@ -213,6 +240,35 @@ private CompletionContext getCompletionContext(JTextComponent comp, int offset)
213240
return new CompletionContext(CompletionType.NONE);
214241
}
215242

243+
@Override
244+
public String getAlreadyEnteredText(JTextComponent comp) {
245+
// Return the partial attribute name currently being typed so AutoCompletion
246+
// can filter the list of completions.
247+
int pos = comp.getCaretPosition();
248+
try {
249+
var text = comp.getText(0, pos);
250+
var lastOpen = text.lastIndexOf('<');
251+
if (lastOpen == -1) {
252+
return "";
253+
}
254+
255+
// Scan backwards from the caret until we hit whitespace or a character that
256+
// can't be part of an attribute name (e.g., '=', '<', quotes).
257+
int i = pos - 1;
258+
while (i > lastOpen) {
259+
char c = text.charAt(i);
260+
if (Character.isWhitespace(c) || c == '<' || c == '=' || c == '"' || c == '\'') {
261+
break;
262+
}
263+
i--;
264+
}
265+
266+
return text.substring(i + 1, pos);
267+
} catch (BadLocationException e) {
268+
return "";
269+
}
270+
}
271+
216272
private Set<String> getAlreadyDefinedAttributes(JTextComponent comp, int tagStartOffset, int caretOffset) {
217273
Set<String> definedAttributes = new HashSet<>();
218274
try {

sierra-previewer/src/test/java/org/httprpc/sierra/previewer/SierraXMLCompletionProviderTest.java

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,8 @@ private void setComponentText(String text, int caretOffset) throws BadLocationEx
6464
when(mockComponent.getCaretPosition()).thenReturn(caretOffset);
6565
}
6666

67-
// --- Core Test Case: 1. Tag Completion ---
68-
6967
@Test
70-
void testCompletion_StartTag_SuggestsAllUIElements() throws BadLocationException {
68+
void testAllUiElements() throws BadLocationException {
7169
// Simulate typing '<' to trigger tag completion (caret at offset 1)
7270
setComponentText("<", 1);
7371

@@ -96,10 +94,8 @@ void testCompletion_StartTag_SuggestsAllUIElements() throws BadLocationException
9694
}
9795
}
9896

99-
// --- Core Test Case: 2. Attribute Completion for <button> ---
100-
10197
@Test
102-
void testCompletion_InsideButtonTag_SuggestsCoreAttributes() throws BadLocationException {
98+
void testButtonAttributes() throws BadLocationException {
10399
var text = "<button ";
104100
var caretOffset = text.length(); // Caret is right after the space in "<button "
105101

@@ -114,7 +110,7 @@ void testCompletion_InsideButtonTag_SuggestsCoreAttributes() throws BadLocationE
114110
.collect(Collectors.toSet());
115111

116112
assertTrue(attributeNames.contains("name"), "Should suggest the mandatory 'name' attribute.");
117-
//assertTrue(attributeNames.contains("text"), "Should suggest the mandatory 'text' attribute.");
113+
assertTrue(attributeNames.contains("text"), "Should suggest the mandatory 'text' attribute.");
118114

119115
// Verify other essential attributes are present (from the DTD)
120116
assertTrue(attributeNames.contains("background"), "Should also suggest the 'background' attribute.");
@@ -127,8 +123,8 @@ void testCompletion_InsideButtonTag_SuggestsCoreAttributes() throws BadLocationE
127123
}
128124
}
129125

130-
@Test
131-
void testCompletion_InsideTextAreaTag_SuggestsCoreAttributes() throws BadLocationException {
126+
@Test
127+
void testTextAreaAtttributes() throws BadLocationException {
132128
var text = "<text-area ";
133129
var caretOffset = text.length();
134130

@@ -143,7 +139,7 @@ void testCompletion_InsideTextAreaTag_SuggestsCoreAttributes() throws BadLocatio
143139
.collect(Collectors.toSet());
144140

145141
assertTrue(attributeNames.contains("name"), "Should suggest the mandatory 'name' attribute.");
146-
//assertTrue(attributeNames.contains("text"), "Should suggest the mandatory 'text' attribute.");
142+
assertTrue(attributeNames.contains("text"), "Should suggest the mandatory 'text' attribute.");
147143

148144
// specific to text area
149145
assertTrue(attributeNames.contains("lineWrap"), "Should suggest the mandatory 'lineWrap' attribute.");
@@ -158,4 +154,76 @@ void testCompletion_InsideTextAreaTag_SuggestsCoreAttributes() throws BadLocatio
158154
assertTrue(current.compareTo(next) <= 0, "Suggestions should be sorted alphabetically.");
159155
}
160156
}
157+
158+
@Test
159+
void testAttributeNarrowing() throws BadLocationException {
160+
// Start with just '<button ' (caret after space)
161+
var base = "<button ";
162+
setComponentText(base, base.length());
163+
164+
var suggestionsAll = provider.getCompletions(mockComponent);
165+
assertFalse(suggestionsAll.isEmpty(), "Should suggest attributes when inside a tag.");
166+
assertTrue(suggestionsAll.stream().anyMatch(c -> c.getInputText().equals("name")),
167+
"Suggestions should include 'name' initially.");
168+
169+
// Type one character: 'n'
170+
var oneChar = "<button n";
171+
setComponentText(oneChar, oneChar.length());
172+
173+
var suggestionsN = provider.getCompletions(mockComponent);
174+
assertFalse(suggestionsN.isEmpty(), "Should still suggest attributes after typing 'n'.");
175+
for (var c : suggestionsN) {
176+
assertTrue(c.getInputText().toLowerCase().startsWith("n"),
177+
"All suggestions after typing 'n' should start with 'n'.");
178+
}
179+
180+
// Type a second character: 'a' -> prefix 'na'
181+
var twoChar = "<button na";
182+
setComponentText(twoChar, twoChar.length());
183+
184+
var suggestionsNA = provider.getCompletions(mockComponent);
185+
assertFalse(suggestionsNA.isEmpty(), "Should still suggest attributes after typing 'na'.");
186+
for (var c : suggestionsNA) {
187+
assertTrue(c.getInputText().toLowerCase().startsWith("na"),
188+
"All suggestions after typing 'na' should start with 'na'.");
189+
}
190+
assertTrue(suggestionsNA.stream().anyMatch(c -> c.getInputText().equals("name")),
191+
"Suggestions after 'na' should include the 'name' attribute.");
192+
}
193+
194+
@Test
195+
void testTagNarrowing() throws BadLocationException {
196+
// Start with just '<' (caret after '<')
197+
var base = "<";
198+
setComponentText(base, base.length());
199+
200+
var suggestionsAll = provider.getCompletions(mockComponent);
201+
assertFalse(suggestionsAll.isEmpty(), "Should suggest tags when starting a tag.");
202+
assertTrue(suggestionsAll.stream().anyMatch(c -> c.getInputText().equals("button")),
203+
"Suggestions should include 'button' initially.");
204+
205+
// Type one character: 'b'
206+
var oneChar = "<b";
207+
setComponentText(oneChar, oneChar.length());
208+
209+
var suggestionsB = provider.getCompletions(mockComponent);
210+
assertFalse(suggestionsB.isEmpty(), "Should still suggest tags after typing 'b'.");
211+
for (var c : suggestionsB) {
212+
assertTrue(c.getInputText().toLowerCase().startsWith("b"),
213+
"All tag suggestions after typing 'b' should start with 'b'.");
214+
}
215+
216+
// Type a second character: 'u' -> prefix 'bu'
217+
var twoChar = "<bu";
218+
setComponentText(twoChar, twoChar.length());
219+
220+
var suggestionsBU = provider.getCompletions(mockComponent);
221+
assertFalse(suggestionsBU.isEmpty(), "Should still suggest tags after typing 'bu'.");
222+
for (var c : suggestionsBU) {
223+
assertTrue(c.getInputText().toLowerCase().startsWith("bu"),
224+
"All tag suggestions after typing 'bu' should start with 'bu'.");
225+
}
226+
assertTrue(suggestionsBU.stream().anyMatch(c -> c.getInputText().equals("button")),
227+
"Suggestions after 'bu' should include the 'button' tag.");
228+
}
161229
}

sierra-test/src/main/resources/org/httprpc/sierra/test/AlignmentXTest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<!DOCTYPE column-panel SYSTEM "sierra.dtd">
44

5-
<column-panel spacing="4" padding="8" alignToGrid="true" opaque="true">
5+
<column-panel spacing="4" padding="8" alignToGrid="true" opaque="false" background="white">
66
<row-panel>
77
<text-pane text="0.0" alignmentX="0.0" border="silver"/>
88
<text-pane text="0.25" horizontalAlignment="center" alignmentX="0.25" border="silver"/>

0 commit comments

Comments
 (0)