Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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();

}
Original file line number Diff line number Diff line change
@@ -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) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.cyclops.integratedscripting.client.key.Keys;
import org.lwjgl.glfw.GLFW;

Expand Down Expand Up @@ -107,6 +108,8 @@ public class WidgetTextArea extends EditBox implements GuiEventListener {
private String indent = " ";
@Nullable
private ShortcutListener 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);
Expand Down Expand Up @@ -208,6 +211,10 @@ public void selectRange(int start, int end) {
this.ensureCursorVisible();
}

public void setSyntaxError(@Nullable ScriptSyntaxError syntaxError) {
this.syntaxError = syntaxError;
}

/**
* @param indent The string that is inserted for one indentation level.
*/
Expand Down Expand Up @@ -747,6 +754,16 @@ 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()) {
// 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));
}
}
}

// Draw lines
int lastLineNumber = -1;
for(LineInfo line : lines) {
Expand All @@ -760,14 +777,18 @@ 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(170, 0, 0) : (line.hasCursor ? 0 : Helpers.RGBToInt(120, 120, 120)),
false,
Font.DisplayMode.NORMAL
);
}
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) {
Expand All @@ -777,6 +798,36 @@ 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));
}
}
}

private void renderSyntaxError(GuiGraphics guiGraphics, DisplayCache displayCache, List<LineInfo> 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));
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,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) -> {
Expand Down Expand Up @@ -544,6 +549,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()));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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_]+");

Expand All @@ -73,6 +80,14 @@ public class ContainerTerminalScripting extends InventoryContainer implements ID
private final int selectionId;
@Nullable
private BiConsumer<Integer, Path> remoteScriptChangeListener;
private final Map<Pair<Integer, Path>, ScriptSyntaxError> syntaxErrors = Maps.newHashMap();
@Nullable
private BiConsumer<Integer, Path> syntaxErrorListener;
@Nullable
private Triple<Integer, Path, String> syntaxCheckPending;
@Nullable
private Triple<Integer, Path, String> syntaxCheckDone;
private int syntaxCheckTicks;

public ContainerTerminalScripting(int id, Inventory playerInventory, RegistryFriendlyByteBuf packetBuffer) {
this(id, playerInventory, PartHelpers.readPartTarget(packetBuffer), Optional.empty(),
Expand Down Expand Up @@ -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<Integer, Path, String> 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<Integer, Path> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TerminalScriptingSyntaxErrorPacket> {

public static final Type<TerminalScriptingSyntaxErrorPacket> ID = new Type<>(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "terminal_scripting_syntax_error"));
public static final StreamCodec<RegistryFriendlyByteBuf, TerminalScriptingSyntaxErrorPacket> 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) {

}
}
Loading
Loading