diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 2950b08f9..e14995162 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1615,11 +1615,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } - // Generate properly indented javadoc block - String javadocStub = "\n" + indent + " * \n" + indent + " */"; + // Look ahead for a function declaration to generate smart JSDoc + String after = getSelectionAfterText(); + String javadocStub = generateJSDocStub(after, indent); addText(javadocStub); - // Position cursor after " * " on the middle line + // Position cursor after " * " on the description line int newCursorPos = before.length() + 1 + indent.length() + 3; // +1 for \n, +3 for " * " selection.reset(newCursorPos); scrollToCursor(); @@ -1702,6 +1703,98 @@ public class GuiScriptTextArea extends GuiNpcTextField { return false; } + + /** + * Generates a JSDoc stub for a function declaration. + * Looks at the text after the cursor to find function signature and generates + * appropriate @param and @return tags. + * + * @param after Text after the /** to search for function + * @param indent Current line indentation + * @return The JSDoc stub string including newlines + */ + private String generateJSDocStub(String after, String indent) { + StringBuilder stub = new StringBuilder(); + stub.append("\n").append(indent).append(" * "); // Description line + + // Try to find a function/method declaration following the JSDoc + // Patterns: + // Java: [modifiers] ReturnType methodName(params) { + // JS: function funcName(params) { or var funcName = function(params) { or funcName(params) { + + // Skip whitespace and newlines + String trimmed = after.replaceFirst("^[\\s\\n\\r]*", ""); + + // Pattern for Java/JS method/function + java.util.regex.Pattern funcPattern = java.util.regex.Pattern.compile( + // Group 1: Optional return type (Java) or 'function' keyword (JS) + "^(?:(\\w+(?:<[^>]+>)?|function)\\s+)?" + + // Group 2: Method/function name + "(\\w+)\\s*" + + // Group 3: Parameters inside parentheses + "\\(([^)]*)\\)" + ); + + java.util.regex.Matcher m = funcPattern.matcher(trimmed); + if (m.find()) { + String returnOrKeyword = m.group(1); + String funcName = m.group(2); + String paramsStr = m.group(3) != null ? m.group(3).trim() : ""; + + // Determine if this is JavaScript (function keyword or no return type) + boolean isJS = "function".equals(returnOrKeyword) || returnOrKeyword == null || + (container != null && container.getDocument() != null && container.getDocument().isJavaScript()); + + // Parse parameters + if (!paramsStr.isEmpty()) { + String[] params = paramsStr.split(","); + for (String param : params) { + param = param.trim(); + if (param.isEmpty()) continue; + + String paramName; + String paramType = "any"; + + // Split on whitespace to get type and name + String[] parts = param.split("\\s+"); + if (parts.length >= 2) { + // Java style: Type name + paramType = parts[parts.length - 2]; + paramName = parts[parts.length - 1]; + } else { + // JS style: just name + paramName = parts[0]; + } + + // Remove any trailing array brackets or varargs + paramName = paramName.replaceAll("[\\[\\]\\.]", ""); + + if (isJS) { + stub.append("\n").append(indent).append(" * @param {").append(paramType).append("} ").append(paramName); + } else { + stub.append("\n").append(indent).append(" * @param ").append(paramName); + } + } + } + + // Add @return/@returns tag if applicable + if (returnOrKeyword != null && !"function".equals(returnOrKeyword) && !"void".equals(returnOrKeyword)) { + if (isJS) { + stub.append("\n").append(indent).append(" * @returns {").append(returnOrKeyword).append("}"); + } else { + stub.append("\n").append(indent).append(" * @return"); + } + } else if (isJS) { + // For JS functions without explicit return type, add empty @returns + stub.append("\n").append(indent).append(" * @returns {any}"); + } + } + + // Close the JSDoc block + stub.append("\n").append(indent).append(" */"); + + return stub.toString(); + } /** * Handles deletion keys: Delete, Backspace, and Ctrl+Backspace. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java index 04777d514..12439e28b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java @@ -8,23 +8,18 @@ import noppes.npcs.client.gui.util.script.interpreter.field.AssignmentInfo; import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; -import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo; -import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSScriptAnalyzer; -import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParamTag; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParser; import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodSignature; import noppes.npcs.client.gui.util.script.interpreter.token.Token; import noppes.npcs.client.gui.util.script.interpreter.token.TokenErrorMessage; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; -import noppes.npcs.client.gui.util.script.interpreter.type.ImportData; -import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo; -import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; -import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; -import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; -import scala.annotation.meta.field; +import noppes.npcs.client.gui.util.script.interpreter.type.*; import java.util.*; import java.util.regex.Matcher; @@ -121,6 +116,9 @@ public class ScriptDocument { // Type resolver private final TypeResolver typeResolver; + // JSDoc parser for extracting type information from JSDoc comments + private final JSDocParser jsDocParser = new JSDocParser(this); + // Script language: "ECMAScript", "Groovy", etc. private String language = "ECMAScript"; @@ -678,12 +676,15 @@ public class ScriptDocument { int bodyEnd = findMatchingBrace(bodyStart); if (bodyEnd < 0) bodyEnd = text.length(); + // Extract documentation before this method + String documentation = extractDocumentationBefore(m.start()); + + // Check for JSDoc before the function declaration + JSDocInfo jsDoc = jsDocParser.extractJSDocBefore(text, m.start()); + // For JS hooks, infer parameter types from registry List params = new ArrayList<>(); TypeInfo returnType = TypeInfo.fromPrimitive("void"); - // Extract documentation before this method - String documentation = extractDocumentationBefore(m.start()); - if (typeResolver.isJSHook(funcName)) { List sigs = typeResolver.getJSHookSignatures(funcName); @@ -703,17 +704,32 @@ public class ScriptDocument { } } } else { - // Non-hook function - params with 'any' type + // Non-hook function - use JSDoc param types if available, else 'any' type if (paramList != null && !paramList.trim().isEmpty()) { int paramOffset = m.start(2); for (String p : paramList.split(",")) { String pn = p.trim(); if (!pn.isEmpty()) { int paramStart = paramOffset + paramList.indexOf(pn); - params.add(FieldInfo.parameter(pn, TypeInfo.fromClass(Object.class), paramStart, null)); + + // Check if JSDoc has a @param tag for this parameter + TypeInfo paramType = TypeInfo.fromClass(Object.class); + if (jsDoc != null) { + JSDocParamTag paramTag = jsDoc.getParamTag(pn); + if (paramTag != null && paramTag.getTypeInfo() != null) { + paramType = paramTag.getTypeInfo(); + } + } + + params.add(FieldInfo.parameter(pn, paramType, paramStart, null)); } } } + + // Use JSDoc @return type if available + if (jsDoc != null && jsDoc.hasReturnTag() && jsDoc.getReturnType() != null) { + returnType = jsDoc.getReturnType(); + } } // Create MethodInfo and add to shared methods list @@ -986,13 +1002,24 @@ public class ScriptDocument { String varName = m.group(1); String initializer = m.group(3); - // Infer type from initializer using resolveExpressionType + // Check for JSDoc type annotation before the declaration + int absStart = bodyStart + m.start(); + JSDocInfo jsDoc = jsDocParser.extractJSDocBefore(text, absStart); + TypeInfo typeInfo = null; - if (initializer != null && !initializer.trim().isEmpty()) { + + // Priority 1: JSDoc @type takes precedence + if (jsDoc != null && jsDoc.hasTypeTag()) { + typeInfo = jsDoc.getDeclaredType(); + } + + // Priority 2: Infer type from initializer if no JSDoc type + if (typeInfo == null && initializer != null && !initializer.trim().isEmpty()) { typeInfo = resolveExpressionType(initializer.trim(), bodyStart + m.start(3)); } + + // Priority 3: Use "any" type for uninitialized variables if (typeInfo == null) { - // Use "any" type for uninitialized variables in JavaScript typeInfo = TypeInfo.ANY; } @@ -1292,13 +1319,24 @@ public class ScriptDocument { String varName = m.group(1); String initializer = m.group(3); - // Infer type from initializer + // First, check for JSDoc type annotation + String documentation = extractDocumentationBefore(m.start()); + JSDocInfo jsDoc = jsDocParser.extractJSDocBefore(text, m.start()); + TypeInfo typeInfo = null; - if (initializer != null && !initializer.trim().isEmpty()) { + + // Priority 1: JSDoc @type takes precedence + if (jsDoc != null && jsDoc.hasTypeTag()) { + typeInfo = jsDoc.getDeclaredType(); + } + + // Priority 2: Infer from initializer if no JSDoc type + if (typeInfo == null && initializer != null && !initializer.trim().isEmpty()) { typeInfo = resolveExpressionType(initializer.trim(), m.start(3)); } + + // Priority 3: Use "any" type for uninitialized variables if (typeInfo == null) { - // Use "any" type for uninitialized variables in JavaScript typeInfo = TypeInfo.ANY; } @@ -1309,7 +1347,6 @@ public class ScriptDocument { initEnd = m.end(3); } - String documentation = extractDocumentationBefore(m.start()); FieldInfo fieldInfo = FieldInfo.globalField(varName, typeInfo, position, documentation, initStart, initEnd, 0); if (globalFields.containsKey(varName)) { @@ -1702,10 +1739,15 @@ public class ScriptDocument { private List buildMarks() { List marks = new ArrayList<>(); - // Comments and strings first (highest priority) - same for both languages - addPatternMarks(marks, COMMENT_PATTERN, TokenType.COMMENT); + // Strings first to protect their content addPatternMarks(marks, STRING_PATTERN, TokenType.STRING); + // JSDoc comments with fragmented marking (avoids conflicts with @tags and {Type}) + markJSDocElements(marks); + + // Regular comments (non-JSDoc) + markNonJSDocComments(marks); + // Keywords - same for both languages (KEYWORD_PATTERN includes JS keywords like function, var, let, const) addPatternMarks(marks, KEYWORD_PATTERN, TokenType.KEYWORD); @@ -2310,6 +2352,149 @@ public class ScriptDocument { } } + /** + * Mark JSDoc elements within comments for syntax highlighting. + * Adds marks for @tags (like @param, @return, @type) and {Type} references. + */ + private void markJSDocElements(List marks) { + // Find all JSDoc comments (/** ... */) + Pattern jsDocPattern = Pattern.compile("/\\*\\*([\\s\\S]*?)\\*/"); + Matcher jsDocMatcher = jsDocPattern.matcher(text); + + while (jsDocMatcher.find()) { + int commentStart = jsDocMatcher.start(); + int commentEnd = jsDocMatcher.end(); + + // Skip if this JSDoc is inside a string + boolean insideString = false; + for (ScriptLine.Mark mark : marks) { + if (mark.type == TokenType.STRING && + commentStart >= mark.start && commentEnd <= mark.end) { + insideString = true; + break; + } + } + if (insideString) { + continue; + } + + String commentContent = jsDocMatcher.group(0); + + // Collect all special element positions (@tags and {Type}s) that should NOT be gray + java.util.List specialRanges = new java.util.ArrayList<>(); + + // Find @tags + Pattern tagPattern = Pattern.compile("@(\\w+)"); + Matcher tagMatcher = tagPattern.matcher(commentContent); + while (tagMatcher.find()) { + int tagStart = commentStart + tagMatcher.start(); + int tagEnd = commentStart + tagMatcher.end(); + specialRanges.add(new int[]{tagStart, tagEnd}); + // Mark the @tag itself + marks.add(new ScriptLine.Mark(tagStart, tagEnd, TokenType.JSDOC_TAG, null)); + } + + // Find {Type} references + Pattern typePattern = Pattern.compile("\\{([^}]+)\\}"); + Matcher typeMatcher = typePattern.matcher(commentContent); + while (typeMatcher.find()) { + int braceStart = commentStart + typeMatcher.start(); + int braceEnd = commentStart + typeMatcher.end(); + specialRanges.add(new int[]{braceStart, braceEnd}); + + // Mark braces + marks.add(new ScriptLine.Mark(braceStart, braceStart + 1, TokenType.JSDOC_TYPE, null)); // { + marks.add(new ScriptLine.Mark(braceEnd - 1, braceEnd, TokenType.JSDOC_TYPE, null)); // } + + // Mark the type name inside braces + String typeName = typeMatcher.group(1).trim(); + int typeStart = commentStart + typeMatcher.start(1); + int typeEnd = commentStart + typeMatcher.end(1); + + // Try to resolve the type for hover info + TypeInfo resolvedType = resolveType(typeName); + marks.add(new ScriptLine.Mark(typeStart, typeEnd, resolvedType != null? resolvedType.getTokenType() : TokenType.UNDEFINED_VAR, resolvedType)); + } + + // Sort special ranges by start position + specialRanges.sort((a, b) -> Integer.compare(a[0], b[0])); + + // Mark the comment in FRAGMENTS between special elements + int lastPos = commentStart; + for (int[] range : specialRanges) { + // Mark from lastPos to start of special element + if (lastPos < range[0]) { + marks.add(new ScriptLine.Mark(lastPos, range[0], TokenType.COMMENT, null)); + } + lastPos = range[1]; // Move past the special element + } + // Mark from last special element to end of comment + if (lastPos < commentEnd) { + marks.add(new ScriptLine.Mark(lastPos, commentEnd, TokenType.COMMENT, null)); + } + } + } + + /** + * Mark all comments EXCEPT JSDoc comments (slash-star-star style). + * JSDoc comments are handled separately in markJSDocElements. + */ + private void markNonJSDocComments(List marks) { + // Match: /* ... */ (but not /**), // ..., # ... + Matcher m = COMMENT_PATTERN.matcher(text); + while (m.find()) { + int start = m.start(); + int end = m.end(); + String comment = m.group(); + + // Skip JSDoc comments (/** style) - they're handled separately + if (comment.startsWith("/**")) { + continue; + } + + marks.add(new ScriptLine.Mark(start, end, TokenType.COMMENT, null)); + } + } + + /** + * Resolve a type name from JSDoc to a TypeInfo. + */ + private TypeInfo resolveJSDocType(String typeName) { + if (typeName == null || typeName.isEmpty()) { + return null; + } + + // Handle union types - use first type + if (typeName.contains("|")) { + typeName = typeName.split("\\|")[0].trim(); + } + + // Handle nullable + typeName = typeName.replace("?", "").trim(); + + // Map JSDoc types to Java types + switch (typeName.toLowerCase()) { + case "string": + return TypeInfo.fromClass(String.class); + case "number": + case "int": + case "integer": + return isJavaScript() ? TypeInfo.fromClass(double.class) : TypeInfo.fromClass(int.class); + case "boolean": + case "bool": + return TypeInfo.fromClass(boolean.class); + case "void": + return TypeInfo.fromPrimitive("void"); + case "any": + case "object": + case "*": + return TypeInfo.ANY; + } + + // Try through the type resolver + return resolveType(typeName); + } + private void markMethodDeclarations(List marks) { Matcher m = METHOD_DECL_PATTERN.matcher(text); while (m.find()) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java new file mode 100644 index 000000000..1cd0050b4 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java @@ -0,0 +1,98 @@ +package noppes.npcs.client.gui.util.script.interpreter.jsdoc; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents parsed JSDoc comment information. + * Captures @type, @param, @return, and other JSDoc tags with their type information. + */ +public class JSDocInfo { + + private final String rawComment; + private final int startOffset; + private final int endOffset; + + private JSDocTypeTag typeTag; + private final List paramTags = new ArrayList<>(); + private JSDocReturnTag returnTag; + private String description; + private final List allTags = new ArrayList<>(); + + public JSDocInfo(String rawComment, int startOffset, int endOffset) { + this.rawComment = rawComment; + this.startOffset = startOffset; + this.endOffset = endOffset; + } + + public void setTypeTag(JSDocTypeTag typeTag) { + this.typeTag = typeTag; + allTags.add(typeTag); + } + + public void addParamTag(JSDocParamTag paramTag) { + this.paramTags.add(paramTag); + allTags.add(paramTag); + } + + public void setReturnTag(JSDocReturnTag returnTag) { + this.returnTag = returnTag; + allTags.add(returnTag); + } + + public void setDescription(String description) { + this.description = description; + } + + public void addTag(JSDocTag tag) { + allTags.add(tag); + } + + public String getRawComment() { return rawComment; } + public int getStartOffset() { return startOffset; } + public int getEndOffset() { return endOffset; } + public JSDocTypeTag getTypeTag() { return typeTag; } + public boolean hasTypeTag() { return typeTag != null; } + + public TypeInfo getDeclaredType() { + return typeTag != null ? typeTag.getTypeInfo() : null; + } + + public List getParamTags() { + return Collections.unmodifiableList(paramTags); + } + + public boolean hasParamTags() { return !paramTags.isEmpty(); } + + public JSDocParamTag getParamTag(String paramName) { + for (JSDocParamTag tag : paramTags) { + if (tag.getParamName().equals(paramName)) { + return tag; + } + } + return null; + } + + public JSDocReturnTag getReturnTag() { return returnTag; } + public boolean hasReturnTag() { return returnTag != null; } + + public TypeInfo getReturnType() { + return returnTag != null ? returnTag.getTypeInfo() : null; + } + + public String getDescription() { return description; } + public List getAllTags() { return Collections.unmodifiableList(allTags); } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("JSDocInfo{"); + if (typeTag != null) sb.append("type=").append(typeTag.getTypeName()); + if (!paramTags.isEmpty()) sb.append(", params=").append(paramTags.size()); + if (returnTag != null) sb.append(", return=").append(returnTag.getTypeName()); + sb.append("}"); + return sb.toString(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParamTag.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParamTag.java new file mode 100644 index 000000000..cdabdb717 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParamTag.java @@ -0,0 +1,43 @@ +package noppes.npcs.client.gui.util.script.interpreter.jsdoc; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +/** + * Represents an @param JSDoc tag. + * Used to declare the type and description of a function parameter. + * Example: @param {string} name - The name of the person + */ +public class JSDocParamTag extends JSDocTag { + + private final String paramName; + private final int paramNameStart; + private final int paramNameEnd; + + public JSDocParamTag(int atSignOffset, int tagNameStart, int tagNameEnd, + String paramName, int paramNameStart, int paramNameEnd) { + super("param", atSignOffset, tagNameStart, tagNameEnd); + this.paramName = paramName; + this.paramNameStart = paramNameStart; + this.paramNameEnd = paramNameEnd; + } + + public static JSDocParamTag create(int atSignOffset, int tagNameStart, int tagNameEnd, + String typeName, TypeInfo typeInfo, int typeStart, int typeEnd, + String paramName, int paramNameStart, int paramNameEnd, + String description) { + JSDocParamTag tag = new JSDocParamTag(atSignOffset, tagNameStart, tagNameEnd, + paramName, paramNameStart, paramNameEnd); + tag.setType(typeName, typeInfo, typeStart, typeEnd); + tag.setDescription(description); + return tag; + } + + public String getParamName() { return paramName; } + public int getParamNameStart() { return paramNameStart; } + public int getParamNameEnd() { return paramNameEnd; } + + @Override + public String toString() { + return "JSDocParamTag{@param " + (typeName != null ? "{" + typeName + "} " : "") + paramName + "}"; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java new file mode 100644 index 000000000..e93ed12eb --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java @@ -0,0 +1,217 @@ +package noppes.npcs.client.gui.util.script.interpreter.jsdoc; + +import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parses JSDoc comments to extract type information. + * Handles @type, @param, @return/@returns tags. + */ +public class JSDocParser { + + private static final Pattern JSDOC_PATTERN = Pattern.compile( + "/\\*\\*([\\s\\S]*?)\\*/", Pattern.MULTILINE); + + private static final Pattern TYPE_TAG_PATTERN = Pattern.compile( + "@(type)\\s*\\{([^}]+)\\}", Pattern.CASE_INSENSITIVE); + + private static final Pattern PARAM_TAG_PATTERN = Pattern.compile( + "@(param)\\s*(?:\\{([^}]+)\\})?\\s*(\\w+)(?:\\s*-?\\s*(.*))?", Pattern.CASE_INSENSITIVE); + + private static final Pattern RETURN_TAG_PATTERN = Pattern.compile( + "@(returns?)\\s*(?:\\{([^}]+)\\})?(?:\\s*-?\\s*(.*))?", Pattern.CASE_INSENSITIVE); + + private static final Pattern ANY_TAG_PATTERN = Pattern.compile( + "@(\\w+)", Pattern.CASE_INSENSITIVE); + + private final ScriptDocument document; + + public JSDocParser(ScriptDocument typeResolver) { + this.document = typeResolver; + } + + public JSDocInfo parse(String comment, int commentStart) { + if (comment == null || !comment.startsWith("/**")) { + return null; + } + + int commentEnd = commentStart + comment.length(); + JSDocInfo info = new JSDocInfo(comment, commentStart, commentEnd); + + String content = comment.substring(3); + if (content.endsWith("*/")) { + content = content.substring(0, content.length() - 2); + } + + int contentOffset = commentStart + 3; + + parseTypeTag(content, contentOffset, info); + parseParamTags(content, contentOffset, info); + parseReturnTag(content, contentOffset, info); + extractDescription(content, info); + + return info; + } + + public JSDocInfo extractJSDocBefore(String source, int position) { + if (source == null || position <= 0) { + return null; + } + + int searchStart = Math.max(0, position - 1); + + while (searchStart > 0 && Character.isWhitespace(source.charAt(searchStart))) { + searchStart--; + } + + int endCommentPos = -1; + if (searchStart >= 1 && source.charAt(searchStart) == '/' && source.charAt(searchStart - 1) == '*') { + endCommentPos = searchStart; + } else if (searchStart >= 1 && source.charAt(searchStart - 1) == '/' && searchStart > 1 && source.charAt( + searchStart - 2) == '*') { + endCommentPos = searchStart - 1; + } + + if (endCommentPos < 0) { + return null; + } + + int startCommentPos = -1; + for (int i = endCommentPos - 1; i >= 2; i--) { + if (source.charAt(i) == '*' && source.charAt(i - 1) == '*' && source.charAt(i - 2) == '/') { + startCommentPos = i - 2; + break; + } + } + + if (startCommentPos < 0) { + return null; + } + + String comment = source.substring(startCommentPos, endCommentPos + 1); + return parse(comment, startCommentPos); + } + + private void parseTypeTag(String content, int contentOffset, JSDocInfo info) { + Matcher m = TYPE_TAG_PATTERN.matcher(content); + if (m.find()) { + int atSignOffset = contentOffset + m.start(); + int tagNameStart = contentOffset + m.start(1); + int tagNameEnd = contentOffset + m.end(1); + + String typeName = m.group(2).trim(); + + int braceStart = content.indexOf('{', m.start()); + int braceEnd = content.indexOf('}', braceStart); + int typeStart = contentOffset + braceStart + 1; + int typeEnd = contentOffset + braceEnd; + + TypeInfo typeInfo = resolveType(typeName); + + JSDocTypeTag tag = JSDocTypeTag.create(atSignOffset, tagNameStart, tagNameEnd, + typeName, typeInfo, typeStart, typeEnd); + info.setTypeTag(tag); + } + } + + private void parseParamTags(String content, int contentOffset, JSDocInfo info) { + Matcher m = PARAM_TAG_PATTERN.matcher(content); + while (m.find()) { + int atSignOffset = contentOffset + m.start(); + int tagNameStart = contentOffset + m.start(1); + int tagNameEnd = contentOffset + m.end(1); + + String typeName = m.group(2); + String paramName = m.group(3); + String description = m.group(4); + + int typeStart = -1; + int typeEnd = -1; + TypeInfo typeInfo = null; + + if (typeName != null) { + typeName = typeName.trim(); + int braceStart = content.indexOf('{', m.start()); + int braceEnd = content.indexOf('}', braceStart); + typeStart = contentOffset + braceStart + 1; + typeEnd = contentOffset + braceEnd; + typeInfo = resolveType(typeName); + } + + int paramNameStart = contentOffset + m.start(3); + int paramNameEnd = contentOffset + m.end(3); + + JSDocParamTag tag = JSDocParamTag.create(atSignOffset, tagNameStart, tagNameEnd, + typeName, typeInfo, typeStart, typeEnd, + paramName, paramNameStart, paramNameEnd, + description != null ? description.trim() : null); + info.addParamTag(tag); + } + } + + private void parseReturnTag(String content, int contentOffset, JSDocInfo info) { + Matcher m = RETURN_TAG_PATTERN.matcher(content); + if (m.find()) { + String tagName = m.group(1); + int atSignOffset = contentOffset + m.start(); + int tagNameStart = contentOffset + m.start(1); + int tagNameEnd = contentOffset + m.end(1); + + String typeName = m.group(2); + String description = m.group(3); + + int typeStart = -1; + int typeEnd = -1; + TypeInfo typeInfo = null; + + if (typeName != null) { + typeName = typeName.trim(); + int braceStart = content.indexOf('{', m.start()); + int braceEnd = content.indexOf('}', braceStart); + typeStart = contentOffset + braceStart + 1; + typeEnd = contentOffset + braceEnd; + typeInfo = resolveType(typeName); + } + + JSDocReturnTag tag = JSDocReturnTag.create(tagName, atSignOffset, tagNameStart, tagNameEnd, + typeName, typeInfo, typeStart, typeEnd, + description != null ? description.trim() : null); + info.setReturnTag(tag); + } + } + + private void extractDescription(String content, JSDocInfo info) { + Matcher m = ANY_TAG_PATTERN.matcher(content); + String description; + + if (m.find()) { + description = content.substring(0, m.start()); + } else { + description = content; + } + + description = description.replaceAll("(?m)^\\s*\\*\\s?", "").trim(); + + if (!description.isEmpty()) { + info.setDescription(description); + } + } + + private TypeInfo resolveType(String typeName) { + return document.resolveType(typeName); + } + + public static List findAllJSDocComments(String source) { + List positions = new ArrayList<>(); + Matcher m = JSDOC_PATTERN.matcher(source); + while (m.find()) { + positions.add(new int[]{m.start(), m.end()}); + } + return positions; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocReturnTag.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocReturnTag.java new file mode 100644 index 000000000..0f5fca416 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocReturnTag.java @@ -0,0 +1,29 @@ +package noppes.npcs.client.gui.util.script.interpreter.jsdoc; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +/** + * Represents an @return or @returns JSDoc tag. + * Used to declare the return type of a function. + * Example: @return {number} The calculated result + */ +public class JSDocReturnTag extends JSDocTag { + + public JSDocReturnTag(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd) { + super(tagName, atSignOffset, tagNameStart, tagNameEnd); + } + + public static JSDocReturnTag create(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, + String typeName, TypeInfo typeInfo, int typeStart, int typeEnd, + String description) { + JSDocReturnTag tag = new JSDocReturnTag(tagName, atSignOffset, tagNameStart, tagNameEnd); + tag.setType(typeName, typeInfo, typeStart, typeEnd); + tag.setDescription(description); + return tag; + } + + @Override + public String toString() { + return "JSDocReturnTag{@" + tagName + " " + (typeName != null ? "{" + typeName + "}" : "") + "}"; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTag.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTag.java new file mode 100644 index 000000000..82fd1edbe --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTag.java @@ -0,0 +1,55 @@ +package noppes.npcs.client.gui.util.script.interpreter.jsdoc; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +/** + * Base class for JSDoc tags. + * Each tag stores its position information for syntax highlighting. + */ +public class JSDocTag { + + protected final String tagName; + protected final int atSignOffset; + protected final int tagNameStart; + protected final int tagNameEnd; + + protected String typeName; + protected TypeInfo typeInfo; + protected int typeStart = -1; + protected int typeEnd = -1; + protected String description; + + public JSDocTag(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd) { + this.tagName = tagName; + this.atSignOffset = atSignOffset; + this.tagNameStart = tagNameStart; + this.tagNameEnd = tagNameEnd; + } + + public void setType(String typeName, TypeInfo typeInfo, int typeStart, int typeEnd) { + this.typeName = typeName; + this.typeInfo = typeInfo; + this.typeStart = typeStart; + this.typeEnd = typeEnd; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getTagName() { return tagName; } + public int getAtSignOffset() { return atSignOffset; } + public int getTagNameStart() { return tagNameStart; } + public int getTagNameEnd() { return tagNameEnd; } + public String getTypeName() { return typeName; } + public TypeInfo getTypeInfo() { return typeInfo; } + public boolean hasType() { return typeName != null && !typeName.isEmpty(); } + public int getTypeStart() { return typeStart; } + public int getTypeEnd() { return typeEnd; } + public String getDescription() { return description; } + + @Override + public String toString() { + return "JSDocTag{@" + tagName + (typeName != null ? " {" + typeName + "}" : "") + "}"; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTypeTag.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTypeTag.java new file mode 100644 index 000000000..0ec2c1389 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTypeTag.java @@ -0,0 +1,27 @@ +package noppes.npcs.client.gui.util.script.interpreter.jsdoc; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +/** + * Represents an @type JSDoc tag. + * Used to declare the type of a variable. + * Example: @type {string} + */ +public class JSDocTypeTag extends JSDocTag { + + public JSDocTypeTag(int atSignOffset, int tagNameStart, int tagNameEnd) { + super("type", atSignOffset, tagNameStart, tagNameEnd); + } + + public static JSDocTypeTag create(int atSignOffset, int tagNameStart, int tagNameEnd, + String typeName, TypeInfo typeInfo, int typeStart, int typeEnd) { + JSDocTypeTag tag = new JSDocTypeTag(atSignOffset, tagNameStart, tagNameEnd); + tag.setType(typeName, typeInfo, typeStart, typeEnd); + return tag; + } + + @Override + public String toString() { + return "JSDocTypeTag{@type " + (typeName != null ? "{" + typeName + "}" : "") + "}"; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java index c932fbfec..2cb57da62 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java @@ -9,8 +9,13 @@ import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; */ public enum TokenType { // Comments and strings have highest priority - they override everything inside them - COMMENT(0x777777, 130), - STRING(0xCC8855, 120), + COMMENT(0x777777, 140), + STRING(0xCC8855, 130), + + // JSDoc elements - lower priority but will fill gaps left by fragmented comment marking + JSDOC_TAG(0xCC9933, 125), // @param, @type, @return etc. (gold/orange) + JSDOC_TYPE(0x00AAAA, 124), // {TypeName} in JSDoc (aqua like types) + UNUSED_IMPORT(0x666666, 119), // unused import statements (gray) // Keywords and modifiers @@ -104,6 +109,10 @@ public enum TokenType { return '7'; // gray case STRING: return '5'; // purple + case JSDOC_TAG: + return '6'; // gold + case JSDOC_TYPE: + return '3'; // dark aqua (like types) case CLASS_KEYWORD: case KEYWORD: return 'c'; // red