Jkdsjk
Some checks failed
build / build (push) Failing after 1m31s

This commit is contained in:
2026-07-31 14:42:27 +02:00
parent aa8fa05506
commit 9226c1cd87
14 changed files with 1237 additions and 92 deletions

View File

@@ -0,0 +1,38 @@
package com.aranroig.client.editor;
/**
* A single autocomplete suggestion.
*
* @param label The text shown in the dropdown (e.g. "System.out.println").
* @param insert The text actually inserted when accepted. Usually equals label,
* but can differ (e.g. snippets with a trailing "(" etc.).
* @param detail Short right-aligned annotation shown in the dropdown (e.g. "method", "keyword").
* @param kind Category used for the icon/colour badge.
*/
public record CompletionItem(String label, String insert, String detail, Kind kind) {
public enum Kind {
KEYWORD, // language keywords
TYPE, // class / interface / enum names
METHOD, // method / function
FIELD, // field / variable
SNIPPET, // multi-character template
}
/** Convenience constructor when label == insert and no detail is needed. */
public static CompletionItem keyword(String word) {
return new CompletionItem(word, word, "keyword", Kind.KEYWORD);
}
public static CompletionItem type(String name) {
return new CompletionItem(name, name, "type", Kind.TYPE);
}
public static CompletionItem method(String label, String insert) {
return new CompletionItem(label, insert, "method", Kind.METHOD);
}
public static CompletionItem snippet(String label, String insert, String detail) {
return new CompletionItem(label, insert, detail, Kind.SNIPPET);
}
}

View File

@@ -0,0 +1,336 @@
package com.aranroig.client.editor;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Generates autocomplete suggestions for Lua code given the current text
* buffer and cursor position. The provider combines:
* <ol>
* <li>A static list of Lua keywords, built-in globals, and standard library members.</li>
* <li>Words already present in the buffer (user-defined functions/variables).</li>
* <li>Library member completion after a "." or ":" trigger.</li>
* </ol>
*
* None of this requires a real LSP it is "good-enough" IDE-lite completion
* that dramatically improves the editing experience without external dependencies.
*/
public class CompletionProvider {
// ── Static vocabulary ─────────────────────────────────────────────────
private static final List<CompletionItem> STATIC_ITEMS;
// Maps a library name (after the dot/colon) to its known members
private static final Map<String, List<CompletionItem>> LIBRARY_MEMBERS;
static {
List<CompletionItem> items = new ArrayList<>();
// ── Lua keywords ─────────────────────────────────────────────────
for (String kw : new String[]{
"and", "break", "do", "else", "elseif", "end",
"false", "for", "function", "goto", "if", "in",
"local", "nil", "not", "or", "repeat", "return",
"then", "true", "until", "while"
}) items.add(CompletionItem.keyword(kw));
// ── Built-in global functions ────────────────────────────────────
for (String fn : new String[]{
"assert", "collectgarbage", "dofile", "error", "getmetatable",
"ipairs", "load", "loadfile", "next", "pairs",
"pcall", "print", "rawequal", "rawget", "rawlen", "rawset",
"require", "select", "setmetatable", "tonumber", "tostring",
"type", "unpack", "xpcall"
}) items.add(CompletionItem.method(fn + "(", fn + "("));
// ── Built-in global tables (as types) ────────────────────────────
for (String lib : new String[]{
"string", "table", "math", "io", "os", "coroutine",
"package", "utf8", "debug", "_G", "_VERSION"
}) items.add(CompletionItem.type(lib));
// ── Common snippets ───────────────────────────────────────────────
items.add(CompletionItem.snippet("fun",
"function ()\n \nend",
"anonymous function"));
items.add(CompletionItem.snippet("function",
"function name()\n \nend",
"named function"));
items.add(CompletionItem.snippet("lfun",
"local function name()\n \nend",
"local function"));
items.add(CompletionItem.snippet("if",
"if then\n \nend",
"if block"));
items.add(CompletionItem.snippet("ifel",
"if then\n \nelse\n \nend",
"if / else"));
items.add(CompletionItem.snippet("ifelseif",
"if then\n \nelseif then\n \nelse\n \nend",
"if / elseif / else"));
items.add(CompletionItem.snippet("for",
"for i = 1, do\n \nend",
"numeric for"));
items.add(CompletionItem.snippet("forin",
"for k, v in pairs() do\n \nend",
"generic for (pairs)"));
items.add(CompletionItem.snippet("fori",
"for i, v in ipairs() do\n \nend",
"generic for (ipairs)"));
items.add(CompletionItem.snippet("while",
"while do\n \nend",
"while loop"));
items.add(CompletionItem.snippet("repeat",
"repeat\n \nuntil ",
"repeat / until"));
items.add(CompletionItem.snippet("pcall",
"local ok, err = pcall(function()\n \nend)",
"protected call"));
items.add(CompletionItem.snippet("class",
"local ClassName = {}\nClassName.__index = ClassName\n\nfunction ClassName.new()\n local self = setmetatable({}, ClassName)\n return self\nend\n\nreturn ClassName",
"OOP class template"));
items.add(CompletionItem.snippet("require",
"local = require(\"\")",
"require module"));
items.add(CompletionItem.snippet("print",
"print()",
"print(…)"));
items.add(CompletionItem.snippet("local",
"local = ",
"local variable"));
items.add(CompletionItem.snippet("return",
"return ",
"return statement"));
STATIC_ITEMS = Collections.unmodifiableList(items);
// ── Library member tables ─────────────────────────────────────────
Map<String, List<CompletionItem>> libs = new LinkedHashMap<>();
// string.*
List<CompletionItem> stringLib = new ArrayList<>();
for (String[] m : new String[][]{
{"byte", "byte(", "string.byte(s [,i [,j]])"},
{"char", "char(", "string.char(…)"},
{"dump", "dump(", "string.dump(func)"},
{"find", "find(", "string.find(s, pattern [,init])"},
{"format", "format(", "string.format(fmt, …)"},
{"gmatch", "gmatch(", "string.gmatch(s, pattern)"},
{"gsub", "gsub(", "string.gsub(s, pattern, repl)"},
{"len", "len(", "string.len(s)"},
{"lower", "lower(", "string.lower(s)"},
{"match", "match(", "string.match(s, pattern [,init])"},
{"rep", "rep(", "string.rep(s, n [,sep])"},
{"reverse", "reverse(", "string.reverse(s)"},
{"sub", "sub(", "string.sub(s, i [,j])"},
{"upper", "upper(", "string.upper(s)"},
}) stringLib.add(CompletionItem.method(m[0] + "(", m[1]));
libs.put("string", stringLib);
// table.*
List<CompletionItem> tableLib = new ArrayList<>();
for (String[] m : new String[][]{
{"concat", "concat(", "table.concat(t [,sep [,i [,j]]])"},
{"insert", "insert(", "table.insert(t [,pos], value)"},
{"move", "move(", "table.move(a1, f, e, t [,a2])"},
{"pack", "pack(", "table.pack(…)"},
{"remove", "remove(", "table.remove(t [,pos])"},
{"sort", "sort(", "table.sort(t [,comp])"},
{"unpack", "unpack(", "table.unpack(t [,i [,j]])"},
}) tableLib.add(CompletionItem.method(m[0] + "(", m[1]));
libs.put("table", tableLib);
// math.*
List<CompletionItem> mathLib = new ArrayList<>();
for (String[] m : new String[][]{
{"abs", "abs(", "math.abs(x)"},
{"ceil", "ceil(", "math.ceil(x)"},
{"cos", "cos(", "math.cos(x)"},
{"exp", "exp(", "math.exp(x)"},
{"floor", "floor(", "math.floor(x)"},
{"fmod", "fmod(", "math.fmod(x, y)"},
{"log", "log(", "math.log(x [,base])"},
{"max", "max(", "math.max(x, …)"},
{"min", "min(", "math.min(x, …)"},
{"modf", "modf(", "math.modf(x)"},
{"pow", "pow(", "math.pow(x, y)"},
{"random", "random(", "math.random([m [,n]])"},
{"randomseed", "randomseed(", "math.randomseed(x)"},
{"sin", "sin(", "math.sin(x)"},
{"sqrt", "sqrt(", "math.sqrt(x)"},
{"tan", "tan(", "math.tan(x)"},
{"tointeger", "tointeger(", "math.tointeger(x)"},
{"type", "type(", "math.type(x)"},
{"huge", "huge", "math.huge (∞)"},
{"pi", "pi", "math.pi"},
{"maxinteger", "maxinteger", "math.maxinteger"},
{"mininteger", "mininteger", "math.mininteger"},
}) mathLib.add(CompletionItem.method(m[0], m[1]));
libs.put("math", mathLib);
// io.*
List<CompletionItem> ioLib = new ArrayList<>();
for (String[] m : new String[][]{
{"close", "close(", "io.close([file])"},
{"flush", "flush()", "io.flush()"},
{"input", "input(", "io.input([file])"},
{"lines", "lines(", "io.lines([filename])"},
{"open", "open(", "io.open(filename [,mode])"},
{"output", "output(", "io.output([file])"},
{"popen", "popen(", "io.popen(prog [,mode])"},
{"read", "read(", "io.read(…)"},
{"tmpfile", "tmpfile()", "io.tmpfile()"},
{"type", "type(", "io.type(obj)"},
{"write", "write(", "io.write(…)"},
}) ioLib.add(CompletionItem.method(m[0], m[1]));
libs.put("io", ioLib);
// os.*
List<CompletionItem> osLib = new ArrayList<>();
for (String[] m : new String[][]{
{"clock", "clock()", "os.clock()"},
{"date", "date(", "os.date([format [,time]])"},
{"difftime", "difftime(", "os.difftime(t2, t1)"},
{"execute", "execute(", "os.execute([command])"},
{"exit", "exit(", "os.exit([code])"},
{"getenv", "getenv(", "os.getenv(varname)"},
{"remove", "remove(", "os.remove(filename)"},
{"rename", "rename(", "os.rename(oldname, newname)"},
{"setlocale","setlocale(","os.setlocale(locale [,category])"},
{"time", "time(", "os.time([table])"},
{"tmpname", "tmpname()", "os.tmpname()"},
}) osLib.add(CompletionItem.method(m[0], m[1]));
libs.put("os", osLib);
// coroutine.*
List<CompletionItem> coLib = new ArrayList<>();
for (String[] m : new String[][]{
{"create", "create(", "coroutine.create(f)"},
{"isyieldable", "isyieldable()", "coroutine.isyieldable()"},
{"resume", "resume(", "coroutine.resume(co [,…])"},
{"running", "running()", "coroutine.running()"},
{"status", "status(", "coroutine.status(co)"},
{"wrap", "wrap(", "coroutine.wrap(f)"},
{"yield", "yield(", "coroutine.yield(…)"},
}) coLib.add(CompletionItem.method(m[0], m[1]));
libs.put("coroutine", coLib);
LIBRARY_MEMBERS = Collections.unmodifiableMap(libs);
}
// Lua identifier chars (no $ unlike Java)
private static final Pattern WORD_PATTERN = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
// ── Public API ─────────────────────────────────────────────────────────
/**
* Returns up to {@code maxResults} completion items for the word currently
* being typed at {@code cursorPos} in {@code buffer}.
*
* @return empty list when no prefix can be extracted or nothing matches.
*/
public List<CompletionItem> getCompletions(String buffer, int cursorPos, int maxResults) {
String prefix = extractPrefix(buffer, cursorPos);
if (prefix.isEmpty()) return Collections.emptyList();
int prefixStart = cursorPos - prefix.length();
String libraryName = resolveLibraryName(buffer, prefixStart);
boolean afterDotOrColon = (libraryName != null);
List<CompletionItem> results = new ArrayList<>();
Set<String> seen = new LinkedHashSet<>();
if (afterDotOrColon && libraryName != null) {
// Member completion: only show items from the known library (if recognised)
List<CompletionItem> members = LIBRARY_MEMBERS.get(libraryName);
if (members != null) {
for (CompletionItem item : members) {
if (item.label().toLowerCase().startsWith(prefix.toLowerCase())) {
if (seen.add(item.label())) results.add(item);
}
}
}
// Also fall through to buffer words so unknown table members appear
Matcher m = WORD_PATTERN.matcher(buffer);
while (m.find()) {
String word = m.group();
if (word.length() > 1
&& word.toLowerCase().startsWith(prefix.toLowerCase())
&& !word.equals(prefix)
&& seen.add(word)) {
results.add(new CompletionItem(word, word, "identifier",
CompletionItem.Kind.FIELD));
}
}
} else {
// Top-level completion: keywords + globals + snippets
for (CompletionItem item : STATIC_ITEMS) {
if (item.label().toLowerCase().startsWith(prefix.toLowerCase())) {
if (seen.add(item.label())) results.add(item);
}
}
// Words from the buffer (user-defined names)
Matcher m = WORD_PATTERN.matcher(buffer);
while (m.find()) {
String word = m.group();
if (word.length() > 2
&& word.toLowerCase().startsWith(prefix.toLowerCase())
&& !word.equals(prefix)
&& seen.add(word)) {
results.add(new CompletionItem(word, word, "identifier",
CompletionItem.Kind.FIELD));
}
}
}
// Sort: exact case match first, then by kind, then alphabetical
results.sort(Comparator
.comparingInt((CompletionItem c) -> c.label().startsWith(prefix) ? 0 : 1)
.thenComparingInt(c -> c.kind().ordinal())
.thenComparing(CompletionItem::label));
return results.subList(0, Math.min(results.size(), maxResults));
}
/**
* Extracts the Lua identifier fragment immediately to the left of the cursor.
* Only the segment after the last dot or colon is returned.
* E.g. "math.sq|rt" → "sq"
*/
public String extractPrefix(String buffer, int cursorPos) {
int end = Math.min(cursorPos, buffer.length());
int start = end;
while (start > 0) {
char c = buffer.charAt(start - 1);
if (Character.isLetterOrDigit(c) || c == '_') start--;
else break;
}
return buffer.substring(start, end);
}
/**
* If the token immediately before the prefix (skipping whitespace) is a '.'
* or ':', returns the identifier that precedes that separator — this is the
* library/table name used for member completion.
* Returns {@code null} when the cursor is not after a dot or colon.
*
* E.g. "math.sq" → "math", "str:fi" → "str", "print" → null
*/
private String resolveLibraryName(String buffer, int prefixStart) {
if (prefixStart <= 0) return null;
int i = prefixStart - 1;
// skip spaces
while (i > 0 && buffer.charAt(i) == ' ') i--;
char sep = buffer.charAt(i);
if (sep != '.' && sep != ':') return null;
// walk back over the library name
int nameEnd = i;
i--;
while (i >= 0 && (Character.isLetterOrDigit(buffer.charAt(i)) || buffer.charAt(i) == '_')) i--;
if (i + 1 >= nameEnd) return null; // nothing before the separator
return buffer.substring(i + 1, nameEnd);
}
}

View File

@@ -1,5 +1,6 @@
package com.aranroig.client.editor; package com.aranroig.client.editor;
import com.aranroig.editor.EditorFile;
import net.minecraft.client.gui.Font; import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.input.CharacterEvent; import net.minecraft.client.input.CharacterEvent;
@@ -21,25 +22,10 @@ public class EditorFileTree extends Widget {
public void draw(@NonNull GuiGraphicsExtractor graphics, int mouseX, int mouseY, double delta) { public void draw(@NonNull GuiGraphicsExtractor graphics, int mouseX, int mouseY, double delta) {
graphics.fill(getStartX(), getStartY(), getEndX(), getEndY(), COLOR_BACKGROUND); graphics.fill(getStartX(), getStartY(), getEndX(), getEndY(), COLOR_BACKGROUND);
// Line numbers + text
/*
for (int i = firstLine; i <= lastLine; i++) {
int screenY = start_y + i * lineH - scrollY;
// Line number — right-aligned, not scrolled horizontally
String lineNum = String.valueOf(i + 1);
int numW = font.width(lineNum);
graphics.text(font, lineNum,
gutterDivX() - 4 - numW,
screenY, COLOR_GUTTER_TEXT, false);
// Code text — offset by scrollX
graphics.text(font, lines[i], textAreaX() - scrollX, screenY, COLOR_TEXT, true);
}
*/
Font font = editorScreen.getFont(); Font font = editorScreen.getFont();
int lineH = font.lineHeight; int lineH = font.lineHeight;
List<com.aranroig.editor.EditorFile> files = editorManager.getFiles(); List<EditorFile> files = editorManager.getFiles();
for(int i = 0; i < files.size(); i++) { for(int i = 0; i < files.size(); i++) {
graphics.text(font, files.get(i).getName(), getStartX(), getStartY() + i * lineH, COLOR_TEXT, true); graphics.text(font, files.get(i).getName(), getStartX(), getStartY() + i * lineH, COLOR_TEXT, true);
} }

View File

@@ -11,6 +11,9 @@ import net.minecraft.client.input.KeyEvent;
import net.minecraft.client.input.MouseButtonEvent; import net.minecraft.client.input.MouseButtonEvent;
import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NonNull;
import java.util.Collections;
import java.util.List;
public class EditorFileView extends Widget { public class EditorFileView extends Widget {
private StringBuilder text = new StringBuilder(); private StringBuilder text = new StringBuilder();
@@ -27,23 +30,16 @@ public class EditorFileView extends Widget {
} }
// ── Scroll state ────────────────────────────────────────────────────── // ── Scroll state ──────────────────────────────────────────────────────
private int scrollX = 0; // horizontal scroll in pixels private int scrollX = 0;
private int scrollY = 0; // vertical scroll in pixels private int scrollY = 0;
/** How many pixels to scroll per mouse-wheel notch (vertically). */ private static final int SCROLL_SPEED = 3;
private static final int SCROLL_SPEED = 3; // lines per notch
/** Width reserved for line numbers + a small right-padding. */
private static final int GUTTER_WIDTH = 30; private static final int GUTTER_WIDTH = 30;
private static final int GUTTER_MARGIN = 5; private static final int GUTTER_MARGIN = 5;
/** Where actual text content starts (after the gutter). */
private int gutterDivX() { return getStartX() + GUTTER_WIDTH; } private int gutterDivX() { return getStartX() + GUTTER_WIDTH; }
private int textAreaX() { return getStartX() + GUTTER_WIDTH + GUTTER_MARGIN; } private int textAreaX() { return getStartX() + GUTTER_WIDTH + GUTTER_MARGIN; }
/** Visible width available for text (excludes gutter). */
private int textAreaWidth() { return getEndX() - textAreaX(); } private int textAreaWidth() { return getEndX() - textAreaX(); }
/** Visible height available for text. */
private int textAreaHeight() { return getEndY() - getStartY(); } private int textAreaHeight() { return getEndY() - getStartY(); }
// ── Colors ──────────────────────────────────────────────────────────── // ── Colors ────────────────────────────────────────────────────────────
@@ -54,6 +50,47 @@ public class EditorFileView extends Widget {
private static final int COLOR_GUTTER_TEXT = 0xFF888888; private static final int COLOR_GUTTER_TEXT = 0xFF888888;
private static final int COLOR_GUTTER_DIV = 0xFF444444; private static final int COLOR_GUTTER_DIV = 0xFF444444;
// ── Autocomplete state ────────────────────────────────────────────────
private final CompletionProvider completionProvider = new CompletionProvider();
private final LuaHighlighter luaHighlighter = new LuaHighlighter();
/**
* Per-line multiline-string continuation state, rebuilt whenever the text
* changes. Index {@code i} is {@code true} when line {@code i} is inside a
* {@code [[…]]} block that started before that line.
* Lazily rebuilt in {@link #rebuildHighlightState()}.
*/
private boolean[] multilineState = new boolean[0];
private int highlightVersion = -1; // bumped on every text edit
/** Current list of suggestions (empty = popup hidden). */
private List<CompletionItem> completions = Collections.emptyList();
/** Index of the highlighted item inside {@link #completions}. */
private int completionIndex = 0;
/** Maximum number of suggestions shown at once. */
private static final int MAX_COMPLETIONS = 8;
// Popup colours — dark, VSCode-ish
private static final int COLOR_POPUP_BG = 0xFF1E1E1E;
private static final int COLOR_POPUP_BORDER = 0xFF666666;
private static final int COLOR_POPUP_SELECTED = 0xFF383868;
private static final int COLOR_POPUP_TEXT = 0xFFD4D4D4;
private static final int COLOR_POPUP_DETAIL = 0xFF888888;
private static final int COLOR_KIND_KEYWORD = 0xFF569CD6;
private static final int COLOR_KIND_TYPE = 0xFF4EC9B0;
private static final int COLOR_KIND_METHOD = 0xFFDCDCAA;
private static final int COLOR_KIND_FIELD = 0xFF9CDCFE;
private static final int COLOR_KIND_SNIPPET = 0xFFCE9178;
private static final int POPUP_ITEM_HEIGHT = 12;
private static final int POPUP_PADDING = 4;
private static final int POPUP_WIDTH = 240;
private static final int KIND_BADGE_WIDTH = 4; // coloured left strip
// ───────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────
private String[] getLines() { private String[] getLines() {
@@ -64,22 +101,16 @@ public class EditorFileView extends Widget {
return selectionAnchor >= 0 && selectionAnchor != cursorPos; return selectionAnchor >= 0 && selectionAnchor != cursorPos;
} }
/** Replaces \t with 4 spaces so font.width() measures correctly. */
private String sanitize(String input) { private String sanitize(String input) {
return input.replace("\t", " "); return input.replace("\t", " ");
} }
/**
* Returns the rendered pixel width of a prefix of a raw line up to
* character index {@code charCount}, expanding tabs properly.
*/
private int widthUpTo(String rawLine, int charCount) { private int widthUpTo(String rawLine, int charCount) {
return font.width(sanitize(rawLine.substring(0, charCount))); return font.width(sanitize(rawLine.substring(0, charCount)));
} }
// ── Scroll helpers ──────────────────────────────────────────────────── // ── Scroll helpers ────────────────────────────────────────────────────
/** Maximum horizontal scroll so the widest line end is just visible. */
private int maxScrollX() { private int maxScrollX() {
if (font == null) return 0; if (font == null) return 0;
int maxW = 0; int maxW = 0;
@@ -87,7 +118,6 @@ public class EditorFileView extends Widget {
return Math.max(0, maxW - textAreaWidth()); return Math.max(0, maxW - textAreaWidth());
} }
/** Maximum vertical scroll so the last line is just visible. */
private int maxScrollY() { private int maxScrollY() {
if (font == null) return 0; if (font == null) return 0;
int totalH = getLines().length * font.lineHeight; int totalH = getLines().length * font.lineHeight;
@@ -99,17 +129,12 @@ public class EditorFileView extends Widget {
scrollY = Math.max(0, Math.min(scrollY, maxScrollY())); scrollY = Math.max(0, Math.min(scrollY, maxScrollY()));
} }
/**
* Scrolls the view so the cursor is always visible.
* Call after any cursor movement.
*/
private void scrollToCursor() { private void scrollToCursor() {
if (font == null) return; if (font == null) return;
int[] lc = offsetToLineCol(cursorPos); int[] lc = offsetToLineCol(cursorPos);
String[] lines = getLines(); String[] lines = getLines();
String lineText = (lc[0] < lines.length) ? lines[lc[0]] : ""; String lineText = (lc[0] < lines.length) ? lines[lc[0]] : "";
// Vertical
int cursorTop = lc[0] * font.lineHeight; int cursorTop = lc[0] * font.lineHeight;
int cursorBottom = cursorTop + font.lineHeight; int cursorBottom = cursorTop + font.lineHeight;
if (cursorTop < scrollY) if (cursorTop < scrollY)
@@ -117,7 +142,6 @@ public class EditorFileView extends Widget {
else if (cursorBottom > scrollY + textAreaHeight()) else if (cursorBottom > scrollY + textAreaHeight())
scrollY = cursorBottom - textAreaHeight(); scrollY = cursorBottom - textAreaHeight();
// Horizontal — measure sanitized prefix so tabs expand correctly
int cursorPx = widthUpTo(lineText, Math.min(lc[1], lineText.length())); int cursorPx = widthUpTo(lineText, Math.min(lc[1], lineText.length()));
if (cursorPx < scrollX) if (cursorPx < scrollX)
scrollX = cursorPx; scrollX = cursorPx;
@@ -148,16 +172,10 @@ public class EditorFileView extends Widget {
return offset + targetCol; return offset + targetCol;
} }
/**
* Maps a pixel coordinate to a text offset.
* Accounts for the gutter AND current scroll position.
* Uses sanitized widths so tab expansion is reflected in click targets.
*/
private int pixelToOffset(double mouseX, double mouseY) { private int pixelToOffset(double mouseX, double mouseY) {
String[] lines = getLines(); String[] lines = getLines();
int lineH = font.lineHeight; int lineH = font.lineHeight;
// Apply scroll to convert screen coords → content coords
double contentY = mouseY - getStartY() + scrollY; double contentY = mouseY - getStartY() + scrollY;
double contentX = mouseX - textAreaX() + scrollX; double contentX = mouseX - textAreaX() + scrollX;
@@ -168,7 +186,6 @@ public class EditorFileView extends Widget {
String sanitized = sanitize(lineText); String sanitized = sanitize(lineText);
int col = 0; int col = 0;
for (int i = 0; i < lineText.length(); i++) { for (int i = 0; i < lineText.length(); i++) {
// Measure using sanitized prefix widths so tabs count as 4 spaces
float charW = font.width(sanitized.substring(i, i + 1)); float charW = font.width(sanitized.substring(i, i + 1));
float midPoint = font.width(sanitized.substring(0, i)) + charW / 2f; float midPoint = font.width(sanitized.substring(0, i)) + charW / 2f;
if (contentX < midPoint) { col = i; break; } if (contentX < midPoint) { col = i; break; }
@@ -186,6 +203,130 @@ public class EditorFileView extends Widget {
selectionAnchor = -1; selectionAnchor = -1;
} }
// ── Autocomplete helpers ──────────────────────────────────────────────
/** Refresh the completion list after every text or cursor change. */
private void updateCompletions() {
completions = completionProvider.getCompletions(text.toString(), cursorPos, MAX_COMPLETIONS);
completionIndex = 0;
}
/** Hide the popup without clearing the suggestion list. */
private void dismissCompletions() {
completions = Collections.emptyList();
}
private boolean isPopupVisible() {
return !completions.isEmpty();
}
/**
* Accept the currently-highlighted suggestion: replace the typed prefix
* with the completion's insertion text.
*/
private void acceptCompletion() {
if (!isPopupVisible()) return;
CompletionItem item = completions.get(completionIndex);
String prefix = completionProvider.extractPrefix(text.toString(), cursorPos);
int prefixStart = cursorPos - prefix.length();
text.replace(prefixStart, cursorPos, item.insert());
cursorPos = prefixStart + item.insert().length();
selectionAnchor = -1;
dismissCompletions();
scrollToCursor();
}
/**
* Pixel coordinates of the top-left corner of the autocomplete popup,
* anchored to the current cursor position.
*/
private int[] popupOrigin() {
if (font == null) return new int[]{0, 0};
int[] lc = offsetToLineCol(cursorPos);
String[] lines = getLines();
String lineText = (lc[0] < lines.length) ? lines[lc[0]] : "";
// Cursor pixel x/y (screen space)
int cx = textAreaX() + widthUpTo(lineText, Math.min(lc[1], lineText.length())) - scrollX;
int cy = getStartY() + lc[0] * font.lineHeight - scrollY;
int popupH = completions.size() * POPUP_ITEM_HEIGHT + POPUP_PADDING * 2;
int popupY = cy + font.lineHeight + 2;
// Flip above the cursor if it would overflow below
if (popupY + popupH > getEndY()) {
popupY = cy - popupH - 2;
}
// Keep within horizontal bounds
int popupX = cx;
if (popupX + POPUP_WIDTH > getEndX()) {
popupX = getEndX() - POPUP_WIDTH;
}
popupX = Math.max(textAreaX(), popupX);
return new int[]{popupX, popupY};
}
private int kindColor(CompletionItem.Kind kind) {
return switch (kind) {
case KEYWORD -> COLOR_KIND_KEYWORD;
case TYPE -> COLOR_KIND_TYPE;
case METHOD -> COLOR_KIND_METHOD;
case FIELD -> COLOR_KIND_FIELD;
case SNIPPET -> COLOR_KIND_SNIPPET;
};
}
/** Single-character kind badge text. */
private String kindLabel(CompletionItem.Kind kind) {
return switch (kind) {
case KEYWORD -> "k";
case TYPE -> "T";
case METHOD -> "m";
case FIELD -> "f";
case SNIPPET -> "s";
};
}
// ── Syntax highlighting ───────────────────────────────────────────────
/**
* Walks all lines once and records which ones start inside a multi-line
* string/comment block. Called lazily before every draw when the text has
* changed (tracked via {@link #highlightVersion}).
*/
private void rebuildHighlightState(String[] lines) {
if (multilineState.length != lines.length) {
multilineState = new boolean[lines.length];
}
boolean inBlock = false;
for (int i = 0; i < lines.length; i++) {
multilineState[i] = inBlock;
LuaHighlighter.LineTokens result = luaHighlighter.tokenizeLine(lines[i], inBlock);
inBlock = result.continuesMultilineString();
}
}
/**
* Draws a single source line with syntax highlighting.
* Tabs have already been expanded by {@link #sanitize} for width, but the
* highlighter works on the raw line; we expand tabs in each span before
* measuring/drawing so the x positions stay consistent.
*/
private void drawHighlightedLine(GuiGraphicsExtractor graphics, String rawLine,
int drawX, int drawY, boolean inMultiline) {
LuaHighlighter.LineTokens tokens = luaHighlighter.tokenizeLine(rawLine, inMultiline);
int x = drawX;
for (LuaHighlighter.Span span : tokens.spans()) {
String display = sanitize(span.text());
graphics.text(font, display, x, drawY, span.color(), true);
x += font.width(display);
}
}
// ── Draw ────────────────────────────────────────────────────────────── // ── Draw ──────────────────────────────────────────────────────────────
public void draw(@NonNull GuiGraphicsExtractor graphics, int mouseX, int mouseY, double delta) { public void draw(@NonNull GuiGraphicsExtractor graphics, int mouseX, int mouseY, double delta) {
@@ -204,11 +345,17 @@ public class EditorFileView extends Widget {
// Background // Background
graphics.fill(start_x, start_y, end_x, end_y, 0xFF303030); graphics.fill(start_x, start_y, end_x, end_y, 0xFF303030);
// Gutter background + divider (not scrolled — always fixed on left) // Gutter background + divider
graphics.fill(start_x, start_y, gutterDivX() - 1, end_y, COLOR_GUTTER_BG); graphics.fill(start_x, start_y, gutterDivX() - 1, end_y, COLOR_GUTTER_BG);
graphics.fill(gutterDivX() - 1, start_y, gutterDivX(), end_y, COLOR_GUTTER_DIV); graphics.fill(gutterDivX() - 1, start_y, gutterDivX(), end_y, COLOR_GUTTER_DIV);
// Only render lines that are at least partially visible // Rebuild per-line multiline-string state whenever text changes
int currentVersion = text.length() ^ text.toString().hashCode();
if (currentVersion != highlightVersion) {
rebuildHighlightState(lines);
highlightVersion = currentVersion;
}
int firstLine = Math.max(0, scrollY / lineH); int firstLine = Math.max(0, scrollY / lineH);
int lastLine = Math.min(lines.length - 1, (scrollY + textAreaHeight()) / lineH + 1); int lastLine = Math.min(lines.length - 1, (scrollY + textAreaHeight()) / lineH + 1);
@@ -225,12 +372,9 @@ public class EditorFileView extends Widget {
int colStart = (ln == loLC[0]) ? loLC[1] : 0; int colStart = (ln == loLC[0]) ? loLC[1] : 0;
int colEnd = (ln == hiLC[0]) ? hiLC[1] : lineText.length(); int colEnd = (ln == hiLC[0]) ? hiLC[1] : lineText.length();
// Use sanitized widths so tabs expand correctly in the highlight
int x0 = textAreaX() + widthUpTo(lineText, colStart) - scrollX; int x0 = textAreaX() + widthUpTo(lineText, colStart) - scrollX;
int x1 = textAreaX() + widthUpTo(lineText, colEnd) - scrollX; int x1 = textAreaX() + widthUpTo(lineText, colEnd) - scrollX;
if (x1 == x0) x1 = x0 + 2; if (x1 == x0) x1 = x0 + 2;
// Clamp to text area so highlight doesn't bleed into the gutter
x0 = Math.max(x0, textAreaX()); x0 = Math.max(x0, textAreaX());
x1 = Math.min(x1, end_x); x1 = Math.min(x1, end_x);
@@ -243,15 +387,14 @@ public class EditorFileView extends Widget {
for (int i = firstLine; i <= lastLine; i++) { for (int i = firstLine; i <= lastLine; i++) {
int screenY = start_y + i * lineH - scrollY; int screenY = start_y + i * lineH - scrollY;
// Line number — right-aligned, not scrolled horizontally
String lineNum = String.valueOf(i + 1); String lineNum = String.valueOf(i + 1);
int numW = font.width(lineNum); int numW = font.width(lineNum);
graphics.text(font, lineNum, graphics.text(font, lineNum,
gutterDivX() - 4 - numW, gutterDivX() - 4 - numW,
screenY, COLOR_GUTTER_TEXT, false); screenY, COLOR_GUTTER_TEXT, false);
// Code text — sanitized so \t renders as 4 spaces, offset by scrollX boolean lineInMultiline = (i < multilineState.length) && multilineState[i];
graphics.text(font, sanitize(lines[i]), textAreaX() - scrollX, screenY, COLOR_TEXT, true); drawHighlightedLine(graphics, lines[i], textAreaX() - scrollX, screenY, lineInMultiline);
} }
// Blinking cursor // Blinking cursor
@@ -261,25 +404,133 @@ public class EditorFileView extends Widget {
int ln = lc[0]; int ln = lc[0];
int col = lc[1]; int col = lc[1];
// Sanitize before measuring so the tab expansion is accounted for
int cursorX = textAreaX() + widthUpTo( int cursorX = textAreaX() + widthUpTo(
(ln < lines.length) ? lines[ln] : "", (ln < lines.length) ? lines[ln] : "",
(ln < lines.length) ? Math.min(col, lines[ln].length()) : 0 (ln < lines.length) ? Math.min(col, lines[ln].length()) : 0
) - scrollX; ) - scrollX;
int cursorY = start_y + ln * lineH - scrollY; int cursorY = start_y + ln * lineH - scrollY;
// Only draw cursor if it's within the visible text area
if (cursorX >= textAreaX() && cursorX <= end_x) { if (cursorX >= textAreaX() && cursorX <= end_x) {
graphics.fill(cursorX, cursorY, cursorX + 1, cursorY + lineH, COLOR_CURSOR); graphics.fill(cursorX, cursorY, cursorX + 1, cursorY + lineH, COLOR_CURSOR);
} }
} }
graphics.disableScissor(); graphics.disableScissor();
// ── Autocomplete popup (drawn outside scissor so it overlaps neighbour widgets) ──
if (isPopupVisible()) {
drawCompletionPopup(graphics, mouseX, mouseY);
}
}
private void drawCompletionPopup(@NonNull GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
int[] origin = popupOrigin();
int px = origin[0];
int py = origin[1];
int popupH = completions.size() * POPUP_ITEM_HEIGHT + POPUP_PADDING * 2;
// Shadow (subtle depth)
graphics.fill(px + 2, py + 2, px + POPUP_WIDTH + 2, py + popupH + 2, 0x55000000);
// Background + border
graphics.fill(px, py, px + POPUP_WIDTH, py + popupH, COLOR_POPUP_BG);
// Top border
graphics.fill(px, py, px + POPUP_WIDTH, py + 1, COLOR_POPUP_BORDER);
// Bottom border
graphics.fill(px, py + popupH - 1, px + POPUP_WIDTH, py + popupH, COLOR_POPUP_BORDER);
// Left border
graphics.fill(px, py, px + 1, py + popupH, COLOR_POPUP_BORDER);
// Right border
graphics.fill(px + POPUP_WIDTH - 1, py, px + POPUP_WIDTH, py + popupH, COLOR_POPUP_BORDER);
String currentPrefix = completionProvider.extractPrefix(text.toString(), cursorPos);
for (int i = 0; i < completions.size(); i++) {
CompletionItem item = completions.get(i);
int itemY = py + POPUP_PADDING + i * POPUP_ITEM_HEIGHT;
boolean selected = (i == completionIndex);
boolean hovered = mouseX >= px && mouseX < px + POPUP_WIDTH
&& mouseY >= itemY && mouseY < itemY + POPUP_ITEM_HEIGHT;
// Row background
if (selected) {
graphics.fill(px + 1, itemY, px + POPUP_WIDTH - 1, itemY + POPUP_ITEM_HEIGHT,
COLOR_POPUP_SELECTED);
} else if (hovered) {
graphics.fill(px + 1, itemY, px + POPUP_WIDTH - 1, itemY + POPUP_ITEM_HEIGHT,
0xFF2A2A3A);
}
// Kind colour strip (left edge)
int kindCol = kindColor(item.kind());
graphics.fill(px + 1, itemY, px + 1 + KIND_BADGE_WIDTH, itemY + POPUP_ITEM_HEIGHT, kindCol);
// Kind letter badge
int badgeTextX = px + 8;
int badgeTextY = itemY + (POPUP_ITEM_HEIGHT - font.lineHeight) / 2;
graphics.text(font, kindLabel(item.kind()), badgeTextX, badgeTextY,
kindCol, false);
// Completion label — highlight the matched prefix in a brighter colour
int labelX = px + KIND_BADGE_WIDTH + 14;
int labelY = itemY + (POPUP_ITEM_HEIGHT - font.lineHeight) / 2;
String label = item.label();
if (!currentPrefix.isEmpty()
&& label.toLowerCase().startsWith(currentPrefix.toLowerCase())) {
// Draw the matched prefix brighter / white
String matchedPart = label.substring(0, currentPrefix.length());
String remainingPart = label.substring(currentPrefix.length());
graphics.text(font, matchedPart, labelX, labelY, 0xFFFFFFFF, false);
int afterMatchX = labelX + font.width(matchedPart);
graphics.text(font, remainingPart, afterMatchX, labelY, COLOR_POPUP_TEXT, false);
} else {
graphics.text(font, label, labelX, labelY, COLOR_POPUP_TEXT, false);
}
// Detail annotation — right-aligned, dimmed
String detail = item.detail();
int detailW = font.width(detail);
int detailX = px + POPUP_WIDTH - detailW - POPUP_PADDING;
// Only draw if it fits without overlapping the label
if (detailX > labelX + font.width(label) + 4) {
graphics.text(font, detail, detailX, labelY, COLOR_POPUP_DETAIL, false);
}
}
// Hint line below the popup when more than one item is present
if (completions.size() > 1) {
int hintY = py + popupH + 2;
String hint = "↑↓ navigate Tab/Enter accept Esc dismiss";
graphics.text(font, hint, px, hintY, 0xFF555577, false);
}
} }
// ── Input handlers ──────────────────────────────────────────────────── // ── Input handlers ────────────────────────────────────────────────────
public boolean handleMouseClicked(MouseButtonEvent event) { public boolean handleMouseClicked(MouseButtonEvent event) {
// Check if clicking inside the popup
if (isPopupVisible()) {
int[] origin = popupOrigin();
int px = origin[0], py = origin[1];
int popupH = completions.size() * POPUP_ITEM_HEIGHT + POPUP_PADDING * 2;
if (event.x() >= px && event.x() < px + POPUP_WIDTH
&& event.y() >= py && event.y() < py + popupH) {
int relY = (int)(event.y() - py - POPUP_PADDING);
int clicked = relY / POPUP_ITEM_HEIGHT;
if (clicked >= 0 && clicked < completions.size()) {
completionIndex = clicked;
acceptCompletion();
}
return true;
}
// Click outside popup → dismiss
dismissCompletions();
}
int clicked = pixelToOffset(event.x(), event.y()); int clicked = pixelToOffset(event.x(), event.y());
cursorPos = clicked; cursorPos = clicked;
selectionAnchor = clicked; selectionAnchor = clicked;
@@ -289,7 +540,6 @@ public class EditorFileView extends Widget {
public boolean handleMouseDragged(MouseButtonEvent event) { public boolean handleMouseDragged(MouseButtonEvent event) {
cursorPos = pixelToOffset(event.x(), event.y()); cursorPos = pixelToOffset(event.x(), event.y());
// Auto-scroll when dragging outside the editor bounds
if (font != null) { if (font != null) {
int lineH = font.lineHeight; int lineH = font.lineHeight;
double my = event.y(); double my = event.y();
@@ -310,17 +560,10 @@ public class EditorFileView extends Widget {
return true; return true;
} }
/**
* Call this from your Screen's mouseScrolled override whenever inside() is true.
*
* @param delta positive = scroll up (content moves down), negative = scroll down
*/
public boolean handleMouseScrolled(double delta) { public boolean handleMouseScrolled(double delta) {
if (font == null) return true; if (font == null) return true;
int amount = (int) (delta * SCROLL_SPEED * font.lineHeight); int amount = (int) (delta * SCROLL_SPEED * font.lineHeight);
scrollY = Math.max(0, Math.min(scrollY - amount, maxScrollY())); scrollY = Math.max(0, Math.min(scrollY - amount, maxScrollY()));
return true; return true;
} }
@@ -332,6 +575,14 @@ public class EditorFileView extends Widget {
cursorPos += Character.charCount(codePoint); cursorPos += Character.charCount(codePoint);
selectionAnchor = -1; selectionAnchor = -1;
scrollToCursor(); scrollToCursor();
// Trigger autocomplete after typing an identifier char or '.'
char c = (char) codePoint;
if (Character.isJavaIdentifierPart(c) || c == '.') {
updateCompletions();
} else {
dismissCompletions();
}
} }
return true; return true;
} }
@@ -343,6 +594,33 @@ public class EditorFileView extends Widget {
Minecraft minecraft = Minecraft.getInstance(); Minecraft minecraft = Minecraft.getInstance();
// ── Popup navigation (intercept before normal key handling) ───────
if (isPopupVisible()) {
switch (key) {
case 264 -> { // Down
completionIndex = (completionIndex + 1) % completions.size();
return true;
}
case 265 -> { // Up
completionIndex = (completionIndex - 1 + completions.size()) % completions.size();
return true;
}
case 258 -> { // Tab — accept
acceptCompletion();
return true;
}
case 257 -> { // Enter — accept (fall through to newline below if no popup)
acceptCompletion();
return true;
}
case 256 -> { // Escape — dismiss
dismissCompletions();
return true;
}
}
}
// ── Normal key handling ──────────────────────────────────────────
switch (key) { switch (key) {
case 257 -> { // Enter case 257 -> { // Enter
if (hasSelection()) deleteSelection(); if (hasSelection()) deleteSelection();
@@ -350,14 +628,16 @@ public class EditorFileView extends Widget {
cursorPos++; cursorPos++;
selectionAnchor = -1; selectionAnchor = -1;
scrollToCursor(); scrollToCursor();
dismissCompletions();
return true; return true;
} }
case 258 -> { // Tab case 258 -> { // Tab (no popup)
if (hasSelection()) deleteSelection(); if (hasSelection()) deleteSelection();
text.insert(cursorPos, '\t'); text.insert(cursorPos, '\t');
cursorPos++; cursorPos++;
selectionAnchor = -1; selectionAnchor = -1;
scrollToCursor(); scrollToCursor();
dismissCompletions();
return true; return true;
} }
case 259 -> { // Backspace case 259 -> { // Backspace
@@ -365,6 +645,7 @@ public class EditorFileView extends Widget {
else if (cursorPos > 0) { text.deleteCharAt(--cursorPos); } else if (cursorPos > 0) { text.deleteCharAt(--cursorPos); }
selectionAnchor = -1; selectionAnchor = -1;
scrollToCursor(); scrollToCursor();
updateCompletions(); // re-compute after deletion
return true; return true;
} }
case 261 -> { // Delete case 261 -> { // Delete
@@ -372,6 +653,7 @@ public class EditorFileView extends Widget {
else if (cursorPos < text.length()) { text.deleteCharAt(cursorPos); } else if (cursorPos < text.length()) { text.deleteCharAt(cursorPos); }
selectionAnchor = -1; selectionAnchor = -1;
scrollToCursor(); scrollToCursor();
updateCompletions();
return true; return true;
} }
case 263 -> { // Left case 263 -> { // Left
@@ -383,6 +665,7 @@ public class EditorFileView extends Widget {
else if (cursorPos > 0) cursorPos--; else if (cursorPos > 0) cursorPos--;
selectionAnchor = -1; selectionAnchor = -1;
} }
dismissCompletions();
scrollToCursor(); scrollToCursor();
return true; return true;
} }
@@ -395,18 +678,20 @@ public class EditorFileView extends Widget {
else if (cursorPos < text.length()) cursorPos++; else if (cursorPos < text.length()) cursorPos++;
selectionAnchor = -1; selectionAnchor = -1;
} }
dismissCompletions();
scrollToCursor(); scrollToCursor();
return true; return true;
} }
case 265 -> { // Up case 265 -> { // Up (no popup open)
if (shift && selectionAnchor < 0) selectionAnchor = cursorPos; if (shift && selectionAnchor < 0) selectionAnchor = cursorPos;
int[] lc = offsetToLineCol(cursorPos); int[] lc = offsetToLineCol(cursorPos);
cursorPos = (lc[0] - 1 >= 0) ? lineColToOffset(lc[0] - 1, lc[1]) : 0; cursorPos = (lc[0] - 1 >= 0) ? lineColToOffset(lc[0] - 1, lc[1]) : 0;
if (!shift) selectionAnchor = -1; if (!shift) selectionAnchor = -1;
dismissCompletions();
scrollToCursor(); scrollToCursor();
return true; return true;
} }
case 264 -> { // Down case 264 -> { // Down (no popup open)
if (shift && selectionAnchor < 0) selectionAnchor = cursorPos; if (shift && selectionAnchor < 0) selectionAnchor = cursorPos;
int[] lc2 = offsetToLineCol(cursorPos); int[] lc2 = offsetToLineCol(cursorPos);
String[] lines = getLines(); String[] lines = getLines();
@@ -414,6 +699,7 @@ public class EditorFileView extends Widget {
? lineColToOffset(lc2[0] + 1, lc2[1]) ? lineColToOffset(lc2[0] + 1, lc2[1])
: text.length(); : text.length();
if (!shift) selectionAnchor = -1; if (!shift) selectionAnchor = -1;
dismissCompletions();
scrollToCursor(); scrollToCursor();
return true; return true;
} }
@@ -421,6 +707,7 @@ public class EditorFileView extends Widget {
if (shift && selectionAnchor < 0) selectionAnchor = cursorPos; if (shift && selectionAnchor < 0) selectionAnchor = cursorPos;
cursorPos = lineColToOffset(offsetToLineCol(cursorPos)[0], 0); cursorPos = lineColToOffset(offsetToLineCol(cursorPos)[0], 0);
if (!shift) selectionAnchor = -1; if (!shift) selectionAnchor = -1;
dismissCompletions();
scrollToCursor(); scrollToCursor();
return true; return true;
} }
@@ -429,11 +716,29 @@ public class EditorFileView extends Widget {
int[] lc4 = offsetToLineCol(cursorPos); int[] lc4 = offsetToLineCol(cursorPos);
cursorPos = lineColToOffset(lc4[0], getLines()[lc4[0]].length()); cursorPos = lineColToOffset(lc4[0], getLines()[lc4[0]].length());
if (!shift) selectionAnchor = -1; if (!shift) selectionAnchor = -1;
dismissCompletions();
scrollToCursor(); scrollToCursor();
return true; return true;
} }
case 256 -> { // Escape (no popup)
dismissCompletions();
return true;
}
case 32 -> { // Space
if (ctrl) {
// Ctrl+Space: manually trigger autocomplete
updateCompletions();
return true;
}
}
case 65 -> { // Ctrl+A case 65 -> { // Ctrl+A
if (ctrl) { selectionAnchor = 0; cursorPos = text.length(); scrollToCursor(); return true; } if (ctrl) {
selectionAnchor = 0;
cursorPos = text.length();
dismissCompletions();
scrollToCursor();
return true;
}
} }
case 67, 88 -> { // Ctrl+C / Ctrl+X case 67, 88 -> { // Ctrl+C / Ctrl+X
if (ctrl && hasSelection()) { if (ctrl && hasSelection()) {
@@ -452,6 +757,7 @@ public class EditorFileView extends Widget {
cursorPos += clip.length(); cursorPos += clip.length();
selectionAnchor = -1; selectionAnchor = -1;
scrollToCursor(); scrollToCursor();
dismissCompletions();
return true; return true;
} }
} }
@@ -465,14 +771,18 @@ public class EditorFileView extends Widget {
public void setEditorFile(EditorFile currentEditorFile) { public void setEditorFile(EditorFile currentEditorFile) {
this.currentEditorFile = currentEditorFile; this.currentEditorFile = currentEditorFile;
text = new StringBuilder(currentEditorFile.getText()); text = new StringBuilder(currentEditorFile.getText());
dismissCompletions();
} }
public void saveEditorFile() { public void saveEditorFile() {
if(this.currentEditorFile == null) return; editorManager.save();
this.currentEditorFile.setText(text.toString()); }
// Send save public void writeChanges() {
ServerboundSaveCodePayload payload = new ServerboundSaveCodePayload(this.currentEditorFile); currentEditorFile.setText(text.toString());
ClientPlayNetworking.send(payload); }
public EditorFile getCurrentEditorFile() {
return currentEditorFile;
} }
} }

View File

@@ -1,6 +1,12 @@
package com.aranroig.client.editor; package com.aranroig.client.editor;
import com.aranroig.SavedProjectData;
import com.aranroig.client.wrapper.Logger; import com.aranroig.client.wrapper.Logger;
import com.aranroig.editor.EditorFile;
import com.aranroig.editor.RunInfo;
import com.aranroig.payloads.ServerboundRunCodePayload;
import com.aranroig.payloads.ServerboundSaveCodePayload;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import party.iroiro.luajava.Lua; import party.iroiro.luajava.Lua;
import party.iroiro.luajava.luaj.LuaJ; import party.iroiro.luajava.luaj.LuaJ;
@@ -16,7 +22,7 @@ public class EditorManager {
EditorTopBar editorTopBar; EditorTopBar editorTopBar;
EditorFileView editorFileView; EditorFileView editorFileView;
List<com.aranroig.editor.EditorFile> files; List<EditorFile> files;
public EditorManager() { public EditorManager() {
client = Minecraft.getInstance(); client = Minecraft.getInstance();
@@ -24,15 +30,9 @@ public class EditorManager {
editorFileTree = new EditorFileTree(this); editorFileTree = new EditorFileTree(this);
editorTopBar = new EditorTopBar(this); editorTopBar = new EditorTopBar(this);
editorFileView = new EditorFileView(this); editorFileView = new EditorFileView(this);
files = new ArrayList<>();
// Add example file
com.aranroig.editor.EditorFile editorFile = new com.aranroig.editor.EditorFile("test.lua", "");
files.add(editorFile);
} }
public List<com.aranroig.editor.EditorFile> getFiles() { public List<EditorFile> getFiles() {
return files; return files;
} }
@@ -54,6 +54,17 @@ public class EditorManager {
editorFileView.setEditorFile(files.getFirst()); editorFileView.setEditorFile(files.getFirst());
editorFileView.addToEditorScreen(textEditorScreen); editorFileView.addToEditorScreen(textEditorScreen);
/*
files = new ArrayList<>();
// Add example file
EditorFile editorFile = new EditorFile("main.lua", "");
files.add(editorFile);
*/
// De alguna forma cargar archivos de server
client.setScreenAndShow(textEditorScreen); client.setScreenAndShow(textEditorScreen);
} }
@@ -61,18 +72,31 @@ public class EditorManager {
} }
public void save() { public void save() {
EditorFile currentEditorFile = editorFileView.getCurrentEditorFile();
if (currentEditorFile == null) return;
editorFileView.writeChanges();
ServerboundSaveCodePayload payload = new ServerboundSaveCodePayload(currentEditorFile);
ClientPlayNetworking.send(payload);
} }
public void undo() { public void undo() {
} }
public void run() { public void run() {
RunInfo runInfo = new RunInfo("main.lua");
ServerboundRunCodePayload payload = new ServerboundRunCodePayload(runInfo);
ClientPlayNetworking.send(payload);
/*
String code = files.getFirst().getText(); String code = files.getFirst().getText();
System.out.println(code); System.out.println(code);
Lua l = new LuaJ(); Lua l = new LuaJ();
l.register("print", Logger.print()); l.register("print", Logger.print());
l.run(code); l.run(code);
*/
} }
public void redo() { public void redo() {

View File

@@ -0,0 +1,267 @@
package com.aranroig.client.editor;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Tokenizes a single raw Lua source line into a list of {@link Span}s, each
* carrying the text to render and its ARGB colour. The caller then draws each
* span in sequence, advancing the x-position by {@code font.width(span.text())}
* between them.
*
* <p>Handles (within a single line):
* <ul>
* <li>Single-line comments {@code -- …}</li>
* <li>Keywords</li>
* <li>Built-in global names ({@code print}, {@code pairs}, …)</li>
* <li>Standard library prefixes ({@code math}, {@code string}, …)</li>
* <li>String literals {@code "…"} and {@code '…'}</li>
* <li>Number literals (integer, hex, float)</li>
* <li>Boolean / nil literals {@code true}, {@code false}, {@code nil}</li>
* <li>Operators and punctuation</li>
* <li>Plain identifiers and everything else (default colour)</li>
* </ul>
*
* <p>Multi-line strings/comments ({@code [[ … ]]}) require cross-line state;
* that state is carried through {@link #tokenizeLine(String, boolean)} via the
* {@code inMultilineString} flag, and the method returns whether the caller
* should pass {@code true} for the <em>next</em> line.
*/
public class LuaHighlighter {
// ── Token colours (VSCode-dark palette) ──────────────────────────────
public static final int C_DEFAULT = 0xFFD4D4D4; // plain identifiers / text
public static final int C_KEYWORD = 0xFF569CD6; // language keywords
public static final int C_BUILTIN = 0xFF4EC9B0; // built-in functions & globals
public static final int C_LIBRARY = 0xFF4EC9B0; // std-lib table names (math, …)
public static final int C_STRING = 0xFFCE9178; // string literals
public static final int C_NUMBER = 0xFFB5CEA8; // numeric literals
public static final int C_COMMENT = 0xFF6A9955; // comments
public static final int C_OPERATOR = 0xFFD4D4D4; // operators / punctuation
public static final int C_BOOLEAN = 0xFF569CD6; // true / false / nil
public static final int C_PARAM = 0xFF9CDCFE; // function parameters (heuristic)
// ── Lua vocabulary ────────────────────────────────────────────────────
private static final Set<String> KEYWORDS = Set.of(
"and", "break", "do", "else", "elseif", "end",
"false", "for", "function", "goto", "if", "in",
"local", "nil", "not", "or", "repeat", "return",
"then", "true", "until", "while"
);
private static final Set<String> BOOLEAN_NIL = Set.of("true", "false", "nil");
private static final Set<String> BUILTINS = Set.of(
"assert", "collectgarbage", "dofile", "error",
"getmetatable", "ipairs", "load", "loadfile", "next",
"pairs", "pcall", "print", "rawequal", "rawget",
"rawlen", "rawset", "require", "select", "setmetatable",
"tonumber", "tostring", "type", "unpack", "xpcall",
"_G", "_VERSION"
);
private static final Set<String> LIBRARIES = Set.of(
"string", "table", "math", "io", "os",
"coroutine", "package", "utf8", "debug"
);
// ── Public types ──────────────────────────────────────────────────────
/** A single coloured text fragment for one draw call. */
public record Span(String text, int color) {}
/**
* Result of tokenizing one line. Carries the spans to draw plus the
* multiline-string continuation state for the next line.
*/
public record LineTokens(List<Span> spans, boolean continuesMultilineString) {}
// ── Main API ──────────────────────────────────────────────────────────
/**
* Tokenizes {@code rawLine} (tabs already kept as-is; the caller sanitizes
* tabs to spaces for width measurement but we work on the raw chars here).
*
* @param rawLine the raw source line
* @param inMultilineString {@code true} if a {@code [[ }block started on a
* previous line and hasn't been closed yet
* @return spans + continuation flag
*/
public LineTokens tokenizeLine(String rawLine, boolean inMultilineString) {
List<Span> spans = new ArrayList<>();
int len = rawLine.length();
int i = 0;
// ── Carry-over multi-line string ──────────────────────────────────
if (inMultilineString) {
int close = rawLine.indexOf("]]");
if (close == -1) {
// Entire line is still inside the block string
spans.add(new Span(rawLine, C_STRING));
return new LineTokens(spans, true);
} else {
// Closing delimiter is on this line
spans.add(new Span(rawLine.substring(0, close + 2), C_STRING));
i = close + 2;
inMultilineString = false;
}
}
StringBuilder plain = new StringBuilder();
while (i < len) {
char c = rawLine.charAt(i);
// ── Single-line comment ───────────────────────────────────────
if (c == '-' && i + 1 < len && rawLine.charAt(i + 1) == '-') {
flushPlain(spans, plain);
// Check for long comment --[[ … ]]
if (i + 3 < len && rawLine.charAt(i + 2) == '[' && rawLine.charAt(i + 3) == '[') {
int close = rawLine.indexOf("]]", i + 4);
if (close == -1) {
spans.add(new Span(rawLine.substring(i), C_COMMENT));
return new LineTokens(spans, true); // multi-line comment (treated like string)
} else {
spans.add(new Span(rawLine.substring(i, close + 2), C_COMMENT));
i = close + 2;
continue;
}
}
// Regular -- comment: rest of line
spans.add(new Span(rawLine.substring(i), C_COMMENT));
return new LineTokens(spans, false);
}
// ── Multi-line string literal [[ ─────────────────────────────
if (c == '[' && i + 1 < len && rawLine.charAt(i + 1) == '[') {
flushPlain(spans, plain);
int close = rawLine.indexOf("]]", i + 2);
if (close == -1) {
spans.add(new Span(rawLine.substring(i), C_STRING));
return new LineTokens(spans, true);
} else {
spans.add(new Span(rawLine.substring(i, close + 2), C_STRING));
i = close + 2;
continue;
}
}
// ── String literal " or ' ─────────────────────────────────────
if (c == '"' || c == '\'') {
flushPlain(spans, plain);
char quote = c;
int start = i;
i++;
while (i < len) {
char ch = rawLine.charAt(i);
if (ch == '\\') { i += 2; continue; } // escape sequence
if (ch == quote) { i++; break; }
i++;
}
spans.add(new Span(rawLine.substring(start, i), C_STRING));
continue;
}
// ── Number literal ────────────────────────────────────────────
if (Character.isDigit(c) || (c == '.' && i + 1 < len && Character.isDigit(rawLine.charAt(i + 1)))) {
flushPlain(spans, plain);
int start = i;
// Hex?
if (c == '0' && i + 1 < len && (rawLine.charAt(i + 1) == 'x' || rawLine.charAt(i + 1) == 'X')) {
i += 2;
while (i < len && isHexDigit(rawLine.charAt(i))) i++;
} else {
while (i < len && (Character.isDigit(rawLine.charAt(i)) || rawLine.charAt(i) == '.')) i++;
if (i < len && (rawLine.charAt(i) == 'e' || rawLine.charAt(i) == 'E')) {
i++;
if (i < len && (rawLine.charAt(i) == '+' || rawLine.charAt(i) == '-')) i++;
while (i < len && Character.isDigit(rawLine.charAt(i))) i++;
}
}
spans.add(new Span(rawLine.substring(start, i), C_NUMBER));
continue;
}
// ── Identifier or keyword ─────────────────────────────────────
if (Character.isLetter(c) || c == '_') {
flushPlain(spans, plain);
int start = i;
while (i < len && (Character.isLetterOrDigit(rawLine.charAt(i)) || rawLine.charAt(i) == '_')) i++;
String word = rawLine.substring(start, i);
// Peek ahead: if followed by '(' it's a call — highlight as builtin/method
boolean isCall = (i < len && rawLine.charAt(i) == '(');
// Peek behind: if preceded by ':' or '.' it's a method — leave as default
boolean isMember = (start > 0 && (rawLine.charAt(start - 1) == '.' || rawLine.charAt(start - 1) == ':'));
if (BOOLEAN_NIL.contains(word)) {
spans.add(new Span(word, C_BOOLEAN));
} else if (KEYWORDS.contains(word)) {
spans.add(new Span(word, C_KEYWORD));
} else if (!isMember && LIBRARIES.contains(word)) {
spans.add(new Span(word, C_LIBRARY));
} else if (!isMember && BUILTINS.contains(word)) {
spans.add(new Span(word, C_BUILTIN));
} else if (isCall) {
// User-defined function call: slightly highlighted
spans.add(new Span(word, C_BUILTIN));
} else {
spans.add(new Span(word, C_DEFAULT));
}
continue;
}
// ── Operator / punctuation ────────────────────────────────────
if (isOperator(c)) {
flushPlain(spans, plain);
// Grab multi-char operators: ~=, ==, <=, >=, .., ...
if (i + 2 < len && rawLine.charAt(i) == '.' && rawLine.charAt(i+1) == '.' && rawLine.charAt(i+2) == '.') {
spans.add(new Span("...", C_OPERATOR)); i += 3;
} else if (i + 1 < len && isTwoCharOp(rawLine, i)) {
spans.add(new Span(rawLine.substring(i, i + 2), C_OPERATOR)); i += 2;
} else {
spans.add(new Span(String.valueOf(c), C_OPERATOR)); i++;
}
continue;
}
// ── Everything else (spaces, tabs, unknown) ───────────────────
plain.append(c);
i++;
}
flushPlain(spans, plain);
return new LineTokens(spans, false);
}
// ── Helpers ───────────────────────────────────────────────────────────
private static void flushPlain(List<Span> spans, StringBuilder sb) {
if (sb.length() > 0) {
spans.add(new Span(sb.toString(), C_DEFAULT));
sb.setLength(0);
}
}
private static boolean isHexDigit(char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
private static boolean isOperator(char c) {
return "+-*/%^#&|~<>=(){}[];:,./\\".indexOf(c) >= 0;
}
private static boolean isTwoCharOp(String line, int i) {
char a = line.charAt(i), b = line.charAt(i + 1);
return (a == '=' && b == '=')
|| (a == '~' && b == '=')
|| (a == '<' && b == '=')
|| (a == '>' && b == '=')
|| (a == '.' && b == '.')
|| (a == ':' && b == ':')
|| (a == '<' && b == '<')
|| (a == '>' && b == '>');
}
}

View File

@@ -1,5 +1,7 @@
package com.aranroig; package com.aranroig;
import com.aranroig.editor.EditorFile;
import com.aranroig.payloads.ServerboundRunCodePayload;
import com.aranroig.payloads.ServerboundSaveCodePayload; import com.aranroig.payloads.ServerboundSaveCodePayload;
import net.fabricmc.api.ModInitializer; import net.fabricmc.api.ModInitializer;
@@ -7,9 +9,13 @@ import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.resources.Identifier; import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.entity.vehicle.minecart.Minecart;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.List;
public class Codecraft implements ModInitializer { public class Codecraft implements ModInitializer {
public static final String MOD_ID = "codecraft"; public static final String MOD_ID = "codecraft";
@@ -18,6 +24,8 @@ public class Codecraft implements ModInitializer {
// That way, it's clear which mod wrote info, warnings, and errors. // That way, it's clear which mod wrote info, warnings, and errors.
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
private List<EditorFile> editorFiles;
@Override @Override
public void onInitialize() { public void onInitialize() {
// This code runs as soon as Minecraft is in a mod-load-ready state. // This code runs as soon as Minecraft is in a mod-load-ready state.
@@ -26,10 +34,49 @@ public class Codecraft implements ModInitializer {
LOGGER.info("Hello Fabric world!"); LOGGER.info("Hello Fabric world!");
PayloadTypeRegistry.serverboundPlay().register(ServerboundSaveCodePayload.TYPE, ServerboundSaveCodePayload.CODEC); PayloadTypeRegistry.serverboundPlay().register(ServerboundSaveCodePayload.TYPE, ServerboundSaveCodePayload.CODEC);
PayloadTypeRegistry.serverboundPlay().register(ServerboundRunCodePayload.TYPE, ServerboundRunCodePayload.CODEC);
ServerPlayNetworking.registerGlobalReceiver(ServerboundSaveCodePayload.TYPE, (payload, context) -> { ServerPlayNetworking.registerGlobalReceiver(ServerboundSaveCodePayload.TYPE, (payload, context) -> {
System.out.println(payload.data().getName()); MinecraftServer server = context.player().level().getServer();
System.out.println(payload.data().getText()); if(server == null) return;
SavedProjectData savedData = SavedProjectData.getSavedBlockData(server);
List<EditorFile> files = savedData.getEditorFiles();
EditorFile editorFile = payload.data();
boolean existing = false;
for(EditorFile file : files) {
if(file.getName().equals(editorFile.getName())) {
existing = true;
file.setText(editorFile.getText());
break;
}
}
if(!existing) {
files.add(editorFile);
}
savedData.setEditorFiles(files);
System.out.println("Successfully saved files");
});
ServerPlayNetworking.registerGlobalReceiver(ServerboundRunCodePayload.TYPE, (payload, context) -> {
// HERE
MinecraftServer server = context.player().level().getServer();
if(server == null) return;
SavedProjectData savedData = SavedProjectData.getSavedBlockData(server);
List<EditorFile> files = savedData.getEditorFiles();
String entrypoint = payload.data().getEntrypoint();
System.out.println("Want to run the code " + payload.data().getEntrypoint());
// TODO: Run the entrypoint, might change later
for(EditorFile file : files) {
if(file.getName().equals(entrypoint)) {
System.out.println(file.getText());
}
}
}); });
} }

View File

@@ -0,0 +1,65 @@
package com.aranroig;
import com.aranroig.editor.EditorFile;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.saveddata.SavedData;
import net.minecraft.world.level.saveddata.SavedDataType;
import java.util.ArrayList;
import java.util.List;
public class SavedProjectData extends SavedData {
private List<EditorFile> editorFiles;
public SavedProjectData() {
editorFiles = new ArrayList<EditorFile>();
}
public SavedProjectData(List<EditorFile> editorFiles) {
this.editorFiles = editorFiles;
}
public List<EditorFile> getEditorFiles() {
return editorFiles;
}
public void setEditorFiles(List<EditorFile> editorFiles) {
this.editorFiles = editorFiles;
setDirty();
}
private static final Codec<SavedProjectData> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
EditorFile.CODEC.listOf()
.fieldOf("editor_files")
.forGetter(SavedProjectData::getEditorFiles)
).apply(instance, SavedProjectData::new)
);
private static final SavedDataType<SavedProjectData> TYPE = new SavedDataType<SavedProjectData>(
Identifier.fromNamespaceAndPath(Codecraft.MOD_ID, "saved_project_data"), // The unique name for this saved data.
SavedProjectData::new, // If there's no 'SavedBlockData', yet create one and refresh fields.
CODEC, // The codec used for serialization/deserialization.
null // A data fixer, which is not needed here.
);
public static SavedProjectData getSavedBlockData(MinecraftServer server) {
// This could be either the overworld or another dimension.
ServerLevel level = server.getLevel(ServerLevel.OVERWORLD);
if (level == null) {
return new SavedProjectData(); // Return a new instance if the level is null.
}
// The first time the following 'computeIfAbsent' function is called, it creates a new 'SavedBlockData'
// instance and stores it inside the 'DimensionDataStorage'.
// Subsequent calls to 'computeIfAbsent' returns the saved 'SavedBlockData' NBT on disk to the Codec in our type,
// using the Codec to decode the NBT into our saved data.
return level.getDataStorage().computeIfAbsent(TYPE);
}
}

View File

@@ -1,11 +1,13 @@
package com.aranroig.editor; package com.aranroig.editor;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.codec.StreamCodec;
public class EditorFile { public class EditorFile {
public static final StreamCodec<RegistryFriendlyByteBuf, EditorFile> CODEC = public static final StreamCodec<RegistryFriendlyByteBuf, EditorFile> STREAM_CODEC =
StreamCodec.composite( StreamCodec.composite(
ByteBufCodecs.STRING_UTF8, ByteBufCodecs.STRING_UTF8,
EditorFile::getName, EditorFile::getName,
@@ -16,6 +18,13 @@ public class EditorFile {
EditorFile::new EditorFile::new
); );
public static final Codec<EditorFile> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
Codec.STRING.fieldOf("name").forGetter(EditorFile::getName),
Codec.STRING.fieldOf("text").forGetter(EditorFile::getText)
).apply(instance, EditorFile::new)
);
// Class with file info // Class with file info
private String name; private String name;
private String text; private String text;

View File

@@ -0,0 +1,9 @@
package com.aranroig.editor;
import java.util.List;
public record Project(List<EditorFile> editorFiles) {
/*
TODO: Acabar esto. En principio hay que sustituir las listas de los archivos de proyecto por esta clase
*/
}

View File

@@ -0,0 +1,30 @@
package com.aranroig.editor;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
public class RunInfo {
public static final StreamCodec<RegistryFriendlyByteBuf, RunInfo> CODEC =
StreamCodec.composite(
ByteBufCodecs.STRING_UTF8,
RunInfo::getEntrypoint,
RunInfo::new
);
// Class with file info
private String entrypoint;
public RunInfo(String entrypoint) {
this.entrypoint = entrypoint;
}
public String getEntrypoint() {
return entrypoint;
}
public void setEntrypoint(String entrypoint) {
this.entrypoint = entrypoint;
}
}

View File

@@ -14,7 +14,7 @@ public record ClientboundSaveCodePayload(EditorFile data) implements CustomPacke
public static final CustomPacketPayload.Type<ClientboundSaveCodePayload> TYPE = new CustomPacketPayload.Type<>(SAVE_CODE_PAYLOAD_ID); public static final CustomPacketPayload.Type<ClientboundSaveCodePayload> TYPE = new CustomPacketPayload.Type<>(SAVE_CODE_PAYLOAD_ID);
public static final StreamCodec<RegistryFriendlyByteBuf, ClientboundSaveCodePayload> CODEC = public static final StreamCodec<RegistryFriendlyByteBuf, ClientboundSaveCodePayload> CODEC =
StreamCodec.composite(EditorFile.CODEC, ClientboundSaveCodePayload::data, ClientboundSaveCodePayload::new); StreamCodec.composite(EditorFile.STREAM_CODEC, ClientboundSaveCodePayload::data, ClientboundSaveCodePayload::new);
@Override @Override
public Type<? extends CustomPacketPayload> type() { public Type<? extends CustomPacketPayload> type() {

View File

@@ -0,0 +1,24 @@
package com.aranroig.payloads;
import com.aranroig.Codecraft;
import com.aranroig.editor.EditorFile;
import com.aranroig.editor.RunInfo;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.Identifier;
public record ServerboundRunCodePayload(RunInfo data) implements CustomPacketPayload {
public static final Identifier RUN_CODE_PAYLOAD_ID = Identifier.fromNamespaceAndPath(Codecraft.MOD_ID, "run_code");
public static final CustomPacketPayload.Type<ServerboundRunCodePayload> TYPE = new CustomPacketPayload.Type<>(RUN_CODE_PAYLOAD_ID);
public static final StreamCodec<RegistryFriendlyByteBuf, ServerboundRunCodePayload> CODEC =
StreamCodec.composite(RunInfo.CODEC, ServerboundRunCodePayload::data, ServerboundRunCodePayload::new);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}

View File

@@ -13,7 +13,7 @@ public record ServerboundSaveCodePayload(EditorFile data) implements CustomPacke
public static final CustomPacketPayload.Type<ServerboundSaveCodePayload> TYPE = new CustomPacketPayload.Type<>(SAVE_CODE_PAYLOAD_ID); public static final CustomPacketPayload.Type<ServerboundSaveCodePayload> TYPE = new CustomPacketPayload.Type<>(SAVE_CODE_PAYLOAD_ID);
public static final StreamCodec<RegistryFriendlyByteBuf, ServerboundSaveCodePayload> CODEC = public static final StreamCodec<RegistryFriendlyByteBuf, ServerboundSaveCodePayload> CODEC =
StreamCodec.composite(EditorFile.CODEC, ServerboundSaveCodePayload::data, ServerboundSaveCodePayload::new); StreamCodec.composite(EditorFile.STREAM_CODEC, ServerboundSaveCodePayload::data, ServerboundSaveCodePayload::new);
@Override @Override
public CustomPacketPayload.Type<? extends CustomPacketPayload> type() { public CustomPacketPayload.Type<? extends CustomPacketPayload> type() {