@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.aranroig.client.editor;
|
||||
|
||||
import com.aranroig.editor.EditorFile;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
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) {
|
||||
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();
|
||||
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++) {
|
||||
graphics.text(font, files.get(i).getName(), getStartX(), getStartY() + i * lineH, COLOR_TEXT, true);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class EditorFileView extends Widget {
|
||||
|
||||
private StringBuilder text = new StringBuilder();
|
||||
@@ -27,23 +30,16 @@ public class EditorFileView extends Widget {
|
||||
}
|
||||
|
||||
// ── Scroll state ──────────────────────────────────────────────────────
|
||||
private int scrollX = 0; // horizontal scroll in pixels
|
||||
private int scrollY = 0; // vertical scroll in pixels
|
||||
private int scrollX = 0;
|
||||
private int scrollY = 0;
|
||||
|
||||
/** How many pixels to scroll per mouse-wheel notch (vertically). */
|
||||
private static final int SCROLL_SPEED = 3; // lines per notch
|
||||
|
||||
/** Width reserved for line numbers + a small right-padding. */
|
||||
private static final int SCROLL_SPEED = 3;
|
||||
private static final int GUTTER_WIDTH = 30;
|
||||
private static final int GUTTER_MARGIN = 5;
|
||||
|
||||
/** Where actual text content starts (after the gutter). */
|
||||
private int gutterDivX() { return getStartX() + GUTTER_WIDTH; }
|
||||
private int textAreaX() { return getStartX() + GUTTER_WIDTH + GUTTER_MARGIN; }
|
||||
|
||||
/** Visible width available for text (excludes gutter). */
|
||||
private int textAreaWidth() { return getEndX() - textAreaX(); }
|
||||
/** Visible height available for text. */
|
||||
private int textAreaHeight() { return getEndY() - getStartY(); }
|
||||
|
||||
// ── Colors ────────────────────────────────────────────────────────────
|
||||
@@ -54,6 +50,47 @@ public class EditorFileView extends Widget {
|
||||
private static final int COLOR_GUTTER_TEXT = 0xFF888888;
|
||||
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() {
|
||||
@@ -64,22 +101,16 @@ public class EditorFileView extends Widget {
|
||||
return selectionAnchor >= 0 && selectionAnchor != cursorPos;
|
||||
}
|
||||
|
||||
/** Replaces \t with 4 spaces so font.width() measures correctly. */
|
||||
private String sanitize(String input) {
|
||||
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) {
|
||||
return font.width(sanitize(rawLine.substring(0, charCount)));
|
||||
}
|
||||
|
||||
// ── Scroll helpers ────────────────────────────────────────────────────
|
||||
|
||||
/** Maximum horizontal scroll so the widest line end is just visible. */
|
||||
private int maxScrollX() {
|
||||
if (font == null) return 0;
|
||||
int maxW = 0;
|
||||
@@ -87,7 +118,6 @@ public class EditorFileView extends Widget {
|
||||
return Math.max(0, maxW - textAreaWidth());
|
||||
}
|
||||
|
||||
/** Maximum vertical scroll so the last line is just visible. */
|
||||
private int maxScrollY() {
|
||||
if (font == null) return 0;
|
||||
int totalH = getLines().length * font.lineHeight;
|
||||
@@ -99,17 +129,12 @@ public class EditorFileView extends Widget {
|
||||
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() {
|
||||
if (font == null) return;
|
||||
int[] lc = offsetToLineCol(cursorPos);
|
||||
String[] lines = getLines();
|
||||
String lineText = (lc[0] < lines.length) ? lines[lc[0]] : "";
|
||||
|
||||
// Vertical
|
||||
int cursorTop = lc[0] * font.lineHeight;
|
||||
int cursorBottom = cursorTop + font.lineHeight;
|
||||
if (cursorTop < scrollY)
|
||||
@@ -117,7 +142,6 @@ public class EditorFileView extends Widget {
|
||||
else if (cursorBottom > scrollY + textAreaHeight())
|
||||
scrollY = cursorBottom - textAreaHeight();
|
||||
|
||||
// Horizontal — measure sanitized prefix so tabs expand correctly
|
||||
int cursorPx = widthUpTo(lineText, Math.min(lc[1], lineText.length()));
|
||||
if (cursorPx < scrollX)
|
||||
scrollX = cursorPx;
|
||||
@@ -148,16 +172,10 @@ public class EditorFileView extends Widget {
|
||||
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) {
|
||||
String[] lines = getLines();
|
||||
int lineH = font.lineHeight;
|
||||
|
||||
// Apply scroll to convert screen coords → content coords
|
||||
double contentY = mouseY - getStartY() + scrollY;
|
||||
double contentX = mouseX - textAreaX() + scrollX;
|
||||
|
||||
@@ -168,7 +186,6 @@ public class EditorFileView extends Widget {
|
||||
String sanitized = sanitize(lineText);
|
||||
int col = 0;
|
||||
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 midPoint = font.width(sanitized.substring(0, i)) + charW / 2f;
|
||||
if (contentX < midPoint) { col = i; break; }
|
||||
@@ -186,6 +203,130 @@ public class EditorFileView extends Widget {
|
||||
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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
public void draw(@NonNull GuiGraphicsExtractor graphics, int mouseX, int mouseY, double delta) {
|
||||
@@ -204,11 +345,17 @@ public class EditorFileView extends Widget {
|
||||
// Background
|
||||
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(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 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 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 x1 = textAreaX() + widthUpTo(lineText, colEnd) - scrollX;
|
||||
if (x1 == x0) x1 = x0 + 2;
|
||||
|
||||
// Clamp to text area so highlight doesn't bleed into the gutter
|
||||
x0 = Math.max(x0, textAreaX());
|
||||
x1 = Math.min(x1, end_x);
|
||||
|
||||
@@ -243,15 +387,14 @@ public class EditorFileView extends Widget {
|
||||
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 — sanitized so \t renders as 4 spaces, offset by scrollX
|
||||
graphics.text(font, sanitize(lines[i]), textAreaX() - scrollX, screenY, COLOR_TEXT, true);
|
||||
boolean lineInMultiline = (i < multilineState.length) && multilineState[i];
|
||||
drawHighlightedLine(graphics, lines[i], textAreaX() - scrollX, screenY, lineInMultiline);
|
||||
}
|
||||
|
||||
// Blinking cursor
|
||||
@@ -261,25 +404,133 @@ public class EditorFileView extends Widget {
|
||||
int ln = lc[0];
|
||||
int col = lc[1];
|
||||
|
||||
// Sanitize before measuring so the tab expansion is accounted for
|
||||
int cursorX = textAreaX() + widthUpTo(
|
||||
(ln < lines.length) ? lines[ln] : "",
|
||||
(ln < lines.length) ? Math.min(col, lines[ln].length()) : 0
|
||||
) - scrollX;
|
||||
int cursorY = start_y + ln * lineH - scrollY;
|
||||
|
||||
// Only draw cursor if it's within the visible text area
|
||||
if (cursorX >= textAreaX() && cursorX <= end_x) {
|
||||
graphics.fill(cursorX, cursorY, cursorX + 1, cursorY + lineH, COLOR_CURSOR);
|
||||
}
|
||||
}
|
||||
|
||||
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 ────────────────────────────────────────────────────
|
||||
|
||||
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());
|
||||
cursorPos = clicked;
|
||||
selectionAnchor = clicked;
|
||||
@@ -289,7 +540,6 @@ public class EditorFileView extends Widget {
|
||||
public boolean handleMouseDragged(MouseButtonEvent event) {
|
||||
cursorPos = pixelToOffset(event.x(), event.y());
|
||||
|
||||
// Auto-scroll when dragging outside the editor bounds
|
||||
if (font != null) {
|
||||
int lineH = font.lineHeight;
|
||||
double my = event.y();
|
||||
@@ -310,17 +560,10 @@ public class EditorFileView extends Widget {
|
||||
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) {
|
||||
if (font == null) return true;
|
||||
int amount = (int) (delta * SCROLL_SPEED * font.lineHeight);
|
||||
|
||||
scrollY = Math.max(0, Math.min(scrollY - amount, maxScrollY()));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -332,6 +575,14 @@ public class EditorFileView extends Widget {
|
||||
cursorPos += Character.charCount(codePoint);
|
||||
selectionAnchor = -1;
|
||||
scrollToCursor();
|
||||
|
||||
// Trigger autocomplete after typing an identifier char or '.'
|
||||
char c = (char) codePoint;
|
||||
if (Character.isJavaIdentifierPart(c) || c == '.') {
|
||||
updateCompletions();
|
||||
} else {
|
||||
dismissCompletions();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -343,6 +594,33 @@ public class EditorFileView extends Widget {
|
||||
|
||||
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) {
|
||||
case 257 -> { // Enter
|
||||
if (hasSelection()) deleteSelection();
|
||||
@@ -350,14 +628,16 @@ public class EditorFileView extends Widget {
|
||||
cursorPos++;
|
||||
selectionAnchor = -1;
|
||||
scrollToCursor();
|
||||
dismissCompletions();
|
||||
return true;
|
||||
}
|
||||
case 258 -> { // Tab
|
||||
case 258 -> { // Tab (no popup)
|
||||
if (hasSelection()) deleteSelection();
|
||||
text.insert(cursorPos, '\t');
|
||||
cursorPos++;
|
||||
selectionAnchor = -1;
|
||||
scrollToCursor();
|
||||
dismissCompletions();
|
||||
return true;
|
||||
}
|
||||
case 259 -> { // Backspace
|
||||
@@ -365,6 +645,7 @@ public class EditorFileView extends Widget {
|
||||
else if (cursorPos > 0) { text.deleteCharAt(--cursorPos); }
|
||||
selectionAnchor = -1;
|
||||
scrollToCursor();
|
||||
updateCompletions(); // re-compute after deletion
|
||||
return true;
|
||||
}
|
||||
case 261 -> { // Delete
|
||||
@@ -372,6 +653,7 @@ public class EditorFileView extends Widget {
|
||||
else if (cursorPos < text.length()) { text.deleteCharAt(cursorPos); }
|
||||
selectionAnchor = -1;
|
||||
scrollToCursor();
|
||||
updateCompletions();
|
||||
return true;
|
||||
}
|
||||
case 263 -> { // Left
|
||||
@@ -383,6 +665,7 @@ public class EditorFileView extends Widget {
|
||||
else if (cursorPos > 0) cursorPos--;
|
||||
selectionAnchor = -1;
|
||||
}
|
||||
dismissCompletions();
|
||||
scrollToCursor();
|
||||
return true;
|
||||
}
|
||||
@@ -395,18 +678,20 @@ public class EditorFileView extends Widget {
|
||||
else if (cursorPos < text.length()) cursorPos++;
|
||||
selectionAnchor = -1;
|
||||
}
|
||||
dismissCompletions();
|
||||
scrollToCursor();
|
||||
return true;
|
||||
}
|
||||
case 265 -> { // Up
|
||||
case 265 -> { // Up (no popup open)
|
||||
if (shift && selectionAnchor < 0) selectionAnchor = cursorPos;
|
||||
int[] lc = offsetToLineCol(cursorPos);
|
||||
cursorPos = (lc[0] - 1 >= 0) ? lineColToOffset(lc[0] - 1, lc[1]) : 0;
|
||||
if (!shift) selectionAnchor = -1;
|
||||
dismissCompletions();
|
||||
scrollToCursor();
|
||||
return true;
|
||||
}
|
||||
case 264 -> { // Down
|
||||
case 264 -> { // Down (no popup open)
|
||||
if (shift && selectionAnchor < 0) selectionAnchor = cursorPos;
|
||||
int[] lc2 = offsetToLineCol(cursorPos);
|
||||
String[] lines = getLines();
|
||||
@@ -414,6 +699,7 @@ public class EditorFileView extends Widget {
|
||||
? lineColToOffset(lc2[0] + 1, lc2[1])
|
||||
: text.length();
|
||||
if (!shift) selectionAnchor = -1;
|
||||
dismissCompletions();
|
||||
scrollToCursor();
|
||||
return true;
|
||||
}
|
||||
@@ -421,6 +707,7 @@ public class EditorFileView extends Widget {
|
||||
if (shift && selectionAnchor < 0) selectionAnchor = cursorPos;
|
||||
cursorPos = lineColToOffset(offsetToLineCol(cursorPos)[0], 0);
|
||||
if (!shift) selectionAnchor = -1;
|
||||
dismissCompletions();
|
||||
scrollToCursor();
|
||||
return true;
|
||||
}
|
||||
@@ -429,11 +716,29 @@ public class EditorFileView extends Widget {
|
||||
int[] lc4 = offsetToLineCol(cursorPos);
|
||||
cursorPos = lineColToOffset(lc4[0], getLines()[lc4[0]].length());
|
||||
if (!shift) selectionAnchor = -1;
|
||||
dismissCompletions();
|
||||
scrollToCursor();
|
||||
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
|
||||
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
|
||||
if (ctrl && hasSelection()) {
|
||||
@@ -452,6 +757,7 @@ public class EditorFileView extends Widget {
|
||||
cursorPos += clip.length();
|
||||
selectionAnchor = -1;
|
||||
scrollToCursor();
|
||||
dismissCompletions();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -465,14 +771,18 @@ public class EditorFileView extends Widget {
|
||||
public void setEditorFile(EditorFile currentEditorFile) {
|
||||
this.currentEditorFile = currentEditorFile;
|
||||
text = new StringBuilder(currentEditorFile.getText());
|
||||
dismissCompletions();
|
||||
}
|
||||
|
||||
public void saveEditorFile() {
|
||||
if(this.currentEditorFile == null) return;
|
||||
this.currentEditorFile.setText(text.toString());
|
||||
editorManager.save();
|
||||
}
|
||||
|
||||
// Send save
|
||||
ServerboundSaveCodePayload payload = new ServerboundSaveCodePayload(this.currentEditorFile);
|
||||
ClientPlayNetworking.send(payload);
|
||||
public void writeChanges() {
|
||||
currentEditorFile.setText(text.toString());
|
||||
}
|
||||
|
||||
public EditorFile getCurrentEditorFile() {
|
||||
return currentEditorFile;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
package com.aranroig.client.editor;
|
||||
|
||||
import com.aranroig.SavedProjectData;
|
||||
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 party.iroiro.luajava.Lua;
|
||||
import party.iroiro.luajava.luaj.LuaJ;
|
||||
@@ -16,7 +22,7 @@ public class EditorManager {
|
||||
EditorTopBar editorTopBar;
|
||||
EditorFileView editorFileView;
|
||||
|
||||
List<com.aranroig.editor.EditorFile> files;
|
||||
List<EditorFile> files;
|
||||
|
||||
public EditorManager() {
|
||||
client = Minecraft.getInstance();
|
||||
@@ -24,15 +30,9 @@ public class EditorManager {
|
||||
editorFileTree = new EditorFileTree(this);
|
||||
editorTopBar = new EditorTopBar(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;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,17 @@ public class EditorManager {
|
||||
editorFileView.setEditorFile(files.getFirst());
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -61,18 +72,31 @@ public class EditorManager {
|
||||
}
|
||||
|
||||
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 run() {
|
||||
RunInfo runInfo = new RunInfo("main.lua");
|
||||
ServerboundRunCodePayload payload = new ServerboundRunCodePayload(runInfo);
|
||||
|
||||
ClientPlayNetworking.send(payload);
|
||||
/*
|
||||
String code = files.getFirst().getText();
|
||||
System.out.println(code);
|
||||
|
||||
Lua l = new LuaJ();
|
||||
l.register("print", Logger.print());
|
||||
l.run(code);
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
public void redo() {
|
||||
|
||||
267
src/client/java/com/aranroig/client/editor/LuaHighlighter.java
Normal file
267
src/client/java/com/aranroig/client/editor/LuaHighlighter.java
Normal 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 == '>');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.aranroig;
|
||||
|
||||
import com.aranroig.editor.EditorFile;
|
||||
import com.aranroig.payloads.ServerboundRunCodePayload;
|
||||
import com.aranroig.payloads.ServerboundSaveCodePayload;
|
||||
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.minecraft.resources.Identifier;
|
||||
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.entity.vehicle.minecart.Minecart;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class Codecraft implements ModInitializer {
|
||||
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.
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
private List<EditorFile> editorFiles;
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
// 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!");
|
||||
|
||||
PayloadTypeRegistry.serverboundPlay().register(ServerboundSaveCodePayload.TYPE, ServerboundSaveCodePayload.CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(ServerboundRunCodePayload.TYPE, ServerboundRunCodePayload.CODEC);
|
||||
|
||||
ServerPlayNetworking.registerGlobalReceiver(ServerboundSaveCodePayload.TYPE, (payload, context) -> {
|
||||
System.out.println(payload.data().getName());
|
||||
System.out.println(payload.data().getText());
|
||||
MinecraftServer server = context.player().level().getServer();
|
||||
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());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
65
src/main/java/com/aranroig/SavedProjectData.java
Normal file
65
src/main/java/com/aranroig/SavedProjectData.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.aranroig.editor;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
|
||||
public class EditorFile {
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, EditorFile> CODEC =
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, EditorFile> STREAM_CODEC =
|
||||
StreamCodec.composite(
|
||||
ByteBufCodecs.STRING_UTF8,
|
||||
EditorFile::getName,
|
||||
@@ -16,6 +18,13 @@ public class EditorFile {
|
||||
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
|
||||
private String name;
|
||||
private String text;
|
||||
|
||||
9
src/main/java/com/aranroig/editor/Project.java
Normal file
9
src/main/java/com/aranroig/editor/Project.java
Normal 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
|
||||
*/
|
||||
}
|
||||
30
src/main/java/com/aranroig/editor/RunInfo.java
Normal file
30
src/main/java/com/aranroig/editor/RunInfo.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 StreamCodec<RegistryFriendlyByteBuf, ClientboundSaveCodePayload> CODEC =
|
||||
StreamCodec.composite(EditorFile.CODEC, ClientboundSaveCodePayload::data, ClientboundSaveCodePayload::new);
|
||||
StreamCodec.composite(EditorFile.STREAM_CODEC, ClientboundSaveCodePayload::data, ClientboundSaveCodePayload::new);
|
||||
|
||||
@Override
|
||||
public Type<? extends CustomPacketPayload> type() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 StreamCodec<RegistryFriendlyByteBuf, ServerboundSaveCodePayload> CODEC =
|
||||
StreamCodec.composite(EditorFile.CODEC, ServerboundSaveCodePayload::data, ServerboundSaveCodePayload::new);
|
||||
StreamCodec.composite(EditorFile.STREAM_CODEC, ServerboundSaveCodePayload::data, ServerboundSaveCodePayload::new);
|
||||
|
||||
@Override
|
||||
public CustomPacketPayload.Type<? extends CustomPacketPayload> type() {
|
||||
|
||||
Reference in New Issue
Block a user