Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public class GeneralConfig extends DummyConfig {
public static int terminalScriptingBaseConsumption = 1;
@ConfigurableProperty(category = "general", comment = "The minimum number of ticks inbetween sending a script change packet from client to server.", minimalValue = 0, configLocation = ModConfig.Type.SERVER)
public static int terminalScriptingClientSyncTickInterval = 20;
@ConfigurableProperty(category = "general", comment = "The number of spaces that tab inserts in the scripting terminal editor.", minimalValue = 1, maximalValue = 16, configLocation = ModConfig.Type.CLIENT)
public static int terminalScriptingIndentSize = 2;

@ConfigurableProperty(category = "general", comment = "If new processes can be created from guest languages. Only enable this on private servers and if you know what you are doing.", configLocation = ModConfig.Type.SERVER)
public static boolean graalAllowCreateProcess = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.cyclops.integratedscripting.client.gui.component.button;

import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.network.chat.Component;
import org.cyclops.cyclopscore.client.gui.component.button.ButtonExtended;
import org.cyclops.cyclopscore.client.gui.image.IImage;

/**
* A button with a centered icon, which is dimmed when the button is inactive.
* @author rubensworks
*/
public class ButtonIcon extends ButtonExtended {

private final IImage image;

public ButtonIcon(int x, int y, int width, int height, Component narrationMessage, OnPress onPress, IImage image) {
super(x, y, width, height, narrationMessage, onPress, true);
this.image = image;
}

@Override
protected void drawButtonInner(GuiGraphics guiGraphics, int mouseX, int mouseY) {
float color = this.active ? 1F : 0.4F;
this.image.drawWithColor(guiGraphics, this.getX() + (this.width - this.image.getWidth()) / 2,
this.getY() + (this.height - this.image.getHeight()) / 2, color, color, color, 1F);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
package org.cyclops.integratedscripting.client.gui.component.input;

/**
* Code editing operations on the state of a text area.
* @author rubensworks
*/
public class TextEditing {


private static int start(TextHistory.State state) {
return Math.min(state.cursorPos(), state.selectionPos());
}

private static int end(TextHistory.State state) {
return Math.max(state.cursorPos(), state.selectionPos());
}

/**
* @return The end of the selection, excluding a line on which the selection ends at the start.
*/
private static int selectionEnd(TextHistory.State state) {
int end = end(state);
return end > start(state) && state.value().charAt(end - 1) == '\n' ? end - 1 : end;
}

public static int lineStart(String value, int pos) {
return value.lastIndexOf('\n', pos - 1) + 1;
}

public static int lineEnd(String value, int pos) {
int i = value.indexOf('\n', pos);
return i < 0 ? value.length() : i;
}

private static String leadingWhitespace(String value, int lineStart) {
int i = lineStart;
while (i < value.length() && (value.charAt(i) == ' ' || value.charAt(i) == '\t')) {
i++;
}
return value.substring(lineStart, i);
}

private static boolean isMultiLineSelection(TextHistory.State state) {
return state.value().substring(start(state), end(state)).indexOf('\n') >= 0;
}

/**
* Indent the selected lines, or insert an indent at the cursor if no lines are selected.
* @param indent The string of one indentation level.
*/
public static TextHistory.State indent(TextHistory.State state, String indent) {
String value = state.value();
if (!isMultiLineSelection(state)) {
int start = start(state);
String newValue = value.substring(0, start) + indent + value.substring(end(state));
return new TextHistory.State(newValue, start + indent.length(), start + indent.length());
}
return mapLines(state, line -> indent + line);
}

/**
* Remove one indent from the start of the lines touched by the cursor or selection.
* @param indent The string of one indentation level.
*/
public static TextHistory.State unindent(TextHistory.State state, String indent) {
return mapLines(state, line -> {
int remove = 0;
while (remove < indent.length() && remove < line.length() && line.charAt(remove) == ' ') {
remove++;
}
if (remove == 0 && !line.isEmpty() && line.charAt(0) == '\t') {
remove = 1;
}
return line.substring(remove);
});
}

/**
* Apply a function to each line touched by the cursor or selection,
* keeping the cursor and selection on the same characters.
*/
private static TextHistory.State mapLines(TextHistory.State state, java.util.function.UnaryOperator<String> mapper) {
String value = state.value();
int from = lineStart(value, start(state));
int to = lineEnd(value, selectionEnd(state));
String[] lines = value.substring(from, to).split("\n", -1);
String[] newLines = new String[lines.length];
for (int i = 0; i < lines.length; i++) {
newLines[i] = mapper.apply(lines[i]);
}
String newValue = value.substring(0, from) + String.join("\n", newLines) + value.substring(to);
return new TextHistory.State(newValue,
mapPosition(state.cursorPos(), from, lines, newLines),
mapPosition(state.selectionPos(), from, lines, newLines));
}

/**
* Map a position from before to after changing lines, where each line only changed at its start.
*/
private static int mapPosition(int pos, int from, String[] lines, String[] newLines) {
if (pos < from) {
return pos;
}
int lineStart = from;
int delta = 0;
for (int i = 0; i < lines.length; i++) {
int lineDelta = newLines[i].length() - lines[i].length();
int lineEnd = lineStart + lines[i].length();
if (pos <= lineEnd || i == lines.length - 1) {
// Positions inside a removed prefix move to the start of the line
return Math.max(lineStart + delta, pos + delta + lineDelta);
}
delta += lineDelta;
lineStart = lineEnd + 1;
}
return pos + delta;
}

/**
* Insert a newline that keeps the indentation of the current line,
* and indents one more level after an opening bracket.
* @param indentUnit The string of one indentation level.
*/
public static TextHistory.State newline(TextHistory.State state, String indentUnit) {
String value = state.value();
int start = start(state);
int end = end(state);
int lineStart = lineStart(value, start);
String indent = leadingWhitespace(value, lineStart);
indent = indent.substring(0, Math.min(indent.length(), start - lineStart));
String before = value.substring(lineStart, start).stripTrailing();
char open = before.isEmpty() ? 0 : before.charAt(before.length() - 1);
boolean opens = open == '{' || open == '(' || open == '[';
String insert = "\n" + indent + (opens ? indentUnit : "");
int cursor = start + insert.length();
// Put a directly following closing bracket on its own line
if (opens && end < value.length() && value.charAt(end) == closing(open)) {
insert += "\n" + indent;
}
String newValue = value.substring(0, start) + insert + value.substring(end);
return new TextHistory.State(newValue, cursor, cursor);
}

private static char closing(char open) {
return switch (open) {
case '{' -> '}';
case '(' -> ')';
case '[' -> ']';
default -> 0;
};
}

private static char opening(char close) {
return switch (close) {
case '}' -> '{';
case ')' -> '(';
case ']' -> '[';
default -> 0;
};
}

/**
* Find the bracket matching the one right after or right before the cursor.
* @return A pair of bracket positions, or null if there is no matching bracket.
*/
public static int[] findMatchingBrackets(String value, int cursorPos) {
for (int pos : new int[]{cursorPos, cursorPos - 1}) {
if (pos < 0 || pos >= value.length()) {
continue;
}
char c = value.charAt(pos);
if (closing(c) != 0) {
int match = scan(value, pos, c, closing(c), 1);
return match < 0 ? null : new int[]{pos, match};
} else if (opening(c) != 0) {
int match = scan(value, pos, c, opening(c), -1);
return match < 0 ? null : new int[]{match, pos};
}
}
return null;
}

private static int scan(String value, int from, char self, char other, int step) {
int depth = 0;
for (int i = from; i >= 0 && i < value.length(); i += step) {
char c = value.charAt(i);
if (c == self) {
depth++;
} else if (c == other && --depth == 0) {
return i;
}
}
return -1;
}

/**
* Find the next case-insensitive occurrence of a query, wrapping around at the end.
* @return The start position of the occurrence, or -1 if there is none.
*/
public static int findNext(String value, String query, int from) {
if (query.isEmpty()) {
return -1;
}
String haystack = value.toLowerCase(java.util.Locale.ROOT);
String needle = query.toLowerCase(java.util.Locale.ROOT);
int i = haystack.indexOf(needle, Math.max(0, Math.min(from, haystack.length())));
return i >= 0 ? i : haystack.indexOf(needle);
}

/**
* Find all non-overlapping case-insensitive occurrences of a query.
* @return The start positions of all occurrences.
*/
public static int[] findAll(String value, String query) {
if (query.isEmpty()) {
return new int[0];
}
String haystack = value.toLowerCase(java.util.Locale.ROOT);
String needle = query.toLowerCase(java.util.Locale.ROOT);
it.unimi.dsi.fastutil.ints.IntList positions = new it.unimi.dsi.fastutil.ints.IntArrayList();
int i = haystack.indexOf(needle);
while (i >= 0) {
positions.add(i);
i = haystack.indexOf(needle, i + needle.length());
}
return positions.toIntArray();
}

/**
* Find the last case-insensitive occurrence of a query that starts before a position, wrapping around at the start.
* @return The start position of the occurrence, or -1 if there is none.
*/
public static int findPrevious(String value, String query, int before) {
int[] positions = findAll(value, query);
if (positions.length == 0) {
return -1;
}
for (int i = positions.length - 1; i >= 0; i--) {
if (positions[i] < before) {
return positions[i];
}
}
return positions[positions.length - 1];
}

/**
* @param line A 1-based line number, which is clamped to the existing lines.
* @return The position of the start of the line.
*/
public static int lineOffset(String value, int line) {
int pos = 0;
for (int i = 1; i < line; i++) {
int next = value.indexOf('\n', pos);
if (next < 0) {
break;
}
pos = next + 1;
}
return pos;
}

/**
* Comment out the lines touched by the cursor or selection, or uncomment them if they are all commented out.
*/
public static TextHistory.State toggleComment(TextHistory.State state) {
String value = state.value();
int from = lineStart(value, start(state));
int to = lineEnd(value, selectionEnd(state));
boolean allCommented = true;
for (String line : value.substring(from, to).split("\n", -1)) {
if (!line.isBlank() && !line.startsWith("//")) {
allCommented = false;
}
}
boolean uncomment = allCommented;
return mapLines(state, line -> uncomment ? (line.startsWith("//") ? line.substring(2) : line) : "//" + line);
}

/**
* Move the lines touched by the cursor or selection one line up or down.
* @param direction -1 for up, 1 for down.
*/
public static TextHistory.State moveLines(TextHistory.State state, int direction) {
String value = state.value();
int from = lineStart(value, start(state));
int to = lineEnd(value, selectionEnd(state));
if (direction < 0) {
if (from == 0) {
return state;
}
int otherFrom = lineStart(value, from - 1);
String other = value.substring(otherFrom, from - 1);
String newValue = value.substring(0, otherFrom) + value.substring(from, to) + "\n" + other + value.substring(to);
int shift = -(other.length() + 1);
return new TextHistory.State(newValue, state.cursorPos() + shift, state.selectionPos() + shift);
} else {
if (to == value.length()) {
return state;
}
int otherTo = lineEnd(value, to + 1);
String other = value.substring(to + 1, otherTo);
String newValue = value.substring(0, from) + other + "\n" + value.substring(from, to) + value.substring(otherTo);
int shift = other.length() + 1;
return new TextHistory.State(newValue, state.cursorPos() + shift, state.selectionPos() + shift);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ public State redo(State current) {
return this.redoStack.pop();
}

public boolean canUndo() {
return !this.undoStack.isEmpty();
}

public boolean canRedo() {
return !this.redoStack.isEmpty();
}

public static record State(String value, int cursorPos, int selectionPos) {}

}
Loading
Loading