Refactored error curly underline rendering to its own helper class

This commit is contained in:
bigguy345 committed 2025-12-28 19:57:10 +02:00
1 parent e2a5d254a9
commit 534ec7b2c5
2 files changed
+281 -388

No files matched your search

@@ -0,0 +1,272 @@
package noppes.npcs.client.gui.util.script.interpreter;
import noppes.npcs.client.ClientProxy;
import org.lwjgl.opengl.GL11;
/**
* Helper class for rendering error underlines in script editor lines.
* Handles the repetitive calculation of underline positions and drawing of curly underlines.
*/
public class ErrorUnderlineRenderer {
private static final int ERROR_COLOR = 0xFF5555;
// Holds the calculation result for an underline
private static class UnderlinePosition {
final int x;
final int width;
UnderlinePosition(int x, int width) {
this.x = x;
this.width = width;
}
boolean isValid() {
return width > 0;
}
}
/**
* Draw error underlines for all validation errors in the document that intersect the given line.
*
* @param doc The script document containing all errors
* @param lineStartX X coordinate where the line starts rendering
* @param baselineY Y coordinate for the underline baseline
* @param lineText The text content of the line
* @param lineStart Global offset where this line starts
* @param lineEnd Global offset where this line ends
*/
public static void drawErrorUnderlines(
ScriptDocument doc,
int lineStartX, int baselineY,
String lineText, int lineStart, int lineEnd) {
if (doc == null)
return;
// Check all method calls in the document
for (MethodCallInfo call : doc.getMethodCalls()) {
// Skip method declarations that were erroneously recorded as calls
boolean isDeclaration = false;
int methodStart = call.getMethodNameStart();
for (MethodInfo mi : doc.getMethods()) {
if (mi.getDeclarationOffset() <= methodStart && mi.getBodyStart() >= methodStart) {
isDeclaration = true;
break;
}
}
if (isDeclaration)
continue;
// Skip if this call doesn't intersect this line
if (call.getCloseParenOffset() < lineStart || call.getOpenParenOffset() > lineEnd)
continue;
// Handle arg count errors (underline the method name)
if (call.hasArgCountError()) {
int methodEnd = methodStart + call.getMethodName().length();
drawUnderlineForSpan(methodStart, methodEnd, lineStartX, baselineY,
lineText, lineStart, lineEnd, ERROR_COLOR);
}
// Handle return type mismatch (underline the method name) - currently commented out
// if (call.hasReturnTypeMismatch()) { ... }
// Handle arg type errors (underline specific arguments)
if (call.hasArgTypeError()) {
for (MethodCallInfo.ArgumentTypeError error : call.getArgumentTypeErrors()) {
MethodCallInfo.Argument arg = error.getArg();
drawUnderlineForSpan(arg.getStartOffset(), arg.getEndOffset(),
lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR);
}
}
}
// Check all field accesses in the document (currently commented out)
// for (FieldAccessInfo access : doc.getFieldAccesses()) { ... }
// Check all errored assignments in the document
for (AssignmentInfo assign : doc.getAllErroredAssignments()) {
int underlineStart, underlineEnd;
if (assign.isLhsError()) {
underlineStart = assign.getLhsStart();
underlineEnd = assign.getLhsEnd();
} else if (assign.isRhsError()) {
underlineStart = assign.getRhsStart();
underlineEnd = assign.getRhsEnd();
} else if (assign.isFullLineError()) {
underlineStart = assign.getStatementStart();
underlineEnd = assign.getRhsEnd();
} else {
continue;
}
drawUnderlineForSpan(underlineStart, underlineEnd, lineStartX, baselineY,
lineText, lineStart, lineEnd, ERROR_COLOR);
}
// Check all method declarations for errors
for (MethodInfo method : doc.getMethods()) {
if (!method.isDeclaration() || !method.hasError())
continue;
// Handle return statement type errors
if (method.hasReturnStatementErrors()) {
for (MethodInfo.ReturnStatementError returnError : method.getReturnStatementErrors()) {
drawUnderlineForSpan(returnError.getStartOffset(), returnError.getEndOffset(),
lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR);
}
}
// Handle missing return error (underline the method name)
if (method.hasMissingReturnError()) {
int methodNameStart = method.getNameOffset();
int methodNameEnd = methodNameStart + method.getName().length();
drawUnderlineForSpan(methodNameStart, methodNameEnd, lineStartX, baselineY,
lineText, lineStart, lineEnd, ERROR_COLOR);
}
// Handle duplicate method error (underline from full declaration start to closing paren)
if (method.getErrorType() == MethodInfo.ErrorType.DUPLICATE_METHOD) {
drawUnderlineForSpan(method.getFullDeclarationOffset(), method.getDeclarationEnd(),
lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR);
}
// Handle parameter errors
if (method.hasParameterErrors()) {
for (MethodInfo.ParameterError paramError : method.getParameterErrors()) {
FieldInfo param = paramError.getParameter();
if (param == null || param.getDeclarationOffset() < 0)
continue;
int paramStart = param.getDeclarationOffset();
int paramEnd = paramStart + param.getName().length();
drawUnderlineForSpan(paramStart, paramEnd, lineStartX, baselineY,
lineText, lineStart, lineEnd, ERROR_COLOR);
}
}
}
}
/**
* Calculate underline position for a span of text within a line.
* Handles clipping to line boundaries and pixel width calculation.
*
* @param spanStart Global offset where the span starts
* @param spanEnd Global offset where the span ends
* @param lineStartX X coordinate where the line starts rendering
* @param lineText The text content of the line
* @param lineStart Global offset where this line starts
* @param lineEnd Global offset where this line ends
* @return UnderlinePosition with x and width, or null if span doesn't intersect line
*/
private static UnderlinePosition calculateUnderlinePosition(
int spanStart, int spanEnd,
int lineStartX, String lineText, int lineStart, int lineEnd) {
// Skip if span doesn't intersect this line
if (spanEnd < lineStart || spanStart > lineEnd)
return null;
// Clip to line boundaries
int clipStart = Math.max(spanStart, lineStart);
int clipEnd = Math.min(spanEnd, lineEnd);
if (clipStart >= clipEnd)
return null;
// Convert to line-local coordinates
int lineLocalStart = clipStart - lineStart;
int lineLocalEnd = clipEnd - lineStart;
// Bounds check
if (lineLocalStart < 0 || lineLocalStart >= lineText.length())
return null;
// Compute pixel position
String beforeSpan = lineText.substring(0, lineLocalStart);
int beforeWidth = ClientProxy.Font.width(beforeSpan);
int spanWidth;
if (lineLocalEnd > lineText.length()) {
// Span extends past line end
spanWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart));
} else {
// Span is fully on this line (or clipped)
String spanTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd);
spanWidth = ClientProxy.Font.width(spanTextOnLine);
}
return new UnderlinePosition(lineStartX + beforeWidth, spanWidth);
}
/**
* Draw an underline for a simple span if it intersects the line.
*
* @param spanStart Global offset where the span starts
* @param spanEnd Global offset where the span ends
* @param lineStartX X coordinate where the line starts rendering
* @param baselineY Y coordinate for the underline baseline
* @param lineText The text content of the line
* @param lineStart Global offset where this line starts
* @param lineEnd Global offset where this line ends
* @param color Underline color
*/
public static void drawUnderlineForSpan(
int spanStart, int spanEnd,
int lineStartX, int baselineY,
String lineText, int lineStart, int lineEnd,
int color) {
UnderlinePosition pos = calculateUnderlinePosition(
spanStart, spanEnd, lineStartX, lineText, lineStart, lineEnd);
if (pos != null && pos.isValid()) {
drawCurlyUnderline(pos.x, baselineY, pos.width, color);
}
}
/**
* Draw a curly/wavy underline (like IDE error highlighting).
* @param x Start X position
* @param y Y position (bottom of text)
* @param width Width of the underline
* @param color Color in ARGB format (e.g., 0xFFFF5555 for red)
*/
public static void drawCurlyUnderline(int x, int y, int width, int color) {
if (width <= 0)
return;
float a = ((color >> 24) & 0xFF) / 255f;
float r = ((color >> 16) & 0xFF) / 255f;
float g = ((color >> 8) & 0xFF) / 255f;
float b = (color & 0xFF) / 255f;
// If alpha is 0, assume full opacity
if (a == 0)
a = 1.0f;
GL11.glPushMatrix();
GL11.glDisable(GL11.GL_TEXTURE_2D);
GL11.glEnable(GL11.GL_BLEND);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GL11.glColor4f(r, g, b, a);
GL11.glLineWidth(1.0f);
GL11.glBegin(GL11.GL_LINE_STRIP);
// Wave parameters: 2 pixels amplitude, 4 pixels wavelength
int waveHeight = 1;
float waveLength = 4f;
for (float i = -0.5f; i <= width - 1; i += 0.125f) {
// Create a sine-like wave pattern
double phase = (double) i / waveLength * Math.PI * 2;
float yOffset = (float) (Math.sin(phase) * waveHeight) - 0.25f;
GL11.glVertex2f(x + i + 2f, y + yOffset);
}
GL11.glEnd();
GL11.glEnable(GL11.GL_TEXTURE_2D);
GL11.glPopMatrix();
}
}
@@ -261,399 +261,20 @@ public class ScriptLine {
/**
* Draw underlines for all validation errors (method calls and field accesses) that intersect this line.
* Uses the same token-walking logic as Token.drawUnderline but applies it
* to ALL validatable items in the document, enabling multi-line underlines.
* Delegates to ErrorUnderlineRenderer for the actual rendering logic.
*/
private void drawErrorUnderlines(int lineStartX, int baselineY) {
if (parent == null)
return;
ScriptDocument doc = parent;
String lineText = getText();
int lineStart = getGlobalStart();
int lineEnd = getGlobalEnd();
// Check all method calls in the document
for (MethodCallInfo call : doc.getMethodCalls()) {
// Skip method declarations that were erroneously recorded as calls.
boolean isDeclaration = false;
int methodStart = call.getMethodNameStart();
for (MethodInfo mi : doc.getMethods()) {
//SKIP method declarations cause for some reason their underlines are drawn here
if (mi.getDeclarationOffset() <= methodStart && mi.getBodyStart() >= methodStart) {
isDeclaration = true;
break;
}
}
if (isDeclaration)
continue;
// Skip if this call doesn't intersect this line
if (call.getCloseParenOffset() < lineStart || call.getOpenParenOffset() > lineEnd)
continue;
// Handle arg count errors (underline the method name)
if (call.hasArgCountError()) {
// Check if method name is on this line
if (methodStart >= lineStart && methodStart < lineEnd) {
int lineLocalStart = methodStart - lineStart;
if (lineLocalStart >= 0 && lineLocalStart < lineText.length()) {
String beforeMethod = lineText.substring(0, lineLocalStart);
int beforeWidth = ClientProxy.Font.width(beforeMethod);
int methodWidth = ClientProxy.Font.width(call.getMethodName());
drawCurlyUnderline(lineStartX + beforeWidth, baselineY, methodWidth, 0xFF5555);
}
}
}
// Handle return type mismatch (underline the method name)
if (call.hasReturnTypeMismatch()) {
// Check if method name is on this line
if (methodStart >= lineStart && methodStart < lineEnd) {
int lineLocalStart = methodStart - lineStart;
if (lineLocalStart >= 0 && lineLocalStart < lineText.length()) {
String beforeMethod = lineText.substring(0, lineLocalStart);
int beforeWidth = ClientProxy.Font.width(beforeMethod);
int methodWidth = ClientProxy.Font.width(call.getMethodName());
// drawCurlyUnderline(lineStartX + beforeWidth, baselineY, methodWidth, 0xFF5555);
}
}
}
// Handle arg type errors (underline specific arguments)
if (call.hasArgTypeError()) {
for (MethodCallInfo.ArgumentTypeError error : call.getArgumentTypeErrors()) {
MethodCallInfo.Argument arg = error.getArg();
int argStart = arg.getStartOffset();
int argEnd = arg.getEndOffset();
// Clip argument to this line's bounds
int clipStart = Math.max(argStart, lineStart);
int clipEnd = Math.min(argEnd, lineEnd);
// Skip if argument doesn't intersect this line
if (clipStart >= clipEnd)
continue;
// Convert to line-local coordinates
int lineLocalStart = clipStart - lineStart;
int lineLocalEnd = clipEnd - lineStart;
// Bounds check
if (lineLocalStart < 0 || lineLocalStart >= lineText.length())
continue;
// Compute pixel position using same logic as Token.drawUnderline
String beforeArg = lineText.substring(0, lineLocalStart);
int beforeWidth = ClientProxy.Font.width(beforeArg);
int argWidth;
if (lineLocalEnd > lineText.length()) {
// Argument extends past line end
argWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart));
} else {
// Argument is fully on this line (or clipped)
String argTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd);
argWidth = ClientProxy.Font.width(argTextOnLine);
}
if (argWidth > 0) {
drawCurlyUnderline(lineStartX + beforeWidth, baselineY, argWidth, 0xFF5555);
}
}
}
}
// Check all field accesses in the document
for (FieldAccessInfo access : doc.getFieldAccesses()) {
// Skip if this access doesn't intersect this line
int fieldStart = access.getFieldNameStart();
int fieldEnd = access.getFieldNameEnd();
if (fieldEnd < lineStart || fieldStart > lineEnd)
continue;
// Handle type mismatch errors (underline the field name)
if (access.hasTypeMismatch()) {
// Check if field name is on this line
if (fieldStart >= lineStart && fieldStart < lineEnd) {
int lineLocalStart = fieldStart - lineStart;
if (lineLocalStart >= 0 && lineLocalStart < lineText.length()) {
String beforeField = lineText.substring(0, lineLocalStart);
int beforeWidth = ClientProxy.Font.width(beforeField);
int fieldWidth = ClientProxy.Font.width(access.getFieldName());
// drawCurlyUnderline(lineStartX + beforeWidth, baselineY, fieldWidth, 0xFF5555);
}
}
}
}
// Check all errored assignments in the document (stored in FieldInfo objects)
for (AssignmentInfo assign : doc.getAllErroredAssignments()) {
// Determine what to underline based on error type
// LHS errors (final reassignment, access errors) underline the LHS
// RHS errors (type mismatch) underline the RHS
int underlineStart, underlineEnd;
String underlineText;
if (assign.isLhsError()) {
// Underline the LHS (target variable)
underlineStart = assign.getLhsStart();
underlineEnd = assign.getLhsEnd();
underlineText = assign.getTargetName();
} else if (assign.isRhsError()) {
// Underline the RHS (source expression)
underlineStart = assign.getRhsStart();
underlineEnd = assign.getRhsEnd();
underlineText = assign.getSourceExpr();
} else if (assign.isFullLineError()) {
underlineStart = assign.getStatementStart();
underlineEnd = assign.getRhsEnd();
} else {
// Skip assignments with no displayable error
continue;
}
// Skip if this doesn't intersect this line
if (underlineEnd < lineStart || underlineStart > lineEnd)
continue;
// Clip to line boundaries
int clipStart = Math.max(underlineStart, lineStart);
int clipEnd = Math.min(underlineEnd, lineEnd);
if (clipStart >= clipEnd)
continue;
// Convert to line-local coordinates
int lineLocalStart = clipStart - lineStart;
int lineLocalEnd = clipEnd - lineStart;
// Bounds check
if (lineLocalStart < 0 || lineLocalStart >= lineText.length())
continue;
// Compute pixel position
String beforeUnderline = lineText.substring(0, lineLocalStart);
int beforeWidth = ClientProxy.Font.width(beforeUnderline);
int underlineWidth;
if (lineLocalEnd > lineText.length()) {
// Extends past line end
underlineWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart));
} else {
String underlineOnLine = lineText.substring(lineLocalStart, lineLocalEnd);
underlineWidth = ClientProxy.Font.width(underlineOnLine);
}
if (underlineWidth > 0) {
drawCurlyUnderline(lineStartX + beforeWidth, baselineY, underlineWidth, 0xFF5555);
}
}
// Check all method declarations for errors (missing return, parameter errors, return type errors)
for (MethodInfo method : doc.getMethods()) {
if (!method.isDeclaration() || !method.hasError())
continue;
// Handle return statement type errors (underline the entire return statement)
if (method.hasReturnStatementErrors()) {
for (MethodInfo.ReturnStatementError returnError : method.getReturnStatementErrors()) {
int returnStart = returnError.getStartOffset();
int returnEnd = returnError.getEndOffset();
//error 247, 258
//line 235, 259
// Skip if return statement doesn't intersect this line
if (returnEnd < lineStart || returnStart > lineEnd)
continue;
// Clip to line boundaries
int clipStart = Math.max(returnStart, lineStart);
int clipEnd = Math.min(returnEnd, lineEnd);
if (clipStart >= clipEnd)
continue;
// Convert to line-local coordinates
int lineLocalStart = clipStart - lineStart;
int lineLocalEnd = clipEnd - lineStart;
// Bounds check
if (lineLocalStart < 0 || lineLocalStart >= lineText.length())
continue;
// Compute pixel position
String beforeReturn = lineText.substring(0, lineLocalStart);
int beforeWidth = ClientProxy.Font.width(beforeReturn);
int returnWidth;
if (lineLocalEnd > lineText.length()) {
returnWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart));
} else {
String returnTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd);
returnWidth = ClientProxy.Font.width(returnTextOnLine);
}
if (returnWidth > 0) {
drawCurlyUnderline(lineStartX + beforeWidth, baselineY, returnWidth, 0xFF5555);
}
}
}
// Handle missing return error (underline the method name)
if (method.hasMissingReturnError()) {
int methodNameStart = method.getNameOffset();
int methodNameEnd = methodNameStart + method.getName().length();
// Skip if method name doesn't intersect this line
if (methodNameEnd < lineStart || methodNameStart > lineEnd)
continue;
// Check if method name is on this line
if (methodNameStart >= lineStart && methodNameStart < lineEnd) {
int lineLocalStart = methodNameStart - lineStart;
if (lineLocalStart >= 0 && lineLocalStart < lineText.length()) {
String beforeMethod = lineText.substring(0, lineLocalStart);
int beforeWidth = ClientProxy.Font.width(beforeMethod);
int methodWidth = ClientProxy.Font.width(method.getName());
drawCurlyUnderline(lineStartX + beforeWidth, baselineY, methodWidth, 0xFF5555);
}
}
}
// Handle duplicate method error (underline from full declaration start to closing paren)
if (method.getErrorType() == MethodInfo.ErrorType.DUPLICATE_METHOD) {
int declStart = method.getFullDeclarationOffset();
int declEnd = method.getDeclarationEnd();
// Skip if declaration doesn't intersect this line
if (declEnd < lineStart || declStart > lineEnd)
continue;
// Clip to line boundaries
int clipStart = Math.max(declStart, lineStart);
int clipEnd = Math.min(declEnd, lineEnd);
if (clipStart >= clipEnd)
continue;
// Convert to line-local coordinates
int lineLocalStart = clipStart - lineStart;
int lineLocalEnd = clipEnd - lineStart;
// Bounds check
if (lineLocalStart < 0 || lineLocalStart >= lineText.length())
continue;
// Compute pixel position
String beforeDecl = lineText.substring(0, lineLocalStart);
int beforeWidth = ClientProxy.Font.width(beforeDecl);
int declWidth;
if (lineLocalEnd > lineText.length()) {
declWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart));
} else {
String declTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd);
declWidth = ClientProxy.Font.width(declTextOnLine);
}
if (declWidth > 0) {
drawCurlyUnderline(lineStartX + beforeWidth, baselineY, declWidth, 0xFF5555);
}
}
// Handle parameter errors (underline specific parameters)
if (method.hasParameterErrors()) {
for (MethodInfo.ParameterError paramError : method.getParameterErrors()) {
FieldInfo param = paramError.getParameter();
if (param == null || param.getDeclarationOffset() < 0)
continue;
int paramStart = param.getDeclarationOffset();
int paramEnd = paramStart + param.getName().length();
// Skip if parameter doesn't intersect this line
if (paramEnd < lineStart || paramStart > lineEnd)
continue;
// Clip to line boundaries
int clipStart = Math.max(paramStart, lineStart);
int clipEnd = Math.min(paramEnd, lineEnd);
if (clipStart >= clipEnd)
continue;
// Convert to line-local coordinates
int lineLocalStart = clipStart - lineStart;
int lineLocalEnd = clipEnd - lineStart;
// Bounds check
if (lineLocalStart < 0 || lineLocalStart >= lineText.length())
continue;
// Compute pixel position
String beforeParam = lineText.substring(0, lineLocalStart);
int beforeWidth = ClientProxy.Font.width(beforeParam);
int paramWidth;
if (lineLocalEnd > lineText.length()) {
paramWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart));
} else {
String paramTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd);
paramWidth = ClientProxy.Font.width(paramTextOnLine);
}
if (paramWidth > 0) {
drawCurlyUnderline(lineStartX + beforeWidth, baselineY, paramWidth, 0xFF5555);
}
}
}
}
}
/**
* Draw a curly/wavy underline (like IDE error highlighting).
* @param x Start X position
* @param y Y position (bottom of text)
* @param width Width of the underline
* @param color Color in ARGB format (e.g., 0xFFFF5555 for red)
*/
protected static void drawCurlyUnderline(int x, int y, int width, int color) {
if (width <= 0)
return;
float a = ((color >> 24) & 0xFF) / 255f;
float r = ((color >> 16) & 0xFF) / 255f;
float g = ((color >> 8) & 0xFF) / 255f;
float b = (color & 0xFF) / 255f;
// If alpha is 0, assume full opacity
if (a == 0)
a = 1.0f;
GL11.glPushMatrix();
GL11.glDisable(GL11.GL_TEXTURE_2D);
GL11.glEnable(GL11.GL_BLEND);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GL11.glColor4f(r, g, b, a);
GL11.glLineWidth(1.0f);
GL11.glBegin(GL11.GL_LINE_STRIP);
// Wave parameters: 2 pixels amplitude, 4 pixels wavelength
int waveHeight = 1;
float waveLength = 4f;
for (float i = -0.5f; i <= width - 1; i += 0.125f) {
// Create a sine-like wave pattern
double phase = (double) i / waveLength * Math.PI * 2;
float yOffset = (float) (Math.sin(phase) * waveHeight) - 0.25f;
GL11.glVertex2f(x + i + 2f, y + yOffset);
}
GL11.glEnd();
GL11.glEnable(GL11.GL_TEXTURE_2D);
GL11.glPopMatrix();
ErrorUnderlineRenderer.drawErrorUnderlines(
parent,
lineStartX,
baselineY,
getText(),
getGlobalStart(),
getGlobalEnd()
);
}
/**