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..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; @@ -66,6 +67,26 @@ 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; + } + + /** + * @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 Component 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/api/language/ScriptCompletions.java b/src/main/java/org/cyclops/integratedscripting/api/language/ScriptCompletions.java new file mode 100644 index 000000000..71fcd4fff --- /dev/null +++ b/src/main/java/org/cyclops/integratedscripting/api/language/ScriptCompletions.java @@ -0,0 +1,22 @@ +package org.cyclops.integratedscripting.api.language; + +import net.minecraft.network.chat.Component; + +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, 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 05987749f..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,13 @@ 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; import net.neoforged.api.distmarker.OnlyIn; @@ -39,6 +43,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 +52,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 +74,14 @@ 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; @@ -85,6 +99,19 @@ public class WidgetTextArea extends EditBox implements GuiEventListener { private IInputListener listenerCursor; @Nullable private IMarkupProvider markupProvider; + @Nullable + private BiFunction completionProvider; + @Nullable + private BiFunction signatureProvider; + @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; @@ -257,6 +284,86 @@ public void setMarkupProvider(@Nullable IMarkupProvider markupProvider) { this.layout = null; } + public void setCompletionProvider(@Nullable BiFunction completionProvider) { + this.completionProvider = completionProvider; + } + + public void setSignatureProvider(@Nullable BiFunction signatureProvider) { + this.signatureProvider = signatureProvider; + } + + /** + * 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; + } + } + + /** + * @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 + 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 +459,14 @@ 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) { // Don't select everything on right-click, as typing would then replace the whole script @@ -404,6 +519,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)) { @@ -451,6 +571,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 +584,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 +605,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 +928,19 @@ 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, 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) { + Component signature = this.signatureProvider.apply(this.getValue(), this.getCursorPos()); + if (signature != null) { + this.renderSignature(guiGraphics, cursor, signature); + } + } + } } // Render scrollbar @@ -854,6 +992,90 @@ 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; + } + this.completionsX = x; + this.completionsY = y; + this.completionsWidth = width; + this.completionsFirstItem = firstItem; + this.completionsRows = rows; + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(0, 0, 200); + // 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, POPUP_SELECTED); + } + String name = this.font.plainSubstrByWidth(item.name(), width - 2); + 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) { + guiGraphics.drawString(this.font, this.truncate(item.detail(), detailWidth), detailX, rowY + 1, POPUP_DETAIL, 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 renderSignature(GuiGraphics guiGraphics, Pos2i cursor, Component signature) { + Pair xAndWidth = this.fitHorizontally(this.font.width(signature) + 2); + int x = xAndWidth.getLeft(); + int width = xAndWidth.getRight(); + // 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, 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/client/gui/container/ContainerScreenTerminalScripting.java b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java index e19101ca9..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 @@ -182,6 +182,14 @@ 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.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; @@ -733,6 +741,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 +908,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..d4d4c6e39 --- /dev/null +++ b/src/main/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletions.java @@ -0,0 +1,98 @@ +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; +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."; + // 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. + * @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); + } + + /** + * @param script A script. + * @param cursor The cursor position. + * @param signatures Operator names mapped to their signatures. + * @return The name and signature of the operator call the cursor is in, or null if it is not in one. + */ + @Nullable + 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); + 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); + Component signature = signatures.get(name); + if (signature != null && script.startsWith(OPS_PREFIX, start - OPS_PREFIX.length())) { + return Component.literal(name).append(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 0fe44e486..681ea95a3 100644 --- a/src/main/java/org/cyclops/integratedscripting/core/language/LanguageHandlerJavaScript.java +++ b/src/main/java/org/cyclops/integratedscripting/core/language/LanguageHandlerJavaScript.java @@ -2,6 +2,8 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.Style; import net.minecraft.network.chat.TextColor; import org.apache.commons.io.FileUtils; @@ -9,9 +11,13 @@ import org.cyclops.cyclopscore.client.gui.image.IImage; import org.cyclops.cyclopscore.helper.Helpers; import org.cyclops.integrateddynamics.api.evaluate.EvaluationException; +import org.cyclops.integrateddynamics.api.evaluate.operator.IOperator; +import org.cyclops.integrateddynamics.api.evaluate.variable.IValueType; +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; @@ -55,6 +61,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 +182,46 @@ public Set getTopLevelMembers(String script) { return JavaScriptDeclarations.find(script); } + @Nullable + @Override + public ScriptCompletions getCompletions(String script, int cursor) { + return JavaScriptCompletions.complete(script, cursor, getOperatorSignatures()); + } + + @Nullable + @Override + public Component 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) { + Map signatures = Maps.newHashMap(); + for (Map.Entry entry : Operators.REGISTRY.getGlobalInteractOperators().entrySet()) { + IOperator operator = entry.getValue(); + 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 new file mode 100644 index 000000000..b700c01e6 --- /dev/null +++ b/src/test/java/org/cyclops/integratedscripting/core/language/JavaScriptCompletionsTest.java @@ -0,0 +1,68 @@ +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; + +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", Component.literal("(Item): Integer"), + "itemstackIsStackable", Component.literal("(Item): Boolean"), + "integerMax", Component.literal("(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", 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", Component.literal("(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()); + } + + @Test + public void testSignatureHint() { + String script = "const a = idContext.ops.integerMax(1, Math.max(2, 3), "; + 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).getString(), 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()); + } + +}