From 0989a66758739ad4e547b8f24c251d9a992f4529 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 20:21:24 +0000 Subject: [PATCH 1/4] Add completion of idContext.ops in the script editor Typing after idContext.ops. now shows the matching global operators with their signatures. Up and Down select one, Tab or Enter inserts it, and Escape closes the list. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../api/language/ILanguageHandler.java | 10 ++ .../api/language/ScriptCompletions.java | 20 +++ .../gui/component/input/WidgetTextArea.java | 138 ++++++++++++++++++ .../ContainerScreenTerminalScripting.java | 15 ++ .../core/language/JavaScriptCompletions.java | 54 +++++++ .../language/LanguageHandlerJavaScript.java | 29 ++++ .../language/JavaScriptCompletionsTest.java | 49 +++++++ 7 files changed, 315 insertions(+) create mode 100644 src/main/java/org/cyclops/integratedscripting/api/language/ScriptCompletions.java create mode 100644 src/main/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletions.java create mode 100644 src/test/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletionsTest.java diff --git a/src/main/java/org/cyclops/integratedscripting/api/language/ILanguageHandler.java b/src/main/java/org/cyclops/integratedscripting/api/language/ILanguageHandler.java index 2b7a39850..551a0eb03 100644 --- a/src/main/java/org/cyclops/integratedscripting/api/language/ILanguageHandler.java +++ b/src/main/java/org/cyclops/integratedscripting/api/language/ILanguageHandler.java @@ -66,6 +66,16 @@ public default Set getTopLevelMembers(String script) { return null; } + /** + * @param script The script contents. + * @param cursor The cursor position. + * @return Completion candidates at the cursor, or null if there are none. + */ + @Nullable + public default ScriptCompletions getCompletions(String script, int cursor) { + return null; + } + /** * Check a script for syntax errors, without executing it. * @param path The script path. diff --git a/src/main/java/org/cyclops/integratedscripting/api/language/ScriptCompletions.java b/src/main/java/org/cyclops/integratedscripting/api/language/ScriptCompletions.java new file mode 100644 index 000000000..095a3b034 --- /dev/null +++ b/src/main/java/org/cyclops/integratedscripting/api/language/ScriptCompletions.java @@ -0,0 +1,20 @@ +package org.cyclops.integratedscripting.api.language; + +import java.util.List; + +/** + * Completion candidates at a position in a script. + * @param start The position from which the text before the cursor is replaced when accepting a candidate. + * @param items The candidates. + * @author rubensworks + */ +public record ScriptCompletions(int start, List items) { + + /** + * @param name The text to insert. + * @param detail A short description, such as a signature. + */ + public record Item(String name, String detail) { + } + +} diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java index 05987749f..81045c7ae 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java @@ -39,6 +39,7 @@ import org.cyclops.cyclopscore.client.gui.component.input.IInputListener; import org.cyclops.cyclopscore.helper.Helpers; import org.cyclops.cyclopscore.helper.RenderHelpers; +import org.cyclops.integratedscripting.api.language.ScriptCompletions; import org.cyclops.integratedscripting.api.language.ScriptSyntaxError; import org.cyclops.integratedscripting.client.key.Keys; import org.lwjgl.glfw.GLFW; @@ -47,6 +48,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.function.BiFunction; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -68,6 +70,7 @@ public class WidgetTextArea extends EditBox implements GuiEventListener { public static final int ROW_HEIGHT = 9; + private static final int COMPLETION_ROWS = 6; public static final int SCROLLBAR_WIDTH = 14; private final TextFieldHelperExtended textFieldHelper; @@ -85,6 +88,11 @@ public class WidgetTextArea extends EditBox implements GuiEventListener { private IInputListener listenerCursor; @Nullable private IMarkupProvider markupProvider; + @Nullable + private BiFunction completionProvider; + @Nullable + private ScriptCompletions completions; + private int completionIndex; private WidgetTextArea.DisplayCache emptyDisplayCache = createEmptyDisplayCache(); @Nullable private WidgetTextArea.DisplayCache displayCache = emptyDisplayCache; @@ -257,6 +265,71 @@ public void setMarkupProvider(@Nullable IMarkupProvider markupProvider) { this.layout = null; } + public void setCompletionProvider(@Nullable BiFunction completionProvider) { + this.completionProvider = completionProvider; + } + + /** + * Only show completions while typing, not when moving the cursor. + */ + private void updateCompletions(boolean textChanged) { + if (textChanged && this.completionProvider != null) { + ScriptCompletions completionsNew = this.completionProvider.apply(this.getValue(), this.getCursorPos()); + if (completionsNew == null || this.completions == null || !completionsNew.items().equals(this.completions.items())) { + this.completionIndex = 0; + } + this.completions = completionsNew; + } else { + this.completions = null; + } + } + + /** + * @return If completions were shown. + */ + public boolean dismissCompletions() { + boolean shown = this.completions != null; + this.completions = null; + return shown; + } + + private boolean completionKeyPressed(int key) { + int count = this.completions.items().size(); + switch (key) { + case GLFW.GLFW_KEY_DOWN: + this.completionIndex = (this.completionIndex + 1) % count; + return true; + case GLFW.GLFW_KEY_UP: + this.completionIndex = (this.completionIndex + count - 1) % count; + return true; + case GLFW.GLFW_KEY_TAB: + case GLFW.GLFW_KEY_ENTER: + case GLFW.GLFW_KEY_KP_ENTER: + this.acceptCompletion(); + return true; + case GLFW.GLFW_KEY_ESCAPE: + this.completions = null; + return true; + default: + return false; + } + } + + private void acceptCompletion() { + TextHistory.State before = this.getState(); + // Replace the text directly instead of selecting it, as a selection would change the bound member + String insert = this.completions.items().get(this.completionIndex).name() + "("; + String value = this.getValue(); + int cursorPos = this.getCursorPos(); + int start = this.completions.start(); + int newCursorPos = start + insert.length(); + this.completions = null; + this.setState(new TextHistory.State(value.substring(0, start) + insert + value.substring(cursorPos), newCursorPos, newCursorPos)); + this.recordHistory(before, false); + this.clearDisplayCache(); + this.ensureCursorVisible(); + } + public void setValue(String value) { this.firstRow = 0; // Another script may have the same contents but another language @@ -352,6 +425,7 @@ public void tick() { @Override public boolean mouseClicked(double mouseX, double mouseY, int mouseButton) { + this.completions = null; if (mouseButton == 1 && mouseX >= this.getX() && mouseX < this.getX() + this.width && mouseY >= this.getY() && mouseY < this.getY() + this.height) { // Don't select everything on right-click, as typing would then replace the whole script @@ -451,6 +525,9 @@ private void updateDragSelection() { @Override public boolean keyPressed(int typedChar, int keyCode, int modifiers) { + if (this.completions != null && this.completionKeyPressed(typedChar)) { + return true; + } if (this.shortcutListener != null && this.shortcutListener.keyPressed(typedChar, keyCode, modifiers)) { return true; } @@ -461,6 +538,7 @@ public boolean keyPressed(int typedChar, int keyCode, int modifiers) { if (!history) { this.recordHistory(before, false); } + this.updateCompletions(!history && !before.value().equals(this.getValue())); this.clearDisplayCache(); this.setFocused(true); this.ensureCursorVisible(); @@ -481,6 +559,7 @@ public boolean charTyped(char typedChar, int keyCode) { TextHistory.State before = this.getState(); this.textFieldHelper.insertText(Character.toString(typedChar)); this.recordHistory(before, true); + this.updateCompletions(true); this.clearDisplayCache(); this.setFocused(true); this.ensureCursorVisible(); @@ -803,6 +882,11 @@ public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float this.renderHighlight(displayCache.selection); if (displayCache.cursor != null) { this.renderCursor(guiGraphics, displayCache.cursor, displayCache.cursorAtEnd); + + // Show completions while typing + if (this.isFocused() && this.completions != null) { + this.renderCompletions(guiGraphics, this.convertLocalToScreen(displayCache.cursor)); + } } // Render scrollbar @@ -854,6 +938,60 @@ private void renderCharBackground(GuiGraphics guiGraphics, DisplayCache displayC guiGraphics.fill(screenPos.x, screenPos.y - 1, screenPos.x + width, screenPos.y + ROW_HEIGHT - 1, color); } + private void renderCompletions(GuiGraphics guiGraphics, Pos2i cursor) { + List items = this.completions.items(); + int rows = Math.min(items.size(), COMPLETION_ROWS); + int firstItem = Math.max(0, Math.min(this.completionIndex - rows / 2, items.size() - rows)); + // Grow beyond the editor if needed, as signatures can be long + int width = getTextWidth(); + for (int i = 0; i < rows; i++) { + ScriptCompletions.Item item = items.get(firstItem + i); + width = Math.max(width, this.font.width(item.name()) + 4 + this.font.width(item.detail()) + 2); + } + Pair xAndWidth = this.fitHorizontally(width); + int x = xAndWidth.getLeft(); + width = xAndWidth.getRight(); + int height = rows * ROW_HEIGHT + 2; + // Show below the line of the cursor if possible, otherwise above it + int y = cursor.y + ROW_HEIGHT; + if (y + height > this.getY() + this.getHeight() && cursor.y - height - 1 >= this.getY()) { + y = cursor.y - height - 1; + } + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(0, 0, 200); + guiGraphics.fill(x - 1, y - 1, x + width + 1, y + height + 1, Helpers.RGBAToInt(80, 80, 80, 255)); + guiGraphics.fill(x, y, x + width, y + height, Helpers.RGBAToInt(250, 250, 250, 255)); + for (int i = 0; i < rows; i++) { + ScriptCompletions.Item item = items.get(firstItem + i); + int rowY = y + 1 + i * ROW_HEIGHT; + if (firstItem + i == this.completionIndex) { + guiGraphics.fill(x, rowY, x + width, rowY + ROW_HEIGHT, Helpers.RGBAToInt(190, 210, 255, 255)); + } + String name = this.font.plainSubstrByWidth(item.name(), width - 2); + guiGraphics.drawString(this.font, name, x + 1, rowY + 1, 0, false); + int detailX = x + 1 + this.font.width(name) + 4; + int detailWidth = x + width - 1 - detailX; + if (detailWidth > 10) { + String detail = item.detail(); + if (this.font.width(detail) > detailWidth) { + detail = this.font.plainSubstrByWidth(detail, detailWidth - this.font.width("\u2026")) + "\u2026"; + } + guiGraphics.drawString(this.font, detail, detailX, rowY + 1, Helpers.RGBToInt(120, 120, 120), false); + } + } + guiGraphics.pose().popPose(); + } + + /** + * @return The x position and width of a popup at the start of the text, moved left if it would leave the screen. + */ + private Pair fitHorizontally(int width) { + int screenWidth = Minecraft.getInstance().getWindow().getGuiScaledWidth(); + width = Math.min(width, screenWidth - 4); + int x = Math.min(this.getX() + getLinesXOffset(), screenWidth - 2 - width); + return Pair.of(x, width); + } + private void renderCursor(GuiGraphics guiGraphics, Pos2i pos, boolean cursorAtEnd) { if (this.isFocused() && this.frameTick / 6 % 2 == 0) { pos = this.convertLocalToScreen(pos); diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java index e19101ca9..312a9167c 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java @@ -182,6 +182,10 @@ public int getTotalRows() { }); textArea.setListenerSelection(this::onSelectionModified); textArea.setListenerCursor(this::onSelectionModified); + textArea.setCompletionProvider((text, cursor) -> { + ILanguageHandler languageHandler = getActiveLanguageHandler(); + return languageHandler != null ? languageHandler.getCompletions(text, cursor) : null; + }); textArea.setMarkupProvider(text -> { Path path = getMenu().getActiveScriptPath(); ILanguageHandler languageHandler = path != null ? LanguageHandlers.REGISTRY.getProvider(path) : null; @@ -733,6 +737,12 @@ public void setFirstRow(int firstRow) { this.firstRow = firstRow; } + @Nullable + private ILanguageHandler getActiveLanguageHandler() { + Path path = getMenu().getActiveScriptPath(); + return path != null ? LanguageHandlers.REGISTRY.getProvider(path) : null; + } + private void onActiveScriptSelected() { String script = getMenu().getActiveScript(); this.textArea.setValue(script == null ? "" : script); @@ -894,6 +904,11 @@ public boolean keyPressed(int typedChar, int keyCode, int modifiers) { return true; } + // Close completions before leaving focus mode or closing the screen + if (typedChar == GLFW.GLFW_KEY_ESCAPE && this.textArea.isFocused() && this.textArea.dismissCompletions()) { + return true; + } + // Leave focus mode instead of closing the screen if (focusMode && typedChar == GLFW.GLFW_KEY_ESCAPE && pendingDialog == null) { this.toggleFocusMode(); diff --git a/src/main/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletions.java b/src/main/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletions.java new file mode 100644 index 000000000..dfb8d68f5 --- /dev/null +++ b/src/main/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletions.java @@ -0,0 +1,54 @@ +package org.cyclops.integratedscripting.core.language; + +import com.google.common.collect.Lists; +import org.cyclops.integratedscripting.api.language.ScriptCompletions; + +import javax.annotation.Nullable; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Completion of the global operators on idContext.ops in JavaScript. + * @author rubensworks + */ +public class JavaScriptCompletions { + + public static final String OPS_PREFIX = "idContext.ops."; + + /** + * @param script A script. + * @param cursor The cursor position. + * @param signatures Operator names mapped to their signatures. + * @return The operators matching the name before the cursor, or null if the cursor is not after idContext.ops. + */ + @Nullable + public static ScriptCompletions complete(String script, int cursor, Map signatures) { + int start = cursor; + while (start > 0 && Character.isJavaIdentifierPart(script.charAt(start - 1))) { + start--; + } + if (!script.startsWith(OPS_PREFIX, start - OPS_PREFIX.length())) { + return null; + } + // Don't complete in the middle of a name + if (cursor < script.length() && Character.isJavaIdentifierPart(script.charAt(cursor))) { + return null; + } + String prefix = script.substring(start, cursor); + String prefixLower = prefix.toLowerCase(Locale.ROOT); + List items = Lists.newArrayList(); + for (Map.Entry entry : signatures.entrySet()) { + if (entry.getKey().toLowerCase(Locale.ROOT).startsWith(prefixLower)) { + items.add(new ScriptCompletions.Item(entry.getKey(), entry.getValue())); + } + } + if (items.isEmpty() || (items.size() == 1 && items.get(0).name().equals(prefix))) { + return null; + } + items.sort(Comparator.comparing(ScriptCompletions.Item::name)); + return new ScriptCompletions(start, items); + } + +} diff --git a/src/main/java/org/cyclops/integratedscripting/core/language/LanguageHandlerJavaScript.java b/src/main/java/org/cyclops/integratedscripting/core/language/LanguageHandlerJavaScript.java index 0fe44e486..13b65039a 100644 --- a/src/main/java/org/cyclops/integratedscripting/core/language/LanguageHandlerJavaScript.java +++ b/src/main/java/org/cyclops/integratedscripting/core/language/LanguageHandlerJavaScript.java @@ -8,10 +8,14 @@ import org.apache.commons.lang3.tuple.Pair; import org.cyclops.cyclopscore.client.gui.image.IImage; import org.cyclops.cyclopscore.helper.Helpers; +import org.cyclops.cyclopscore.helper.L10NHelpers; import org.cyclops.integrateddynamics.api.evaluate.EvaluationException; +import org.cyclops.integrateddynamics.api.evaluate.operator.IOperator; +import org.cyclops.integrateddynamics.core.evaluate.operator.Operators; import org.cyclops.integratedscripting.IntegratedScripting; import org.cyclops.integratedscripting.Reference; import org.cyclops.integratedscripting.api.language.ILanguageHandler; +import org.cyclops.integratedscripting.api.language.ScriptCompletions; import org.cyclops.integratedscripting.api.language.ScriptSyntaxError; import org.cyclops.integratedscripting.api.network.IScriptFactory; import org.cyclops.integratedscripting.client.gui.image.ScriptImages; @@ -31,6 +35,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** * @author rubensworks @@ -55,6 +60,8 @@ public class LanguageHandlerJavaScript implements ILanguageHandler { public final Map tokenStyles; private final IScriptFactory scriptFactory; + @Nullable + private Map operatorSignatures; public LanguageHandlerJavaScript() { this.tokenStyles = Maps.newHashMap(); @@ -174,6 +181,28 @@ public Set getTopLevelMembers(String script) { return JavaScriptDeclarations.find(script); } + @Nullable + @Override + public ScriptCompletions getCompletions(String script, int cursor) { + return JavaScriptCompletions.complete(script, cursor, getOperatorSignatures()); + } + + protected Map getOperatorSignatures() { + // Operators are registered once, so this can be cached + if (this.operatorSignatures == null) { + Map signatures = Maps.newHashMap(); + for (Map.Entry entry : Operators.REGISTRY.getGlobalInteractOperators().entrySet()) { + IOperator operator = entry.getValue(); + String inputs = Arrays.stream(operator.getInputTypes()) + .map(type -> L10NHelpers.localize(type.getTranslationKey())) + .collect(Collectors.joining(", ")); + signatures.put(entry.getKey(), "(" + inputs + "): " + L10NHelpers.localize(operator.getOutputType().getTranslationKey())); + } + this.operatorSignatures = signatures; + } + return this.operatorSignatures; + } + @Override public IScriptFactory getScriptFactory() throws EvaluationException { return this.scriptFactory; diff --git a/src/test/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletionsTest.java b/src/test/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletionsTest.java new file mode 100644 index 000000000..bd0fda702 --- /dev/null +++ b/src/test/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletionsTest.java @@ -0,0 +1,49 @@ +package org.cyclops.integratedscripting.core.language; + +import com.google.common.collect.Lists; +import org.cyclops.integratedscripting.api.language.ScriptCompletions; +import org.junit.Test; + +import java.util.Map; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; + +public class JavaScriptCompletionsTest { + + private static final Map SIGNATURES = Map.of( + "itemstackSize", "(Item): Integer", + "itemstackIsStackable", "(Item): Boolean", + "integerMax", "(Integer, Integer): Integer" + ); + + private static ScriptCompletions complete(String script) { + return JavaScriptCompletions.complete(script, script.length(), SIGNATURES); + } + + @Test + public void testCompleteAll() { + assertThat(complete("const a = idContext.ops."), equalTo(new ScriptCompletions(24, Lists.newArrayList( + new ScriptCompletions.Item("integerMax", "(Integer, Integer): Integer"), + new ScriptCompletions.Item("itemstackIsStackable", "(Item): Boolean"), + new ScriptCompletions.Item("itemstackSize", "(Item): Integer") + )))); + } + + @Test + public void testCompletePrefixIgnoringCase() { + assertThat(complete("idContext.ops.ITEMSTACKs"), equalTo(new ScriptCompletions(14, Lists.newArrayList( + new ScriptCompletions.Item("itemstackSize", "(Item): Integer") + )))); + } + + @Test + public void testCompleteNone() { + assertThat(complete("const a = ops."), nullValue()); + assertThat(complete("idContext.ops.foo"), nullValue()); + assertThat(complete("idContext.ops.itemstackSize"), nullValue()); + assertThat(JavaScriptCompletions.complete("idContext.ops.itemstack", 18, SIGNATURES), nullValue()); + } + +} From 77d5d2bb4c944dbc505957a4824842c0a653ccd3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 20:22:31 +0000 Subject: [PATCH 2/4] Show the signature of idContext.ops calls in the script editor While the cursor is inside the arguments of an idContext.ops call, its argument and output types are shown above the line. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../api/language/ILanguageHandler.java | 10 +++++ .../gui/component/input/WidgetTextArea.java | 40 +++++++++++++++-- .../ContainerScreenTerminalScripting.java | 4 ++ .../core/language/JavaScriptCompletions.java | 43 +++++++++++++++++++ .../language/LanguageHandlerJavaScript.java | 6 +++ .../language/JavaScriptCompletionsTest.java | 18 ++++++++ 6 files changed, 118 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/cyclops/integratedscripting/api/language/ILanguageHandler.java b/src/main/java/org/cyclops/integratedscripting/api/language/ILanguageHandler.java index 551a0eb03..c3ac9eab1 100644 --- a/src/main/java/org/cyclops/integratedscripting/api/language/ILanguageHandler.java +++ b/src/main/java/org/cyclops/integratedscripting/api/language/ILanguageHandler.java @@ -76,6 +76,16 @@ public default ScriptCompletions getCompletions(String script, int cursor) { return null; } + /** + * @param script The script contents. + * @param cursor The cursor position. + * @return The signature of the function call the cursor is in, or null if unknown. + */ + @Nullable + public default String getSignatureHint(String script, int cursor) { + return null; + } + /** * Check a script for syntax errors, without executing it. * @param path The script path. diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java index 81045c7ae..dc63f2fbb 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java @@ -91,6 +91,8 @@ public class WidgetTextArea extends EditBox implements GuiEventListener { @Nullable private BiFunction completionProvider; @Nullable + private BiFunction signatureProvider; + @Nullable private ScriptCompletions completions; private int completionIndex; private WidgetTextArea.DisplayCache emptyDisplayCache = createEmptyDisplayCache(); @@ -269,6 +271,10 @@ public void setCompletionProvider(@Nullable BiFunction signatureProvider) { + this.signatureProvider = signatureProvider; + } + /** * Only show completions while typing, not when moving the cursor. */ @@ -883,9 +889,17 @@ public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float if (displayCache.cursor != null) { this.renderCursor(guiGraphics, displayCache.cursor, displayCache.cursorAtEnd); - // Show completions while typing - if (this.isFocused() && this.completions != null) { - this.renderCompletions(guiGraphics, this.convertLocalToScreen(displayCache.cursor)); + // Show completions while typing, or else the signature of the call at the cursor + if (this.isFocused()) { + Pos2i cursor = this.convertLocalToScreen(displayCache.cursor); + if (this.completions != null) { + this.renderCompletions(guiGraphics, cursor); + } else if (this.signatureProvider != null) { + String signature = this.signatureProvider.apply(this.getValue(), this.getCursorPos()); + if (signature != null) { + this.renderSignature(guiGraphics, cursor, signature); + } + } } } @@ -992,6 +1006,26 @@ private Pair fitHorizontally(int width) { return Pair.of(x, width); } + private void renderSignature(GuiGraphics guiGraphics, Pos2i cursor, String signature) { + Pair xAndWidth = this.fitHorizontally(this.font.width(signature) + 2); + int x = xAndWidth.getLeft(); + int width = xAndWidth.getRight(); + if (this.font.width(signature) > width - 2) { + signature = this.font.plainSubstrByWidth(signature, width - 2 - this.font.width("\u2026")) + "\u2026"; + } + // Show above the line of the cursor if possible, otherwise below it + int y = cursor.y - ROW_HEIGHT - 1; + if (y < this.getY() - 1) { + y = cursor.y + ROW_HEIGHT + 1; + } + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(0, 0, 200); + guiGraphics.fill(x - 1, y - 1, x + width + 1, y + ROW_HEIGHT + 1, Helpers.RGBAToInt(80, 80, 80, 255)); + guiGraphics.fill(x, y, x + width, y + ROW_HEIGHT, Helpers.RGBAToInt(255, 250, 220, 255)); + guiGraphics.drawString(this.font, signature, x + 1, y + 1, Helpers.RGBToInt(60, 60, 60), false); + guiGraphics.pose().popPose(); + } + private void renderCursor(GuiGraphics guiGraphics, Pos2i pos, boolean cursorAtEnd) { if (this.isFocused() && this.frameTick / 6 % 2 == 0) { pos = this.convertLocalToScreen(pos); diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java index 312a9167c..0f126149a 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java @@ -186,6 +186,10 @@ public int getTotalRows() { ILanguageHandler languageHandler = getActiveLanguageHandler(); return languageHandler != null ? languageHandler.getCompletions(text, cursor) : null; }); + textArea.setSignatureProvider((text, cursor) -> { + ILanguageHandler languageHandler = getActiveLanguageHandler(); + return languageHandler != null ? languageHandler.getSignatureHint(text, cursor) : null; + }); textArea.setMarkupProvider(text -> { Path path = getMenu().getActiveScriptPath(); ILanguageHandler languageHandler = path != null ? LanguageHandlers.REGISTRY.getProvider(path) : null; diff --git a/src/main/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletions.java b/src/main/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletions.java index dfb8d68f5..d81ae087c 100644 --- a/src/main/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletions.java +++ b/src/main/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletions.java @@ -16,6 +16,8 @@ public class JavaScriptCompletions { public static final String OPS_PREFIX = "idContext.ops."; + // How far back to look for the call that the cursor is in + private static final int MAX_CALL_DISTANCE = 1000; /** * @param script A script. @@ -51,4 +53,45 @@ public static ScriptCompletions complete(String script, int cursor, Map signatures) { + int depth = 0; + for (int i = cursor - 1; i >= 0 && i >= cursor - MAX_CALL_DISTANCE; i--) { + char c = script.charAt(i); + if (c == ')' || c == ']' || c == '}') { + depth++; + } else if (c == '[' || c == '{') { + if (depth == 0) { + return null; + } + depth--; + } else if (c == '(') { + if (depth > 0) { + depth--; + } else { + int end = i; + int start = end; + while (start > 0 && Character.isJavaIdentifierPart(script.charAt(start - 1))) { + start--; + } + String name = script.substring(start, end); + String signature = signatures.get(name); + if (signature != null && script.startsWith(OPS_PREFIX, start - OPS_PREFIX.length())) { + return name + signature; + } + // Look further, for calls nested in arguments + } + } else if (c == ';' && depth == 0) { + return null; + } + } + return null; + } + } diff --git a/src/main/java/org/cyclops/integratedscripting/core/language/LanguageHandlerJavaScript.java b/src/main/java/org/cyclops/integratedscripting/core/language/LanguageHandlerJavaScript.java index 13b65039a..3fa51aecf 100644 --- a/src/main/java/org/cyclops/integratedscripting/core/language/LanguageHandlerJavaScript.java +++ b/src/main/java/org/cyclops/integratedscripting/core/language/LanguageHandlerJavaScript.java @@ -187,6 +187,12 @@ public ScriptCompletions getCompletions(String script, int cursor) { return JavaScriptCompletions.complete(script, cursor, getOperatorSignatures()); } + @Nullable + @Override + public String getSignatureHint(String script, int cursor) { + return JavaScriptCompletions.signatureHint(script, cursor, getOperatorSignatures()); + } + protected Map getOperatorSignatures() { // Operators are registered once, so this can be cached if (this.operatorSignatures == null) { diff --git a/src/test/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletionsTest.java b/src/test/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletionsTest.java index bd0fda702..47fa596e9 100644 --- a/src/test/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletionsTest.java +++ b/src/test/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletionsTest.java @@ -46,4 +46,22 @@ public void testCompleteNone() { assertThat(JavaScriptCompletions.complete("idContext.ops.itemstack", 18, SIGNATURES), nullValue()); } + @Test + public void testSignatureHint() { + String script = "const a = idContext.ops.integerMax(1, Math.max(2, 3), "; + assertThat(JavaScriptCompletions.signatureHint(script, script.length(), SIGNATURES), equalTo("integerMax(Integer, Integer): Integer")); + String nested = "idContext.ops.integerMax(idContext.ops.itemstackSize(x"; + assertThat(JavaScriptCompletions.signatureHint(nested, nested.length(), SIGNATURES), equalTo("itemstackSize(Item): Integer")); + } + + @Test + public void testSignatureHintNone() { + String closed = "idContext.ops.itemstackSize(x) + 1"; + assertThat(JavaScriptCompletions.signatureHint(closed, closed.length(), SIGNATURES), nullValue()); + String other = "foo(x"; + assertThat(JavaScriptCompletions.signatureHint(other, other.length(), SIGNATURES), nullValue()); + String statement = "idContext.ops.itemstackSize(x;\nfoo("; + assertThat(JavaScriptCompletions.signatureHint(statement, statement.length(), SIGNATURES), nullValue()); + } + } From 70e093ad9679c4bc02b55580dc43575cf200c401 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 17:15:26 +0000 Subject: [PATCH 3/4] Insert completions by clicking them in the script editor Completions could only be chosen with the keyboard. Clicking one now inserts it, and scrolling on the list moves through it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../gui/component/input/WidgetTextArea.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java index dc63f2fbb..e90973df2 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java @@ -28,6 +28,7 @@ import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.Style; +import net.minecraft.util.Mth; import net.minecraft.util.StringUtil; import net.neoforged.api.distmarker.Dist; import net.neoforged.api.distmarker.OnlyIn; @@ -95,6 +96,12 @@ public class WidgetTextArea extends EditBox implements GuiEventListener { @Nullable private ScriptCompletions completions; private int completionIndex; + // Where the completions were last drawn, to handle clicks and scrolling on them + private int completionsX; + private int completionsY; + private int completionsWidth; + private int completionsFirstItem; + private int completionsRows; private WidgetTextArea.DisplayCache emptyDisplayCache = createEmptyDisplayCache(); @Nullable private WidgetTextArea.DisplayCache displayCache = emptyDisplayCache; @@ -321,6 +328,17 @@ private boolean completionKeyPressed(int key) { } } + /** + * @return The row of the shown completions at the given position, or -1 if there is none. + */ + private int getCompletionRowAt(double mouseX, double mouseY) { + if (this.completions == null || this.completionsRows == 0 || mouseX < this.completionsX || mouseX >= this.completionsX + this.completionsWidth + || mouseY < this.completionsY + 1 || mouseY >= this.completionsY + 1 + this.completionsRows * ROW_HEIGHT) { + return -1; + } + return Math.min((int) (mouseY - this.completionsY - 1) / ROW_HEIGHT, this.completionsRows - 1); + } + private void acceptCompletion() { TextHistory.State before = this.getState(); // Replace the text directly instead of selecting it, as a selection would change the bound member @@ -431,6 +449,13 @@ public void tick() { @Override public boolean mouseClicked(double mouseX, double mouseY, int mouseButton) { + // Insert a clicked completion + int completionRow = this.getCompletionRowAt(mouseX, mouseY); + if (completionRow >= 0 && mouseButton == 0) { + this.completionIndex = this.completionsFirstItem + completionRow; + this.acceptCompletion(); + return true; + } this.completions = null; if (mouseButton == 1 && mouseX >= this.getX() && mouseX < this.getX() + this.width && mouseY >= this.getY() && mouseY < this.getY() + this.height) { @@ -484,6 +509,11 @@ private void selectWord(int p_98142_) { @Override public boolean mouseScrolled(double mouseX, double mouseY, double mouseZ, double scroll) { + // Move through the completions when scrolling on them + if (scroll != 0 && this.getCompletionRowAt(mouseX, mouseY) >= 0) { + this.completionIndex = Mth.clamp(this.completionIndex - (int) Math.signum(scroll), 0, this.completions.items().size() - 1); + return true; + } if (this.scrollBar != null && mouseX >= this.getX() && mouseX < this.getX() + this.width && mouseY >= this.getY() && mouseY < this.getY() + this.height && this.scrollBar.mouseScrolled(mouseX, mouseY, mouseZ, scroll)) { @@ -971,6 +1001,11 @@ private void renderCompletions(GuiGraphics guiGraphics, Pos2i cursor) { if (y + height > this.getY() + this.getHeight() && cursor.y - height - 1 >= this.getY()) { y = cursor.y - height - 1; } + this.completionsX = x; + this.completionsY = y; + this.completionsWidth = width; + this.completionsFirstItem = firstItem; + this.completionsRows = rows; guiGraphics.pose().pushPose(); guiGraphics.pose().translate(0, 0, 200); guiGraphics.fill(x - 1, y - 1, x + width + 1, y + height + 1, Helpers.RGBAToInt(80, 80, 80, 255)); From 741db4099bcbf7378aea32c7c6ea0d324a315d0b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 17:24:29 +0000 Subject: [PATCH 4/4] Color value types in script editor completions and signatures Types in the completion list and signature hint were plain text. They are now shown in the colors of their value types, as in tooltips, on a dark background those colors are made for, with a neutral selection. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../api/language/ILanguageHandler.java | 3 +- .../api/language/ScriptCompletions.java | 4 +- .../gui/component/input/WidgetTextArea.java | 53 ++++++++++++------- .../core/language/JavaScriptCompletions.java | 11 ++-- .../language/LanguageHandlerJavaScript.java | 33 ++++++++---- .../language/JavaScriptCompletionsTest.java | 21 ++++---- 6 files changed, 79 insertions(+), 46 deletions(-) diff --git a/src/main/java/org/cyclops/integratedscripting/api/language/ILanguageHandler.java b/src/main/java/org/cyclops/integratedscripting/api/language/ILanguageHandler.java index c3ac9eab1..0f19c837e 100644 --- a/src/main/java/org/cyclops/integratedscripting/api/language/ILanguageHandler.java +++ b/src/main/java/org/cyclops/integratedscripting/api/language/ILanguageHandler.java @@ -1,5 +1,6 @@ package org.cyclops.integratedscripting.api.language; +import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Style; import net.neoforged.api.distmarker.Dist; import net.neoforged.api.distmarker.OnlyIn; @@ -82,7 +83,7 @@ public default ScriptCompletions getCompletions(String script, int cursor) { * @return The signature of the function call the cursor is in, or null if unknown. */ @Nullable - public default String getSignatureHint(String script, int cursor) { + public default Component getSignatureHint(String script, int cursor) { return null; } diff --git a/src/main/java/org/cyclops/integratedscripting/api/language/ScriptCompletions.java b/src/main/java/org/cyclops/integratedscripting/api/language/ScriptCompletions.java index 095a3b034..71fcd4fff 100644 --- a/src/main/java/org/cyclops/integratedscripting/api/language/ScriptCompletions.java +++ b/src/main/java/org/cyclops/integratedscripting/api/language/ScriptCompletions.java @@ -1,5 +1,7 @@ package org.cyclops.integratedscripting.api.language; +import net.minecraft.network.chat.Component; + import java.util.List; /** @@ -14,7 +16,7 @@ public record ScriptCompletions(int start, List items) { * @param name The text to insert. * @param detail A short description, such as a signature. */ - public record Item(String name, String detail) { + public record Item(String name, Component detail) { } } diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java index e90973df2..ef1564ed7 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java @@ -25,9 +25,12 @@ import net.minecraft.client.gui.screens.inventory.BookEditScreen; import net.minecraft.client.renderer.GameRenderer; import net.minecraft.client.renderer.Rect2i; +import net.minecraft.locale.Language; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.Style; +import net.minecraft.util.FormattedCharSequence; import net.minecraft.util.Mth; import net.minecraft.util.StringUtil; import net.neoforged.api.distmarker.Dist; @@ -72,6 +75,13 @@ public class WidgetTextArea extends EditBox implements GuiEventListener { public static final int ROW_HEIGHT = 9; private static final int COMPLETION_ROWS = 6; + // Colors of the completion and signature popups + private static final int POPUP_BACKGROUND = 0xFF202020; + private static final int POPUP_BORDER = 0xFFA0A0A0; + // Neutral, so the colors of value types stay readable + private static final int POPUP_SELECTED = 0xFF505050; + private static final int POPUP_TEXT = 0xFFE0E0E0; + private static final int POPUP_DETAIL = 0xFF909090; public static final int SCROLLBAR_WIDTH = 14; private final TextFieldHelperExtended textFieldHelper; @@ -92,7 +102,7 @@ public class WidgetTextArea extends EditBox implements GuiEventListener { @Nullable private BiFunction completionProvider; @Nullable - private BiFunction signatureProvider; + private BiFunction signatureProvider; @Nullable private ScriptCompletions completions; private int completionIndex; @@ -278,7 +288,7 @@ public void setCompletionProvider(@Nullable BiFunction signatureProvider) { + public void setSignatureProvider(@Nullable BiFunction signatureProvider) { this.signatureProvider = signatureProvider; } @@ -925,7 +935,7 @@ public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float if (this.completions != null) { this.renderCompletions(guiGraphics, cursor); } else if (this.signatureProvider != null) { - String signature = this.signatureProvider.apply(this.getValue(), this.getCursorPos()); + Component signature = this.signatureProvider.apply(this.getValue(), this.getCursorPos()); if (signature != null) { this.renderSignature(guiGraphics, cursor, signature); } @@ -1008,24 +1018,21 @@ private void renderCompletions(GuiGraphics guiGraphics, Pos2i cursor) { this.completionsRows = rows; guiGraphics.pose().pushPose(); guiGraphics.pose().translate(0, 0, 200); - guiGraphics.fill(x - 1, y - 1, x + width + 1, y + height + 1, Helpers.RGBAToInt(80, 80, 80, 255)); - guiGraphics.fill(x, y, x + width, y + height, Helpers.RGBAToInt(250, 250, 250, 255)); + // Dark, like tooltips, as the colors of value types are made for those + guiGraphics.fill(x - 1, y - 1, x + width + 1, y + height + 1, POPUP_BORDER); + guiGraphics.fill(x, y, x + width, y + height, POPUP_BACKGROUND); for (int i = 0; i < rows; i++) { ScriptCompletions.Item item = items.get(firstItem + i); int rowY = y + 1 + i * ROW_HEIGHT; if (firstItem + i == this.completionIndex) { - guiGraphics.fill(x, rowY, x + width, rowY + ROW_HEIGHT, Helpers.RGBAToInt(190, 210, 255, 255)); + guiGraphics.fill(x, rowY, x + width, rowY + ROW_HEIGHT, POPUP_SELECTED); } String name = this.font.plainSubstrByWidth(item.name(), width - 2); - guiGraphics.drawString(this.font, name, x + 1, rowY + 1, 0, false); + guiGraphics.drawString(this.font, name, x + 1, rowY + 1, POPUP_TEXT, false); int detailX = x + 1 + this.font.width(name) + 4; int detailWidth = x + width - 1 - detailX; if (detailWidth > 10) { - String detail = item.detail(); - if (this.font.width(detail) > detailWidth) { - detail = this.font.plainSubstrByWidth(detail, detailWidth - this.font.width("\u2026")) + "\u2026"; - } - guiGraphics.drawString(this.font, detail, detailX, rowY + 1, Helpers.RGBToInt(120, 120, 120), false); + guiGraphics.drawString(this.font, this.truncate(item.detail(), detailWidth), detailX, rowY + 1, POPUP_DETAIL, false); } } guiGraphics.pose().popPose(); @@ -1041,13 +1048,10 @@ private Pair fitHorizontally(int width) { return Pair.of(x, width); } - private void renderSignature(GuiGraphics guiGraphics, Pos2i cursor, String signature) { + private void renderSignature(GuiGraphics guiGraphics, Pos2i cursor, Component signature) { Pair xAndWidth = this.fitHorizontally(this.font.width(signature) + 2); int x = xAndWidth.getLeft(); int width = xAndWidth.getRight(); - if (this.font.width(signature) > width - 2) { - signature = this.font.plainSubstrByWidth(signature, width - 2 - this.font.width("\u2026")) + "\u2026"; - } // Show above the line of the cursor if possible, otherwise below it int y = cursor.y - ROW_HEIGHT - 1; if (y < this.getY() - 1) { @@ -1055,12 +1059,23 @@ private void renderSignature(GuiGraphics guiGraphics, Pos2i cursor, String signa } guiGraphics.pose().pushPose(); guiGraphics.pose().translate(0, 0, 200); - guiGraphics.fill(x - 1, y - 1, x + width + 1, y + ROW_HEIGHT + 1, Helpers.RGBAToInt(80, 80, 80, 255)); - guiGraphics.fill(x, y, x + width, y + ROW_HEIGHT, Helpers.RGBAToInt(255, 250, 220, 255)); - guiGraphics.drawString(this.font, signature, x + 1, y + 1, Helpers.RGBToInt(60, 60, 60), false); + guiGraphics.fill(x - 1, y - 1, x + width + 1, y + ROW_HEIGHT + 1, POPUP_BORDER); + guiGraphics.fill(x, y, x + width, y + ROW_HEIGHT, POPUP_BACKGROUND); + guiGraphics.drawString(this.font, this.truncate(signature, width - 2), x + 1, y + 1, POPUP_TEXT, false); guiGraphics.pose().popPose(); } + /** + * @return The text, cut off with an ellipsis if it is wider than the given width. + */ + private FormattedCharSequence truncate(Component text, int width) { + if (this.font.width(text) <= width) { + return text.getVisualOrderText(); + } + FormattedText truncated = FormattedText.composite(this.font.substrByWidth(text, Math.max(0, width - this.font.width("\u2026"))), FormattedText.of("\u2026")); + return Language.getInstance().getVisualOrder(truncated); + } + private void renderCursor(GuiGraphics guiGraphics, Pos2i pos, boolean cursorAtEnd) { if (this.isFocused() && this.frameTick / 6 % 2 == 0) { pos = this.convertLocalToScreen(pos); diff --git a/src/main/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletions.java b/src/main/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletions.java index d81ae087c..d4d4c6e39 100644 --- a/src/main/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletions.java +++ b/src/main/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletions.java @@ -1,6 +1,7 @@ package org.cyclops.integratedscripting.core.language; import com.google.common.collect.Lists; +import net.minecraft.network.chat.Component; import org.cyclops.integratedscripting.api.language.ScriptCompletions; import javax.annotation.Nullable; @@ -26,7 +27,7 @@ public class JavaScriptCompletions { * @return The operators matching the name before the cursor, or null if the cursor is not after idContext.ops. */ @Nullable - public static ScriptCompletions complete(String script, int cursor, Map signatures) { + public static ScriptCompletions complete(String script, int cursor, Map signatures) { int start = cursor; while (start > 0 && Character.isJavaIdentifierPart(script.charAt(start - 1))) { start--; @@ -41,7 +42,7 @@ public static ScriptCompletions complete(String script, int cursor, Map items = Lists.newArrayList(); - for (Map.Entry entry : signatures.entrySet()) { + for (Map.Entry entry : signatures.entrySet()) { if (entry.getKey().toLowerCase(Locale.ROOT).startsWith(prefixLower)) { items.add(new ScriptCompletions.Item(entry.getKey(), entry.getValue())); } @@ -60,7 +61,7 @@ public static ScriptCompletions complete(String script, int cursor, Map signatures) { + public static Component signatureHint(String script, int cursor, Map signatures) { int depth = 0; for (int i = cursor - 1; i >= 0 && i >= cursor - MAX_CALL_DISTANCE; i--) { char c = script.charAt(i); @@ -81,9 +82,9 @@ public static String signatureHint(String script, int cursor, Map tokenStyles; private final IScriptFactory scriptFactory; @Nullable - private Map operatorSignatures; + private Map operatorSignatures; public LanguageHandlerJavaScript() { this.tokenStyles = Maps.newHashMap(); @@ -189,26 +190,38 @@ public ScriptCompletions getCompletions(String script, int cursor) { @Nullable @Override - public String getSignatureHint(String script, int cursor) { + public Component getSignatureHint(String script, int cursor) { return JavaScriptCompletions.signatureHint(script, cursor, getOperatorSignatures()); } - protected Map getOperatorSignatures() { + protected Map getOperatorSignatures() { // Operators are registered once, so this can be cached if (this.operatorSignatures == null) { - Map signatures = Maps.newHashMap(); + Map signatures = Maps.newHashMap(); for (Map.Entry entry : Operators.REGISTRY.getGlobalInteractOperators().entrySet()) { IOperator operator = entry.getValue(); - String inputs = Arrays.stream(operator.getInputTypes()) - .map(type -> L10NHelpers.localize(type.getTranslationKey())) - .collect(Collectors.joining(", ")); - signatures.put(entry.getKey(), "(" + inputs + "): " + L10NHelpers.localize(operator.getOutputType().getTranslationKey())); + MutableComponent signature = Component.literal("("); + IValueType[] inputs = operator.getInputTypes(); + for (int i = 0; i < inputs.length; i++) { + if (i > 0) { + signature.append(", "); + } + signature.append(getTypeName(inputs[i])); + } + signatures.put(entry.getKey(), signature.append("): ").append(getTypeName(operator.getOutputType()))); } this.operatorSignatures = signatures; } return this.operatorSignatures; } + /** + * @return The name of a value type in its color, as in tooltips. + */ + protected static Component getTypeName(IValueType type) { + return Component.translatable(type.getTranslationKey()).withStyle(type.getDisplayColorFormat()); + } + @Override public IScriptFactory getScriptFactory() throws EvaluationException { return this.scriptFactory; diff --git a/src/test/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletionsTest.java b/src/test/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletionsTest.java index 47fa596e9..b700c01e6 100644 --- a/src/test/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletionsTest.java +++ b/src/test/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletionsTest.java @@ -1,6 +1,7 @@ package org.cyclops.integratedscripting.core.language; import com.google.common.collect.Lists; +import net.minecraft.network.chat.Component; import org.cyclops.integratedscripting.api.language.ScriptCompletions; import org.junit.Test; @@ -12,10 +13,10 @@ public class JavaScriptCompletionsTest { - private static final Map SIGNATURES = Map.of( - "itemstackSize", "(Item): Integer", - "itemstackIsStackable", "(Item): Boolean", - "integerMax", "(Integer, Integer): Integer" + private static final Map SIGNATURES = Map.of( + "itemstackSize", Component.literal("(Item): Integer"), + "itemstackIsStackable", Component.literal("(Item): Boolean"), + "integerMax", Component.literal("(Integer, Integer): Integer") ); private static ScriptCompletions complete(String script) { @@ -25,16 +26,16 @@ private static ScriptCompletions complete(String script) { @Test public void testCompleteAll() { assertThat(complete("const a = idContext.ops."), equalTo(new ScriptCompletions(24, Lists.newArrayList( - new ScriptCompletions.Item("integerMax", "(Integer, Integer): Integer"), - new ScriptCompletions.Item("itemstackIsStackable", "(Item): Boolean"), - new ScriptCompletions.Item("itemstackSize", "(Item): Integer") + new ScriptCompletions.Item("integerMax", Component.literal("(Integer, Integer): Integer")), + new ScriptCompletions.Item("itemstackIsStackable", Component.literal("(Item): Boolean")), + new ScriptCompletions.Item("itemstackSize", Component.literal("(Item): Integer")) )))); } @Test public void testCompletePrefixIgnoringCase() { assertThat(complete("idContext.ops.ITEMSTACKs"), equalTo(new ScriptCompletions(14, Lists.newArrayList( - new ScriptCompletions.Item("itemstackSize", "(Item): Integer") + new ScriptCompletions.Item("itemstackSize", Component.literal("(Item): Integer")) )))); } @@ -49,9 +50,9 @@ public void testCompleteNone() { @Test public void testSignatureHint() { String script = "const a = idContext.ops.integerMax(1, Math.max(2, 3), "; - assertThat(JavaScriptCompletions.signatureHint(script, script.length(), SIGNATURES), equalTo("integerMax(Integer, Integer): Integer")); + assertThat(JavaScriptCompletions.signatureHint(script, script.length(), SIGNATURES).getString(), equalTo("integerMax(Integer, Integer): Integer")); String nested = "idContext.ops.integerMax(idContext.ops.itemstackSize(x"; - assertThat(JavaScriptCompletions.signatureHint(nested, nested.length(), SIGNATURES), equalTo("itemstackSize(Item): Integer")); + assertThat(JavaScriptCompletions.signatureHint(nested, nested.length(), SIGNATURES).getString(), equalTo("itemstackSize(Item): Integer")); } @Test