From a64469b3d6929678fa4c4cf74633c8a090dcdec0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 19:50:41 +0000 Subject: [PATCH 1/3] Show syntax errors in the script editor Syntax errors only showed up when a variable card using the script was evaluated somewhere, for example as an error in a display panel. Check the active script for syntax errors on the server, without executing it, once it hasn't changed for half a second. Mark the line and position of the first error in the editor, and show its message when hovering it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../api/language/ILanguageHandler.java | 13 ++++ .../api/language/ScriptSyntaxError.java | 11 +++ .../gui/component/input/WidgetTextArea.java | 35 ++++++++- .../ContainerScreenTerminalScripting.java | 6 ++ .../language/LanguageHandlerJavaScript.java | 29 ++++++++ .../container/ContainerTerminalScripting.java | 66 +++++++++++++++++ .../TerminalScriptingSyntaxErrorPacket.java | 71 +++++++++++++++++++ .../proxy/CommonProxy.java | 2 + .../integratedscripting/lang/en_us.json | 2 +- .../LanguageHandlerJavaScriptSyntaxTest.java | 43 +++++++++++ 10 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/cyclops/integratedscripting/api/language/ScriptSyntaxError.java create mode 100644 src/main/java/org/cyclops/integratedscripting/network/packet/TerminalScriptingSyntaxErrorPacket.java create mode 100644 src/test/java/org/cyclops/integratedscripting/core/language/LanguageHandlerJavaScriptSyntaxTest.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 55962bb24..cef253c07 100644 --- a/src/main/java/org/cyclops/integratedscripting/api/language/ILanguageHandler.java +++ b/src/main/java/org/cyclops/integratedscripting/api/language/ILanguageHandler.java @@ -8,6 +8,8 @@ import org.cyclops.integrateddynamics.api.evaluate.EvaluationException; import org.cyclops.integratedscripting.api.network.IScriptFactory; +import javax.annotation.Nullable; +import java.nio.file.Path; import java.util.List; /** @@ -27,6 +29,17 @@ public interface ILanguageHandler { public IScriptFactory getScriptFactory() throws EvaluationException; + /** + * Check a script for syntax errors, without executing it. + * @param path The script path. + * @param script The script contents. + * @return The first syntax error, or null if there is none or if checking is not supported. + */ + @Nullable + public default ScriptSyntaxError checkSyntax(Path path, String script) { + return null; + } + public void onServerStarted(); } diff --git a/src/main/java/org/cyclops/integratedscripting/api/language/ScriptSyntaxError.java b/src/main/java/org/cyclops/integratedscripting/api/language/ScriptSyntaxError.java new file mode 100644 index 000000000..b41bb28a3 --- /dev/null +++ b/src/main/java/org/cyclops/integratedscripting/api/language/ScriptSyntaxError.java @@ -0,0 +1,11 @@ +package org.cyclops.integratedscripting.api.language; + +/** + * A syntax error in a script. + * @param message The error message. + * @param line The 1-based line of the error. + * @param column The 1-based column of the error. + * @author rubensworks + */ +public record ScriptSyntaxError(String message, int line, int column) { +} 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 f2c3bd2f0..ef0c06f32 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.ScriptSyntaxError; import org.lwjgl.glfw.GLFW; import javax.annotation.Nullable; @@ -96,6 +97,8 @@ public class WidgetTextArea extends EditBox implements GuiEventListener { private boolean draggingSelection; @Nullable private IntPredicate shortcutListener; + @Nullable + private ScriptSyntaxError syntaxError; public WidgetTextArea(Font font, int x, int y, int width, int height, Component narrationMessage, boolean scrollBar, boolean showLineNumbers) { super(font, x, y, width, height, narrationMessage); @@ -175,6 +178,10 @@ public void selectRange(int start, int end) { this.ensureCursorVisible(); } + public void setSyntaxError(@Nullable ScriptSyntaxError syntaxError) { + this.syntaxError = syntaxError; + } + public void setHistory(TextHistory history) { this.history = history; } @@ -587,7 +594,7 @@ public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float this.getX(), line.y - offsetY + 2, 0.5f, - line.hasCursor ? 0 : Helpers.RGBToInt(120, 120, 120), + this.syntaxError != null && this.syntaxError.line() == line.lineNumber + 1 ? Helpers.RGBToInt(220, 0, 0) : (line.hasCursor ? 0 : Helpers.RGBToInt(120, 120, 120)), false, Font.DisplayMode.NORMAL ); @@ -595,6 +602,10 @@ public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float lastLineNumber = line.lineNumber; } + if (this.syntaxError != null) { + this.renderSyntaxError(guiGraphics, displayCache, lines, mouseX, mouseY); + } + // Show highlighting and cursor this.renderHighlight(displayCache.selection); if (displayCache.cursor != null) { @@ -607,6 +618,28 @@ public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float } } + private void renderSyntaxError(GuiGraphics guiGraphics, DisplayCache displayCache, List lines, int mouseX, int mouseY) { + String value = this.getValue(); + // Underline the character at the error position + int pos = Math.min(value.length(), TextEditing.lineOffset(value, this.syntaxError.line()) + Math.max(0, this.syntaxError.column() - 1)); + int line = findLineFromPos(displayCache.lineStarts, pos); + int row = line - this.firstRow; + if (row >= 0 && row < this.getHeight() / ROW_HEIGHT) { + int x = this.font.width(value.substring(displayCache.lineStarts[line], pos)); + int width = pos < value.length() && value.charAt(pos) != '\n' ? this.font.width(value.substring(pos, pos + 1)) : 4; + Pos2i screenPos = this.convertLocalToScreen(new Pos2i(x, row * ROW_HEIGHT)); + guiGraphics.fill(screenPos.x, screenPos.y + ROW_HEIGHT - 2, screenPos.x + width, screenPos.y + ROW_HEIGHT - 1, Helpers.RGBAToInt(220, 0, 0, 255)); + } + + // Show the message when hovering the error line + for (LineInfo lineInfo : lines) { + if (lineInfo.lineNumber + 1 == this.syntaxError.line() && mouseX >= this.getX() && mouseX < this.getX() + this.getWidth() + && mouseY >= lineInfo.y && mouseY < lineInfo.y + ROW_HEIGHT && Minecraft.getInstance().screen != null) { + Minecraft.getInstance().screen.setTooltipForNextRenderPass(this.font.split(Component.literal(this.syntaxError.message()), 200)); + } + } + } + private void renderCharBackground(GuiGraphics guiGraphics, DisplayCache displayCache, int pos, int color) { int line = findLineFromPos(displayCache.lineStarts, pos); int row = line - this.firstRow; 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 7311dbf2f..3d5fdd37f 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 @@ -135,6 +135,11 @@ public int getTotalRows() { return false; }); getMenu().setRemoteScriptChangeListener(this::onRemoteScriptChanged); + getMenu().setSyntaxErrorListener((disk, path) -> { + if (disk == getMenu().getActiveDisk() && path.equals(getMenu().getActiveScriptPath())) { + this.textArea.setSyntaxError(getMenu().getSyntaxError(disk, path)); + } + }); textArea.setListenerSelection(this::onSelectionModified); textArea.setListenerCursor(this::onSelectionModified); textArea.setMarkupProvider((style, line) -> { @@ -466,6 +471,7 @@ private void onActiveScriptSelected() { String script = getMenu().getActiveScript(); this.textArea.setValue(script == null ? "" : script); Path path = getMenu().getActiveScriptPath(); + this.textArea.setSyntaxError(path == null ? null : getMenu().getSyntaxError(getMenu().getActiveDisk(), path)); this.textArea.setHistory(path == null ? new TextHistory() : scriptHistories.computeIfAbsent(Pair.of(getMenu().getActiveDisk(), path), k -> new TextHistory())); } 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 a5ce5568a..26777eb70 100644 --- a/src/main/java/org/cyclops/integratedscripting/core/language/LanguageHandlerJavaScript.java +++ b/src/main/java/org/cyclops/integratedscripting/core/language/LanguageHandlerJavaScript.java @@ -12,9 +12,17 @@ import org.cyclops.integratedscripting.IntegratedScripting; import org.cyclops.integratedscripting.Reference; import org.cyclops.integratedscripting.api.language.ILanguageHandler; +import org.cyclops.integratedscripting.api.language.ScriptSyntaxError; import org.cyclops.integratedscripting.api.network.IScriptFactory; import org.cyclops.integratedscripting.client.gui.image.ScriptImages; import org.cyclops.integratedscripting.core.network.GraalScriptFactory; +import org.cyclops.integratedscripting.evaluate.ScriptHelpers; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.PolyglotException; +import org.graalvm.polyglot.Source; +import org.graalvm.polyglot.SourceSection; + +import javax.annotation.Nullable; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -115,6 +123,27 @@ public IScriptFactory getScriptFactory() throws EvaluationException { return this.scriptFactory; } + @Nullable + @Override + public ScriptSyntaxError checkSyntax(Path path, String script) { + try (Context context = ScriptHelpers.createBaseContext(null)) { + // Parsing doesn't execute the script + context.parse(Source.newBuilder("js", script, path.toString()).build()); + return null; + } catch (PolyglotException e) { + if (!e.isSyntaxError()) { + return null; + } + SourceSection location = e.getSourceLocation(); + // Only keep the first line, as the following lines contain a source excerpt + return new ScriptSyntaxError(e.getMessage().split("\n", 2)[0], + location != null && location.hasLines() ? location.getStartLine() : 1, + location != null && location.hasColumns() ? location.getStartColumn() : 1); + } catch (IOException e) { + return null; + } + } + @Override public void onServerStarted() { this.generateTypeScriptTypes(); diff --git a/src/main/java/org/cyclops/integratedscripting/inventory/container/ContainerTerminalScripting.java b/src/main/java/org/cyclops/integratedscripting/inventory/container/ContainerTerminalScripting.java index bc637de19..47e87e303 100644 --- a/src/main/java/org/cyclops/integratedscripting/inventory/container/ContainerTerminalScripting.java +++ b/src/main/java/org/cyclops/integratedscripting/inventory/container/ContainerTerminalScripting.java @@ -18,6 +18,7 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.lang3.tuple.Triple; import org.cyclops.cyclopscore.helper.ValueNotifierHelpers; import org.cyclops.cyclopscore.inventory.SimpleInventory; import org.cyclops.cyclopscore.inventory.container.InventoryContainer; @@ -33,14 +34,18 @@ import org.cyclops.integratedscripting.IntegratedScripting; import org.cyclops.integratedscripting.RegistryEntries; import org.cyclops.integratedscripting.api.item.IScriptVariableFacade; +import org.cyclops.integratedscripting.api.language.ILanguageHandler; +import org.cyclops.integratedscripting.api.language.ScriptSyntaxError; import org.cyclops.integratedscripting.api.network.IScriptingData; import org.cyclops.integratedscripting.api.network.IScriptingNetwork; import org.cyclops.integratedscripting.core.evaluate.ScriptVariableFacadeHandler; import org.cyclops.integratedscripting.core.item.ScriptVariableFacade; +import org.cyclops.integratedscripting.core.language.LanguageHandlers; import org.cyclops.integratedscripting.core.network.ScriptingNetworkHelpers; import org.cyclops.integratedscripting.network.packet.TerminalScriptingCreateNewScriptPacket; import org.cyclops.integratedscripting.network.packet.TerminalScriptingDeleteScriptPacket; import org.cyclops.integratedscripting.network.packet.TerminalScriptingModifiedScriptPacket; +import org.cyclops.integratedscripting.network.packet.TerminalScriptingSyntaxErrorPacket; import org.cyclops.integratedscripting.part.PartTypeTerminalScripting; import javax.annotation.Nullable; @@ -55,6 +60,8 @@ */ public class ContainerTerminalScripting extends InventoryContainer implements IDirtyMarkListener { + public static final int SYNTAX_CHECK_DELAY_TICKS = 10; + public static Pattern INVALID_MEMBER_NAME = Pattern.compile("[^0-9a-zA-Z_]"); public static Pattern VALID_MEMBER_NAME = Pattern.compile("[0-9a-zA-Z_]+"); @@ -73,6 +80,14 @@ public class ContainerTerminalScripting extends InventoryContainer implements ID private final int selectionId; @Nullable private BiConsumer remoteScriptChangeListener; + private final Map, ScriptSyntaxError> syntaxErrors = Maps.newHashMap(); + @Nullable + private BiConsumer syntaxErrorListener; + @Nullable + private Triple syntaxCheckPending; + @Nullable + private Triple syntaxCheckDone; + private int syntaxCheckTicks; public ContainerTerminalScripting(int id, Inventory playerInventory, RegistryFriendlyByteBuf packetBuffer) { this(id, playerInventory, PartHelpers.readPartTarget(packetBuffer), Optional.empty(), @@ -216,7 +231,58 @@ public void broadcastChanges() { } } }); + + this.checkActiveScriptSyntax(); + } + } + + /** + * Check the active script for syntax errors once it hasn't changed for a while, and send the result to the client. + */ + protected void checkActiveScriptSyntax() { + Path path = getActiveScriptPath(); + int disk = getActiveDisk(); + if (path == null || disk < 0) { + return; + } + String script = ScriptingNetworkHelpers.getScriptingData().getScripts(disk).get(path); + if (script == null) { + return; } + Triple key = Triple.of(disk, path, script); + if (!key.equals(this.syntaxCheckPending)) { + this.syntaxCheckPending = key; + this.syntaxCheckTicks = 0; + return; + } + if (key.equals(this.syntaxCheckDone) || ++this.syntaxCheckTicks < SYNTAX_CHECK_DELAY_TICKS) { + return; + } + this.syntaxCheckDone = key; + ILanguageHandler languageHandler = LanguageHandlers.REGISTRY.getProvider(path); + ScriptSyntaxError error = languageHandler == null ? null : languageHandler.checkSyntax(path, script); + IntegratedScripting._instance.getPacketHandler() + .sendToPlayer(new TerminalScriptingSyntaxErrorPacket(disk, path, error), (ServerPlayer) player); + } + + public void setSyntaxErrorListener(@Nullable BiConsumer syntaxErrorListener) { + this.syntaxErrorListener = syntaxErrorListener; + } + + public void setSyntaxError(int disk, Path path, @Nullable ScriptSyntaxError error) { + if (error == null) { + this.syntaxErrors.remove(Pair.of(disk, path)); + } else { + this.syntaxErrors.put(Pair.of(disk, path), error); + } + if (this.syntaxErrorListener != null) { + this.syntaxErrorListener.accept(disk, path); + } + } + + @Nullable + public ScriptSyntaxError getSyntaxError(int disk, Path path) { + return this.syntaxErrors.get(Pair.of(disk, path)); } @Override diff --git a/src/main/java/org/cyclops/integratedscripting/network/packet/TerminalScriptingSyntaxErrorPacket.java b/src/main/java/org/cyclops/integratedscripting/network/packet/TerminalScriptingSyntaxErrorPacket.java new file mode 100644 index 000000000..eacb71ecc --- /dev/null +++ b/src/main/java/org/cyclops/integratedscripting/network/packet/TerminalScriptingSyntaxErrorPacket.java @@ -0,0 +1,71 @@ +package org.cyclops.integratedscripting.network.packet; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.cyclops.cyclopscore.network.CodecField; +import org.cyclops.cyclopscore.network.PacketCodec; +import org.cyclops.integratedscripting.Reference; +import org.cyclops.integratedscripting.api.language.ScriptSyntaxError; +import org.cyclops.integratedscripting.inventory.container.ContainerTerminalScripting; + +import javax.annotation.Nullable; +import java.nio.file.Path; + +/** + * Packet for sending the syntax error of a script from server to client. + * @author rubensworks + * + */ +public class TerminalScriptingSyntaxErrorPacket extends PacketCodec { + + public static final Type ID = new Type<>(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "terminal_scripting_syntax_error")); + public static final StreamCodec CODEC = getCodec(TerminalScriptingSyntaxErrorPacket::new); + + @CodecField + private int disk; + @CodecField + private String path; + @CodecField + private String message; + @CodecField + private int line; + @CodecField + private int column; + + public TerminalScriptingSyntaxErrorPacket() { + super(ID); + } + + public TerminalScriptingSyntaxErrorPacket(int disk, Path path, @Nullable ScriptSyntaxError error) { + super(ID); + this.disk = disk; + this.path = path.toString(); + this.message = error == null ? "" : error.message(); + this.line = error == null ? 0 : error.line(); + this.column = error == null ? 0 : error.column(); + } + + @Override + public boolean isAsync() { + return false; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void actionClient(Level world, Player player) { + if(player.containerMenu instanceof ContainerTerminalScripting container) { + container.setSyntaxError(disk, Path.of(path), message.isEmpty() ? null : new ScriptSyntaxError(message, line, column)); + } + } + + @Override + public void actionServer(Level world, ServerPlayer player) { + + } +} diff --git a/src/main/java/org/cyclops/integratedscripting/proxy/CommonProxy.java b/src/main/java/org/cyclops/integratedscripting/proxy/CommonProxy.java index 826341979..9f579a612 100644 --- a/src/main/java/org/cyclops/integratedscripting/proxy/CommonProxy.java +++ b/src/main/java/org/cyclops/integratedscripting/proxy/CommonProxy.java @@ -7,6 +7,7 @@ import org.cyclops.integratedscripting.network.packet.TerminalScriptingCreateNewScriptPacket; import org.cyclops.integratedscripting.network.packet.TerminalScriptingDeleteScriptPacket; import org.cyclops.integratedscripting.network.packet.TerminalScriptingModifiedScriptPacket; +import org.cyclops.integratedscripting.network.packet.TerminalScriptingSyntaxErrorPacket; /** * Proxy for server and client side. @@ -27,5 +28,6 @@ public void registerPacketHandlers(PacketHandler packetHandler) { packetHandler.register(TerminalScriptingModifiedScriptPacket.ID, TerminalScriptingModifiedScriptPacket.CODEC); packetHandler.register(TerminalScriptingCreateNewScriptPacket.ID, TerminalScriptingCreateNewScriptPacket.CODEC); packetHandler.register(TerminalScriptingDeleteScriptPacket.ID, TerminalScriptingDeleteScriptPacket.CODEC); + packetHandler.register(TerminalScriptingSyntaxErrorPacket.ID, TerminalScriptingSyntaxErrorPacket.CODEC); } } diff --git a/src/main/resources/assets/integratedscripting/lang/en_us.json b/src/main/resources/assets/integratedscripting/lang/en_us.json index 2305d1397..56190e8aa 100644 --- a/src/main/resources/assets/integratedscripting/lang/en_us.json +++ b/src/main/resources/assets/integratedscripting/lang/en_us.json @@ -115,7 +115,7 @@ "info_book.integratedscripting.terminal.text4": "If the disk does not contain any script files yet, you can create them by clicking on the \"+\"-button on the bottom-left side of the screen.", "info_book.integratedscripting.terminal.text5": "The script file list on the left-hand side of the screen is used to select a script for editing. When clicking on a script, its editable contents will show up on the right-hand-side of the screen.", "info_book.integratedscripting.terminal.text6": "The text field on the right-hand side of the screen allows you to edit the contents of script files, which will be saved automatically after each modification.", - "info_book.integratedscripting.terminal.text7": "While the editor will perform limited syntax highlighting, it is not able to detect syntax errors at this time. Syntax errors will be reported when executing scripts via &lVariable Cards&r.", + "info_book.integratedscripting.terminal.text7": "The editor performs limited syntax highlighting, and marks the line of the first syntax error in red. Hover over that line to see the error message.", "info_book.integratedscripting.terminal.text8": "In order to use a script member as &lIntegrated Dynamics&r logic, you can bind it to a &lVariable Card&r.", "info_book.integratedscripting.terminal.text9": "You can do this by first selecting a script member in the text editor by selecting it with your cursor, such as a variable name or function name.", "info_book.integratedscripting.terminal.text10": "After that, you can insert a &lVariable Card&r into the slot on the right-hand side of the screen to bind it to this script member.", diff --git a/src/test/java/org/cyclops/integratedscripting/core/language/LanguageHandlerJavaScriptSyntaxTest.java b/src/test/java/org/cyclops/integratedscripting/core/language/LanguageHandlerJavaScriptSyntaxTest.java new file mode 100644 index 000000000..87fb24ef0 --- /dev/null +++ b/src/test/java/org/cyclops/integratedscripting/core/language/LanguageHandlerJavaScriptSyntaxTest.java @@ -0,0 +1,43 @@ +package org.cyclops.integratedscripting.core.language; + +import org.cyclops.integratedscripting.api.language.ScriptSyntaxError; +import org.junit.Test; + +import java.nio.file.Path; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +public class LanguageHandlerJavaScriptSyntaxTest { + + private static ScriptSyntaxError check(String script) { + return new LanguageHandlerJavaScript().checkSyntax(Path.of("script0.js"), script); + } + + @Test + public void testValid() { + assertThat(check("const a = 1;\nfunction f(b) { return a + b; }"), nullValue()); + } + + @Test + public void testValidDoesNotExecute() { + assertThat(check("throw new Error('runtime');"), nullValue()); + } + + @Test + public void testSyntaxError() { + ScriptSyntaxError error = check("const a = 1;\nconst = ;"); + assertThat(error.line(), equalTo(2)); + assertThat(error.column(), equalTo(7)); + assertTrue(error.message(), error.message().contains("SyntaxError")); + } + + @Test + public void testUnclosedBracket() { + ScriptSyntaxError error = check("function f() {\n return 1;\n"); + assertTrue(error.message(), error.message().contains("SyntaxError")); + } + +} From 4e41fbbb0923983107a4b1cdcba391366ec1b16e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 06:24:26 +0000 Subject: [PATCH 2/3] Make the syntax error marker easier to see Only the line number of the error line turned red, which is easy to miss at its small size, and nothing indicated an error that was scrolled out of view. Also give the gutter of that line a red background, and mark the error position on the scrollbar. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../gui/component/input/WidgetTextArea.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 ae1553ab9..9135c8c27 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 @@ -658,6 +658,15 @@ public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float } } + // Mark the gutter of the line with a syntax error + if (this.syntaxError != null && this.showLineNumbers) { + for (LineInfo line : lines) { + if (line.lineNumber + 1 == this.syntaxError.line()) { + guiGraphics.fill(this.getX(), line.y - 1, this.getX() + getLinesXOffset() - 1, line.y + ROW_HEIGHT - 1, Helpers.RGBAToInt(255, 160, 160, 255)); + } + } + } + // Draw lines int lastLineNumber = -1; for(LineInfo line : lines) { @@ -671,7 +680,7 @@ public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float this.getX(), line.y - offsetY + 2, 0.5f, - this.syntaxError != null && this.syntaxError.line() == line.lineNumber + 1 ? Helpers.RGBToInt(220, 0, 0) : (line.hasCursor ? 0 : Helpers.RGBToInt(120, 120, 120)), + this.syntaxError != null && this.syntaxError.line() == line.lineNumber + 1 ? Helpers.RGBToInt(170, 0, 0) : (line.hasCursor ? 0 : Helpers.RGBToInt(120, 120, 120)), false, Font.DisplayMode.NORMAL ); @@ -692,6 +701,14 @@ public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float // Render scrollbar if (this.scrollBar != null) { this.scrollBar.render(guiGraphics, getX(), getY(), partialTicks); + + // Mark the position of the syntax error on the scrollbar, so it can be found when scrolled out of view + if (this.syntaxError != null && displayCache.linesTotal > this.scrollBar.getVisibleRows()) { + int errorRow = findLineFromPos(displayCache.lineStarts, TextEditing.lineOffset(this.getValue(), this.syntaxError.line())); + int y = this.getY() + (int) ((long) errorRow * (this.getHeight() - 2) / displayCache.linesTotal); + int x = this.getX() + this.getWidth() - SCROLLBAR_WIDTH + 1; + guiGraphics.fill(x, y, x + SCROLLBAR_WIDTH - 2, y + 2, Helpers.RGBAToInt(220, 0, 0, 255)); + } } } From 6fc7d52543dbd1f341efbea8c397661836d3ed00 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 06:35:17 +0000 Subject: [PATCH 3/3] Align the syntax error gutter marker with the gutter borders The red background started one pixel right of the editor border and covered the gutter separator line. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../client/gui/component/input/WidgetTextArea.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 9135c8c27..5dc7c6834 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 @@ -662,7 +662,8 @@ public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float if (this.syntaxError != null && this.showLineNumbers) { for (LineInfo line : lines) { if (line.lineNumber + 1 == this.syntaxError.line()) { - guiGraphics.fill(this.getX(), line.y - 1, this.getX() + getLinesXOffset() - 1, line.y + ROW_HEIGHT - 1, Helpers.RGBAToInt(255, 160, 160, 255)); + // Fill the space between the editor border and the gutter separator + guiGraphics.fill(this.getX() - 1, line.y - 1, this.getX() + getLinesXOffset() - 2, line.y + ROW_HEIGHT - 1, Helpers.RGBAToInt(255, 160, 160, 255)); } } }