mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-14 01:24:09 +00:00
Merge pull request #249 from KAMKEEL/syntax-highlighter-unified
Syntax Highlighter to Janino Scripts
This commit is contained in:
119 files changed
+45890
-912
No files matched your search
@@ -44,3 +44,5 @@ run
|
||||
src/main/resources/assets/customnpcs/api
|
||||
df/
|
||||
tmp/
|
||||
src/test
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# CNPC+ 1.11 Beta - The Scripting Update
|
||||
|
||||
🟡 **In Testing**
|
||||
|
||||
|
||||
|
||||
## Features: ⚒️
|
||||
|
||||
- **Syntax Highlighting:**
|
||||
|
||||
> Full color-coded script editor. Keywords, types, variables, methods, strings, and comments are all distinct. Scope-aware coloring distinguishes local fields from globals and chained method calls.
|
||||
|
||||
|
||||
|
||||
- **Autocomplete:**
|
||||
|
||||
> Smart suggestions as you type. Context-aware for static vs instance access, shows method parameters, resolves chained calls, and suggests auto imports for classes. Frequently used members prioritized.
|
||||
|
||||
|
||||
|
||||
- **Error Detection:**
|
||||
|
||||
> Catch mistakes before running. Type mismatches, wrong arguments, missing methods, unused imports, duplicate definitions, and more -- all underlined with hover messages explaining the issue.
|
||||
|
||||
|
||||
|
||||
- **Hover Information:**
|
||||
|
||||
> Hover any symbol to see its type, documentation, and declaration. Works on methods, fields, classes, and even generics. Click to pin tooltips in place.
|
||||
|
||||
|
||||
|
||||
- **Script Editor Overhaul:**
|
||||
|
||||
> - **Line Numbers** - Gutter with current line highlighting.
|
||||
> - **Go To Line** - CTRL+G to jump to any line.
|
||||
> - **Search & Replace** - CTRL+F / CTRL+H with real-time highlighting.
|
||||
> - **Go To Definition** - CTRL+Click to jump to any symbol.
|
||||
> - **Smart Undo/Redo** - Word-level instead of character-level.
|
||||
> - **Line Operations** - Duplicate, delete, move up/down.
|
||||
> - **Comment Toggle** - CTRL+/ for selected lines.
|
||||
> - **Rename** - Rename a symbol across all occurrences.
|
||||
> - **Smooth Scrolling** - Eased scroll animation.
|
||||
> - **Indent Guides** - Scope-highlighted vertical indent lines.
|
||||
> - **Brace Matching** - Highlighted pairs, red for unclosed.
|
||||
> - **Smart Brackets** - Auto-pairing for `{}`, `()`, `[]`, `""`, `''`.
|
||||
> - **Fullscreen Mode** - Expand editor to fill the game window.
|
||||
> - **Shortcut Overlay** - View all keyboard shortcuts at a glance.
|
||||
|
||||
|
||||
|
||||
- **Java Scripting (Janino):**
|
||||
|
||||
> Write scripts in real Java compiled at runtime. Name methods after hooks for automatic wiring. Language selector per tab to switch between Java and JavaScript. External `.java` file support.
|
||||
|
||||
|
||||
|
||||
- **Client-Side Scripting:**
|
||||
|
||||
> Run scripts on the client for responsive experiences. Server-controlled -- off by default, must be explicitly enabled. Script files sync automatically on login and reload.
|
||||
|
||||
|
||||
|
||||
- **Cloner Tab Overhaul:**
|
||||
|
||||
> Folder-based organization for saved NPCs, items, and entries. Full-width directory browser for large collections. Improved tab navigation.
|
||||
|
||||
|
||||
|
||||
- **Addon API Support:**
|
||||
|
||||
> Addons can ship type definitions that load into the script editor automatically. Full autocomplete, hover info, and documentation for addon APIs. Addons can extend existing CNPC+ types with new methods.
|
||||
|
||||
|
||||
|
||||
- **Animation Improvements:**
|
||||
|
||||
> Data store for passing data across animation events and frames. New consumer-based task system for animation lifecycle. Expanded scripting API for animation control.
|
||||
|
||||
|
||||
|
||||
**Extras**
|
||||
|
||||
- Player Dialog Events now work like NPC Dialog Events.
|
||||
- Attack speed configuration for linked items.
|
||||
- Client-side balance prediction during Trader interactions.
|
||||
- Right-click cycling on multi-option NPC buttons (Hair, Eyes, Fur, etc.).
|
||||
- Fixed quest cooldown for MC Custom and RL Custom timer types.
|
||||
- Fixed Bard music restarting when opening dialogs.
|
||||
- Fixed NPC biome spawn settings not saving after editing.
|
||||
- Fixed block waypoint issues.
|
||||
- Fixed script config syncing between server and client.
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
# ✏️ CustomNPC+ 1.11 - The Scripting Update ✏️
|
||||
|
||||
---
|
||||
|
||||
The **Scripting Update** completely transforms how you write scripts in CNPC+! A brand new **Script Editor** with **syntax highlighting**, **autocomplete**, and **error detection** makes scripting feel like a real IDE. Write scripts in **Java** with the new Janino engine, enable **client-side scripting** for responsive experiences, and organize your Cloner with **folders**!
|
||||
|
||||
---
|
||||
|
||||
## Script Editor 📝
|
||||
|
||||
The script editor has been rebuilt from the ground up into a **full-featured code editor** right inside Minecraft.
|
||||
|
||||
### Writing Code
|
||||
|
||||
- **Syntax Highlighting** - Keywords, types, variables, methods, strings, and comments are all color-coded automatically. The editor understands your code -- local variables, global fields, and chained method calls each get their own color
|
||||
- **Autocomplete** - Start typing and get smart suggestions for methods, fields, and classes. Suggestions know whether you're in a static or instance context, and frequently used members appear first
|
||||
- **Auto Imports** - Type a class name and the editor suggests the correct import for you
|
||||
- **Error Detection** - Catch mistakes before you even run your script! Wrong argument types, missing methods, type mismatches, unused imports, and more are all underlined with detailed error messages on hover
|
||||
|
||||
> Hover over any symbol to see its **full type information**, documentation, and declaration -- just like a desktop IDE.
|
||||
|
||||
---
|
||||
|
||||
### Navigation
|
||||
|
||||
- **Go To Line** - `CTRL+G` jumps to any line instantly
|
||||
- **Search & Replace** - `CTRL+F` to search, `CTRL+H` to replace. All matches highlight in real-time
|
||||
- **Go To Definition** - `CTRL+Click` any method, field, or type to jump straight to its definition
|
||||
- **Fullscreen Mode** - Expand the editor to fill your entire game window for maximum workspace
|
||||
|
||||
### Editing
|
||||
|
||||
- **Undo/Redo** - Smart undo that works on whole words at a time, not individual characters
|
||||
- **Line Operations** - Duplicate lines with `CTRL+D`, delete lines, and move lines up or down with shortcuts
|
||||
- **Comment Toggle** - `CTRL+/` to comment or uncomment selected lines
|
||||
- **Rename** - Rename a variable or method and every occurrence updates at once
|
||||
- **Copy Whole Line** - Copy with nothing selected grabs the entire line
|
||||
- **Triple Click** - Select an entire line instantly
|
||||
|
||||
### Visual Polish
|
||||
|
||||
- **Line Numbers** - A clean gutter with line numbers and current line highlighting
|
||||
- **Smooth Scrolling** - Eased scroll animations for a polished feel
|
||||
- **Indent Guides** - Vertical lines show your code's nesting depth, with the current scope highlighted in green
|
||||
- **Brace Matching** - Matching `{}` braces light up, and unclosed braces turn red
|
||||
- **Smart Brackets** - Typing `{` automatically creates the closing `}` with correct indentation. Same for `()`, `[]`, `""`, and `''`
|
||||
- **Keyboard Shortcut Overlay** - Press a button to see all available shortcuts at a glance
|
||||
|
||||
---
|
||||
|
||||
## Java Scripting ☕
|
||||
|
||||
Write scripts in **real Java** instead of JavaScript! Powered by the **Janino** compiler, your Java code is compiled and runs natively.
|
||||
|
||||
- **Full Java Language** - Use classes, interfaces, enums, generics, lambdas, and everything you know from Java
|
||||
- **Automatic Hook Resolution** - Just name your methods after the hook you want (e.g., `init`, `tick`, `interact`) and they're wired up automatically
|
||||
- **Language Selector** - Each script tab has a dropdown to choose between JavaScript and Java. Mix and match across tabs
|
||||
- **External Files** - Write `.java` scripts in an external editor and they're loaded automatically
|
||||
|
||||
> Java and JavaScript scripts coexist side by side. Pick whichever language fits your workflow!
|
||||
|
||||
---
|
||||
|
||||
## Client-Side Scripting 🖥️
|
||||
|
||||
Scripts can now run **on the client** for responsive, visual experiences!
|
||||
|
||||
- **Server Controlled** - Server owners decide whether client scripting is allowed. Players can't enable it on their own
|
||||
- **Automatic Sync** - Script files are sent from the server to all connected clients on login, and re-synced whenever scripts are reloaded
|
||||
- **Safe by Default** - Client scripting is off by default and must be explicitly enabled in the server config
|
||||
|
||||
> Client-side scripts open the door to custom UI effects, visual feedback, and client-only hooks -- all while the server stays in full control.
|
||||
|
||||
---
|
||||
|
||||
## Cloner Tab Overhaul 📂
|
||||
|
||||
The Cloner now supports **folders** for organizing your saved NPCs, items, and entries!
|
||||
|
||||
- **Folder System** - Create folders and subfolders to keep your cloner organized however you like
|
||||
- **Full Screen Browser** - A new full-width directory view makes browsing large collections much easier
|
||||
- **Improved Tabs** - Better tab navigation for switching between cloner categories
|
||||
|
||||
---
|
||||
|
||||
## Addon API Support 📘
|
||||
|
||||
Addon developers can now ship **type definitions** alongside their mods, giving scripters full autocomplete and documentation for addon APIs!
|
||||
|
||||
- **Automatic Loading** - Any mod can include API definitions and they'll appear in the script editor automatically
|
||||
- **Patch Support** - Addons can extend existing CNPC+ types with new methods (e.g., a DBC addon adding `getDBCPlayer()` to the Player type)
|
||||
- **Full Documentation** - Parameter names, return types, and descriptions all show up in autocomplete and hover info
|
||||
|
||||
> If you're a scripter using addons, you get autocomplete for their APIs with zero setup. If you're an addon developer, just include your definitions and everything works.
|
||||
|
||||
---
|
||||
|
||||
## Animation Improvements 🎬
|
||||
|
||||
- **Data Store** - Animations can now store and pass data across events and frames for more complex animation logic
|
||||
- **Task System** - A new consumer-based system for reacting to animation lifecycle events, replacing the old approach
|
||||
- **Expanded API** - More animation control exposed to the scripting API
|
||||
|
||||
---
|
||||
|
||||
## Additional Changes 🔧
|
||||
|
||||
- **Player Dialog Events** - Now work the same way as NPC Dialog Events for consistency
|
||||
- **Linked Item Attack Speed** - Added attack speed configuration to linked items
|
||||
- **Trader Balance Preview** - Client-side balance prediction shows your expected balance during trades
|
||||
- **Right-Click Cycling** - Multi-option NPC buttons (Hair, Eyes, Fur, etc.) can now be right-clicked to cycle backwards
|
||||
- **Quest Cooldown Fix** - Fixed cooldown not working for MC Custom and RL Custom timer types
|
||||
- **Bard Music Fix** - Fixed music restarting or duplicating when opening dialogs
|
||||
- **Biome Spawn Fix** - Fixed NPC biome spawn settings not saving after editing
|
||||
- **Block Waypoint Fix** - Fixed issues with block waypoints
|
||||
- **Script Config Fix** - Fixed script configuration not syncing properly between server and client
|
||||
|
||||
---
|
||||
@@ -3,6 +3,7 @@
|
||||
plugins {
|
||||
id 'com.gtnewhorizons.gtnhconvention'
|
||||
id 'idea'
|
||||
id "dts.typescript-generator"
|
||||
}
|
||||
|
||||
version = "1.11-prerelease3"
|
||||
@@ -56,6 +57,30 @@ sourceSets.main.java {
|
||||
srcDirs += apiDir
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TypeScript Definition Generation Task
|
||||
// Generates .d.ts files from Java API sources for scripting IDE support
|
||||
// ============================================================================
|
||||
// TypeScript plugin is applied above in the main plugins block
|
||||
|
||||
tasks.named("generateTypeScriptDefinitions").configure {
|
||||
sourceDirectories = ['src/api/java', 'src/main/java']
|
||||
outputDirectory = "src/main/resources/assets/${project.property("modId")}/api"
|
||||
apiPackages = ['noppes.npcs.api', 'net.minecraft'] as Set
|
||||
cleanOutputFirst = true // Clean old generated files before regenerating
|
||||
patchesDirectory = "dts-patches"
|
||||
implementationPackages = [
|
||||
'noppes.npcs.scripted',
|
||||
'noppes.npcs.controllers.data',
|
||||
'noppes.npcs.entity.data',
|
||||
'noppes.npcs.quests',
|
||||
'kamkeel.npcs.controllers.data'
|
||||
] as Set
|
||||
}
|
||||
processResources.dependsOn generateTypeScriptDefinitions
|
||||
sourcesJar.dependsOn generateTypeScriptDefinitions
|
||||
|
||||
|
||||
// Register @hookName as a valid Javadoc tag
|
||||
tasks.withType(Javadoc) {
|
||||
options.tags = [
|
||||
|
||||
@@ -45,6 +45,8 @@ dependencies {
|
||||
|
||||
shadowImplementation(project.files("tools/standalone-JaninoLoader-1.0.1-ALPHA.jar"))
|
||||
annotationProcessor(project.files("tools/standalone-JaninoLoader-1.0.1-ALPHA.jar"))
|
||||
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
|
||||
if(embedMixin){
|
||||
// Normal Jar will include built in Mixins
|
||||
|
||||
Vendored
+126
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* ECMAScript 5.1 - Array type for working with ordered collections of elements.
|
||||
* Supports static methods like Array.isArray() and instance methods like push(), map(), filter().
|
||||
*
|
||||
* ## How to Use
|
||||
*
|
||||
* ### Creating Arrays
|
||||
* ```js
|
||||
* var empty = []; // Empty array
|
||||
* var nums = [1, 2, 3]; // Array with values
|
||||
* var mixed = ["hello", 42, true]; // Mixed types
|
||||
* var nested = [[1, 2], [3, 4]]; // Nested arrays
|
||||
* ```
|
||||
*
|
||||
* ### Static Methods
|
||||
* ```js
|
||||
* Array.isArray([1, 2, 3]); // true
|
||||
* Array.isArray("hello"); // false
|
||||
* Array.isArray({length: 3}); // false
|
||||
* ```
|
||||
*
|
||||
* ### Adding & Removing Elements
|
||||
* ```js
|
||||
* var arr = [1, 2, 3];
|
||||
* arr.push(4); // [1, 2, 3, 4] — add to end
|
||||
* arr.pop(); // [1, 2, 3] — remove from end
|
||||
* arr.unshift(0); // [0, 1, 2, 3] — add to start
|
||||
* arr.shift(); // [1, 2, 3] — remove from start
|
||||
* arr.splice(1, 1, 99); // [1, 99, 3] — replace at index 1
|
||||
* ```
|
||||
*
|
||||
* ### Searching & Iterating
|
||||
* ```js
|
||||
* var arr = [10, 20, 30, 20];
|
||||
* arr.indexOf(20); // 1 (first match)
|
||||
* arr.lastIndexOf(20); // 3 (last match)
|
||||
* arr.forEach(function(val) { log(val); });
|
||||
* var doubled = arr.map(function(v) { return v * 2; }); // [20, 40, 60, 40]
|
||||
* var big = arr.filter(function(v) { return v > 15; }); // [20, 30, 20]
|
||||
* ```
|
||||
*
|
||||
* ### Reducing & Testing
|
||||
* ```js
|
||||
* var sum = [1, 2, 3].reduce(function(acc, v) { return acc + v; }, 0); // 6
|
||||
* var allPositive = [1, 2, 3].every(function(v) { return v > 0; }); // true
|
||||
* var hasNeg = [1, -2, 3].some(function(v) { return v < 0; }); // true
|
||||
* ```
|
||||
*
|
||||
* ### ❌ Common Mistakes
|
||||
* ```js
|
||||
* Array.push(1); // TypeError — push is an instance method
|
||||
* var a = [3,1,2]; a.sort(); // Sorts as strings! Use compareFn for numbers
|
||||
* a.sort(function(a, b) { return a - b; }); // Correct numeric sort: [1, 2, 3]
|
||||
* ```
|
||||
*/
|
||||
export interface Array<T> {
|
||||
/** Returns true if the given object is an array, false otherwise */
|
||||
static isArray(arg: any): boolean;
|
||||
|
||||
/** The number of elements in the array */
|
||||
length: number;
|
||||
|
||||
/** Combines two or more arrays and returns a new array without modifying the original */
|
||||
concat(...items: T[]): T[];
|
||||
|
||||
/** Tests whether all elements in the array pass the provided test function */
|
||||
every(callback: Java.java.util.function.Function<T, boolean>): boolean;
|
||||
|
||||
/** Creates a new array with all elements that pass the test implemented by the provided function */
|
||||
filter(callback: Java.java.util.function.Function<T, boolean>): T[];
|
||||
|
||||
/** Executes a provided function once for each array element. Returns undefined */
|
||||
forEach(callback: Java.java.util.function.Consumer<T>): void;
|
||||
|
||||
/** Returns the first index at which an element is found, or -1 if not present */
|
||||
indexOf(searchElement: T): number;
|
||||
indexOf(searchElement: T, fromIndex: number): number;
|
||||
|
||||
/** Joins all elements of an array into a string separated by the given separator */
|
||||
join(separator: string): string;
|
||||
|
||||
/** Returns the last index at which an element is found, or -1 if not present */
|
||||
lastIndexOf(searchElement: T): number;
|
||||
lastIndexOf(searchElement: T, fromIndex: number): number;
|
||||
|
||||
/** Creates a new array with the results of calling a function on every element */
|
||||
map(callback: Java.java.util.function.Function<T, any>): any[];
|
||||
|
||||
/** Removes and returns the last element from the array, modifying the original array */
|
||||
pop(): T;
|
||||
|
||||
/** Adds one or more elements to the end of an array and returns the new length */
|
||||
push(...items: T[]): number;
|
||||
|
||||
/** Applies a function against an accumulator and each element to reduce the array to a single value */
|
||||
reduce(callback: Java.java.util.function.BiFunction<any, T, any>): any;
|
||||
reduce(callback: Java.java.util.function.BiFunction<any, T, any>, initialValue: any): any;
|
||||
|
||||
/** Same as reduce() but processes the array from right to left */
|
||||
reduceRight(callback: Java.java.util.function.BiFunction<any, T, any>): any;
|
||||
reduceRight(callback: Java.java.util.function.BiFunction<any, T, any>, initialValue: any): any;
|
||||
|
||||
/** Reverses an array in place. The first element becomes the last, and vice versa */
|
||||
reverse(): T[];
|
||||
|
||||
/** Removes and returns the first element from the array, modifying the original array */
|
||||
shift(): T;
|
||||
|
||||
/** Returns a shallow copy of a portion of an array as a new array object */
|
||||
slice(start: number, end: number): T[];
|
||||
|
||||
/** Tests whether at least one element in the array passes the test implemented by the provided function */
|
||||
some(callback: Java.java.util.function.Function<T, boolean>): boolean;
|
||||
|
||||
/** Sorts the elements of an array in place and returns the sorted array */
|
||||
sort(compareFn: Java.java.util.function.BiFunction<T, T, number>): T[];
|
||||
|
||||
/** Changes the contents of an array by removing or replacing existing elements and/or adding new ones */
|
||||
splice(start: number, deleteCount: number, ...items: T[]): T[];
|
||||
|
||||
/** Returns a string representation of the array */
|
||||
toString(): string;
|
||||
|
||||
/** Adds one or more elements to the beginning of an array and returns the new length */
|
||||
unshift(...items: T[]): number;
|
||||
}
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* ECMAScript 5.1 - Boolean type for true/false values.
|
||||
* Booleans are the result of comparison and logical operations.
|
||||
*
|
||||
* ## How to Use
|
||||
*
|
||||
* ### Creating Booleans
|
||||
* ```js
|
||||
* var yes = true; // Boolean literal
|
||||
* var no = false; // Boolean literal
|
||||
* var fromExpr = (5 > 3); // true (comparison result)
|
||||
* ```
|
||||
*
|
||||
* ### Boolean Methods
|
||||
* ```js
|
||||
* var b = true;
|
||||
* b.toString(); // "true"
|
||||
* b.valueOf(); // true (primitive value)
|
||||
* ```
|
||||
*
|
||||
* ### Truthy & Falsy Values
|
||||
* ```js
|
||||
* // These are FALSY (evaluate to false):
|
||||
* // false, 0, "", null, undefined, NaN
|
||||
*
|
||||
* // These are TRUTHY (evaluate to true):
|
||||
* // true, any non-zero number, any non-empty string, objects, arrays
|
||||
*
|
||||
*/
|
||||
export interface Boolean {
|
||||
/** Returns the string "true" or "false" */
|
||||
toString(): string;
|
||||
|
||||
/** Returns the primitive boolean value */
|
||||
valueOf(): boolean;
|
||||
}
|
||||
Vendored
+168
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* ECMAScript 5.1 - Date type for working with dates and times.
|
||||
* Create with `new Date()` or use static methods like Date.now() to get current timestamp.
|
||||
*
|
||||
* ## How to Use
|
||||
*
|
||||
* ### Creating Dates
|
||||
* ```js
|
||||
* var now = new Date(); // Current date & time
|
||||
* var epoch = new Date(0); // Jan 1, 1970
|
||||
* var specific = new Date(2024, 0, 15); // Jan 15, 2024 (months are 0-based!)
|
||||
* var full = new Date(2024, 5, 15, 10, 30, 0); // Jun 15, 2024 10:30:00
|
||||
* var fromStr = new Date("2024-01-15"); // Parse from ISO string
|
||||
* ```
|
||||
*
|
||||
* ### Static Methods (no `new` needed)
|
||||
* ```js
|
||||
* var timestamp = Date.now(); // Current time in milliseconds
|
||||
* var parsed = Date.parse("2024-01-15"); // Parse string to timestamp
|
||||
* var utc = Date.UTC(2024, 0, 15); // UTC timestamp for Jan 15, 2024
|
||||
* ```
|
||||
*
|
||||
* ### Common Mistakes
|
||||
* ```js
|
||||
* Date.now; // Returns the function, not the timestamp!
|
||||
* Date.getMinutes(); // TypeError — instance method, not static
|
||||
* new Date(2024, 1, 1); // Feb 1, not Jan 1 — months are 0-based!
|
||||
* ```
|
||||
*/
|
||||
export interface Date {
|
||||
/** Returns the current time as the number of milliseconds since 1970-01-01 00:00:00 UTC */
|
||||
static now(): number;
|
||||
|
||||
/** Parses a date string and returns the number of milliseconds since 1970-01-01 00:00:00 UTC */
|
||||
static parse(s: string): number;
|
||||
|
||||
/** Returns the number of milliseconds since 1970-01-01 00:00:00 UTC for the given UTC date/time components */
|
||||
static UTC(year: number, month: number, date: number, hours: number, minutes: number, seconds: number, ms: number): number;
|
||||
|
||||
/** Returns the day of the month (1-31) in local time */
|
||||
getDate(): number;
|
||||
|
||||
/** Returns the day of the week (0=Sunday, 6=Saturday) in local time */
|
||||
getDay(): number;
|
||||
|
||||
/** Returns the year in local time */
|
||||
getFullYear(): number;
|
||||
|
||||
/** Returns the hour (0-23) in local time */
|
||||
getHours(): number;
|
||||
|
||||
/** Returns the milliseconds (0-999) in local time */
|
||||
getMilliseconds(): number;
|
||||
|
||||
/** Returns the minutes (0-59) in local time */
|
||||
getMinutes(): number;
|
||||
|
||||
/** Returns the month (0=January, 11=December) in local time */
|
||||
getMonth(): number;
|
||||
|
||||
/** Returns the seconds (0-59) in local time */
|
||||
getSeconds(): number;
|
||||
|
||||
/** Returns the number of milliseconds since 1970-01-01 00:00:00 UTC */
|
||||
getTime(): number;
|
||||
|
||||
/** Returns the time zone offset in minutes between local time and UTC */
|
||||
getTimezoneOffset(): number;
|
||||
|
||||
/** Returns the day of the month (1-31) in UTC */
|
||||
getUTCDate(): number;
|
||||
|
||||
/** Returns the day of the week (0=Sunday, 6=Saturday) in UTC */
|
||||
getUTCDay(): number;
|
||||
|
||||
/** Returns the year in UTC */
|
||||
getUTCFullYear(): number;
|
||||
|
||||
/** Returns the hour (0-23) in UTC */
|
||||
getUTCHours(): number;
|
||||
|
||||
/** Returns the milliseconds (0-999) in UTC */
|
||||
getUTCMilliseconds(): number;
|
||||
|
||||
/** Returns the minutes (0-59) in UTC */
|
||||
getUTCMinutes(): number;
|
||||
|
||||
/** Returns the month (0=January, 11=December) in UTC */
|
||||
getUTCMonth(): number;
|
||||
|
||||
/** Returns the seconds (0-59) in UTC */
|
||||
getUTCSeconds(): number;
|
||||
|
||||
/** Sets the day of the month and returns the new timestamp */
|
||||
setDate(date: number): number;
|
||||
|
||||
/** Sets the year, and optionally month and date, then returns the new timestamp */
|
||||
setFullYear(year: number, month: number, date: number): number;
|
||||
|
||||
/** Sets the hour, minutes, seconds, and milliseconds, then returns the new timestamp */
|
||||
setHours(hour: number, min: number, sec: number, ms: number): number;
|
||||
|
||||
/** Sets the milliseconds and returns the new timestamp */
|
||||
setMilliseconds(ms: number): number;
|
||||
|
||||
/** Sets the minutes, seconds, and milliseconds, then returns the new timestamp */
|
||||
setMinutes(min: number, sec: number, ms: number): number;
|
||||
|
||||
/** Sets the month, and optionally date, then returns the new timestamp */
|
||||
setMonth(month: number, date: number): number;
|
||||
|
||||
/** Sets the seconds and milliseconds, then returns the new timestamp */
|
||||
setSeconds(sec: number, ms: number): number;
|
||||
|
||||
/** Sets the timestamp (milliseconds since epoch) and returns it */
|
||||
setTime(time: number): number;
|
||||
|
||||
/** Sets the day of the month (UTC) and returns the new timestamp */
|
||||
setUTCDate(date: number): number;
|
||||
|
||||
/** Sets the year, month, and date (UTC), then returns the new timestamp */
|
||||
setUTCFullYear(year: number, month: number, date: number): number;
|
||||
|
||||
/** Sets the hour, minutes, seconds, and milliseconds (UTC), then returns the new timestamp */
|
||||
setUTCHours(hour: number, min: number, sec: number, ms: number): number;
|
||||
|
||||
/** Sets the milliseconds (UTC) and returns the new timestamp */
|
||||
setUTCMilliseconds(ms: number): number;
|
||||
|
||||
/** Sets the minutes, seconds, and milliseconds (UTC), then returns the new timestamp */
|
||||
setUTCMinutes(min: number, sec: number, ms: number): number;
|
||||
|
||||
/** Sets the month and date (UTC), then returns the new timestamp */
|
||||
setUTCMonth(month: number, date: number): number;
|
||||
|
||||
/** Sets the seconds and milliseconds (UTC), then returns the new timestamp */
|
||||
setUTCSeconds(sec: number, ms: number): number;
|
||||
|
||||
/** Returns a string representation of the date portion in local time (e.g., "Mon Jan 01 2024") */
|
||||
toDateString(): string;
|
||||
|
||||
/** Returns the date as an ISO 8601 string (e.g., "2024-01-01T00:00:00.000Z") */
|
||||
toISOString(): string;
|
||||
|
||||
/** Returns a JSON representation of the date (typically an ISO 8601 string) */
|
||||
toJSON(key: any): string;
|
||||
|
||||
/** Returns a localized string for the date portion (format depends on locale) */
|
||||
toLocaleDateString(): string;
|
||||
|
||||
/** Returns a localized string for the entire date and time (format depends on locale) */
|
||||
toLocaleString(): string;
|
||||
|
||||
/** Returns a localized string for the time portion (format depends on locale) */
|
||||
toLocaleTimeString(): string;
|
||||
|
||||
/** Returns a string representation of the date and time in local time */
|
||||
toString(): string;
|
||||
|
||||
/** Returns a string representation of the time portion in local time (e.g., "00:00:00 GMT+0000") */
|
||||
toTimeString(): string;
|
||||
|
||||
/** Returns a string representation of the date and time in UTC (e.g., "Mon, 01 Jan 2024 00:00:00 GMT") */
|
||||
toUTCString(): string;
|
||||
|
||||
/** Returns the primitive value of the date object (milliseconds since 1970-01-01 00:00:00 UTC) */
|
||||
valueOf(): number;
|
||||
}
|
||||
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* ECMAScript 5.1 - Error base type for runtime errors.
|
||||
* Throw Error objects to indicate failure conditions in your code.
|
||||
*
|
||||
* ## How to Use
|
||||
*
|
||||
* ### Creating & Throwing Errors
|
||||
* ```js
|
||||
* throw new Error("Something went wrong");
|
||||
* throw new TypeError("Expected a string");
|
||||
* throw new RangeError("Value out of range");
|
||||
* ```
|
||||
*
|
||||
* ### Catching Errors
|
||||
* ```js
|
||||
* try {
|
||||
* var result = riskyOperation();
|
||||
* } catch (e) {
|
||||
* log(e.name); // "Error", "TypeError", etc.
|
||||
* log(e.message); // "Something went wrong"
|
||||
* log(e.toString()); // "Error: Something went wrong"
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### Custom Error Messages
|
||||
* ```js
|
||||
* function divide(a, b) {
|
||||
* if (b === 0) {
|
||||
* throw new Error("Cannot divide by zero");
|
||||
* }
|
||||
* return a / b;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### ❌ Common Mistakes
|
||||
* ```js
|
||||
* throw "Something went wrong"; // Works but no .name or .message properties
|
||||
* Error("oops"); // Creates error but doesn't throw it!
|
||||
* throw Error("oops"); // ✅ Works (with or without `new`)
|
||||
* ```
|
||||
*/
|
||||
export interface Error {
|
||||
/** The name of the error type (e.g., "Error", "TypeError", "RangeError") */
|
||||
name: string;
|
||||
|
||||
/** A human-readable description of what went wrong */
|
||||
message: string;
|
||||
|
||||
/** Returns a string representation of the error (typically "name: message") */
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* EvalError - Thrown when eval() function is misused (rarely used in modern JavaScript)
|
||||
*/
|
||||
export interface EvalError extends Error {}
|
||||
|
||||
/**
|
||||
* RangeError - Thrown when a numeric value is out of the acceptable range (e.g., array length < 0)
|
||||
*/
|
||||
export interface RangeError extends Error {}
|
||||
|
||||
/**
|
||||
* ReferenceError - Thrown when trying to reference a variable that does not exist or is not in scope
|
||||
*/
|
||||
export interface ReferenceError extends Error {}
|
||||
|
||||
/**
|
||||
* SyntaxError - Thrown when parsing invalid syntax (usually caught during compile/parse, not runtime)
|
||||
*/
|
||||
export interface SyntaxError extends Error {}
|
||||
|
||||
/**
|
||||
* TypeError - Thrown when an operation is performed on a value of the wrong type (e.g., calling a non-function)
|
||||
*/
|
||||
export interface TypeError extends Error {}
|
||||
|
||||
/**
|
||||
* URIError - Thrown by URI encoding/decoding functions when given invalid characters or sequences
|
||||
*/
|
||||
export interface URIError extends Error {}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* ECMAScript 5.1 - Global functions available in the global scope.
|
||||
* These are not on any specific object - call them directly (e.g., parseInt("42")).
|
||||
*
|
||||
* ## How to Use
|
||||
*
|
||||
* ### Parsing Numbers
|
||||
* ```js
|
||||
* parseInt("42"); // 42
|
||||
* parseInt("42px"); // 42 (stops at non-digit)
|
||||
* parseInt("0xFF", 16); // 255 (hexadecimal)
|
||||
* parseInt("111", 2); // 7 (binary)
|
||||
* parseFloat("3.14"); // 3.14
|
||||
* parseFloat("3.14abc"); // 3.14 (stops at invalid char)
|
||||
* ```
|
||||
*
|
||||
* ### Checking Special Values
|
||||
* ```js
|
||||
* isNaN(NaN); // true
|
||||
* isNaN(42); // false
|
||||
* isNaN("hello"); // true (cannot parse as number)
|
||||
* isNaN("42"); // false (can parse as number)
|
||||
* isFinite(42); // true
|
||||
* isFinite(Infinity); // false
|
||||
* isFinite(NaN); // false
|
||||
* ```
|
||||
*
|
||||
* ### URI Encoding/Decoding
|
||||
* ```js
|
||||
* encodeURI("hello world"); // "hello%20world"
|
||||
* decodeURI("hello%20world"); // "hello world"
|
||||
* encodeURIComponent("a=1&b=2"); // "a%3D1%26b%3D2" (encodes & and =)
|
||||
* decodeURIComponent("a%3D1%26b%3D2"); // "a=1&b=2"
|
||||
* ```
|
||||
*
|
||||
* ### ❌ Common Mistakes
|
||||
* ```js
|
||||
* parseInt("08"); // Some engines treat as octal! Always specify radix
|
||||
* parseInt("08", 10); // 8 ✅ — explicitly base-10
|
||||
* parseInt(""); // NaN — empty string returns NaN, not 0
|
||||
* parseFloat(""); // NaN
|
||||
* isNaN(undefined); // true — coerces to NaN first
|
||||
* ```
|
||||
*/
|
||||
export interface GlobalFunctions {
|
||||
/** Evaluates JavaScript code in a string (use with caution - slow and potential security issues) */
|
||||
static eval(x: any): any;
|
||||
|
||||
/** Parses a string and returns an integer. Stops parsing at the first non-digit character */
|
||||
static parseInt(string: string): number;
|
||||
|
||||
/** Parses a string and returns an integer using the specified radix (base). Stops parsing at the first non-digit character */
|
||||
static parseInt(string: string, radix: number): number;
|
||||
|
||||
/** Parses a string and returns a floating-point number. Stops parsing at the first invalid character */
|
||||
static parseFloat(string: string): number;
|
||||
|
||||
/** Determines whether a value is NaN (not-a-number). The value is first coerced to a number before testing */
|
||||
static isNaN(value: any): boolean;
|
||||
|
||||
/** Determines whether a value is a finite number. The value is first coerced to a number. Returns false for Infinity, -Infinity, and NaN */
|
||||
static isFinite(value: any): boolean;
|
||||
|
||||
/** Decodes a URI by replacing encoded sequences like %20 with their original characters */
|
||||
static decodeURI(encodedURI: string): string;
|
||||
|
||||
/** Decodes URI component by replacing ALL encoded sequences including those for reserved URI characters */
|
||||
static decodeURIComponent(encodedURIComponent: string): string;
|
||||
|
||||
/** Encodes a URI by replacing special characters with %HH sequences (but preserves reserved characters like / : ?) */
|
||||
static encodeURI(uri: string): string;
|
||||
|
||||
/** Encodes URI component by replacing ALL special characters including reserved ones with %HH sequences */
|
||||
static encodeURIComponent(uriComponent: string): string;
|
||||
}
|
||||
Vendored
+100
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* ECMAScript 5.1 - JSON built-in object for parsing and serializing JSON data.
|
||||
*
|
||||
* ## How to Use
|
||||
*
|
||||
* JSON is a **static object** — use its methods directly, never with `new`.
|
||||
*
|
||||
* ### Parsing JSON Strings to Objects
|
||||
* ```js
|
||||
* var obj = JSON.parse('{"name": "Steve", "level": 5}');
|
||||
* log(obj.name); // "Steve"
|
||||
* log(obj.level); // 5
|
||||
*
|
||||
* var arr = JSON.parse('[1, 2, 3]');
|
||||
* log(arr[0]); // 1
|
||||
* ```
|
||||
*
|
||||
* ### Converting Objects to JSON Strings
|
||||
* ```js
|
||||
* var data = { name: "Steve", hp: 100 };
|
||||
* var json = JSON.stringify(data); // '{"name":"Steve","hp":100}'
|
||||
*
|
||||
* // Pretty-printed with 2-space indent:
|
||||
* var pretty = JSON.stringify(data, null, 2);
|
||||
* // '{
|
||||
* // "name": "Steve",
|
||||
* // "hp": 100
|
||||
* // }'
|
||||
* ```
|
||||
*
|
||||
* ### Storing & Loading Data
|
||||
* ```js
|
||||
* // Save to NPC stored data:
|
||||
* var inventory = { sword: 1, potion: 5 };
|
||||
* npc.getStoredData().put("inv", JSON.stringify(inventory));
|
||||
*
|
||||
* // Load from NPC stored data:
|
||||
* var raw = npc.getStoredData().get("inv");
|
||||
* var loaded = JSON.parse(raw);
|
||||
* log(loaded.potion); // 5
|
||||
* ```
|
||||
*
|
||||
* ### ❌ Common Mistakes
|
||||
* ```js
|
||||
* new JSON(); // TypeError — JSON is not a constructor
|
||||
* JSON.parse('{name: "Steve"}'); // SyntaxError — keys must be quoted
|
||||
* JSON.parse("{'name': 'Steve'}"); // SyntaxError — must use double quotes
|
||||
* JSON.parse('{"name": "Steve"}'); // ✅ Correct — double-quoted keys & values
|
||||
* ```
|
||||
*/
|
||||
export interface JSON {
|
||||
/**
|
||||
* Parses a JSON string and returns the parsed value (object, array, string, number, boolean, or null).
|
||||
* @param text - A valid JSON string to parse
|
||||
* @returns The parsed JavaScript value
|
||||
*/
|
||||
static parse(text: string): any;
|
||||
|
||||
/**
|
||||
* Parses a JSON string with a reviver function that transforms each parsed key-value pair.
|
||||
* @param text - A valid JSON string to parse
|
||||
* @param reviver - Function called for each key-value pair; receives the key and parsed value, returns the transformed value
|
||||
* @returns The parsed JavaScript value after applying the reviver
|
||||
*/
|
||||
static parse(text: string, reviver: Java.java.util.function.BiFunction<string, any, any>): any;
|
||||
|
||||
/**
|
||||
* Converts a JavaScript value to a JSON string.
|
||||
* @param value - The value to serialize (object, array, string, number, boolean, or null)
|
||||
* @returns A JSON-formatted string representation of the value
|
||||
*/
|
||||
static stringify(value: any): string;
|
||||
|
||||
/**
|
||||
* Converts a JavaScript value to a JSON string, using a replacer function to filter or transform properties.
|
||||
* @param value - The value to serialize
|
||||
* @param replacer - Function called for each key-value pair; receives the key and value, returns the value to include (or undefined to omit)
|
||||
* @param space - Optional indentation: a number of spaces (up to 10), or a string prefix per level
|
||||
* @returns A JSON-formatted string representation of the value
|
||||
*/
|
||||
static stringify(value: any, replacer: Java.java.util.function.BiFunction<string, any, any>, space?: any): string;
|
||||
|
||||
/**
|
||||
* Converts a JavaScript value to a JSON string, including only the specified properties.
|
||||
* @param value - The value to serialize
|
||||
* @param replacer - Array of property names to include in the output
|
||||
* @param space - Optional indentation: a number of spaces (up to 10), or a string prefix per level
|
||||
* @returns A JSON-formatted string representation of the value
|
||||
*/
|
||||
static stringify(value: any, replacer: string[], space?: any): string;
|
||||
|
||||
/**
|
||||
* Converts a JavaScript value to a JSON string with optional pretty-printing.
|
||||
* @param value - The value to serialize
|
||||
* @param replacer - Pass null to include all properties
|
||||
* @param space - Indentation: a number of spaces (up to 10), or a string prefix per level
|
||||
* @returns A JSON-formatted string representation of the value
|
||||
*/
|
||||
static stringify(value: any, replacer: null, space: any): string;
|
||||
}
|
||||
Vendored
+128
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* ECMAScript 5.1 - Math built-in object providing mathematical constants and functions.
|
||||
* All methods are static and called as Math.methodName(). No constructor.
|
||||
*
|
||||
* ## How to Use
|
||||
*
|
||||
* Math is a **static object** — use its methods directly, never with `new`.
|
||||
*
|
||||
* ### Basic Math Operations
|
||||
* ```js
|
||||
* var rounded = Math.floor(3.7); // 3
|
||||
* var ceiling = Math.ceil(3.2); // 4
|
||||
* var nearest = Math.round(3.5); // 4
|
||||
* var absolute = Math.abs(-42); // 42
|
||||
* ```
|
||||
*
|
||||
* ### Powers, Roots & Logarithms
|
||||
* ```js
|
||||
* var squared = Math.pow(2, 3); // 8
|
||||
* var root = Math.sqrt(16); // 4
|
||||
* var natural = Math.log(Math.E); // 1
|
||||
* ```
|
||||
*
|
||||
* ### Random Numbers
|
||||
* ```js
|
||||
* var rand = Math.random(); // 0.0 to 0.999...
|
||||
* // Random integer between 1 and 10:
|
||||
* var dice = Math.floor(Math.random() * 10) + 1;
|
||||
* ```
|
||||
*
|
||||
* ### Finding Min/Max
|
||||
* ```js
|
||||
* var biggest = Math.max(5, 10, 3); // 10
|
||||
* var smallest = Math.min(5, 10, 3); // 3
|
||||
* ```
|
||||
*
|
||||
* ### Trigonometry
|
||||
* ```js
|
||||
* var sine = Math.sin(Math.PI / 2); // 1
|
||||
* var cosine = Math.cos(0); // 1
|
||||
* var angle = Math.atan2(1, 1); // 0.785... (PI/4)
|
||||
* ```
|
||||
*
|
||||
* ### ❌ Common Mistakes
|
||||
* ```js
|
||||
* new Math() // TypeError — Math is not a constructor
|
||||
* Math() // TypeError — Math is not a function
|
||||
* ```
|
||||
*/
|
||||
export interface Math {
|
||||
/** Euler's number (~2.718), the base of natural logarithms */
|
||||
static readonly E: number;
|
||||
|
||||
/** Natural logarithm of 2 (~0.693) */
|
||||
static readonly LN2: number;
|
||||
|
||||
/** Natural logarithm of 10 (~2.303) */
|
||||
static readonly LN10: number;
|
||||
|
||||
/** Base-2 logarithm of e (~1.443) */
|
||||
static readonly LOG2E: number;
|
||||
|
||||
/** Base-10 logarithm of e (~0.434) */
|
||||
static readonly LOG10E: number;
|
||||
|
||||
/** Ratio of a circle's circumference to its diameter (~3.14159) */
|
||||
static readonly PI: number;
|
||||
|
||||
/** Square root of 0.5 (~0.707) */
|
||||
static readonly SQRT1_2: number;
|
||||
|
||||
/** Square root of 2 (~1.414) */
|
||||
static readonly SQRT2: number;
|
||||
|
||||
/** Returns the absolute value of a number */
|
||||
static abs(x: number): number;
|
||||
|
||||
/** Returns the arccosine (in radians) of a number */
|
||||
static acos(x: number): number;
|
||||
|
||||
/** Returns the arcsine (in radians) of a number */
|
||||
static asin(x: number): number;
|
||||
|
||||
/** Returns the arctangent (in radians) of a number */
|
||||
static atan(x: number): number;
|
||||
|
||||
/** Returns the arctangent of two numbers (y/x), with proper quadrant handling */
|
||||
static atan2(y: number, x: number): number;
|
||||
|
||||
/** Returns the smallest integer greater than or equal to a number */
|
||||
static ceil(x: number): number;
|
||||
|
||||
/** Returns the cosine (in radians) of a number */
|
||||
static cos(x: number): number;
|
||||
|
||||
/** Returns e raised to the power of a number */
|
||||
static exp(x: number): number;
|
||||
|
||||
/** Returns the largest integer less than or equal to a number */
|
||||
static floor(x: number): number;
|
||||
|
||||
/** Returns the natural logarithm (base e) of a number */
|
||||
static log(x: number): number;
|
||||
|
||||
/** Returns the largest of zero or more numbers */
|
||||
static max(...values: number[]): number;
|
||||
|
||||
/** Returns the smallest of zero or more numbers */
|
||||
static min(...values: number[]): number;
|
||||
|
||||
/** Returns base raised to the power of exponent */
|
||||
static pow(x: number, y: number): number;
|
||||
|
||||
/** Returns a random number between 0 (inclusive) and 1 (exclusive) */
|
||||
static random(): number;
|
||||
|
||||
/** Returns the value of a number rounded to the nearest integer */
|
||||
static round(x: number): number;
|
||||
|
||||
/** Returns the sine (in radians) of a number */
|
||||
static sin(x: number): number;
|
||||
|
||||
/** Returns the square root of a number */
|
||||
static sqrt(x: number): number;
|
||||
|
||||
/** Returns the tangent (in radians) of a number */
|
||||
static tan(x: number): number;
|
||||
}
|
||||
Vendored
+74
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* ECMAScript 5.1 - Number type for working with numeric values and constants.
|
||||
* Access static constants like Number.MAX_VALUE or format numbers with instance methods.
|
||||
*
|
||||
* ## How to Use
|
||||
*
|
||||
* ### Number Constants
|
||||
* ```js
|
||||
* Number.MAX_VALUE; // ~1.79e+308 (largest representable number)
|
||||
* Number.MIN_VALUE; // ~5e-324 (smallest positive number)
|
||||
* Number.NaN; // NaN
|
||||
* Number.POSITIVE_INFINITY; // Infinity
|
||||
* Number.NEGATIVE_INFINITY; // -Infinity
|
||||
* ```
|
||||
*
|
||||
* ### Formatting Numbers
|
||||
* ```js
|
||||
* var n = 3.14159;
|
||||
* n.toFixed(2); // "3.14" (fixed decimal places)
|
||||
* n.toFixed(0); // "3"
|
||||
* n.toPrecision(4); // "3.142" (significant digits)
|
||||
* n.toExponential(2); // "3.14e+0" (scientific notation)
|
||||
* ```
|
||||
*
|
||||
* ### Converting to String
|
||||
* ```js
|
||||
* var n = 255;
|
||||
* n.toString(); // "255" (base 10, default)
|
||||
* n.toString(16); // "ff" (hexadecimal)
|
||||
* n.toString(2); // "11111111" (binary)
|
||||
* n.toString(8); // "377" (octal)
|
||||
* ```
|
||||
*
|
||||
* ### ❌ Common Mistakes
|
||||
* ```js
|
||||
* Number.toFixed(2); // TypeError — instance method, not static
|
||||
* (3).toFixed(2); // "3.00" ✅ — wrap literal in parentheses
|
||||
* 3.toFixed(2); // SyntaxError — parser reads 3. as float
|
||||
* ```
|
||||
*/
|
||||
export interface Number {
|
||||
/** The largest representable number (~1.79e+308) */
|
||||
static readonly MAX_VALUE: number;
|
||||
|
||||
/** The smallest representable positive number (~5e-324) */
|
||||
static readonly MIN_VALUE: number;
|
||||
|
||||
/** Represents "Not a Number" (NaN) - the result of undefined or invalid arithmetic operations */
|
||||
static readonly NaN: number;
|
||||
|
||||
/** Represents negative infinity, the result of overflow or division by zero */
|
||||
static readonly NEGATIVE_INFINITY: number;
|
||||
|
||||
/** Represents positive infinity, the result of overflow or division by zero */
|
||||
static readonly POSITIVE_INFINITY: number;
|
||||
|
||||
/** Formats the number using exponential notation with optional fractional digits */
|
||||
toExponential(fractionDigits: number): string;
|
||||
|
||||
/** Formats the number as a fixed-point string with the specified number of fractional digits (0-20) */
|
||||
toFixed(fractionDigits: number): string;
|
||||
|
||||
/** Returns a localized string representation of the number (format depends on locale) */
|
||||
toLocaleString(): string;
|
||||
|
||||
/** Formats the number as a string with the specified number of significant digits (1-21) */
|
||||
toPrecision(precision: number): string;
|
||||
|
||||
/** Converts the number to a string in the specified radix (base 2-36, default 10) */
|
||||
toString(radix: number): string;
|
||||
|
||||
/** Returns the primitive numeric value of this Number object */
|
||||
valueOf(): number;
|
||||
}
|
||||
Vendored
+105
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* ECMAScript 5.1 - Object type, the base type for all JavaScript objects.
|
||||
* Use static methods like Object.keys() and Object.create() to manipulate object structure.
|
||||
*
|
||||
* ## How to Use
|
||||
*
|
||||
* ### Creating Objects
|
||||
* ```js
|
||||
* var obj = {}; // Empty object literal
|
||||
* var person = { name: "Steve", age: 20 }; // Object with properties
|
||||
* var child = Object.create(person); // Create with prototype
|
||||
* ```
|
||||
*
|
||||
* ### Inspecting Objects
|
||||
* ```js
|
||||
* var obj = { a: 1, b: 2, c: 3 };
|
||||
* Object.keys(obj); // ["a", "b", "c"]
|
||||
* Object.getOwnPropertyNames(obj); // ["a", "b", "c"] (includes non-enumerable)
|
||||
* obj.hasOwnProperty("a"); // true
|
||||
* obj.hasOwnProperty("toString"); // false (inherited, not own)
|
||||
* ```
|
||||
*
|
||||
* ### Freezing & Sealing
|
||||
* ```js
|
||||
* var config = { debug: false };
|
||||
* Object.freeze(config); // Cannot modify, add, or delete properties
|
||||
* config.debug = true; // Silently fails (or throws in strict mode)
|
||||
* Object.isFrozen(config); // true
|
||||
*
|
||||
* var settings = { volume: 50 };
|
||||
* Object.seal(settings); // Can modify values, but cannot add/delete
|
||||
* settings.volume = 75; // ✅ Works
|
||||
* settings.newProp = true; // Silently fails
|
||||
* ```
|
||||
*
|
||||
* ### Property Descriptors
|
||||
* ```js
|
||||
* var obj = {};
|
||||
* Object.defineProperty(obj, "id", {
|
||||
* value: 42,
|
||||
* writable: false, // Cannot be changed
|
||||
* enumerable: true // Shows up in for-in / Object.keys
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* ### ❌ Common Mistakes
|
||||
* ```js
|
||||
* Object.keys(); // TypeError — needs an argument
|
||||
* obj.keys(); // TypeError — keys is a static method
|
||||
* Object.hasOwnProperty("a"); // Wrong — instance method on the object
|
||||
* ```
|
||||
*/
|
||||
export interface Object {
|
||||
/** Creates a new object with the specified prototype and optional property descriptors */
|
||||
static create(proto: any, propertiesObject: any): any;
|
||||
|
||||
/** Defines or modifies multiple properties on an object and returns the modified object */
|
||||
static defineProperties(obj: any, props: any): any;
|
||||
|
||||
/** Defines a new property on an object or modifies an existing property and returns the object */
|
||||
static defineProperty(obj: any, prop: string, descriptor: any): any;
|
||||
|
||||
/** Prevents all modifications to an object (cannot add, modify, or delete properties) */
|
||||
static freeze(obj: any): any;
|
||||
|
||||
/** Returns a property descriptor for a named property on an object, or undefined if not found */
|
||||
static getOwnPropertyDescriptor(obj: any, prop: string): any;
|
||||
|
||||
/** Returns an array of all property names (both enumerable and non-enumerable) on an object */
|
||||
static getOwnPropertyNames(obj: any): string[];
|
||||
|
||||
/** Returns the prototype object of the specified object */
|
||||
static getPrototypeOf(obj: any): any;
|
||||
|
||||
/** Determines whether an object is extensible (can have new properties added to it) */
|
||||
static isExtensible(obj: any): boolean;
|
||||
|
||||
/** Determines whether an object has been frozen (is immutable) */
|
||||
static isFrozen(obj: any): boolean;
|
||||
|
||||
/** Determines whether an object is sealed (cannot add or delete properties) */
|
||||
static isSealed(obj: any): boolean;
|
||||
|
||||
/** Returns an array of all enumerable property names on an object */
|
||||
static keys(obj: any): string[];
|
||||
|
||||
/** Prevents new properties from being added to an object but allows modification of existing ones */
|
||||
static preventExtensions(obj: any): any;
|
||||
|
||||
/** Prevents all modifications to an object except for reading and writing property values */
|
||||
static seal(obj: any): any;
|
||||
|
||||
/** Determines whether an object has a property with the specified name (direct property, not inherited) */
|
||||
hasOwnProperty(prop: string): boolean;
|
||||
|
||||
/** Determines whether an object is in the prototype chain of another object */
|
||||
isPrototypeOf(obj: any): boolean;
|
||||
|
||||
/** Determines whether an object property is enumerable (would be included in a for-in loop) */
|
||||
propertyIsEnumerable(prop: string): boolean;
|
||||
|
||||
/** Returns a localized string representation of the object (format depends on locale) */
|
||||
toLocaleString(): string;
|
||||
|
||||
}
|
||||
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* ECMAScript 5.1 - RegExp type for pattern matching and string manipulation.
|
||||
* Create with the RegExp constructor or literal syntax: /pattern/flags
|
||||
*
|
||||
* ## How to Use
|
||||
*
|
||||
* ### Creating Regular Expressions
|
||||
* ```js
|
||||
* // Method 1: Regex literal (preferred)
|
||||
* var re = /hello/;
|
||||
* var reFlags = /hello/gi; // g=global, i=ignore case, m=multiline
|
||||
*
|
||||
* // Method 2: Constructor (useful for dynamic patterns)
|
||||
* var re2 = new RegExp("hello");
|
||||
* var re3 = new RegExp("hello", "gi");
|
||||
* var dynamic = new RegExp(playerName); // Build pattern from variable
|
||||
* ```
|
||||
*
|
||||
* ### Testing for Matches
|
||||
* ```js
|
||||
* /\d+/.test("abc123"); // true — contains digits
|
||||
* /^\d+$/.test("abc123"); // false — not ALL digits
|
||||
* /^\d+$/.test("12345"); // true — all digits
|
||||
* /hello/i.test("Hello World"); // true — case insensitive
|
||||
* ```
|
||||
*
|
||||
* ### Extracting Matches
|
||||
* ```js
|
||||
* var match = /\d+/.exec("abc123def"); // ["123"]
|
||||
* var noMatch = /\d+/.exec("no digits"); // null
|
||||
*
|
||||
* // With capturing groups
|
||||
* var result = /(\w+)@(\w+)/.exec("user@domain");
|
||||
* // result[0] = "user@domain" (full match)
|
||||
* // result[1] = "user" (first group)
|
||||
* // result[2] = "domain" (second group)
|
||||
* ```
|
||||
*
|
||||
* ### Using with String Methods
|
||||
* ```js
|
||||
* "abc123".match(/\d+/); // ["123"]
|
||||
* "abc123".search(/\d/); // 3 (index of first digit)
|
||||
* "abc123".replace(/\d+/, "NUM"); // "abcNUMdef"
|
||||
* "a,b,,c".split(/,+/); // ["a", "b", "c"]
|
||||
* ```
|
||||
*
|
||||
* ### RegExp Properties
|
||||
* ```js
|
||||
* var re = /hello/gi;
|
||||
* re.source; // "hello"
|
||||
* re.global; // true
|
||||
* re.ignoreCase; // true
|
||||
* re.multiline; // false
|
||||
* re.lastIndex; // 0 (changes with global exec/test)
|
||||
* ```
|
||||
*
|
||||
* ### ❌ Common Mistakes
|
||||
* ```js
|
||||
* RegExp.test("string"); // TypeError — test is an INSTANCE method!
|
||||
* new RegExp.test("string"); // TypeError — incorrect syntax
|
||||
* // ✅ CORRECT:
|
||||
* /pattern/.test("string"); // Use on a regex instance
|
||||
* new RegExp("pattern").test("string"); // Or create instance first
|
||||
*
|
||||
* // Double-escape backslashes in constructor strings:
|
||||
* new RegExp("\d+"); // Wrong — \d becomes just d
|
||||
* new RegExp("\\d+"); // ✅ Correct — \\d becomes \d
|
||||
* ```
|
||||
*/
|
||||
export interface RegExp {
|
||||
/** The pattern string that was used to create this regular expression */
|
||||
source: string;
|
||||
|
||||
/** True if the 'g' (global) flag is set, affecting match behavior in exec() and test() */
|
||||
global: boolean;
|
||||
|
||||
/** True if the 'i' (ignore case) flag is set, making pattern matching case-insensitive */
|
||||
ignoreCase: boolean;
|
||||
|
||||
/** True if the 'm' (multiline) flag is set, affecting ^ and $ anchor behavior */
|
||||
multiline: boolean;
|
||||
|
||||
/** The index at which to start searching in the string (used with 'g' flag in exec() loops) */
|
||||
lastIndex: number;
|
||||
|
||||
/** Searches the string for a match and returns an array of matches or null if no match found */
|
||||
exec(string: string): any[];
|
||||
|
||||
/** Tests whether the pattern matches the string, returning true or false */
|
||||
test(string: string): boolean;
|
||||
|
||||
/** Returns a string representation of the regular expression (e.g., "/pattern/flags") */
|
||||
toString(): string;
|
||||
}
|
||||
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* ECMAScript 5.1 - String type for working with text. Strings are immutable sequences of characters.
|
||||
* Use String.fromCharCode() to create a string from character codes.
|
||||
*
|
||||
* ## How to Use
|
||||
*
|
||||
* ### Creating Strings
|
||||
* ```js
|
||||
* var greeting = "Hello, world!"; // String literal (double quotes)
|
||||
* var name = 'Steve'; // String literal (single quotes)
|
||||
* var fromCode = String.fromCharCode(65); // "A" (from char code)
|
||||
* ```
|
||||
*
|
||||
* ### Searching & Checking
|
||||
* ```js
|
||||
* var str = "Hello, world!";
|
||||
* str.indexOf("world"); // 7
|
||||
* str.indexOf("missing"); // -1 (not found)
|
||||
* str.lastIndexOf("l"); // 10
|
||||
* str.search(/world/); // 7 (regex search)
|
||||
* str.charAt(0); // "H"
|
||||
* str.charCodeAt(0); // 72
|
||||
* str.length; // 13
|
||||
* ```
|
||||
*
|
||||
* ### Extracting & Transforming
|
||||
* ```js
|
||||
* var str = "Hello, world!";
|
||||
* str.slice(7, 12); // "world"
|
||||
* str.substring(0, 5); // "Hello"
|
||||
* str.toUpperCase(); // "HELLO, WORLD!"
|
||||
* str.toLowerCase(); // "hello, world!"
|
||||
* str.trim(); // Removes leading/trailing whitespace
|
||||
* str.replace("world", "NPC"); // "Hello, NPC!"
|
||||
* str.replace(/l/g, "L"); // "HeLLo, worLd!" (regex with global flag)
|
||||
* ```
|
||||
*
|
||||
* ### Splitting & Joining
|
||||
* ```js
|
||||
* "a,b,c".split(","); // ["a", "b", "c"]
|
||||
* "hello".split(""); // ["h", "e", "l", "l", "o"]
|
||||
* ["a", "b", "c"].join("-"); // "a-b-c" (Array.join, but related)
|
||||
* "one two three".split(" ", 2); // ["one", "two"] (limit results)
|
||||
* ```
|
||||
*
|
||||
* ### Pattern Matching
|
||||
* ```js
|
||||
* "abc123".match(/\d+/); // ["123"]
|
||||
* "no numbers".match(/\d+/); // null
|
||||
* "hello".concat(" ", "world"); // "hello world"
|
||||
* ```
|
||||
*
|
||||
* ### ❌ Common Mistakes
|
||||
* ```js
|
||||
* str.replace("l", "L"); // Only replaces FIRST "l" — use /l/g for all
|
||||
* String.charAt(0); // TypeError — instance method, not static
|
||||
* "abc"[0]; // May not work in all engines — use charAt()
|
||||
* ```
|
||||
*/
|
||||
export interface String {
|
||||
/** The number of characters in the string */
|
||||
length: number;
|
||||
|
||||
/** Creates a string from one or more Unicode character codes (e.g., String.fromCharCode(65, 66, 67) returns "ABC") */
|
||||
static fromCharCode(...codes: number[]): string;
|
||||
|
||||
/** Returns the Unicode code point (0–65535) of the character at the specified index. Unlike Java's charAt() + cast, this returns the numeric code directly. */
|
||||
charCodeAt(pos: number): number;
|
||||
|
||||
/** Tests the string against a regular expression and returns an array of matches, or null if no match found */
|
||||
match(regexp: any): any[];
|
||||
|
||||
/**
|
||||
* Replaces a pattern (string or RegExp object) with a replacement string.
|
||||
* Unlike Java's replace/replaceAll which only accept String/CharSequence,
|
||||
* this accepts a RegExp object as the first argument.
|
||||
*/
|
||||
replace(searchValue: any, replaceValue: any): string;
|
||||
|
||||
/** Returns the index of the first character that matches a regular expression, or -1 if not found */
|
||||
search(regexp: any): number;
|
||||
}
|
||||
+12
-1
@@ -1,6 +1,14 @@
|
||||
|
||||
pluginManagement {
|
||||
// resolutionStrategy {
|
||||
// eachPlugin {
|
||||
// if(requested.id.toString() == "dts.typescript-generator") {
|
||||
// useModule("com.github.bigguy345:dts-gradle-plugin:95b8ab4")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
repositories {
|
||||
maven { url "https://jitpack.io"}
|
||||
maven {
|
||||
// RetroFuturaGradle
|
||||
name "GTNH Maven"
|
||||
@@ -19,3 +27,6 @@ pluginManagement {
|
||||
plugins {
|
||||
id 'com.gtnewhorizons.gtnhsettingsconvention' version '1.0.27'
|
||||
}
|
||||
|
||||
// Include the local gradle-plugins composite build so plugin is resolved locally
|
||||
includeBuild 'gradle-plugins'
|
||||
@@ -159,6 +159,8 @@ import noppes.npcs.client.gui.roles.GuiNpcTransporter;
|
||||
import noppes.npcs.client.gui.script.GuiScriptGlobal;
|
||||
import noppes.npcs.client.gui.script.GuiScriptInterface;
|
||||
import noppes.npcs.client.gui.util.script.PackageFinder;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.ClassIndex;
|
||||
import noppes.npcs.client.model.ModelNPCGolem;
|
||||
import noppes.npcs.client.model.ModelNpcCrystal;
|
||||
import noppes.npcs.client.model.ModelNpcDragon;
|
||||
@@ -863,6 +865,8 @@ public class ClientProxy extends CommonProxy {
|
||||
public void buildPackageIndex() {
|
||||
try {
|
||||
PackageFinder.init(Thread.currentThread().getContextClassLoader());
|
||||
ClassIndex.init();
|
||||
JSTypeRegistry.getInstance().initializeFromResources();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import net.minecraft.util.ResourceLocation;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import noppes.npcs.CustomNpcs;
|
||||
import noppes.npcs.LogWriter;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.Graphics;
|
||||
@@ -30,6 +31,11 @@ public class CustomNpcResourceListener implements
|
||||
|
||||
FolderResourcePack pack = new FolderResourcePack(CustomNpcs.Dir);
|
||||
simplemanager.reloadResourcePack(pack);
|
||||
|
||||
// Reload scripting .d.ts files
|
||||
JSTypeRegistry registry = JSTypeRegistry.getInstance();
|
||||
registry.clear();
|
||||
registry.initializeFromResources();
|
||||
|
||||
try {
|
||||
DefaultTextColor = Integer.parseInt(StatCollector.translateToLocal("customnpcs.defaultTextColor"), 16);
|
||||
|
||||
@@ -16,6 +16,8 @@ import noppes.npcs.client.gui.util.GuiNpcButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcLabel;
|
||||
import noppes.npcs.client.gui.util.GuiNpcTextArea;
|
||||
import noppes.npcs.client.gui.util.GuiScriptTextArea;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer;
|
||||
import noppes.npcs.constants.EnumScriptType;
|
||||
import noppes.npcs.constants.ScriptContext;
|
||||
import noppes.npcs.controllers.ScriptContainer;
|
||||
import noppes.npcs.controllers.ScriptController;
|
||||
@@ -88,18 +90,25 @@ public class GuiScript extends GuiScriptInterface {
|
||||
super.initGui();
|
||||
this.guiTop += 10;
|
||||
|
||||
// ==================== TOP BUTTONS (hidden in fullscreen) ====================
|
||||
if (!isFullscreen) {
|
||||
GuiMenuTopButton top;
|
||||
addTopButton(top = new GuiMenuTopButton(13, guiLeft + 4, guiTop - 17, "script.scripts"));
|
||||
addTopButton(new GuiMenuTopButton(16, guiLeft + (xSize - 102), guiTop - 17, "eventscript.eventScripts"));
|
||||
addTopButton(new GuiMenuTopButton(17, guiLeft + (xSize - 22), guiTop - 17, "X"));
|
||||
top.active = showScript;
|
||||
addTopButton(top = new GuiMenuTopButton(14, top, "gui.settings"));
|
||||
top.active = !showScript;
|
||||
addTopButton(new GuiMenuTopButton(15, top, "gui.website"));
|
||||
if (isFullscreen) {
|
||||
FullscreenConfig.paddingTop = 30;
|
||||
}
|
||||
|
||||
// ==================== TOP BUTTONS ====================
|
||||
boolean isFullscreenView = isFullscreen && showScript;
|
||||
int menuX = isFullscreenView ? FullscreenConfig.paddingLeft : guiLeft + 4;
|
||||
int menuY = isFullscreenView ? FullscreenConfig.paddingTop - 20 : guiTop - 17;
|
||||
int rightX = isFullscreenView ? width - FullscreenConfig.paddingRight : guiLeft + xSize;
|
||||
|
||||
GuiMenuTopButton top;
|
||||
addTopButton(top = new GuiMenuTopButton(13, menuX, menuY, "script.scripts"));
|
||||
addTopButton(new GuiMenuTopButton(16, rightX - 102, menuY, "eventscript.eventScripts"));
|
||||
addTopButton(new GuiMenuTopButton(17, rightX - 22, menuY, "X"));
|
||||
top.active = showScript;
|
||||
addTopButton(top = new GuiMenuTopButton(14, top, "gui.settings"));
|
||||
top.active = !showScript;
|
||||
addTopButton(new GuiMenuTopButton(15, top, "gui.website"));
|
||||
|
||||
if (showScript) {
|
||||
initScriptView();
|
||||
} else {
|
||||
@@ -162,6 +171,10 @@ public class GuiScript extends GuiScriptInterface {
|
||||
String language = (container != null) ? container.getLanguage() : script.getLanguage();
|
||||
activeArea.setLanguage(language);
|
||||
activeArea.setScriptContext(getScriptContext());
|
||||
|
||||
// Set editor globals based on the active NPC hook
|
||||
String hookName = EnumScriptType.values()[activeTab].function;
|
||||
applyEditorGlobals(activeArea, hookName);
|
||||
|
||||
// Setup fullscreen key binding
|
||||
GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> {
|
||||
@@ -244,6 +257,19 @@ public class GuiScript extends GuiScriptInterface {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply editor globals for the active NPC hook.
|
||||
private void applyEditorGlobals(GuiScriptTextArea activeArea, String hookName) {
|
||||
if (activeArea == null)
|
||||
return;
|
||||
|
||||
ScriptTextContainer textContainer = activeArea.getContainer();
|
||||
if (textContainer == null)
|
||||
return;
|
||||
|
||||
if (script != null)
|
||||
textContainer.setEditorGlobalsMap(script.getEditorGlobals(hookName));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getConsoleText() {
|
||||
Map<Long, String> map = this.script.getOldConsoleText();
|
||||
@@ -382,24 +408,4 @@ public class GuiScript extends GuiScriptInterface {
|
||||
initGui();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawScreen(int mouseX, int mouseY, float partialTicks) {
|
||||
super.drawScreen(mouseX, mouseY, partialTicks);
|
||||
|
||||
// Draw fullscreen button when in script view (GuiScript uses 0-based activeTab)
|
||||
if (showScript) {
|
||||
fullscreenButton.draw(mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClicked(int mouseX, int mouseY, int mouseButton) {
|
||||
// Check fullscreen button first when in script view
|
||||
if (showScript && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) {
|
||||
return;
|
||||
}
|
||||
super.mouseClicked(mouseX, mouseY, mouseButton);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import noppes.npcs.client.gui.util.IGuiData;
|
||||
import noppes.npcs.client.gui.util.IJTextAreaListener;
|
||||
import noppes.npcs.client.gui.util.ITextChangeListener;
|
||||
import noppes.npcs.client.gui.util.ITextfieldListener;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry;
|
||||
import noppes.npcs.constants.ScriptContext;
|
||||
import noppes.npcs.controllers.ScriptContainer;
|
||||
import noppes.npcs.controllers.ScriptController;
|
||||
@@ -32,15 +33,11 @@ import noppes.npcs.controllers.data.ForgeDataScript;
|
||||
import noppes.npcs.controllers.data.IScriptHandler;
|
||||
import noppes.npcs.controllers.data.IScriptHandlerPacket;
|
||||
import noppes.npcs.controllers.data.IScriptUnit;
|
||||
import noppes.npcs.janino.JaninoScript;
|
||||
import noppes.npcs.scripted.item.ScriptCustomItem;
|
||||
import org.lwjgl.opengl.Display;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallback, IGuiData, ITextChangeListener, ICustomScrollListener, IJTextAreaListener, ITextfieldListener {
|
||||
@@ -105,6 +102,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
|
||||
this.drawDefaultBackground = true;
|
||||
this.closeOnEsc = true;
|
||||
this.xSize = 420;
|
||||
this.isPannableGUI = true;
|
||||
this.setBackground("menubg.png");
|
||||
}
|
||||
|
||||
@@ -120,6 +118,12 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
|
||||
// Initialize hooks from handler
|
||||
gui.hookList = new ArrayList<>(handler.getHooks());
|
||||
|
||||
JSTypeRegistry registry = JSTypeRegistry.getInstance();
|
||||
if (!registry.isInitialized()) {
|
||||
registry.initializeFromResources();
|
||||
}
|
||||
registry.syncHooksFromScriptHookControllerIfNeeded();
|
||||
|
||||
// Request data from server
|
||||
if (handler instanceof IScriptHandlerPacket)
|
||||
((IScriptHandlerPacket) handler).requestData();
|
||||
@@ -139,6 +143,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
|
||||
|
||||
public void initGui() {
|
||||
// ==================== BASE LAYOUT CALCULATION ====================
|
||||
isPanning = false;
|
||||
this.ySize = (int) ((double) this.xSize * 0.56D);
|
||||
if ((double) this.ySize > (double) this.height * 0.95D) {
|
||||
this.ySize = (int) ((double) this.height * 0.95D);
|
||||
@@ -278,6 +283,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
|
||||
|
||||
// Set the script context for context-aware hook autocomplete
|
||||
activeArea.setScriptContext(getScriptContext());
|
||||
activeArea.enableCodeHighlighting();
|
||||
|
||||
updateScriptDocumentImports(activeArea, container);
|
||||
|
||||
// Setup fullscreen key binding
|
||||
GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> {
|
||||
@@ -328,8 +336,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
|
||||
}
|
||||
this.addScroll(scroll);
|
||||
}
|
||||
}
|
||||
|
||||
computePanBounds(editorX, editorY, editorWidth, editorHeight);
|
||||
}
|
||||
/**
|
||||
* Initialize the settings tab layout (console view).
|
||||
*/
|
||||
@@ -371,6 +380,12 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
|
||||
// Language button requires options to be available
|
||||
this.getButton(103).enabled = languageOptions.size() > 0;
|
||||
}
|
||||
|
||||
int consoleX = this.guiLeft + 4 + yoffset;
|
||||
int consoleY = this.guiTop + 6 + yoffset;
|
||||
int consoleW = this.xSize - 160 - yoffset;
|
||||
int consoleH = (int) ((float) this.ySize * 0.92F) - yoffset * 2;
|
||||
computePanBounds(consoleX, consoleY, consoleW, consoleH);
|
||||
}
|
||||
|
||||
public GuiScriptInterface setDimensions(int x, int y) {
|
||||
@@ -389,30 +404,34 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
|
||||
return handler.getContext();
|
||||
}
|
||||
|
||||
// ==================== RENDERING ====================
|
||||
|
||||
@Override
|
||||
public void drawScreen(int mouseX, int mouseY, float partialTicks) {
|
||||
super.drawScreen(mouseX, mouseY, partialTicks);
|
||||
|
||||
// Draw fullscreen button on top of everything when on script editor tab
|
||||
// Skip for useSettingsToggle GUIs (like GuiScript) - they handle this themselves
|
||||
if (this.activeTab > 0 && !useSettingsToggle && !handler.isSingleContainer()) {
|
||||
fullscreenButton.draw(mouseX, mouseY);
|
||||
protected boolean isPannableArea(int mx, int my) {
|
||||
if (activeTab > 0 && fullscreenButton.isMouseOver(mx, my)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
GuiScriptTextArea activeArea = getActiveScriptArea();
|
||||
if (activeArea != null && activeArea.isPointOnAutocompleteMenu(mx, my)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.isPannableArea(mx, my);
|
||||
}
|
||||
|
||||
// ==================== MOUSE HANDLING ====================
|
||||
|
||||
@Override
|
||||
public void mouseClicked(int mouseX, int mouseY, int mouseButton) {
|
||||
// Check fullscreen button first when on script editor tab
|
||||
// Skip for useSettingsToggle GUIs (like GuiScript) - they handle this themselves
|
||||
if (this.activeTab > 0 && !useSettingsToggle && !handler.isSingleContainer() && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) {
|
||||
public void mouseClicked(int i, int j, int k) {
|
||||
int adjX = panAdjustedX(i);
|
||||
int adjY = panAdjustedY(j);
|
||||
|
||||
GuiScriptTextArea activeArea = getActiveScriptArea();
|
||||
boolean isOverAutocomplete = activeArea != null
|
||||
&& activeArea.isPointOnAutocompleteMenu(adjX, adjY);
|
||||
if (isOverAutocomplete) {
|
||||
activeArea.mouseClicked(adjX, adjY, k);
|
||||
return;
|
||||
}
|
||||
|
||||
super.mouseClicked(mouseX, mouseY, mouseButton);
|
||||
super.mouseClicked(i, j, k);
|
||||
}
|
||||
|
||||
public String previousHookClicked = "";
|
||||
@@ -435,11 +454,40 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
|
||||
|
||||
this.getTextField(2).setText(this.getTextField(2).getText() + addString);
|
||||
previousHookClicked = "";
|
||||
|
||||
updateScriptDocumentImports(getActiveScriptArea(), container);
|
||||
} else {
|
||||
previousHookClicked = hook;
|
||||
}
|
||||
}
|
||||
|
||||
public void updateScriptDocumentImports(GuiScriptTextArea activeArea, IScriptUnit container) {
|
||||
// For JaninoScripts, add implicit imports (default imports + hook signature types)
|
||||
// These allow the syntax highlighter to resolve types without explicit import statements
|
||||
if (container instanceof JaninoScript) {
|
||||
JaninoScript<?> janinoScript = (JaninoScript<?>) container;
|
||||
ScriptContext ctx = getScriptContext();
|
||||
// Add default imports (e.g., noppes.npcs.api.*, noppes.npcs.api.entity.*, etc.)
|
||||
activeArea.addImplicitImports(janinoScript.getDefaultImports());
|
||||
|
||||
// Add hook types from signatures (parameters + return types)
|
||||
// e.g., INpcEvent.InitEvent, Color, String, IOverlayContext, etc.
|
||||
Set<String> hookTypes = janinoScript.getHookTypes();
|
||||
activeArea.addImplicitImports(hookTypes.toArray(new String[0]));
|
||||
|
||||
// Add wildcard imports for each event class in the script context: INpcEvent.*, IProjectileEvent.*
|
||||
// so nested types like INpcEvent.CollideEvent resolve without explicit import
|
||||
Set<String> wildcardImports = new HashSet<>();
|
||||
for (String fqn : ctx.getNamespaceFQNs())
|
||||
wildcardImports.add(fqn + ".*");
|
||||
activeArea.addImplicitImports(wildcardImports.toArray(new String[0]));
|
||||
|
||||
|
||||
// Format to update which implicit imports are actually used
|
||||
activeArea.formatCodeText();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unFocused(GuiNpcTextField textfield) {
|
||||
|
||||
|
||||
@@ -61,6 +61,30 @@ public abstract class GuiNPCInterface extends GuiScreen {
|
||||
public float bgScaleZ = 1;
|
||||
public int bgTextureHeight = 256; // Actual content height in texture (for bottom border stitching)
|
||||
|
||||
// ==================== VIEWPORT PANNING ====================
|
||||
/**
|
||||
* If true, this GUI supports viewport panning via click-drag on empty areas.
|
||||
* Subclasses set this to true and call computePanBounds() to enable.
|
||||
*/
|
||||
protected boolean isPannableGUI = false;
|
||||
protected boolean isPanning = false;
|
||||
protected double panStartX, panStartY;
|
||||
|
||||
/**
|
||||
* Cumulative pan offset in scaled GUI pixels.
|
||||
* Positive values = viewport shifted right/down (content appears to move left/up).
|
||||
*/
|
||||
public double currentPanX, currentPanY;
|
||||
|
||||
public double getPanX() { return currentPanX; }
|
||||
public double getPanY() { return currentPanY; }
|
||||
|
||||
protected double minPanX, maxPanX, minPanY, maxPanY;
|
||||
protected int panViewportX, panViewportY, panViewportWidth, panViewportHeight;
|
||||
|
||||
/** Pan-adjusted mouse coordinates, updated every frame in drawScreen for hit-testing panned components. */
|
||||
protected int panAdjMouseX, panAdjMouseY;
|
||||
|
||||
public GuiNPCInterface(EntityNPCInterface npc) {
|
||||
this.player = Minecraft.getMinecraft().thePlayer;
|
||||
this.npc = npc;
|
||||
@@ -161,28 +185,39 @@ public abstract class GuiNPCInterface extends GuiScreen {
|
||||
if (subgui != null)
|
||||
subgui.mouseClicked(i, j, k);
|
||||
else {
|
||||
for (GuiNpcTextField tf : new ArrayList<GuiNpcTextField>(textfields.values()))
|
||||
if (tf.enabled)
|
||||
tf.mouseClicked(i, j, k);
|
||||
int adjX = isPannableGUI ? panAdjustedX(i) : i;
|
||||
int adjY = isPannableGUI ? panAdjustedY(j) : j;
|
||||
|
||||
if (isPannableGUI && k == 0 && isPannableArea(adjX, adjY)) {
|
||||
isPanning = true;
|
||||
panStartX = i;
|
||||
panStartY = j;
|
||||
return;
|
||||
}
|
||||
|
||||
mouseEvent(adjX, adjY, k);
|
||||
vanillaMouseClicked(adjX, adjY, k);
|
||||
|
||||
for (GuiScrollWindow guiScrollableComponent : scrollWindows.values()) {
|
||||
guiScrollableComponent.mouseClicked(i, j, k);
|
||||
guiScrollableComponent.mouseClicked(adjX, adjY, k);
|
||||
}
|
||||
|
||||
if (k == 0) {
|
||||
for (GuiCustomScroll scroll : new ArrayList<GuiCustomScroll>(scrolls.values())) {
|
||||
scroll.mouseClicked(i, j, k);
|
||||
scroll.mouseClicked(adjX, adjY, k);
|
||||
}
|
||||
}
|
||||
// Process diagram mouse clicks
|
||||
for (GuiDiagram diagram : diagrams.values()) {
|
||||
if (diagram.isWithin(i, j)) {
|
||||
if (diagram.mouseClicked(i, j, k))
|
||||
if (diagram.isWithin(adjX, adjY)) {
|
||||
if (diagram.mouseClicked(adjX, adjY, k))
|
||||
return;
|
||||
}
|
||||
}
|
||||
mouseEvent(i, j, k);
|
||||
vanillaMouseClicked(i, j, k);
|
||||
|
||||
for (GuiNpcTextField tf : new ArrayList<GuiNpcTextField>(textfields.values()))
|
||||
if (tf.enabled)
|
||||
tf.mouseClicked(adjX, adjY, k);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +258,12 @@ public abstract class GuiNPCInterface extends GuiScreen {
|
||||
subgui.mouseClickMove(mouseX, mouseY, clickedMouseButton, timeSinceLastClick);
|
||||
return;
|
||||
}
|
||||
super.mouseClickMove(mouseX, mouseY, clickedMouseButton, timeSinceLastClick);
|
||||
if (isPannableGUI && isPanning) {
|
||||
return;
|
||||
}
|
||||
int adjX = isPannableGUI ? panAdjustedX(mouseX) : mouseX;
|
||||
int adjY = isPannableGUI ? panAdjustedY(mouseY) : mouseY;
|
||||
super.mouseClickMove(adjX, adjY, clickedMouseButton, timeSinceLastClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -232,12 +272,72 @@ public abstract class GuiNPCInterface extends GuiScreen {
|
||||
subgui.mouseMovedOrUp(mouseX, mouseY, state);
|
||||
return;
|
||||
}
|
||||
super.mouseMovedOrUp(mouseX, mouseY, state);
|
||||
if (isPannableGUI && state == 0 && isPanning) {
|
||||
isPanning = false;
|
||||
panStartX = 0;
|
||||
panStartY = 0;
|
||||
return;
|
||||
}
|
||||
int adjX = isPannableGUI ? panAdjustedX(mouseX) : mouseX;
|
||||
int adjY = isPannableGUI ? panAdjustedY(mouseY) : mouseY;
|
||||
super.mouseMovedOrUp(adjX, adjY, state);
|
||||
}
|
||||
|
||||
public void mouseEvent(int i, int j, int k) {
|
||||
}
|
||||
|
||||
// ==================== VIEWPORT PANNING METHODS ====================
|
||||
|
||||
protected boolean isPannableArea(int mx, int my) {
|
||||
for (Object obj : this.buttonList) {
|
||||
GuiButton btn = (GuiButton) obj;
|
||||
if (btn.visible && mx >= btn.xPosition && mx < btn.xPosition + btn.width
|
||||
&& my >= btn.yPosition && my < btn.yPosition + btn.height) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (GuiCustomScroll scroll : scrolls.values()) {
|
||||
if (scroll.visible && scroll.isMouseOver(mx, my)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (GuiNpcTextField tf : textfields.values()) {
|
||||
if (tf.enabled && mx >= tf.xPosition && mx < tf.xPosition + tf.width
|
||||
&& my >= tf.yPosition && my < tf.yPosition + tf.height) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void computePanBounds(int vpX, int vpY, int vpW, int vpH) {
|
||||
this.panViewportX = vpX;
|
||||
this.panViewportY = vpY;
|
||||
this.panViewportWidth = vpW;
|
||||
this.panViewportHeight = vpH;
|
||||
|
||||
int panRange = Math.max(this.width, this.height) / 2;
|
||||
this.minPanX = -panRange;
|
||||
this.maxPanX = panRange;
|
||||
this.minPanY = -panRange;
|
||||
this.maxPanY = panRange;
|
||||
}
|
||||
|
||||
protected int panAdjustedX(int rawMouseX) {
|
||||
return rawMouseX + (int) currentPanX;
|
||||
}
|
||||
|
||||
protected int panAdjustedY(int rawMouseY) {
|
||||
return rawMouseY + (int) currentPanY;
|
||||
}
|
||||
|
||||
public boolean hasPanOffset() {
|
||||
return isPannableGUI && (currentPanX != 0 || currentPanY != 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void actionPerformed(GuiButton guibutton) {
|
||||
if (subgui != null)
|
||||
@@ -375,9 +475,41 @@ public abstract class GuiNPCInterface extends GuiScreen {
|
||||
public void drawScreen(int i, int j, float f) {
|
||||
mouseX = i;
|
||||
mouseY = j;
|
||||
|
||||
boolean panning = hasPanOffset();
|
||||
int drawX = panning ? panAdjustedX(i) : i;
|
||||
int drawY = panning ? panAdjustedY(j) : j;
|
||||
panAdjMouseX = drawX;
|
||||
panAdjMouseY = drawY;
|
||||
|
||||
// Smooth pan: update pan offset every frame using raw mouse delta
|
||||
if (isPannableGUI && isPanning && Mouse.isButtonDown(0)) {
|
||||
double deltaX = i - panStartX;
|
||||
double deltaY = j - panStartY;
|
||||
|
||||
currentPanX = Math.max(minPanX, Math.min(maxPanX, currentPanX - deltaX));
|
||||
currentPanY = Math.max(minPanY, Math.min(maxPanY, currentPanY - deltaY));
|
||||
|
||||
panStartX = i;
|
||||
panStartY = j;
|
||||
|
||||
panning = hasPanOffset();
|
||||
drawX = panning ? panAdjustedX(i) : i;
|
||||
drawY = panning ? panAdjustedY(j) : j;
|
||||
panAdjMouseX = drawX;
|
||||
panAdjMouseY = drawY;
|
||||
} else if (isPanning && !Mouse.isButtonDown(0)) {
|
||||
isPanning = false;
|
||||
}
|
||||
|
||||
if (drawDefaultBackground && subgui == null)
|
||||
drawDefaultBackground();
|
||||
|
||||
if (panning) {
|
||||
GL11.glPushMatrix();
|
||||
GL11.glTranslated(-currentPanX, -currentPanY, 0);
|
||||
}
|
||||
|
||||
if (background != null && mc.renderEngine != null) {
|
||||
drawBackground();
|
||||
}
|
||||
@@ -386,45 +518,45 @@ public abstract class GuiNPCInterface extends GuiScreen {
|
||||
drawCenteredString(fontRendererObj, title, width / 2, guiTop + 4, 0xffffff);
|
||||
for (GuiNpcLabel label : labels.values())
|
||||
label.drawLabel(this, fontRendererObj);
|
||||
for (GuiNpcTextField tf : textfields.values()) {
|
||||
tf.drawTextBox(i, j);
|
||||
}
|
||||
for (GuiCustomScroll scroll : scrolls.values()) {
|
||||
scroll.updateSubGUI(subGui);
|
||||
scroll.drawScreen(i, j, f, !subGui && scroll.isMouseOver(i, j) ? Mouse.getDWheel() : 0);
|
||||
scroll.drawScreen(drawX, drawY, f, !subGui && scroll.isMouseOver(drawX, drawY) ? Mouse.getDWheel() : 0);
|
||||
}
|
||||
// Draw scrollable windows.
|
||||
for (GuiScrollWindow guiScrollableComponent : scrollWindows.values()) {
|
||||
guiScrollableComponent.drawScreen(i, j, f, !subGui && guiScrollableComponent.isMouseOver(i, j) ? Mouse.getDWheel() : 0);
|
||||
guiScrollableComponent.drawScreen(drawX, drawY, f, !subGui && guiScrollableComponent.isMouseOver(drawX, drawY) ? Mouse.getDWheel() : 0);
|
||||
}
|
||||
for (GuiDiagram diagram : diagrams.values()) {
|
||||
diagram.drawDiagram(i, j, subGui);
|
||||
diagram.drawDiagram(drawX, drawY, subGui);
|
||||
}
|
||||
super.drawScreen(i, j, f);
|
||||
super.drawScreen(drawX, drawY, f);
|
||||
for (GuiCustomScroll scroll : scrolls.values())
|
||||
if (scroll.hoverableText) {
|
||||
scroll.drawHover(i, j);
|
||||
scroll.drawHover(drawX, drawY);
|
||||
}
|
||||
for (GuiNpcButton button : buttons.values()) {
|
||||
button.updateSubGUI(subGui);
|
||||
if (!button.hoverableText.isEmpty()) {
|
||||
button.drawHover(i, j, subGui);
|
||||
button.drawHover(drawX, drawY, subGui);
|
||||
}
|
||||
}
|
||||
for (GuiNpcTextField textField : textfields.values()) {
|
||||
if (textField.hasHoverText()) {
|
||||
textField.drawHover(i, j, subGui);
|
||||
}
|
||||
for (GuiNpcTextField tf : textfields.values()) {
|
||||
tf.drawTextBox(drawX, drawY);
|
||||
if (tf.hasHoverText())
|
||||
tf.drawHover(drawX, drawY, subGui);
|
||||
}
|
||||
|
||||
for(GuiScrollWindow guiScrollableComponent : scrollWindows.values()) {
|
||||
if (guiScrollableComponent.isMouseOver(mouseX, mouseY)) {
|
||||
guiScrollableComponent.drawHoverTexts(mouseX, mouseY);
|
||||
if (guiScrollableComponent.isMouseOver(drawX, drawY)) {
|
||||
guiScrollableComponent.drawHoverTexts(drawX, drawY);
|
||||
}
|
||||
}
|
||||
|
||||
for (GuiScreen gui : extra.values())
|
||||
gui.drawScreen(i, j, f);
|
||||
gui.drawScreen(drawX, drawY, f);
|
||||
|
||||
if (panning) {
|
||||
GL11.glPopMatrix();
|
||||
}
|
||||
|
||||
if (subgui != null) {
|
||||
subgui.drawScreen(i, j, f);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -10,11 +10,19 @@ import org.lwjgl.opengl.GL11;
|
||||
public class GuiUtil {
|
||||
|
||||
public static void setScissorClip(int x, int y, int width, int height) {
|
||||
setScissorClip(x, y, width, height, null);
|
||||
}
|
||||
|
||||
public static void setScissorClip(int x, int y, int width, int height, GuiNPCInterface gui) {
|
||||
Minecraft mc = Minecraft.getMinecraft();
|
||||
ScaledResolution res = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
||||
int scale = res.getScaleFactor();
|
||||
|
||||
|
||||
if (gui != null) {
|
||||
x -= (int) gui.getPanX();
|
||||
y -= (int) gui.getPanY();
|
||||
}
|
||||
|
||||
// Adjust position to Top-Left origin (OpenGL window/screen space uses bottom-left origin)
|
||||
y = mc.displayHeight - y * scale;
|
||||
height *= scale;
|
||||
@@ -159,6 +167,32 @@ public class GuiUtil {
|
||||
tessellator.draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a scaled textured rectangle (for rendering icons).
|
||||
*/
|
||||
public static void drawScaledTexturedRect(int x, int y, int u, int v, int srcWidth, int srcHeight,
|
||||
int destWidth, int destHeight, int textureWidth, int textureHeight) {
|
||||
float uScale = 1.0f / textureWidth;
|
||||
float vScale = 1.0f / textureHeight;
|
||||
|
||||
GL11.glEnable(GL11.GL_BLEND);
|
||||
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
GL11.glBegin(GL11.GL_QUADS);
|
||||
GL11.glTexCoord2f(u * uScale, v * vScale);
|
||||
GL11.glVertex2f(x, y);
|
||||
GL11.glTexCoord2f(u * uScale, (v + srcHeight) * vScale);
|
||||
GL11.glVertex2f(x, y + destHeight);
|
||||
GL11.glTexCoord2f((u + srcWidth) * uScale, (v + srcHeight) * vScale);
|
||||
GL11.glVertex2f(x + destWidth, y + destHeight);
|
||||
GL11.glTexCoord2f((u + srcWidth) * uScale, v * vScale);
|
||||
GL11.glVertex2f(x + destWidth, y);
|
||||
GL11.glEnd();
|
||||
|
||||
GL11.glDisable(GL11.GL_BLEND);
|
||||
}
|
||||
|
||||
|
||||
public static void setMouse(int guiX, int guiY) {
|
||||
Minecraft mc = Minecraft.getMinecraft();
|
||||
ScaledResolution scaledResolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
||||
|
||||
@@ -375,7 +375,8 @@ public class ClassPathFinder {
|
||||
if (p == null || p.isEmpty()) continue;
|
||||
String[] segs = p.split("\\.");
|
||||
String last = segs[segs.length - 1];
|
||||
boolean lastIsClass = last.length() > 0 && Character.isUpperCase(last.charAt(0));
|
||||
// Check if last segment is actually a class (try to resolve it)
|
||||
boolean lastIsClass = last.length() > 0 && tryResolveClass(p) != null;
|
||||
|
||||
// Try package wildcard: pkg.SimpleName
|
||||
if (!lastIsClass) {
|
||||
@@ -539,6 +540,8 @@ public class ClassPathFinder {
|
||||
private int findFirstUppercaseSegment(String[] segments) {
|
||||
for (int i = 0; i < segments.length; i++) {
|
||||
if (segments[i].length() > 0 && Character.isUpperCase(segments[i].charAt(0))) {
|
||||
// Keep uppercase check here as it's a heuristic for package splitting
|
||||
// This is about Java naming conventions, not actual type resolution
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -653,10 +656,10 @@ public class ClassPathFinder {
|
||||
}
|
||||
String typeName = content.substring(start, i);
|
||||
|
||||
// Only process if it looks like a type name (starts with uppercase)
|
||||
if (Character.isUpperCase(typeName.charAt(0))) {
|
||||
// Resolve the type
|
||||
ClassInfo info = resolveSimpleName(typeName, importedClasses, importedPackages);
|
||||
// Try to resolve as an actual type
|
||||
ClassInfo info = resolveSimpleName(typeName, importedClasses, importedPackages);
|
||||
if (info != null) {
|
||||
// Only process if it actually resolves to a type
|
||||
ClassType classType = (info != null) ? info.type : ClassType.CLASS;
|
||||
|
||||
results.add(new TypeOccurrence(baseOffset + start, baseOffset + i, typeName, classType));
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -8,6 +8,7 @@ import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Deprecated
|
||||
public class JavaTextContainer extends TextContainer {
|
||||
|
||||
public static final Pattern MODIFIER = Pattern.compile(
|
||||
|
||||
@@ -2,6 +2,12 @@ package noppes.npcs.client.gui.util.script;
|
||||
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
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.method.MethodInfo;
|
||||
import org.lwjgl.input.Keyboard;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -11,7 +17,7 @@ import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Handles IntelliJ-like rename refactoring with scope-aware renaming.
|
||||
* <p>
|
||||
*
|
||||
* Features:
|
||||
* - Scope detection: local variables rename only within their block, global fields rename everywhere
|
||||
* - Visual feedback: primary occurrence has white border, others just highlighted
|
||||
@@ -32,7 +38,11 @@ public class RenameRefactorHandler {
|
||||
private int primaryOccurrenceEnd = -1;
|
||||
private List<int[]> allOccurrences = new ArrayList<>();
|
||||
private ScopeInfo scope = null;
|
||||
|
||||
|
||||
// For parameter renaming: track containing method to find JSDoc
|
||||
private boolean isParameterRename = false;
|
||||
private MethodInfo containingMethod = null;
|
||||
|
||||
// For global scope, track positions that have local shadowing (to exclude them)
|
||||
private List<int[]> localShadowedPositions = new ArrayList<>();
|
||||
|
||||
@@ -82,32 +92,11 @@ public class RenameRefactorHandler {
|
||||
|
||||
void scrollToPosition(int pos);
|
||||
|
||||
JavaTextContainer getContainer();
|
||||
ScriptTextContainer getContainer();
|
||||
|
||||
int getViewportWidth(); // For determining if clicks are in rename box
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope information for a variable
|
||||
*/
|
||||
public static class ScopeInfo {
|
||||
public final int startOffset;
|
||||
public final int endOffset;
|
||||
public final boolean isGlobal;
|
||||
public final String scopeType; // "global", "method", "block"
|
||||
|
||||
public ScopeInfo(int start, int end, boolean isGlobal, String type) {
|
||||
this.startOffset = start;
|
||||
this.endOffset = end;
|
||||
this.isGlobal = isGlobal;
|
||||
this.scopeType = type;
|
||||
}
|
||||
|
||||
public boolean containsPosition(int pos) {
|
||||
return pos >= startOffset && pos < endOffset;
|
||||
}
|
||||
}
|
||||
|
||||
public void setCallback(RenameCallback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
@@ -140,29 +129,62 @@ public class RenameRefactorHandler {
|
||||
initialWord = text.substring(wordBounds[0], wordBounds[1]); // Remember for undo
|
||||
originalWord = initialWord;
|
||||
currentWord = initialWord;
|
||||
|
||||
|
||||
// Determine scope for this identifier - MUST check if local shadows global
|
||||
scope = determineScope(text, wordBounds[0], originalWord, callback.getContainer());
|
||||
|
||||
|
||||
// Track if this is a parameter rename by checking if position is in any method's parameter declaration
|
||||
isParameterRename = false;
|
||||
containingMethod = null;
|
||||
|
||||
ScriptTextContainer container = callback.getContainer();
|
||||
if (container != null && container.getDocument() != null) {
|
||||
ScriptDocument document = container.getDocument();
|
||||
List<MethodInfo> methods = document.getAllMethods();
|
||||
|
||||
// Check if the word at cursor position is a parameter of any method
|
||||
// A rename is a parameter rename if:
|
||||
// 1. The word matches a parameter name
|
||||
// 2. The cursor is within that method's declaration or body
|
||||
for (MethodInfo method : methods) {
|
||||
// Check if cursor is within this method's range
|
||||
int methodStart = method.getFullDeclarationOffset();
|
||||
int methodEnd = method.getBodyEnd();
|
||||
if (methodStart < 0 || methodEnd < 0) continue; // External method
|
||||
|
||||
if (wordBounds[0] >= methodStart && wordBounds[0] <= methodEnd) {
|
||||
// Check if the word matches any parameter of this method
|
||||
for (FieldInfo param : method.getParameters()) {
|
||||
if (param.getName().equals(originalWord)) {
|
||||
isParameterRename = true;
|
||||
containingMethod = method;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isParameterRename) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If global scope, find all positions where local variables shadow this name
|
||||
localShadowedPositions.clear();
|
||||
if (scope != null && scope.isGlobal) {
|
||||
findLocalShadowedPositions(text, originalWord);
|
||||
}
|
||||
|
||||
// Find all occurrences within scope
|
||||
// Find all occurrences within scope (including JSDoc if it's a parameter)
|
||||
findOccurrences(text, originalWord);
|
||||
|
||||
if (allOccurrences.isEmpty())
|
||||
return false;
|
||||
|
||||
active = true;
|
||||
|
||||
|
||||
// Set selection to full word (uses GuiScriptTextArea's selection)
|
||||
primaryOccurrenceStart = wordBounds[0];
|
||||
primaryOccurrenceEnd = wordBounds[1];
|
||||
callback.getSelectionState().setSelection(wordBounds[0], wordBounds[1]);
|
||||
// callback.setCursorPosition(wordBounds[1]);
|
||||
// callback.setCursorPosition(wordBounds[1]);
|
||||
callback.getSelectionState().markActivity();
|
||||
|
||||
return true;
|
||||
@@ -206,7 +228,7 @@ public class RenameRefactorHandler {
|
||||
// During live editing we used setTextWithoutUndo to avoid creating intermediate
|
||||
// undo entries; pushing the original snapshot here gives a single predictable
|
||||
// undo step back to the pre-rename state.
|
||||
if (originalText != null && !originalText.isEmpty())
|
||||
if (originalText != null && !originalText.isEmpty())
|
||||
callback.pushUndoState(originalText, originalCursorPos);
|
||||
|
||||
// Calculate cursor position relative to primary occurrence
|
||||
@@ -230,13 +252,14 @@ public class RenameRefactorHandler {
|
||||
allOccurrences.clear();
|
||||
localShadowedPositions.clear();
|
||||
scope = null;
|
||||
isParameterRename = false;
|
||||
containingMethod = null;
|
||||
originalText = "";
|
||||
originalCursorPos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle keyboard input during rename
|
||||
*
|
||||
* @return true if input was consumed
|
||||
*/
|
||||
public boolean keyTyped(char c, int keyCode) {
|
||||
@@ -264,7 +287,7 @@ public class RenameRefactorHandler {
|
||||
// Get selection state
|
||||
SelectionState sel = callback.getSelectionState();
|
||||
boolean hasSelection = sel.hasSelection();
|
||||
|
||||
|
||||
// Calculate cursor position within the word
|
||||
int cursorInWord = callback.getCursorPosition() - primaryOccurrenceStart;
|
||||
cursorInWord = Math.max(0, Math.min(cursorInWord, currentWord.length()));
|
||||
@@ -315,7 +338,7 @@ public class RenameRefactorHandler {
|
||||
sel.setSelection(primaryOccurrenceStart, primaryOccurrenceStart);
|
||||
callback.setCursorPosition(primaryOccurrenceStart);
|
||||
}
|
||||
|
||||
|
||||
// Left arrow
|
||||
if (keyCode == Keyboard.KEY_LEFT) {
|
||||
if (cursorInWord > 0) {
|
||||
@@ -354,7 +377,7 @@ public class RenameRefactorHandler {
|
||||
|
||||
// Ctrl+A - select all (select the whole word)
|
||||
if (keyCode == Keyboard.KEY_A &&
|
||||
Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) {
|
||||
Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) {
|
||||
sel.setSelection(primaryOccurrenceStart, primaryOccurrenceStart + currentWord.length());
|
||||
callback.setCursorPosition(primaryOccurrenceStart + currentWord.length());
|
||||
sel.markActivity();
|
||||
@@ -536,23 +559,86 @@ public class RenameRefactorHandler {
|
||||
allOccurrences.add(new int[]{start, end});
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a parameter rename, also look for JSDoc @param occurrences
|
||||
if (isParameterRename && containingMethod != null) {
|
||||
findJSDocParamOccurrences(text, word);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find JSDoc @param occurrences for a parameter name.
|
||||
* Uses the JSDocInfo stored in the MethodInfo to locate the JSDoc comment,
|
||||
* then searches for the parameter name pattern in the current text.
|
||||
*/
|
||||
private void findJSDocParamOccurrences(String text, String paramName) {
|
||||
if (containingMethod == null) return;
|
||||
|
||||
// Get the JSDocInfo from the method to verify it has a @param for our parameter
|
||||
JSDocInfo jsDocInfo = containingMethod.getJSDocInfo();
|
||||
if (jsDocInfo == null) return;
|
||||
|
||||
// Check that this parameter has a JSDoc @param tag (using initial word to find the tag definition)
|
||||
JSDocParamTag paramTag = jsDocInfo.getParamTag(initialWord);
|
||||
if (paramTag == null) return;
|
||||
|
||||
// Get the approximate JSDoc location from the original document
|
||||
int originalJsDocStart = jsDocInfo.getStartOffset();
|
||||
int originalJsDocEnd = jsDocInfo.getEndOffset();
|
||||
if (originalJsDocStart < 0 || originalJsDocEnd < 0) return;
|
||||
|
||||
// Calculate how much positions have shifted due to renames before the JSDoc
|
||||
// Count occurrences that are currently in the text before the original JSDoc start
|
||||
int shiftBeforeJsDoc = 0;
|
||||
int lengthDiff = paramName.length() - initialWord.length();
|
||||
|
||||
// The occurrences we've already found (code occurrences) - count how many are before the JSDoc
|
||||
for (int[] occ : allOccurrences) {
|
||||
// Map current position back to approximate original position
|
||||
int approxOriginalPos = occ[0] - shiftBeforeJsDoc;
|
||||
if (approxOriginalPos < originalJsDocStart) {
|
||||
shiftBeforeJsDoc += lengthDiff;
|
||||
}
|
||||
}
|
||||
|
||||
// Now search for the @param pattern in the current text around where the JSDoc should be
|
||||
int adjustedJsDocStart = originalJsDocStart + shiftBeforeJsDoc;
|
||||
int adjustedJsDocEnd = originalJsDocEnd + shiftBeforeJsDoc;
|
||||
|
||||
// Clamp to text bounds
|
||||
adjustedJsDocStart = Math.max(0, adjustedJsDocStart);
|
||||
adjustedJsDocEnd = Math.min(text.length(), adjustedJsDocEnd + 50); // Add some buffer for expanded text
|
||||
|
||||
if (adjustedJsDocStart >= adjustedJsDocEnd) return;
|
||||
|
||||
String jsDocRegion = text.substring(adjustedJsDocStart, adjustedJsDocEnd);
|
||||
|
||||
// Search for @param {Type} paramName or @param paramName pattern
|
||||
Pattern pattern = Pattern.compile("@param\\s+(?:\\{[^}]*\\}\\s+)?(" + Pattern.quote(paramName) + ")(?:\\s|$|-)");
|
||||
Matcher m = pattern.matcher(jsDocRegion);
|
||||
|
||||
while (m.find()) {
|
||||
int paramNameStart = adjustedJsDocStart + m.start(1);
|
||||
int paramNameEnd = adjustedJsDocStart + m.end(1);
|
||||
allOccurrences.add(new int[]{paramNameStart, paramNameEnd});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all positions where local variables with the same name shadow the global variable.
|
||||
* This populates localShadowedPositions with ranges where local declarations exist.
|
||||
*/
|
||||
private void findLocalShadowedPositions(String text, String varName) {
|
||||
localShadowedPositions.clear();
|
||||
|
||||
|
||||
List<MethodBlock> methods = MethodBlock.collectMethodBlocks(text);
|
||||
List<int[]> excluded = MethodBlock.getExcludedRanges(text);
|
||||
|
||||
|
||||
for (MethodBlock method : methods) {
|
||||
// Check if this method has a local variable or parameter with the same name
|
||||
boolean hasLocalDecl = method.localVariables.contains(varName);
|
||||
boolean hasParamDecl = isParameterInMethod(text, varName, method);
|
||||
|
||||
|
||||
if (hasLocalDecl || hasParamDecl) {
|
||||
// Find where the local/param scope starts
|
||||
int scopeStart;
|
||||
@@ -564,18 +650,18 @@ public class RenameRefactorHandler {
|
||||
scopeStart = findLocalDeclarationPosition(text, varName, method);
|
||||
if (scopeStart < 0) scopeStart = method.startOffset;
|
||||
}
|
||||
|
||||
|
||||
// Find all occurrences of varName within this method's shadowed range
|
||||
Pattern pattern = Pattern.compile("\\b" + Pattern.quote(varName) + "\\b");
|
||||
Matcher m = pattern.matcher(text);
|
||||
|
||||
|
||||
while (m.find()) {
|
||||
int pos = m.start();
|
||||
|
||||
|
||||
// Skip if in string or comment
|
||||
if (isInExcludedRange(pos, excluded))
|
||||
continue;
|
||||
|
||||
|
||||
// Check if within the shadowed scope of this method
|
||||
if (pos >= scopeStart && pos < method.endOffset) {
|
||||
localShadowedPositions.add(new int[]{m.start(), m.end()});
|
||||
@@ -584,7 +670,7 @@ public class RenameRefactorHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if a position is within a locally shadowed range
|
||||
*/
|
||||
@@ -595,13 +681,13 @@ public class RenameRefactorHandler {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if varName is a parameter in the method
|
||||
*/
|
||||
private boolean isParameterInMethod(String text, String varName, MethodBlock method) {
|
||||
String methodHeader = text.substring(method.startOffset,
|
||||
Math.min(method.startOffset + 500, method.endOffset));
|
||||
Math.min(method.startOffset + 500, method.endOffset));
|
||||
int parenStart = methodHeader.indexOf('(');
|
||||
int parenEnd = methodHeader.indexOf(')');
|
||||
if (parenStart >= 0 && parenEnd > parenStart) {
|
||||
@@ -687,7 +773,7 @@ public class RenameRefactorHandler {
|
||||
// Cursor is inside a method
|
||||
// Check if it's a method parameter first
|
||||
String methodHeader = text.substring(containingMethod.startOffset,
|
||||
Math.min(containingMethod.startOffset + 500, containingMethod.endOffset));
|
||||
Math.min(containingMethod.startOffset + 500, containingMethod.endOffset));
|
||||
int parenStart = methodHeader.indexOf('(');
|
||||
int parenEnd = methodHeader.indexOf(')');
|
||||
if (parenStart >= 0 && parenEnd > parenStart) {
|
||||
@@ -861,9 +947,9 @@ public class RenameRefactorHandler {
|
||||
public boolean isWordFullySelected() {
|
||||
if (callback == null) return false;
|
||||
SelectionState selection = callback.getSelectionState();
|
||||
return selection.hasSelection() &&
|
||||
selection.getStartSelection() == primaryOccurrenceStart &&
|
||||
selection.getEndSelection() == primaryOccurrenceEnd;
|
||||
return selection.hasSelection() &&
|
||||
selection.getStartSelection() == primaryOccurrenceStart &&
|
||||
selection.getEndSelection() == primaryOccurrenceEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -884,7 +970,7 @@ public class RenameRefactorHandler {
|
||||
public boolean handleClick(int clickPosInText) {
|
||||
if (!active || callback == null)
|
||||
return false;
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package noppes.npcs.client.gui.util.script;
|
||||
|
||||
/**
|
||||
* Scope information for a variable
|
||||
*/
|
||||
public class ScopeInfo {
|
||||
public final int startOffset;
|
||||
public final int endOffset;
|
||||
public final boolean isGlobal;
|
||||
public final String scopeType; // "global", "method", "block"
|
||||
|
||||
public ScopeInfo(int start, int end, boolean isGlobal, String type) {
|
||||
this.startOffset = start;
|
||||
this.endOffset = end;
|
||||
this.isGlobal = isGlobal;
|
||||
this.scopeType = type;
|
||||
}
|
||||
|
||||
public boolean containsPosition(int pos) {
|
||||
return pos >= startOffset && pos < endOffset;
|
||||
}
|
||||
|
||||
public boolean isWithin(ScopeInfo other) {
|
||||
return this.startOffset >= other.startOffset && this.endOffset <= other.endOffset;
|
||||
}
|
||||
}
|
||||
@@ -21,12 +21,12 @@ public class ScrollState {
|
||||
private int scrollbarDragOffset = 0;
|
||||
// Whether currently dragging the scrollbar
|
||||
private boolean clickScrolling = false;
|
||||
|
||||
|
||||
// Animation parameters
|
||||
private static final double TAU = 0.1; // Time constant (~55ms feels snappy)
|
||||
private static final double SNAP_THRESHOLD = 0.01; // Snap to target when this close
|
||||
private static final double MAX_DT = 0.05; // Max delta time for stability
|
||||
|
||||
private static final double TAU = 0.15; // Time constant (~55ms feels snappy)
|
||||
private static final double SNAP_THRESHOLD = 0.1; // Snap to target when this close (increased to avoid microstutters)
|
||||
private static final double MAX_DT = 0.1; // Max delta time for stability
|
||||
|
||||
/**
|
||||
* Reset scroll state to initial values
|
||||
*/
|
||||
@@ -37,7 +37,7 @@ public class ScrollState {
|
||||
scrolledLine = 0;
|
||||
lastScrollTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize scroll position if not already initialized
|
||||
*/
|
||||
@@ -47,19 +47,18 @@ public class ScrollState {
|
||||
lastScrollTime = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update scroll animation. Call once per frame.
|
||||
*
|
||||
* @param maxScroll Maximum valid scroll position
|
||||
*/
|
||||
public void update(int maxScroll) {
|
||||
long nowMs = System.currentTimeMillis();
|
||||
double dt = Math.min(MAX_DT, (nowMs - lastScrollTime) / 1000.0);
|
||||
lastScrollTime = nowMs;
|
||||
|
||||
|
||||
double dist = targetScroll - scrollPos;
|
||||
|
||||
|
||||
// Snap to target if close enough
|
||||
if (Math.abs(dist) < SNAP_THRESHOLD) {
|
||||
scrollPos = targetScroll;
|
||||
@@ -70,21 +69,21 @@ public class ScrollState {
|
||||
double prev = scrollPos;
|
||||
scrollPos += dist * alpha;
|
||||
scrollVelocity = (scrollPos - prev) / (dt > 0 ? dt : 1e-6);
|
||||
|
||||
|
||||
// Clamp overshoot
|
||||
if ((dist > 0 && scrollPos > targetScroll) || (dist < 0 && scrollPos < targetScroll)) {
|
||||
scrollPos = targetScroll;
|
||||
scrollVelocity = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Keep in bounds
|
||||
clampToBounds(maxScroll);
|
||||
|
||||
|
||||
// Update integer line for rendering
|
||||
scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clamp scroll position to valid bounds
|
||||
*/
|
||||
@@ -95,37 +94,35 @@ public class ScrollState {
|
||||
if (targetScroll > maxScroll) targetScroll = maxScroll;
|
||||
scrolledLine = Math.max(0, Math.min(scrolledLine, maxScroll));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Apply mouse wheel scroll
|
||||
*
|
||||
* @param wheelDelta Positive = scroll up, negative = scroll down
|
||||
* @param maxScroll Maximum valid scroll position
|
||||
* @param maxScroll Maximum valid scroll position
|
||||
*/
|
||||
public void applyWheelScroll(int wheelDelta, int maxScroll) {
|
||||
double sign = Math.copySign(1, wheelDelta);
|
||||
targetScroll -= sign * 2.0; // 2 lines per wheel tick
|
||||
clampToBounds(maxScroll);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set target scroll directly (for scrollbar dragging)
|
||||
*/
|
||||
public void setTargetScroll(double target, int maxScroll) {
|
||||
targetScroll = Math.max(0, Math.min(target, maxScroll));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Scroll to make a specific line visible
|
||||
*
|
||||
* @param lineIdx Line index to make visible
|
||||
* @param lineIdx Line index to make visible
|
||||
* @param visibleLines Number of visible lines in viewport
|
||||
* @param maxScroll Maximum scroll position
|
||||
* @param maxScroll Maximum scroll position
|
||||
*/
|
||||
public void scrollToLine(int lineIdx, int visibleLines, int maxScroll) {
|
||||
int firstVisible = scrolledLine;
|
||||
int lastFullyVisible = scrolledLine + visibleLines;
|
||||
|
||||
|
||||
if (lineIdx < firstVisible) {
|
||||
// Line is above viewport - scroll up
|
||||
targetScroll = lineIdx;
|
||||
@@ -135,66 +132,41 @@ public class ScrollState {
|
||||
}
|
||||
// If line is visible, don't change scroll
|
||||
}
|
||||
|
||||
|
||||
// Getters
|
||||
public double getScrollPos() {
|
||||
return scrollPos;
|
||||
}
|
||||
public double getScrollPos() { return scrollPos; }
|
||||
public double getTargetScroll() { return targetScroll; }
|
||||
|
||||
public double getTargetScroll() {
|
||||
return targetScroll;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return First visible line
|
||||
**/
|
||||
/** @return First visible line **/
|
||||
public int getScrolledLine() {
|
||||
return scrolledLine;
|
||||
}
|
||||
|
||||
public double getScrollVelocity() {
|
||||
return scrollVelocity;
|
||||
}
|
||||
|
||||
public boolean isClickScrolling() {
|
||||
return clickScrolling;
|
||||
}
|
||||
|
||||
public int getScrollbarDragOffset() {
|
||||
return scrollbarDragOffset;
|
||||
}
|
||||
|
||||
public double getScrollVelocity() { return scrollVelocity; }
|
||||
public boolean isClickScrolling() { return clickScrolling; }
|
||||
public int getScrollbarDragOffset() { return scrollbarDragOffset; }
|
||||
|
||||
/**
|
||||
* Get fractional offset for sub-pixel rendering
|
||||
*/
|
||||
public double getFractionalOffset() {
|
||||
return scrollPos - scrolledLine;
|
||||
}
|
||||
|
||||
|
||||
// Setters for scrollbar interaction
|
||||
public void setClickScrolling(boolean scrolling) {
|
||||
this.clickScrolling = scrolling;
|
||||
}
|
||||
|
||||
public void setScrollbarDragOffset(int offset) {
|
||||
this.scrollbarDragOffset = offset;
|
||||
}
|
||||
|
||||
public void setScrolledLine(int line) {
|
||||
this.scrolledLine = line;
|
||||
}
|
||||
public void setClickScrolling(boolean scrolling) { this.clickScrolling = scrolling; }
|
||||
public void setScrollbarDragOffset(int offset) { this.scrollbarDragOffset = offset; }
|
||||
public void setScrolledLine(int line) { this.scrolledLine = line; }
|
||||
|
||||
/**
|
||||
* Handle scrollbar dragging interaction. This encapsulates the logic that was
|
||||
* previously in the GUI class for handling thumb clicks/drags.
|
||||
*
|
||||
* @param yMouse current mouse Y coordinate
|
||||
* @param areaX GUI area X (unused but kept for parity)
|
||||
* @param areaY GUI area Y (top of text area)
|
||||
* @param areaHeight height of the scroll track area
|
||||
* @param yMouse current mouse Y coordinate
|
||||
* @param areaX GUI area X (unused but kept for parity)
|
||||
* @param areaY GUI area Y (top of text area)
|
||||
* @param areaHeight height of the scroll track area
|
||||
* @param visibleLines number of visible lines in viewport
|
||||
* @param linesCount total number of lines
|
||||
* @param maxScroll maximum allowed scroll value
|
||||
* @param linesCount total number of lines
|
||||
* @param maxScroll maximum allowed scroll value
|
||||
*/
|
||||
public void handleClickScrolling(int yMouse, int areaX, int areaY, int areaHeight, int visibleLines, int linesCount, int maxScroll) {
|
||||
// Keep dragging while mouse button is down
|
||||
@@ -238,4 +210,4 @@ public class ScrollState {
|
||||
int thumbTop = (int) (areaY + 1f * getScrollPos() / linesCountD * (areaHeight - 4)) + 1;
|
||||
setScrollbarDragOffset(yMouse - thumbTop);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import net.minecraft.client.gui.FontRenderer;
|
||||
import net.minecraft.client.gui.Gui;
|
||||
import net.minecraft.util.ChatAllowedCharacters;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import noppes.npcs.client.gui.util.GuiNPCInterface;
|
||||
import noppes.npcs.client.gui.util.GuiScriptTextArea.UndoData;
|
||||
import noppes.npcs.client.gui.util.GuiUtil;
|
||||
import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer;
|
||||
@@ -35,6 +36,7 @@ public class SearchReplaceBar {
|
||||
|
||||
// ==================== DIMENSIONS ====================
|
||||
private int x, y, width;
|
||||
public GuiNPCInterface ownerGui;
|
||||
private int barHeight = 24;
|
||||
private int replaceBarHeight = 22;
|
||||
private int textFieldWidth = 150;
|
||||
@@ -538,7 +540,7 @@ public class SearchReplaceBar {
|
||||
}
|
||||
|
||||
GL11.glEnable(GL11.GL_SCISSOR_TEST);
|
||||
GuiUtil.setScissorClip(fieldX + 2, fieldY, fieldWidth - 4, fieldHeight);
|
||||
GuiUtil.setScissorClip(fieldX + 2, fieldY, fieldWidth - 4, fieldHeight, ownerGui);
|
||||
|
||||
|
||||
// Draw selection highlight
|
||||
@@ -604,7 +606,7 @@ public class SearchReplaceBar {
|
||||
}
|
||||
|
||||
GL11.glEnable(GL11.GL_SCISSOR_TEST);
|
||||
GuiUtil.setScissorClip(fieldX + 2, fieldY, fieldWidth - 4, fieldHeight);
|
||||
GuiUtil.setScissorClip(fieldX + 2, fieldY, fieldWidth - 4, fieldHeight, ownerGui);
|
||||
|
||||
{
|
||||
// Draw selection highlight
|
||||
|
||||
@@ -0,0 +1,830 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete;
|
||||
|
||||
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.JSTypeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticField;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticMethod;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticParameter;
|
||||
|
||||
/**
|
||||
* Represents a single autocomplete suggestion.
|
||||
* Unified representation for both Java and JavaScript completions.
|
||||
*/
|
||||
public class AutocompleteItem implements Comparable<AutocompleteItem> {
|
||||
|
||||
/**
|
||||
* The kind of completion item.
|
||||
*/
|
||||
public enum Kind {
|
||||
ENUM_CONSTANT(-1), // Enum constant value (highest priority for enums)
|
||||
METHOD(0), // Method or function
|
||||
FIELD(1), // Field or property
|
||||
VARIABLE(2), // Local variable or parameter
|
||||
CLASS(3), // Class or interface type
|
||||
ENUM(4), // Enum type
|
||||
KEYWORD(6), // Language keyword
|
||||
SNIPPET(7); // Code snippet
|
||||
|
||||
private final int priority;
|
||||
|
||||
Kind(int priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
}
|
||||
|
||||
private final String name; // Display name (e.g., "getPlayer" or "getPlayer(UUID id)" for methods)
|
||||
private final String searchName; // Name to search against (just the identifier, e.g., "getPlayer")
|
||||
private final String insertText; // Text to insert (e.g., "getPlayer()")
|
||||
private final Kind kind; // Type of completion
|
||||
private final String typeLabel; // Type label (e.g., "IPlayer", "void")
|
||||
private final TypeInfo typeInfo; // TypeInfo for the typeLabel
|
||||
private final String signature; // Full signature for methods
|
||||
private final String documentation; // Documentation/description
|
||||
private final Object sourceData; // Original source (MethodInfo, FieldInfo, etc.)
|
||||
private final boolean deprecated; // Whether this item is deprecated
|
||||
|
||||
// Import tracking
|
||||
private final boolean requiresImport; // Whether selecting this item requires adding an import
|
||||
private final String importPath; // Full path for import (e.g., "net.minecraft.client.Minecraft")
|
||||
|
||||
// Inheritance tracking (for JS types)
|
||||
private final int inheritanceDepth; // Depth in inheritance tree (0 = child, 1 = parent, 2 = grandparent, etc.)
|
||||
|
||||
// Match scoring
|
||||
private int matchScore = 0; // How well this matches the query
|
||||
private int[] matchIndices; // Indices of matched characters for highlighting
|
||||
|
||||
private AutocompleteItem(String name, String searchName, String insertText, Kind kind, String typeLabel,
|
||||
TypeInfo typeLabelTypeInfo, String signature, String documentation, Object sourceData,
|
||||
boolean deprecated, boolean requiresImport, String importPath, int inheritanceDepth) {
|
||||
this.name = name;
|
||||
this.searchName = searchName != null ? searchName : name; // Default to name if not provided
|
||||
this.insertText = insertText;
|
||||
this.kind = kind;
|
||||
this.typeLabel = typeLabel;
|
||||
this.typeInfo = typeLabelTypeInfo;
|
||||
this.signature = signature;
|
||||
this.documentation = documentation;
|
||||
this.sourceData = sourceData;
|
||||
this.deprecated = deprecated;
|
||||
this.requiresImport = requiresImport;
|
||||
this.importPath = importPath;
|
||||
this.inheritanceDepth = inheritanceDepth;
|
||||
}
|
||||
|
||||
// ==================== FACTORY METHODS ====================
|
||||
|
||||
/**
|
||||
* Create from a Java MethodInfo.
|
||||
*/
|
||||
public static AutocompleteItem fromMethod(MethodInfo method) {
|
||||
return fromMethod(method, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a Java MethodInfo, optionally for method reference context.
|
||||
* @param method The method info
|
||||
* @param forMethodReference If true, insert text will NOT include parentheses
|
||||
*/
|
||||
public static AutocompleteItem fromMethod(MethodInfo method, boolean forMethodReference) {
|
||||
String name = method.getName();
|
||||
StringBuilder insertText = new StringBuilder(name);
|
||||
|
||||
// For method references, don't add () since it's ::methodName not ::methodName()
|
||||
if (!forMethodReference) {
|
||||
insertText.append("(");
|
||||
// Add placeholders for parameters if any
|
||||
if (method.getParameterCount() > 0) {
|
||||
// Just add () - user will fill in params
|
||||
}
|
||||
insertText.append(")");
|
||||
}
|
||||
|
||||
String returnType = method.getReturnType() != null ?
|
||||
getName(method.getReturnType()) : "void";
|
||||
|
||||
String signature = buildMethodSignature(method);
|
||||
|
||||
// Build display name with parameters like "read(byte[] b)"
|
||||
StringBuilder displayName = new StringBuilder(name);
|
||||
displayName.append("(");
|
||||
for (int i = 0; i < method.getParameterCount(); i++) {
|
||||
if (i > 0) displayName.append(", ");
|
||||
FieldInfo param = method.getParameters().get(i);
|
||||
String paramType = param.getTypeInfo() != null ?
|
||||
getName(param.getTypeInfo()) : "?";
|
||||
displayName.append(paramType);
|
||||
if (param.isVarArg()) displayName.append("...");
|
||||
if (param.getName() != null && !param.getName().isEmpty()) {
|
||||
displayName.append(" ").append(param.getName());
|
||||
}
|
||||
}
|
||||
displayName.append(")");
|
||||
|
||||
return new AutocompleteItem(
|
||||
displayName.toString(),
|
||||
name, // Search against just the method name, not the params
|
||||
insertText.toString(),
|
||||
Kind.METHOD,
|
||||
returnType,
|
||||
method.getReturnType(), // TypeInfo for return type
|
||||
signature,
|
||||
method.getDocumentation(),
|
||||
method,
|
||||
false, // TODO: Check for @Deprecated annotation
|
||||
false, // Methods don't require imports
|
||||
null,
|
||||
-1 // Java methods don't use inheritance depth sorting
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a Java FieldInfo.
|
||||
*/
|
||||
public static AutocompleteItem fromField(FieldInfo field) {
|
||||
String typeLabel = field.getTypeInfo() != null ?
|
||||
getName(field.getTypeInfo()) : "?";
|
||||
|
||||
Kind kind;
|
||||
switch (field.getScope()) {
|
||||
case PARAMETER:
|
||||
kind = Kind.VARIABLE;
|
||||
break;
|
||||
case LOCAL:
|
||||
kind = Kind.VARIABLE;
|
||||
break;
|
||||
case ENUM_CONSTANT:
|
||||
kind = Kind.ENUM_CONSTANT;
|
||||
break;
|
||||
default:
|
||||
kind = Kind.FIELD;
|
||||
}
|
||||
|
||||
return new AutocompleteItem(
|
||||
field.getName(),
|
||||
field.getName(), // searchName same as display name for fields
|
||||
field.getName(),
|
||||
kind,
|
||||
typeLabel,
|
||||
field.getTypeInfo(), // TypeInfo for field type
|
||||
typeLabel + " " + field.getName(),
|
||||
null,
|
||||
field,
|
||||
false,
|
||||
false, // Fields don't require imports
|
||||
null,
|
||||
-1// Java fields don't use inheritance depth sorting
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a Java TypeInfo.
|
||||
*/
|
||||
public static AutocompleteItem fromType(TypeInfo type) {
|
||||
Kind kind;
|
||||
switch (type.getKind()) {
|
||||
case INTERFACE:
|
||||
kind = Kind.CLASS;
|
||||
break;
|
||||
case ENUM:
|
||||
kind = Kind.ENUM;
|
||||
break;
|
||||
default:
|
||||
kind = Kind.CLASS;
|
||||
}
|
||||
String name = getName(type);
|
||||
return new AutocompleteItem(
|
||||
name,
|
||||
name, // searchName same as display name for types
|
||||
name,
|
||||
kind,
|
||||
type.getPackageName(),
|
||||
type, // TypeInfo for package name
|
||||
type.getFullName(),
|
||||
null,
|
||||
type,
|
||||
false,
|
||||
false, // Will be overridden for unimported types
|
||||
null,
|
||||
-1 // Java types don't use inheritance depth sorting
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a JavaScript JSMethodInfo.
|
||||
*/
|
||||
public static AutocompleteItem fromJSMethod(JSMethodInfo method) {
|
||||
return fromJSMethod(method, null, 0, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a JavaScript JSMethodInfo with inheritance depth.
|
||||
* @param method The method info
|
||||
* @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.)
|
||||
*/
|
||||
public static AutocompleteItem fromJSMethod(JSMethodInfo method, int inheritanceDepth) {
|
||||
return fromJSMethod(method, null, inheritanceDepth, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a JavaScript JSMethodInfo with type parameter resolution for display.
|
||||
* @param method The method info
|
||||
* @param contextType The TypeInfo context for resolving type parameters (e.g., IPlayer to resolve T → EntityPlayerMP)
|
||||
* @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.)
|
||||
*/
|
||||
public static AutocompleteItem fromJSMethod(JSMethodInfo method, TypeInfo contextType, int inheritanceDepth) {
|
||||
return fromJSMethod(method, contextType, inheritanceDepth, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a JavaScript JSMethodInfo with type parameter resolution for display.
|
||||
* @param method The method info
|
||||
* @param contextType The TypeInfo context for resolving type parameters (e.g., IPlayer to resolve T → EntityPlayerMP)
|
||||
* @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.)
|
||||
* @param forMethodReference If true, insert text will NOT include parentheses
|
||||
*/
|
||||
public static AutocompleteItem fromJSMethod(JSMethodInfo method, TypeInfo contextType, int inheritanceDepth, boolean forMethodReference) {
|
||||
String name = method.getName();
|
||||
StringBuilder insertText = new StringBuilder(name);
|
||||
|
||||
// For method references, don't add () since it's ::methodName not ::methodName()
|
||||
if (!forMethodReference) {
|
||||
insertText.append("(");
|
||||
insertText.append(")");
|
||||
}
|
||||
|
||||
// Build display name with resolved parameter types
|
||||
StringBuilder displayName = new StringBuilder(name);
|
||||
displayName.append("(");
|
||||
for (int i = 0; i < method.getParameters().size(); i++) {
|
||||
if (i > 0) displayName.append(", ");
|
||||
JSMethodInfo.JSParameterInfo param = method.getParameters().get(i);
|
||||
|
||||
// Use resolved type with full generic display
|
||||
TypeInfo paramType = param.getResolvedType(contextType);
|
||||
String paramTypeName = paramType.isResolved() ? paramType.getDisplayName() : param.getType();
|
||||
if (param.isVarArg()) paramTypeName = paramTypeName + "...";
|
||||
displayName.append(paramTypeName).append(" ").append(param.getName());
|
||||
}
|
||||
displayName.append(")");
|
||||
|
||||
// Get return type using the new method with full generic display
|
||||
TypeInfo returnTypeInfo = method.getResolvedReturnType(contextType);
|
||||
String returnType = returnTypeInfo.isResolved() ? returnTypeInfo.getDisplayName() : method.getReturnType();
|
||||
|
||||
return new AutocompleteItem(
|
||||
displayName.toString(),
|
||||
name, // Search against just the method name
|
||||
insertText.toString(),
|
||||
Kind.METHOD,
|
||||
returnType,
|
||||
returnTypeInfo, // TypeInfo for return type
|
||||
method.getSignature(),
|
||||
method.getDocumentation(),
|
||||
method,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
inheritanceDepth
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a JavaScript JSFieldInfo.
|
||||
*/
|
||||
public static AutocompleteItem fromJSField(JSFieldInfo field) {
|
||||
return fromJSField(field, null, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a JavaScript JSFieldInfo with inheritance depth.
|
||||
* @param field The field info
|
||||
* @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.)
|
||||
*/
|
||||
public static AutocompleteItem fromJSField(JSFieldInfo field, int inheritanceDepth) {
|
||||
return fromJSField(field, null, inheritanceDepth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a JavaScript JSFieldInfo with type parameter resolution for display.
|
||||
* @param field The field info
|
||||
* @param contextType The TypeInfo context for resolving type parameters
|
||||
* @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.)
|
||||
*/
|
||||
public static AutocompleteItem fromJSField(JSFieldInfo field, TypeInfo contextType, int inheritanceDepth) {
|
||||
// Get field type using the new method with full generic display
|
||||
TypeInfo fieldTypeInfo = field.getResolvedType(contextType);
|
||||
String fieldType = fieldTypeInfo.isResolved() ? fieldTypeInfo.getDisplayName() : field.getType();
|
||||
|
||||
return new AutocompleteItem(
|
||||
field.getName(),
|
||||
field.getName(), // searchName same as display name
|
||||
field.getName(),
|
||||
Kind.FIELD,
|
||||
fieldType,
|
||||
fieldTypeInfo, // TypeInfo for field type
|
||||
field.toString(),
|
||||
field.getDocumentation(),
|
||||
field,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
inheritanceDepth
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a keyword item.
|
||||
*/
|
||||
public static AutocompleteItem keyword(String keyword) {
|
||||
return new AutocompleteItem(
|
||||
keyword,
|
||||
keyword, // searchName same as display name
|
||||
keyword,
|
||||
Kind.KEYWORD,
|
||||
"keyword",
|
||||
null, // TypeInfo for keyword
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
-1 // Keywords don't use inheritance depth sorting
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a synthetic method (e.g., Nashorn built-in like Java.type).
|
||||
*/
|
||||
public static AutocompleteItem fromSyntheticMethod(
|
||||
SyntheticMethod method,
|
||||
TypeInfo containingType) {
|
||||
return fromSyntheticMethod(method, containingType, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a synthetic method (e.g., Nashorn built-in like Java.type).
|
||||
* @param method The synthetic method
|
||||
* @param containingType The containing type info
|
||||
* @param forMethodReference If true, insert text will NOT include parentheses
|
||||
*/
|
||||
public static AutocompleteItem fromSyntheticMethod(
|
||||
SyntheticMethod method,
|
||||
TypeInfo containingType,
|
||||
boolean forMethodReference) {
|
||||
String name = method.name;
|
||||
StringBuilder insertText = new StringBuilder(name);
|
||||
|
||||
// For method references, don't add () since it's ::methodName not ::methodName()
|
||||
if (!forMethodReference) {
|
||||
insertText.append("(");
|
||||
insertText.append(")");
|
||||
}
|
||||
|
||||
// Build display name with parameters - use simple names for types
|
||||
StringBuilder displayName = new StringBuilder(name);
|
||||
displayName.append("(");
|
||||
for (int i = 0; i < method.parameters.size(); i++) {
|
||||
if (i > 0) displayName.append(", ");
|
||||
SyntheticParameter param =
|
||||
method.parameters.get(i);
|
||||
// Use simple name for display (e.g., "Class" instead of "java.lang.Class")
|
||||
String simpleTypeName = getSimpleTypeName(param.typeName);
|
||||
displayName.append(simpleTypeName).append(" ").append(param.name);
|
||||
}
|
||||
displayName.append(")");
|
||||
|
||||
// Use simple name for return type display too
|
||||
String simpleReturnType = getSimpleTypeName(method.returnType);
|
||||
|
||||
return new AutocompleteItem(
|
||||
displayName.toString(),
|
||||
name, // Search against just the method name
|
||||
insertText.toString(),
|
||||
Kind.METHOD,
|
||||
simpleReturnType,
|
||||
method.getReturnTypeInfo(), // TypeInfo for return type - would need to resolve from TypeResolver
|
||||
method.getSignature(),
|
||||
method.documentation,
|
||||
method,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
0 // Synthetic types are at depth 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get simple name from a fully-qualified type name.
|
||||
*/
|
||||
private static String getSimpleTypeName(String typeName) {
|
||||
if (typeName == null) return "void";
|
||||
int lastDot = typeName.lastIndexOf('.');
|
||||
return lastDot >= 0 ? typeName.substring(lastDot + 1) : typeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a synthetic field (e.g., Nashorn built-in).
|
||||
*/
|
||||
public static AutocompleteItem fromSyntheticField(
|
||||
SyntheticField field) {
|
||||
return new AutocompleteItem(
|
||||
field.name,
|
||||
field.name, // searchName same as display name
|
||||
field.name,
|
||||
Kind.FIELD,
|
||||
field.typeName,
|
||||
field.getTypeInfo(), // TypeInfo for field type - would need to resolve from TypeResolver
|
||||
field.typeName + " " + field.name,
|
||||
field.documentation,
|
||||
field,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
0 // Synthetic types are at depth 0
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== HELPER METHODS ====================
|
||||
|
||||
private static String buildMethodSignature(MethodInfo method) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String returnType = method.getReturnType() != null ?
|
||||
getName(method.getReturnType()) : "void";
|
||||
sb.append(returnType).append(" ").append(method.getName()).append("(");
|
||||
|
||||
for (int i = 0; i < method.getParameterCount(); i++) {
|
||||
if (i > 0) sb.append(", ");
|
||||
FieldInfo param = method.getParameters().get(i);
|
||||
String paramType = param.getTypeInfo() != null ?
|
||||
getName(param.getTypeInfo()) : "?";
|
||||
sb.append(paramType);
|
||||
if (param.isVarArg()) sb.append("...");
|
||||
sb.append(" ").append(param.getName());
|
||||
}
|
||||
|
||||
sb.append(")");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// ==================== MATCHING ====================
|
||||
|
||||
/**
|
||||
* Calculate fuzzy match score against a query string.
|
||||
* Returns -1 if no match, or a positive score (higher = better match).
|
||||
*
|
||||
* @param query The search query
|
||||
* @param requirePrefix If true, only match items that start with the query (no fuzzy/contains matching)
|
||||
*/
|
||||
public int calculateMatchScore(String query, boolean requirePrefix) {
|
||||
if (query == null || query.isEmpty()) {
|
||||
matchScore = 100; // Everything matches empty query
|
||||
matchIndices = new int[0];
|
||||
return matchScore;
|
||||
}
|
||||
|
||||
String lowerName = searchName.toLowerCase();
|
||||
String lowerQuery = query.toLowerCase();
|
||||
|
||||
// Exact prefix match is best
|
||||
if (lowerName.startsWith(lowerQuery)) {
|
||||
matchScore = 1000 - query.length(); // Shorter prefixes score higher
|
||||
matchIndices = new int[query.length()];
|
||||
for (int i = 0; i < query.length(); i++) {
|
||||
matchIndices[i] = i;
|
||||
}
|
||||
return matchScore;
|
||||
}
|
||||
|
||||
// If requirePrefix is true, stop here - no fuzzy/substring matching
|
||||
if (requirePrefix) {
|
||||
matchScore = -1;
|
||||
matchIndices = new int[0];
|
||||
return matchScore;
|
||||
}
|
||||
|
||||
// Exact substring match
|
||||
int subIndex = lowerName.indexOf(lowerQuery);
|
||||
if (subIndex >= 0) {
|
||||
matchScore = 500 - subIndex; // Earlier substrings score higher
|
||||
matchIndices = new int[query.length()];
|
||||
for (int i = 0; i < query.length(); i++) {
|
||||
matchIndices[i] = subIndex + i;
|
||||
}
|
||||
return matchScore;
|
||||
}
|
||||
|
||||
// Fuzzy match - characters must appear in order
|
||||
int[] indices = new int[query.length()];
|
||||
int nameIdx = 0;
|
||||
int queryIdx = 0;
|
||||
int gaps = 0;
|
||||
int consecutiveBonus = 0;
|
||||
int lastMatchIdx = -2;
|
||||
|
||||
while (queryIdx < query.length() && nameIdx < searchName.length()) {
|
||||
if (Character.toLowerCase(searchName.charAt(nameIdx)) == lowerQuery.charAt(queryIdx)) {
|
||||
indices[queryIdx] = nameIdx;
|
||||
|
||||
// Bonus for consecutive matches
|
||||
if (nameIdx == lastMatchIdx + 1) {
|
||||
consecutiveBonus += 10;
|
||||
}
|
||||
|
||||
// Bonus for matching at word boundaries (camelCase)
|
||||
if (nameIdx == 0 || !Character.isLetterOrDigit(searchName.charAt(nameIdx - 1)) ||
|
||||
(Character.isUpperCase(searchName.charAt(nameIdx)) && nameIdx > 0 &&
|
||||
Character.isLowerCase(searchName.charAt(nameIdx - 1)))) {
|
||||
consecutiveBonus += 20;
|
||||
}
|
||||
|
||||
lastMatchIdx = nameIdx;
|
||||
queryIdx++;
|
||||
} else {
|
||||
gaps++;
|
||||
}
|
||||
nameIdx++;
|
||||
}
|
||||
|
||||
if (queryIdx < query.length()) {
|
||||
// Didn't match all characters
|
||||
matchScore = -1;
|
||||
matchIndices = null;
|
||||
return -1;
|
||||
}
|
||||
|
||||
matchScore = 100 + consecutiveBonus - gaps;
|
||||
matchIndices = indices;
|
||||
return matchScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward compatibility overload - defaults to fuzzy matching (no prefix requirement).
|
||||
*/
|
||||
public int calculateMatchScore(String query) {
|
||||
return calculateMatchScore(query, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a boost to the match score (e.g., from usage tracking).
|
||||
*/
|
||||
public void addScoreBoost(int boost) {
|
||||
this.matchScore += boost;
|
||||
}
|
||||
|
||||
// ==================== GETTERS ====================
|
||||
|
||||
public String getName() { return name; }
|
||||
public String getSearchName() { return searchName; }
|
||||
public String getInsertText() { return insertText; }
|
||||
public Kind getKind() { return kind; }
|
||||
public String getTypeLabel() { return typeLabel; }
|
||||
public TypeInfo getTypeInfo() { return typeInfo; }
|
||||
public String getSignature() { return signature; }
|
||||
public String getDocumentation() { return documentation; }
|
||||
public Object getSourceData() { return sourceData; }
|
||||
public boolean isDeprecated() { return deprecated; }
|
||||
public boolean requiresImport() { return requiresImport; }
|
||||
public String getImportPath() { return importPath; }
|
||||
public int getMatchScore() { return matchScore; }
|
||||
public int[] getMatchIndices() { return matchIndices; }
|
||||
|
||||
/**
|
||||
* Get the display name for a type, including generic arguments.
|
||||
* Uses the unified getDisplayName() method which handles:
|
||||
* - Simple types: "String"
|
||||
* - Parameterized types: "List<String>", "Map<String, Integer>"
|
||||
* - Nested generics: "List<Map<String, Integer>>"
|
||||
* - JS types with namespace: "IPlayerEvent.InteractEvent"
|
||||
*/
|
||||
public static String getName(TypeInfo type) {
|
||||
return type.getDisplayName();
|
||||
}
|
||||
|
||||
public int getParameterCount() {
|
||||
if (sourceData instanceof MethodInfo) {
|
||||
return ((MethodInfo) sourceData).getParameterCount();
|
||||
} else if (sourceData instanceof JSMethodInfo) {
|
||||
return ((JSMethodInfo) sourceData).getParameterCount();
|
||||
} else if (sourceData instanceof SyntheticMethod) {
|
||||
return ((SyntheticMethod) sourceData).parameters.size();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this item represents a static member.
|
||||
*/
|
||||
public boolean isStatic() {
|
||||
if (sourceData instanceof MethodInfo) {
|
||||
return ((MethodInfo) sourceData).isStatic();
|
||||
} else if (sourceData instanceof FieldInfo) {
|
||||
return ((FieldInfo) sourceData).isStatic();
|
||||
} else if (sourceData instanceof JSMethodInfo) {
|
||||
return ((JSMethodInfo) sourceData).isStatic();
|
||||
} else if (sourceData instanceof JSFieldInfo) {
|
||||
return ((JSFieldInfo) sourceData).isStatic();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this item represents a final member.
|
||||
*/
|
||||
public boolean isFinal() {
|
||||
if (sourceData instanceof MethodInfo) {
|
||||
return ((MethodInfo) sourceData).isFinal();
|
||||
} else if (sourceData instanceof FieldInfo) {
|
||||
return ((FieldInfo) sourceData).isFinal();
|
||||
}else if (sourceData instanceof JSFieldInfo) {
|
||||
return ((JSFieldInfo) sourceData).isReadonly();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this method is inherited from Object class and NOT overridden.
|
||||
* These methods should be deprioritized in autocomplete.
|
||||
*/
|
||||
public boolean isInheritedObjectMethod() {
|
||||
if (kind != Kind.METHOD) return false;
|
||||
|
||||
if (sourceData instanceof MethodInfo) {
|
||||
MethodInfo methodInfo = (MethodInfo) sourceData;
|
||||
java.lang.reflect.Method javaMethod = methodInfo.getJavaMethod();
|
||||
if (javaMethod != null) {
|
||||
return Object.class.equals(javaMethod.getDeclaringClass());
|
||||
}
|
||||
TypeInfo containingType = methodInfo.getContainingType();
|
||||
return containingType != null
|
||||
&& containingType.getFullName().equals("java.lang.Object")
|
||||
&& !methodInfo.isStatic()
|
||||
&& !methodInfo.isOverride();
|
||||
}
|
||||
|
||||
if (sourceData instanceof JSMethodInfo) {
|
||||
JSMethodInfo jsMethod = (JSMethodInfo) sourceData;
|
||||
JSTypeInfo definingType = jsMethod.getContainingType();
|
||||
return definingType != null
|
||||
&& "Object".equals(definingType.getSimpleName())
|
||||
&& !jsMethod.isStatic()
|
||||
&& inheritanceDepth > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get icon identifier based on kind.
|
||||
*/
|
||||
public String getIconId() {
|
||||
switch (kind) {
|
||||
case METHOD: return "m";
|
||||
case FIELD: return "f";
|
||||
case VARIABLE: return "v";
|
||||
case CLASS: return "C";
|
||||
case ENUM: return "E";
|
||||
case ENUM_CONSTANT: return "e";
|
||||
case KEYWORD: return "k";
|
||||
case SNIPPET: return "s";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for the kind icon.
|
||||
*/
|
||||
public int getIconColor() {
|
||||
switch (kind) {
|
||||
case METHOD: return 0xFFB877DB; // Purple for methods
|
||||
case FIELD: return 0xFF79C0FF; // Blue for fields
|
||||
case VARIABLE: return 0xFF7EE787; // Green for variables
|
||||
case CLASS: return 0xFFFFA657; // Orange for classes
|
||||
case ENUM: return 0xFFFFD866; // Yellow for enums
|
||||
case ENUM_CONSTANT: return 0xFFFFD866;
|
||||
case KEYWORD: return 0xFFFF7B72; // Red for keywords
|
||||
case SNIPPET: return 0xFFCCCCCC; // Gray for snippets
|
||||
default: return 0xFFCCCCCC;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(AutocompleteItem other) {
|
||||
// First by match score (descending) - this includes all penalties and boosts
|
||||
if (this.matchScore != other.matchScore) {
|
||||
return other.matchScore - this.matchScore;
|
||||
}
|
||||
|
||||
// Then by kind priority
|
||||
if (this.kind.getPriority() != other.kind.getPriority()) {
|
||||
return this.kind.getPriority() - other.kind.getPriority();
|
||||
}
|
||||
|
||||
// For JS types, prioritize by inheritance depth (child class members first)
|
||||
// Only apply if both items have valid inheritance depth (>= 0)
|
||||
if (this.inheritanceDepth >= 0 && other.inheritanceDepth >= 0) {
|
||||
if (this.inheritanceDepth != other.inheritanceDepth) {
|
||||
return this.inheritanceDepth - other.inheritanceDepth; // Lower depth = closer to child = higher priority
|
||||
}
|
||||
}
|
||||
|
||||
// Finally alphabetically
|
||||
return this.name.compareToIgnoreCase(other.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name + " (" + kind + ")";
|
||||
}
|
||||
|
||||
// ==================== BUILDER ====================
|
||||
|
||||
/**
|
||||
* Builder for creating AutocompleteItem instances without source data.
|
||||
*/
|
||||
public static class Builder {
|
||||
private String name;
|
||||
private String searchName;
|
||||
private String insertText;
|
||||
private Kind kind = Kind.FIELD;
|
||||
private String typeLabel = "";
|
||||
private String signature;
|
||||
private String documentation;
|
||||
private Object sourceData;
|
||||
private boolean deprecated = false;
|
||||
private boolean requiresImport = false;
|
||||
private String importPath = null;
|
||||
|
||||
public Builder name(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder searchName(String searchName) {
|
||||
this.searchName = searchName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder insertText(String insertText) {
|
||||
this.insertText = insertText;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder kind(Kind kind) {
|
||||
this.kind = kind;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder typeLabel(String typeLabel) {
|
||||
this.typeLabel = typeLabel;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder signature(String signature) {
|
||||
this.signature = signature;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder documentation(String documentation) {
|
||||
this.documentation = documentation;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder sourceData(Object sourceData) {
|
||||
this.sourceData = sourceData;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder deprecated(boolean deprecated) {
|
||||
this.deprecated = deprecated;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder requiresImport(boolean requiresImport) {
|
||||
this.requiresImport = requiresImport;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder importPath(String importPath) {
|
||||
this.importPath = importPath;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AutocompleteItem build() {
|
||||
if (insertText == null) {
|
||||
insertText = name;
|
||||
}
|
||||
return new AutocompleteItem(name, searchName, insertText, kind, typeLabel,
|
||||
null, signature, documentation, sourceData, deprecated, requiresImport, importPath, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1117
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,1000 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
import net.minecraft.client.gui.Gui;
|
||||
import net.minecraft.client.gui.ScaledResolution;
|
||||
import noppes.npcs.client.gui.util.GuiNPCInterface;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.token.TokenType;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* UI component for displaying autocomplete suggestions.
|
||||
* Styled similar to SearchReplaceBar with IntelliJ-like appearance.
|
||||
*/
|
||||
public class AutocompleteMenu extends Gui {
|
||||
|
||||
// ==================== CONSTANTS ====================
|
||||
private static final int MAX_VISIBLE_ITEMS = 10;
|
||||
private static final int ITEM_HEIGHT = 16;
|
||||
private static final int PADDING = 4;
|
||||
private static final int ICON_WIDTH = 16;
|
||||
private static final int MIN_WIDTH = 200;
|
||||
private static final int MAX_WIDTH = 400;
|
||||
private static final int HINT_HEIGHT = 18;
|
||||
|
||||
// ==================== COLORS ====================
|
||||
private static final int BG_COLOR = 0xFF2d2d30;
|
||||
private static final int BORDER_COLOR = 0xFF3c3c3c;
|
||||
private static final int SELECTED_BG = 0xFF094771;
|
||||
private static final int HOVER_BG = 0xFF37373d;
|
||||
private static final int TEXT_COLOR = 0xFFe0e0e0;
|
||||
private static final int DIM_TEXT_COLOR = 0xFF808080;
|
||||
private static final int HIGHLIGHT_COLOR = 0xFFFFD866;
|
||||
private static final int HINT_BG_COLOR = 0xFF252526;
|
||||
private static final int SCROLLBAR_BG = 0xFF3c3c3c;
|
||||
private static final int SCROLLBAR_FG = 0xFF606060;
|
||||
|
||||
// ==================== STATE ====================
|
||||
private boolean visible = false;
|
||||
private List<AutocompleteItem> items = new ArrayList<>();
|
||||
private int selectedIndex = 0;
|
||||
private int scrollOffset = 0;
|
||||
private int hoveredIndex = -1;
|
||||
|
||||
// Scrollbar drag state
|
||||
private boolean isDraggingScrollbar = false;
|
||||
private int dragStartY = 0;
|
||||
private int dragStartScroll = 0;
|
||||
|
||||
// Panel pan/drag state
|
||||
private boolean isDraggingPanel;
|
||||
private int panelDragOffsetX, panelDragOffsetY;
|
||||
private boolean panelPositionOverridden;
|
||||
private int overriddenX, overriddenY;
|
||||
private boolean hasPendingPanelDrag;
|
||||
private int pendingDragStartX, pendingDragStartY;
|
||||
private boolean pendingItemConfirm;
|
||||
|
||||
// Panel resize state
|
||||
private boolean isResizingPanel;
|
||||
private int resizeInitMouseX, resizeInitMouseY;
|
||||
private int resizeInitW, resizeInitH;
|
||||
private boolean panelSizeOverridden;
|
||||
private int overriddenW, overriddenH;
|
||||
private static final int MIN_RESIZE_W = 140;
|
||||
private static final int MIN_RESIZE_H = 60;
|
||||
private static final int DRAG_THRESHOLD = 4;
|
||||
|
||||
// ==================== POSITION ====================
|
||||
private int x, y;
|
||||
public GuiNPCInterface ownerGui;
|
||||
private int width, height;
|
||||
private int menuWidth;
|
||||
private int visibleItemsCount; // Actual number of items that fit in the menu
|
||||
|
||||
// ==================== FONT ====================
|
||||
private final FontRenderer font;
|
||||
|
||||
// ==================== CALLBACK ====================
|
||||
private AutocompleteCallback callback;
|
||||
|
||||
/**
|
||||
* Callback interface for autocomplete events.
|
||||
*/
|
||||
public interface AutocompleteCallback {
|
||||
void onItemSelected(AutocompleteItem item);
|
||||
void onDismiss();
|
||||
}
|
||||
|
||||
public AutocompleteMenu() {
|
||||
this.font = Minecraft.getMinecraft().fontRenderer;
|
||||
this.visibleItemsCount = MAX_VISIBLE_ITEMS;
|
||||
}
|
||||
|
||||
private int getVisibleItemCapacity() {
|
||||
if (items == null || items.isEmpty())
|
||||
return 0;
|
||||
int cap = visibleItemsCount > 0 ? visibleItemsCount : MAX_VISIBLE_ITEMS;
|
||||
if (!panelSizeOverridden) cap = Math.min(cap, MAX_VISIBLE_ITEMS);
|
||||
return Math.max(1, Math.min(cap, items.size()));
|
||||
}
|
||||
|
||||
private int getMaxScrollOffset() {
|
||||
int cap = getVisibleItemCapacity();
|
||||
if (cap <= 0)
|
||||
return 0;
|
||||
return Math.max(0, items.size() - cap);
|
||||
}
|
||||
|
||||
public void setCallback(AutocompleteCallback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
// ==================== VISIBILITY ====================
|
||||
|
||||
/**
|
||||
* Show the menu at the specified position with the given items.
|
||||
*/
|
||||
public void show(int x, int y, List<AutocompleteItem> items, int viewportWidth, int viewportHeight) {
|
||||
this.items = items != null ? new ArrayList<>(items) : new ArrayList<>();
|
||||
this.selectedIndex = 0;
|
||||
this.scrollOffset = 0;
|
||||
this.hoveredIndex = -1;
|
||||
|
||||
// Calculate menu dimensions
|
||||
calculateDimensions(x, y, viewportWidth, viewportHeight);
|
||||
|
||||
this.visible = !this.items.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the items while keeping the menu open.
|
||||
*/
|
||||
public void updateItems(List<AutocompleteItem> newItems) {
|
||||
this.items = newItems != null ? new ArrayList<>(newItems) : new ArrayList<>();
|
||||
|
||||
// Clamp selection
|
||||
if (selectedIndex >= items.size()) {
|
||||
selectedIndex = Math.max(0, items.size() - 1);
|
||||
}
|
||||
|
||||
// Clamp scroll
|
||||
int maxScroll = getMaxScrollOffset();
|
||||
if (scrollOffset > maxScroll) {
|
||||
scrollOffset = maxScroll;
|
||||
}
|
||||
|
||||
visible = !items.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the menu.
|
||||
*/
|
||||
public void hide() {
|
||||
visible = false;
|
||||
isDraggingPanel = false;
|
||||
isResizingPanel = false;
|
||||
panelPositionOverridden = false;
|
||||
panelSizeOverridden = false;
|
||||
hasPendingPanelDrag = false;
|
||||
pendingItemConfirm = false;
|
||||
if (callback != null) {
|
||||
callback.onDismiss();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return visible;
|
||||
}
|
||||
|
||||
public boolean hasItems() {
|
||||
return !items.isEmpty();
|
||||
}
|
||||
|
||||
// ==================== DIMENSIONS ====================
|
||||
|
||||
private void calculateDimensions(int cursorX, int cursorY, int viewportWidth, int viewportHeight) {
|
||||
// Calculate width based on item content
|
||||
int maxItemWidth = MIN_WIDTH;
|
||||
for (AutocompleteItem item : items) {
|
||||
int itemWidth = ICON_WIDTH + PADDING + font.getStringWidth(item.getName()) + PADDING;
|
||||
if (item.getTypeLabel() != null) {
|
||||
itemWidth += font.getStringWidth(item.getTypeLabel()) + PADDING * 2;
|
||||
}
|
||||
maxItemWidth = Math.max(maxItemWidth, itemWidth);
|
||||
}
|
||||
menuWidth = Math.min(maxItemWidth + 20, MAX_WIDTH); // +20 for scrollbar
|
||||
|
||||
// Calculate initial height
|
||||
int visibleItems = Math.min(items.size(), MAX_VISIBLE_ITEMS);
|
||||
int menuHeight = visibleItems * ITEM_HEIGHT + HINT_HEIGHT + PADDING * 2;
|
||||
|
||||
// Line height estimation (cursor position is typically at baseline)
|
||||
int lineHeight = 20;
|
||||
|
||||
// Calculate available space
|
||||
int spaceBelow = viewportHeight - cursorY - 10; // Space below cursor
|
||||
int spaceAbove = cursorY - lineHeight - 10; // Space above current line
|
||||
|
||||
// Determine if we should use horizontal positioning
|
||||
// Use horizontal if both vertical positions would be cramped or block the cursor line
|
||||
boolean useHorizontalPosition = false;
|
||||
if (spaceBelow < menuHeight && spaceAbove < menuHeight) {
|
||||
// Check if horizontal positioning would work better
|
||||
int spaceRight = viewportWidth - (cursorX + 50) - 10; // 50 = offset from cursor
|
||||
if (spaceRight >= menuWidth && viewportHeight > menuHeight + 20) {
|
||||
useHorizontalPosition = true;
|
||||
} else {
|
||||
// Reduce height to fit in the best available vertical space
|
||||
int availableVerticalSpace = Math.max(spaceBelow, spaceAbove);
|
||||
if (availableVerticalSpace < menuHeight) {
|
||||
// Calculate how many items we can show in available space
|
||||
int maxItemsForSpace = (availableVerticalSpace - HINT_HEIGHT - PADDING * 2) / ITEM_HEIGHT;
|
||||
maxItemsForSpace = Math.max(3, Math.min(maxItemsForSpace, items.size())); // At least 3 items
|
||||
visibleItems = maxItemsForSpace;
|
||||
menuHeight = visibleItems * ITEM_HEIGHT + HINT_HEIGHT + PADDING * 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the calculated visible items count
|
||||
this.visibleItemsCount = visibleItems;
|
||||
|
||||
// Position the menu
|
||||
if (useHorizontalPosition) {
|
||||
// Position to the right of the cursor
|
||||
this.x = cursorX + 50;
|
||||
this.y = cursorY - lineHeight; // Align with current line
|
||||
|
||||
// Ensure doesn't go off bottom
|
||||
if (y + menuHeight > viewportHeight - 10) {
|
||||
y = viewportHeight - menuHeight - 10;
|
||||
}
|
||||
} else {
|
||||
// Standard vertical positioning
|
||||
this.x = cursorX;
|
||||
|
||||
// Try below first
|
||||
if (spaceBelow >= menuHeight) {
|
||||
this.y = cursorY ;
|
||||
} else if (spaceAbove >= menuHeight) {
|
||||
// Show above cursor line
|
||||
this.y = cursorY - menuHeight - lineHeight;
|
||||
} else {
|
||||
// Use the larger space
|
||||
if (spaceBelow >= spaceAbove) {
|
||||
this.y = cursorY;
|
||||
} else {
|
||||
this.y = cursorY - menuHeight - lineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if menu would go off-screen to the right
|
||||
if (x + menuWidth > viewportWidth - 10) {
|
||||
x = viewportWidth - menuWidth - 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure not off-screen to the left or top
|
||||
x = Math.max(5, x);
|
||||
y = Math.max(5, y);
|
||||
|
||||
this.width = menuWidth;
|
||||
this.height = menuHeight;
|
||||
// Apply position override if user has dragged the panel
|
||||
if (panelPositionOverridden) { this.x = overriddenX; this.y = overriddenY; }
|
||||
// Apply size override if user has resized the panel
|
||||
if (panelSizeOverridden) {
|
||||
this.menuWidth = overriddenW;
|
||||
this.width = overriddenW;
|
||||
this.height = overriddenH;
|
||||
this.visibleItemsCount = Math.max(1, (overriddenH - HINT_HEIGHT - PADDING * 2) / ITEM_HEIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SELECTION ====================
|
||||
|
||||
/**
|
||||
* Move selection up.
|
||||
*/
|
||||
public void selectPrevious() {
|
||||
if (items.isEmpty()) return;
|
||||
|
||||
selectedIndex--;
|
||||
if (selectedIndex < 0) {
|
||||
selectedIndex = items.size() - 1;
|
||||
scrollOffset = getMaxScrollOffset();
|
||||
} else if (selectedIndex < scrollOffset) {
|
||||
scrollOffset = selectedIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move selection down.
|
||||
*/
|
||||
public void selectNext() {
|
||||
if (items.isEmpty()) return;
|
||||
|
||||
selectedIndex++;
|
||||
if (selectedIndex >= items.size()) {
|
||||
selectedIndex = 0;
|
||||
scrollOffset = 0;
|
||||
} else {
|
||||
int cap = getVisibleItemCapacity();
|
||||
if (cap > 0 && selectedIndex >= scrollOffset + cap) {
|
||||
scrollOffset = selectedIndex - cap + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the current selection.
|
||||
*/
|
||||
public void confirmSelection() {
|
||||
if (items.isEmpty() || selectedIndex < 0 || selectedIndex >= items.size()) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
AutocompleteItem selected = items.get(selectedIndex);
|
||||
if (callback != null) {
|
||||
callback.onItemSelected(selected);
|
||||
}
|
||||
hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected item.
|
||||
*/
|
||||
public AutocompleteItem getSelectedItem() {
|
||||
if (items.isEmpty() || selectedIndex < 0 || selectedIndex >= items.size()) {
|
||||
return null;
|
||||
}
|
||||
return items.get(selectedIndex);
|
||||
}
|
||||
|
||||
// ==================== DRAWING ====================
|
||||
|
||||
/**
|
||||
* Draw the autocomplete menu.
|
||||
*/
|
||||
public void draw(int mouseX, int mouseY) {
|
||||
if (!visible || items.isEmpty()) return;
|
||||
|
||||
// Update hover state
|
||||
updateHoverState(mouseX, mouseY);
|
||||
|
||||
// Enable scissoring to clip content
|
||||
GL11.glEnable(GL11.GL_SCISSOR_TEST);
|
||||
setScissor(x - 2, y - 2, width + 4, height + 4);
|
||||
|
||||
// Draw background
|
||||
drawRect(x, y, x + width, y + height, BG_COLOR);
|
||||
|
||||
// Draw border
|
||||
drawRect(x, y, x + width, y + 1, BORDER_COLOR);
|
||||
drawRect(x, y + height - 1, x + width, y + height, BORDER_COLOR);
|
||||
drawRect(x, y, x + 1, y + height, BORDER_COLOR);
|
||||
drawRect(x + width - 1, y, x + width, y + height, BORDER_COLOR);
|
||||
|
||||
// Draw items — tighter scissor so partial last item is clipped at the items-area boundary
|
||||
int itemAreaH = height - HINT_HEIGHT - PADDING * 2;
|
||||
setScissor(x + 1, y + PADDING , width - 2, itemAreaH);
|
||||
int itemY = y + PADDING;
|
||||
|
||||
int visibleCount = getVisibleItemCapacity();
|
||||
int renderCount = Math.min(visibleCount + 1, items.size() - scrollOffset);
|
||||
for (int i = 0; i < renderCount; i++) {
|
||||
int itemIndex = scrollOffset + i;
|
||||
if (itemIndex >= items.size()) break;
|
||||
|
||||
AutocompleteItem item = items.get(itemIndex);
|
||||
boolean isSelected = (itemIndex == selectedIndex);
|
||||
boolean isHovered = (itemIndex == hoveredIndex);
|
||||
|
||||
drawItem(item, x + PADDING, itemY, width - PADDING * 2 - 8, isSelected, isHovered);
|
||||
itemY += ITEM_HEIGHT;
|
||||
}
|
||||
|
||||
// Restore full-panel scissor for scrollbar and hint bar
|
||||
setScissor(x - 2, y - 2, width + 4, height + 4);
|
||||
|
||||
// Draw scrollbar if needed
|
||||
if (items.size() > visibleCount) {
|
||||
drawScrollbar(mouseX, mouseY);
|
||||
}
|
||||
|
||||
// Draw hint bar at bottom
|
||||
drawHintBar();
|
||||
|
||||
GL11.glDisable(GL11.GL_SCISSOR_TEST);
|
||||
// Draw resize handle outside scissor
|
||||
drawResizeHandle(mouseX, mouseY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a single autocomplete item.
|
||||
*/
|
||||
private void drawItem(AutocompleteItem item, int itemX, int itemY, int itemWidth,
|
||||
boolean selected, boolean hovered) {
|
||||
// Background
|
||||
if (selected) {
|
||||
drawRect(itemX - 2, itemY, itemX + itemWidth + 2, itemY + ITEM_HEIGHT, SELECTED_BG);
|
||||
} else if (hovered) {
|
||||
drawRect(itemX - 2, itemY, itemX + itemWidth + 2, itemY + ITEM_HEIGHT, HOVER_BG);
|
||||
}
|
||||
|
||||
int textX = itemX;
|
||||
int textY = itemY + (ITEM_HEIGHT - font.FONT_HEIGHT) / 2;
|
||||
|
||||
// Draw modifier text (static/final) if present
|
||||
boolean isStatic = item.isStatic();
|
||||
boolean isFinal = item.isFinal();
|
||||
if (isStatic || isFinal) {
|
||||
GL11.glPushMatrix();
|
||||
float scale = 0.5f;
|
||||
GL11.glScalef(scale, scale, scale);
|
||||
int col = TokenType.MODIFIER.getHexColor();
|
||||
|
||||
if (isStatic)
|
||||
font.drawString("s", (int) (textX / scale), (int) (textY / scale), col);
|
||||
if (isFinal)
|
||||
font.drawString("f", (int) (textX / scale), (int) (textY / scale) + 10, col);
|
||||
GL11.glPopMatrix();
|
||||
}
|
||||
|
||||
// Draw icon
|
||||
String icon = item.getIconId();
|
||||
int iconColor = item.getIconColor();
|
||||
font.drawString(icon, textX + (ICON_WIDTH - font.getStringWidth(icon)) / 2, textY, iconColor);
|
||||
textX += ICON_WIDTH;
|
||||
|
||||
// Calculate available width for text (leave space for type label)
|
||||
int availableWidth = itemWidth - (textX - itemX);
|
||||
if (item.getTypeLabel() != null && !item.getTypeLabel().isEmpty()) {
|
||||
int typeLabelWidth = font.getStringWidth(item.getTypeLabel());
|
||||
availableWidth -= typeLabelWidth + PADDING * 2;
|
||||
}
|
||||
|
||||
// Determine text color - gray for inherited Object methods
|
||||
int textColor = getColor(item);
|
||||
if (item.getKind() == AutocompleteItem.Kind.METHOD) {
|
||||
drawMethodNameTruncated(item.getName(), item.getMatchIndices(), textX, textY,
|
||||
textColor, availableWidth);
|
||||
} else {
|
||||
drawHighlightedTextTruncated(item.getName(), item.getMatchIndices(), textX, textY,
|
||||
textColor, availableWidth);
|
||||
}
|
||||
|
||||
// Draw type label on the right
|
||||
if (item.getTypeLabel() != null && !item.getTypeLabel().isEmpty()) {
|
||||
String typeLabel = item.getTypeLabel();
|
||||
int typeLabelWidth = font.getStringWidth(typeLabel);
|
||||
int typeLabelX = itemX + itemWidth - typeLabelWidth - PADDING;
|
||||
|
||||
TypeInfo type = item.getTypeInfo();
|
||||
int col = type != null ? type.getTokenType().getHexColor() : DIM_TEXT_COLOR;
|
||||
drawTypeLabel(typeLabel, typeLabelX, textY, col, type);
|
||||
}
|
||||
}
|
||||
|
||||
public int getColor(AutocompleteItem item) {
|
||||
if (item.isInheritedObjectMethod() || item.isDeprecated())
|
||||
return DIM_TEXT_COLOR;
|
||||
|
||||
switch (item.getKind()) {
|
||||
case METHOD:
|
||||
return TokenType.METHOD_CALL.getHexColor();
|
||||
case FIELD:
|
||||
return TokenType.GLOBAL_FIELD.getHexColor();
|
||||
case ENUM_CONSTANT:
|
||||
return TokenType.ENUM_CONSTANT.getHexColor();
|
||||
case CLASS:
|
||||
return TokenType.getColor(item.getTypeInfo());
|
||||
case VARIABLE:
|
||||
return TokenType.LOCAL_FIELD.getHexColor();
|
||||
case KEYWORD:
|
||||
return TokenType.KEYWORD.getHexColor();
|
||||
default:
|
||||
return TEXT_COLOR;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw method name with parameters colored gray.
|
||||
*/
|
||||
private void drawMethodName(String text, int[] matchIndices, int x, int y, int baseColor) {
|
||||
// Find the opening parenthesis
|
||||
int parenIndex = text.indexOf('(');
|
||||
if (parenIndex == -1) {
|
||||
// No parameters, just draw normally
|
||||
drawHighlightedText(text, matchIndices, x, y, baseColor);
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw method name part with highlighting
|
||||
String methodName = text.substring(0, parenIndex);
|
||||
drawHighlightedText(methodName, matchIndices, x, y, baseColor);
|
||||
|
||||
// Draw parameters part in gray (no highlighting)
|
||||
String params = text.substring(parenIndex);
|
||||
int paramX = x + font.getStringWidth(methodName);
|
||||
font.drawString(params, paramX, y, DIM_TEXT_COLOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw method name with parameters colored gray, truncated if too long.
|
||||
*/
|
||||
private void drawMethodNameTruncated(String text, int[] matchIndices, int x, int y, int baseColor, int maxWidth) {
|
||||
// Find the opening parenthesis
|
||||
int parenIndex = text.indexOf('(');
|
||||
if (parenIndex == -1) {
|
||||
// No parameters, just draw normally
|
||||
drawHighlightedTextTruncated(text, matchIndices, x, y, baseColor, maxWidth);
|
||||
return;
|
||||
}
|
||||
|
||||
String methodName = text.substring(0, parenIndex);
|
||||
String params = text.substring(parenIndex);
|
||||
|
||||
int methodNameWidth = font.getStringWidth(methodName);
|
||||
int paramsWidth = font.getStringWidth(params);
|
||||
int totalWidth = methodNameWidth + paramsWidth;
|
||||
|
||||
if (totalWidth <= maxWidth) {
|
||||
// Fits, draw normally
|
||||
drawHighlightedText(methodName, matchIndices, x, y, baseColor);
|
||||
int paramX = x + methodNameWidth;
|
||||
font.drawString(params, paramX, y, DIM_TEXT_COLOR);
|
||||
} else {
|
||||
// Need to truncate
|
||||
String ellipsis = "...";
|
||||
int ellipsisWidth = font.getStringWidth(ellipsis);
|
||||
|
||||
// Always show method name, truncate params if needed
|
||||
if (methodNameWidth + ellipsisWidth < maxWidth) {
|
||||
drawHighlightedText(methodName, matchIndices, x, y, baseColor);
|
||||
int paramX = x + methodNameWidth;
|
||||
|
||||
// Truncate parameters
|
||||
int availableForParams = maxWidth - methodNameWidth - ellipsisWidth;
|
||||
String truncatedParams = truncateString(params, availableForParams);
|
||||
font.drawString(truncatedParams + ellipsis, paramX, y, DIM_TEXT_COLOR);
|
||||
} else {
|
||||
// Even method name doesn't fit, truncate it too
|
||||
int availableForMethod = maxWidth - ellipsisWidth;
|
||||
String truncatedMethod = truncateString(methodName, availableForMethod);
|
||||
drawHighlightedText(truncatedMethod, matchIndices, x, y, baseColor);
|
||||
font.drawString(ellipsis, x + font.getStringWidth(truncatedMethod), y, baseColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw text with specific characters highlighted.
|
||||
*/
|
||||
private void drawHighlightedText(String text, int[] matchIndices, int x, int y, int baseColor) {
|
||||
if (matchIndices == null || matchIndices.length == 0) {
|
||||
font.drawString(text, x, y, baseColor);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a set of highlighted indices for quick lookup
|
||||
java.util.Set<Integer> highlightSet = new java.util.HashSet<>();
|
||||
for (int idx : matchIndices) {
|
||||
highlightSet.add(idx);
|
||||
}
|
||||
|
||||
// Draw character by character
|
||||
int currentX = x;
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
String ch = String.valueOf(text.charAt(i));
|
||||
int color = highlightSet.contains(i) ? HIGHLIGHT_COLOR : baseColor;
|
||||
font.drawString(ch, currentX, y, color);
|
||||
currentX += font.getStringWidth(ch);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw text with highlighting, truncated if too long.
|
||||
*/
|
||||
private void drawHighlightedTextTruncated(String text, int[] matchIndices, int x, int y, int baseColor, int maxWidth) {
|
||||
int textWidth = font.getStringWidth(text);
|
||||
|
||||
if (textWidth <= maxWidth) {
|
||||
// Fits, draw normally
|
||||
drawHighlightedText(text, matchIndices, x, y, baseColor);
|
||||
} else {
|
||||
// Truncate with ellipsis
|
||||
String ellipsis = "...";
|
||||
int ellipsisWidth = font.getStringWidth(ellipsis);
|
||||
String truncated = truncateString(text, maxWidth - ellipsisWidth);
|
||||
|
||||
// Adjust match indices for truncated text
|
||||
int[] adjustedIndices = null;
|
||||
if (matchIndices != null) {
|
||||
java.util.List<Integer> validIndices = new java.util.ArrayList<>();
|
||||
for (int idx : matchIndices) {
|
||||
if (idx < truncated.length()) {
|
||||
validIndices.add(idx);
|
||||
}
|
||||
}
|
||||
adjustedIndices = new int[validIndices.size()];
|
||||
for (int i = 0; i < validIndices.size(); i++) {
|
||||
adjustedIndices[i] = validIndices.get(i);
|
||||
}
|
||||
}
|
||||
|
||||
drawHighlightedText(truncated, adjustedIndices, x, y, baseColor);
|
||||
font.drawString(ellipsis, x + font.getStringWidth(truncated), y, baseColor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate a string to fit within the given width.
|
||||
*/
|
||||
private String truncateString(String text, int maxWidth) {
|
||||
if (text.isEmpty()) return text;
|
||||
|
||||
int width = 0;
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
width += font.getStringWidth(String.valueOf(text.charAt(i)));
|
||||
if (width > maxWidth) {
|
||||
return text.substring(0, Math.max(0, i));
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private void drawTypeLabel(String typeLabel, int x, int y, int typeColor, TypeInfo typeInfo) {
|
||||
if (typeInfo == null) {
|
||||
drawSimpleTypeWithArraySuffix(typeLabel, x, y, typeColor);
|
||||
return;
|
||||
}
|
||||
drawTypeLabel(x, y, typeInfo, 0);
|
||||
}
|
||||
|
||||
private int drawTypeLabel(int x, int y, TypeInfo typeInfo, int depth) {
|
||||
if (depth > 25) {
|
||||
String name = typeInfo.getDisplayName();
|
||||
font.drawString(name, x, y, TokenType.getColor(typeInfo));
|
||||
return x + font.getStringWidth(name);
|
||||
}
|
||||
int defaultColor = TokenType.DEFAULT.getHexColor();
|
||||
if (typeInfo.isParameterized()) {
|
||||
TypeInfo raw = typeInfo.getRawType();
|
||||
x = drawSimpleTypeWithArraySuffix(raw.getDisplayName(), x, y, TokenType.getColor(raw));
|
||||
font.drawString("<", x, y, defaultColor);
|
||||
x += font.getStringWidth("<");
|
||||
java.util.List<TypeInfo> args = typeInfo.getAppliedTypeArgs();
|
||||
for (int i = 0; i < args.size(); i++) {
|
||||
if (i > 0) {
|
||||
font.drawString(", ", x, y, defaultColor);
|
||||
x += font.getStringWidth(", ");
|
||||
}
|
||||
x = drawTypeLabel(x, y, args.get(i), depth + 1);
|
||||
}
|
||||
font.drawString(">", x, y, defaultColor);
|
||||
return x + font.getStringWidth(">");
|
||||
}
|
||||
return drawSimpleTypeWithArraySuffix(typeInfo.getDisplayName(), x, y, TokenType.getColor(typeInfo));
|
||||
}
|
||||
|
||||
/** @return x position after last drawn character */
|
||||
private int drawSimpleTypeWithArraySuffix(String typeLabel, int x, int y, int typeColor) {
|
||||
if (!typeLabel.endsWith("[]")) {
|
||||
font.drawString(typeLabel, x, y, typeColor);
|
||||
return x + font.getStringWidth(typeLabel);
|
||||
}
|
||||
int suffixStart = typeLabel.length() - 2;
|
||||
while (suffixStart >= 2 && typeLabel.charAt(suffixStart - 2) == '[' && typeLabel.charAt(suffixStart - 1) == ']') {
|
||||
suffixStart -= 2;
|
||||
}
|
||||
String core = typeLabel.substring(0, suffixStart);
|
||||
String suffix = typeLabel.substring(suffixStart);
|
||||
font.drawString(core, x, y, typeColor);
|
||||
int coreWidth = font.getStringWidth(core);
|
||||
font.drawString(suffix, x + coreWidth, y, TokenType.DEFAULT.getHexColor());
|
||||
return x + coreWidth + font.getStringWidth(suffix);
|
||||
}
|
||||
|
||||
private void drawScrollbar(int mouseX, int mouseY) {
|
||||
int scrollbarX = x + width - 8;
|
||||
int scrollbarY = y + PADDING;
|
||||
int visibleCount = getVisibleItemCapacity();
|
||||
if (visibleCount <= 0)
|
||||
return;
|
||||
int scrollbarHeight = visibleCount * ITEM_HEIGHT - 8; // leave room for resize handle
|
||||
|
||||
// Background
|
||||
drawRect(scrollbarX, scrollbarY, scrollbarX + 6, scrollbarY + scrollbarHeight, SCROLLBAR_BG);
|
||||
|
||||
// Thumb
|
||||
float thumbRatio = (float) visibleCount / items.size();
|
||||
int thumbHeight = Math.max(20, (int) (scrollbarHeight * thumbRatio));
|
||||
float thumbPosRatio = Math.min(1f, (float) scrollOffset / Math.max(1, items.size() - visibleCount));
|
||||
int thumbY = scrollbarY + (int) ((scrollbarHeight - thumbHeight) * thumbPosRatio);
|
||||
thumbY = Math.max(scrollbarY, Math.min(scrollbarY + scrollbarHeight - thumbHeight, thumbY));
|
||||
|
||||
// Check if mouse is above the scrollbar thumb
|
||||
boolean isAboveScrollbar = mouseX >= scrollbarX && mouseX <= scrollbarX + 6 &&
|
||||
mouseY >= thumbY && mouseY < thumbY + thumbHeight;
|
||||
|
||||
int col = isDraggingScrollbar || isAboveScrollbar? 0xFF808080 : SCROLLBAR_FG;
|
||||
|
||||
drawRect(scrollbarX + 1, thumbY, scrollbarX + 5, thumbY + thumbHeight, col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the hint bar at the bottom (shows Tab/Enter to confirm).
|
||||
*/
|
||||
private void drawHintBar() {
|
||||
int hintY = y + height - HINT_HEIGHT;
|
||||
|
||||
// Darker background for hint area
|
||||
drawRect(x + 1, hintY, x + width - 1, y + height - 1, HINT_BG_COLOR);
|
||||
|
||||
// Draw hints
|
||||
int hintTextY = hintY + (HINT_HEIGHT - font.FONT_HEIGHT) / 2;
|
||||
int hintX = x + PADDING;
|
||||
|
||||
// Tab hint
|
||||
drawKeyHint("Tab", hintX, hintTextY);
|
||||
hintX += font.getStringWidth("Tab") + 8;
|
||||
font.drawString("or", hintX, hintTextY, DIM_TEXT_COLOR);
|
||||
hintX += font.getStringWidth("or") + 4;
|
||||
|
||||
// Enter hint
|
||||
drawKeyHint("Enter", hintX, hintTextY);
|
||||
hintX += font.getStringWidth("Enter") + 8;
|
||||
font.drawString("to insert", hintX, hintTextY, DIM_TEXT_COLOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a key hint with a darker background box.
|
||||
*/
|
||||
private void drawKeyHint(String key, int x, int y) {
|
||||
int keyWidth = font.getStringWidth(key);
|
||||
int boxPadding = 2;
|
||||
|
||||
// Draw box
|
||||
drawRect(x - boxPadding, y - boxPadding,
|
||||
x + keyWidth + boxPadding, y + font.FONT_HEIGHT + boxPadding,
|
||||
0xFF404040);
|
||||
|
||||
// Draw key text
|
||||
font.drawString(key, x, y, TEXT_COLOR);
|
||||
}
|
||||
|
||||
// ==================== MOUSE HANDLING ====================
|
||||
|
||||
/**
|
||||
* Update hover state based on mouse position.
|
||||
*/
|
||||
private void updateHoverState(int mouseX, int mouseY) {
|
||||
hoveredIndex = -1;
|
||||
|
||||
if (!isMouseInBounds(mouseX, mouseY)) return;
|
||||
|
||||
int itemY = y + PADDING;
|
||||
|
||||
int visibleItems = getVisibleItemCapacity();
|
||||
|
||||
for (int i = 0; i <= visibleItems; i++) {
|
||||
int idx = scrollOffset + i;
|
||||
if (idx >= items.size()) break;
|
||||
if (mouseY >= itemY && mouseY < itemY + ITEM_HEIGHT) {
|
||||
hoveredIndex = idx;
|
||||
break;
|
||||
}
|
||||
itemY += ITEM_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse click.
|
||||
* @return true if click was consumed
|
||||
*/
|
||||
public boolean mouseClicked(int mouseX, int mouseY, int button) {
|
||||
if (!visible) return false;
|
||||
|
||||
// Check if click is outside menu
|
||||
if (!isMouseInBounds(mouseX, mouseY)) {
|
||||
hide();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resize handle has top priority
|
||||
if (button == 0 && isMouseOverResizeHandle(mouseX, mouseY)) {
|
||||
isResizingPanel = true;
|
||||
resizeInitMouseX = mouseX; resizeInitMouseY = mouseY;
|
||||
resizeInitW = width; resizeInitH = height;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Scrollbar
|
||||
if (button == 0 && items.size() > getVisibleItemCapacity()) {
|
||||
int scrollbarX = x + width - 8;
|
||||
if (mouseX >= scrollbarX && mouseX <= scrollbarX + 6) {
|
||||
isDraggingScrollbar = true;
|
||||
dragStartY = mouseY;
|
||||
dragStartScroll = scrollOffset;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Anywhere inside panel body — start pending drag, defer item confirm to release
|
||||
if (button == 0) {
|
||||
hasPendingPanelDrag = true;
|
||||
pendingDragStartX = mouseX;
|
||||
pendingDragStartY = mouseY;
|
||||
panelDragOffsetX = mouseX - x;
|
||||
panelDragOffsetY = mouseY - y;
|
||||
if (hoveredIndex >= 0 && hoveredIndex < items.size()) {
|
||||
selectedIndex = hoveredIndex;
|
||||
pendingItemConfirm = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return true; // Consume click if inside menu
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse release.
|
||||
* @return true if release was consumed
|
||||
*/
|
||||
public boolean mouseReleased(int mouseX, int mouseY, int button) {
|
||||
if (button == 0) {
|
||||
if (isDraggingScrollbar) { isDraggingScrollbar = false; return true; }
|
||||
if (isDraggingPanel) { isDraggingPanel = false; return true; }
|
||||
if (isResizingPanel) { isResizingPanel = false; return true; }
|
||||
hasPendingPanelDrag = false;
|
||||
if (pendingItemConfirm) {
|
||||
pendingItemConfirm = false;
|
||||
confirmSelection();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse drag.
|
||||
* @return true if drag was consumed
|
||||
*/
|
||||
public boolean mouseDragged(int mouseX, int mouseY) {
|
||||
if (!visible) return false;
|
||||
if (isDraggingPanel) { updatePanelDrag(mouseX, mouseY); return true; }
|
||||
if (isResizingPanel) { updatePanelResize(mouseX, mouseY); return true; }
|
||||
|
||||
// Activate panel drag once movement exceeds threshold
|
||||
if (hasPendingPanelDrag) {
|
||||
int dx = Math.abs(mouseX - pendingDragStartX);
|
||||
int dy = Math.abs(mouseY - pendingDragStartY);
|
||||
if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) {
|
||||
isDraggingPanel = true;
|
||||
hasPendingPanelDrag = false;
|
||||
pendingItemConfirm = false; // cancel any pending selection
|
||||
updatePanelDrag(mouseX, mouseY);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isDraggingScrollbar) return false;
|
||||
// Scrollbar drag (kept for backward compat — also handled via draw loop)
|
||||
int visibleCount = getVisibleItemCapacity();
|
||||
if (visibleCount <= 0) return false;
|
||||
int scrollbarHeight = visibleCount * ITEM_HEIGHT - 8;
|
||||
int thumbHeight = Math.max(20, (int) (scrollbarHeight * (float) visibleCount / Math.max(1, items.size())));
|
||||
int scrollTrackHeight = Math.max(1, scrollbarHeight - thumbHeight);
|
||||
int maxScroll = items.size() - visibleCount;
|
||||
int deltaY = mouseY - dragStartY;
|
||||
int scrollDelta = (deltaY * maxScroll) / scrollTrackHeight;
|
||||
scrollOffset = Math.max(0, Math.min(maxScroll, dragStartScroll + scrollDelta));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse scroll.
|
||||
* @return true if scroll was consumed
|
||||
*/
|
||||
public boolean mouseScrolled(int mouseX, int mouseY, int delta) {
|
||||
if (!visible || !isMouseInBounds(mouseX, mouseY)) return false;
|
||||
|
||||
int visibleCount = getVisibleItemCapacity();
|
||||
if (items.size() > visibleCount) {
|
||||
if (delta > 0) {
|
||||
scrollOffset = Math.max(0, scrollOffset - 1);
|
||||
} else {
|
||||
scrollOffset = Math.min(items.size() - visibleCount, scrollOffset + 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isMouseInBounds(int mouseX, int mouseY) {
|
||||
return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height;
|
||||
}
|
||||
|
||||
// ==================== UTILITY ====================
|
||||
|
||||
private void setScissor(int x, int y, int width, int height) {
|
||||
Minecraft mc = Minecraft.getMinecraft();
|
||||
ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
||||
int scaleFactor = sr.getScaleFactor();
|
||||
|
||||
if (ownerGui != null) {
|
||||
x -= (int) ownerGui.getPanX();
|
||||
y -= (int) ownerGui.getPanY();
|
||||
}
|
||||
|
||||
int scissorX = x * scaleFactor;
|
||||
int scissorY = mc.displayHeight - (y + height) * scaleFactor;
|
||||
int scissorW = width * scaleFactor;
|
||||
int scissorH = height * scaleFactor;
|
||||
|
||||
GL11.glScissor(scissorX, scissorY, scissorW, scissorH);
|
||||
}
|
||||
|
||||
// ==================== GETTERS ====================
|
||||
|
||||
public int getX() { return x; }
|
||||
public int getY() { return y; }
|
||||
public int getWidth() { return width; }
|
||||
public int getHeight() { return height; }
|
||||
public List<AutocompleteItem> getItems() { return items; }
|
||||
// ==================== PANEL DRAG / RESIZE ====================
|
||||
|
||||
public boolean isMouseOverResizeHandle(int mx, int my) {
|
||||
if (!visible) return false;
|
||||
return mx >= x + width - 8 && mx <= x + width && my >= y + height - 8 && my <= y + height;
|
||||
}
|
||||
|
||||
public void updatePanelDrag(int mouseX, int mouseY) {
|
||||
if (!isDraggingPanel) return;
|
||||
x = mouseX - panelDragOffsetX;
|
||||
y = mouseY - panelDragOffsetY;
|
||||
overriddenX = x;
|
||||
overriddenY = y;
|
||||
panelPositionOverridden = true;
|
||||
}
|
||||
|
||||
public void releasePanelDrag() { isDraggingPanel = false; hasPendingPanelDrag = false; }
|
||||
|
||||
public void updatePanelResize(int mouseX, int mouseY) {
|
||||
if (!isResizingPanel) return;
|
||||
overriddenW = Math.max(MIN_RESIZE_W, resizeInitW + (mouseX - resizeInitMouseX));
|
||||
overriddenH = Math.max(MIN_RESIZE_H, resizeInitH + (mouseY - resizeInitMouseY));
|
||||
panelSizeOverridden = true;
|
||||
menuWidth = overriddenW;
|
||||
width = overriddenW;
|
||||
height = overriddenH;
|
||||
visibleItemsCount = Math.max(1, (overriddenH - HINT_HEIGHT - PADDING * 2) / ITEM_HEIGHT);
|
||||
}
|
||||
|
||||
public void releasePanelResize() { isResizingPanel = false; }
|
||||
|
||||
/** Used by draw-loop scrollbar update in GuiScriptTextArea */
|
||||
public void updateScrollbarDragDraw(int mouseY) {
|
||||
if (!isDraggingScrollbar || items.isEmpty()) return;
|
||||
int visibleCount = getVisibleItemCapacity();
|
||||
if (visibleCount <= 0) return;
|
||||
int scrollbarHeight = visibleCount * ITEM_HEIGHT - 8;
|
||||
int thumbHeight = Math.max(20, (int) (scrollbarHeight * (float) visibleCount / items.size()));
|
||||
int scrollTrackHeight = Math.max(1, scrollbarHeight - thumbHeight);
|
||||
int maxScroll = items.size() - visibleCount;
|
||||
int deltaY = mouseY - dragStartY;
|
||||
int scrollDelta = (deltaY * maxScroll) / scrollTrackHeight;
|
||||
scrollOffset = Math.max(0, Math.min(maxScroll, dragStartScroll + scrollDelta));
|
||||
}
|
||||
|
||||
public void releaseScrollbarDrag() { isDraggingScrollbar = false; }
|
||||
|
||||
public boolean isDraggingPanel() { return isDraggingPanel; }
|
||||
public boolean isDraggingScrollbarMenu() { return isDraggingScrollbar; }
|
||||
public boolean isResizingPanel() { return isResizingPanel; }
|
||||
|
||||
private void drawResizeHandle(int mouseX, int mouseY) {
|
||||
boolean resizeActive = isResizingPanel || isMouseOverResizeHandle(mouseX, mouseY);
|
||||
int dotColor = resizeActive ? 0xFFCCCCCC : 0x80888888;
|
||||
int rhX = x + width - 2;
|
||||
int rhY = y + height - 2;
|
||||
// Three-dot diagonal pattern (◢)
|
||||
drawRect(rhX - 1, rhY - 1, rhX, rhY, dotColor);
|
||||
drawRect(rhX - 3, rhY - 1, rhX - 2, rhY, dotColor);
|
||||
drawRect(rhX - 1, rhY - 3, rhX, rhY - 2, dotColor);
|
||||
drawRect(rhX - 5, rhY - 1, rhX - 4, rhY, dotColor);
|
||||
drawRect(rhX - 3, rhY - 3, rhX - 2, rhY - 2, dotColor);
|
||||
drawRect(rhX - 1, rhY - 5, rhX, rhY - 4, dotColor);
|
||||
}
|
||||
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Abstract provider for autocomplete suggestions.
|
||||
* Implementations handle different contexts (after dot, in identifier, etc.)
|
||||
*/
|
||||
public interface AutocompleteProvider {
|
||||
|
||||
/**
|
||||
* Context information for autocomplete requests.
|
||||
*/
|
||||
class Context {
|
||||
/** Full text of the document */
|
||||
public final String text;
|
||||
/** Current cursor position in the document */
|
||||
public final int cursorPosition;
|
||||
/** The current line number (0-indexed) */
|
||||
public final int lineNumber;
|
||||
/** Position of cursor within the current line */
|
||||
public final int columnPosition;
|
||||
/** The current line text */
|
||||
public final String currentLine;
|
||||
/** The word being typed (prefix before cursor, after last separator) */
|
||||
public final String prefix;
|
||||
/** Start position of the prefix in the document */
|
||||
public final int prefixStart;
|
||||
/** Whether this is after a dot (member access) */
|
||||
public final boolean isMemberAccess;
|
||||
/** Expression before the dot (for member access) */
|
||||
public final String receiverExpression;
|
||||
/** Whether autocomplete was explicitly triggered (CTRL+Space) */
|
||||
public final boolean explicitTrigger;
|
||||
/** Whether to show only methods (for method reference context ::) */
|
||||
public final boolean methodsOnly;
|
||||
|
||||
public Context(String text, int cursorPosition, int lineNumber, int columnPosition,
|
||||
String currentLine, String prefix, int prefixStart,
|
||||
boolean isMemberAccess, String receiverExpression, boolean explicitTrigger) {
|
||||
this(text, cursorPosition, lineNumber, columnPosition, currentLine, prefix, prefixStart,
|
||||
isMemberAccess, receiverExpression, explicitTrigger, false);
|
||||
}
|
||||
|
||||
public Context(String text, int cursorPosition, int lineNumber, int columnPosition,
|
||||
String currentLine, String prefix, int prefixStart,
|
||||
boolean isMemberAccess, String receiverExpression, boolean explicitTrigger,
|
||||
boolean methodsOnly) {
|
||||
this.text = text;
|
||||
this.cursorPosition = cursorPosition;
|
||||
this.lineNumber = lineNumber;
|
||||
this.columnPosition = columnPosition;
|
||||
this.currentLine = currentLine;
|
||||
this.prefix = prefix;
|
||||
this.prefixStart = prefixStart;
|
||||
this.isMemberAccess = isMemberAccess;
|
||||
this.receiverExpression = receiverExpression;
|
||||
this.explicitTrigger = explicitTrigger;
|
||||
this.methodsOnly = methodsOnly;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get autocomplete suggestions for the given context.
|
||||
* @param context The context containing cursor position, text, etc.
|
||||
* @return List of autocomplete items, sorted by relevance
|
||||
*/
|
||||
List<AutocompleteItem> getSuggestions(Context context);
|
||||
|
||||
/**
|
||||
* Check if this provider can handle the given context.
|
||||
*/
|
||||
boolean canProvide(Context context);
|
||||
}
|
||||
+354
@@ -0,0 +1,354 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.js_parser.*;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.*;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
import noppes.npcs.config.ConfigScript;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Autocomplete provider for JavaScript/ECMAScript scripts.
|
||||
* Uses ScriptDocument's unified TypeInfo/TypeResolver system for type resolution.
|
||||
*/
|
||||
public class JSAutocompleteProvider extends JavaAutocompleteProvider {
|
||||
|
||||
private final JSTypeRegistry registry;
|
||||
|
||||
public JSAutocompleteProvider() {
|
||||
this.registry = JSTypeRegistry.getInstance();
|
||||
if (!registry.isInitialized()) {
|
||||
registry.initializeFromResources();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canProvide(Context context) {
|
||||
return document != null && document.isJavaScript();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add suggestions for member access (after dot).
|
||||
* Uses ScriptDocument's unified type resolution system.
|
||||
*/
|
||||
protected void addMemberSuggestions(Context context, List<AutocompleteItem> items) {
|
||||
String receiverExpr = context.receiverExpression;
|
||||
if (receiverExpr == null || receiverExpr.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use ScriptDocument's resolveExpressionType - handles both Java and JS
|
||||
TypeInfo receiverType = document.resolveExpressionType(receiverExpr, getMemberAccessResolvePosition(context));
|
||||
if (receiverType == null || !receiverType.isResolved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for synthetic types first (e.g., Nashorn Java object, custom types)
|
||||
SyntheticType syntheticType =
|
||||
document.getTypeResolver().getSyntheticType(receiverType.getSimpleName());
|
||||
if (syntheticType != null) {
|
||||
addSyntheticTypeSuggestions(syntheticType, items, context.methodsOnly);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isStaticContext = isStaticAccess(receiverExpr, getMemberAccessResolvePosition(context));
|
||||
|
||||
Class<?> javaClass = receiverType.getJavaClass();
|
||||
if (javaClass != null) {
|
||||
Set<String> addedMethods = new HashSet<>();
|
||||
for (Method method : javaClass.getMethods()) {
|
||||
if (!Modifier.isPublic(method.getModifiers())) continue;
|
||||
if (isStaticContext && !Modifier.isStatic(method.getModifiers())) continue;
|
||||
String sig = method.getName() + "(" + method.getParameterCount() + ")";
|
||||
if (addedMethods.add(sig)) {
|
||||
items.add(AutocompleteItem.fromMethod(MethodInfo.fromReflection(method, receiverType), context.methodsOnly));
|
||||
}
|
||||
}
|
||||
List<MethodInfo> effectiveSyntheticMethods = receiverType.getSyntheticMethods();
|
||||
if (!effectiveSyntheticMethods.isEmpty()) {
|
||||
for (MethodInfo method : effectiveSyntheticMethods) {
|
||||
if (isStaticContext && !method.isStatic()) continue;
|
||||
items.add(AutocompleteItem.fromMethod(method, context.methodsOnly));
|
||||
}
|
||||
} else {
|
||||
JSTypeInfo jsType = getMergeRegistryType(javaClass);
|
||||
if (jsType != null) {
|
||||
addMethodsFromType(jsType, receiverType, items, new HashSet<>(), context.methodsOnly, isStaticContext);
|
||||
}
|
||||
}
|
||||
if (!context.methodsOnly) {
|
||||
for (Field field : javaClass.getFields()) {
|
||||
if (!Modifier.isPublic(field.getModifiers())) continue;
|
||||
if (isStaticContext && !Modifier.isStatic(field.getModifiers())) continue;
|
||||
items.add(AutocompleteItem.fromField(FieldInfo.fromReflection(field, receiverType)));
|
||||
}
|
||||
if (!receiverType.getSyntheticFields().isEmpty()) {
|
||||
for (FieldInfo field : receiverType.getSyntheticFields()) {
|
||||
if (isStaticContext && !field.isStatic()) continue;
|
||||
items.add(AutocompleteItem.fromField(field));
|
||||
}
|
||||
} else {
|
||||
JSTypeInfo jsType = getMergeRegistryType(javaClass);
|
||||
if (jsType != null) {
|
||||
addFieldsFromType(jsType, receiverType, items, new HashSet<>(), isStaticContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isStaticContext && javaClass != Object.class) {
|
||||
JSTypeInfo objJSType = registry.getType("Object");
|
||||
if (objJSType != null) {
|
||||
for (JSMethodInfo m : objJSType.getMethods().values()) {
|
||||
if (!m.isStatic()) {
|
||||
items.add(AutocompleteItem.fromJSMethod(m, receiverType, 1, context.methodsOnly));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> syntheticMethodNames = new HashSet<>();
|
||||
for (MethodInfo method : receiverType.getSyntheticMethods()) {
|
||||
syntheticMethodNames.add(method.getName());
|
||||
if (isStaticContext && !method.isStatic()) continue;
|
||||
items.add(AutocompleteItem.fromMethod(method, context.methodsOnly));
|
||||
}
|
||||
for (FieldInfo field : receiverType.getSyntheticFields()) {
|
||||
if (!context.methodsOnly) {
|
||||
if (isStaticContext && !field.isStatic()) continue;
|
||||
if (syntheticMethodNames.contains(field.getName())) continue;
|
||||
items.add(AutocompleteItem.fromField(field));
|
||||
}
|
||||
}
|
||||
|
||||
JSTypeInfo jsTypeInfo = receiverType.getJSTypeInfo();
|
||||
if (jsTypeInfo != null) {
|
||||
addMethodsFromType(jsTypeInfo, receiverType, items, new HashSet<>(), context.methodsOnly, isStaticContext);
|
||||
if (!context.methodsOnly && ConfigScript.ShowImplementationFieldsInAutocomplete) {
|
||||
addFieldsFromType(jsTypeInfo, receiverType, items, new HashSet<>(), isStaticContext);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isStaticContext) {
|
||||
Set<String> addedObjMethods = new HashSet<>();
|
||||
for (Method m : Object.class.getMethods()) {
|
||||
if (!Modifier.isPublic(m.getModifiers()) || Modifier.isStatic(m.getModifiers())) continue;
|
||||
String sig = m.getName() + "(" + m.getParameterCount() + ")";
|
||||
if (addedObjMethods.add(sig)) {
|
||||
items.add(AutocompleteItem.fromMethod(MethodInfo.fromReflection(m, receiverType), context.methodsOnly));
|
||||
}
|
||||
}
|
||||
JSTypeInfo objJSType = registry.getType("Object");
|
||||
if (objJSType != null) {
|
||||
for (JSMethodInfo m : objJSType.getMethods().values()) {
|
||||
if (!m.isStatic()) {
|
||||
items.add(AutocompleteItem.fromJSMethod(m, receiverType, 1, context.methodsOnly));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private JSTypeInfo getMergeRegistryType(Class<?> javaClass) {
|
||||
return document.getTypeResolver().findJSTypeForJavaClass(javaClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add autocomplete suggestions for a synthetic type's methods and fields.
|
||||
* @param syntheticType The synthetic type
|
||||
* @param items The list to add items to
|
||||
* @param forMethodReference If true, only add methods with no parentheses in insert text
|
||||
*/
|
||||
private void addSyntheticTypeSuggestions(SyntheticType syntheticType, List<AutocompleteItem> items, boolean forMethodReference) {
|
||||
// Add methods
|
||||
for (SyntheticMethod method : syntheticType.getMethods()) {
|
||||
AutocompleteItem item = AutocompleteItem.fromSyntheticMethod(method, syntheticType.getTypeInfo(), forMethodReference);
|
||||
items.add(item);
|
||||
}
|
||||
|
||||
// Add fields (skip if forMethodReference is true)
|
||||
if (!forMethodReference) {
|
||||
for (SyntheticField field : syntheticType.getFields()) {
|
||||
AutocompleteItem item = AutocompleteItem.fromSyntheticField(field);
|
||||
items.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively add methods from a type and its parents.
|
||||
* @param type The current JS type to get methods from (changes as we walk up inheritance)
|
||||
* @param contextType The original TypeInfo context for resolving type parameters (stays constant)
|
||||
*/
|
||||
protected void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added) {
|
||||
addMethodsFromType(type, contextType, items, added, 0, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively add methods from a type and its parents.
|
||||
* @param type The current JS type to get methods from (changes as we walk up inheritance)
|
||||
* @param contextType The original TypeInfo context for resolving type parameters (stays constant)
|
||||
* @param forMethodReference If true, insert text will NOT include parentheses
|
||||
*/
|
||||
protected void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added, boolean forMethodReference) {
|
||||
addMethodsFromType(type, contextType, items, added, 0, forMethodReference, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively add methods from a type and its parents.
|
||||
* @param type The current JS type to get methods from (changes as we walk up inheritance)
|
||||
* @param contextType The original TypeInfo context for resolving type parameters (stays constant)
|
||||
* @param forMethodReference If true, insert text will NOT include parentheses
|
||||
* @param isStaticContext If true, only add static methods
|
||||
*/
|
||||
protected void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added, boolean forMethodReference, boolean isStaticContext) {
|
||||
addMethodsFromType(type, contextType, items, added, 0, forMethodReference, isStaticContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively add methods from a type and its parents with inheritance depth tracking.
|
||||
* Shows all overloads - one autocomplete item per overload.
|
||||
* @param type The current JS type in inheritance chain
|
||||
* @param contextType The original TypeInfo context for resolving type parameters (e.g., IPlayer with T → EntityPlayerMP)
|
||||
*/
|
||||
private void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added, int depth) {
|
||||
addMethodsFromType(type, contextType, items, added, depth, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively add methods from a type and its parents with inheritance depth tracking.
|
||||
* Shows all overloads - one autocomplete item per overload.
|
||||
* @param type The current JS type in inheritance chain
|
||||
* @param contextType The original TypeInfo context for resolving type parameters (e.g., IPlayer with T → EntityPlayerMP)
|
||||
* @param forMethodReference If true, insert text will NOT include parentheses
|
||||
*/
|
||||
private void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added, int depth, boolean forMethodReference) {
|
||||
addMethodsFromType(type, contextType, items, added, depth, forMethodReference, false);
|
||||
}
|
||||
|
||||
private void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added, int depth, boolean forMethodReference, boolean isStaticContext) {
|
||||
// Collect base method names (without $N suffix) that we haven't processed yet
|
||||
Set<String> baseNames = new HashSet<>();
|
||||
for (String key : type.getMethods().keySet()) {
|
||||
String baseName = key.contains("$") ? key.substring(0, key.indexOf('$')) : key;
|
||||
if (!added.contains(baseName)) {
|
||||
baseNames.add(baseName);
|
||||
}
|
||||
}
|
||||
|
||||
// For each base method name, add all overloads
|
||||
for (String baseName : baseNames) {
|
||||
added.add(baseName);
|
||||
// Use getMethodOverloads to get all overloads for this method (excluding inherited)
|
||||
List<JSMethodInfo> overloads = new ArrayList<>();
|
||||
if (type.getMethods().containsKey(baseName)) {
|
||||
overloads.add(type.getMethods().get(baseName));
|
||||
}
|
||||
int index = 1;
|
||||
while (type.getMethods().containsKey(baseName + "$" + index)) {
|
||||
overloads.add(type.getMethods().get(baseName + "$" + index));
|
||||
index++;
|
||||
}
|
||||
// Add one autocomplete item per overload
|
||||
for (JSMethodInfo method : overloads) {
|
||||
if (isStaticContext && !method.isStatic()) continue;
|
||||
items.add(AutocompleteItem.fromJSMethod(method, contextType, depth, forMethodReference));
|
||||
}
|
||||
}
|
||||
|
||||
// Add from parent type with incremented depth - keep same contextType
|
||||
if (type.getResolvedParent() != null) {
|
||||
addMethodsFromType(type.getResolvedParent(), contextType, items, added, depth + 1, forMethodReference, isStaticContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively add fields from a type and its parents.
|
||||
* @param type The current JS type to get fields from (changes as we walk up inheritance)
|
||||
* @param contextType The original TypeInfo context for resolving type parameters (stays constant)
|
||||
*/
|
||||
protected void addFieldsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added) {
|
||||
addFieldsFromType(type, contextType, items, added, 0, false);
|
||||
}
|
||||
|
||||
protected void addFieldsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added, boolean isStaticContext) {
|
||||
addFieldsFromType(type, contextType, items, added, 0, isStaticContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively add fields from a type and its parents with inheritance depth tracking.
|
||||
* @param type The current JS type in inheritance chain
|
||||
* @param contextType The original TypeInfo context for resolving type parameters
|
||||
*/
|
||||
private void addFieldsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added, int depth) {
|
||||
addFieldsFromType(type, contextType, items, added, depth, false);
|
||||
}
|
||||
|
||||
private void addFieldsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added, int depth, boolean isStaticContext) {
|
||||
for (JSFieldInfo field : type.getFields().values()) {
|
||||
if (!added.contains(field.getName())) {
|
||||
if (isStaticContext && !field.isStatic()) continue;
|
||||
added.add(field.getName());
|
||||
// Pass contextType (original receiver) for type parameter resolution, not current type
|
||||
items.add(AutocompleteItem.fromJSField(field, contextType, depth));
|
||||
}
|
||||
}
|
||||
|
||||
// Add from parent type with incremented depth - keep same contextType
|
||||
if (type.getResolvedParent() != null) {
|
||||
addFieldsFromType(type.getResolvedParent(), contextType, items, added, depth + 1, isStaticContext);
|
||||
}
|
||||
}
|
||||
|
||||
protected void addUnimportedClassSuggestions(String prefix, List<AutocompleteItem> items) {
|
||||
// For JavaScript, we typically don't suggest unimported classes
|
||||
// as imports are handled differently. This can be customized
|
||||
// if needed to suggest global JS types.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addLanguageUniqueSuggestions(Context context, List<AutocompleteItem> items) {
|
||||
super.addLanguageUniqueSuggestions(context, items);
|
||||
|
||||
// Add global variables from both global engine objects and document editor/DataScript globals
|
||||
List<String> globalNames = new ArrayList<>();
|
||||
globalNames.addAll(registry.getGlobalEngineObjects().keySet());
|
||||
globalNames.addAll(document.getEditorGlobals().keySet());
|
||||
|
||||
for (String name : globalNames) {
|
||||
FieldInfo fieldInfo = document.resolveVariable(name, context.cursorPosition);
|
||||
if (fieldInfo == null || !fieldInfo.isResolved())
|
||||
continue;
|
||||
|
||||
items.add(AutocompleteItem.fromField(fieldInfo));
|
||||
}
|
||||
|
||||
// Add global imports in JSTypeRegistry.globalEngineImports
|
||||
for (String importName : registry.getGlobalEngineImports().keySet()) {
|
||||
JSTypeInfo jsTypeInfo = registry.getGlobalImportType(importName);
|
||||
if (jsTypeInfo != null) {
|
||||
items.add(AutocompleteItem.fromType(TypeInfo.fromJSTypeInfo(jsTypeInfo)));
|
||||
}
|
||||
}
|
||||
|
||||
// Add global engine functions (parseInt, parseFloat, isNaN, isFinite, etc.)
|
||||
for (JSMethodInfo fn : registry.getGlobalEngineFunctions().values()) {
|
||||
items.add(AutocompleteItem.fromJSMethod(fn, 0));
|
||||
}
|
||||
}
|
||||
|
||||
protected UsageTracker getUsageTracker() {
|
||||
return UsageTracker.getJSInstance();
|
||||
}
|
||||
|
||||
public String[] getKeywords() {
|
||||
return TypeChecker.getJavaScriptKeywords();
|
||||
}
|
||||
}
|
||||
+605
@@ -0,0 +1,605 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
|
||||
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 java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Autocomplete provider for Java/Groovy scripts.
|
||||
* Uses ScriptDocument for type resolution and scope analysis.
|
||||
*/
|
||||
public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
|
||||
protected ScriptDocument document;
|
||||
|
||||
public void setDocument(ScriptDocument document) {
|
||||
this.document = document;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canProvide(Context context) {
|
||||
return document != null && !document.isJavaScript();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AutocompleteItem> getSuggestions(Context context) {
|
||||
List<AutocompleteItem> items = new ArrayList<>();
|
||||
|
||||
if (document == null) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// Resolve owner type for usage tracking and static context detection
|
||||
String ownerFullName = null;
|
||||
boolean isStaticContext = false;
|
||||
if (context.isMemberAccess && context.receiverExpression != null) {
|
||||
int resolvePos = getMemberAccessResolvePosition(context);
|
||||
TypeInfo receiverType = document.resolveExpressionType(
|
||||
context.receiverExpression, resolvePos);
|
||||
if (receiverType != null && receiverType.isResolved()) {
|
||||
ownerFullName = receiverType.getFullName();
|
||||
}
|
||||
// Check if accessing through a class (static context) vs instance
|
||||
isStaticContext = isStaticAccess(context.receiverExpression, resolvePos);
|
||||
}
|
||||
|
||||
if (context.isMemberAccess) {
|
||||
// Member access: resolve type of receiver and get its members
|
||||
addMemberSuggestions(context, items);
|
||||
} else {
|
||||
// Identifier context: show variables, methods, types in scope
|
||||
addScopeSuggestions(context, items);
|
||||
}
|
||||
|
||||
// For method reference context, filter to show only methods (and constructor "new")
|
||||
if (context.methodsOnly) {
|
||||
items.removeIf(item -> item.getKind() != AutocompleteItem.Kind.METHOD);
|
||||
|
||||
// Add "new" constructor reference option if the receiver is a class
|
||||
if (context.receiverExpression != null) {
|
||||
int resolvePos = getMemberAccessResolvePosition(context);
|
||||
TypeInfo receiverType = document.resolveExpressionType(
|
||||
context.receiverExpression, resolvePos);
|
||||
if (receiverType != null && receiverType.isResolved() && isStaticContext) {
|
||||
items.add(new AutocompleteItem.Builder()
|
||||
.name("new")
|
||||
.insertText("new")
|
||||
.kind(AutocompleteItem.Kind.KEYWORD)
|
||||
.typeLabel("constructor")
|
||||
.signature(receiverType.getSimpleName() + "::new")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter and score by prefix, then apply usage boosts and static penalties
|
||||
filterAndScore(items, context.prefix, context.isMemberAccess, isStaticContext, ownerFullName);
|
||||
|
||||
// Sort by score
|
||||
Collections.sort(items);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add suggestions for member access (after dot).
|
||||
*/
|
||||
protected void addMemberSuggestions(Context context, List<AutocompleteItem> items) {
|
||||
String receiverExpr = context.receiverExpression;
|
||||
if (receiverExpr == null || receiverExpr.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve the type of the receiver expression
|
||||
TypeInfo receiverType = document.resolveExpressionType(receiverExpr, getMemberAccessResolvePosition(context));
|
||||
|
||||
if (receiverType == null || !receiverType.isResolved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine if this is a static context (accessing a class type)
|
||||
boolean isStaticContext = isStaticAccess(receiverExpr, getMemberAccessResolvePosition(context));
|
||||
|
||||
Class<?> clazz = receiverType.getJavaClass();
|
||||
if (clazz == null) {
|
||||
// Try ScriptTypeInfo
|
||||
if (receiverType instanceof ScriptTypeInfo) {
|
||||
addScriptTypeMembers((ScriptTypeInfo) receiverType, items, isStaticContext, context.methodsOnly);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Add methods
|
||||
Set<String> addedMethods = new HashSet<>();
|
||||
for (Method method : clazz.getMethods()) {
|
||||
if (Modifier.isPublic(method.getModifiers())) {
|
||||
// Filter by static context
|
||||
if (isStaticContext && !Modifier.isStatic(method.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String sig = method.getName() + "(" + method.getParameterCount() + ")";
|
||||
if (!addedMethods.contains(sig)) {
|
||||
addedMethods.add(sig);
|
||||
MethodInfo methodInfo = MethodInfo.fromReflection(method, receiverType);
|
||||
items.add(AutocompleteItem.fromMethod(methodInfo, context.methodsOnly));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (MethodInfo method : receiverType.getSyntheticMethods()) {
|
||||
if (isStaticContext && !Modifier.isStatic(method.getModifiers()))
|
||||
continue;
|
||||
|
||||
items.add(AutocompleteItem.fromMethod(method, context.methodsOnly));
|
||||
}
|
||||
|
||||
// Add fields (skip if methodsOnly is true - but the filtering will be done in getSuggestions)
|
||||
if (!context.methodsOnly) {
|
||||
for (Field field : clazz.getFields()) {
|
||||
if (Modifier.isPublic(field.getModifiers())) {
|
||||
// Filter by static context
|
||||
if (isStaticContext && !Modifier.isStatic(field.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
FieldInfo fieldInfo = FieldInfo.fromReflection(field, receiverType);
|
||||
items.add(AutocompleteItem.fromField(fieldInfo));
|
||||
}
|
||||
}
|
||||
|
||||
for (FieldInfo field : receiverType.getSyntheticFields()) {
|
||||
if (isStaticContext && !Modifier.isStatic(field.getModifiers()))
|
||||
continue;
|
||||
|
||||
items.add(AutocompleteItem.fromField(field));
|
||||
}
|
||||
}
|
||||
|
||||
// Add nested types (inner classes/interfaces) in static context
|
||||
if (isStaticContext && !context.methodsOnly) {
|
||||
try {
|
||||
for (Class<?> nested : clazz.getDeclaredClasses()) {
|
||||
if (!Modifier.isPublic(nested.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
// Convert $ notation to dot notation for resolution
|
||||
String nestedFullName = nested.getName().replace('$', '.');
|
||||
TypeInfo nestedType = TypeResolver.getInstance().resolveFullName(nestedFullName);
|
||||
if (nestedType != null && nestedType.isResolved()) {
|
||||
items.add(AutocompleteItem.fromType(nestedType));
|
||||
}
|
||||
}
|
||||
} catch (SecurityException ignored) {
|
||||
// Some runtimes may restrict getDeclaredClasses()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a stable position for resolving member-access receiver types.
|
||||
*
|
||||
* Using `prefixStart` can land on end-exclusive scope boundaries (position == bodyEnd)
|
||||
* depending on caller cursor semantics and surrounding syntax, causing scope lookups
|
||||
* to fail. For member access we prefer the dot position (or nearest non-whitespace)
|
||||
* immediately before prefixStart.
|
||||
*/
|
||||
protected int getMemberAccessResolvePosition(Context context) {
|
||||
if (context == null || context.text == null || context.text.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pos = Math.max(0, Math.min(context.prefixStart, context.text.length()));
|
||||
int i = Math.min(pos - 1, context.text.length() - 1);
|
||||
|
||||
// Skip whitespace backwards
|
||||
while (i >= 0 && Character.isWhitespace(context.text.charAt(i))) {
|
||||
i--;
|
||||
}
|
||||
|
||||
// Prefer the dot index if present
|
||||
if (i >= 0 && context.text.charAt(i) == '.') {
|
||||
return i;
|
||||
}
|
||||
|
||||
// Fallback: use the provided prefixStart (caret context)
|
||||
return Math.max(0, Math.min(context.prefixStart, context.text.length()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add members from a script-defined type.
|
||||
*/
|
||||
protected void addScriptTypeMembers(ScriptTypeInfo scriptType, List<AutocompleteItem> items, boolean isStaticContext) {
|
||||
addScriptTypeMembers(scriptType, items, isStaticContext, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add members from a script-defined type.
|
||||
* @param scriptType The script type to get members from
|
||||
* @param items The list to add items to
|
||||
* @param isStaticContext Whether we're in a static context
|
||||
* @param forMethodReference If true, only add methods with no parentheses in insert text
|
||||
*/
|
||||
protected void addScriptTypeMembers(ScriptTypeInfo scriptType, List<AutocompleteItem> items, boolean isStaticContext, boolean forMethodReference) {
|
||||
// Add methods (getMethods returns Map<String, List<MethodInfo>>)
|
||||
for (List<MethodInfo> overloads : scriptType.getMethods().values()) {
|
||||
for (MethodInfo method : overloads) {
|
||||
// Filter by static context
|
||||
if (isStaticContext && !method.isStatic()) {
|
||||
continue;
|
||||
}
|
||||
items.add(AutocompleteItem.fromMethod(method, forMethodReference));
|
||||
}
|
||||
}
|
||||
|
||||
// Add fields (skip if methodsOnly/forMethodReference is true)
|
||||
if (!forMethodReference) {
|
||||
// Add fields (getFields returns Map<String, FieldInfo>)
|
||||
for (FieldInfo field : scriptType.getFields().values()) {
|
||||
// Filter by static context
|
||||
if (isStaticContext && !field.isStatic()) {
|
||||
continue;
|
||||
}
|
||||
items.add(AutocompleteItem.fromField(field));
|
||||
}
|
||||
|
||||
// Add enum constants (getEnumConstants returns Map<String, EnumConstantInfo>)
|
||||
for (EnumConstantInfo enumConstant : scriptType.getEnumConstants().values()) {
|
||||
items.add(AutocompleteItem.fromField(enumConstant.getFieldInfo()));
|
||||
}
|
||||
|
||||
// Add nested types (inner classes/interfaces) in static context
|
||||
if (isStaticContext) {
|
||||
for (ScriptTypeInfo inner : scriptType.getInnerClasses()) {
|
||||
items.add(AutocompleteItem.fromType(inner));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add parent class members
|
||||
if (scriptType.hasSuperClass()) {
|
||||
TypeInfo superType = scriptType.getSuperClass();
|
||||
if (superType != null && superType.getJavaClass() != null) {
|
||||
Class<?> superClazz = superType.getJavaClass();
|
||||
Set<String> addedMethods = new HashSet<>();
|
||||
for (Method method : superClazz.getMethods()) {
|
||||
if (Modifier.isPublic(method.getModifiers())) {
|
||||
String sig = method.getName() + "(" + method.getParameterCount() + ")";
|
||||
if (!addedMethods.contains(sig)) {
|
||||
addedMethods.add(sig);
|
||||
MethodInfo methodInfo = MethodInfo.fromReflection(method, superType);
|
||||
items.add(AutocompleteItem.fromMethod(methodInfo, forMethodReference));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add suggestions based on current scope (not after a dot).
|
||||
*/
|
||||
protected void addScopeSuggestions(Context context, List<AutocompleteItem> items) {
|
||||
int pos = context.cursorPosition;
|
||||
|
||||
// Find containing method
|
||||
MethodInfo containingMethod = document.findContainingMethod(pos);
|
||||
|
||||
// Add local variables
|
||||
if (containingMethod != null) {
|
||||
List<FieldInfo> available = document.getAvailableVariablesAt(pos);
|
||||
Set<String> seenNames = new HashSet<>();
|
||||
for (FieldInfo field : available) {
|
||||
if (!(field.isLocal() || field.isParameter())) {
|
||||
continue;
|
||||
}
|
||||
if (seenNames.add(field.getName())) {
|
||||
items.add(AutocompleteItem.fromField(field));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add global fields
|
||||
for (FieldInfo globalField : document.getGlobalFields().values()) {
|
||||
if (globalField.isVisibleAt(pos)) {
|
||||
items.add(AutocompleteItem.fromField(globalField));
|
||||
}
|
||||
}
|
||||
|
||||
// Add enclosing type fields (find via script types map)
|
||||
ScriptTypeInfo enclosingType = findEnclosingType(pos);
|
||||
if (enclosingType != null) {
|
||||
for (FieldInfo field : enclosingType.getFields().values()) {
|
||||
if (field.isVisibleAt(pos)) {
|
||||
items.add(AutocompleteItem.fromField(field));
|
||||
}
|
||||
}
|
||||
|
||||
// Add methods from enclosing type
|
||||
for (List<MethodInfo> overloads : enclosingType.getMethods().values()) {
|
||||
for (MethodInfo method : overloads) {
|
||||
items.add(AutocompleteItem.fromMethod(method, context.methodsOnly));
|
||||
}
|
||||
}
|
||||
|
||||
// Add inner classes of the enclosing type as type suggestions
|
||||
for (ScriptTypeInfo inner : enclosingType.getInnerClasses()) {
|
||||
items.add(AutocompleteItem.fromType(inner));
|
||||
}
|
||||
|
||||
// Walk up the outerClass chain to include outer class members and sibling types.
|
||||
// Inside a nested class, users can access outer class fields/methods/sibling types.
|
||||
ScriptTypeInfo outer = enclosingType.getOuterClass();
|
||||
while (outer != null) {
|
||||
for (ScriptTypeInfo sibling : outer.getInnerClasses()) {
|
||||
if (sibling != enclosingType) {
|
||||
items.add(AutocompleteItem.fromType(sibling));
|
||||
}
|
||||
}
|
||||
for (FieldInfo field : outer.getFields().values()) {
|
||||
items.add(AutocompleteItem.fromField(field));
|
||||
}
|
||||
for (List<MethodInfo> overloads : outer.getMethods().values()) {
|
||||
for (MethodInfo method : overloads) {
|
||||
items.add(AutocompleteItem.fromMethod(method, context.methodsOnly));
|
||||
}
|
||||
}
|
||||
outer = outer.getOuterClass();
|
||||
}
|
||||
}
|
||||
|
||||
// Add script-defined methods
|
||||
for (MethodInfo method : document.getAllMethods()) {
|
||||
items.add(AutocompleteItem.fromMethod(method, context.methodsOnly));
|
||||
}
|
||||
|
||||
addLanguageUniqueSuggestions(context, items);
|
||||
|
||||
// Add keywords
|
||||
addKeywords(items);
|
||||
}
|
||||
|
||||
protected void addLanguageUniqueSuggestions(Context context, List<AutocompleteItem> items) {
|
||||
// Add imported types
|
||||
for (TypeInfo type : document.getImportedTypes()) {
|
||||
items.add(AutocompleteItem.fromType(type));
|
||||
}
|
||||
|
||||
// Add script-defined types
|
||||
for (ScriptTypeInfo scriptType : document.getScriptTypesMap().values()) {
|
||||
items.add(AutocompleteItem.fromType(scriptType));
|
||||
}
|
||||
|
||||
// Add unimported classes that match the prefix (for auto-import)
|
||||
if (context.prefix != null && context.prefix.length() >= 2 && Character.isUpperCase(context.prefix.charAt(0))) {
|
||||
addUnimportedClassSuggestions(context.prefix, items);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add suggestions for unimported classes that match the prefix.
|
||||
* These will trigger auto-import when selected.
|
||||
*/
|
||||
protected void addUnimportedClassSuggestions(String prefix, List<AutocompleteItem> items) {
|
||||
// Get the type resolver
|
||||
TypeResolver resolver = TypeResolver.getInstance();
|
||||
// Find classes matching this prefix (not just exact matches)
|
||||
List<String> matchingClasses = resolver.findClassesByPrefix(prefix, -1);
|
||||
|
||||
// Track what's already imported to avoid duplicates
|
||||
Set<String> importedFullNames = new HashSet<>();
|
||||
for (TypeInfo imported : document.getImportedTypes()) {
|
||||
importedFullNames.add(imported.getFullName());
|
||||
}
|
||||
|
||||
// Also track simple names already in the list to avoid showing both
|
||||
// imported and unimported versions of the same class
|
||||
Set<String> existingSimpleNames = new HashSet<>();
|
||||
for (AutocompleteItem item : items) {
|
||||
if (item.getKind() == AutocompleteItem.Kind.CLASS ||
|
||||
item.getKind() == AutocompleteItem.Kind.ENUM) {
|
||||
existingSimpleNames.add(item.getName());
|
||||
}
|
||||
}
|
||||
|
||||
for (String fullName : matchingClasses) {
|
||||
// Skip if already imported
|
||||
if (importedFullNames.contains(fullName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TypeInfo type = resolver.resolveFullName(fullName);
|
||||
if (type != null && type.isResolved()) {
|
||||
// Skip if a class with this simple name is already in the list
|
||||
if (existingSimpleNames.contains(type.getSimpleName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create an item that requires import
|
||||
AutocompleteItem item = new AutocompleteItem.Builder()
|
||||
.name(type.getSimpleName())
|
||||
.insertText(type.getSimpleName())
|
||||
.kind(type.getKind() == TypeInfo.Kind.ENUM ?
|
||||
AutocompleteItem.Kind.ENUM : AutocompleteItem.Kind.CLASS)
|
||||
.typeLabel(type.getPackageName())
|
||||
.signature(type.getFullName())
|
||||
.sourceData(type)
|
||||
.requiresImport(true)
|
||||
.importPath(fullName)
|
||||
.build();
|
||||
items.add(item);
|
||||
existingSimpleNames.add(type.getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Java keywords.
|
||||
*/
|
||||
protected void addKeywords(List<AutocompleteItem> items) {
|
||||
for (String keyword : getKeywords()) {
|
||||
items.add(AutocompleteItem.keyword(keyword));
|
||||
}
|
||||
}
|
||||
|
||||
public String[] getKeywords() {
|
||||
return TypeChecker.getJavaKeywords();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the enclosing script type at a position.
|
||||
* Delegates to ScriptDocument's recursive descent implementation
|
||||
* which correctly returns the innermost enclosing type for nested classes.
|
||||
*/
|
||||
protected ScriptTypeInfo findEnclosingType(int position) {
|
||||
return document.findEnclosingScriptType(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the receiver expression represents static access (class type).
|
||||
* Returns true if the expression is a TYPE NAME (not a variable).
|
||||
*/
|
||||
protected boolean isStaticAccess(String receiverExpr, int position) {
|
||||
// Use unified static access checker
|
||||
return TypeResolver.isStaticAccessExpression(receiverExpr, position, document);
|
||||
}
|
||||
|
||||
protected UsageTracker getUsageTracker() {
|
||||
return UsageTracker.getJavaInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items by prefix, calculate match scores, apply usage boosts, and penalize static members in instance contexts.
|
||||
*/
|
||||
protected void filterAndScore(List<AutocompleteItem> items, String prefix,
|
||||
boolean isMemberAccess, boolean isStaticContext, String ownerFullName) {
|
||||
UsageTracker tracker = getUsageTracker();
|
||||
|
||||
if (prefix == null || prefix.isEmpty()) {
|
||||
// No filtering needed, all items get a base score + usage boost
|
||||
for (AutocompleteItem item : items) {
|
||||
item.calculateMatchScore("", false);
|
||||
applyUsageBoost(item, tracker, ownerFullName);
|
||||
applyStaticPenalty(item, isMemberAccess, isStaticContext);
|
||||
applyObjectMethodPenalty(item);
|
||||
applyKeywordPenalty(item, prefix);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// For non-member access (first word), require strict prefix matching
|
||||
// For member access (after dot), allow fuzzy/contains matching
|
||||
boolean requirePrefix = !isMemberAccess;
|
||||
|
||||
// Filter, score, apply usage boosts, and apply penalties
|
||||
Iterator<AutocompleteItem> iter = items.iterator();
|
||||
while (iter.hasNext()) {
|
||||
AutocompleteItem item = iter.next();
|
||||
int score = item.calculateMatchScore(prefix, requirePrefix);
|
||||
if (score < 0) {
|
||||
iter.remove();
|
||||
} else {
|
||||
applyUsageBoost(item, tracker, ownerFullName);
|
||||
applyStaticPenalty(item, isMemberAccess, isStaticContext);
|
||||
applyObjectMethodPenalty(item);
|
||||
applyKeywordPenalty(item, prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply usage-based score boost to an item.
|
||||
*/
|
||||
protected void applyUsageBoost(AutocompleteItem item, UsageTracker tracker, String ownerFullName) {
|
||||
int usageCount = tracker.getUsageCount(item, ownerFullName);
|
||||
int boost = UsageTracker.calculateUsageBoost(usageCount);
|
||||
item.addScoreBoost(boost);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply penalty to static members when accessed in a non-static (instance) context.
|
||||
* This matches IntelliJ's behavior where static members are deprioritized when
|
||||
* accessing through an instance (e.g., Minecraft.getMinecraft().getMinecraft()).
|
||||
* However, if the item is a very strong match (exact prefix), don't penalize as much.
|
||||
*/
|
||||
protected void applyStaticPenalty(AutocompleteItem item, boolean isMemberAccess, boolean isStaticContext) {
|
||||
// Only apply penalty in member access contexts (after dot)
|
||||
if (!isMemberAccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only penalize when we're in an instance context (not static)
|
||||
if (isStaticContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isStatic = item.isStatic();
|
||||
|
||||
// Apply penalty to static members in instance context
|
||||
if (isStatic) {
|
||||
int matchScore = item.getMatchScore();
|
||||
// Strong prefix matches (score >= 800) get a lighter penalty
|
||||
// Weaker matches get pushed down more aggressively
|
||||
if (matchScore >= 800) {
|
||||
// Light penalty for exact prefix matches - just deprioritize slightly
|
||||
item.addScoreBoost(-200);
|
||||
} else {
|
||||
// Heavy penalty for fuzzy/substring matches - push to bottom
|
||||
item.addScoreBoost(-matchScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply penalty to inherited Object methods to push them to bottom.
|
||||
* Strong matches get lighter penalty, weak matches get pushed all the way down.
|
||||
*/
|
||||
protected void applyObjectMethodPenalty(AutocompleteItem item) {
|
||||
if (!item.isInheritedObjectMethod()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int matchScore = item.getMatchScore();
|
||||
// Strong prefix matches (score >= 900) get a moderate penalty
|
||||
// Everything else gets pushed to the very bottom
|
||||
if (matchScore >= 900) {
|
||||
// Strong match - moderate penalty to keep it visible but below normal methods
|
||||
item.addScoreBoost(-500);
|
||||
} else {
|
||||
// Weak match - heavy penalty to push to bottom
|
||||
item.addScoreBoost(-10000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push keywords below non-keywords unless user typed a longer prefix.
|
||||
*/
|
||||
protected void applyKeywordPenalty(AutocompleteItem item, String prefix) {
|
||||
if (item.getKind() != AutocompleteItem.Kind.KEYWORD) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prefix == null || prefix.length() < 2) {
|
||||
item.addScoreBoost(-10000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for AutocompleteItem to handle cases without source data.
|
||||
*/
|
||||
private static class AutocompleteItemBuilder {
|
||||
// This would be needed if AutocompleteItem had a builder pattern
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import noppes.npcs.CustomNpcs;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Tracks user autocomplete selections to improve suggestion ranking.
|
||||
* Usage counts are persisted to JSON and used to boost frequently selected items.
|
||||
*
|
||||
* Key format: "ownerFullName|memberName|KIND"
|
||||
* - ownerFullName: Full class name for members, empty for standalone items
|
||||
* - memberName: The item name (method, field, type, etc.)
|
||||
* - KIND: The item kind (METHOD, FIELD, CLASS, ENUM, etc.)
|
||||
*
|
||||
* Examples:
|
||||
* - "net.minecraft.client.Minecraft|getMinecraft|METHOD"
|
||||
* - "net.minecraft.entity.player.EntityPlayer|inventory|FIELD"
|
||||
* - "|EntityPlayer|CLASS" (standalone type suggestion)
|
||||
* - "|if|KEYWORD"
|
||||
*/
|
||||
public class UsageTracker {
|
||||
|
||||
private static final String JAVA_FILE = "java_usages.json";
|
||||
private static final String JS_FILE = "js_usages.json";
|
||||
private static final long SAVE_INTERVAL_MS = 60_000; // Auto-save every 60 seconds
|
||||
private static final int USAGE_SCORE_MULTIPLIER = 50; // Score boost per usage
|
||||
private static final int MAX_USAGE_BOOST = 5000; // Cap the boost to prevent runaway scores
|
||||
|
||||
private static UsageTracker javaInstance;
|
||||
private static UsageTracker jsInstance;
|
||||
private static boolean initialized = false;
|
||||
|
||||
private final Map<String, Integer> usageCounts = new ConcurrentHashMap<>();
|
||||
private final File file;
|
||||
private final AtomicBoolean dirty = new AtomicBoolean(false);
|
||||
private final AtomicBoolean loaded = new AtomicBoolean(false);
|
||||
|
||||
private static ScheduledExecutorService scheduler;
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
// ==================== SINGLETON ACCESS ====================
|
||||
|
||||
public static synchronized UsageTracker getJavaInstance() {
|
||||
ensureInitialized();
|
||||
if (javaInstance == null) {
|
||||
javaInstance = new UsageTracker(JAVA_FILE);
|
||||
javaInstance.load();
|
||||
}
|
||||
return javaInstance;
|
||||
}
|
||||
|
||||
public static synchronized UsageTracker getJSInstance() {
|
||||
ensureInitialized();
|
||||
if (jsInstance == null) {
|
||||
jsInstance = new UsageTracker(JS_FILE);
|
||||
jsInstance.load();
|
||||
}
|
||||
return jsInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the tracker system is initialized with auto-save and shutdown hook.
|
||||
*/
|
||||
private static void ensureInitialized() {
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
initialize();
|
||||
|
||||
// Add shutdown hook to save on JVM exit
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
shutdown();
|
||||
}, "UsageTracker-Shutdown"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the auto-save scheduler. Call once on client startup.
|
||||
*/
|
||||
public static void initialize() {
|
||||
if (scheduler == null || scheduler.isShutdown()) {
|
||||
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "UsageTracker-AutoSave");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
scheduler.scheduleAtFixedRate(() -> {
|
||||
saveAllIfDirty();
|
||||
}, SAVE_INTERVAL_MS, SAVE_INTERVAL_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown and save all data. Call on client shutdown.
|
||||
*/
|
||||
public static void shutdown() {
|
||||
saveAllIfDirty();
|
||||
if (scheduler != null && !scheduler.isShutdown()) {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private static void saveAllIfDirty() {
|
||||
if (javaInstance != null) {
|
||||
javaInstance.saveIfDirty();
|
||||
}
|
||||
if (jsInstance != null) {
|
||||
jsInstance.saveIfDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== INSTANCE METHODS ====================
|
||||
|
||||
private UsageTracker(String filename) {
|
||||
File dir = getDir();
|
||||
this.file = new File(dir, filename);
|
||||
}
|
||||
|
||||
private File getDir() {
|
||||
File dir = new File(CustomNpcs.Dir, "tracked_usages");
|
||||
if (!dir.exists()) {
|
||||
dir.mkdir();
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that the user selected an autocomplete item.
|
||||
*
|
||||
* @param owner The owner context (full class name for members, null/empty for standalone)
|
||||
* @param name The item name
|
||||
* @param kind The item kind
|
||||
*/
|
||||
public void recordUsage(String owner, String name, AutocompleteItem.Kind kind) {
|
||||
String key = buildKey(owner, name, kind);
|
||||
usageCounts.merge(key, 1, Integer::sum);
|
||||
dirty.set(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record usage directly from an AutocompleteItem.
|
||||
*
|
||||
* @param item The selected autocomplete item
|
||||
* @param ownerFullName The full class name of the owner (for member access), or null
|
||||
*/
|
||||
public void recordUsage(AutocompleteItem item, String ownerFullName) {
|
||||
String name = item.getSearchName() != null ? item.getSearchName() : item.getName();
|
||||
|
||||
// For methods, include parameter count to distinguish overloads
|
||||
if (item.getKind() == AutocompleteItem.Kind.METHOD) {
|
||||
int paramCount = item.getParameterCount();
|
||||
name = name + "(" + paramCount + ")";
|
||||
}
|
||||
|
||||
recordUsage(ownerFullName, name, item.getKind());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the usage count for an item.
|
||||
*
|
||||
* @param owner The owner context
|
||||
* @param name The item name
|
||||
* @param kind The item kind
|
||||
* @return The number of times this item was selected
|
||||
*/
|
||||
public int getUsageCount(String owner, String name, AutocompleteItem.Kind kind) {
|
||||
String key = buildKey(owner, name, kind);
|
||||
return usageCounts.getOrDefault(key, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage count directly from an AutocompleteItem.
|
||||
*/
|
||||
public int getUsageCount(AutocompleteItem item, String ownerFullName) {
|
||||
String name = item.getSearchName() != null ? item.getSearchName() : item.getName();
|
||||
|
||||
// For methods, include parameter count to distinguish overloads
|
||||
if (item.getKind() == AutocompleteItem.Kind.METHOD) {
|
||||
int paramCount = item.getParameterCount();
|
||||
name = name + "(" + paramCount + ")";
|
||||
}
|
||||
|
||||
return getUsageCount(ownerFullName, name, item.getKind());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the score boost based on usage count.
|
||||
* Uses a logarithmic scale to prevent very frequent items from dominating completely.
|
||||
*
|
||||
* @param usageCount The number of times the item was selected
|
||||
* @return The score boost to add
|
||||
*/
|
||||
public static int calculateUsageBoost(int usageCount) {
|
||||
if (usageCount <= 0) return 0;
|
||||
|
||||
// Logarithmic scaling: log2(count + 1) * multiplier
|
||||
// This gives diminishing returns for very high counts
|
||||
int boost = (int) (Math.log(usageCount + 1) / Math.log(2) * USAGE_SCORE_MULTIPLIER);
|
||||
return Math.min(boost, MAX_USAGE_BOOST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a unique key for an item.
|
||||
*/
|
||||
public static String buildKey(String owner, String name, AutocompleteItem.Kind kind) {
|
||||
String ownerPart = owner != null ? owner : "";
|
||||
String kindPart = kind != null ? kind.name() : "UNKNOWN";
|
||||
return ownerPart + "|" + name + "|" + kindPart;
|
||||
}
|
||||
|
||||
// ==================== PERSISTENCE ====================
|
||||
|
||||
/**
|
||||
* Load usage data from file.
|
||||
*/
|
||||
public void load() {
|
||||
if (loaded.get()) return;
|
||||
|
||||
if (!file.exists()) {
|
||||
loaded.set(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
|
||||
Type type = new TypeToken<Map<String, Integer>>(){}.getType();
|
||||
Map<String, Integer> data = GSON.fromJson(reader, type);
|
||||
if (data != null) {
|
||||
usageCounts.putAll(data);
|
||||
}
|
||||
loaded.set(true);
|
||||
} catch (Exception e) {
|
||||
System.err.println("[UsageTracker] Failed to load " + file.getName() + ": " + e.getMessage());
|
||||
loaded.set(true); // Mark as loaded even on failure to prevent retry loops
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save usage data to file if there are pending changes.
|
||||
*/
|
||||
public void saveIfDirty() {
|
||||
if (!dirty.compareAndSet(true, false)) {
|
||||
return; // Not dirty, nothing to save
|
||||
}
|
||||
save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Force save usage data to file.
|
||||
*/
|
||||
public void save() {
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
|
||||
GSON.toJson(usageCounts, writer);
|
||||
} catch (Exception e) {
|
||||
System.err.println("[UsageTracker] Failed to save " + file.getName() + ": " + e.getMessage());
|
||||
dirty.set(true); // Mark dirty again so we retry later
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all usage data (for testing/reset purposes).
|
||||
*/
|
||||
public void clear() {
|
||||
usageCounts.clear();
|
||||
dirty.set(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of tracked items.
|
||||
*/
|
||||
public int size() {
|
||||
return usageCounts.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter;
|
||||
|
||||
/**
|
||||
* Utility class for parsing and analyzing code text.
|
||||
* Provides methods for finding matching braces/parentheses, removing comments/strings,
|
||||
* and locating keywords in code.
|
||||
*/
|
||||
public final class CodeParser {
|
||||
|
||||
private CodeParser() {} // Utility class
|
||||
|
||||
// ==================== Matching Delimiters ====================
|
||||
|
||||
/**
|
||||
* Find the closing brace that matches the opening brace at the given position.
|
||||
* @param text The text to search
|
||||
* @param openBrace Position of the opening '{'
|
||||
* @return Position of matching '}' or -1 if not found
|
||||
*/
|
||||
public static int findMatchingBrace(String text, int openBrace) {
|
||||
if (openBrace < 0 || openBrace >= text.length() || text.charAt(openBrace) != '{') {
|
||||
return -1;
|
||||
}
|
||||
int depth = 1;
|
||||
for (int i = openBrace + 1; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
if (c == '{') depth++;
|
||||
else if (c == '}') {
|
||||
depth--;
|
||||
if (depth == 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closing parenthesis that matches the opening parenthesis at the given position.
|
||||
* @param text The text to search
|
||||
* @param openParen Position of the opening '('
|
||||
* @return Position of matching ')' or -1 if not found
|
||||
*/
|
||||
public static int findMatchingParen(String text, int openParen) {
|
||||
if (openParen < 0 || openParen >= text.length() || text.charAt(openParen) != '(') {
|
||||
return -1;
|
||||
}
|
||||
int depth = 1;
|
||||
for (int i = openParen + 1; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
if (c == '(') depth++;
|
||||
else if (c == ')') {
|
||||
depth--;
|
||||
if (depth == 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ==================== Comment/String Removal ====================
|
||||
|
||||
/**
|
||||
* Remove string literals and comments from code for structural analysis.
|
||||
* This is used when analyzing control flow to avoid false positives from
|
||||
* keywords inside strings or comments.
|
||||
* @param code The source code
|
||||
* @return Code with strings and comments removed
|
||||
*/
|
||||
public static String removeStringsAndComments(String code) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean inString = false;
|
||||
boolean inChar = false;
|
||||
boolean inLineComment = false;
|
||||
boolean inBlockComment = false;
|
||||
|
||||
for (int i = 0; i < code.length(); i++) {
|
||||
char c = code.charAt(i);
|
||||
char next = (i + 1 < code.length()) ? code.charAt(i + 1) : 0;
|
||||
|
||||
if (inLineComment) {
|
||||
if (c == '\n') {
|
||||
inLineComment = false;
|
||||
result.append(c);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inBlockComment) {
|
||||
if (c == '*' && next == '/') {
|
||||
inBlockComment = false;
|
||||
i++; // skip '/'
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inString || inChar) {
|
||||
if (c == '\\' && i + 1 < code.length()) {
|
||||
i++; // skip escaped character
|
||||
continue;
|
||||
}
|
||||
if ((inString && c == '"') || (inChar && c == '\'')) {
|
||||
inString = false;
|
||||
inChar = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for start of comments or strings
|
||||
if (c == '/' && next == '/') {
|
||||
inLineComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && next == '*') {
|
||||
inBlockComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') {
|
||||
inString = true;
|
||||
continue;
|
||||
}
|
||||
if (c == '\'') {
|
||||
inChar = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
result.append(c);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove only comments from code, keeping strings intact and preserving positions.
|
||||
* Comments are replaced with spaces to maintain character positions.
|
||||
* @param code The source code
|
||||
* @return Code with comments replaced by spaces
|
||||
*/
|
||||
public static String removeComments(String code) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean inString = false;
|
||||
boolean inChar = false;
|
||||
boolean inLineComment = false;
|
||||
boolean inBlockComment = false;
|
||||
|
||||
for (int i = 0; i < code.length(); i++) {
|
||||
char c = code.charAt(i);
|
||||
char next = (i + 1 < code.length()) ? code.charAt(i + 1) : 0;
|
||||
|
||||
if (inLineComment) {
|
||||
if (c == '\n') {
|
||||
inLineComment = false;
|
||||
result.append(c);
|
||||
} else {
|
||||
result.append(' '); // Preserve position with spaces
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inBlockComment) {
|
||||
if (c == '*' && next == '/') {
|
||||
inBlockComment = false;
|
||||
result.append(" "); // Preserve position
|
||||
i++;
|
||||
} else {
|
||||
result.append(c == '\n' ? '\n' : ' '); // Preserve newlines
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inString) {
|
||||
result.append(c);
|
||||
if (c == '\\' && i + 1 < code.length()) {
|
||||
result.append(code.charAt(++i));
|
||||
continue;
|
||||
}
|
||||
if (c == '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inChar) {
|
||||
result.append(c);
|
||||
if (c == '\\' && i + 1 < code.length()) {
|
||||
result.append(code.charAt(++i));
|
||||
continue;
|
||||
}
|
||||
if (c == '\'') inChar = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for start of comments
|
||||
if (c == '/' && next == '/') {
|
||||
inLineComment = true;
|
||||
result.append(" "); // Preserve position
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && next == '*') {
|
||||
inBlockComment = true;
|
||||
result.append(" "); // Preserve position
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') { inString = true; }
|
||||
if (c == '\'') { inChar = true; }
|
||||
|
||||
result.append(c);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
// ==================== Keyword Detection ====================
|
||||
|
||||
/**
|
||||
* Check if a keyword exists at the given position in text.
|
||||
* Verifies the keyword is not part of a larger identifier.
|
||||
* @param text The text to check
|
||||
* @param pos Position to check
|
||||
* @param keyword The keyword to look for
|
||||
* @return true if the keyword is at this position as a standalone word
|
||||
*/
|
||||
public static boolean isKeywordAt(String text, int pos, String keyword) {
|
||||
if (pos + keyword.length() > text.length()) return false;
|
||||
if (!text.substring(pos).startsWith(keyword)) return false;
|
||||
|
||||
boolean validBefore = pos == 0 || !Character.isLetterOrDigit(text.charAt(pos - 1));
|
||||
boolean validAfter = pos + keyword.length() >= text.length()
|
||||
|| !Character.isLetterOrDigit(text.charAt(pos + keyword.length()));
|
||||
|
||||
return validBefore && validAfter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next "return" keyword that is not inside a string literal.
|
||||
* @param text The text to search
|
||||
* @param start Position to start searching from
|
||||
* @return Position of "return" or -1 if not found
|
||||
*/
|
||||
public static int findReturnKeyword(String text, int start) {
|
||||
boolean inString = false;
|
||||
boolean inChar = false;
|
||||
|
||||
for (int i = start; i < text.length() - 5; i++) {
|
||||
char c = text.charAt(i);
|
||||
|
||||
if (inString) {
|
||||
if (c == '\\' && i + 1 < text.length()) { i++; continue; }
|
||||
if (c == '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (inChar) {
|
||||
if (c == '\\' && i + 1 < text.length()) { i++; continue; }
|
||||
if (c == '\'') inChar = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '"') { inString = true; continue; }
|
||||
if (c == '\'') { inChar = true; continue; }
|
||||
|
||||
// Check for "return" keyword
|
||||
if (isKeywordAt(text, i, "return")) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the semicolon that ends a return statement, handling nested structures.
|
||||
* @param text The text to search
|
||||
* @param start Position to start searching (after "return" keyword)
|
||||
* @return Position of ';' or -1 if not found
|
||||
*/
|
||||
public static int findReturnSemicolon(String text, int start) {
|
||||
int parenDepth = 0;
|
||||
int braceDepth = 0;
|
||||
boolean inString = false;
|
||||
boolean inChar = false;
|
||||
|
||||
for (int i = start; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
|
||||
if (inString) {
|
||||
if (c == '\\' && i + 1 < text.length()) { i++; continue; }
|
||||
if (c == '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (inChar) {
|
||||
if (c == '\\' && i + 1 < text.length()) { i++; continue; }
|
||||
if (c == '\'') inChar = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '"') { inString = true; continue; }
|
||||
if (c == '\'') { inChar = true; continue; }
|
||||
if (c == '(') { parenDepth++; continue; }
|
||||
if (c == ')') { parenDepth--; continue; }
|
||||
if (c == '{') { braceDepth++; continue; }
|
||||
if (c == '}') { braceDepth--; continue; }
|
||||
|
||||
if (c == ';' && parenDepth == 0 && braceDepth == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ==================== Whitespace Utilities ====================
|
||||
|
||||
/**
|
||||
* Skip whitespace characters starting from the given position.
|
||||
* @param text The text
|
||||
* @param pos Starting position
|
||||
* @return Position of first non-whitespace character
|
||||
*/
|
||||
public static int skipWhitespace(String text, int pos) {
|
||||
while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) {
|
||||
pos++;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter;
|
||||
|
||||
/**
|
||||
* Analyzes control flow in method bodies to determine if all paths return a value.
|
||||
* Used for detecting "missing return statement" errors.
|
||||
*/
|
||||
public final class ControlFlowAnalyzer {
|
||||
|
||||
private ControlFlowAnalyzer() {} // Utility class
|
||||
|
||||
/**
|
||||
* Check if the method body has a guaranteed return on all code paths.
|
||||
*/
|
||||
public static boolean hasGuaranteedReturn(String body) {
|
||||
String cleanBody = CodeParser.removeStringsAndComments(body);
|
||||
|
||||
return hasTopLevelReturn(cleanBody)
|
||||
|| hasCompleteIfElseReturn(cleanBody)
|
||||
|| hasTryFinallyReturn(cleanBody)
|
||||
|| hasCompleteSwitchReturn(cleanBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for a bare return statement at the method's top level (brace depth 0).
|
||||
*/
|
||||
private static boolean hasTopLevelReturn(String body) {
|
||||
int braceDepth = 0;
|
||||
int parenDepth = 0;
|
||||
|
||||
for (int i = 0; i < body.length(); i++) {
|
||||
char c = body.charAt(i);
|
||||
|
||||
if (c == '{') braceDepth++;
|
||||
else if (c == '}') braceDepth--;
|
||||
else if (c == '(') parenDepth++;
|
||||
else if (c == ')') parenDepth--;
|
||||
|
||||
if (braceDepth == 0 && parenDepth == 0 && isKeywordAt(body, i, "return")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for complete if-else chains where all branches return.
|
||||
*/
|
||||
private static boolean hasCompleteIfElseReturn(String body) {
|
||||
int braceDepth = 0;
|
||||
|
||||
for (int i = 0; i < body.length(); i++) {
|
||||
char c = body.charAt(i);
|
||||
if (c == '{') { braceDepth++; continue; }
|
||||
if (c == '}') { braceDepth--; continue; }
|
||||
|
||||
if (braceDepth == 0 && isKeywordAt(body, i, "if")) {
|
||||
if (checkIfElseChainReturns(body, i)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an if-else chain starting at the given position guarantees a return.
|
||||
* Handles: if(true) { return; } - always returns (else unreachable)
|
||||
* if(false) { } else { return; } - else always executes
|
||||
*/
|
||||
private static boolean checkIfElseChainReturns(String body, int ifStart) {
|
||||
int pos = ifStart;
|
||||
|
||||
while (pos < body.length()) {
|
||||
if (!body.substring(pos).startsWith("if")) return false;
|
||||
pos += 2;
|
||||
|
||||
pos = skipWhitespace(body, pos);
|
||||
if (pos >= body.length() || body.charAt(pos) != '(') return false;
|
||||
|
||||
int condStart = pos;
|
||||
int condEnd = CodeParser.findMatchingParen(body, pos);
|
||||
if (condEnd < 0) return false;
|
||||
|
||||
// Check for literal true/false conditions
|
||||
String condition = body.substring(condStart + 1, condEnd).trim();
|
||||
boolean isAlwaysTrue = condition.equals("true");
|
||||
boolean isAlwaysFalse = condition.equals("false");
|
||||
|
||||
pos = skipWhitespace(body, condEnd + 1);
|
||||
if (pos >= body.length() || body.charAt(pos) != '{') return false;
|
||||
|
||||
int blockEnd = CodeParser.findMatchingBrace(body, pos);
|
||||
if (blockEnd < 0) return false;
|
||||
|
||||
String ifBlock = body.substring(pos + 1, blockEnd);
|
||||
boolean ifBlockReturns = hasGuaranteedReturn(ifBlock);
|
||||
|
||||
// if(true) with return means we always return (else is unreachable)
|
||||
if (isAlwaysTrue && ifBlockReturns) {
|
||||
return true;
|
||||
}
|
||||
|
||||
pos = skipWhitespace(body, blockEnd + 1);
|
||||
|
||||
// Must have else clause for guaranteed return
|
||||
if (!body.substring(pos).startsWith("else")) {
|
||||
return false;
|
||||
}
|
||||
pos += 4;
|
||||
pos = skipWhitespace(body, pos);
|
||||
|
||||
// else if - continue the chain
|
||||
if (isKeywordAt(body, pos, "if")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Plain else block - final branch
|
||||
if (pos >= body.length() || body.charAt(pos) != '{') return false;
|
||||
int elseBlockEnd = CodeParser.findMatchingBrace(body, pos);
|
||||
if (elseBlockEnd < 0) return false;
|
||||
|
||||
String elseBlock = body.substring(pos + 1, elseBlockEnd);
|
||||
boolean elseBlockReturns = hasGuaranteedReturn(elseBlock);
|
||||
|
||||
// For if-else to guarantee return: both branches must return
|
||||
// Special case: if(false) means if-block never executes, so only else matters
|
||||
if (isAlwaysFalse) {
|
||||
return elseBlockReturns;
|
||||
}
|
||||
|
||||
// Normal case: both if and else must return
|
||||
return ifBlockReturns && elseBlockReturns;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for try-finally where finally block returns.
|
||||
*/
|
||||
private static boolean hasTryFinallyReturn(String body) {
|
||||
for (int i = 0; i < body.length(); i++) {
|
||||
if (!isKeywordAt(body, i, "try")) continue;
|
||||
|
||||
int tryBlockStart = body.indexOf('{', i);
|
||||
if (tryBlockStart < 0) continue;
|
||||
|
||||
int tryBlockEnd = CodeParser.findMatchingBrace(body, tryBlockStart);
|
||||
if (tryBlockEnd < 0) continue;
|
||||
|
||||
// Skip catch blocks
|
||||
int searchPos = skipCatchBlocks(body, tryBlockEnd + 1);
|
||||
searchPos = skipWhitespace(body, searchPos);
|
||||
|
||||
// Check for finally
|
||||
if (body.substring(searchPos).startsWith("finally")) {
|
||||
int finallyBlockStart = body.indexOf('{', searchPos);
|
||||
if (finallyBlockStart >= 0) {
|
||||
int finallyBlockEnd = CodeParser.findMatchingBrace(body, finallyBlockStart);
|
||||
if (finallyBlockEnd >= 0) {
|
||||
String finallyBlock = body.substring(finallyBlockStart + 1, finallyBlockEnd);
|
||||
if (hasGuaranteedReturn(finallyBlock)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int skipCatchBlocks(String body, int start) {
|
||||
int pos = start;
|
||||
while (true) {
|
||||
pos = skipWhitespace(body, pos);
|
||||
if (pos >= body.length() || !body.substring(pos).startsWith("catch")) break;
|
||||
|
||||
int catchBlockStart = body.indexOf('{', pos);
|
||||
if (catchBlockStart < 0) break;
|
||||
|
||||
int catchBlockEnd = CodeParser.findMatchingBrace(body, catchBlockStart);
|
||||
if (catchBlockEnd < 0) break;
|
||||
|
||||
pos = catchBlockEnd + 1;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for switch with default where all cases return.
|
||||
* Returns false for now - full implementation is complex.
|
||||
*/
|
||||
private static boolean hasCompleteSwitchReturn(String body) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ==================== Utility Methods ====================
|
||||
|
||||
private static boolean isKeywordAt(String text, int pos, String keyword) {
|
||||
if (pos + keyword.length() > text.length()) return false;
|
||||
if (!text.substring(pos).startsWith(keyword)) return false;
|
||||
|
||||
boolean validBefore = pos == 0 || !Character.isLetterOrDigit(text.charAt(pos - 1));
|
||||
boolean validAfter = pos + keyword.length() >= text.length()
|
||||
|| !Character.isLetterOrDigit(text.charAt(pos + keyword.length()));
|
||||
|
||||
return validBefore && validAfter;
|
||||
}
|
||||
|
||||
private static int skipWhitespace(String text, int pos) {
|
||||
while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) {
|
||||
pos++;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter;
|
||||
|
||||
import noppes.npcs.client.ClientProxy;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.AssignmentInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
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.type.ScriptTypeInfo;
|
||||
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.getAllMethods()) {
|
||||
//!call.isConstructor for enum declarations
|
||||
if (!call.isConstructor() && 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);
|
||||
} else if (call.hasArgTypeError()) {
|
||||
// Handle return type mismatch (underline the method name) - currently commented out
|
||||
// if (call.hasReturnTypeMismatch()) { ... }
|
||||
|
||||
// Handle arg type errors (underline specific arguments)
|
||||
|
||||
for (MethodCallInfo.ArgumentTypeError error : call.getArgumentTypeErrors()) {
|
||||
MethodCallInfo.Argument arg = error.getArg();
|
||||
drawUnderlineForSpan(arg.getStartOffset(), arg.getEndOffset(),
|
||||
lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR);
|
||||
}
|
||||
} else if(call.hasError()){
|
||||
// Handle other call errors (underline the whole call)
|
||||
drawUnderlineForSpan(call.getMethodNameStart(), call.getCloseParenOffset() + 1,
|
||||
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.getAllMethods()) {
|
||||
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)
|
||||
else if (method.hasMissingReturnError()) {
|
||||
int methodNameStart = method.getNameOffset();
|
||||
int methodNameEnd = methodNameStart + method.getName().length();
|
||||
drawUnderlineForSpan(methodNameStart, methodNameEnd, lineStartX, baselineY,
|
||||
lineText, lineStart, lineEnd, ERROR_COLOR);
|
||||
}
|
||||
// Handle parameter errors
|
||||
else 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle all other errors
|
||||
else if (method.hasError()) {
|
||||
drawUnderlineForSpan(method.getFullDeclarationOffset(), method.getDeclarationEnd(), lineStartX,
|
||||
baselineY, lineText, lineStart, lineEnd, ERROR_COLOR);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
for(ScriptTypeInfo types : doc.getScriptTypes()){
|
||||
if(!types.hasError())
|
||||
continue;
|
||||
|
||||
int typeStart = types.getDeclarationOffset();
|
||||
int typeEnd = types.getBodyStart();
|
||||
drawUnderlineForSpan(typeStart, typeEnd, 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.token.TokenType;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.token.TokenErrorMessage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Helper class for marking chained field accesses in script documents.
|
||||
* Handles patterns like: identifier.field, this.field, super.field, method().field
|
||||
*/
|
||||
public class FieldChainMarker {
|
||||
private final ScriptDocument document;
|
||||
private final String text;
|
||||
|
||||
public FieldChainMarker(ScriptDocument document, String text) {
|
||||
this.document = document;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all chained field accesses in the document.
|
||||
* Uses two passes: one for standard chains, one for method/array result chains.
|
||||
*/
|
||||
public void markChainedFieldAccesses(List<ScriptLine.Mark> marks) {
|
||||
// ==================== PASS 1: Standard identifier chains ====================
|
||||
// Pattern: identifier.identifier, this.identifier, super.identifier
|
||||
Pattern chainPattern = Pattern.compile(
|
||||
"\\b(this|super|[a-zA-Z_][a-zA-Z0-9_]*)\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)");
|
||||
Matcher m = chainPattern.matcher(text);
|
||||
|
||||
while (m.find()) {
|
||||
int chainStart = m.start(1);
|
||||
|
||||
// Skip excluded ranges and imports
|
||||
if (document.isExcluded(chainStart) || document.isInImportOrPackage(chainStart)) continue;
|
||||
|
||||
// Skip if second segment is a method call - markMethodCalls handles it
|
||||
if (document.isFollowedByParen(m.end(2))) continue;
|
||||
|
||||
// Build the full chain
|
||||
ChainData chain = buildChain(m);
|
||||
if (chain == null) continue;
|
||||
|
||||
// Resolve the starting type
|
||||
ChainContext ctx = resolveChainStart(chain, chainStart);
|
||||
|
||||
// Mark each segment
|
||||
for (int i = ctx.startIndex; i < chain.segments.size(); i++) {
|
||||
ctx.currentIndex = i;
|
||||
int[] pos = chain.positions.get(i);
|
||||
|
||||
if (document.isExcluded(pos[0])) continue;
|
||||
|
||||
// Determine what type of marking this segment needs
|
||||
MarkResult result = resolveSegmentMark(ctx, chainStart);
|
||||
|
||||
// Apply the mark
|
||||
if (result.mark != null) {
|
||||
marks.add(result.mark);
|
||||
}
|
||||
|
||||
// Update context for next segment
|
||||
ctx.currentType = result.nextType;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PASS 2: Method/array result chains ====================
|
||||
// Handle: getMinecraft().thePlayer, array[0].length
|
||||
Pattern dotIdent = Pattern.compile("\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)");
|
||||
Matcher md = dotIdent.matcher(text);
|
||||
|
||||
while (md.find()) {
|
||||
int identStart = md.start(1);
|
||||
int identEnd = md.end(1);
|
||||
int dotPos = md.start();
|
||||
|
||||
// Skip excluded ranges and imports
|
||||
if (document.isExcluded(identStart) || document.isInImportOrPackage(identStart)) continue;
|
||||
|
||||
// Only handle when preceded by ')' or ']' (method call or array access result)
|
||||
Character precedingChar = getNonWhitespaceBefore(dotPos);
|
||||
if (precedingChar == null || (precedingChar != ')' && precedingChar != ']')) continue;
|
||||
|
||||
// Get receiver type from the expression before the dot
|
||||
int[] bounds = document.findReceiverBoundsBefore(dotPos);
|
||||
if (bounds == null) continue;
|
||||
|
||||
if (document.isExcluded(bounds[0]) || document.isInImportOrPackage(bounds[0])) continue;
|
||||
|
||||
String receiverExpr = text.substring(bounds[0], bounds[1]).trim();
|
||||
if (receiverExpr.isEmpty()) continue;
|
||||
|
||||
TypeInfo receiverType = document.resolveExpressionType(receiverExpr, bounds[0]);
|
||||
|
||||
// Mark the first field and continue the chain
|
||||
markReceiverChainSegments(marks, md.group(1), identStart, identEnd, receiverType);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== CHAIN BUILDING HELPERS ====================
|
||||
|
||||
/** Data class for a parsed chain */
|
||||
private static class ChainData {
|
||||
final List<String> segments = new ArrayList<>();
|
||||
final List<int[]> positions = new ArrayList<>();
|
||||
}
|
||||
|
||||
/** Context for chain resolution */
|
||||
private static class ChainContext {
|
||||
ChainData chain;
|
||||
int currentIndex;
|
||||
TypeInfo currentType;
|
||||
ScriptTypeInfo enclosingType; // The enclosing script type for 'this' resolution
|
||||
int startIndex;
|
||||
boolean firstIsThis;
|
||||
boolean firstIsSuper;
|
||||
boolean firstIsPrecededByDot;
|
||||
}
|
||||
|
||||
/** Result of resolving a segment's mark */
|
||||
private static class MarkResult {
|
||||
final ScriptLine.Mark mark;
|
||||
final TypeInfo nextType;
|
||||
|
||||
MarkResult(ScriptLine.Mark mark, TypeInfo nextType) {
|
||||
this.mark = mark;
|
||||
this.nextType = nextType;
|
||||
}
|
||||
}
|
||||
|
||||
/** Build a complete chain from a regex match, continuing to read more segments */
|
||||
private ChainData buildChain(Matcher m) {
|
||||
ChainData chain = new ChainData();
|
||||
|
||||
// Add first two segments from the match
|
||||
chain.segments.add(m.group(1));
|
||||
chain.positions.add(new int[]{m.start(1), m.end(1)});
|
||||
chain.segments.add(m.group(2));
|
||||
chain.positions.add(new int[]{m.start(2), m.end(2)});
|
||||
|
||||
// Continue reading subsequent .identifier segments
|
||||
int pos = m.end(2);
|
||||
while (pos < text.length()) {
|
||||
pos = document.skipWhitespace(pos);
|
||||
if (pos >= text.length() || text.charAt(pos) != '.') break;
|
||||
pos++; // Skip dot
|
||||
|
||||
pos = document.skipWhitespace(pos);
|
||||
if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos))) break;
|
||||
|
||||
// Read identifier
|
||||
int identStart = pos;
|
||||
while (pos < text.length() && Character.isJavaIdentifierPart(text.charAt(pos))) pos++;
|
||||
int identEnd = pos;
|
||||
|
||||
// Stop if this is a method call
|
||||
if (document.isFollowedByParen(identEnd)) break;
|
||||
|
||||
chain.segments.add(text.substring(identStart, identEnd));
|
||||
chain.positions.add(new int[]{identStart, identEnd});
|
||||
}
|
||||
|
||||
return chain;
|
||||
}
|
||||
|
||||
/** Resolve the starting context for a chain */
|
||||
private ChainContext resolveChainStart(ChainData chain, int chainStart) {
|
||||
ChainContext ctx = new ChainContext();
|
||||
ctx.chain = chain;
|
||||
ctx.currentIndex = 0;
|
||||
String first = chain.segments.get(0);
|
||||
|
||||
ctx.firstIsThis = first.equals("this");
|
||||
ctx.firstIsSuper = first.equals("super");
|
||||
ctx.firstIsPrecededByDot = document.isPrecededByDot(chainStart);
|
||||
ctx.startIndex = ctx.firstIsPrecededByDot ? 0 : 1;
|
||||
ctx.currentType = null;
|
||||
ctx.enclosingType = document.findEnclosingScriptType(chainStart);
|
||||
|
||||
if (ctx.firstIsThis) {
|
||||
// 'this' - resolve to enclosing class/global fields
|
||||
ctx.currentType = document.resolveThisType(chainStart);
|
||||
if (ctx.currentType == null) {
|
||||
ctx.currentType = ctx.enclosingType;
|
||||
}
|
||||
} else if (ctx.firstIsSuper) {
|
||||
// 'super' - resolve to parent class
|
||||
if (ctx.enclosingType != null && ctx.enclosingType.hasSuperClass()) {
|
||||
ctx.currentType = ctx.enclosingType.getSuperClass();
|
||||
}
|
||||
} else if (ctx.firstIsPrecededByDot) {
|
||||
// Field access on a receiver (e.g., getMinecraft().thePlayer)
|
||||
TypeInfo receiverType = document.resolveReceiverChain(chainStart);
|
||||
if (receiverType != null && receiverType.hasField(first)) {
|
||||
FieldInfo varInfo = receiverType.getFieldInfo(first);
|
||||
ctx.currentType = (varInfo != null) ? varInfo.getTypeInfo() : null;
|
||||
}
|
||||
} else {
|
||||
// Try as type first (static access), then as variable
|
||||
TypeInfo typeCheck = document.resolveType(first);
|
||||
if (typeCheck != null && typeCheck.isResolved()) {
|
||||
ctx.currentType = typeCheck;
|
||||
} else {
|
||||
FieldInfo varInfo = document.resolveVariable(first, chainStart);
|
||||
ctx.currentType = (varInfo != null) ? varInfo.getTypeInfo() : null;
|
||||
}
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/** Resolve what mark a segment should get */
|
||||
private MarkResult resolveSegmentMark(ChainContext ctx, int chainStart) {
|
||||
int index = ctx.currentIndex;
|
||||
String segment = ctx.chain.segments.get(index);
|
||||
int[] pos = ctx.chain.positions.get(index);
|
||||
|
||||
// Case 1: First segment preceded by dot (field on receiver)
|
||||
if (index == 0 && ctx.firstIsPrecededByDot) {
|
||||
return resolveReceiverFieldMark(ctx, chainStart);
|
||||
}
|
||||
|
||||
// Case 2: this.field
|
||||
if (index == 1 && ctx.firstIsThis) {
|
||||
return resolveThisFieldMark(ctx);
|
||||
}
|
||||
|
||||
// Case 3: super.field
|
||||
if (index == 1 && ctx.firstIsSuper) {
|
||||
return resolveSuperFieldMark(ctx);
|
||||
}
|
||||
|
||||
// Case 4: Resolved type with field
|
||||
if (ctx.currentType != null && ctx.currentType.isResolved()) {
|
||||
return resolveTypedFieldMark(ctx);
|
||||
}
|
||||
|
||||
// Case 5: Unresolved - mark as undefined
|
||||
return new MarkResult(
|
||||
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== SEGMENT RESOLUTION HELPERS ====================
|
||||
|
||||
/** Resolve mark for field access on a receiver (preceded by dot) */
|
||||
private MarkResult resolveReceiverFieldMark(ChainContext ctx, int chainStart) {
|
||||
int index = ctx.currentIndex;
|
||||
String segment = ctx.chain.segments.get(index);
|
||||
int[] pos = ctx.chain.positions.get(index);
|
||||
boolean isLast = (index == ctx.chain.segments.size() - 1);
|
||||
boolean isStatic = isStaticContext(ctx);
|
||||
|
||||
TypeInfo receiverType = document.resolveReceiverChain(chainStart);
|
||||
|
||||
if (receiverType != null && receiverType.hasField(segment)) {
|
||||
FieldInfo fieldInfo = receiverType.getFieldInfo(segment);
|
||||
FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1],
|
||||
receiverType, fieldInfo, isLast, isStatic);
|
||||
|
||||
return new MarkResult(new ScriptLine.Mark(pos[0], pos[1], getFieldTokenType(fieldInfo), accessInfo),
|
||||
(fieldInfo != null) ? fieldInfo.getTypeInfo() : null
|
||||
);
|
||||
}
|
||||
|
||||
return new MarkResult(
|
||||
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolve mark for this.field access */
|
||||
private MarkResult resolveThisFieldMark(ChainContext ctx) {
|
||||
int index = ctx.currentIndex;
|
||||
String segment = ctx.chain.segments.get(index);
|
||||
int[] pos = ctx.chain.positions.get(index);
|
||||
boolean isLast = (index == ctx.chain.segments.size() - 1);
|
||||
|
||||
boolean found = false;
|
||||
FieldInfo fieldInfo = null;
|
||||
|
||||
TypeInfo thisType = ctx.currentType != null ? ctx.currentType : ctx.enclosingType;
|
||||
|
||||
if (thisType != null && thisType.hasField(segment)) {
|
||||
found = true;
|
||||
fieldInfo = thisType.getFieldInfo(segment);
|
||||
} else if (document.getGlobalFields().containsKey(segment)) {
|
||||
found = true;
|
||||
fieldInfo = document.getGlobalFields().get(segment);
|
||||
}
|
||||
|
||||
if (found) {
|
||||
FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1], thisType,
|
||||
fieldInfo, isLast, false);
|
||||
|
||||
return new MarkResult(new ScriptLine.Mark(pos[0], pos[1], getFieldTokenType(fieldInfo), accessInfo),
|
||||
(fieldInfo != null) ? fieldInfo.getTypeInfo() : null
|
||||
);
|
||||
}
|
||||
|
||||
return new MarkResult(new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR), null
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolve mark for super.field access */
|
||||
private MarkResult resolveSuperFieldMark(ChainContext ctx) {
|
||||
int index = ctx.currentIndex;
|
||||
String segment = ctx.chain.segments.get(index);
|
||||
int[] pos = ctx.chain.positions.get(index);
|
||||
boolean isLast = (index == ctx.chain.segments.size() - 1);
|
||||
TypeInfo superType = ctx.currentType;
|
||||
|
||||
if (superType == null) {
|
||||
return new MarkResult(
|
||||
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR,
|
||||
TokenErrorMessage.from("Cannot resolve field '" + segment + "'").clearOtherErrors()),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
// Search through inheritance hierarchy
|
||||
boolean found = false;
|
||||
FieldInfo fieldInfo = null;
|
||||
|
||||
if (superType instanceof ScriptTypeInfo) {
|
||||
ScriptTypeInfo scriptSuper = (ScriptTypeInfo) superType;
|
||||
found = scriptSuper.hasFieldInHierarchy(segment);
|
||||
if (found) fieldInfo = scriptSuper.getFieldInfoInHierarchy(segment);
|
||||
} else {
|
||||
found = superType.hasField(segment);
|
||||
if (found) fieldInfo = superType.getFieldInfo(segment);
|
||||
}
|
||||
|
||||
if (found) {
|
||||
FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1],
|
||||
superType, fieldInfo, isLast, false);
|
||||
return new MarkResult(new ScriptLine.Mark(pos[0], pos[1], getFieldTokenType(fieldInfo), accessInfo),
|
||||
(fieldInfo != null) ? fieldInfo.getTypeInfo() : null
|
||||
);
|
||||
}
|
||||
|
||||
String errorMsg = "Field '" + segment + "' not found in parent class hierarchy starting from '"
|
||||
+ superType.getSimpleName() + "'";
|
||||
return new MarkResult(
|
||||
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR,
|
||||
TokenErrorMessage.from(errorMsg).clearOtherErrors()),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolve mark for typed field access */
|
||||
private MarkResult resolveTypedFieldMark(ChainContext ctx) {
|
||||
int index = ctx.currentIndex;
|
||||
String segment = ctx.chain.segments.get(index);
|
||||
int[] pos = ctx.chain.positions.get(index);
|
||||
boolean isLast = (index == ctx.chain.segments.size() - 1);
|
||||
boolean isStatic = isStaticContext(ctx);
|
||||
TypeInfo currentType = ctx.currentType;
|
||||
|
||||
if (!currentType.hasField(segment)) {
|
||||
// Field not found — check if the segment names an inner class.
|
||||
// This handles chains like Outer.Inner where Inner is a nested type, not a field.
|
||||
if (currentType instanceof ScriptTypeInfo) {
|
||||
ScriptTypeInfo innerClass = ((ScriptTypeInfo) currentType).getInnerClass(segment);
|
||||
if (innerClass != null) {
|
||||
return new MarkResult(
|
||||
new ScriptLine.Mark(pos[0], pos[1], innerClass.getTokenType(), innerClass),
|
||||
innerClass
|
||||
);
|
||||
}
|
||||
}
|
||||
// Also check Java inner classes via reflection (e.g., Map.Entry)
|
||||
if (currentType.getJavaClass() != null) {
|
||||
try {
|
||||
for (Class<?> nested : currentType.getJavaClass().getDeclaredClasses()) {
|
||||
if (java.lang.reflect.Modifier.isPublic(nested.getModifiers())
|
||||
&& nested.getSimpleName().equals(segment)) {
|
||||
TypeInfo nestedType = TypeInfo.fromClass(nested);
|
||||
return new MarkResult(
|
||||
new ScriptLine.Mark(pos[0], pos[1], nestedType.getTokenType(), nestedType),
|
||||
nestedType
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (SecurityException ignored) { }
|
||||
}
|
||||
|
||||
return new MarkResult(
|
||||
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
FieldInfo fieldInfo = currentType.getFieldInfo(segment);
|
||||
|
||||
// Check for static access error
|
||||
if (isStatic && fieldInfo != null && !fieldInfo.isStatic()) {
|
||||
TokenErrorMessage errorMsg = TokenErrorMessage
|
||||
.from("Cannot access non-static field '" + segment + "' from static context '"
|
||||
+ currentType.getSimpleName() + "'")
|
||||
.clearOtherErrors();
|
||||
return new MarkResult(
|
||||
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR, errorMsg),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
// Valid field access
|
||||
FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1],
|
||||
currentType, fieldInfo, isLast, isStatic);
|
||||
|
||||
return new MarkResult(new ScriptLine.Mark(pos[0], pos[1], getFieldTokenType(fieldInfo), accessInfo),
|
||||
(fieldInfo != null) ? fieldInfo.getTypeInfo() : null
|
||||
);
|
||||
}
|
||||
|
||||
/** Mark segments following a method call or array access result */
|
||||
private void markReceiverChainSegments(List<ScriptLine.Mark> marks, String firstField,
|
||||
int identStart, int identEnd, TypeInfo receiverType) {
|
||||
// Mark the first field
|
||||
FieldInfo fInfo = null;
|
||||
TypeInfo currentType = null;
|
||||
|
||||
if (receiverType != null && receiverType.hasField(firstField)) {
|
||||
fInfo = receiverType.getFieldInfo(firstField);
|
||||
boolean hasMore = document.isFollowedByDot(identEnd);
|
||||
// For method/array result chains, the receiver is always an instance, not a type
|
||||
boolean isStatic = false;
|
||||
|
||||
FieldAccessInfo accessInfo = document.createFieldAccessInfo(firstField, identStart, identEnd,
|
||||
receiverType, fInfo, !hasMore, isStatic);
|
||||
|
||||
marks.add(new ScriptLine.Mark(identStart, identEnd, getFieldTokenType(fInfo), accessInfo));
|
||||
currentType = (fInfo != null) ? fInfo.getTypeInfo() : null;
|
||||
} else {
|
||||
marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.UNDEFINED_VAR));
|
||||
return;
|
||||
}
|
||||
|
||||
// Continue the chain
|
||||
int pos = identEnd;
|
||||
while (pos < text.length()) {
|
||||
pos = document.skipWhitespace(pos);
|
||||
if (pos >= text.length() || text.charAt(pos) != '.') break;
|
||||
pos++; // Skip dot
|
||||
|
||||
pos = document.skipWhitespace(pos);
|
||||
if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos))) break;
|
||||
|
||||
// Read identifier
|
||||
int nStart = pos;
|
||||
while (pos < text.length() && Character.isJavaIdentifierPart(text.charAt(pos))) pos++;
|
||||
int nEnd = pos;
|
||||
String seg = text.substring(nStart, nEnd);
|
||||
|
||||
// Stop if method call
|
||||
if (document.isFollowedByParen(nEnd)) break;
|
||||
if (document.isExcluded(nStart)) break;
|
||||
|
||||
boolean isLast = !document.isFollowedByDot(nEnd);
|
||||
|
||||
if (currentType != null && currentType.isResolved() && currentType.hasField(seg)) {
|
||||
FieldInfo segInfo = currentType.getFieldInfo(seg);
|
||||
// In method/array result chains, segments are always instance access
|
||||
boolean isStatic = false;
|
||||
|
||||
FieldAccessInfo accessInfo = document.createFieldAccessInfo(seg, nStart, nEnd,
|
||||
currentType, segInfo, isLast, isStatic);
|
||||
|
||||
marks.add(new ScriptLine.Mark(nStart, nEnd, getFieldTokenType(segInfo), accessInfo));
|
||||
currentType = (segInfo != null) ? segInfo.getTypeInfo() : null;
|
||||
} else {
|
||||
marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.UNDEFINED_VAR));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== UTILITY HELPERS ====================
|
||||
|
||||
private TokenType getFieldTokenType(FieldInfo fieldInfo) {
|
||||
if (fieldInfo != null && fieldInfo.isEnumConstant())
|
||||
return TokenType.ENUM_CONSTANT;
|
||||
|
||||
return TokenType.GLOBAL_FIELD;
|
||||
}
|
||||
|
||||
/** Get the non-whitespace character before a position */
|
||||
private Character getNonWhitespaceBefore(int pos) {
|
||||
int before = pos - 1;
|
||||
while (before >= 0 && Character.isWhitespace(text.charAt(before))) before--;
|
||||
return (before >= 0) ? text.charAt(before) : null;
|
||||
}
|
||||
|
||||
/** Check if the receiver type is a class/type (static context) */
|
||||
private boolean isStaticContext(ChainContext ctx) {
|
||||
// Static context if the previous segment name resolves to a type/class
|
||||
if (ctx.currentIndex <= 0)
|
||||
return false;
|
||||
|
||||
String previousSegment = ctx.chain.segments.get(ctx.currentIndex - 1);
|
||||
int prevPos = ctx.chain.positions.get(ctx.currentIndex - 1)[0];
|
||||
|
||||
// Use unified static access checker
|
||||
return TypeResolver.isStaticAccessExpression(previousSegment, prevPos, document);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* Represents an inner callable scope (lambda or JS function expression).
|
||||
* These are NOT added to the methods list - they're resolution-only scopes.
|
||||
*/
|
||||
public class InnerCallableScope {
|
||||
|
||||
public enum Kind {
|
||||
JAVA_LAMBDA,
|
||||
JS_FUNCTION_EXPR,
|
||||
JS_ARROW_FUNC,
|
||||
JS_SHORTHAND_METHOD
|
||||
}
|
||||
|
||||
private final Kind kind;
|
||||
private final int headerStart; // Start of "(params) ->" or "function(params)"
|
||||
private final int headerEnd; // End of header (position of "->" for lambda, ")" for JS func)
|
||||
private final int bodyStart; // Start of body (expression start or '{')
|
||||
private final int bodyEnd; // End of body (expression end or '}')
|
||||
private final List<FieldInfo> parameters = new ArrayList<>();
|
||||
private final Map<String, FieldInfo> locals = new HashMap<>();
|
||||
private InnerCallableScope parentScope; // Enclosing inner scope (for nested lambdas)
|
||||
private TypeInfo expectedType; // The expected functional interface type (set during resolution)
|
||||
private TypeInfo containingObjectType; // For object-literal methods, the synthetic object-literal type
|
||||
|
||||
public InnerCallableScope(Kind kind, int headerStart, int headerEnd, int bodyStart, int bodyEnd) {
|
||||
this.kind = kind;
|
||||
this.headerStart = headerStart;
|
||||
this.headerEnd = headerEnd;
|
||||
this.bodyStart = bodyStart;
|
||||
this.bodyEnd = bodyEnd;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public Kind getKind() { return kind; }
|
||||
public int getHeaderStart() { return headerStart; }
|
||||
public int getHeaderEnd() { return headerEnd; }
|
||||
public int getBodyStart() { return bodyStart; }
|
||||
public int getBodyEnd() { return bodyEnd; }
|
||||
public List<FieldInfo> getParameters() { return parameters; }
|
||||
public Map<String, FieldInfo> getLocals() { return locals; }
|
||||
public InnerCallableScope getParentScope() { return parentScope; }
|
||||
public TypeInfo getExpectedType() { return expectedType; }
|
||||
public TypeInfo getContainingObjectType() { return containingObjectType; }
|
||||
|
||||
// Setters
|
||||
public void setParentScope(InnerCallableScope parent) { this.parentScope = parent; }
|
||||
public void setExpectedType(TypeInfo type) { this.expectedType = type; }
|
||||
public void setContainingObjectType(TypeInfo type) { this.containingObjectType = type; }
|
||||
|
||||
public void addParameter(FieldInfo param) {
|
||||
parameters.add(param);
|
||||
}
|
||||
|
||||
public void addLocal(String name, FieldInfo local) {
|
||||
locals.put(name, local);
|
||||
}
|
||||
|
||||
public FieldInfo getParameter(String name) {
|
||||
for (FieldInfo param : parameters) {
|
||||
if (param.getName().equals(name)) {
|
||||
return param;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean hasParameter(String name) {
|
||||
return getParameter(name) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a position is inside this scope's body.
|
||||
*/
|
||||
public boolean containsPosition(int position) {
|
||||
return position >= bodyStart && position < bodyEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a position is inside this scope's header (parameter list).
|
||||
*/
|
||||
public boolean containsHeaderPosition(int position) {
|
||||
return position >= headerStart && position < headerEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full range (header + body).
|
||||
*/
|
||||
public int getFullStart() {
|
||||
return headerStart;
|
||||
}
|
||||
|
||||
public int getFullEnd() {
|
||||
return bodyEnd;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.ScopeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
class LoopVariableParser {
|
||||
|
||||
private static final Pattern ENHANCED_FOR = Pattern.compile(
|
||||
"\\bfor\\s*\\(\\s*([A-Za-z_][\\w.<>\\[\\] \\t]*?(?:\\[\\s*\\])*)\\s+(\\w+)\\s*:\\s*([^)]+)\\)");
|
||||
private static final Pattern CLASSIC_FOR = Pattern.compile(
|
||||
"\\bfor\\s*\\(\\s*([A-Za-z_][\\w.<>\\[\\] \\t]*?(?:\\[\\s*\\])*)\\s+(\\w+)\\s*=");
|
||||
private static final Pattern FOR_IN = Pattern.compile(
|
||||
"\\bfor\\s*\\(\\s*(?:var|let|const)\\s+(\\w+)\\s+in\\s+([^)]+)\\)");
|
||||
private static final Pattern FOR_OF = Pattern.compile(
|
||||
"\\bfor\\s*\\(\\s*(?:var|let|const)\\s+(\\w+)\\s+of\\s+([^)]+)\\)");
|
||||
static final Pattern FOR_IN_OF_LOOKAHEAD = Pattern.compile("^\\s+(?:in|of)\\s");
|
||||
|
||||
private static final Set<String> SKIP_TYPES = new HashSet<>(Arrays.asList(
|
||||
"return", "if", "while", "for", "switch", "catch", "new", "throw"));
|
||||
|
||||
private final ScriptDocument doc;
|
||||
private final String text;
|
||||
|
||||
LoopVariableParser(ScriptDocument doc, String text) {
|
||||
this.doc = doc;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
boolean isForInOrOf(String textAfterVarName) {
|
||||
return FOR_IN_OF_LOOKAHEAD.matcher(textAfterVarName).find();
|
||||
}
|
||||
|
||||
boolean isInsideForHeader(int bodyStart, int absPos) {
|
||||
int enclosingParen = doc.findEnclosingParenStart(bodyStart, absPos);
|
||||
return enclosingParen >= 0 && "for".equals(doc.readKeywordBefore(enclosingParen));
|
||||
}
|
||||
|
||||
void parse(Map<Integer, Map<String, List<FieldInfo>>> methodLocals) {
|
||||
boolean js = doc.isJavaScript();
|
||||
|
||||
for (MethodInfo method : doc.getAllMethods()) {
|
||||
int bodyStart = method.getBodyStart();
|
||||
int bodyEnd = method.getBodyEnd();
|
||||
if (bodyStart < 0 || bodyEnd <= bodyStart) continue;
|
||||
String bodyText = text.substring(bodyStart, Math.min(bodyEnd, text.length()));
|
||||
Map<String, List<FieldInfo>> locals = methodLocals.computeIfAbsent(method.getDeclarationOffset(), k -> new HashMap<>());
|
||||
scanRange(bodyText, bodyStart, bodyEnd, method, locals, js);
|
||||
}
|
||||
|
||||
for (InnerCallableScope scope : doc.getInnerScopes()) {
|
||||
int bodyStart = scope.getBodyStart();
|
||||
int bodyEnd = scope.getBodyEnd();
|
||||
if (bodyStart < 0 || bodyEnd <= bodyStart) continue;
|
||||
String bodyText = text.substring(bodyStart, Math.min(bodyEnd, text.length()));
|
||||
MethodInfo ownerMethod = doc.findMethodAtPosition(bodyStart);
|
||||
if (ownerMethod == null) continue;
|
||||
Map<String, List<FieldInfo>> locals = methodLocals.computeIfAbsent(ownerMethod.getDeclarationOffset(), k -> new HashMap<>());
|
||||
scanRange(bodyText, bodyStart, bodyEnd, ownerMethod, locals, js);
|
||||
}
|
||||
|
||||
scanTopLevelLoops();
|
||||
}
|
||||
|
||||
private void scanTopLevelLoops() {
|
||||
boolean js = doc.isJavaScript();
|
||||
|
||||
if (!js) {
|
||||
for (Matcher m = ENHANCED_FOR.matcher(text); m.find(); ) {
|
||||
int absForPos = m.start();
|
||||
if (doc.isExcluded(absForPos) || isInsideAnyScope(absForPos)) continue;
|
||||
String typeName = m.group(1).trim();
|
||||
String varName = m.group(2);
|
||||
TypeInfo typeInfo = doc.resolveType(typeName);
|
||||
ScopeInfo scope = computeForBodyScope(absForPos, 0, text.length());
|
||||
FieldInfo fi = FieldInfo.localField(varName, typeInfo, m.start(2), null, -1, -1, 0);
|
||||
fi.setScopeInfo(scope);
|
||||
doc.addTopLevelLocal(fi);
|
||||
}
|
||||
|
||||
for (Matcher m = CLASSIC_FOR.matcher(text); m.find(); ) {
|
||||
int absForPos = m.start();
|
||||
if (doc.isExcluded(absForPos) || isInsideAnyScope(absForPos)) continue;
|
||||
String typeName = m.group(1).trim();
|
||||
String varName = m.group(2);
|
||||
if (SKIP_TYPES.contains(typeName)) continue;
|
||||
TypeInfo typeInfo = doc.resolveType(typeName);
|
||||
ScopeInfo scope = computeForBodyScope(absForPos, 0, text.length());
|
||||
FieldInfo fi = FieldInfo.localField(varName, typeInfo, m.start(2), null, -1, -1, 0);
|
||||
fi.setScopeInfo(scope);
|
||||
doc.addTopLevelLocal(fi);
|
||||
}
|
||||
} else {
|
||||
for (Matcher m = FOR_IN.matcher(text); m.find(); ) {
|
||||
int absForPos = m.start();
|
||||
if (doc.isExcluded(absForPos) || isInsideAnyScope(absForPos)) continue;
|
||||
String varName = m.group(1);
|
||||
ScopeInfo scope = computeForBodyScope(absForPos, 0, text.length());
|
||||
FieldInfo fi = FieldInfo.localField(varName, TypeInfo.fromPrimitive("int"), m.start(1), null, -1, -1, 0);
|
||||
fi.setScopeInfo(scope);
|
||||
doc.addTopLevelLocal(fi);
|
||||
}
|
||||
|
||||
for (Matcher m = FOR_OF.matcher(text); m.find(); ) {
|
||||
int absForPos = m.start();
|
||||
if (doc.isExcluded(absForPos) || isInsideAnyScope(absForPos)) continue;
|
||||
String varName = m.group(1);
|
||||
String iterableExpr = m.group(2).trim();
|
||||
TypeInfo iterableType = doc.resolveExpressionType(iterableExpr, absForPos);
|
||||
TypeInfo elementType = (iterableType != null) ? iterableType.getElementType() : null;
|
||||
if (elementType == null) elementType = TypeInfo.ANY;
|
||||
ScopeInfo scope = computeForBodyScope(absForPos, 0, text.length());
|
||||
FieldInfo fi = FieldInfo.localField(varName, elementType, m.start(1), null, -1, -1, 0);
|
||||
fi.setScopeInfo(scope);
|
||||
doc.addTopLevelLocal(fi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInsideAnyScope(int pos) {
|
||||
for (MethodInfo method : doc.getAllMethods()) {
|
||||
if (method.containsPosition(pos)) return true;
|
||||
}
|
||||
for (InnerCallableScope scope : doc.getInnerScopes()) {
|
||||
if (scope.containsPosition(pos)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void scanRange(String bodyText, int bodyStart, int bodyEnd,
|
||||
MethodInfo method, Map<String, List<FieldInfo>> locals, boolean js) {
|
||||
if (!js) {
|
||||
Matcher m = ENHANCED_FOR.matcher(bodyText);
|
||||
while (m.find()) {
|
||||
int absForPos = bodyStart + m.start();
|
||||
if (doc.isExcluded(absForPos)) continue;
|
||||
String typeName = m.group(1).trim();
|
||||
String varName = m.group(2);
|
||||
int absVarPos = bodyStart + m.start(2);
|
||||
TypeInfo typeInfo = doc.resolveType(typeName);
|
||||
ScopeInfo scope = computeForBodyScope(absForPos, bodyStart, bodyEnd);
|
||||
FieldInfo fi = FieldInfo.localField(varName, typeInfo, absVarPos, method, -1, -1, 0);
|
||||
fi.setScopeInfo(scope);
|
||||
locals.computeIfAbsent(varName, k -> new ArrayList<>()).add(fi);
|
||||
}
|
||||
|
||||
m = CLASSIC_FOR.matcher(bodyText);
|
||||
while (m.find()) {
|
||||
int absForPos = bodyStart + m.start();
|
||||
if (doc.isExcluded(absForPos)) continue;
|
||||
String typeName = m.group(1).trim();
|
||||
String varName = m.group(2);
|
||||
if (SKIP_TYPES.contains(typeName)) continue;
|
||||
int absVarPos = bodyStart + m.start(2);
|
||||
TypeInfo typeInfo = doc.resolveType(typeName);
|
||||
ScopeInfo scope = computeForBodyScope(absForPos, bodyStart, bodyEnd);
|
||||
FieldInfo fi = FieldInfo.localField(varName, typeInfo, absVarPos, method, -1, -1, 0);
|
||||
fi.setScopeInfo(scope);
|
||||
locals.computeIfAbsent(varName, k -> new ArrayList<>()).add(fi);
|
||||
}
|
||||
} else {
|
||||
Matcher m = FOR_IN.matcher(bodyText);
|
||||
while (m.find()) {
|
||||
int absForPos = bodyStart + m.start();
|
||||
if (doc.isExcluded(absForPos)) continue;
|
||||
String varName = m.group(1);
|
||||
int absVarPos = bodyStart + m.start(1);
|
||||
ScopeInfo scope = computeForBodyScope(absForPos, bodyStart, bodyEnd);
|
||||
FieldInfo fi = FieldInfo.localField(varName, TypeInfo.fromPrimitive("int"), absVarPos, method, -1, -1, 0);
|
||||
fi.setScopeInfo(scope);
|
||||
locals.computeIfAbsent(varName, k -> new ArrayList<>()).add(fi);
|
||||
}
|
||||
|
||||
m = FOR_OF.matcher(bodyText);
|
||||
while (m.find()) {
|
||||
int absForPos = bodyStart + m.start();
|
||||
if (doc.isExcluded(absForPos)) continue;
|
||||
String varName = m.group(1);
|
||||
String iterableExpr = m.group(2).trim();
|
||||
int absVarPos = bodyStart + m.start(1);
|
||||
TypeInfo iterableType = doc.resolveExpressionType(iterableExpr, absForPos);
|
||||
TypeInfo elementType = (iterableType != null) ? iterableType.getElementType() : null;
|
||||
if (elementType == null) elementType = TypeInfo.ANY;
|
||||
ScopeInfo scope = computeForBodyScope(absForPos, bodyStart, bodyEnd);
|
||||
FieldInfo fi = FieldInfo.localField(varName, elementType, absVarPos, method, -1, -1, 0);
|
||||
fi.setScopeInfo(scope);
|
||||
locals.computeIfAbsent(varName, k -> new ArrayList<>()).add(fi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ScopeInfo computeForBodyScope(int forKeywordPos, int bodyStart, int bodyEnd) {
|
||||
int max = Math.min(bodyEnd, text.length());
|
||||
int openParen = -1;
|
||||
for (int i = forKeywordPos; i < max; i++) {
|
||||
if (doc.isExcluded(i)) continue;
|
||||
char c = text.charAt(i);
|
||||
if (c == '(') { openParen = i; break; }
|
||||
if (!Character.isWhitespace(c) && c != 'f' && c != 'o' && c != 'r') break;
|
||||
}
|
||||
if (openParen < 0) return new ScopeInfo(forKeywordPos, bodyEnd, false, "block");
|
||||
|
||||
int closeParen = doc.findMatchingParen(openParen, max);
|
||||
if (closeParen < 0) return new ScopeInfo(forKeywordPos, bodyEnd, false, "block");
|
||||
|
||||
int after = doc.skipWhitespaceAndExcluded(closeParen + 1, max);
|
||||
if (after < 0 || after >= max) return new ScopeInfo(forKeywordPos, bodyEnd, false, "block");
|
||||
|
||||
if (text.charAt(after) == '{') {
|
||||
int closeBrace = doc.findMatchingBrace(after);
|
||||
if (closeBrace > 0) return new ScopeInfo(forKeywordPos, closeBrace + 1, false, "block");
|
||||
return new ScopeInfo(forKeywordPos, bodyEnd, false, "block");
|
||||
}
|
||||
|
||||
int stmtEnd = doc.findStatementEnd(after, max);
|
||||
if (stmtEnd > after) return new ScopeInfo(forKeywordPos, stmtEnd, false, "block");
|
||||
return new ScopeInfo(forKeywordPos, bodyEnd, false, "block");
|
||||
}
|
||||
}
|
||||
+405
@@ -0,0 +1,405 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter;
|
||||
|
||||
// ==================== MULTI-DECLARATOR HELPERS ====================
|
||||
//
|
||||
// JavaScript and Java both allow declaring multiple variables in a single
|
||||
// statement, separated by commas:
|
||||
//
|
||||
// JS: var a = 1, b = "x", c = player;
|
||||
// Java: int x = 1, y, z = 3;
|
||||
//
|
||||
// The existing regex patterns only match the FIRST declarator because:
|
||||
// - The JS pattern `(var|let|const)\s+(\w+)` requires a keyword prefix.
|
||||
// - The Java FIELD_DECL_PATTERN requires a type name prefix.
|
||||
//
|
||||
// Continuation declarators after the first comma have no keyword/type prefix,
|
||||
// so they must be scanned manually. The two helpers below handle this by
|
||||
// scanning forward from the end of the first declarator for commas at
|
||||
// nesting depth 0, then extracting the `name [= initializer]` pattern
|
||||
// for each continuation declarator.
|
||||
|
||||
/**
|
||||
* Static helpers for parsing comma-separated multi-variable declarations.
|
||||
*
|
||||
* <p>Extracted from {@link ScriptDocument} to keep the main document class
|
||||
* focused on orchestration while this class owns the scanning algorithms.</p>
|
||||
*/
|
||||
public final class MultiDeclaratorParser {
|
||||
|
||||
private MultiDeclaratorParser() {}
|
||||
|
||||
// ======================== CALLBACKS ========================
|
||||
|
||||
/** Callback for {@link #scanJSContinuationDeclarators}. */
|
||||
@FunctionalInterface
|
||||
public interface JSContinuationCallback {
|
||||
void accept(String varName, int absNamePos, String initializer, int absInitStart, int absInitEnd);
|
||||
}
|
||||
|
||||
/** Callback for {@link #scanJavaContinuationDeclarators}. */
|
||||
@FunctionalInterface
|
||||
public interface JavaContinuationCallback {
|
||||
void accept(String varName, int absNamePos, int absInitStart, int absInitEnd);
|
||||
}
|
||||
|
||||
// ======================== JS SCANNING ========================
|
||||
|
||||
/**
|
||||
* Scan forward from {@code scanStart} in {@code source} for comma-separated
|
||||
* continuation declarators in a JS {@code var/let/const} statement.
|
||||
*
|
||||
* <p>Each continuation declarator follows the pattern: {@code name [= initializer]}.
|
||||
* The initializer (if present) is terminated using comma-aware scanning via
|
||||
* {@link ScriptDocument#findJsInitializerEnd(String, int, boolean)}, which stops at
|
||||
* top-level commas so that each declarator's RHS is correctly bounded.</p>
|
||||
*
|
||||
* <p>The callback receives (varName, absNamePos, initializer, absInitStart, absInitEnd)
|
||||
* for each continuation declarator found.</p>
|
||||
*
|
||||
* @param doc the owning ScriptDocument (provides {@code findJsInitializerEnd}
|
||||
* and {@code skipSegmentWhitespace})
|
||||
* @param source the source text to scan within (e.g. method body or global text)
|
||||
* @param scanStart the position in {@code source} to begin scanning (typically
|
||||
* right after the first declarator's initializer end or name end)
|
||||
* @param absBase the absolute offset of {@code source[0]} in the full document text
|
||||
* (used to compute absolute positions for the callback)
|
||||
* @param callback receives each continuation declarator
|
||||
*/
|
||||
public static void scanJSContinuationDeclarators(
|
||||
ScriptDocument doc, String source, int scanStart, int absBase,
|
||||
JSContinuationCallback callback) {
|
||||
|
||||
int pos = scanStart;
|
||||
while (pos < source.length()) {
|
||||
// Skip whitespace
|
||||
while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
|
||||
if (pos >= source.length()) break;
|
||||
|
||||
// Expect a comma separating the next declarator
|
||||
if (source.charAt(pos) != ',') break;
|
||||
pos++; // skip the comma
|
||||
|
||||
// Skip whitespace after comma
|
||||
while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
|
||||
if (pos >= source.length()) break;
|
||||
|
||||
// Read the variable name (must be an identifier)
|
||||
int nameStart = pos;
|
||||
while (pos < source.length() && Character.isJavaIdentifierPart(source.charAt(pos))) pos++;
|
||||
if (pos == nameStart) break; // no identifier found; malformed
|
||||
String varName = source.substring(nameStart, pos);
|
||||
|
||||
// Skip whitespace after name
|
||||
while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
|
||||
|
||||
String initializer = null;
|
||||
int initStart = -1;
|
||||
int initEnd = -1;
|
||||
|
||||
// Check for optional '=' initializer
|
||||
if (pos < source.length() && source.charAt(pos) == '=') {
|
||||
// Make sure it's not ==
|
||||
if (pos + 1 < source.length() && source.charAt(pos + 1) == '=') break;
|
||||
pos++; // skip '='
|
||||
int rhsStart = doc.skipSegmentWhitespace(source, pos);
|
||||
// Use comma-aware initializer termination so that the RHS stops at
|
||||
// the next top-level comma (which starts the next declarator).
|
||||
int rhsEnd = doc.findJsInitializerEnd(source, rhsStart, true);
|
||||
if (rhsEnd > rhsStart) {
|
||||
initializer = source.substring(rhsStart, rhsEnd).trim();
|
||||
if (initializer.isEmpty()) initializer = null;
|
||||
}
|
||||
initStart = rhsStart;
|
||||
initEnd = rhsEnd;
|
||||
pos = rhsEnd;
|
||||
}
|
||||
|
||||
int absNamePos = absBase + nameStart;
|
||||
int absInitStart = initStart >= 0 ? absBase + initStart : -1;
|
||||
int absInitEnd = initEnd >= 0 ? absBase + initEnd : -1;
|
||||
|
||||
callback.accept(varName, absNamePos, initializer, absInitStart, absInitEnd);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== JAVA SCANNING ========================
|
||||
|
||||
/**
|
||||
* Scan forward from {@code scanStart} in {@code source} for comma-separated
|
||||
* continuation declarators in a Java multi-variable declaration.
|
||||
*
|
||||
* <p>Java allows {@code int x = 1, y, z = 3;} where all declarators share
|
||||
* the same declared type. The {@code FIELD_DECL_PATTERN} regex only matches
|
||||
* the first declarator ({@code int x =}); subsequent names like {@code y}
|
||||
* and {@code z} lack a type prefix. This method manually scans the remainder
|
||||
* of the statement for those continuation names.</p>
|
||||
*
|
||||
* <p>Each continuation declarator follows the pattern: {@code name [= initializer]}.
|
||||
* The initializer is terminated at {@code ;} or {@code ,} at depth 0.</p>
|
||||
*
|
||||
* @param source the source text to scan within
|
||||
* @param scanStart the position in {@code source} to begin scanning (typically
|
||||
* right after the semicolon/delimiter of the first declarator
|
||||
* match, or right after the first declarator's initializer)
|
||||
* @param absBase the absolute offset of {@code source[0]} in the full document text
|
||||
* @param callback receives each continuation declarator
|
||||
*/
|
||||
public static void scanJavaContinuationDeclarators(
|
||||
String source, int scanStart, int absBase,
|
||||
JavaContinuationCallback callback) {
|
||||
|
||||
int pos = scanStart;
|
||||
while (pos < source.length()) {
|
||||
// Skip whitespace
|
||||
while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
|
||||
if (pos >= source.length()) break;
|
||||
|
||||
// Expect a comma separating the next declarator
|
||||
if (source.charAt(pos) != ',') break;
|
||||
pos++; // skip the comma
|
||||
|
||||
// Skip whitespace after comma
|
||||
while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
|
||||
if (pos >= source.length()) break;
|
||||
|
||||
// Read the variable name (must be an identifier)
|
||||
int nameStart = pos;
|
||||
while (pos < source.length() && Character.isJavaIdentifierPart(source.charAt(pos))) pos++;
|
||||
if (pos == nameStart) break; // no identifier found; malformed
|
||||
String varName = source.substring(nameStart, pos);
|
||||
|
||||
// Skip whitespace after name
|
||||
while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
|
||||
|
||||
int initStart = -1;
|
||||
int initEnd = -1;
|
||||
|
||||
// Check for optional '=' initializer
|
||||
if (pos < source.length() && source.charAt(pos) == '=') {
|
||||
// Make sure it's not ==
|
||||
if (pos + 1 < source.length() && source.charAt(pos + 1) == '=') break;
|
||||
|
||||
initStart = pos; // position of '='
|
||||
pos++; // skip '='
|
||||
int rhsStart = pos;
|
||||
// Skip whitespace after '='
|
||||
while (rhsStart < source.length() && Character.isWhitespace(source.charAt(rhsStart))) rhsStart++;
|
||||
|
||||
// Scan forward for ';' or ',' at depth 0 to terminate this declarator's initializer
|
||||
int depth = 0;
|
||||
int angleDepth = 0;
|
||||
int scanPos = rhsStart;
|
||||
while (scanPos < source.length()) {
|
||||
char c = source.charAt(scanPos);
|
||||
if (c == '(' || c == '[' || c == '{') depth++;
|
||||
else if (c == ')' || c == ']' || c == '}') depth--;
|
||||
else if (c == '<') angleDepth++;
|
||||
else if (c == '>') angleDepth--;
|
||||
else if ((c == ';' || c == ',') && depth == 0 && angleDepth == 0) {
|
||||
initEnd = scanPos;
|
||||
break;
|
||||
}
|
||||
scanPos++;
|
||||
}
|
||||
pos = (initEnd >= 0) ? initEnd : scanPos;
|
||||
} else if (pos < source.length() && (source.charAt(pos) == ';' || source.charAt(pos) == ',')) {
|
||||
// No initializer — name only (e.g., the `y` in `int x, y, z;`)
|
||||
// Don't advance past ';' here; let the outer loop handle it.
|
||||
// If it's a comma, we'll pick it up on the next iteration.
|
||||
} else {
|
||||
// Unexpected character; stop scanning
|
||||
break;
|
||||
}
|
||||
|
||||
int absNamePos = absBase + nameStart;
|
||||
int absInitStartAbs = initStart >= 0 ? absBase + initStart : -1;
|
||||
int absInitEndAbs = initEnd >= 0 ? absBase + initEnd : -1;
|
||||
|
||||
callback.accept(varName, absNamePos, absInitStartAbs, absInitEndAbs);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== SCAN-START POSITION HELPERS ========================
|
||||
|
||||
/**
|
||||
* Determine the scan position for continuation declarators after a JS
|
||||
* declarator match. If the first declarator has an initializer, we scan
|
||||
* from the initializer end; otherwise we scan from after the variable name.
|
||||
*
|
||||
* @param bodyText the body text being parsed
|
||||
* @param matchEnd the end position of the regex match (end of var name or '=')
|
||||
* @param hasEquals whether the first declarator has '='
|
||||
* @param initEnd the initializer end position (in bodyText coords), or -1
|
||||
* @return the position in bodyText to start scanning for continuation commas
|
||||
*/
|
||||
public static int jsDeclaratorScanStart(String bodyText, int matchEnd, boolean hasEquals, int initEnd) {
|
||||
if (hasEquals && initEnd >= 0) {
|
||||
return initEnd;
|
||||
}
|
||||
// No initializer: scan from after the variable name
|
||||
return matchEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the scan position for continuation declarators after a Java
|
||||
* FIELD_DECL_PATTERN match. The pattern's group(3) matches either '=', ';',
|
||||
* or ','. If it matched '=', the scan starts after the initializer expression;
|
||||
* if ';', there are no continuations (semicolon ends the statement).
|
||||
*
|
||||
* <p><b>Note:</b> when the regex greedily consumed commas into the type name
|
||||
* (e.g. {@code "int x, y,"} as the type group), the caller must detect this
|
||||
* via {@link #findFirstDepthZeroComma(String)} on the matched type string
|
||||
* and use the returned comma position directly as the scan start, bypassing
|
||||
* this method. See the call sites in {@link ScriptDocument} for details.</p>
|
||||
*
|
||||
* @param source the source text
|
||||
* @param delimiter the delimiter matched by group(3): "=", ";", or ","
|
||||
* @param delimEnd the end position of the delimiter in source coords
|
||||
* @param initEnd the initializer end position, or -1 if no initializer
|
||||
* @return the position in source to start scanning for continuation commas,
|
||||
* or -1 if the statement ended at a semicolon
|
||||
*/
|
||||
public static int javaDeclaratorScanStart(String source, String delimiter, int delimEnd, int initEnd) {
|
||||
if (";".equals(delimiter)) {
|
||||
return -1; // semicolon terminates the statement; no continuations possible
|
||||
}
|
||||
// delimiter is '=' or ',' — scan from after the initializer (which stops at ',' or ';')
|
||||
if (initEnd >= 0) {
|
||||
return initEnd;
|
||||
}
|
||||
// When delimiter is ',' with no initializer (e.g. the 'x' in "int x,y,z;"),
|
||||
// m.end(3) lands AFTER the comma. The scanner expects to START at the comma
|
||||
// so it can consume it as the continuation separator — so back up by one.
|
||||
if (",".equals(delimiter)) {
|
||||
return delimEnd - 1;
|
||||
}
|
||||
return delimEnd;
|
||||
}
|
||||
|
||||
// ======================== GREEDY-REGEX COMMA HELPERS ========================
|
||||
|
||||
/**
|
||||
* Find the first comma at bracket-depth 0 in the given string.
|
||||
*
|
||||
* <p>The Java {@code FIELD_DECL_PATTERN} regex allows commas inside the
|
||||
* type-name character class (to support generics like {@code Map<K, V>}).
|
||||
* This causes the regex to greedily consume multi-declarator commas into
|
||||
* group(1). For example, {@code "int x, y, z;"} matches with
|
||||
* group(1)={@code "int x, y,"} and group(2)={@code "z"}.</p>
|
||||
*
|
||||
* <p>This helper scans for the first comma that is NOT inside angle brackets
|
||||
* ({@code <>}), parentheses, square brackets, or braces, i.e. at depth 0.
|
||||
* A depth-0 comma indicates a multi-declarator separator that was consumed
|
||||
* by the greedy regex.</p>
|
||||
*
|
||||
* @param s the string to search (typically the matched type group)
|
||||
* @return the index of the first depth-0 comma, or -1 if none found
|
||||
*/
|
||||
public static int findFirstDepthZeroComma(String s) {
|
||||
int depth = 0;
|
||||
int angleDepth = 0;
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if (c == '(' || c == '[' || c == '{') depth++;
|
||||
else if (c == ')' || c == ']' || c == '}') depth--;
|
||||
else if (c == '<') angleDepth++;
|
||||
else if (c == '>') angleDepth--;
|
||||
else if (c == ',' && depth == 0 && angleDepth == 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the real type name from a regex group(1) that greedily consumed
|
||||
* extra variable names and commas.
|
||||
*
|
||||
* <p>When the regex matches {@code "int x, y, z;"} as group(1)={@code "int x, y,"},
|
||||
* the real type name is {@code "int"} — everything before the first identifier
|
||||
* that precedes the first depth-0 comma.</p>
|
||||
*
|
||||
* <p>For {@code "final int x, y,"}, returns {@code "final int"}.
|
||||
* For {@code "Map<String, Integer> x, y,"}, returns {@code "Map<String, Integer>"}.</p>
|
||||
*
|
||||
* @param typeGroup the full group(1) string from the regex match
|
||||
* @param commaIndex the index of the first depth-0 comma in {@code typeGroup}
|
||||
* (as returned by {@link #findFirstDepthZeroComma(String)})
|
||||
* @return the real type name (may include modifiers like "final")
|
||||
*/
|
||||
public static String extractRealTypeFromGreedyMatch(String typeGroup, int commaIndex) {
|
||||
// The part before the first depth-0 comma contains "Type firstVar"
|
||||
// (possibly with modifiers). We need to split off the last identifier
|
||||
// token before the comma, as that's the first variable name, not the type.
|
||||
String beforeComma = typeGroup.substring(0, commaIndex).trim();
|
||||
int lastSpace = beforeComma.lastIndexOf(' ');
|
||||
int lastTab = beforeComma.lastIndexOf('\t');
|
||||
int lastWS = Math.max(lastSpace, lastTab);
|
||||
if (lastWS < 0) {
|
||||
// No whitespace found — the "type" is actually just a variable name
|
||||
// This shouldn't happen for valid declarations but handle gracefully
|
||||
return beforeComma;
|
||||
}
|
||||
return beforeComma.substring(0, lastWS).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the first variable name from a regex group(1) that greedily consumed
|
||||
* extra variable names and commas.
|
||||
*
|
||||
* <p>When the regex matches {@code "int x, y, z;"} as group(1)={@code "int x, y,"},
|
||||
* the first variable name is {@code "x"} — the last identifier token before
|
||||
* the first depth-0 comma.</p>
|
||||
*
|
||||
* @param typeGroup the full group(1) string from the regex match
|
||||
* @param commaIndex the index of the first depth-0 comma in {@code typeGroup}
|
||||
* (as returned by {@link #findFirstDepthZeroComma(String)})
|
||||
* @return the first variable name
|
||||
*/
|
||||
public static String extractFirstVarFromGreedyMatch(String typeGroup, int commaIndex) {
|
||||
String beforeComma = typeGroup.substring(0, commaIndex).trim();
|
||||
int lastSpace = beforeComma.lastIndexOf(' ');
|
||||
int lastTab = beforeComma.lastIndexOf('\t');
|
||||
int lastWS = Math.max(lastSpace, lastTab);
|
||||
if (lastWS < 0) {
|
||||
return beforeComma;
|
||||
}
|
||||
return beforeComma.substring(lastWS + 1).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the absolute position of the first variable name within a greedily-consumed
|
||||
* type group. Searches for the first variable name (the last token before the
|
||||
* first depth-0 comma) within the source text starting from the type group start.
|
||||
*
|
||||
* @param source the full source text
|
||||
* @param typeGroupStart the absolute start of group(1) in the source
|
||||
* @param typeGroup the group(1) string
|
||||
* @param commaIndex the index of the first depth-0 comma in typeGroup
|
||||
* @return the absolute position of the first variable name in source
|
||||
*/
|
||||
public static int findFirstVarPosition(String source, int typeGroupStart, String typeGroup, int commaIndex) {
|
||||
String firstVar = extractFirstVarFromGreedyMatch(typeGroup, commaIndex);
|
||||
// The first variable appears in typeGroup just before the comma.
|
||||
// Search backwards from commaIndex to find it.
|
||||
String beforeComma = typeGroup.substring(0, commaIndex).trim();
|
||||
int varStartInGroup = beforeComma.lastIndexOf(firstVar);
|
||||
return typeGroupStart + varStartInGroup;
|
||||
}
|
||||
|
||||
// ======================== MISC HELPERS ========================
|
||||
|
||||
/**
|
||||
* Check whether the statement starting at {@code stmtStart} begins with
|
||||
* a JS declaration keyword ({@code var}, {@code let}, or {@code const}).
|
||||
* Used by {@link ScriptDocument}'s {@code parseAssignments()} to decide
|
||||
* whether the RHS of an {@code =} should stop at a top-level comma
|
||||
* (multi-declarator context).
|
||||
*/
|
||||
public static boolean startsWithJSKeyword(String source, int stmtStart, int equalsPos) {
|
||||
int i = stmtStart;
|
||||
while (i < equalsPos && Character.isWhitespace(source.charAt(i))) i++;
|
||||
String prefix = source.substring(i, Math.min(i + 6, equalsPos)).trim();
|
||||
return prefix.startsWith("var ") || prefix.startsWith("var\t")
|
||||
|| prefix.startsWith("let ") || prefix.startsWith("let\t")
|
||||
|| prefix.startsWith("const ") || prefix.startsWith("const\t");
|
||||
}
|
||||
}
|
||||
+1494
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,431 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter;
|
||||
|
||||
import noppes.npcs.client.ClientProxy;
|
||||
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.method.MethodCallInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
|
||||
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.TypeInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Represents a single line of source code with its tokens.
|
||||
* Handles token management, navigation, and rendering.
|
||||
*
|
||||
* A ScriptLine is responsible for:
|
||||
* - Storing tokens that fall within this line's character range
|
||||
* - Managing token navigation (prev/next within line)
|
||||
* - Rendering the line with proper syntax highlighting
|
||||
* - Computing indent guides for this line
|
||||
*/
|
||||
public class ScriptLine {
|
||||
|
||||
private static final char COLOR_CHAR = '\u00A7';
|
||||
|
||||
private final String text; // The actual text of this line
|
||||
public final int globalStart; // Start offset in the full document
|
||||
public final int globalEnd; // End offset in the full document (exclusive)
|
||||
public final int lineIndex; // 0-based line number
|
||||
|
||||
private final List<Token> tokens = new ArrayList<>();
|
||||
private final List<Integer> indentGuides = new ArrayList<>(); // Column positions for indent guides
|
||||
|
||||
// Navigation
|
||||
private ScriptLine prev;
|
||||
private ScriptLine next;
|
||||
private ScriptDocument parent;
|
||||
|
||||
public ScriptLine(String text, int globalStart, int globalEnd, int lineIndex) {
|
||||
this.text = text;
|
||||
this.globalStart = globalStart;
|
||||
this.globalEnd = globalEnd;
|
||||
this.lineIndex = lineIndex;
|
||||
}
|
||||
|
||||
// ==================== GETTERS ====================
|
||||
|
||||
public String getText() { return text; }
|
||||
public int getGlobalStart() { return globalStart; }
|
||||
public int getGlobalEnd() { return globalEnd; }
|
||||
public int getLineIndex() { return lineIndex; }
|
||||
public int getLength() { return text.length(); }
|
||||
public List<Token> getTokens() { return Collections.unmodifiableList(tokens); }
|
||||
public List<Integer> getIndentGuides() { return Collections.unmodifiableList(indentGuides); }
|
||||
public ScriptDocument getParent() { return parent; }
|
||||
|
||||
void setParent(ScriptDocument parent) { this.parent = parent; }
|
||||
void setPrev(ScriptLine prev) { this.prev = prev; }
|
||||
void setNext(ScriptLine next) { this.next = next; }
|
||||
|
||||
// ==================== NAVIGATION ====================
|
||||
|
||||
public ScriptLine prev() { return prev; }
|
||||
public ScriptLine next() { return next; }
|
||||
|
||||
public Token getFirstToken() {
|
||||
return tokens.isEmpty() ? null : tokens.get(0);
|
||||
}
|
||||
|
||||
public Token getLastToken() {
|
||||
return tokens.isEmpty() ? null : tokens.get(tokens.size() - 1);
|
||||
}
|
||||
|
||||
public Token getTokenAt(int globalPosition) {
|
||||
for (Token t : tokens)
|
||||
if (globalPosition >= t.getGlobalStart() && globalPosition <= t.getGlobalEnd())
|
||||
return t;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Token getTokenAt(int globalPosition, Predicate<Token> condition) {
|
||||
for (Token t : tokens)
|
||||
if (globalPosition >= t.getGlobalStart() && globalPosition <= t.getGlobalEnd() && condition.test(t))
|
||||
return t;
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
// ==================== TOKEN MANAGEMENT ====================
|
||||
|
||||
/**
|
||||
* Clear all tokens from this line.
|
||||
*/
|
||||
public void clearTokens() {
|
||||
tokens.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a token to this line and set up navigation links.
|
||||
*/
|
||||
public void addToken(Token token) {
|
||||
if (!tokens.isEmpty()) {
|
||||
Token last = tokens.get(tokens.size() - 1);
|
||||
last.setNext(token);
|
||||
token.setPrev(last);
|
||||
}
|
||||
token.setParentLine(this);
|
||||
tokens.add(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build tokens from a list of marks (highlight regions).
|
||||
* Fills gaps between marks with DEFAULT tokens.
|
||||
* Attaches metadata from marks to the created tokens.
|
||||
*
|
||||
* @param marks List of highlight marks that overlap this line
|
||||
* @param fullText The complete document text
|
||||
* @param document The parent ScriptDocument (for context lookups)
|
||||
*/
|
||||
public void buildTokensFromMarks(List<Mark> marks, String fullText, ScriptDocument document) {
|
||||
clearTokens();
|
||||
int cursor = globalStart;
|
||||
|
||||
for (Mark mark : marks) {
|
||||
// Skip marks that don't overlap this line
|
||||
if (mark.end <= globalStart || mark.start >= globalEnd) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clamp mark to line boundaries
|
||||
int tokenStart = Math.max(mark.start, globalStart);
|
||||
int tokenEnd = Math.min(mark.end, globalEnd);
|
||||
|
||||
// Validate bounds
|
||||
tokenStart = Math.max(0, Math.min(tokenStart, fullText.length()));
|
||||
tokenEnd = Math.max(0, Math.min(tokenEnd, fullText.length()));
|
||||
|
||||
// Add default token for any gap before this mark
|
||||
if (cursor < tokenStart) {
|
||||
int gapEnd = Math.min(tokenStart, fullText.length());
|
||||
String gapText = fullText.substring(cursor, gapEnd);
|
||||
addToken(Token.defaultToken(gapText, cursor, gapEnd));
|
||||
}
|
||||
|
||||
// Add the marked token with metadata
|
||||
if (tokenStart < tokenEnd) {
|
||||
String tokenText = fullText.substring(tokenStart, tokenEnd);
|
||||
Token token = new Token(tokenText, tokenStart, tokenEnd, mark.type);
|
||||
|
||||
// Attach metadata based on type
|
||||
if (mark.metadata != null)
|
||||
applyTokenMetadata(token, mark.metadata);
|
||||
|
||||
addToken(token);
|
||||
}
|
||||
|
||||
cursor = tokenEnd;
|
||||
}
|
||||
|
||||
// Add trailing default token if needed
|
||||
if (cursor < globalEnd) {
|
||||
int end = Math.min(globalEnd, fullText.length());
|
||||
if (cursor < end) {
|
||||
String trailingText = fullText.substring(cursor, end);
|
||||
addToken(Token.defaultToken(trailingText, cursor, end));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void applyTokenMetadata(Token token, Object metadata) {
|
||||
if (metadata instanceof TypeInfo) {
|
||||
token.setTypeInfo((TypeInfo) metadata);
|
||||
} else if (metadata instanceof MethodCallInfo) {
|
||||
MethodCallInfo callInfo = (MethodCallInfo) metadata;
|
||||
if (callInfo.isConstructor()) {
|
||||
token.setTypeInfo(callInfo.getReceiverType());
|
||||
token.setMethodInfo(callInfo.getResolvedMethod());
|
||||
}
|
||||
token.setMethodCallInfo(callInfo);
|
||||
} else if (metadata instanceof FieldInfo.ArgInfo) {
|
||||
FieldInfo.ArgInfo ctx = (FieldInfo.ArgInfo) metadata;
|
||||
token.setFieldInfo(ctx.fieldInfo);
|
||||
token.setMethodCallInfo(ctx.methodCallInfo);
|
||||
} else if (metadata instanceof FieldInfo) {
|
||||
token.setFieldInfo((FieldInfo) metadata);
|
||||
} else if (metadata instanceof MethodInfo) {
|
||||
token.setMethodInfo((MethodInfo) metadata);
|
||||
} else if (metadata instanceof ImportData) {
|
||||
token.setImportData((ImportData) metadata);
|
||||
} else if (metadata instanceof FieldAccessInfo) {
|
||||
FieldAccessInfo accessInfo = (FieldAccessInfo) metadata;
|
||||
token.setFieldAccessInfo(accessInfo);
|
||||
token.setFieldInfo(accessInfo.getResolvedField());
|
||||
} else if (metadata instanceof TokenErrorMessage) {
|
||||
token.setErrorMessage((TokenErrorMessage) metadata);
|
||||
}
|
||||
}
|
||||
// ==================== INDENT GUIDES ====================
|
||||
|
||||
/**
|
||||
* Clear indent guides for this line.
|
||||
*/
|
||||
public void clearIndentGuides() {
|
||||
indentGuides.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an indent guide at the specified column.
|
||||
*/
|
||||
public void addIndentGuide(int column) {
|
||||
if (!indentGuides.contains(column)) {
|
||||
indentGuides.add(column);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== RENDERING ====================
|
||||
|
||||
/**
|
||||
* Draw this line with syntax highlighting using Minecraft color codes.
|
||||
* Compatible with the existing rendering system.
|
||||
* Also draws curly underlines for tokens with errors (method call validation failures).
|
||||
* Supports bold (§l) and italic (§o) formatting for certain token types.
|
||||
*/
|
||||
public void drawString(int x, int y, int defaultColor) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int lastIndex = 0;
|
||||
|
||||
// Track positions for underlines
|
||||
int currentX = x;
|
||||
|
||||
for (Token t : tokens) {
|
||||
int tokenStart = t.getGlobalStart() - globalStart; // relative position in line
|
||||
int tokenWidth = ClientProxy.Font.width(t.getText());
|
||||
|
||||
// Calculate gap width before this token
|
||||
if (tokenStart > lastIndex && tokenStart <= text.length()) {
|
||||
String gapText = text.substring(lastIndex, tokenStart);
|
||||
builder.append(gapText);
|
||||
currentX += ClientProxy.Font.width(gapText);
|
||||
}
|
||||
|
||||
|
||||
// Append style codes (bold/italic) if applicable
|
||||
String stylePrefix = t.getStylePrefix();
|
||||
|
||||
// Append the colored token with style
|
||||
builder.append(COLOR_CHAR)
|
||||
.append(t.getColorCode())
|
||||
.append(stylePrefix)
|
||||
.append(t.getText())
|
||||
.append(COLOR_CHAR)
|
||||
.append('r'); // Reset all formatting (color + bold/italic)
|
||||
|
||||
currentX += tokenWidth;
|
||||
lastIndex = tokenStart + t.getText().length();
|
||||
}
|
||||
|
||||
// Append any remaining text after the last token
|
||||
if (lastIndex < text.length()) {
|
||||
builder.append(text.substring(lastIndex));
|
||||
}
|
||||
|
||||
// Draw the text
|
||||
ClientProxy.Font.drawString(builder.toString(), x, y, defaultColor);
|
||||
// Minecraft.getMinecraft().fontRenderer.drawString(builder.toString(), x, y, defaultColor);
|
||||
|
||||
|
||||
// Draw underlines for validation errors (method calls and field accesses)
|
||||
drawErrorUnderlines(x, y + ClientProxy.Font.height() - 1);
|
||||
}
|
||||
/**
|
||||
* Draw this line with hex colors instead of Minecraft color codes.
|
||||
* More flexible but requires custom font rendering.
|
||||
* Also draws wavy underlines for tokens with errors.
|
||||
*/
|
||||
public void drawStringHex(int x, int y) {
|
||||
// Build the complete text with all tokens and gaps, draw it as ONE string
|
||||
// to match the spacing behavior of drawString which draws everything at once
|
||||
StringBuilder fullText = new StringBuilder();
|
||||
int lastIndex = 0;
|
||||
|
||||
// Build segments with token position info
|
||||
java.util.List<TextSegment> segments = new java.util.ArrayList<>();
|
||||
|
||||
for (Token t : tokens) {
|
||||
int tokenStart = t.getGlobalStart() - globalStart;
|
||||
|
||||
// Add any gap before this token
|
||||
if (tokenStart > lastIndex && tokenStart <= text.length()) {
|
||||
String gap = text.substring(lastIndex, tokenStart);
|
||||
segments.add(new TextSegment(fullText.length(), gap, 0xFFFFFFFF, false));
|
||||
fullText.append(gap);
|
||||
}
|
||||
|
||||
// Add the colored token
|
||||
String styledText = t.getStylePrefix() + t.getText();
|
||||
segments.add(new TextSegment(fullText.length(), styledText, t.getHexColor(), true));
|
||||
fullText.append(styledText);
|
||||
|
||||
lastIndex = tokenStart + styledText.length();
|
||||
}
|
||||
|
||||
// Add any remaining text after the last token
|
||||
if (lastIndex < text.length()) {
|
||||
String remaining = text.substring(lastIndex);
|
||||
segments.add(new TextSegment(fullText.length(), remaining, 0xFFFFFFFF, false));
|
||||
fullText.append(remaining);
|
||||
}
|
||||
|
||||
// Draw each segment at the correct position
|
||||
// Calculate positions based on the full string to match drawString's spacing
|
||||
for (TextSegment seg : segments) {
|
||||
if (!seg.text.isEmpty()) {
|
||||
// Get the width of everything before this segment
|
||||
String prefix = fullText.substring(0, seg.startPos);
|
||||
int prefixWidth = ClientProxy.Font.width(prefix);
|
||||
int color = (seg.color & 0xFF000000) == 0 ? (0xFF000000 | seg.color) : seg.color;
|
||||
|
||||
// Draw this segment at the correct position
|
||||
ClientProxy.Font.drawString(seg.text, x + prefixWidth, y, color);
|
||||
// Minecraft.getMinecraft().fontRenderer.drawString(seg.text, x + prefixWidth, y, color);
|
||||
}
|
||||
}
|
||||
|
||||
drawErrorUnderlines(x, y + ClientProxy.Font.height() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw underlines for all validation errors (method calls and field accesses) that intersect this line.
|
||||
* Delegates to ErrorUnderlineRenderer for the actual rendering logic.
|
||||
*/
|
||||
private void drawErrorUnderlines(int lineStartX, int baselineY) {
|
||||
if (parent == null)
|
||||
return;
|
||||
|
||||
ErrorUnderlineRenderer.drawErrorUnderlines(
|
||||
parent,
|
||||
lineStartX,
|
||||
baselineY,
|
||||
getText(),
|
||||
getGlobalStart(),
|
||||
getGlobalEnd()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Helper class to track text segments
|
||||
private static class TextSegment {
|
||||
final int startPos;
|
||||
final String text;
|
||||
final int color;
|
||||
final boolean isToken;
|
||||
|
||||
TextSegment(int startPos, String text, int color, boolean isToken) {
|
||||
this.startPos = startPos;
|
||||
this.text = text;
|
||||
this.color = color;
|
||||
this.isToken = isToken;
|
||||
}
|
||||
}
|
||||
// ==================== UTILITIES ====================
|
||||
|
||||
/**
|
||||
* Check if a global position falls within this line.
|
||||
*/
|
||||
public boolean containsPosition(int globalPosition) {
|
||||
return globalPosition >= globalStart && globalPosition < globalEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a global position to a column (local offset) within this line.
|
||||
*/
|
||||
public int toColumn(int globalPosition) {
|
||||
return Math.max(0, Math.min(globalPosition - globalStart, text.length()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a column (local offset) to a global position.
|
||||
*/
|
||||
public int toGlobal(int column) {
|
||||
return globalStart + Math.max(0, Math.min(column, text.length()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ScriptLine{" + lineIndex + ": '" + text + "' [" + globalStart + "-" + globalEnd + "], " + tokens.size() + " tokens}";
|
||||
}
|
||||
|
||||
// ==================== MARK CLASS (for token building) ====================
|
||||
|
||||
/**
|
||||
* A simple mark representing a highlighted region.
|
||||
* Used during token building phase.
|
||||
* Can optionally carry metadata (TypeInfo, FieldInfo, MethodInfo, ImportData).
|
||||
*/
|
||||
public static class Mark {
|
||||
public final int start;
|
||||
public final int end;
|
||||
public final TokenType type;
|
||||
public final Object metadata;
|
||||
|
||||
public Mark(int start, int end, TokenType type) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.type = type;
|
||||
this.metadata = null;
|
||||
}
|
||||
|
||||
public Mark(int start, int end, TokenType type, Object metadata) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.type = type;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Mark{" + type + " [" + start + "-" + end + "]" + (metadata != null ? " " + metadata.getClass().getSimpleName() : "") + "}";
|
||||
}
|
||||
}
|
||||
}
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter;
|
||||
|
||||
import noppes.npcs.client.ClientProxy;
|
||||
import noppes.npcs.client.gui.util.script.JavaTextContainer;
|
||||
import noppes.npcs.constants.ScriptContext;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* New interpreter-based text container that replaces JavaTextContainer.
|
||||
*
|
||||
* This class provides the same interface as JavaTextContainer but uses the
|
||||
* new modular interpreter system internally. It produces compatible LineData
|
||||
* objects for seamless integration with GuiScriptTextArea.
|
||||
*
|
||||
* Key differences from JavaTextContainer:
|
||||
* - Clean single-pass tokenization with 7 distinct phases
|
||||
* - Proper token navigation via prev()/next() methods
|
||||
* - Type-specific metadata on tokens (TypeInfo, FieldInfo, MethodInfo)
|
||||
* - Better separation of concerns (TypeResolver, ScriptDocument, ScriptLine)
|
||||
* - Hex color support in addition to Minecraft color codes
|
||||
*/
|
||||
public class ScriptTextContainer extends JavaTextContainer {
|
||||
|
||||
/**
|
||||
* Feature flag to enable/disable the new interpreter system.
|
||||
* When false, falls back to original JavaTextContainer behavior.
|
||||
*/
|
||||
public static boolean USE_NEW_INTERPRETER = true;
|
||||
|
||||
private ScriptDocument document;
|
||||
|
||||
/** The scripting language: "ECMAScript", "Groovy", etc. */
|
||||
private String language = "ECMAScript";
|
||||
|
||||
/** The script context: NPC, PLAYER, BLOCK, ITEM, etc. */
|
||||
private ScriptContext scriptContext = ScriptContext.GLOBAL;
|
||||
|
||||
/** Editor-injected globals (name -> type name) */
|
||||
private Map<String, String> editorGlobals = Collections.emptyMap();
|
||||
|
||||
public ScriptTextContainer(String text) {
|
||||
super(text);
|
||||
if (USE_NEW_INTERPRETER) {
|
||||
document = new ScriptDocument(text);
|
||||
}
|
||||
}
|
||||
|
||||
public ScriptTextContainer(String text, String language) {
|
||||
super(text);
|
||||
this.language = language != null ? language : "ECMAScript";
|
||||
if (USE_NEW_INTERPRETER) {
|
||||
document = new ScriptDocument(text, this.language);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the scripting language. This affects syntax highlighting and type inference.
|
||||
* @param language The language name (e.g., "ECMAScript", "Groovy")
|
||||
*/
|
||||
public void setLanguage(String language) {
|
||||
this.language = language != null ? language : "ECMAScript";
|
||||
if (document != null) {
|
||||
document.setLanguage(this.language);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current scripting language.
|
||||
*/
|
||||
public String getLanguage() {
|
||||
return language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the script context (NPC, PLAYER, BLOCK, ITEM, etc.).
|
||||
* This determines which hooks and event types are available for autocomplete.
|
||||
*
|
||||
* @param context The script context
|
||||
*/
|
||||
public void setScriptContext(ScriptContext context) {
|
||||
this.scriptContext = context != null ? context : ScriptContext.GLOBAL;
|
||||
if (document != null) {
|
||||
document.setScriptContext(this.scriptContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current script context.
|
||||
*
|
||||
* @return The script context (NPC, PLAYER, BLOCK, ITEM, etc.)
|
||||
*/
|
||||
public ScriptContext getScriptContext() {
|
||||
return scriptContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set editor-injected globals (name -> type name).
|
||||
*/
|
||||
public void setEditorGlobalsMap(java.util.Map<String, String> globals) {
|
||||
this.editorGlobals = globals != null ? globals : java.util.Collections.emptyMap();
|
||||
if (document != null) {
|
||||
document.setEditorGlobals(this.editorGlobals);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add implicit imports that should be resolved without explicit import statements.
|
||||
* Used for JaninoScript default imports and hook parameter types.
|
||||
*
|
||||
* @param patterns Array of import patterns to add (wildcard packages like "noppes.npcs.api.*" or FQ class names)
|
||||
*/
|
||||
public void addImplicitImports(String... patterns) {
|
||||
if (document != null) {
|
||||
document.addImplicitImports(patterns);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(int width, int height) {
|
||||
if (!USE_NEW_INTERPRETER) {
|
||||
super.init(width, height);
|
||||
return;
|
||||
}
|
||||
|
||||
lineHeight = ClientProxy.Font.height();
|
||||
if (lineHeight == 0) lineHeight = 12;
|
||||
|
||||
// Initialize the document
|
||||
document.setText(text);
|
||||
document.init(width, height);
|
||||
|
||||
// Convert ScriptLines to LineData for compatibility
|
||||
rebuildLineData();
|
||||
|
||||
linesCount = lines.size();
|
||||
totalHeight = linesCount * lineHeight;
|
||||
visibleLines = Math.max(height / lineHeight - 1, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize with explicit text (called when text changes).
|
||||
*/
|
||||
@Override
|
||||
public void init(String text, int width, int height) {
|
||||
this.text = text == null ? "" : text.replaceAll("\\r?\\n|\\r", "\n");
|
||||
|
||||
if (!USE_NEW_INTERPRETER) {
|
||||
super.init(text, width, height);
|
||||
return;
|
||||
}
|
||||
|
||||
if (document == null) {
|
||||
document = new ScriptDocument(this.text, this.language);
|
||||
document.setScriptContext(this.scriptContext);
|
||||
document.setEditorGlobals(this.editorGlobals);
|
||||
}
|
||||
|
||||
init(width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert ScriptDocument lines to the legacy LineData format.
|
||||
*/
|
||||
private void rebuildLineData() {
|
||||
lines.clear();
|
||||
for (ScriptLine scriptLine : document.getLines()) {
|
||||
LineData ld = new LineData(
|
||||
scriptLine.getText(),
|
||||
scriptLine.getGlobalStart(),
|
||||
scriptLine.getGlobalEnd()
|
||||
);
|
||||
|
||||
// Copy indent guides
|
||||
ld.indentCols.addAll(scriptLine.getIndentGuides());
|
||||
|
||||
// Convert tokens - use fully qualified name to avoid conflict with inherited Token
|
||||
for (noppes.npcs.client.gui.util.script.interpreter.token.Token interpreterToken : scriptLine.getTokens()) {
|
||||
ld.tokens.add(new JavaTextContainer.Token(
|
||||
interpreterToken.getText(),
|
||||
toLegacyTokenType(interpreterToken.getType()),
|
||||
interpreterToken.getGlobalStart(),
|
||||
interpreterToken.getGlobalEnd()
|
||||
));
|
||||
}
|
||||
|
||||
lines.add(ld);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main formatting entry point - matches JavaTextContainer signature.
|
||||
*/
|
||||
@Override
|
||||
public void formatCodeText() {
|
||||
if (!USE_NEW_INTERPRETER) {
|
||||
super.formatCodeText();
|
||||
return;
|
||||
}
|
||||
|
||||
document.formatCodeText();
|
||||
rebuildLineData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert new interpreter TokenType to legacy JavaTextContainer.TokenType format.
|
||||
*/
|
||||
private JavaTextContainer.TokenType toLegacyTokenType(
|
||||
noppes.npcs.client.gui.util.script.interpreter.token.TokenType type) {
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.COMMENT) return JavaTextContainer.TokenType.COMMENT;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.STRING) return JavaTextContainer.TokenType.STRING;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.CLASS_KEYWORD) return JavaTextContainer.TokenType.CLASS_KEYWORD;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.IMPORT_KEYWORD) return JavaTextContainer.TokenType.IMPORT_KEYWORD;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.KEYWORD) return JavaTextContainer.TokenType.KEYWORD;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.MODIFIER) return JavaTextContainer.TokenType.MODIFIER;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.INTERFACE_DECL) return JavaTextContainer.TokenType.INTERFACE_DECL;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.ENUM_DECL) return JavaTextContainer.TokenType.ENUM_DECL;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.CLASS_DECL) return JavaTextContainer.TokenType.CLASS_DECL;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.IMPORTED_CLASS) return JavaTextContainer.TokenType.IMPORTED_CLASS;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.TYPE_DECL) return JavaTextContainer.TokenType.TYPE_DECL;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.METHOD_DECL) return JavaTextContainer.TokenType.METHOD_DECARE;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.METHOD_CALL) return JavaTextContainer.TokenType.METHOD_CALL;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.LITERAL) return JavaTextContainer.TokenType.NUMBER;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.GLOBAL_FIELD) return JavaTextContainer.TokenType.GLOBAL_FIELD;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.LOCAL_FIELD) return JavaTextContainer.TokenType.LOCAL_FIELD;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.PARAMETER) return JavaTextContainer.TokenType.PARAMETER;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.UNDEFINED_VAR) return JavaTextContainer.TokenType.UNDEFINED_VAR;
|
||||
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.VARIABLE) return JavaTextContainer.TokenType.VARIABLE;
|
||||
return JavaTextContainer.TokenType.DEFAULT;
|
||||
}
|
||||
|
||||
// ==================== ACCESSORS ====================
|
||||
|
||||
/**
|
||||
* Get the underlying ScriptDocument for advanced operations.
|
||||
*/
|
||||
public ScriptDocument getDocument() {
|
||||
return document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the interpreter Token at a specific global position in the text.
|
||||
* Returns null if no token is at that position or if the interpreter is disabled.
|
||||
*
|
||||
* @param globalPosition Position in the document text
|
||||
* @return The Token at that position, or null
|
||||
*/
|
||||
public noppes.npcs.client.gui.util.script.interpreter.token.Token getInterpreterTokenAt(int globalPosition) {
|
||||
if (!USE_NEW_INTERPRETER || document == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ScriptLine line = document.getLineAt(globalPosition);
|
||||
if (line == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return line.getTokenAt(globalPosition);
|
||||
}
|
||||
}
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.bridge;
|
||||
|
||||
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.JSTypeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class DtsJavaBridge {
|
||||
|
||||
/** Cache of reflection Method -> matching JS method info (for docs/param names). */
|
||||
private static final Map<Method, JSMethodInfo> METHOD_CACHE = new ConcurrentHashMap<>();
|
||||
/** Cache of reflection Field -> matching JS field info (for docs/typing). */
|
||||
private static final Map<Field, JSFieldInfo> FIELD_CACHE = new ConcurrentHashMap<>();
|
||||
/** Cache of reflection Method -> resolved Java TypeInfo override from .d.ts. */
|
||||
private static final Map<Method, TypeInfo> RETURN_OVERRIDE_CACHE = new ConcurrentHashMap<>();
|
||||
/** Negative cache for methods with no usable override. */
|
||||
private static final Set<Method> RETURN_OVERRIDE_MISSES = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private DtsJavaBridge() {}
|
||||
|
||||
public static void clearCache() {
|
||||
METHOD_CACHE.clear();
|
||||
FIELD_CACHE.clear();
|
||||
RETURN_OVERRIDE_CACHE.clear();
|
||||
RETURN_OVERRIDE_MISSES.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a Java return type override for a reflected method using its .d.ts twin.
|
||||
*
|
||||
* The override is used only for editor typing (e.g., Janino hover/autocomplete) and
|
||||
* never changes runtime reflection. This returns a Java-resolved TypeInfo when the
|
||||
* .d.ts return type can be mapped to a concrete Java class.
|
||||
*/
|
||||
public static TypeInfo resolveReturnTypeOverride(Method method, TypeInfo containingType, JSMethodInfo jsMethod) {
|
||||
if (method == null || jsMethod == null) return null;
|
||||
|
||||
TypeInfo cached = RETURN_OVERRIDE_CACHE.get(method);
|
||||
if (cached != null) return cached;
|
||||
if (RETURN_OVERRIDE_MISSES.contains(method)) return null;
|
||||
|
||||
String jsReturnType = jsMethod.getReturnType();
|
||||
if (jsReturnType == null || jsReturnType.isEmpty()) {
|
||||
RETURN_OVERRIDE_MISSES.add(method);
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalizedJsReturn = jsReturnType;
|
||||
if (normalizedJsReturn.contains("|")) {
|
||||
normalizedJsReturn = normalizedJsReturn.split("\\|")[0].trim();
|
||||
}
|
||||
normalizedJsReturn = normalizedJsReturn.replace("?", "").trim();
|
||||
|
||||
// Fast-path: skip resolver if the .d.ts return matches reflection (common case).
|
||||
Class<?> reflectedReturn = method.getReturnType();
|
||||
if (reflectedReturn != null) {
|
||||
if (normalizedJsReturn.equals(reflectedReturn.getName())
|
||||
|| normalizedJsReturn.equals(reflectedReturn.getSimpleName())
|
||||
|| normalizedJsReturn.equals("Java." + reflectedReturn.getName())) {
|
||||
RETURN_OVERRIDE_MISSES.add(method);
|
||||
return null;
|
||||
}
|
||||
|
||||
String lower = normalizedJsReturn.toLowerCase();
|
||||
if (("void".equals(lower) && (reflectedReturn == void.class || reflectedReturn == Void.class))
|
||||
|| ("boolean".equals(lower) && (reflectedReturn == boolean.class || reflectedReturn == Boolean.class))
|
||||
|| ("string".equals(lower) && reflectedReturn == String.class)
|
||||
|| ("number".equals(lower) && (reflectedReturn == double.class || reflectedReturn == Double.class))) {
|
||||
RETURN_OVERRIDE_MISSES.add(method);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
TypeResolver resolver = TypeResolver.getInstance();
|
||||
TypeInfo resolved = resolver.resolveJSType(normalizedJsReturn);
|
||||
TypeInfo javaResolved = null;
|
||||
|
||||
if (resolved != null) {
|
||||
if (resolved.getJavaClass() != null) {
|
||||
javaResolved = resolved;
|
||||
} else if (resolved.isJSType() && resolved.getJSTypeInfo() != null) {
|
||||
String javaFqn = resolved.getJSTypeInfo().getJavaFqn();
|
||||
if (javaFqn != null && !javaFqn.isEmpty()) {
|
||||
javaResolved = resolver.resolveFullName(javaFqn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (javaResolved == null || javaResolved.getJavaClass() == null) {
|
||||
RETURN_OVERRIDE_MISSES.add(method);
|
||||
return null;
|
||||
}
|
||||
|
||||
RETURN_OVERRIDE_CACHE.put(method, javaResolved);
|
||||
return javaResolved;
|
||||
}
|
||||
|
||||
public static JSMethodInfo findMatchingMethod(Method method, TypeInfo containingType) {
|
||||
JSMethodInfo cached = METHOD_CACHE.get(method);
|
||||
if (cached != null) return cached;
|
||||
|
||||
JSTypeInfo jsType = findJSTypeInfo(method, containingType);
|
||||
if (jsType == null) return null;
|
||||
|
||||
List<JSMethodInfo> overloads = jsType.getMethodOverloads(method.getName());
|
||||
if (overloads.isEmpty()) return null;
|
||||
|
||||
Class<?>[] paramTypes = method.getParameterTypes();
|
||||
JSMethodInfo best = null;
|
||||
int bestScore = -1;
|
||||
|
||||
for (JSMethodInfo candidate : overloads) {
|
||||
if (candidate.getParameterCount() != paramTypes.length) continue;
|
||||
int score = scoreOverload(candidate, paramTypes, containingType, method.isVarArgs());
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
best = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestScore >= 0 && best != null) {
|
||||
METHOD_CACHE.put(method, best);
|
||||
return best;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static JSFieldInfo findMatchingField(Field field, TypeInfo containingType) {
|
||||
JSFieldInfo cached = FIELD_CACHE.get(field);
|
||||
if (cached != null) return cached;
|
||||
|
||||
JSTypeInfo jsType = findJSTypeInfo(field, containingType);
|
||||
if (jsType == null) return null;
|
||||
JSFieldInfo match = jsType.getField(field.getName());
|
||||
if (match != null) {
|
||||
FIELD_CACHE.put(field, match);
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
private static int scoreOverload(JSMethodInfo method, Class<?>[] paramTypes, TypeInfo containingType, boolean isVarArgs) {
|
||||
int score = 0;
|
||||
List<JSMethodInfo.JSParameterInfo> jsParams = method.getParameters();
|
||||
for (int i = 0; i < paramTypes.length; i++) {
|
||||
int paramScore = scoreParam(paramTypes[i], jsParams.get(i), containingType, isVarArgs, i == paramTypes.length - 1);
|
||||
if (paramScore < 0) return -1;
|
||||
score += paramScore;
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
private static int scoreParam(Class<?> javaParam, JSMethodInfo.JSParameterInfo jsParam, TypeInfo containingType, boolean isVarArgs, boolean isLastParam) {
|
||||
if (javaParam == null || jsParam == null) return -1;
|
||||
|
||||
TypeInfo resolved = jsParam.getResolvedType(containingType);
|
||||
if (resolved != null && resolved.getJavaClass() != null) {
|
||||
Class<?> jsClass = resolved.getJavaClass();
|
||||
if (javaParam.equals(jsClass)) return 4;
|
||||
if (jsClass.isAssignableFrom(javaParam)) return 3;
|
||||
if (javaParam.isAssignableFrom(jsClass)) return 2;
|
||||
}
|
||||
|
||||
String jsTypeName = jsParam.getType();
|
||||
if (jsTypeName == null || jsTypeName.isEmpty()) return -1;
|
||||
|
||||
if (isVarArgs && isLastParam && javaParam.isArray()) {
|
||||
if (matchesArrayElement(javaParam.getComponentType(), jsTypeName, containingType)) {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchesPrimitive(jsTypeName, javaParam)) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (javaParam.isArray() && jsTypeName.endsWith("[]")) {
|
||||
String elementType = jsTypeName.substring(0, jsTypeName.length() - 2);
|
||||
return matchesArrayElement(javaParam.getComponentType(), elementType, containingType) ? 2 : -1;
|
||||
}
|
||||
|
||||
JSTypeInfo jsType = resolveJSTypeInfo(jsTypeName);
|
||||
if (jsType != null && jsType.getJavaFqn() != null) {
|
||||
String javaFqn = normalizeJavaFqn(jsType.getJavaFqn());
|
||||
String paramFqn = normalizeJavaFqn(javaParam.getName());
|
||||
if (javaFqn.equals(paramFqn)) return 3;
|
||||
}
|
||||
|
||||
if (jsTypeName.equals(javaParam.getSimpleName())) return 1;
|
||||
if (jsTypeName.equals(javaParam.getName())) return 1;
|
||||
if ("any".equals(jsTypeName)) return 1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static boolean matchesArrayElement(Class<?> elementClass, String jsElementType, TypeInfo containingType) {
|
||||
if (elementClass == null) return false;
|
||||
if (matchesPrimitive(jsElementType, elementClass)) return true;
|
||||
|
||||
JSTypeInfo jsType = resolveJSTypeInfo(jsElementType);
|
||||
if (jsType != null && jsType.getJavaFqn() != null) {
|
||||
String javaFqn = normalizeJavaFqn(jsType.getJavaFqn());
|
||||
String elementFqn = normalizeJavaFqn(elementClass.getName());
|
||||
return javaFqn.equals(elementFqn);
|
||||
}
|
||||
|
||||
return jsElementType.equals(elementClass.getSimpleName()) || jsElementType.equals(elementClass.getName());
|
||||
}
|
||||
|
||||
private static boolean matchesPrimitive(String jsTypeName, Class<?> javaParam) {
|
||||
switch (jsTypeName.toLowerCase()) {
|
||||
case "string":
|
||||
case "char":
|
||||
return javaParam == String.class || javaParam == char.class || javaParam == Character.class;
|
||||
case "boolean":
|
||||
case "bool":
|
||||
return javaParam == boolean.class || javaParam == Boolean.class;
|
||||
case "number":
|
||||
case "byte":
|
||||
case "short":
|
||||
case "int":
|
||||
case "integer":
|
||||
case "long":
|
||||
case "float":
|
||||
case "double":
|
||||
return isNumberType(javaParam);
|
||||
case "void":
|
||||
return javaParam == void.class || javaParam == Void.class;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isNumberType(Class<?> type) {
|
||||
if (type == null) return false;
|
||||
return type == byte.class || type == short.class || type == int.class || type == long.class
|
||||
|| type == float.class || type == double.class
|
||||
|| Number.class.isAssignableFrom(type);
|
||||
}
|
||||
|
||||
private static JSTypeInfo findJSTypeInfo(Method method, TypeInfo containingType) {
|
||||
if (containingType != null && containingType.isJSType()) {
|
||||
return containingType.getJSTypeInfo();
|
||||
}
|
||||
|
||||
Class<?> javaClass = containingType != null ? containingType.getJavaClass() : method.getDeclaringClass();
|
||||
return resolveJSTypeInfo(javaClass);
|
||||
}
|
||||
|
||||
private static JSTypeInfo findJSTypeInfo(Field field, TypeInfo containingType) {
|
||||
if (containingType != null && containingType.isJSType()) {
|
||||
return containingType.getJSTypeInfo();
|
||||
}
|
||||
|
||||
Class<?> javaClass = containingType != null ? containingType.getJavaClass() : field.getDeclaringClass();
|
||||
return resolveJSTypeInfo(javaClass);
|
||||
}
|
||||
|
||||
private static JSTypeInfo resolveJSTypeInfo(Class<?> javaClass) {
|
||||
if (javaClass == null) return null;
|
||||
|
||||
JSTypeInfo direct = resolveJSTypeInfo(javaClass.getName());
|
||||
if (direct != null) return direct;
|
||||
|
||||
for (Class<?> iface : javaClass.getInterfaces()) {
|
||||
JSTypeInfo ifaceType = resolveJSTypeInfo(iface.getName());
|
||||
if (ifaceType != null) return ifaceType;
|
||||
}
|
||||
|
||||
Class<?> superClass = javaClass.getSuperclass();
|
||||
if (superClass != null && superClass != Object.class) {
|
||||
return resolveJSTypeInfo(superClass);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static JSTypeInfo resolveJSTypeInfo(String javaFqnOrType) {
|
||||
if (javaFqnOrType == null || javaFqnOrType.isEmpty()) return null;
|
||||
if (javaFqnOrType.startsWith("Java.")) {
|
||||
javaFqnOrType = javaFqnOrType.substring(5);
|
||||
}
|
||||
JSTypeRegistry registry = TypeResolver.getInstance().getJSTypeRegistry();
|
||||
JSTypeInfo direct = registry.getTypeByJavaFqn(javaFqnOrType);
|
||||
if (direct != null) return direct;
|
||||
|
||||
String normalized = normalizeJavaFqn(javaFqnOrType);
|
||||
if (!normalized.equals(javaFqnOrType)) {
|
||||
JSTypeInfo normalizedType = registry.getTypeByJavaFqn(normalized);
|
||||
if (normalizedType != null) return normalizedType;
|
||||
}
|
||||
|
||||
return registry.getType(javaFqnOrType);
|
||||
}
|
||||
|
||||
private static String normalizeJavaFqn(String javaFqn) {
|
||||
if (javaFqn == null) return null;
|
||||
return javaFqn.replace('$', '.');
|
||||
}
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.expression;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Helper class for resolving cast expressions and distinguishing them from parenthesized expressions.
|
||||
* Handles:
|
||||
* - Simple casts: (Type) expr
|
||||
* - Nested casts: ((Type) expr)
|
||||
* - Cast chains: ((Type) expr).method()
|
||||
* - Parenthesized expressions: (expr)
|
||||
*
|
||||
* @Deprecated ExpressionTypeResolver API has all this built in.
|
||||
*/
|
||||
@Deprecated
|
||||
public class CastExpressionResolver {
|
||||
|
||||
/**
|
||||
* Resolve a cast or parenthesized expression.
|
||||
*
|
||||
* @param expr The expression starting with '('
|
||||
* @param position The position in the source text
|
||||
* @param resolveType Function to resolve type names to TypeInfo
|
||||
* @param resolveExpression Function to resolve arbitrary expressions
|
||||
* @param parseChain Function to parse expression chain
|
||||
* @param resolveChainSegment Function to resolve chain segments
|
||||
* @return The resolved type
|
||||
*/
|
||||
public static <T> TypeInfo resolveCastOrParenthesizedExpression(
|
||||
String expr,
|
||||
int position,
|
||||
Function<String, TypeInfo> resolveType,
|
||||
TypeResolver resolveExpression,
|
||||
ChainParser<T> parseChain,
|
||||
ChainSegmentResolver<T> resolveChainSegment) {
|
||||
|
||||
if (!expr.startsWith("(")) return null;
|
||||
|
||||
// Find the matching closing parenthesis for the first open paren
|
||||
int depth = 0;
|
||||
int closeParen = -1;
|
||||
boolean inString = false;
|
||||
char stringChar = 0;
|
||||
|
||||
for (int i = 0; i < expr.length(); i++) {
|
||||
char c = expr.charAt(i);
|
||||
|
||||
// Handle string literals
|
||||
if ((c == '"' || c == '\'') && (i == 0 || expr.charAt(i - 1) != '\\')) {
|
||||
if (!inString) {
|
||||
inString = true;
|
||||
stringChar = c;
|
||||
} else if (c == stringChar) {
|
||||
inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inString) continue;
|
||||
|
||||
if (c == '(') {
|
||||
depth++;
|
||||
} else if (c == ')') {
|
||||
depth--;
|
||||
if (depth == 0) {
|
||||
closeParen = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (closeParen < 0) return null;
|
||||
|
||||
// Extract content inside parens
|
||||
String insideParens = expr.substring(1, closeParen).trim();
|
||||
String afterParens = expr.substring(closeParen + 1).trim();
|
||||
|
||||
// Check if this is a cast expression
|
||||
// A cast has a type name inside the parens (starts with uppercase or is primitive)
|
||||
// and is followed by an expression (not an operator or nothing)
|
||||
if (looksLikeCastType(insideParens) && looksLikeExpressionStart(afterParens)) {
|
||||
// This is a cast: (Type) rest
|
||||
TypeInfo castType = resolveType.apply(insideParens);
|
||||
if (castType != null && castType.isResolved()) {
|
||||
// If there's more after the cast target, resolve the chain
|
||||
// E.g., ((EntityPlayer) entity).worldObj
|
||||
if (!afterParens.isEmpty()) {
|
||||
// Check if afterParens starts with '.' for a method/field chain
|
||||
if (afterParens.startsWith(".")) {
|
||||
// Continue resolving the chain from the cast type
|
||||
String chainExpr = afterParens.substring(1).trim();
|
||||
List<T> segments = parseChain.parse(chainExpr);
|
||||
TypeInfo currentType = castType;
|
||||
for (T segment : segments) {
|
||||
currentType = resolveChainSegment.resolve(currentType, segment);
|
||||
if (currentType == null) return null;
|
||||
}
|
||||
return currentType;
|
||||
}
|
||||
}
|
||||
return castType;
|
||||
}
|
||||
}
|
||||
|
||||
// Not a cast - treat as parenthesized expression
|
||||
// Could be ((expr).method()) or just (expr)
|
||||
|
||||
// First, resolve what's inside the parens
|
||||
TypeInfo innerType = resolveExpression.resolve(insideParens, position);
|
||||
|
||||
// If there's a chain after the parens, continue resolving
|
||||
if (innerType != null && !afterParens.isEmpty() && afterParens.startsWith(".")) {
|
||||
String chainExpr = afterParens.substring(1).trim();
|
||||
List<T> segments = parseChain.parse(chainExpr);
|
||||
TypeInfo currentType = innerType;
|
||||
for (T segment : segments) {
|
||||
currentType = resolveChainSegment.resolve(currentType, segment);
|
||||
if (currentType == null) return null;
|
||||
}
|
||||
return currentType;
|
||||
}
|
||||
|
||||
return innerType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string looks like a valid type name for a cast.
|
||||
* Valid: EntityPlayer, int, String, java.lang.String, int[]
|
||||
*/
|
||||
public static boolean looksLikeCastType(String s) {
|
||||
if (s == null || s.isEmpty()) return false;
|
||||
s = s.trim();
|
||||
|
||||
// Handle array types: Type[] or Type[][]
|
||||
while (s.endsWith("[]")) {
|
||||
s = s.substring(0, s.length() - 2).trim();
|
||||
}
|
||||
|
||||
if (s.isEmpty()) return false;
|
||||
|
||||
// Primitive types
|
||||
switch (s) {
|
||||
case "byte": case "short": case "int": case "long":
|
||||
case "float": case "double": case "char": case "boolean":
|
||||
return true;
|
||||
}
|
||||
|
||||
// Class type: starts with uppercase or is fully qualified
|
||||
if (Character.isUpperCase(s.charAt(0))) return true;
|
||||
|
||||
// Fully qualified name: contains dots and ends with uppercase segment
|
||||
if (s.contains(".")) {
|
||||
String[] parts = s.split("\\.");
|
||||
String lastPart = parts[parts.length - 1];
|
||||
return !lastPart.isEmpty() && Character.isUpperCase(lastPart.charAt(0));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string looks like the start of an expression (what follows a cast).
|
||||
* Valid: identifier, (, new, literals, this, etc.
|
||||
* Invalid: empty, operators like +, -, etc.
|
||||
*/
|
||||
public static boolean looksLikeExpressionStart(String s) {
|
||||
if (s == null || s.isEmpty()) return false;
|
||||
s = s.trim();
|
||||
if (s.isEmpty()) return false;
|
||||
|
||||
char first = s.charAt(0);
|
||||
|
||||
// Parenthesis (nested cast or parenthesized expr)
|
||||
if (first == '(') return true;
|
||||
|
||||
// Identifier or keyword
|
||||
if (Character.isJavaIdentifierStart(first)) return true;
|
||||
|
||||
// String or char literal
|
||||
if (first == '"' || first == '\'') return true;
|
||||
|
||||
// Number literal (could start with digit or dot for .5)
|
||||
if (Character.isDigit(first)) return true;
|
||||
if (first == '.' && s.length() > 1 && Character.isDigit(s.charAt(1))) return true;
|
||||
|
||||
// Unary operators followed by expression
|
||||
if (first == '!' || first == '~') return true;
|
||||
if ((first == '+' || first == '-') && s.length() > 1) {
|
||||
// Could be unary +/- or ++/--
|
||||
char second = s.charAt(1);
|
||||
if (second == first || Character.isDigit(second) || Character.isJavaIdentifierStart(second) || second == '(') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional interface for resolving type expressions.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TypeResolver {
|
||||
TypeInfo resolve(String expr, int position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional interface for parsing expression chains.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ChainParser<T> {
|
||||
List<T> parse(String expr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional interface for resolving chain segments.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ChainSegmentResolver<T> {
|
||||
TypeInfo resolve(TypeInfo currentType, T segment);
|
||||
}
|
||||
}
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.expression;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class ExpressionNode {
|
||||
protected final int start;
|
||||
protected final int end;
|
||||
|
||||
protected ExpressionNode(int start, int end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
public int getStart() { return start; }
|
||||
public int getEnd() { return end; }
|
||||
public abstract TypeInfo resolveType(TypeResolverContext resolver);
|
||||
|
||||
public static class IntLiteralNode extends ExpressionNode {
|
||||
private final String value;
|
||||
public IntLiteralNode(String value, int start, int end) { super(start, end); this.value = value; }
|
||||
public String getValue() { return value; }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("int"); }
|
||||
}
|
||||
|
||||
public static class LongLiteralNode extends ExpressionNode {
|
||||
public LongLiteralNode(String value, int start, int end) { super(start, end); }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("long"); }
|
||||
}
|
||||
|
||||
public static class FloatLiteralNode extends ExpressionNode {
|
||||
public FloatLiteralNode(String value, int start, int end) { super(start, end); }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("float"); }
|
||||
}
|
||||
|
||||
public static class DoubleLiteralNode extends ExpressionNode {
|
||||
public DoubleLiteralNode(String value, int start, int end) { super(start, end); }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("double"); }
|
||||
}
|
||||
|
||||
public static class BooleanLiteralNode extends ExpressionNode {
|
||||
public BooleanLiteralNode(boolean value, int start, int end) { super(start, end); }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("boolean"); }
|
||||
}
|
||||
|
||||
public static class CharLiteralNode extends ExpressionNode {
|
||||
public CharLiteralNode(String value, int start, int end) { super(start, end); }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("char"); }
|
||||
}
|
||||
|
||||
public static class StringLiteralNode extends ExpressionNode {
|
||||
private final String value;
|
||||
public StringLiteralNode(String value, int start, int end) { super(start, end); this.value = value; }
|
||||
public String getValue() { return value; }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.string(); }
|
||||
}
|
||||
|
||||
public static class NullLiteralNode extends ExpressionNode {
|
||||
public NullLiteralNode(int start, int end) { super(start, end); }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) {
|
||||
return TypeInfo.unresolved("null", "<null>");
|
||||
}
|
||||
}
|
||||
|
||||
public static class IdentifierNode extends ExpressionNode {
|
||||
private final String name;
|
||||
public IdentifierNode(String name, int start, int end) { super(start, end); this.name = name; }
|
||||
public String getName() { return name; }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) { return resolver.resolveIdentifier(name); }
|
||||
}
|
||||
|
||||
public static class MemberAccessNode extends ExpressionNode {
|
||||
private final ExpressionNode target;
|
||||
private final String memberName;
|
||||
public MemberAccessNode(ExpressionNode target, String memberName, int start, int end) {
|
||||
super(start, end); this.target = target; this.memberName = memberName;
|
||||
}
|
||||
public ExpressionNode getTarget() { return target; }
|
||||
public String getMemberName() { return memberName; }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) {
|
||||
TypeInfo targetType = target.resolveType(resolver);
|
||||
if (targetType == null || !targetType.isResolved()) return null;
|
||||
return resolver.resolveMemberAccess(targetType, memberName);
|
||||
}
|
||||
}
|
||||
|
||||
public static class MethodCallNode extends ExpressionNode {
|
||||
private final ExpressionNode target;
|
||||
private final String methodName;
|
||||
private final List<ExpressionNode> arguments;
|
||||
public MethodCallNode(ExpressionNode target, String methodName, List<ExpressionNode> arguments, int start, int end) {
|
||||
super(start, end); this.target = target; this.methodName = methodName;
|
||||
this.arguments = arguments != null ? new ArrayList<>(arguments) : new ArrayList<>();
|
||||
}
|
||||
public ExpressionNode getTarget() { return target; }
|
||||
public String getMethodName() { return methodName; }
|
||||
public List<ExpressionNode> getArguments() { return Collections.unmodifiableList(arguments); }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) {
|
||||
TypeInfo targetType = target != null ? target.resolveType(resolver) : null;
|
||||
TypeInfo[] argTypes = new TypeInfo[arguments.size()];
|
||||
for (int i = 0; i < arguments.size(); i++) argTypes[i] = arguments.get(i).resolveType(resolver);
|
||||
return resolver.resolveMethodCall(targetType, methodName, argTypes);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ArrayAccessNode extends ExpressionNode {
|
||||
private final ExpressionNode array;
|
||||
private final ExpressionNode index;
|
||||
public ArrayAccessNode(ExpressionNode array, ExpressionNode index, int start, int end) {
|
||||
super(start, end); this.array = array; this.index = index;
|
||||
}
|
||||
public ExpressionNode getArray() { return array; }
|
||||
public ExpressionNode getIndex() { return index; }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) {
|
||||
TypeInfo arrayType = array.resolveType(resolver);
|
||||
if (arrayType == null || !arrayType.isResolved()) return null;
|
||||
return resolver.resolveArrayAccess(arrayType);
|
||||
}
|
||||
}
|
||||
|
||||
public static class NewNode extends ExpressionNode {
|
||||
private final String typeName;
|
||||
private final List<ExpressionNode> arguments;
|
||||
public NewNode(String typeName, List<ExpressionNode> arguments, int start, int end) {
|
||||
super(start, end); this.typeName = typeName;
|
||||
this.arguments = arguments != null ? new ArrayList<>(arguments) : new ArrayList<>();
|
||||
}
|
||||
public String getTypeName() { return typeName; }
|
||||
public List<ExpressionNode> getArguments() { return Collections.unmodifiableList(arguments); }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) { return resolver.resolveTypeName(typeName); }
|
||||
}
|
||||
|
||||
public static class BinaryOpNode extends ExpressionNode {
|
||||
private final ExpressionNode left;
|
||||
private final OperatorType operator;
|
||||
private final ExpressionNode right;
|
||||
public BinaryOpNode(ExpressionNode left, OperatorType operator, ExpressionNode right, int start, int end) {
|
||||
super(start, end); this.left = left; this.operator = operator; this.right = right;
|
||||
}
|
||||
public ExpressionNode getLeft() { return left; }
|
||||
public OperatorType getOperator() { return operator; }
|
||||
public ExpressionNode getRight() { return right; }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) {
|
||||
TypeInfo leftType = left.resolveType(resolver);
|
||||
TypeInfo rightType = right.resolveType(resolver);
|
||||
return TypeRules.resolveBinaryOperatorType(operator, leftType, rightType);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnaryOpNode extends ExpressionNode {
|
||||
private final OperatorType operator;
|
||||
private final ExpressionNode operand;
|
||||
private final boolean prefix;
|
||||
public UnaryOpNode(OperatorType operator, ExpressionNode operand, boolean prefix, int start, int end) {
|
||||
super(start, end); this.operator = operator; this.operand = operand; this.prefix = prefix;
|
||||
}
|
||||
public OperatorType getOperator() { return operator; }
|
||||
public ExpressionNode getOperand() { return operand; }
|
||||
public boolean isPrefix() { return prefix; }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) {
|
||||
TypeInfo operandType = operand.resolveType(resolver);
|
||||
return TypeRules.resolveUnaryOperatorType(operator, operandType);
|
||||
}
|
||||
}
|
||||
|
||||
public static class TernaryNode extends ExpressionNode {
|
||||
private final ExpressionNode condition;
|
||||
private final ExpressionNode thenExpr;
|
||||
private final ExpressionNode elseExpr;
|
||||
public TernaryNode(ExpressionNode condition, ExpressionNode thenExpr, ExpressionNode elseExpr, int start, int end) {
|
||||
super(start, end); this.condition = condition; this.thenExpr = thenExpr; this.elseExpr = elseExpr;
|
||||
}
|
||||
public ExpressionNode getCondition() { return condition; }
|
||||
public ExpressionNode getThenExpr() { return thenExpr; }
|
||||
public ExpressionNode getElseExpr() { return elseExpr; }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) {
|
||||
TypeInfo thenType = thenExpr.resolveType(resolver);
|
||||
TypeInfo elseType = elseExpr.resolveType(resolver);
|
||||
return TypeRules.resolveTernaryType(thenType, elseType);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InstanceofNode extends ExpressionNode {
|
||||
private final ExpressionNode expression;
|
||||
private final String typeName;
|
||||
public InstanceofNode(ExpressionNode expression, String typeName, int start, int end) {
|
||||
super(start, end); this.expression = expression; this.typeName = typeName;
|
||||
}
|
||||
public ExpressionNode getExpression() { return expression; }
|
||||
public String getTypeName() { return typeName; }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("boolean"); }
|
||||
}
|
||||
|
||||
public static class CastNode extends ExpressionNode {
|
||||
private final String typeName;
|
||||
private final ExpressionNode expression;
|
||||
public CastNode(String typeName, ExpressionNode expression, int start, int end) {
|
||||
super(start, end); this.typeName = typeName; this.expression = expression;
|
||||
}
|
||||
public String getTypeName() { return typeName; }
|
||||
public ExpressionNode getExpression() { return expression; }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) { return resolver.resolveTypeName(typeName); }
|
||||
}
|
||||
|
||||
public static class AssignmentNode extends ExpressionNode {
|
||||
private final ExpressionNode target;
|
||||
private final OperatorType operator;
|
||||
private final ExpressionNode value;
|
||||
public AssignmentNode(ExpressionNode target, OperatorType operator, ExpressionNode value, int start, int end) {
|
||||
super(start, end); this.target = target; this.operator = operator; this.value = value;
|
||||
}
|
||||
public ExpressionNode getTarget() { return target; }
|
||||
public OperatorType getOperator() { return operator; }
|
||||
public ExpressionNode getValue() { return value; }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) { return target.resolveType(resolver); }
|
||||
}
|
||||
|
||||
public static class ParenthesizedNode extends ExpressionNode {
|
||||
private final ExpressionNode inner;
|
||||
public ParenthesizedNode(ExpressionNode inner, int start, int end) { super(start, end); this.inner = inner; }
|
||||
public ExpressionNode getInner() { return inner; }
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) { return inner.resolveType(resolver); }
|
||||
}
|
||||
|
||||
public static class LambdaNode extends ExpressionNode {
|
||||
private final List<String> parameterNames;
|
||||
private final ExpressionNode body;
|
||||
private final boolean isBlock;
|
||||
private noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope scopeRef;
|
||||
|
||||
public LambdaNode(List<String> parameterNames, ExpressionNode body, boolean isBlock, int start, int end) {
|
||||
super(start, end);
|
||||
this.parameterNames = parameterNames != null ? new ArrayList<>(parameterNames) : new ArrayList<>();
|
||||
this.body = body;
|
||||
this.isBlock = isBlock;
|
||||
this.scopeRef = null;
|
||||
}
|
||||
|
||||
public List<String> getParameterNames() { return Collections.unmodifiableList(parameterNames); }
|
||||
public ExpressionNode getBody() { return body; }
|
||||
public boolean isBlock() { return isBlock; }
|
||||
public noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope getScopeRef() { return scopeRef; }
|
||||
public void setScopeRef(noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope scope) {
|
||||
this.scopeRef = scope;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) {
|
||||
// Lambda type is determined by the expected functional interface type
|
||||
// See ExpressionTypeResolver.resolveLambdaType()
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (parameterNames.size() == 1 && !isBlock) {
|
||||
sb.append(parameterNames.get(0));
|
||||
} else {
|
||||
sb.append("(");
|
||||
sb.append(String.join(", ", parameterNames));
|
||||
sb.append(")");
|
||||
}
|
||||
sb.append(" -> ");
|
||||
if (isBlock) {
|
||||
sb.append("{ ... }");
|
||||
} else {
|
||||
sb.append(body);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static class JSFunctionNode extends ExpressionNode {
|
||||
private final String name; // Optional name for named function expressions
|
||||
private final List<String> parameterNames;
|
||||
private final String bodyText; // Raw body text (for block functions)
|
||||
private noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope scopeRef;
|
||||
|
||||
public JSFunctionNode(String name, List<String> parameterNames, String bodyText, int start, int end) {
|
||||
super(start, end);
|
||||
this.name = name;
|
||||
this.parameterNames = parameterNames != null ? new ArrayList<>(parameterNames) : new ArrayList<>();
|
||||
this.bodyText = bodyText;
|
||||
this.scopeRef = null;
|
||||
}
|
||||
|
||||
public String getName() { return name; }
|
||||
public List<String> getParameterNames() { return Collections.unmodifiableList(parameterNames); }
|
||||
public String getBodyText() { return bodyText; }
|
||||
public noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope getScopeRef() { return scopeRef; }
|
||||
public void setScopeRef(noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope scope) {
|
||||
this.scopeRef = scope;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) {
|
||||
// JS function type is determined by the expected functional interface type
|
||||
// See ExpressionTypeResolver.resolveJSFunctionType()
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("function");
|
||||
if (name != null && !name.isEmpty()) {
|
||||
sb.append(" ").append(name);
|
||||
}
|
||||
sb.append("(");
|
||||
sb.append(String.join(", ", parameterNames));
|
||||
sb.append(") { ");
|
||||
if (bodyText.length() > 30) {
|
||||
sb.append(bodyText.substring(0, 30)).append("...");
|
||||
} else {
|
||||
sb.append(bodyText);
|
||||
}
|
||||
sb.append(" }");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static class JSArrowNode extends ExpressionNode {
|
||||
private final List<String> parameterNames;
|
||||
private final ExpressionNode body;
|
||||
private final boolean isBlock;
|
||||
private InnerCallableScope scopeRef;
|
||||
|
||||
public JSArrowNode(List<String> parameterNames, ExpressionNode body, boolean isBlock, int start, int end) {
|
||||
super(start, end);
|
||||
this.parameterNames = parameterNames != null ? new ArrayList<>(parameterNames) : new ArrayList<>();
|
||||
this.body = body;
|
||||
this.isBlock = isBlock;
|
||||
}
|
||||
|
||||
public List<String> getParameterNames() { return Collections.unmodifiableList(parameterNames); }
|
||||
public ExpressionNode getBody() { return body; }
|
||||
public boolean isBlock() { return isBlock; }
|
||||
public InnerCallableScope getScopeRef() { return scopeRef; }
|
||||
public void setScopeRef(InnerCallableScope scope) {
|
||||
this.scopeRef = scope;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) {
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("(");
|
||||
sb.append(String.join(", ", parameterNames));
|
||||
sb.append(") => ");
|
||||
if (isBlock) {
|
||||
sb.append("{ ... }");
|
||||
} else if (body != null) {
|
||||
sb.append(body);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static class MethodReferenceNode extends ExpressionNode {
|
||||
private final ExpressionNode target; // Object or class name
|
||||
private final String methodName;
|
||||
private final boolean isStatic; // Whether it's Class::method vs obj::method
|
||||
|
||||
public MethodReferenceNode(ExpressionNode target, String methodName, boolean isStatic, int start, int end) {
|
||||
super(start, end);
|
||||
this.target = target;
|
||||
this.methodName = methodName;
|
||||
this.isStatic = isStatic;
|
||||
}
|
||||
|
||||
public ExpressionNode getTarget() { return target; }
|
||||
public String getMethodName() { return methodName; }
|
||||
public boolean isStatic() { return isStatic; }
|
||||
|
||||
@Override
|
||||
public TypeInfo resolveType(TypeResolverContext resolver) {
|
||||
// Method reference type is determined by the expected functional interface type
|
||||
// See ExpressionTypeResolver.resolveMethodReferenceType()
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return target + "::" + methodName;
|
||||
}
|
||||
}
|
||||
|
||||
public interface TypeResolverContext {
|
||||
TypeInfo resolveIdentifier(String name);
|
||||
TypeInfo resolveMemberAccess(TypeInfo targetType, String memberName);
|
||||
TypeInfo resolveMethodCall(TypeInfo targetType, String methodName, TypeInfo[] argTypes);
|
||||
TypeInfo resolveArrayAccess(TypeInfo arrayType);
|
||||
TypeInfo resolveTypeName(String typeName);
|
||||
}
|
||||
}
|
||||
+559
@@ -0,0 +1,559 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.expression;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ExpressionParser {
|
||||
private final List<ExpressionToken> tokens;
|
||||
private int pos;
|
||||
|
||||
public ExpressionParser(List<ExpressionToken> tokens) {
|
||||
this.tokens = tokens;
|
||||
this.pos = 0;
|
||||
}
|
||||
|
||||
public ExpressionNode parse() {
|
||||
if (tokens.isEmpty()) return null;
|
||||
return parseLambdaOrExpression();
|
||||
}
|
||||
|
||||
private ExpressionNode parseLambdaOrExpression() {
|
||||
// Check if this is a lambda: (params) -> or param ->
|
||||
// Look ahead to see if there's a -> after the next identifier/parens
|
||||
|
||||
int checkpoint = pos;
|
||||
List<String> paramNames = null;
|
||||
int lambdaStart = current().getStart();
|
||||
boolean isJSArrow = false;
|
||||
|
||||
// Try to parse parameter list
|
||||
if (check(ExpressionToken.TokenKind.IDENTIFIER)) {
|
||||
// Single param without parens
|
||||
String paramName = current().getText();
|
||||
advance();
|
||||
if (check(ExpressionToken.TokenKind.LAMBDA_ARROW)) {
|
||||
paramNames = new ArrayList<>();
|
||||
paramNames.add(paramName);
|
||||
} else if (check(ExpressionToken.TokenKind.JS_ARROW)) {
|
||||
paramNames = new ArrayList<>();
|
||||
paramNames.add(paramName);
|
||||
isJSArrow = true;
|
||||
} else {
|
||||
// Not a lambda, rewind
|
||||
pos = checkpoint;
|
||||
}
|
||||
} else if (check(ExpressionToken.TokenKind.LEFT_PAREN)) {
|
||||
// Parenthesized params: (a, b) or ()
|
||||
advance(); // consume '('
|
||||
paramNames = new ArrayList<>();
|
||||
|
||||
if (!check(ExpressionToken.TokenKind.RIGHT_PAREN)) {
|
||||
// Parse param list
|
||||
while (true) {
|
||||
if (!check(ExpressionToken.TokenKind.IDENTIFIER)) {
|
||||
// Not a valid lambda param list, rewind
|
||||
pos = checkpoint;
|
||||
paramNames = null;
|
||||
break;
|
||||
}
|
||||
paramNames.add(current().getText());
|
||||
advance();
|
||||
|
||||
if (check(ExpressionToken.TokenKind.COMMA)) {
|
||||
advance();
|
||||
} else if (check(ExpressionToken.TokenKind.RIGHT_PAREN)) {
|
||||
break;
|
||||
} else {
|
||||
// Invalid syntax, rewind
|
||||
pos = checkpoint;
|
||||
paramNames = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (paramNames != null) {
|
||||
if (check(ExpressionToken.TokenKind.RIGHT_PAREN)) {
|
||||
advance(); // consume ')'
|
||||
if (check(ExpressionToken.TokenKind.LAMBDA_ARROW)) {
|
||||
// Valid lambda!
|
||||
} else if (check(ExpressionToken.TokenKind.JS_ARROW)) {
|
||||
isJSArrow = true;
|
||||
} else {
|
||||
// Not a lambda, rewind
|
||||
pos = checkpoint;
|
||||
paramNames = null;
|
||||
}
|
||||
} else {
|
||||
// No closing paren, rewind
|
||||
pos = checkpoint;
|
||||
paramNames = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (paramNames != null) {
|
||||
// This is a lambda! Parse it
|
||||
advance(); // consume '->' or '=>'
|
||||
|
||||
// Parse body
|
||||
ExpressionNode body;
|
||||
boolean isBlock = false;
|
||||
int bodyEnd;
|
||||
|
||||
if (check(ExpressionToken.TokenKind.LEFT_BRACE)) {
|
||||
// Block lambda: { ... }
|
||||
isBlock = true;
|
||||
int braceStart = current().getStart();
|
||||
advance(); // consume '{'
|
||||
// Skip to matching '}'
|
||||
int depth = 1;
|
||||
while (depth > 0 && pos < tokens.size() && !check(ExpressionToken.TokenKind.EOF)) {
|
||||
if (check(ExpressionToken.TokenKind.LEFT_BRACE)) depth++;
|
||||
else if (check(ExpressionToken.TokenKind.RIGHT_BRACE)) depth--;
|
||||
if (depth > 0) advance();
|
||||
}
|
||||
bodyEnd = current().getEnd();
|
||||
if (check(ExpressionToken.TokenKind.RIGHT_BRACE)) {
|
||||
advance(); // consume '}'
|
||||
}
|
||||
body = new ExpressionNode.StringLiteralNode("<block>", braceStart, bodyEnd); // Placeholder
|
||||
} else {
|
||||
// Expression lambda
|
||||
body = parseExpressionInternal(0);
|
||||
if (body == null) {
|
||||
// Failed to parse body, return error recovery
|
||||
return null;
|
||||
}
|
||||
bodyEnd = body.getEnd();
|
||||
}
|
||||
|
||||
// Create lambda node (scopeRef will be set during type resolution)
|
||||
if (isJSArrow)
|
||||
return new ExpressionNode.JSArrowNode(paramNames, body, isBlock, lambdaStart, bodyEnd);
|
||||
return new ExpressionNode.LambdaNode(paramNames, body, isBlock, lambdaStart, bodyEnd);
|
||||
}
|
||||
|
||||
// Not a lambda, parse as regular expression
|
||||
return parseExpressionInternal(0);
|
||||
}
|
||||
|
||||
private ExpressionToken current() {
|
||||
if (pos >= tokens.size()) return tokens.get(tokens.size() - 1);
|
||||
return tokens.get(pos);
|
||||
}
|
||||
|
||||
private ExpressionToken advance() {
|
||||
ExpressionToken tok = current();
|
||||
if (pos < tokens.size()) pos++;
|
||||
return tok;
|
||||
}
|
||||
|
||||
private boolean check(ExpressionToken.TokenKind kind) { return current().getKind() == kind; }
|
||||
|
||||
private boolean match(ExpressionToken.TokenKind kind) {
|
||||
if (check(kind)) { advance(); return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
private ExpressionNode parseExpression(int minPrecedence) {
|
||||
// For top-level expressions (minPrecedence = 0), check for lambda first
|
||||
if (minPrecedence == 0) {
|
||||
return parseLambdaOrExpression();
|
||||
}
|
||||
|
||||
return parseExpressionInternal(minPrecedence);
|
||||
}
|
||||
|
||||
private ExpressionNode parseExpressionInternal(int minPrecedence) {
|
||||
ExpressionNode left = parsePrefixExpression();
|
||||
if (left == null) return null;
|
||||
|
||||
while (true) {
|
||||
if (check(ExpressionToken.TokenKind.QUESTION) && minPrecedence <= 2) {
|
||||
left = parseTernary(left);
|
||||
continue;
|
||||
}
|
||||
|
||||
OperatorType op = getCurrentBinaryOperator();
|
||||
if (op == null) break;
|
||||
|
||||
int precedence = op.getPrecedence();
|
||||
if (precedence < minPrecedence) break;
|
||||
|
||||
advance();
|
||||
|
||||
int nextMinPrecedence = (op.getAssociativity() == OperatorType.Associativity.LEFT)
|
||||
? precedence + 1 : precedence;
|
||||
|
||||
ExpressionNode right = parseExpression(nextMinPrecedence);
|
||||
if (right == null) break;
|
||||
|
||||
if (op == OperatorType.INSTANCEOF) {
|
||||
String typeName = extractTypeName(right);
|
||||
left = new ExpressionNode.InstanceofNode(left, typeName, left.getStart(), right.getEnd());
|
||||
} else if (op.isAssignment()) {
|
||||
left = new ExpressionNode.AssignmentNode(left, op, right, left.getStart(), right.getEnd());
|
||||
} else {
|
||||
left = new ExpressionNode.BinaryOpNode(left, op, right, left.getStart(), right.getEnd());
|
||||
}
|
||||
}
|
||||
|
||||
return parsePostfixExpression(left);
|
||||
}
|
||||
|
||||
private String extractTypeName(ExpressionNode node) {
|
||||
if (node instanceof ExpressionNode.IdentifierNode) {
|
||||
return ((ExpressionNode.IdentifierNode) node).getName();
|
||||
}
|
||||
if (node instanceof ExpressionNode.MemberAccessNode) {
|
||||
ExpressionNode.MemberAccessNode ma = (ExpressionNode.MemberAccessNode) node;
|
||||
return extractTypeName(ma.getTarget()) + "." + ma.getMemberName();
|
||||
}
|
||||
return "Object";
|
||||
}
|
||||
|
||||
private ExpressionNode parseTernary(ExpressionNode condition) {
|
||||
int start = condition.getStart();
|
||||
advance();
|
||||
ExpressionNode thenExpr = parseExpression(0);
|
||||
if (thenExpr == null) return condition;
|
||||
if (!match(ExpressionToken.TokenKind.COLON)) return condition;
|
||||
ExpressionNode elseExpr = parseExpression(2);
|
||||
if (elseExpr == null) return condition;
|
||||
return new ExpressionNode.TernaryNode(condition, thenExpr, elseExpr, start, elseExpr.getEnd());
|
||||
}
|
||||
|
||||
private OperatorType getCurrentBinaryOperator() {
|
||||
ExpressionToken tok = current();
|
||||
if (tok.getKind() == ExpressionToken.TokenKind.OPERATOR) {
|
||||
OperatorType op = tok.getOperatorType();
|
||||
if (op != null && (op.isBinary() || op.isAssignment())) return op;
|
||||
}
|
||||
if (tok.getKind() == ExpressionToken.TokenKind.INSTANCEOF) return OperatorType.INSTANCEOF;
|
||||
return null;
|
||||
}
|
||||
|
||||
private ExpressionNode parsePrefixExpression() {
|
||||
ExpressionToken tok = current();
|
||||
int start = tok.getStart();
|
||||
|
||||
// Check for function expression (JS only)
|
||||
if (tok.getKind() == ExpressionToken.TokenKind.FUNCTION) {
|
||||
return parseFunctionExpression();
|
||||
}
|
||||
|
||||
if (tok.getKind() == ExpressionToken.TokenKind.OPERATOR) {
|
||||
OperatorType op = tok.getOperatorType();
|
||||
if (op != null && isUnaryOperator(op)) {
|
||||
advance();
|
||||
ExpressionNode operand = parsePrefixExpression();
|
||||
if (operand == null) return null;
|
||||
OperatorType unaryOp = toUnaryOperator(op);
|
||||
return new ExpressionNode.UnaryOpNode(unaryOp, operand, true, start, operand.getEnd());
|
||||
}
|
||||
}
|
||||
|
||||
if (tok.getKind() == ExpressionToken.TokenKind.LEFT_PAREN) {
|
||||
return parseCastOrParenthesized();
|
||||
}
|
||||
|
||||
return parsePrimaryExpression();
|
||||
}
|
||||
|
||||
private ExpressionNode parseFunctionExpression() {
|
||||
int start = current().getStart();
|
||||
advance(); // consume 'function'
|
||||
|
||||
// Optional function name
|
||||
String functionName = null;
|
||||
if (check(ExpressionToken.TokenKind.IDENTIFIER)) {
|
||||
functionName = current().getText();
|
||||
advance();
|
||||
}
|
||||
|
||||
// Expect '('
|
||||
if (!check(ExpressionToken.TokenKind.LEFT_PAREN)) {
|
||||
return null; // Invalid syntax
|
||||
}
|
||||
advance(); // consume '('
|
||||
|
||||
// Parse parameters
|
||||
List<String> params = new ArrayList<>();
|
||||
while (!check(ExpressionToken.TokenKind.RIGHT_PAREN) && !check(ExpressionToken.TokenKind.EOF)) {
|
||||
if (!check(ExpressionToken.TokenKind.IDENTIFIER)) {
|
||||
return null; // Invalid parameter
|
||||
}
|
||||
params.add(current().getText());
|
||||
advance();
|
||||
|
||||
if (check(ExpressionToken.TokenKind.COMMA)) {
|
||||
advance();
|
||||
} else if (!check(ExpressionToken.TokenKind.RIGHT_PAREN)) {
|
||||
return null; // Expected comma or closing paren
|
||||
}
|
||||
}
|
||||
|
||||
if (!check(ExpressionToken.TokenKind.RIGHT_PAREN)) {
|
||||
return null; // Expected closing paren
|
||||
}
|
||||
advance(); // consume ')'
|
||||
|
||||
// Expect '{'
|
||||
if (!check(ExpressionToken.TokenKind.LEFT_BRACE)) {
|
||||
return null; // Invalid syntax
|
||||
}
|
||||
advance(); // consume '{'
|
||||
|
||||
// Collect body tokens until matching '}'
|
||||
StringBuilder bodyBuilder = new StringBuilder();
|
||||
int depth = 1;
|
||||
while (depth > 0 && !check(ExpressionToken.TokenKind.EOF)) {
|
||||
ExpressionToken bodyToken = current();
|
||||
if (bodyToken.getKind() == ExpressionToken.TokenKind.LEFT_BRACE) {
|
||||
depth++;
|
||||
} else if (bodyToken.getKind() == ExpressionToken.TokenKind.RIGHT_BRACE) {
|
||||
depth--;
|
||||
if (depth == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bodyBuilder.append(bodyToken.getText()).append(" ");
|
||||
advance();
|
||||
}
|
||||
|
||||
String bodyText = bodyBuilder.toString().trim();
|
||||
int end = current().getEnd();
|
||||
|
||||
if (!check(ExpressionToken.TokenKind.RIGHT_BRACE)) {
|
||||
return null; // Expected closing brace
|
||||
}
|
||||
advance(); // consume final '}'
|
||||
|
||||
// Find the corresponding InnerCallableScope from ScriptDocument
|
||||
// This will be set during type resolution when we have access to ScriptDocument
|
||||
return new ExpressionNode.JSFunctionNode(functionName, params, bodyText, start, end);
|
||||
}
|
||||
|
||||
private boolean isUnaryOperator(OperatorType op) {
|
||||
switch (op) {
|
||||
case ADD: case SUBTRACT: case LOGICAL_NOT: case BITWISE_NOT:
|
||||
case PRE_INCREMENT: case PRE_DECREMENT: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
private OperatorType toUnaryOperator(OperatorType op) {
|
||||
switch (op) {
|
||||
case ADD: return OperatorType.UNARY_PLUS;
|
||||
case SUBTRACT: return OperatorType.UNARY_MINUS;
|
||||
default: return op;
|
||||
}
|
||||
}
|
||||
|
||||
private ExpressionNode parseCastOrParenthesized() {
|
||||
int start = current().getStart();
|
||||
advance();
|
||||
|
||||
if (check(ExpressionToken.TokenKind.IDENTIFIER)) {
|
||||
String possibleType = current().getText();
|
||||
int savedPos = pos;
|
||||
advance();
|
||||
|
||||
while (check(ExpressionToken.TokenKind.DOT)) {
|
||||
advance();
|
||||
if (check(ExpressionToken.TokenKind.IDENTIFIER)) {
|
||||
possibleType += "." + current().getText();
|
||||
advance();
|
||||
}
|
||||
}
|
||||
|
||||
while (check(ExpressionToken.TokenKind.LEFT_BRACKET)) {
|
||||
advance();
|
||||
if (!match(ExpressionToken.TokenKind.RIGHT_BRACKET)) { pos = savedPos; break; }
|
||||
possibleType += "[]";
|
||||
}
|
||||
|
||||
if (check(ExpressionToken.TokenKind.RIGHT_PAREN)) {
|
||||
advance();
|
||||
if (canStartExpression()) {
|
||||
ExpressionNode expr = parsePrefixExpression();
|
||||
if (expr != null) {
|
||||
return new ExpressionNode.CastNode(possibleType, expr, start, expr.getEnd());
|
||||
}
|
||||
}
|
||||
}
|
||||
pos = savedPos;
|
||||
}
|
||||
|
||||
ExpressionNode inner = parseExpression(0);
|
||||
if (inner != null && match(ExpressionToken.TokenKind.RIGHT_PAREN)) {
|
||||
ExpressionNode paren = new ExpressionNode.ParenthesizedNode(inner, start, current().getStart());
|
||||
return parseAccessChain(paren);
|
||||
}
|
||||
return inner;
|
||||
}
|
||||
|
||||
private boolean canStartExpression() {
|
||||
ExpressionToken.TokenKind kind = current().getKind();
|
||||
switch (kind) {
|
||||
case IDENTIFIER: case INT_LITERAL: case LONG_LITERAL: case FLOAT_LITERAL:
|
||||
case DOUBLE_LITERAL: case BOOLEAN_LITERAL: case CHAR_LITERAL: case STRING_LITERAL:
|
||||
case NULL_LITERAL: case NEW: case LEFT_PAREN: case OPERATOR: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
private ExpressionNode parsePrimaryExpression() {
|
||||
ExpressionToken tok = current();
|
||||
int start = tok.getStart();
|
||||
|
||||
switch (tok.getKind()) {
|
||||
case INT_LITERAL: advance(); return new ExpressionNode.IntLiteralNode(tok.getText(), start, tok.getEnd());
|
||||
case LONG_LITERAL: advance(); return new ExpressionNode.LongLiteralNode(tok.getText(), start, tok.getEnd());
|
||||
case FLOAT_LITERAL: advance(); return new ExpressionNode.FloatLiteralNode(tok.getText(), start, tok.getEnd());
|
||||
case DOUBLE_LITERAL: advance(); return new ExpressionNode.DoubleLiteralNode(tok.getText(), start, tok.getEnd());
|
||||
case BOOLEAN_LITERAL: advance(); return new ExpressionNode.BooleanLiteralNode("true".equals(tok.getText()), start, tok.getEnd());
|
||||
case CHAR_LITERAL: advance(); return new ExpressionNode.CharLiteralNode(tok.getText(), start, tok.getEnd());
|
||||
case STRING_LITERAL: advance(); return new ExpressionNode.StringLiteralNode(tok.getText(), start, tok.getEnd());
|
||||
case NULL_LITERAL: advance(); return new ExpressionNode.NullLiteralNode(start, tok.getEnd());
|
||||
case NEW: return parseNewExpression();
|
||||
case IDENTIFIER: return parseIdentifierOrMethodCall();
|
||||
case LEFT_PAREN: return parseCastOrParenthesized();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ExpressionNode parseNewExpression() {
|
||||
int start = current().getStart();
|
||||
advance();
|
||||
if (!check(ExpressionToken.TokenKind.IDENTIFIER)) return null;
|
||||
|
||||
StringBuilder typeName = new StringBuilder(current().getText());
|
||||
advance();
|
||||
|
||||
while (check(ExpressionToken.TokenKind.DOT)) {
|
||||
advance();
|
||||
if (check(ExpressionToken.TokenKind.IDENTIFIER)) {
|
||||
typeName.append(".").append(current().getText());
|
||||
advance();
|
||||
}
|
||||
}
|
||||
|
||||
List<ExpressionNode> args = new ArrayList<>();
|
||||
if (match(ExpressionToken.TokenKind.LEFT_PAREN)) {
|
||||
args = parseArgumentList();
|
||||
match(ExpressionToken.TokenKind.RIGHT_PAREN);
|
||||
}
|
||||
|
||||
return new ExpressionNode.NewNode(typeName.toString(), args, start, current().getStart());
|
||||
}
|
||||
|
||||
private ExpressionNode parseIdentifierOrMethodCall() {
|
||||
int start = current().getStart();
|
||||
String name = current().getText();
|
||||
advance();
|
||||
|
||||
ExpressionNode result = new ExpressionNode.IdentifierNode(name, start, current().getStart());
|
||||
return parseAccessChain(result);
|
||||
}
|
||||
|
||||
private ExpressionNode parseAccessChain(ExpressionNode base) {
|
||||
while (true) {
|
||||
if (check(ExpressionToken.TokenKind.METHOD_REFERENCE)) {
|
||||
advance(); // consume '::'
|
||||
|
||||
if (!check(ExpressionToken.TokenKind.IDENTIFIER)) {
|
||||
// Invalid syntax, return what we have
|
||||
break;
|
||||
}
|
||||
|
||||
String methodName = current().getText();
|
||||
int end = current().getEnd();
|
||||
advance();
|
||||
|
||||
// isStatic is determined during type resolution, not parsing
|
||||
// We pass false as a placeholder - ExpressionTypeResolver will compute
|
||||
// the real value based on whether target resolves to a ClassTypeInfo
|
||||
base = new ExpressionNode.MethodReferenceNode(base, methodName, false, base.getStart(), end);
|
||||
// Method references don't chain further (can't do obj::method.something)
|
||||
break;
|
||||
} else if (check(ExpressionToken.TokenKind.LEFT_PAREN)) {
|
||||
advance();
|
||||
List<ExpressionNode> args = parseArgumentList();
|
||||
int end = current().getStart();
|
||||
match(ExpressionToken.TokenKind.RIGHT_PAREN);
|
||||
|
||||
String methodName;
|
||||
ExpressionNode target;
|
||||
|
||||
if (base instanceof ExpressionNode.IdentifierNode) {
|
||||
methodName = ((ExpressionNode.IdentifierNode) base).getName();
|
||||
target = null;
|
||||
} else if (base instanceof ExpressionNode.MemberAccessNode) {
|
||||
ExpressionNode.MemberAccessNode ma = (ExpressionNode.MemberAccessNode) base;
|
||||
methodName = ma.getMemberName();
|
||||
target = ma.getTarget();
|
||||
} else {
|
||||
methodName = "apply";
|
||||
target = base;
|
||||
}
|
||||
|
||||
base = new ExpressionNode.MethodCallNode(target, methodName, args, base.getStart(), end);
|
||||
} else if (check(ExpressionToken.TokenKind.DOT)) {
|
||||
advance();
|
||||
if (!check(ExpressionToken.TokenKind.IDENTIFIER)) break;
|
||||
String memberName = current().getText();
|
||||
int end = current().getEnd();
|
||||
advance();
|
||||
base = new ExpressionNode.MemberAccessNode(base, memberName, base.getStart(), end);
|
||||
} else if (check(ExpressionToken.TokenKind.LEFT_BRACKET)) {
|
||||
advance();
|
||||
ExpressionNode index = parseExpression(0);
|
||||
int end = current().getStart();
|
||||
match(ExpressionToken.TokenKind.RIGHT_BRACKET);
|
||||
if (index != null) {
|
||||
base = new ExpressionNode.ArrayAccessNode(base, index, base.getStart(), end);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
private List<ExpressionNode> parseArgumentList() {
|
||||
List<ExpressionNode> args = new ArrayList<>();
|
||||
if (check(ExpressionToken.TokenKind.RIGHT_PAREN)) return args;
|
||||
|
||||
ExpressionNode first = parseExpression(0);
|
||||
if (first != null) args.add(first);
|
||||
|
||||
while (match(ExpressionToken.TokenKind.COMMA)) {
|
||||
ExpressionNode arg = parseExpression(0);
|
||||
if (arg != null) args.add(arg);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
private ExpressionNode parsePostfixExpression(ExpressionNode expr) {
|
||||
if (expr == null) return null;
|
||||
|
||||
while (true) {
|
||||
ExpressionToken tok = current();
|
||||
if (tok.getKind() != ExpressionToken.TokenKind.OPERATOR) break;
|
||||
|
||||
OperatorType op = tok.getOperatorType();
|
||||
if (op == OperatorType.PRE_INCREMENT) {
|
||||
advance();
|
||||
expr = new ExpressionNode.UnaryOpNode(OperatorType.POST_INCREMENT, expr, false, expr.getStart(), tok.getEnd());
|
||||
} else if (op == OperatorType.PRE_DECREMENT) {
|
||||
advance();
|
||||
expr = new ExpressionNode.UnaryOpNode(OperatorType.POST_DECREMENT, expr, false, expr.getStart(), tok.getEnd());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.expression;
|
||||
|
||||
public class ExpressionToken {
|
||||
|
||||
public enum TokenKind {
|
||||
INT_LITERAL, LONG_LITERAL, FLOAT_LITERAL, DOUBLE_LITERAL,
|
||||
BOOLEAN_LITERAL, CHAR_LITERAL, STRING_LITERAL, NULL_LITERAL,
|
||||
IDENTIFIER, NEW, INSTANCEOF, FUNCTION,
|
||||
OPERATOR,
|
||||
LEFT_PAREN, RIGHT_PAREN, LEFT_BRACKET, RIGHT_BRACKET,
|
||||
LEFT_BRACE, RIGHT_BRACE,
|
||||
DOT, COMMA, QUESTION, COLON, SEMICOLON,
|
||||
LAMBDA_ARROW, JS_ARROW, METHOD_REFERENCE,
|
||||
EOF
|
||||
}
|
||||
|
||||
private final TokenKind kind;
|
||||
private final String text;
|
||||
private final int start;
|
||||
private final int end;
|
||||
private final OperatorType operatorType;
|
||||
|
||||
public ExpressionToken(TokenKind kind, String text, int start, int end) {
|
||||
this(kind, text, start, end, null);
|
||||
}
|
||||
|
||||
public ExpressionToken(TokenKind kind, String text, int start, int end, OperatorType operatorType) {
|
||||
this.kind = kind;
|
||||
this.text = text;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.operatorType = operatorType;
|
||||
}
|
||||
|
||||
public TokenKind getKind() { return kind; }
|
||||
public String getText() { return text; }
|
||||
public int getStart() { return start; }
|
||||
public int getEnd() { return end; }
|
||||
public OperatorType getOperatorType() { return operatorType; }
|
||||
|
||||
public static ExpressionToken operator(String symbol, int start, int end) {
|
||||
OperatorType op = OperatorType.fromBinarySymbol(symbol);
|
||||
if (op == null) op = OperatorType.fromSymbol(symbol);
|
||||
return new ExpressionToken(TokenKind.OPERATOR, symbol, start, end, op);
|
||||
}
|
||||
|
||||
public static ExpressionToken identifier(String name, int start, int end) {
|
||||
if ("true".equals(name) || "false".equals(name)) {
|
||||
return new ExpressionToken(TokenKind.BOOLEAN_LITERAL, name, start, end);
|
||||
}
|
||||
if ("null".equals(name)) {
|
||||
return new ExpressionToken(TokenKind.NULL_LITERAL, name, start, end);
|
||||
}
|
||||
if ("new".equals(name)) {
|
||||
return new ExpressionToken(TokenKind.NEW, name, start, end);
|
||||
}
|
||||
if ("instanceof".equals(name)) {
|
||||
return new ExpressionToken(TokenKind.INSTANCEOF, name, start, end);
|
||||
}
|
||||
if ("function".equals(name)) {
|
||||
return new ExpressionToken(TokenKind.FUNCTION, name, start, end);
|
||||
}
|
||||
return new ExpressionToken(TokenKind.IDENTIFIER, name, start, end);
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.expression;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ExpressionTokenizer {
|
||||
|
||||
public static List<ExpressionToken> tokenize(String expr) {
|
||||
List<ExpressionToken> tokens = new ArrayList<>();
|
||||
int pos = 0;
|
||||
int len = expr.length();
|
||||
|
||||
while (pos < len) {
|
||||
char c = expr.charAt(pos);
|
||||
|
||||
if (Character.isWhitespace(c)) { pos++; continue; }
|
||||
|
||||
if (c == '"') {
|
||||
int start = pos++;
|
||||
while (pos < len && expr.charAt(pos) != '"') {
|
||||
if (expr.charAt(pos) == '\\' && pos + 1 < len) pos++;
|
||||
pos++;
|
||||
}
|
||||
if (pos < len) pos++;
|
||||
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.STRING_LITERAL, expr.substring(start, pos), start, pos));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '\'') {
|
||||
int start = pos++;
|
||||
while (pos < len && expr.charAt(pos) != '\'') {
|
||||
if (expr.charAt(pos) == '\\' && pos + 1 < len) pos++;
|
||||
pos++;
|
||||
}
|
||||
if (pos < len) pos++;
|
||||
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.CHAR_LITERAL, expr.substring(start, pos), start, pos));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Character.isDigit(c) || (c == '.' && pos + 1 < len && Character.isDigit(expr.charAt(pos + 1)))) {
|
||||
int start = pos;
|
||||
if (c == '0' && pos + 1 < len) {
|
||||
char next = expr.charAt(pos + 1);
|
||||
if (next == 'x' || next == 'X') {
|
||||
pos += 2;
|
||||
while (pos < len && isHexDigit(expr.charAt(pos))) pos++;
|
||||
if (pos < len && (expr.charAt(pos) == 'L' || expr.charAt(pos) == 'l')) {
|
||||
pos++;
|
||||
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LONG_LITERAL, expr.substring(start, pos), start, pos));
|
||||
} else {
|
||||
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.INT_LITERAL, expr.substring(start, pos), start, pos));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (next == 'b' || next == 'B') {
|
||||
pos += 2;
|
||||
while (pos < len && (expr.charAt(pos) == '0' || expr.charAt(pos) == '1')) pos++;
|
||||
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.INT_LITERAL, expr.substring(start, pos), start, pos));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
while (pos < len && Character.isDigit(expr.charAt(pos))) pos++;
|
||||
boolean hasDecimal = false;
|
||||
if (pos < len && expr.charAt(pos) == '.') {
|
||||
if (pos + 1 < len && Character.isDigit(expr.charAt(pos + 1))) {
|
||||
hasDecimal = true;
|
||||
pos++;
|
||||
while (pos < len && Character.isDigit(expr.charAt(pos))) pos++;
|
||||
}
|
||||
}
|
||||
if (pos < len && (expr.charAt(pos) == 'e' || expr.charAt(pos) == 'E')) {
|
||||
pos++;
|
||||
if (pos < len && (expr.charAt(pos) == '+' || expr.charAt(pos) == '-')) pos++;
|
||||
while (pos < len && Character.isDigit(expr.charAt(pos))) pos++;
|
||||
hasDecimal = true;
|
||||
}
|
||||
|
||||
ExpressionToken.TokenKind kind = ExpressionToken.TokenKind.INT_LITERAL;
|
||||
if (pos < len) {
|
||||
char suffix = expr.charAt(pos);
|
||||
if (suffix == 'f' || suffix == 'F') { kind = ExpressionToken.TokenKind.FLOAT_LITERAL; pos++; }
|
||||
else if (suffix == 'd' || suffix == 'D') { kind = ExpressionToken.TokenKind.DOUBLE_LITERAL; pos++; }
|
||||
else if (suffix == 'L' || suffix == 'l') { kind = ExpressionToken.TokenKind.LONG_LITERAL; pos++; }
|
||||
else if (hasDecimal) kind = ExpressionToken.TokenKind.DOUBLE_LITERAL;
|
||||
} else if (hasDecimal) kind = ExpressionToken.TokenKind.DOUBLE_LITERAL;
|
||||
|
||||
tokens.add(new ExpressionToken(kind, expr.substring(start, pos), start, pos));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Character.isJavaIdentifierStart(c)) {
|
||||
int start = pos;
|
||||
while (pos < len && Character.isJavaIdentifierPart(expr.charAt(pos))) pos++;
|
||||
tokens.add(ExpressionToken.identifier(expr.substring(start, pos), start, pos));
|
||||
continue;
|
||||
}
|
||||
|
||||
int start = pos;
|
||||
String op = null;
|
||||
|
||||
// Check for method reference :: (before lambda arrow and single colon)
|
||||
if (pos + 1 < len && c == ':' && expr.charAt(pos + 1) == ':') {
|
||||
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.METHOD_REFERENCE, "::", pos, pos + 2));
|
||||
pos += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for lambda arrow (Java ->)
|
||||
if (pos + 1 < len && c == '-' && expr.charAt(pos + 1) == '>') {
|
||||
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LAMBDA_ARROW, "->", pos, pos + 2));
|
||||
pos += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for JS arrow (=>)
|
||||
if (pos + 1 < len && c == '=' && expr.charAt(pos + 1) == '>') {
|
||||
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.JS_ARROW, "=>", pos, pos + 2));
|
||||
pos += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pos + 3 <= len) {
|
||||
String s3 = expr.substring(pos, pos + 3);
|
||||
if (">>>".equals(s3) || "<<=".equals(s3) || ">>=".equals(s3)) op = s3;
|
||||
}
|
||||
if (op == null && pos + 4 <= len) {
|
||||
String s4 = expr.substring(pos, pos + 4);
|
||||
if (">>>=".equals(s4)) op = s4;
|
||||
}
|
||||
if (op == null && pos + 2 <= len) {
|
||||
String s2 = expr.substring(pos, pos + 2);
|
||||
if ("++".equals(s2) || "--".equals(s2) || "+=".equals(s2) || "-=".equals(s2) ||
|
||||
"*=".equals(s2) || "/=".equals(s2) || "%=".equals(s2) || "&=".equals(s2) ||
|
||||
"|=".equals(s2) || "^=".equals(s2) || "==".equals(s2) || "!=".equals(s2) ||
|
||||
"<=".equals(s2) || ">=".equals(s2) || "&&".equals(s2) || "||".equals(s2) ||
|
||||
"<<".equals(s2) || ">>".equals(s2)) op = s2;
|
||||
}
|
||||
if (op == null) {
|
||||
switch (c) {
|
||||
case '+': case '-': case '*': case '/': case '%': case '&': case '|':
|
||||
case '^': case '~': case '!': case '<': case '>': case '=':
|
||||
op = String.valueOf(c); break;
|
||||
}
|
||||
}
|
||||
|
||||
if (op != null) {
|
||||
pos += op.length();
|
||||
tokens.add(ExpressionToken.operator(op, start, pos));
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (c) {
|
||||
case '(': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LEFT_PAREN, "(", pos, pos + 1)); break;
|
||||
case ')': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.RIGHT_PAREN, ")", pos, pos + 1)); break;
|
||||
case '[': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LEFT_BRACKET, "[", pos, pos + 1)); break;
|
||||
case ']': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.RIGHT_BRACKET, "]", pos, pos + 1)); break;
|
||||
case '{': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LEFT_BRACE, "{", pos, pos + 1)); break;
|
||||
case '}': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.RIGHT_BRACE, "}", pos, pos + 1)); break;
|
||||
case '.': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.DOT, ".", pos, pos + 1)); break;
|
||||
case ',': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.COMMA, ",", pos, pos + 1)); break;
|
||||
case '?': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.QUESTION, "?", pos, pos + 1)); break;
|
||||
case ':': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.COLON, ":", pos, pos + 1)); break;
|
||||
case ';': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.SEMICOLON, ";", pos, pos + 1)); break;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.EOF, "", len, len));
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private static boolean isHexDigit(char c) {
|
||||
return Character.isDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||||
}
|
||||
}
|
||||
+949
@@ -0,0 +1,949 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.expression;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.ClassTypeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.OverloadSelector;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ExpressionTypeResolver {
|
||||
private final ExpressionNode.TypeResolverContext context;
|
||||
private final ScriptDocument document; // Access to document for inner scope lookups
|
||||
private final int basePosition; // Document offset for this expression
|
||||
|
||||
/**
|
||||
* Static field to track the expected/desired type for the current expression being resolved.
|
||||
* This allows TypeRules to validate type compatibility in context (e.g., ternary operator branches
|
||||
* must be compatible with the assignment target type).
|
||||
* Should be set before calling resolve() and cleared after.
|
||||
*/
|
||||
public static TypeInfo CURRENT_EXPECTED_TYPE = null;
|
||||
|
||||
public ExpressionTypeResolver(ExpressionNode.TypeResolverContext context) {
|
||||
this.context = context;
|
||||
this.document = null;
|
||||
this.basePosition = 0;
|
||||
}
|
||||
|
||||
public ExpressionTypeResolver(ExpressionNode.TypeResolverContext context, ScriptDocument document) {
|
||||
this(context, document, 0);
|
||||
}
|
||||
|
||||
public ExpressionTypeResolver(ExpressionNode.TypeResolverContext context, ScriptDocument document, int basePosition) {
|
||||
this.context = context;
|
||||
this.document = document;
|
||||
this.basePosition = basePosition;
|
||||
}
|
||||
|
||||
public TypeInfo resolve(String expression) {
|
||||
if (expression == null || expression.trim().isEmpty()) return null;
|
||||
|
||||
try {
|
||||
List<ExpressionToken> tokens = ExpressionTokenizer.tokenize(expression);
|
||||
if (tokens.isEmpty()) return null;
|
||||
|
||||
ExpressionParser parser = new ExpressionParser(tokens);
|
||||
ExpressionNode ast = parser.parse();
|
||||
if (ast == null) return null;
|
||||
|
||||
return resolveNodeType(ast);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean containsOperators(String expression) {
|
||||
if (expression == null) return false;
|
||||
for (int i = 0; i < expression.length(); i++) {
|
||||
char c = expression.charAt(i);
|
||||
switch (c) {
|
||||
case '+': case '-': case '*': case '/': case '%':
|
||||
case '&': case '|': case '^': case '~':
|
||||
case '<': case '>': case '=': case '!':
|
||||
case '?': case ':': return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private TypeInfo resolveNodeType(ExpressionNode node) {
|
||||
if (node == null) return null;
|
||||
|
||||
if (node instanceof ExpressionNode.IntLiteralNode) {
|
||||
if (CURRENT_EXPECTED_TYPE != null) {
|
||||
TypeInfo narrowed = TypeChecker.narrowLiteralToExpectedType(
|
||||
((ExpressionNode.IntLiteralNode) node).getValue(), CURRENT_EXPECTED_TYPE);
|
||||
if (narrowed != null) return narrowed;
|
||||
}
|
||||
return TypeInfo.fromPrimitive("int");
|
||||
}
|
||||
if (node instanceof ExpressionNode.LongLiteralNode) return TypeInfo.fromPrimitive("long");
|
||||
if (node instanceof ExpressionNode.FloatLiteralNode) return TypeInfo.fromPrimitive("float");
|
||||
if (node instanceof ExpressionNode.DoubleLiteralNode) return TypeInfo.fromPrimitive("double");
|
||||
if (node instanceof ExpressionNode.BooleanLiteralNode) return TypeInfo.fromPrimitive("boolean");
|
||||
if (node instanceof ExpressionNode.CharLiteralNode) return TypeInfo.fromPrimitive("char");
|
||||
if (node instanceof ExpressionNode.StringLiteralNode) return TypeInfo.string();
|
||||
if (node instanceof ExpressionNode.NullLiteralNode) return TypeInfo.unresolved("null", "<null>");
|
||||
|
||||
if (node instanceof ExpressionNode.IdentifierNode) {
|
||||
return context.resolveIdentifier(((ExpressionNode.IdentifierNode) node).getName());
|
||||
}
|
||||
|
||||
if (node instanceof ExpressionNode.MemberAccessNode) {
|
||||
ExpressionNode.MemberAccessNode ma = (ExpressionNode.MemberAccessNode) node;
|
||||
TypeInfo targetType = resolveNodeType(ma.getTarget());
|
||||
if (targetType == null || !targetType.isResolved()) return null;
|
||||
if (targetType == TypeInfo.ANY || "any".equals(targetType.getFullName())) return TypeInfo.ANY;
|
||||
return context.resolveMemberAccess(targetType, ma.getMemberName());
|
||||
}
|
||||
|
||||
if (node instanceof ExpressionNode.MethodCallNode) {
|
||||
ExpressionNode.MethodCallNode mc = (ExpressionNode.MethodCallNode) node;
|
||||
TypeInfo targetType = mc.getTarget() == null ? context.resolveIdentifier("this") : resolveNodeType(mc.getTarget());
|
||||
if (targetType == null || !targetType.isResolved()) return null;
|
||||
if (targetType == TypeInfo.ANY || "any".equals(targetType.getFullName())) return TypeInfo.ANY;
|
||||
TypeInfo[] argTypes = new TypeInfo[mc.getArguments().size()];
|
||||
for (int i = 0; i < argTypes.length; i++) argTypes[i] = resolveNodeType(mc.getArguments().get(i));
|
||||
return context.resolveMethodCall(targetType, mc.getMethodName(), argTypes);
|
||||
}
|
||||
|
||||
if (node instanceof ExpressionNode.ArrayAccessNode) {
|
||||
ExpressionNode.ArrayAccessNode aa = (ExpressionNode.ArrayAccessNode) node;
|
||||
TypeInfo arrayType = resolveNodeType(aa.getArray());
|
||||
if (arrayType == null || !arrayType.isResolved()) return null;
|
||||
if (arrayType == TypeInfo.ANY || "any".equals(arrayType.getFullName())) return TypeInfo.ANY;
|
||||
String typeName = arrayType.getFullName();
|
||||
if (typeName.endsWith("[]")) {
|
||||
String elementTypeName = typeName.substring(0, typeName.length() - 2);
|
||||
return context.resolveTypeName(elementTypeName);
|
||||
}
|
||||
return context.resolveArrayAccess(arrayType);
|
||||
}
|
||||
|
||||
if (node instanceof ExpressionNode.NewNode) {
|
||||
return context.resolveTypeName(((ExpressionNode.NewNode) node).getTypeName());
|
||||
}
|
||||
|
||||
if (node instanceof ExpressionNode.BinaryOpNode) {
|
||||
ExpressionNode.BinaryOpNode bin = (ExpressionNode.BinaryOpNode) node;
|
||||
TypeInfo leftType = resolveNodeType(bin.getLeft());
|
||||
TypeInfo rightType = resolveNodeType(bin.getRight());
|
||||
return TypeRules.resolveBinaryOperatorType(bin.getOperator(), leftType, rightType);
|
||||
}
|
||||
|
||||
if (node instanceof ExpressionNode.UnaryOpNode) {
|
||||
ExpressionNode.UnaryOpNode un = (ExpressionNode.UnaryOpNode) node;
|
||||
// Special case: negation of an IntLiteralNode can narrow to byte/short
|
||||
if (un.getOperator() == OperatorType.UNARY_MINUS
|
||||
&& un.getOperand() instanceof ExpressionNode.IntLiteralNode
|
||||
&& CURRENT_EXPECTED_TYPE != null) {
|
||||
String negatedText = "-" + ((ExpressionNode.IntLiteralNode) un.getOperand()).getValue();
|
||||
TypeInfo narrowed = TypeChecker.narrowLiteralToExpectedType(negatedText, CURRENT_EXPECTED_TYPE);
|
||||
if (narrowed != null) return narrowed;
|
||||
}
|
||||
TypeInfo operandType = resolveNodeType(un.getOperand());
|
||||
return TypeRules.resolveUnaryOperatorType(un.getOperator(), operandType);
|
||||
}
|
||||
|
||||
if (node instanceof ExpressionNode.TernaryNode) {
|
||||
ExpressionNode.TernaryNode tern = (ExpressionNode.TernaryNode) node;
|
||||
TypeInfo thenType = resolveNodeType(tern.getThenExpr());
|
||||
TypeInfo elseType = resolveNodeType(tern.getElseExpr());
|
||||
return TypeRules.resolveTernaryType(thenType, elseType);
|
||||
}
|
||||
|
||||
if (node instanceof ExpressionNode.CastNode) {
|
||||
return context.resolveTypeName(((ExpressionNode.CastNode) node).getTypeName());
|
||||
}
|
||||
|
||||
if (node instanceof ExpressionNode.InstanceofNode) {
|
||||
return TypeInfo.fromPrimitive("boolean");
|
||||
}
|
||||
|
||||
if (node instanceof ExpressionNode.AssignmentNode) {
|
||||
return resolveNodeType(((ExpressionNode.AssignmentNode) node).getTarget());
|
||||
}
|
||||
|
||||
if (node instanceof ExpressionNode.ParenthesizedNode) {
|
||||
return resolveNodeType(((ExpressionNode.ParenthesizedNode) node).getInner());
|
||||
}
|
||||
|
||||
if (node instanceof ExpressionNode.LambdaNode) {
|
||||
return resolveLambdaType((ExpressionNode.LambdaNode) node);
|
||||
}
|
||||
|
||||
if (node instanceof ExpressionNode.JSFunctionNode) {
|
||||
return resolveJSFunctionType((ExpressionNode.JSFunctionNode) node);
|
||||
}
|
||||
|
||||
if (node instanceof ExpressionNode.JSArrowNode) {
|
||||
return resolveJSArrowType((ExpressionNode.JSArrowNode) node);
|
||||
}
|
||||
|
||||
if (node instanceof ExpressionNode.MethodReferenceNode) {
|
||||
return resolveMethodReferenceType((ExpressionNode.MethodReferenceNode) node);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private TypeInfo resolveLambdaType(ExpressionNode.LambdaNode lambda) {
|
||||
// Lambda type is determined by the expected functional interface
|
||||
TypeInfo expectedType = CURRENT_EXPECTED_TYPE;
|
||||
|
||||
if (expectedType == null) {
|
||||
// No expected type context
|
||||
// Return a generic Object type
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
// Get SAM method from the expected functional interface
|
||||
MethodInfo sam = expectedType.getSingleAbstractMethod();
|
||||
if (sam != null && document != null) {
|
||||
// Find the InnerCallableScope for this lambda by matching position and parameters
|
||||
InnerCallableScope scope = findLambdaScopeByPosition(lambda.getStart(), lambda.getParameterNames());
|
||||
|
||||
if (scope != null) {
|
||||
scope.setExpectedType(expectedType);
|
||||
|
||||
// Extract and apply parameter types from SAM to lambda parameters
|
||||
List<FieldInfo> samParams = sam.getParameters();
|
||||
List<FieldInfo> lambdaParams = scope.getParameters();
|
||||
|
||||
if (samParams.size() == lambdaParams.size()) {
|
||||
for (int i = 0; i < lambdaParams.size(); i++) {
|
||||
FieldInfo lambdaParam = lambdaParams.get(i);
|
||||
TypeInfo inferredType = samParams.get(i).getTypeInfo();
|
||||
|
||||
// Set inferred type on the parameter
|
||||
if (inferredType != null) {
|
||||
lambdaParam.setInferredType(inferredType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Type check lambda body (if expression lambda)
|
||||
if (!lambda.isBlock() && lambda.getBody() != null) {
|
||||
// Resolve body type for validation
|
||||
TypeInfo bodyType = resolveNodeType(lambda.getBody());
|
||||
// The actual compatibility check will be done during marking phase
|
||||
}
|
||||
|
||||
return expectedType;
|
||||
}
|
||||
|
||||
// Fallback: check if it's a recognized functional interface by name
|
||||
String typeName = expectedType.getFullName();
|
||||
if (typeName != null && (typeName.contains("Function") || typeName.contains("Consumer") ||
|
||||
typeName.contains("Predicate") || typeName.contains("Supplier") ||
|
||||
typeName.equals("java.lang.Runnable"))) {
|
||||
// Update the scope reference if available
|
||||
if (lambda.getScopeRef() != null) {
|
||||
lambda.getScopeRef().setExpectedType(expectedType);
|
||||
}
|
||||
|
||||
return expectedType;
|
||||
}
|
||||
|
||||
// Not a recognized functional interface, return Object
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the InnerCallableScope for a lambda by matching position and parameter names.
|
||||
*
|
||||
* @param expressionRelativePos The start position of the lambda (relative to expression string)
|
||||
* @param paramNames The parameter names of the lambda
|
||||
* @return The matching InnerCallableScope, or null if not found
|
||||
*/
|
||||
private InnerCallableScope findLambdaScopeByPosition(int expressionRelativePos, List<String> paramNames) {
|
||||
if (document == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert expression-relative position to absolute document position
|
||||
int absolutePos = basePosition + expressionRelativePos;
|
||||
|
||||
for (InnerCallableScope scope : document.getInnerScopes()) {
|
||||
if (scope.getKind() == InnerCallableScope.Kind.JAVA_LAMBDA) {
|
||||
// Check if absolute position is in range
|
||||
if (absolutePos >= scope.getHeaderStart() && absolutePos < scope.getFullEnd()) {
|
||||
// Verify parameter names match
|
||||
if (scope.getParameters().size() == paramNames.size()) {
|
||||
boolean match = true;
|
||||
for (int i = 0; i < paramNames.size(); i++) {
|
||||
if (!scope.getParameters().get(i).getName().equals(paramNames.get(i))) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
return scope;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private TypeInfo resolveJSFunctionType(ExpressionNode.JSFunctionNode functionNode) {
|
||||
// JS function type is determined by the expected functional interface (if any)
|
||||
TypeInfo expectedType = CURRENT_EXPECTED_TYPE;
|
||||
|
||||
if (expectedType == null) {
|
||||
// No expected type - return generic Function type or Object
|
||||
// Try to resolve "Function" as a type from the context
|
||||
TypeInfo functionType = context.resolveTypeName("Function");
|
||||
if (functionType != null && functionType.isResolved()) {
|
||||
return functionType;
|
||||
}
|
||||
return TypeInfo.fromClass(Object.class); // Fallback
|
||||
}
|
||||
|
||||
// Get SAM method from the expected functional interface
|
||||
MethodInfo sam = expectedType.getSingleAbstractMethod();
|
||||
if (sam != null && document != null) {
|
||||
// Find the InnerCallableScope for this JS function by matching position and parameters
|
||||
InnerCallableScope scope = findJSFunctionScopeByPosition(functionNode.getStart(), functionNode.getParameterNames());
|
||||
|
||||
if (scope != null) {
|
||||
scope.setExpectedType(expectedType);
|
||||
|
||||
// Extract and apply parameter types from SAM to function parameters
|
||||
List<FieldInfo> samParams = sam.getParameters();
|
||||
List<FieldInfo> functionParams = scope.getParameters();
|
||||
|
||||
if (samParams.size() == functionParams.size()) {
|
||||
for (int i = 0; i < functionParams.size(); i++) {
|
||||
FieldInfo functionParam = functionParams.get(i);
|
||||
TypeInfo inferredType = samParams.get(i).getTypeInfo();
|
||||
|
||||
// Set inferred type on the parameter
|
||||
if (inferredType != null) {
|
||||
functionParam.setInferredType(inferredType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return expectedType;
|
||||
}
|
||||
|
||||
// Fallback: check if expected type is compatible with a function
|
||||
String typeName = expectedType.getFullName();
|
||||
if (typeName != null && (typeName.contains("Function") || typeName.contains("Consumer") ||
|
||||
typeName.contains("Predicate") || typeName.contains("Supplier") ||
|
||||
typeName.equals("java.lang.Runnable"))) {
|
||||
// This is likely a functional interface
|
||||
|
||||
// Update the InnerCallableScope with the expected type
|
||||
if (functionNode.getScopeRef() != null) {
|
||||
functionNode.getScopeRef().setExpectedType(expectedType);
|
||||
}
|
||||
|
||||
return expectedType;
|
||||
}
|
||||
|
||||
// Expected type is not a recognized functional interface
|
||||
// Check if it's a generic JS "Function" type expectation
|
||||
if (typeName != null && typeName.equals("Function")) {
|
||||
return expectedType;
|
||||
}
|
||||
|
||||
return TypeInfo.fromClass(Object.class); // Fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the InnerCallableScope for a JS function by matching position and parameter names.
|
||||
*
|
||||
* @param expressionRelativePos The start position of the function (relative to expression string)
|
||||
* @param paramNames The parameter names of the function
|
||||
* @return The matching InnerCallableScope, or null if not found
|
||||
*/
|
||||
private InnerCallableScope findJSFunctionScopeByPosition(int expressionRelativePos, List<String> paramNames) {
|
||||
if (document == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert expression-relative position to absolute document position
|
||||
int absolutePos = basePosition + expressionRelativePos;
|
||||
|
||||
for (InnerCallableScope scope : document.getInnerScopes()) {
|
||||
if (scope.getKind() == InnerCallableScope.Kind.JS_FUNCTION_EXPR) {
|
||||
// Check if absolute position is in range
|
||||
if (absolutePos >= scope.getHeaderStart() && absolutePos < scope.getFullEnd()) {
|
||||
// Verify parameter names match
|
||||
if (scope.getParameters().size() == paramNames.size()) {
|
||||
boolean match = true;
|
||||
for (int i = 0; i < paramNames.size(); i++) {
|
||||
if (!scope.getParameters().get(i).getName().equals(paramNames.get(i))) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
return scope;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private TypeInfo resolveJSArrowType(ExpressionNode.JSArrowNode arrowNode) {
|
||||
TypeInfo expectedType = CURRENT_EXPECTED_TYPE;
|
||||
|
||||
if (expectedType == null) {
|
||||
TypeInfo functionType = context.resolveTypeName("Function");
|
||||
if (functionType != null && functionType.isResolved()) {
|
||||
return functionType;
|
||||
}
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
MethodInfo sam = expectedType.getSingleAbstractMethod();
|
||||
if (sam != null && document != null) {
|
||||
InnerCallableScope scope = findJSArrowScopeByPosition(arrowNode.getStart(), arrowNode.getParameterNames());
|
||||
|
||||
if (scope != null) {
|
||||
scope.setExpectedType(expectedType);
|
||||
|
||||
List<FieldInfo> samParams = sam.getParameters();
|
||||
List<FieldInfo> arrowParams = scope.getParameters();
|
||||
|
||||
if (samParams.size() == arrowParams.size()) {
|
||||
for (int i = 0; i < arrowParams.size(); i++) {
|
||||
TypeInfo inferredType = samParams.get(i).getTypeInfo();
|
||||
if (inferredType != null) {
|
||||
arrowParams.get(i).setInferredType(inferredType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return expectedType;
|
||||
}
|
||||
|
||||
String typeName = expectedType.getFullName();
|
||||
if (typeName != null && (typeName.contains("Function") || typeName.contains("Consumer") ||
|
||||
typeName.contains("Predicate") || typeName.contains("Supplier") ||
|
||||
typeName.equals("java.lang.Runnable"))) {
|
||||
if (arrowNode.getScopeRef() != null) {
|
||||
arrowNode.getScopeRef().setExpectedType(expectedType);
|
||||
}
|
||||
return expectedType;
|
||||
}
|
||||
|
||||
if (typeName != null && typeName.equals("Function")) {
|
||||
return expectedType;
|
||||
}
|
||||
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
private InnerCallableScope findJSArrowScopeByPosition(int expressionRelativePos, List<String> paramNames) {
|
||||
if (document == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int absolutePos = basePosition + expressionRelativePos;
|
||||
|
||||
for (InnerCallableScope scope : document.getInnerScopes()) {
|
||||
if (scope.getKind() == InnerCallableScope.Kind.JS_ARROW_FUNC) {
|
||||
if (absolutePos >= scope.getHeaderStart() && absolutePos < scope.getFullEnd()) {
|
||||
if (scope.getParameters().size() == paramNames.size()) {
|
||||
boolean match = true;
|
||||
for (int i = 0; i < paramNames.size(); i++) {
|
||||
if (!scope.getParameters().get(i).getName().equals(paramNames.get(i))) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
return scope;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== Method Reference Resolution ====================
|
||||
|
||||
/**
|
||||
* Resolve the type of a method reference expression (target::methodName).
|
||||
*
|
||||
* Supports all Java method reference forms:
|
||||
* - this::method - Instance method on current object
|
||||
* - super::method - Instance method on superclass
|
||||
* - variable::method - Instance method on a variable's value
|
||||
* - ClassName::method - Static method OR unbound instance method
|
||||
* - pkg.ClassName::method - Fully qualified class reference
|
||||
* - ClassName::new - Constructor reference
|
||||
* - Type[]::new - Array constructor reference
|
||||
*
|
||||
* @param methodRef The method reference AST node
|
||||
* @return The resolved type (functional interface type if valid, Object if invalid)
|
||||
*/
|
||||
private TypeInfo resolveMethodReferenceType(ExpressionNode.MethodReferenceNode methodRef) {
|
||||
TypeInfo expectedType = CURRENT_EXPECTED_TYPE;
|
||||
|
||||
// Must have expected functional interface context
|
||||
if (expectedType == null || !expectedType.isFunctionalInterface()) {
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
MethodInfo sam = expectedType.getSingleAbstractMethod();
|
||||
if (sam == null) {
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
// Resolve target and determine if it's a class reference (static context)
|
||||
MethodReferenceTarget resolvedTarget = resolveMethodReferenceTarget(methodRef);
|
||||
if (resolvedTarget == null || resolvedTarget.type == null) {
|
||||
// Unresolved target - return expected type to allow forward references
|
||||
return expectedType;
|
||||
}
|
||||
|
||||
String methodName = methodRef.getMethodName();
|
||||
|
||||
// Handle constructor references (ClassName::new or Type[]::new)
|
||||
if ("new".equals(methodName)) {
|
||||
return resolveConstructorReference(resolvedTarget, sam, expectedType);
|
||||
}
|
||||
|
||||
// Handle method references
|
||||
return resolveMethodReference(resolvedTarget, methodName, sam, expectedType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates the resolved target of a method reference.
|
||||
*/
|
||||
private static class MethodReferenceTarget {
|
||||
final TypeInfo type; // The resolved type
|
||||
final boolean isClassRef; // True if target is a class reference (for static access)
|
||||
|
||||
MethodReferenceTarget(TypeInfo type, boolean isClassRef) {
|
||||
this.type = type;
|
||||
this.isClassRef = isClassRef;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the target (left side of ::) for a method reference.
|
||||
* Returns both the type and whether it's a class reference.
|
||||
*/
|
||||
private MethodReferenceTarget resolveMethodReferenceTarget(ExpressionNode.MethodReferenceNode methodRef) {
|
||||
ExpressionNode target = methodRef.getTarget();
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle simple identifiers: this, super, variable, ClassName
|
||||
if (target instanceof ExpressionNode.IdentifierNode) {
|
||||
String name = ((ExpressionNode.IdentifierNode) target).getName();
|
||||
return resolveSimpleTarget(name);
|
||||
}
|
||||
|
||||
// Handle qualified names: pkg.ClassName or outer.Inner
|
||||
if (target instanceof ExpressionNode.MemberAccessNode) {
|
||||
return resolveQualifiedTarget((ExpressionNode.MemberAccessNode) target);
|
||||
}
|
||||
|
||||
// Handle array type targets: Type[]
|
||||
if (target instanceof ExpressionNode.ArrayAccessNode) {
|
||||
TypeInfo arrayType = resolveNodeType(target);
|
||||
if (arrayType != null && arrayType.isResolved()) {
|
||||
// Array type is always a class reference (for Type[]::new)
|
||||
return new MethodReferenceTarget(arrayType, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: resolve as expression (e.g., (expr)::method)
|
||||
TypeInfo exprType = resolveNodeType(target);
|
||||
if (exprType != null && exprType.isResolved()) {
|
||||
// Expression results are always instance references
|
||||
return new MethodReferenceTarget(exprType, false);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a simple identifier as method reference target.
|
||||
*/
|
||||
private MethodReferenceTarget resolveSimpleTarget(String name) {
|
||||
// this::method - instance method on current object
|
||||
if ("this".equals(name)) {
|
||||
TypeInfo thisType = context.resolveIdentifier("this");
|
||||
return thisType != null ? new MethodReferenceTarget(thisType, false) : null;
|
||||
}
|
||||
|
||||
// super::method - instance method on superclass
|
||||
if ("super".equals(name)) {
|
||||
TypeInfo superType = context.resolveIdentifier("super");
|
||||
return superType != null ? new MethodReferenceTarget(superType, false) : null;
|
||||
}
|
||||
|
||||
// Try as type name first (ClassName::method)
|
||||
TypeInfo typeInfo = context.resolveTypeName(name);
|
||||
if (typeInfo != null && typeInfo.isResolved()) {
|
||||
// Wrap as ClassTypeInfo to indicate class reference
|
||||
TypeInfo classRef = new ClassTypeInfo(typeInfo);
|
||||
return new MethodReferenceTarget(classRef, true);
|
||||
}
|
||||
|
||||
// Try as variable (instance::method)
|
||||
TypeInfo varType = context.resolveIdentifier(name);
|
||||
if (varType != null && varType.isResolved()) {
|
||||
// Check if variable holds a Class reference (e.g., var File = Java.type("java.io.File"))
|
||||
boolean isClassRef = varType.isClassReference();
|
||||
return new MethodReferenceTarget(varType, isClassRef);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a qualified name (a.b.c) as method reference target.
|
||||
* Handles both package-qualified class names and nested class access.
|
||||
*/
|
||||
private MethodReferenceTarget resolveQualifiedTarget(ExpressionNode.MemberAccessNode memberAccess) {
|
||||
// Try to build qualified name and resolve as type
|
||||
String qualifiedName = buildQualifiedName(memberAccess);
|
||||
if (qualifiedName != null) {
|
||||
TypeInfo typeInfo = context.resolveTypeName(qualifiedName);
|
||||
if (typeInfo != null && typeInfo.isResolved()) {
|
||||
TypeInfo classRef = new ClassTypeInfo(typeInfo);
|
||||
return new MethodReferenceTarget(classRef, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Try resolving as expression chain (object.field::method)
|
||||
TypeInfo exprType = resolveNodeType(memberAccess);
|
||||
if (exprType != null && exprType.isResolved()) {
|
||||
boolean isClassRef = exprType.isClassReference();
|
||||
return new MethodReferenceTarget(exprType, isClassRef);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a qualified name string from a MemberAccessNode chain.
|
||||
* Example: a.b.c.ClassName -> "a.b.c.ClassName"
|
||||
*/
|
||||
private String buildQualifiedName(ExpressionNode node) {
|
||||
if (node instanceof ExpressionNode.IdentifierNode) {
|
||||
return ((ExpressionNode.IdentifierNode) node).getName();
|
||||
}
|
||||
|
||||
if (node instanceof ExpressionNode.MemberAccessNode) {
|
||||
ExpressionNode.MemberAccessNode ma = (ExpressionNode.MemberAccessNode) node;
|
||||
String baseName = buildQualifiedName(ma.getTarget());
|
||||
if (baseName != null) {
|
||||
return baseName + "." + ma.getMemberName();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a method reference (target::methodName).
|
||||
*/
|
||||
private TypeInfo resolveMethodReference(MethodReferenceTarget target, String methodName,
|
||||
MethodInfo sam, TypeInfo expectedType) {
|
||||
TypeInfo targetType = target.isClassRef && target.type instanceof ClassTypeInfo
|
||||
? ((ClassTypeInfo) target.type).getInstanceType()
|
||||
: target.type;
|
||||
|
||||
if (targetType == null || !targetType.hasMethod(methodName)) {
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
// Find best matching method using OverloadSelector
|
||||
MethodInfo method = findBestMethodForReference(targetType, methodName, sam, target.isClassRef);
|
||||
if (method == null) {
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
// Validate signature compatibility
|
||||
String error = validateMethodSignature(method, sam, target.isClassRef, targetType);
|
||||
if (error != null) {
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
return expectedType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the best matching method for a method reference using OverloadSelector logic.
|
||||
*/
|
||||
private MethodInfo findBestMethodForReference(TypeInfo targetType, String methodName,
|
||||
MethodInfo sam, boolean isClassRef) {
|
||||
List<MethodInfo> allOverloads = targetType.getAllMethodOverloads(methodName);
|
||||
if (allOverloads.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract SAM parameter types for overload matching
|
||||
TypeInfo[] samParamTypes = extractSamParamTypes(sam, isClassRef, targetType);
|
||||
|
||||
// Filter candidates by arity first, then use OverloadSelector
|
||||
List<MethodInfo> candidates = new ArrayList<>();
|
||||
int expectedArity = samParamTypes.length;
|
||||
|
||||
for (MethodInfo method : allOverloads) {
|
||||
int methodArity = method.getParameters().size();
|
||||
|
||||
// Direct match: instance::method or ClassName::staticMethod
|
||||
if (methodArity == expectedArity) {
|
||||
candidates.add(method);
|
||||
}
|
||||
// Unbound instance method: ClassName::instanceMethod (first SAM param is receiver)
|
||||
else if (isClassRef && methodArity == sam.getParameters().size() - 1 && !isStaticMethod(method)) {
|
||||
candidates.add(method);
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (candidates.size() == 1) {
|
||||
return candidates.get(0);
|
||||
}
|
||||
|
||||
// Use OverloadSelector for multi-candidate selection
|
||||
return OverloadSelector.selectBestOverload(candidates, samParamTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the effective parameter types from SAM for method matching.
|
||||
* For unbound instance methods, excludes the first receiver parameter.
|
||||
*/
|
||||
private TypeInfo[] extractSamParamTypes(MethodInfo sam, boolean isClassRef, TypeInfo targetType) {
|
||||
List<FieldInfo> samParams = sam.getParameters();
|
||||
|
||||
// For class references, we might be matching an unbound instance method
|
||||
// where the first SAM param is the receiver - handled in findBestMethodForReference
|
||||
TypeInfo[] types = new TypeInfo[samParams.size()];
|
||||
for (int i = 0; i < samParams.size(); i++) {
|
||||
types[i] = samParams.get(i).getTypeInfo();
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a method is static (Java reflection).
|
||||
*/
|
||||
private boolean isStaticMethod(MethodInfo method) {
|
||||
java.lang.reflect.Method javaMethod = method.getJavaMethod();
|
||||
if (javaMethod != null) {
|
||||
return java.lang.reflect.Modifier.isStatic(javaMethod.getModifiers());
|
||||
}
|
||||
// For script-defined methods, assume non-static unless marked
|
||||
return method.isStatic();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate method signature compatibility with SAM.
|
||||
*/
|
||||
private String validateMethodSignature(MethodInfo method, MethodInfo sam,
|
||||
boolean isClassRef, TypeInfo targetType) {
|
||||
List<FieldInfo> methodParams = method.getParameters();
|
||||
List<FieldInfo> samParams = sam.getParameters();
|
||||
|
||||
int methodArity = methodParams.size();
|
||||
int samArity = samParams.size();
|
||||
|
||||
// Check for unbound instance method reference (ClassName::instanceMethod)
|
||||
boolean isUnbound = isClassRef && methodArity == samArity - 1 && !isStaticMethod(method);
|
||||
|
||||
// Validate parameter count
|
||||
if (methodArity != samArity && !isUnbound) {
|
||||
return "Parameter count mismatch";
|
||||
}
|
||||
|
||||
// Validate parameter types
|
||||
int offset = isUnbound ? 1 : 0;
|
||||
for (int i = 0; i < methodArity; i++) {
|
||||
TypeInfo methodParamType = methodParams.get(i).getTypeInfo();
|
||||
TypeInfo samParamType = samParams.get(i + offset).getTypeInfo();
|
||||
|
||||
if (methodParamType != null && samParamType != null) {
|
||||
if (!TypeChecker.isTypeCompatible(methodParamType, samParamType)) {
|
||||
return "Parameter type mismatch at position " + (i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For unbound reference, validate receiver compatibility
|
||||
if (isUnbound && samArity > 0) {
|
||||
TypeInfo firstSamParam = samParams.get(0).getTypeInfo();
|
||||
if (firstSamParam != null && !TypeChecker.isTypeCompatible(targetType, firstSamParam)) {
|
||||
return "Receiver type mismatch";
|
||||
}
|
||||
}
|
||||
|
||||
// Validate return type (covariant)
|
||||
TypeInfo methodReturn = method.getReturnType();
|
||||
TypeInfo samReturn = sam.getReturnType();
|
||||
|
||||
if (samReturn != null && methodReturn != null) {
|
||||
boolean samIsVoid = "void".equals(samReturn.getFullName()) || samReturn.getJavaClass() == void.class;
|
||||
if (!samIsVoid && !TypeChecker.isTypeCompatible(samReturn, methodReturn)) {
|
||||
return "Return type mismatch";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a constructor reference (ClassName::new or Type[]::new).
|
||||
*/
|
||||
private TypeInfo resolveConstructorReference(MethodReferenceTarget target, MethodInfo sam,
|
||||
TypeInfo expectedType) {
|
||||
TypeInfo targetType = target.type;
|
||||
|
||||
// Handle array constructor reference: Type[]::new
|
||||
if (isArrayType(targetType)) {
|
||||
return resolveArrayConstructorReference(targetType, sam, expectedType);
|
||||
}
|
||||
|
||||
// Get the actual class type for constructor lookup
|
||||
TypeInfo classType = target.type instanceof ClassTypeInfo
|
||||
? ((ClassTypeInfo) target.type).getInstanceType()
|
||||
: target.type;
|
||||
|
||||
if (classType == null) {
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
// Cannot construct interfaces or abstract classes
|
||||
if (classType.getKind() == TypeInfo.Kind.INTERFACE) {
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
// Extract SAM parameter types for constructor matching
|
||||
List<FieldInfo> samParams = sam.getParameters();
|
||||
TypeInfo[] paramTypes = new TypeInfo[samParams.size()];
|
||||
for (int i = 0; i < samParams.size(); i++) {
|
||||
paramTypes[i] = samParams.get(i).getTypeInfo();
|
||||
}
|
||||
|
||||
// Find matching constructor
|
||||
MethodInfo constructor = classType.findConstructor(paramTypes);
|
||||
if (constructor == null && samParams.size() > 0) {
|
||||
// Try by arity if exact type match fails
|
||||
constructor = classType.findConstructor(samParams.size());
|
||||
}
|
||||
|
||||
if (constructor == null) {
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
// Validate return type compatibility
|
||||
TypeInfo samReturn = sam.getReturnType();
|
||||
if (samReturn != null && !TypeChecker.isTypeCompatible(samReturn, classType)) {
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
return expectedType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an array constructor reference: Type[]::new
|
||||
* Must match IntFunction<Type[]> or similar single-int-param functional interface.
|
||||
*/
|
||||
private TypeInfo resolveArrayConstructorReference(TypeInfo arrayType, MethodInfo sam,
|
||||
TypeInfo expectedType) {
|
||||
List<FieldInfo> samParams = sam.getParameters();
|
||||
|
||||
// Array constructor takes exactly one int parameter (the size)
|
||||
if (samParams.size() != 1) {
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
TypeInfo sizeParam = samParams.get(0).getTypeInfo();
|
||||
if (sizeParam == null || !isIntLike(sizeParam)) {
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
// Return type must be compatible with array type
|
||||
TypeInfo samReturn = sam.getReturnType();
|
||||
if (samReturn != null && !TypeChecker.isTypeCompatible(samReturn, arrayType)) {
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
}
|
||||
|
||||
return expectedType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a type is an array type.
|
||||
*/
|
||||
private boolean isArrayType(TypeInfo type) {
|
||||
if (type == null) return false;
|
||||
String name = type.getFullName();
|
||||
return name != null && name.endsWith("[]");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a type is int-like (int, Integer, or numeric that can represent array size).
|
||||
*/
|
||||
private boolean isIntLike(TypeInfo type) {
|
||||
if (type == null) return false;
|
||||
String name = type.getFullName();
|
||||
return "int".equals(name) || "java.lang.Integer".equals(name)
|
||||
|| "long".equals(name) || "java.lang.Long".equals(name);
|
||||
}
|
||||
|
||||
public static ExpressionNode.TypeResolverContext createBasicContext() {
|
||||
return new ExpressionNode.TypeResolverContext() {
|
||||
public TypeInfo resolveIdentifier(String name) {
|
||||
if ("true".equals(name) || "false".equals(name)) return TypeInfo.fromPrimitive("boolean");
|
||||
if ("null".equals(name)) return TypeInfo.unresolved("null", "<null>");
|
||||
return null;
|
||||
}
|
||||
public TypeInfo resolveMemberAccess(TypeInfo targetType, String memberName) { return null; }
|
||||
public TypeInfo resolveMethodCall(TypeInfo targetType, String methodName, TypeInfo[] argTypes) { return null; }
|
||||
public TypeInfo resolveArrayAccess(TypeInfo arrayType) {
|
||||
String typeName = arrayType.getFullName();
|
||||
if (typeName.endsWith("[]")) {
|
||||
// For primitive arrays, we need to map back to primitive TypeInfo
|
||||
String elementType = typeName.substring(0, typeName.length() - 2);
|
||||
switch (elementType) {
|
||||
case "int": case "long": case "float": case "double":
|
||||
case "byte": case "short": case "char": case "boolean":
|
||||
return TypeInfo.fromPrimitive(elementType);
|
||||
default:
|
||||
return TypeInfo.unresolved(elementType, elementType);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public TypeInfo resolveTypeName(String typeName) {
|
||||
// Handle primitives
|
||||
switch (typeName) {
|
||||
case "int": case "long": case "float": case "double":
|
||||
case "byte": case "short": case "char": case "boolean": case "void":
|
||||
return TypeInfo.fromPrimitive(typeName);
|
||||
default:
|
||||
return TypeInfo.unresolved(typeName, typeName);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.expression;
|
||||
|
||||
public enum OperatorType {
|
||||
|
||||
ADD("+", 11, Associativity.LEFT, Category.ARITHMETIC),
|
||||
SUBTRACT("-", 11, Associativity.LEFT, Category.ARITHMETIC),
|
||||
MULTIPLY("*", 12, Associativity.LEFT, Category.ARITHMETIC),
|
||||
DIVIDE("/", 12, Associativity.LEFT, Category.ARITHMETIC),
|
||||
MODULO("%", 12, Associativity.LEFT, Category.ARITHMETIC),
|
||||
|
||||
|
||||
ASSIGN("=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
|
||||
ADD_ASSIGN("+=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
|
||||
SUBTRACT_ASSIGN("-=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
|
||||
MULTIPLY_ASSIGN("*=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
|
||||
DIVIDE_ASSIGN("/=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
|
||||
MODULO_ASSIGN("%=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
|
||||
AND_ASSIGN("&=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
|
||||
OR_ASSIGN("|=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
|
||||
XOR_ASSIGN("^=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
|
||||
LEFT_SHIFT_ASSIGN("<<=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
|
||||
RIGHT_SHIFT_ASSIGN(">>=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
|
||||
UNSIGNED_RIGHT_SHIFT_ASSIGN(">>>=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
|
||||
|
||||
TERNARY_QUESTION("?", 2, Associativity.RIGHT, Category.TERNARY),
|
||||
TERNARY_COLON(":", 2, Associativity.RIGHT, Category.TERNARY),
|
||||
|
||||
LOGICAL_OR("||", 3, Associativity.LEFT, Category.LOGICAL),
|
||||
LOGICAL_AND("&&", 4, Associativity.LEFT, Category.LOGICAL),
|
||||
|
||||
BITWISE_OR("|", 5, Associativity.LEFT, Category.BITWISE),
|
||||
BITWISE_XOR("^", 6, Associativity.LEFT, Category.BITWISE),
|
||||
BITWISE_AND("&", 7, Associativity.LEFT, Category.BITWISE),
|
||||
|
||||
EQUALS("==", 8, Associativity.LEFT, Category.RELATIONAL),
|
||||
NOT_EQUALS("!=", 8, Associativity.LEFT, Category.RELATIONAL),
|
||||
LESS_THAN("<", 9, Associativity.LEFT, Category.RELATIONAL),
|
||||
GREATER_THAN(">", 9, Associativity.LEFT, Category.RELATIONAL),
|
||||
LESS_THAN_OR_EQUAL("<=", 9, Associativity.LEFT, Category.RELATIONAL),
|
||||
GREATER_THAN_OR_EQUAL(">=", 9, Associativity.LEFT, Category.RELATIONAL),
|
||||
|
||||
LEFT_SHIFT("<<", 10, Associativity.LEFT, Category.BITWISE),
|
||||
RIGHT_SHIFT(">>", 10, Associativity.LEFT, Category.BITWISE),
|
||||
UNSIGNED_RIGHT_SHIFT(">>>", 10, Associativity.LEFT, Category.BITWISE),
|
||||
|
||||
UNARY_PLUS("+", 14, Associativity.RIGHT, Category.UNARY),
|
||||
UNARY_MINUS("-", 14, Associativity.RIGHT, Category.UNARY),
|
||||
LOGICAL_NOT("!", 14, Associativity.RIGHT, Category.UNARY),
|
||||
BITWISE_NOT("~", 14, Associativity.RIGHT, Category.UNARY),
|
||||
PRE_INCREMENT("++", 14, Associativity.RIGHT, Category.UNARY),
|
||||
PRE_DECREMENT("--", 14, Associativity.RIGHT, Category.UNARY),
|
||||
POST_INCREMENT("++", 15, Associativity.LEFT, Category.UNARY_POSTFIX),
|
||||
POST_DECREMENT("--", 15, Associativity.LEFT, Category.UNARY_POSTFIX),
|
||||
|
||||
MEMBER_ACCESS(".", 16, Associativity.LEFT, Category.ACCESS),
|
||||
ARRAY_ACCESS("[]", 16, Associativity.LEFT, Category.ACCESS),
|
||||
METHOD_CALL("()", 16, Associativity.LEFT, Category.ACCESS),
|
||||
|
||||
CAST("(type)", 14, Associativity.RIGHT, Category.CAST),
|
||||
INSTANCEOF("instanceof", 9, Associativity.LEFT, Category.INSTANCEOF);
|
||||
|
||||
|
||||
private final String symbol;
|
||||
private final int precedence;
|
||||
private final Associativity associativity;
|
||||
private final Category category;
|
||||
|
||||
OperatorType(String symbol, int precedence, Associativity associativity, Category category) {
|
||||
this.symbol = symbol;
|
||||
this.precedence = precedence;
|
||||
this.associativity = associativity;
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
public String getSymbol() { return symbol; }
|
||||
public int getPrecedence() { return precedence; }
|
||||
public Associativity getAssociativity() { return associativity; }
|
||||
public Category getCategory() { return category; }
|
||||
|
||||
public boolean isBinary() {
|
||||
return category == Category.ARITHMETIC || category == Category.RELATIONAL ||
|
||||
category == Category.LOGICAL || category == Category.BITWISE;
|
||||
}
|
||||
|
||||
public boolean isUnary() {
|
||||
return category == Category.UNARY || category == Category.UNARY_POSTFIX;
|
||||
}
|
||||
|
||||
public boolean isAssignment() {
|
||||
return category == Category.ASSIGNMENT;
|
||||
}
|
||||
|
||||
public boolean isComparison() {
|
||||
return category == Category.RELATIONAL;
|
||||
}
|
||||
|
||||
public enum Associativity { LEFT, RIGHT }
|
||||
|
||||
public enum Category {
|
||||
ARITHMETIC, UNARY, UNARY_POSTFIX, RELATIONAL, LOGICAL, BITWISE,
|
||||
TERNARY, INSTANCEOF, ASSIGNMENT, CAST, ACCESS
|
||||
}
|
||||
|
||||
public static OperatorType fromSymbol(String symbol) {
|
||||
for (OperatorType op : values()) {
|
||||
if (op.symbol.equals(symbol)) return op;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static OperatorType fromBinarySymbol(String symbol) {
|
||||
switch (symbol) {
|
||||
case "+": return ADD;
|
||||
case "-": return SUBTRACT;
|
||||
case "*": return MULTIPLY;
|
||||
case "/": return DIVIDE;
|
||||
case "%": return MODULO;
|
||||
case "==": return EQUALS;
|
||||
case "!=": return NOT_EQUALS;
|
||||
case "<": return LESS_THAN;
|
||||
case ">": return GREATER_THAN;
|
||||
case "<=": return LESS_THAN_OR_EQUAL;
|
||||
case ">=": return GREATER_THAN_OR_EQUAL;
|
||||
case "&&": return LOGICAL_AND;
|
||||
case "||": return LOGICAL_OR;
|
||||
case "&": return BITWISE_AND;
|
||||
case "|": return BITWISE_OR;
|
||||
case "^": return BITWISE_XOR;
|
||||
case "<<": return LEFT_SHIFT;
|
||||
case ">>": return RIGHT_SHIFT;
|
||||
case ">>>": return UNSIGNED_RIGHT_SHIFT;
|
||||
case "=": return ASSIGN;
|
||||
case "+=": return ADD_ASSIGN;
|
||||
case "-=": return SUBTRACT_ASSIGN;
|
||||
case "*=": return MULTIPLY_ASSIGN;
|
||||
case "/=": return DIVIDE_ASSIGN;
|
||||
case "%=": return MODULO_ASSIGN;
|
||||
case "&=": return AND_ASSIGN;
|
||||
case "|=": return OR_ASSIGN;
|
||||
case "^=": return XOR_ASSIGN;
|
||||
case "<<=": return LEFT_SHIFT_ASSIGN;
|
||||
case ">>=": return RIGHT_SHIFT_ASSIGN;
|
||||
case ">>>=": return UNSIGNED_RIGHT_SHIFT_ASSIGN;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+349
@@ -0,0 +1,349 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.expression;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
|
||||
public class TypeRules {
|
||||
|
||||
private static boolean isJsNumber(TypeInfo type) {
|
||||
if (type == null || !type.isResolved()) return false;
|
||||
String name = type.getSimpleName();
|
||||
String full = type.getFullName();
|
||||
return "number".equals(name) || "number".equals(full);
|
||||
}
|
||||
|
||||
private static boolean isNumericPrimitiveOrWrapper(TypeInfo type) {
|
||||
if (type == null || type.getJavaClass() == null) return false;
|
||||
Class<?> cls = type.getJavaClass();
|
||||
return cls == byte.class || cls == Byte.class ||
|
||||
cls == short.class || cls == Short.class ||
|
||||
cls == int.class || cls == Integer.class ||
|
||||
cls == long.class || cls == Long.class ||
|
||||
cls == float.class || cls == Float.class ||
|
||||
cls == double.class || cls == Double.class ||
|
||||
cls == char.class || cls == Character.class;
|
||||
}
|
||||
|
||||
private static boolean isAny(TypeInfo type) {
|
||||
if (type == null) return false;
|
||||
if (type == TypeInfo.ANY) return true;
|
||||
String full = type.getFullName();
|
||||
return "any".equals(full);
|
||||
}
|
||||
|
||||
public static boolean isNumeric(TypeInfo type) {
|
||||
if (type == null || !type.isResolved()) return false;
|
||||
if (isJsNumber(type)) return true;
|
||||
String name = type.getSimpleName();
|
||||
return "int".equals(name) || "long".equals(name) || "float".equals(name) ||
|
||||
"double".equals(name) || "byte".equals(name) || "short".equals(name) || "char".equals(name);
|
||||
}
|
||||
|
||||
public static boolean isIntegral(TypeInfo type) {
|
||||
if (type == null || !type.isResolved()) return false;
|
||||
String name = type.getSimpleName();
|
||||
return "int".equals(name) || "long".equals(name) || "byte".equals(name) ||
|
||||
"short".equals(name) || "char".equals(name);
|
||||
}
|
||||
|
||||
public static boolean isFloatingPoint(TypeInfo type) {
|
||||
if (type == null || !type.isResolved()) return false;
|
||||
if (isJsNumber(type)) return true;
|
||||
String name = type.getSimpleName();
|
||||
return "float".equals(name) || "double".equals(name);
|
||||
}
|
||||
|
||||
public static boolean isBoolean(TypeInfo type) {
|
||||
if (type == null || !type.isResolved()) return false;
|
||||
return "boolean".equals(type.getSimpleName());
|
||||
}
|
||||
|
||||
public static boolean isString(TypeInfo type) {
|
||||
if (type == null || !type.isResolved()) return false;
|
||||
String name = type.getSimpleName();
|
||||
String fullName = type.getFullName();
|
||||
return "String".equals(name) || "java.lang.String".equals(fullName);
|
||||
}
|
||||
|
||||
public static TypeInfo binaryNumericPromotion(TypeInfo left, TypeInfo right) {
|
||||
if (isJsNumber(left) || isJsNumber(right)) return TypeInfo.NUMBER;
|
||||
if (left == null || right == null || !left.isResolved() || !right.isResolved()) {
|
||||
return null;
|
||||
}
|
||||
String l = left.getSimpleName();
|
||||
String r = right.getSimpleName();
|
||||
if ("double".equals(l) || "double".equals(r)) return TypeInfo.fromPrimitive("double");
|
||||
if ("float".equals(l) || "float".equals(r)) return TypeInfo.fromPrimitive("float");
|
||||
if ("long".equals(l) || "long".equals(r)) return TypeInfo.fromPrimitive("long");
|
||||
return TypeInfo.fromPrimitive("int");
|
||||
}
|
||||
|
||||
public static TypeInfo unaryNumericPromotion(TypeInfo type) {
|
||||
if (type == null || !type.isResolved()) return null;
|
||||
if (isJsNumber(type)) return TypeInfo.NUMBER;
|
||||
String name = type.getSimpleName();
|
||||
if ("double".equals(name) || "float".equals(name) || "long".equals(name)) return type;
|
||||
if ("byte".equals(name) || "short".equals(name) || "char".equals(name) || "int".equals(name)) {
|
||||
return TypeInfo.fromPrimitive("int");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static TypeInfo resolveBinaryOperatorType(OperatorType op, TypeInfo left, TypeInfo right) {
|
||||
if (op == null) return null;
|
||||
|
||||
switch (op.getCategory()) {
|
||||
case ARITHMETIC:
|
||||
if (op == OperatorType.ADD && (isString(left) || isString(right))) {
|
||||
return validateAgainstExpectedType(TypeInfo.string());
|
||||
}
|
||||
if (isAny(left) || isAny(right)) {
|
||||
return validateAgainstExpectedType(TypeInfo.ANY);
|
||||
}
|
||||
if (isNumeric(left) && isNumeric(right)) {
|
||||
TypeInfo promoted = binaryNumericPromotion(left, right);
|
||||
return validateAgainstExpectedType(promoted);
|
||||
}
|
||||
return null;
|
||||
|
||||
case RELATIONAL:
|
||||
if (op == OperatorType.EQUALS || op == OperatorType.NOT_EQUALS) {
|
||||
return TypeInfo.fromPrimitive("boolean");
|
||||
}
|
||||
if (isAny(left) || isAny(right)) {
|
||||
return TypeInfo.fromPrimitive("boolean");
|
||||
}
|
||||
if (isNumeric(left) && isNumeric(right)) {
|
||||
return TypeInfo.fromPrimitive("boolean");
|
||||
}
|
||||
return null;
|
||||
|
||||
case LOGICAL:
|
||||
if (isAny(left) || isAny(right)) {
|
||||
return TypeInfo.ANY;
|
||||
}
|
||||
if (isBoolean(left) && isBoolean(right)) {
|
||||
return TypeInfo.fromPrimitive("boolean");
|
||||
}
|
||||
return null;
|
||||
|
||||
case BITWISE:
|
||||
if (isAny(left) || isAny(right)) {
|
||||
return validateAgainstExpectedType(TypeInfo.ANY);
|
||||
}
|
||||
if (op == OperatorType.LEFT_SHIFT || op == OperatorType.RIGHT_SHIFT ||
|
||||
op == OperatorType.UNSIGNED_RIGHT_SHIFT) {
|
||||
if (isIntegral(left)) {
|
||||
TypeInfo promoted = unaryNumericPromotion(left);
|
||||
return validateAgainstExpectedType(promoted);
|
||||
}
|
||||
}
|
||||
if (isIntegral(left) && isIntegral(right)) {
|
||||
TypeInfo promoted = binaryNumericPromotion(left, right);
|
||||
return validateAgainstExpectedType(promoted);
|
||||
}
|
||||
if (isBoolean(left) && isBoolean(right)) {
|
||||
return TypeInfo.fromPrimitive("boolean");
|
||||
}
|
||||
return null;
|
||||
|
||||
case ASSIGNMENT:
|
||||
return left;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static TypeInfo resolveUnaryOperatorType(OperatorType op, TypeInfo operand) {
|
||||
if (op == null || operand == null || !operand.isResolved()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isAny(operand)) {
|
||||
if (op == OperatorType.LOGICAL_NOT) {
|
||||
return TypeInfo.fromPrimitive("boolean");
|
||||
}
|
||||
return TypeInfo.ANY;
|
||||
}
|
||||
|
||||
switch (op) {
|
||||
case UNARY_PLUS:
|
||||
case UNARY_MINUS:
|
||||
if (isNumeric(operand)) {
|
||||
TypeInfo promoted = unaryNumericPromotion(operand);
|
||||
return validateAgainstExpectedType(promoted);
|
||||
}
|
||||
return null;
|
||||
|
||||
case BITWISE_NOT:
|
||||
if (isIntegral(operand)) {
|
||||
TypeInfo promoted = unaryNumericPromotion(operand);
|
||||
return validateAgainstExpectedType(promoted);
|
||||
}
|
||||
return null;
|
||||
|
||||
case LOGICAL_NOT:
|
||||
if (isBoolean(operand)) return TypeInfo.fromPrimitive("boolean");
|
||||
return null;
|
||||
|
||||
case PRE_INCREMENT:
|
||||
case PRE_DECREMENT:
|
||||
case POST_INCREMENT:
|
||||
case POST_DECREMENT:
|
||||
if (isNumeric(operand)) return operand;
|
||||
return null;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if sourceType can be assigned to targetType.
|
||||
* Handles primitives, numeric conversions, null compatibility, and reference types.
|
||||
*/
|
||||
public static boolean isAssignmentCompatible(TypeInfo sourceType, TypeInfo targetType) {
|
||||
if (sourceType == null || targetType == null)
|
||||
return false;
|
||||
if (!targetType.isResolved())
|
||||
return true; // Can't validate against unresolved type
|
||||
|
||||
if (isAny(sourceType) || isAny(targetType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Null literal can be assigned to any reference type
|
||||
if ("<null>".equals(sourceType.getFullName())) {
|
||||
return !isPrimitive(targetType);
|
||||
}
|
||||
|
||||
if (!sourceType.isResolved())
|
||||
return false;
|
||||
|
||||
if ((isJsNumber(sourceType) && isNumericPrimitiveOrWrapper(targetType)) ||
|
||||
(isJsNumber(targetType) && isNumericPrimitiveOrWrapper(sourceType))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Exact match
|
||||
if (sourceType.equals(targetType))
|
||||
return true;
|
||||
if (sourceType.getFullName().equals(targetType.getFullName()))
|
||||
return true;
|
||||
|
||||
// Numeric conversions (widening primitive conversions)
|
||||
String source = sourceType.getSimpleName();
|
||||
String target = targetType.getSimpleName();
|
||||
|
||||
// byte -> short, int, long, float, double
|
||||
if ("byte".equals(source)) {
|
||||
return "short".equals(target) || "int".equals(target) || "long".equals(target) ||
|
||||
"float".equals(target) || "double".equals(target);
|
||||
}
|
||||
// short -> int, long, float, double
|
||||
if ("short".equals(source)) {
|
||||
return "int".equals(target) || "long".equals(target) ||
|
||||
"float".equals(target) || "double".equals(target);
|
||||
}
|
||||
// char -> int, long, float, double
|
||||
if ("char".equals(source)) {
|
||||
return "int".equals(target) || "long".equals(target) ||
|
||||
"float".equals(target) || "double".equals(target);
|
||||
}
|
||||
// int -> long, float, double
|
||||
if ("int".equals(source)) {
|
||||
return "long".equals(target) || "float".equals(target) || "double".equals(target);
|
||||
}
|
||||
// long -> float, double
|
||||
if ("long".equals(source)) {
|
||||
return "float".equals(target) || "double".equals(target);
|
||||
}
|
||||
// float -> double
|
||||
if ("float".equals(source)) {
|
||||
return "double".equals(target);
|
||||
}
|
||||
|
||||
// For reference types, we'd need inheritance/interface checking
|
||||
// For now, return false for incompatible types
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a type is a primitive type.
|
||||
*/
|
||||
public static boolean isPrimitive(TypeInfo type) {
|
||||
if (type == null || !type.isResolved())
|
||||
return false;
|
||||
String name = type.getSimpleName();
|
||||
return "int".equals(name) || "long".equals(name) || "float".equals(name) ||
|
||||
"double".equals(name) || "byte".equals(name) || "short".equals(name) ||
|
||||
"char".equals(name) || "boolean".equals(name) || "void".equals(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a computed type against the current expected type context.
|
||||
* If there's an expected type and the computed type is compatible, returns the expected type.
|
||||
* If incompatible, returns the computed type so the error can be properly reported.
|
||||
* If no expected type context, returns the computed type unchanged.
|
||||
*
|
||||
* @param computedType The type computed by normal type rules
|
||||
* @return The type to use (either expectedType if compatible, or computedType)
|
||||
*/
|
||||
public static TypeInfo validateAgainstExpectedType(TypeInfo computedType) {
|
||||
if (computedType == null) return null;
|
||||
|
||||
TypeInfo expectedType = ExpressionTypeResolver.CURRENT_EXPECTED_TYPE;
|
||||
if (expectedType != null && expectedType.isResolved() && computedType.isResolved()) {
|
||||
if (isAssignmentCompatible(computedType, expectedType)) {
|
||||
return expectedType;
|
||||
}
|
||||
// Return computed type so error can be detected (incompatible with expected)
|
||||
return computedType;
|
||||
}
|
||||
|
||||
return computedType;
|
||||
}
|
||||
|
||||
public static TypeInfo resolveTernaryType(TypeInfo thenType, TypeInfo elseType) {
|
||||
if (thenType == null && elseType == null) return null;
|
||||
if (thenType == null) return elseType;
|
||||
if (elseType == null) return thenType;
|
||||
|
||||
// Handle null literal type
|
||||
boolean thenIsNull = "<null>".equals(thenType.getFullName());
|
||||
boolean elseIsNull = "<null>".equals(elseType.getFullName());
|
||||
|
||||
if (thenIsNull && elseIsNull) return thenType; // both null -> null
|
||||
if (thenIsNull) return elseType; // null : T -> T
|
||||
if (elseIsNull) return thenType; // T : null -> T
|
||||
|
||||
if (!thenType.isResolved()) return elseType.isResolved() ? elseType : null;
|
||||
if (!elseType.isResolved()) return thenType;
|
||||
|
||||
// If there's an expected type context, validate both branches against it
|
||||
TypeInfo expectedType = ExpressionTypeResolver.CURRENT_EXPECTED_TYPE;
|
||||
if (expectedType != null && expectedType.isResolved()) {
|
||||
boolean thenCompatible = isAssignmentCompatible(thenType, expectedType);
|
||||
boolean elseCompatible = isAssignmentCompatible(elseType, expectedType);
|
||||
|
||||
// If both branches are compatible with expected type, return expected type
|
||||
if (thenCompatible && elseCompatible) {
|
||||
return expectedType;
|
||||
} else if (!elseCompatible) {
|
||||
// Return the incompatible type so error can be detected
|
||||
return elseType;
|
||||
} else {
|
||||
// thenType is incompatible
|
||||
return thenType;
|
||||
}
|
||||
}
|
||||
|
||||
// Normal ternary type resolution without expected type context
|
||||
if (thenType.equals(elseType)) return thenType;
|
||||
if (isNumeric(thenType) && isNumeric(elseType)) {
|
||||
return binaryNumericPromotion(thenType, elseType);
|
||||
}
|
||||
|
||||
return thenType;
|
||||
}
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.field;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.ScopeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
/**
|
||||
* Metadata for assignment validation.
|
||||
* Tracks assignment statements like "varName = expr" or "obj.field = expr"
|
||||
* and validates type compatibility, access modifiers, and final status.
|
||||
*
|
||||
* Each AssignmentInfo represents a single assignment to a field.
|
||||
* The positions are:
|
||||
* - lhsStart/lhsEnd: The target variable/field position (left-hand side)
|
||||
* - rhsStart/rhsEnd: From first token of RHS to the semicolon (inclusive)
|
||||
*/
|
||||
public class AssignmentInfo {
|
||||
|
||||
/**
|
||||
* Error type for assignment validation.
|
||||
*/
|
||||
public enum ErrorType {
|
||||
NONE,
|
||||
TYPE_MISMATCH, // Assigned value type doesn't match target type
|
||||
FINAL_REASSIGNMENT, // Attempting to reassign a final field
|
||||
PRIVATE_ACCESS, // Attempting to access private field
|
||||
PROTECTED_ACCESS, // Attempting to access protected field from invalid context
|
||||
UNRESOLVED_TARGET, // Target variable/field doesn't exist
|
||||
STATIC_CONTEXT_ERROR, // Accessing instance field from static context
|
||||
DUPLICATE_DECLARATION // Variable is already defined in the scope
|
||||
}
|
||||
|
||||
// Statement position
|
||||
private final int statementStart; // Absolute start of statement (includes modifiers/type for declarations)
|
||||
|
||||
// Target/LHS info
|
||||
private final String targetName;
|
||||
private final int lhsStart; // Position of first char of target variable/field
|
||||
private final int lhsEnd; // Position after last char of target
|
||||
private final TypeInfo targetType; // Declared type of target
|
||||
|
||||
// Source/RHS info (from first token of RHS expression to semicolon)
|
||||
private final int rhsStart; // Position of first non-whitespace char after '='
|
||||
private final int rhsEnd; // Position of ';' (inclusive in the range for underlining)
|
||||
private final TypeInfo sourceType; // Resolved type of RHS expression
|
||||
private final String sourceExpr; // The RHS expression text (for display)
|
||||
|
||||
// For chained field access (e.g., obj.field = value)
|
||||
private final TypeInfo receiverType; // Type of receiver (null for simple variables)
|
||||
private final Field reflectionField; // Java reflection field (for modifier checks)
|
||||
|
||||
// Flag for script-defined final variables
|
||||
private final boolean isFinal;
|
||||
|
||||
// Validation
|
||||
private ErrorType errorType = ErrorType.NONE;
|
||||
private String errorMessage;
|
||||
private String requiredType;
|
||||
private String providedType;
|
||||
|
||||
private ScopeInfo scopeInfo;
|
||||
|
||||
public AssignmentInfo(String targetName, int statementStart, int lhsStart, int lhsEnd,
|
||||
TypeInfo targetType, int rhsStart, int rhsEnd,
|
||||
TypeInfo sourceType, String sourceExpr,
|
||||
TypeInfo receiverType, java.lang.reflect.Field reflectionField,
|
||||
boolean isFinal) {
|
||||
this.targetName = targetName;
|
||||
this.statementStart = statementStart;
|
||||
this.lhsStart = lhsStart;
|
||||
this.lhsEnd = lhsEnd;
|
||||
this.targetType = targetType;
|
||||
this.rhsStart = rhsStart;
|
||||
this.rhsEnd = rhsEnd;
|
||||
this.sourceType = sourceType;
|
||||
this.sourceExpr = sourceExpr;
|
||||
this.receiverType = receiverType;
|
||||
this.reflectionField = reflectionField;
|
||||
this.isFinal = isFinal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create an AssignmentInfo representing a duplicate declaration error.
|
||||
* Only the LHS (variable name) position is relevant for underlining.
|
||||
*/
|
||||
public static AssignmentInfo duplicateDeclaration(String varName, int nameStart, int nameEnd, String errorMessage) {
|
||||
AssignmentInfo info = new AssignmentInfo(
|
||||
varName,
|
||||
nameStart, // statementStart = nameStart for underline positioning
|
||||
nameStart, // lhsStart
|
||||
nameEnd, // lhsEnd
|
||||
null, // targetType
|
||||
-1, // rhsStart (not applicable)
|
||||
-1, // rhsEnd (not applicable)
|
||||
null, // sourceType
|
||||
null, // sourceExpr
|
||||
null, // receiverType
|
||||
null, // reflectionField
|
||||
false // isFinal
|
||||
);
|
||||
info.setError(ErrorType.DUPLICATE_DECLARATION, errorMessage);
|
||||
return info;
|
||||
}
|
||||
|
||||
// ==================== VALIDATION ====================
|
||||
|
||||
/**
|
||||
* Validate this assignment.
|
||||
* Checks type compatibility, final status, and access modifiers.
|
||||
*/
|
||||
public void validate() {
|
||||
// Check for final field reassignment from reflection
|
||||
if (reflectionField != null && Modifier.isFinal(reflectionField.getModifiers())) {
|
||||
setError(ErrorType.FINAL_REASSIGNMENT,
|
||||
"Cannot assign a value to final variable '" + targetName + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for final field reassignment from FieldInfo (script-defined finals)
|
||||
if (isFinal) {
|
||||
setError(ErrorType.FINAL_REASSIGNMENT,
|
||||
"Cannot assign a value to final variable '" + targetName + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check access modifiers for chained field access
|
||||
if (reflectionField != null && receiverType != null) {
|
||||
int mods = reflectionField.getModifiers();
|
||||
if (Modifier.isPrivate(mods)) {
|
||||
setError(ErrorType.PRIVATE_ACCESS,
|
||||
"'" + targetName + "' has private access in '" + receiverType.getFullName() + "'");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check type compatibility
|
||||
if (targetType != null && sourceType != null) {
|
||||
if (!TypeChecker.isTypeCompatible(targetType, sourceType)) {
|
||||
this.requiredType = targetType.getSimpleName();
|
||||
this.providedType = sourceType.getSimpleName();
|
||||
setError(ErrorType.TYPE_MISMATCH, buildTypeMismatchMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a formatted type mismatch error message (IntelliJ style).
|
||||
*/
|
||||
private String buildTypeMismatchMessage() {
|
||||
return "Provided type: " + providedType + "\nRequired: " + requiredType;
|
||||
}
|
||||
|
||||
private void setError(ErrorType type, String message) {
|
||||
this.errorType = type;
|
||||
this.errorMessage = message;
|
||||
}
|
||||
|
||||
// ==================== POSITION CHECKS ====================
|
||||
|
||||
/**
|
||||
* Check if the given global position falls within the LHS (target) of this assignment.
|
||||
*/
|
||||
public boolean containsLhsPosition(int position) {
|
||||
return position >= statementStart && position < lhsEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given global position falls within the RHS (source) of this assignment.
|
||||
*/
|
||||
public boolean containsRhsPosition(int position) {
|
||||
return position >= rhsStart && position <= rhsEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given global position falls anywhere in this assignment (LHS or RHS).
|
||||
*/
|
||||
public boolean containsPosition(int position) {
|
||||
return position >= statementStart && position <= rhsEnd;
|
||||
}
|
||||
|
||||
// ==================== ERROR TYPE CHECKS ====================
|
||||
|
||||
/**
|
||||
* Check if this is an LHS error (final reassignment, access errors, duplicate declarations).
|
||||
* These errors should underline the LHS.
|
||||
*/
|
||||
public boolean isLhsError() {
|
||||
return errorType == ErrorType.FINAL_REASSIGNMENT ||
|
||||
errorType == ErrorType.PRIVATE_ACCESS ||
|
||||
errorType == ErrorType.PROTECTED_ACCESS ||
|
||||
errorType == ErrorType.STATIC_CONTEXT_ERROR ||
|
||||
errorType == ErrorType.DUPLICATE_DECLARATION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is an RHS error (type mismatch).
|
||||
* These errors should underline the RHS.
|
||||
*/
|
||||
public boolean isRhsError() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isFullLineError() {
|
||||
return errorType == ErrorType.TYPE_MISMATCH;
|
||||
}
|
||||
|
||||
// ==================== GETTERS ====================
|
||||
|
||||
public String getTargetName() { return targetName; }
|
||||
public int getStatementStart() { return statementStart; }
|
||||
public int getLhsStart() { return lhsStart; }
|
||||
public int getLhsEnd() { return lhsEnd; }
|
||||
public TypeInfo getTargetType() { return targetType; }
|
||||
public int getRhsStart() { return rhsStart; }
|
||||
public int getRhsEnd() { return rhsEnd; }
|
||||
public TypeInfo getSourceType() { return sourceType; }
|
||||
public String getSourceExpr() { return sourceExpr; }
|
||||
public TypeInfo getReceiverType() { return receiverType; }
|
||||
public java.lang.reflect.Field getReflectionField() { return reflectionField; }
|
||||
|
||||
public ErrorType getErrorType() { return errorType; }
|
||||
public String getErrorMessage() { return errorMessage; }
|
||||
public String getRequiredType() { return requiredType; }
|
||||
public String getProvidedType() { return providedType; }
|
||||
|
||||
public boolean hasError() { return errorType != ErrorType.NONE; }
|
||||
public boolean hasTypeMismatch() { return errorType == ErrorType.TYPE_MISMATCH; }
|
||||
public boolean hasFinalReassignment() { return errorType == ErrorType.FINAL_REASSIGNMENT; }
|
||||
public boolean hasAccessError() { return errorType == ErrorType.PRIVATE_ACCESS || errorType == ErrorType.PROTECTED_ACCESS; }
|
||||
|
||||
public ScopeInfo getScopeInfo() { return scopeInfo; }
|
||||
public void setScopeInfo(ScopeInfo scopeInfo) { this.scopeInfo = scopeInfo; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AssignmentInfo{" +
|
||||
"target='" + targetName + "', " +
|
||||
"stmt=" + statementStart + ", " +
|
||||
"lhs=[" + lhsStart + "-" + lhsEnd + "], " +
|
||||
"rhs=[" + rhsStart + "-" + rhsEnd + "], " +
|
||||
"targetType=" + targetType + ", " +
|
||||
"sourceType=" + sourceType + ", " +
|
||||
"error=" + errorType +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.field;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument;
|
||||
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.type.ScriptTypeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Represents an enum constant declaration with its constructor call.
|
||||
* Combines a FieldInfo (the constant itself) with a MethodCallInfo (the constructor call validation).
|
||||
* This allows us to reuse existing method call validation logic for enum constructor matching.
|
||||
*/
|
||||
public class EnumConstantInfo {
|
||||
|
||||
private final FieldInfo fieldInfo;
|
||||
private final MethodCallInfo constructorCall; // Can be null if no args provided
|
||||
private final TypeInfo enumType;
|
||||
|
||||
public EnumConstantInfo(FieldInfo fieldInfo, MethodCallInfo constructorCall, TypeInfo enumType) {
|
||||
this.fieldInfo = fieldInfo;
|
||||
this.constructorCall = constructorCall;
|
||||
this.enumType = enumType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse enum constants from an enum body.
|
||||
* Returns a list of EnumConstantInfo objects, each representing one constant.
|
||||
*
|
||||
* @param enumType The enum type being parsed
|
||||
* @param bodyText The text content of the enum body
|
||||
* @param bodyOffset The absolute offset where the enum body starts
|
||||
* @param keywordPattern Pattern to detect Java keywords (to skip them)
|
||||
* @return List of parsed enum constants
|
||||
*/
|
||||
public static List<EnumConstantInfo> parseEnumConstants(
|
||||
ScriptTypeInfo enumType,
|
||||
String bodyText,
|
||||
int bodyOffset,
|
||||
Pattern keywordPattern) {
|
||||
|
||||
List<EnumConstantInfo> constants = new ArrayList<>();
|
||||
|
||||
// Find where enum constants end (first semicolon not in parens, or first method/field declaration)
|
||||
int constantsEnd = findEnumConstantsEnd(bodyText);
|
||||
if (constantsEnd <= 0) {
|
||||
// No semicolon found - entire body might be constants
|
||||
constantsEnd = bodyText.length();
|
||||
}
|
||||
|
||||
// Pattern to match enum constants: NAME or NAME() or NAME(args)
|
||||
Pattern constantPattern = Pattern.compile(
|
||||
"([A-Za-z_][a-zA-Z0-9_]*)\\s*(\\(([^)]*)\\))?");
|
||||
|
||||
Matcher m = constantPattern.matcher(bodyText);
|
||||
int lastEnd = 0;
|
||||
|
||||
while (m.find()) {
|
||||
// Skip if we're past the constants section
|
||||
if (m.start() >= constantsEnd) {
|
||||
break;
|
||||
}
|
||||
|
||||
int absPos = bodyOffset + m.start();
|
||||
if (ScriptDocument.INSTANCE.isExcluded(absPos)) {
|
||||
lastEnd = m.end();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is at a valid position (after comma or at start)
|
||||
String beforeMatch = bodyText.substring(lastEnd, m.start()).trim();
|
||||
if (!beforeMatch.isEmpty() && !beforeMatch.equals(",")) {
|
||||
// Not a valid enum constant position - might be inside parens
|
||||
continue;
|
||||
}
|
||||
|
||||
String constantName = m.group(1);
|
||||
String argsClause = m.group(2); // includes parens if present
|
||||
String args = m.group(3); // just the args without parens
|
||||
|
||||
// Skip keywords
|
||||
if (keywordPattern.matcher(constantName).matches()) {
|
||||
lastEnd = m.end();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine init range (the constructor arguments)
|
||||
int initStart = -1;
|
||||
int initEnd = -1;
|
||||
|
||||
// Create MethodCallInfo for constructor validation if args present
|
||||
MethodCallInfo constructorCall = null;
|
||||
if (argsClause != null && !argsClause.isEmpty()) {
|
||||
initStart = bodyOffset + m.start(2); // Position of '('
|
||||
initEnd = bodyOffset + m.end(2) - 1; // Position after ')'
|
||||
|
||||
constructorCall = createConstructorCall(
|
||||
enumType,
|
||||
constantName,
|
||||
absPos,
|
||||
initStart,
|
||||
initEnd
|
||||
);
|
||||
} else if (enumType.hasConstructors()) {
|
||||
// No args provided, but enum has constructors - validate against no-arg constructor
|
||||
initStart = absPos + constantName.length();
|
||||
initEnd = initStart;
|
||||
|
||||
constructorCall = createConstructorCall(
|
||||
enumType,
|
||||
constantName,
|
||||
absPos,
|
||||
initStart,
|
||||
initEnd
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Create FieldInfo for the enum constant
|
||||
FieldInfo fieldInfo = FieldInfo.enumConstant(
|
||||
constantName,
|
||||
enumType,
|
||||
absPos,
|
||||
initStart,
|
||||
initEnd,
|
||||
null
|
||||
);
|
||||
|
||||
EnumConstantInfo constantInfo = new EnumConstantInfo(fieldInfo, constructorCall, enumType);
|
||||
fieldInfo.setEnumConstantInfo(constantInfo);
|
||||
constants.add(constantInfo);
|
||||
|
||||
lastEnd = m.end();
|
||||
}
|
||||
|
||||
return constants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a MethodCallInfo for an enum constant's constructor call.
|
||||
* Validates the arguments against available constructors.
|
||||
*/
|
||||
private static MethodCallInfo createConstructorCall(
|
||||
ScriptTypeInfo enumType,
|
||||
String constantName,
|
||||
int constantStart,
|
||||
int openParenPos,
|
||||
int closeParenPos) {
|
||||
|
||||
List<MethodInfo> constructors = enumType.getConstructors();
|
||||
|
||||
// Parse arguments - pass absolute positions (bodyOffset accounts for the offset into the document)
|
||||
List<MethodCallInfo.Argument> arguments = ScriptDocument.INSTANCE.parseMethodArguments(
|
||||
openParenPos + 1,
|
||||
closeParenPos,
|
||||
null
|
||||
);
|
||||
|
||||
// Find matching constructor
|
||||
MethodInfo matchedConstructor = null;
|
||||
List<MethodInfo> candidates = new ArrayList<>();
|
||||
|
||||
for (MethodInfo constructor : constructors) {
|
||||
candidates.add(constructor);
|
||||
if (constructor.getParameterCount() == arguments.size()) {
|
||||
matchedConstructor = constructor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create MethodCallInfo
|
||||
MethodCallInfo callInfo = new MethodCallInfo(
|
||||
constantName, // Use constant name as "method" name
|
||||
constantStart,
|
||||
openParenPos,
|
||||
openParenPos,
|
||||
closeParenPos,
|
||||
arguments,
|
||||
enumType, // Receiver is the enum type
|
||||
matchedConstructor
|
||||
);
|
||||
|
||||
|
||||
callInfo.setConstructor(true);
|
||||
callInfo.validate();
|
||||
return callInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create EnumConstantInfo from a reflection-based enum constant.
|
||||
* Used for enum constants from external libraries (via reflection).
|
||||
*
|
||||
* @param constantName The name of the enum constant
|
||||
* @param enumType The enum type this constant belongs to
|
||||
* @return EnumConstantInfo representing this constant, or null if not found
|
||||
*/
|
||||
public static EnumConstantInfo fromReflection(String constantName, TypeInfo enumType, Field javaField) {
|
||||
if (enumType == null || enumType.getJavaClass() == null || !enumType.getJavaClass().isEnum()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
FieldInfo fieldInfo = FieldInfo.enumConstant(
|
||||
constantName,
|
||||
enumType,
|
||||
-1, // No declaration offset for reflection-based constants
|
||||
-1,
|
||||
-1,
|
||||
javaField
|
||||
);
|
||||
|
||||
EnumConstantInfo enumInfo = new EnumConstantInfo(fieldInfo, null, enumType);
|
||||
fieldInfo.setEnumConstantInfo(enumInfo);
|
||||
return enumInfo;
|
||||
} catch (SecurityException e) {
|
||||
// Constant not found or access denied
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find where enum constants section ends.
|
||||
* Returns the position of the first semicolon that's not inside parentheses,
|
||||
* or the position of the first method/field declaration.
|
||||
*/
|
||||
private static int findEnumConstantsEnd(String bodyText) {
|
||||
int parenDepth = 0;
|
||||
boolean foundConstant = false;
|
||||
|
||||
for (int i = 0; i < bodyText.length(); i++) {
|
||||
char c = bodyText.charAt(i);
|
||||
|
||||
if (c == '(') {
|
||||
parenDepth++;
|
||||
} else if (c == ')') {
|
||||
parenDepth--;
|
||||
} else if (c == ';' && parenDepth == 0) {
|
||||
return i;
|
||||
} else if (c == '{' && parenDepth == 0) {
|
||||
// Start of method body - constants end before this
|
||||
return findStatementStart(bodyText, i);
|
||||
}
|
||||
|
||||
// Track if we've found at least one identifier (potential constant)
|
||||
if (Character.isJavaIdentifierStart(c)) {
|
||||
foundConstant = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we found constants but no semicolon, return the full length
|
||||
return foundConstant ? bodyText.length() : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the start of a statement by going backwards from a position.
|
||||
*/
|
||||
private static int findStatementStart(String text, int fromPos) {
|
||||
int pos = fromPos - 1;
|
||||
int parenDepth = 0;
|
||||
|
||||
while (pos >= 0) {
|
||||
char c = text.charAt(pos);
|
||||
if (c == ')')
|
||||
parenDepth++;
|
||||
else if (c == '(')
|
||||
parenDepth--;
|
||||
else if ((c == ',' || c == ';' || c == '{' || c == '}') && parenDepth == 0) {
|
||||
return pos + 1;
|
||||
}
|
||||
pos--;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ==================== GETTERS ====================
|
||||
|
||||
public FieldInfo getFieldInfo() {
|
||||
return fieldInfo;
|
||||
}
|
||||
|
||||
public MethodCallInfo getConstructorCall() {
|
||||
return constructorCall;
|
||||
}
|
||||
|
||||
public TypeInfo getEnumType() {
|
||||
return enumType;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return fieldInfo.getName();
|
||||
}
|
||||
|
||||
public int getDeclarationOffset() {
|
||||
return fieldInfo.getDeclarationOffset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this enum constant has any errors (constructor mismatch, etc).
|
||||
*/
|
||||
public boolean hasError() {
|
||||
return (constructorCall != null && constructorCall.hasError());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error message if any.
|
||||
*/
|
||||
public String getErrorMessage() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.field;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
|
||||
/**
|
||||
* Metadata for field access validation.
|
||||
* Parallel to MethodCallInfo but for field accesses like `obj.field` or `Class.staticField`.
|
||||
*/
|
||||
public class FieldAccessInfo {
|
||||
|
||||
/**
|
||||
* Validation error type for field accesses.
|
||||
*/
|
||||
public enum ErrorType {
|
||||
NONE,
|
||||
TYPE_MISMATCH, // Field type doesn't match expected type (e.g., assignment LHS)
|
||||
UNRESOLVED_FIELD, // Field doesn't exist on the receiver type
|
||||
STATIC_ACCESS_ERROR // Trying to access instance field statically or vice versa
|
||||
}
|
||||
|
||||
private final String fieldName;
|
||||
private final int fieldNameStart;
|
||||
private final int fieldNameEnd;
|
||||
private final TypeInfo receiverType; // The type on which this field is accessed
|
||||
private final FieldInfo resolvedField; // The resolved field (null if unresolved)
|
||||
private final boolean isStaticAccess; // True if this is Class.field style access
|
||||
private TypeInfo expectedType; // Expected field type (from assignment LHS, etc.)
|
||||
|
||||
private ErrorType errorType = ErrorType.NONE;
|
||||
private String errorMessage;
|
||||
|
||||
public FieldAccessInfo(String fieldName, int fieldNameStart, int fieldNameEnd,
|
||||
TypeInfo receiverType, FieldInfo resolvedField, boolean isStaticAccess) {
|
||||
this.fieldName = fieldName;
|
||||
this.fieldNameStart = fieldNameStart;
|
||||
this.fieldNameEnd = fieldNameEnd;
|
||||
this.receiverType = receiverType;
|
||||
this.resolvedField = resolvedField;
|
||||
this.isStaticAccess = isStaticAccess;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getFieldName() {
|
||||
return fieldName;
|
||||
}
|
||||
|
||||
public int getFieldNameStart() {
|
||||
return fieldNameStart;
|
||||
}
|
||||
|
||||
public int getFieldNameEnd() {
|
||||
return fieldNameEnd;
|
||||
}
|
||||
|
||||
public TypeInfo getReceiverType() {
|
||||
return receiverType;
|
||||
}
|
||||
|
||||
public FieldInfo getResolvedField() {
|
||||
return resolvedField;
|
||||
}
|
||||
|
||||
public boolean isStaticAccess() {
|
||||
return isStaticAccess;
|
||||
}
|
||||
|
||||
public boolean isEnumConstantAccess() {
|
||||
return resolvedField != null && resolvedField.isEnumConstant();
|
||||
}
|
||||
|
||||
public TypeInfo getExpectedType() {
|
||||
return expectedType;
|
||||
}
|
||||
|
||||
public void setExpectedType(TypeInfo expectedType) {
|
||||
this.expectedType = expectedType;
|
||||
}
|
||||
|
||||
public ErrorType getErrorType() {
|
||||
return errorType;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate this field access.
|
||||
* Checks type compatibility with expected type.
|
||||
*/
|
||||
public void validate() {
|
||||
// Check if field was resolved
|
||||
if (resolvedField == null) {
|
||||
setError(ErrorType.UNRESOLVED_FIELD, "Cannot resolve field '" + fieldName + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check return type compatibility with expected type (e.g., assignment LHS)
|
||||
if (expectedType != null && resolvedField != null) {
|
||||
TypeInfo fieldType = resolvedField.getTypeInfo();
|
||||
if (fieldType != null && !TypeChecker.isTypeCompatible(expectedType, fieldType)) {
|
||||
//extra space is necessary for alignment
|
||||
//setError(ErrorType.TYPE_MISMATCH, "Provided type: " + fieldType.getSimpleName()+
|
||||
//"\nRequired: " + expectedType.getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setError(ErrorType type, String message) {
|
||||
this.errorType = type;
|
||||
this.errorMessage = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this field access has any validation error.
|
||||
*/
|
||||
public boolean hasError() {
|
||||
return errorType != ErrorType.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a type mismatch error.
|
||||
*/
|
||||
public boolean hasTypeMismatch() {
|
||||
return errorType == ErrorType.TYPE_MISMATCH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is an unresolved field error.
|
||||
*/
|
||||
public boolean hasUnresolvedField() {
|
||||
return errorType == ErrorType.UNRESOLVED_FIELD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a static access error.
|
||||
*/
|
||||
public boolean hasStaticAccessError() {
|
||||
return errorType == ErrorType.STATIC_ACCESS_ERROR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FieldAccessInfo{" +
|
||||
"fieldName='" + fieldName + "', " +
|
||||
"receiverType=" + receiverType + ", " +
|
||||
"resolvedField=" + resolvedField + ", " +
|
||||
"errorType=" + errorType +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,616 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.field;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.ScopeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo;
|
||||
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.bridge.DtsJavaBridge;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.token.TokenType;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.GenericContext;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeSubstitutor;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Metadata for a field (variable) declaration or reference.
|
||||
* Tracks the field name, type, scope, and declaration location.
|
||||
* Also tracks all assignments made to this field throughout the script.
|
||||
*/
|
||||
public final class FieldInfo {
|
||||
|
||||
public enum Scope {
|
||||
GLOBAL, // Class-level field
|
||||
LOCAL, // Local variable inside a method
|
||||
PARAMETER, // Method parameter
|
||||
ENUM_CONSTANT // Enum constant value
|
||||
}
|
||||
|
||||
private final String name;
|
||||
private final Scope scope;
|
||||
private final TypeInfo declaredType; // The declared type (e.g., String, List<Integer>)
|
||||
private final int declarationOffset; // Where this field was declared in the source
|
||||
private final boolean resolved;
|
||||
private final String documentation; // Javadoc/comment documentation for this field
|
||||
private JSDocInfo jsDocInfo; // Parsed JSDoc info for this method (may be null)
|
||||
|
||||
// Initialization value range (for displaying "= value" in hover info)
|
||||
private final int initStart; // Position of '=' or -1 if no initializer
|
||||
private final int initEnd; // Position after initializer (before ';') or -1
|
||||
|
||||
// For local/parameter fields, track the containing method
|
||||
private final MethodInfo containingMethod;
|
||||
|
||||
// Modifiers for script-defined fields
|
||||
private final int modifiers; // Java Modifier flags (e.g., Modifier.FINAL)
|
||||
|
||||
// Reflection field for external types
|
||||
private final Field reflectionField;
|
||||
|
||||
private ScopeInfo scopeInfo;
|
||||
|
||||
// Assignments to this field (populated after parsing)
|
||||
private final List<AssignmentInfo> assignments = new ArrayList<>();
|
||||
|
||||
// Declaration assignment (for initial value validation)
|
||||
private AssignmentInfo declarationAssignment;
|
||||
|
||||
// Enum constant specific info
|
||||
private EnumConstantInfo enumConstantInfo;
|
||||
|
||||
private boolean isVarArg = false;
|
||||
|
||||
// Type inference support (for JavaScript "any" typed variables)
|
||||
// When a variable is declared without type (var x;), it starts as "any" but can be
|
||||
// refined through assignments or JSDoc comments
|
||||
private TypeInfo inferredType;
|
||||
|
||||
private FieldInfo(String name, Scope scope, TypeInfo declaredType,
|
||||
int declarationOffset, boolean resolved, MethodInfo containingMethod,
|
||||
String documentation, int initStart, int initEnd, int modifiers,
|
||||
Field reflectionField) {
|
||||
this.name = name;
|
||||
this.scope = scope;
|
||||
this.declaredType = declaredType;
|
||||
this.declarationOffset = declarationOffset;
|
||||
this.resolved = resolved;
|
||||
this.containingMethod = containingMethod;
|
||||
this.documentation = documentation;
|
||||
this.initStart = initStart;
|
||||
this.initEnd = initEnd;
|
||||
this.modifiers = modifiers;
|
||||
this.reflectionField = reflectionField;
|
||||
}
|
||||
|
||||
// Factory methods
|
||||
public static FieldInfo globalField(String name, TypeInfo type, int declOffset) {
|
||||
return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, null, -1,
|
||||
-1, 0, null);
|
||||
}
|
||||
|
||||
public static FieldInfo globalField(String name, TypeInfo type, int declOffset, String documentation) {
|
||||
return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null,
|
||||
documentation, -1, -1, 0, null);
|
||||
}
|
||||
|
||||
public static FieldInfo globalField(String name, TypeInfo type, int declOffset, String documentation, int initStart, int initEnd) {
|
||||
return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null,
|
||||
documentation, initStart, initEnd, 0, null);
|
||||
}
|
||||
|
||||
public static FieldInfo globalField(String name, TypeInfo type, int declOffset, String documentation, int initStart,
|
||||
int initEnd, int modifiers) {
|
||||
return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null,
|
||||
documentation, initStart, initEnd, modifiers, null);
|
||||
}
|
||||
|
||||
public static FieldInfo localField(String name, TypeInfo type, int declOffset, MethodInfo method) {
|
||||
return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null, -1,
|
||||
-1, 0, null);
|
||||
}
|
||||
|
||||
public static FieldInfo localField(String name, TypeInfo type, int declOffset, MethodInfo method, int initStart, int initEnd) {
|
||||
return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null,
|
||||
initStart, initEnd, 0, null);
|
||||
}
|
||||
|
||||
public static FieldInfo localField(String name, TypeInfo type, int declOffset, MethodInfo method, int initStart,
|
||||
int initEnd, int modifiers) {
|
||||
return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null,
|
||||
initStart, initEnd, modifiers, null);
|
||||
}
|
||||
|
||||
public static FieldInfo parameter(String name, TypeInfo type, int declOffset, MethodInfo method) {
|
||||
return new FieldInfo(name, Scope.PARAMETER, type, declOffset, type != null && type.isResolved(), method, null,
|
||||
-1, -1, 0, null);
|
||||
}
|
||||
|
||||
public static FieldInfo unresolved(String name, Scope scope) {
|
||||
return new FieldInfo(name, scope, null, -1, false, null, null, -1, -1, 0, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a FieldInfo from reflection data for method parameters.
|
||||
*/
|
||||
public static FieldInfo reflectionParam(String name, TypeInfo type) {
|
||||
return new FieldInfo(name, Scope.PARAMETER, type, -1, true, null, null, -1, -1, 0, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a FieldInfo for a synthetic/external field.
|
||||
* Used for built-in types like Nashorn's Java object.
|
||||
*/
|
||||
public static FieldInfo external(String name, TypeInfo type, String documentation, int modifiers) {
|
||||
return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, documentation, -1, -1, modifiers, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a FieldInfo from reflection data for a class field.
|
||||
* Preserves generic type information like List<String>.
|
||||
*/
|
||||
public static FieldInfo fromReflection(Field field, TypeInfo containingType) {
|
||||
String name = field.getName();
|
||||
// Use getGenericType() to preserve generic information
|
||||
TypeInfo type = TypeInfo.fromGenericType(field.getGenericType());
|
||||
if (type == null) {
|
||||
type = TypeInfo.fromClass(field.getType());
|
||||
}
|
||||
|
||||
// If the receiver is parameterized, substitute class type variables in the field type
|
||||
if (GenericContext.hasGenerics(containingType)) {
|
||||
Map<String, TypeInfo> receiverBindings = TypeSubstitutor.createBindingsFromReceiver(containingType);
|
||||
if (!receiverBindings.isEmpty()) {
|
||||
type = TypeSubstitutor.substitute(type, receiverBindings);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is an enum constant
|
||||
if (field.isEnumConstant()) {
|
||||
// Create enum constant FieldInfo
|
||||
EnumConstantInfo constantInfo = EnumConstantInfo.fromReflection(name, containingType,field);
|
||||
if (constantInfo != null)
|
||||
return constantInfo.getFieldInfo();
|
||||
}
|
||||
|
||||
// Try to find matching JSFieldInfo to bridge over documentation
|
||||
JSFieldInfo jsField = DtsJavaBridge.findMatchingField(field, containingType);
|
||||
String documentation = null;
|
||||
JSDocInfo jsDocInfo = null;
|
||||
if (jsField != null) {
|
||||
jsDocInfo = jsField.getJsDocInfo();
|
||||
String jsDocDesc = jsDocInfo != null ? jsDocInfo.getDescription() : null;
|
||||
documentation = jsDocDesc != null ? jsDocDesc : jsField.getDocumentation();
|
||||
}
|
||||
|
||||
FieldInfo fieldInfo = new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, documentation, -1, -1, field.getModifiers(), field);
|
||||
if (jsDocInfo != null) {
|
||||
fieldInfo.setJSDocInfo(jsDocInfo);
|
||||
}
|
||||
return fieldInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a FieldInfo from a JSFieldInfo (parsed from .d.ts files).
|
||||
* Used when resolving field access on JavaScript types.
|
||||
*
|
||||
* @param jsField The JavaScript field info from the type registry
|
||||
* @param containingType The TypeInfo that owns this field
|
||||
* @return A FieldInfo representing the JavaScript field
|
||||
*/
|
||||
public static FieldInfo fromJSField(JSFieldInfo jsField, TypeInfo containingType) {
|
||||
String name = jsField.getName();
|
||||
|
||||
TypeInfo type = jsField.getResolvedType(containingType);
|
||||
if (type == null || !type.isResolved()) {
|
||||
type = resolveJSType(jsField.getType());
|
||||
}
|
||||
|
||||
// JS fields are public by default, readonly maps to final
|
||||
int modifiers = Modifier.PUBLIC;
|
||||
if (jsField.isReadonly()) {
|
||||
modifiers |= Modifier.FINAL;
|
||||
}
|
||||
if (jsField.isStatic()) {
|
||||
modifiers |= Modifier.STATIC;
|
||||
}
|
||||
|
||||
// Use documentation if available
|
||||
JSDocInfo jsDocInfo = jsField.getJsDocInfo();
|
||||
String jsDocDesc = jsDocInfo != null ? jsDocInfo.getDescription() : null;
|
||||
String documentation = jsDocDesc != null ? jsDocDesc : jsField.getDocumentation();
|
||||
|
||||
FieldInfo fieldInfo = new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, documentation, -1, -1, modifiers, null);
|
||||
fieldInfo.setJSDocInfo(jsDocInfo);
|
||||
return fieldInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a JavaScript type name to a TypeInfo.
|
||||
* Handles primitives, mapped types, and custom types from the registry.
|
||||
*/
|
||||
private static TypeInfo resolveJSType(String jsTypeName) {
|
||||
if (jsTypeName == null || jsTypeName.isEmpty() || "void".equals(jsTypeName)) {
|
||||
return TypeInfo.fromPrimitive("void");
|
||||
}
|
||||
|
||||
// Handle JS primitives
|
||||
switch (jsTypeName) {
|
||||
case "string":
|
||||
return TypeInfo.fromClass(String.class);
|
||||
case "number":
|
||||
return TypeInfo.fromClass(double.class);
|
||||
case "boolean":
|
||||
return TypeInfo.fromClass(boolean.class);
|
||||
case "any":
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
case "void":
|
||||
return TypeInfo.fromPrimitive("void");
|
||||
}
|
||||
|
||||
// Handle array types
|
||||
if (jsTypeName.endsWith("[]")) {
|
||||
String elementType = jsTypeName.substring(0, jsTypeName.length() - 2);
|
||||
TypeInfo elementTypeInfo = resolveJSType(elementType);
|
||||
return TypeInfo.arrayOf(elementTypeInfo);
|
||||
}
|
||||
|
||||
// Try to resolve from the JS type registry
|
||||
JSTypeRegistry registry = JSTypeRegistry.getInstance();
|
||||
if (registry != null) {
|
||||
JSTypeInfo jsTypeInfo = registry.getType(jsTypeName);
|
||||
if (jsTypeInfo != null) {
|
||||
return TypeInfo.fromJSTypeInfo(jsTypeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: unresolved type
|
||||
return TypeInfo.unresolved(jsTypeName, jsTypeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a FieldInfo for an enum constant.
|
||||
* @param name The constant name (e.g., "NORTH")
|
||||
* @param type The enum type itself
|
||||
* @param declOffset The declaration position
|
||||
* @param initStart The position of '(' if args present, else -1
|
||||
* @param initEnd The position after ')' if args present, else -1
|
||||
*/
|
||||
public static FieldInfo enumConstant(String name, TypeInfo type, int declOffset, int initStart, int initEnd,Field javaField) {
|
||||
// Enum constants are implicitly public static final
|
||||
int modifiers = Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL;
|
||||
return new FieldInfo(name, Scope.ENUM_CONSTANT, type, declOffset, true, null, null, initStart, initEnd,
|
||||
modifiers, javaField);
|
||||
}
|
||||
|
||||
// ==================== ENUM HANDLING ====================
|
||||
|
||||
public void setEnumConstantInfo(EnumConstantInfo enumConstantInfo) {
|
||||
this.enumConstantInfo = enumConstantInfo;
|
||||
}
|
||||
|
||||
public EnumConstantInfo getEnumInfo() {
|
||||
return enumConstantInfo;
|
||||
}
|
||||
/**
|
||||
* Check if this is an enum constant.
|
||||
*/
|
||||
public boolean isEnumConstant() {
|
||||
return scope == Scope.ENUM_CONSTANT;
|
||||
}
|
||||
|
||||
|
||||
// ==================== ASSIGNMENT MANAGEMENT ====================
|
||||
|
||||
/**
|
||||
* Add an assignment to this field.
|
||||
*/
|
||||
public void addAssignment(AssignmentInfo assignment) {
|
||||
assignments.add(assignment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all assignments to this field (unmodifiable).
|
||||
*/
|
||||
public List<AssignmentInfo> getAssignments() {
|
||||
return Collections.unmodifiableList(assignments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the declaration assignment (initial value assignment).
|
||||
*/
|
||||
public void setDeclarationAssignment(AssignmentInfo assignment) {
|
||||
this.declarationAssignment = assignment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the declaration assignment (initial value assignment).
|
||||
* Returns null if there was no initializer or it hasn't been validated yet.
|
||||
*/
|
||||
public AssignmentInfo getDeclarationAssignment() {
|
||||
return declarationAssignment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an assignment that contains the given position.
|
||||
* Returns null if no assignment contains this position.
|
||||
* Prioritizes LHS matches over RHS matches.
|
||||
*/
|
||||
public AssignmentInfo findAssignmentAtPosition(int position) {
|
||||
// Check declaration assignment first
|
||||
if (declarationAssignment != null && declarationAssignment.containsPosition(position)) {
|
||||
// Prioritize LHS match
|
||||
if (declarationAssignment.containsLhsPosition(position)) {
|
||||
return declarationAssignment;
|
||||
}
|
||||
}
|
||||
|
||||
// Check other assignments for LHS matches first (more specific)
|
||||
for (AssignmentInfo assign : assignments) {
|
||||
if (assign.containsLhsPosition(position)) {
|
||||
return assign;
|
||||
}
|
||||
}
|
||||
|
||||
// Then check RHS matches (less specific - might just be a reference in the value)
|
||||
if (declarationAssignment != null && declarationAssignment.containsRhsPosition(position)) {
|
||||
return declarationAssignment;
|
||||
}
|
||||
|
||||
for (AssignmentInfo assign : assignments) {
|
||||
if (assign.containsRhsPosition(position)) {
|
||||
return assign;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all errored assignments for this field.
|
||||
*/
|
||||
public List<AssignmentInfo> getErroredAssignments() {
|
||||
List<AssignmentInfo> errored = new ArrayList<>();
|
||||
|
||||
// Include declaration assignment if it has an error
|
||||
if (declarationAssignment != null && declarationAssignment.hasError()) {
|
||||
errored.add(declarationAssignment);
|
||||
}
|
||||
|
||||
// Include all other assignments with errors
|
||||
for (AssignmentInfo assign : assignments) {
|
||||
if (assign.hasError()) {
|
||||
errored.add(assign);
|
||||
}
|
||||
}
|
||||
return errored;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all assignments (for re-parsing).
|
||||
*/
|
||||
public void clearAssignments() {
|
||||
assignments.clear();
|
||||
declarationAssignment = null;
|
||||
}
|
||||
|
||||
// ==================== MODIFIER CHECKS ====================
|
||||
|
||||
/**
|
||||
* Check if this field is declared as final.
|
||||
* Works for both script-defined fields (via modifiers) and reflection fields.
|
||||
*/
|
||||
public boolean isFinal() {
|
||||
if (reflectionField != null)
|
||||
return Modifier.isFinal(reflectionField.getModifiers());
|
||||
|
||||
return Modifier.isFinal(modifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this field is declared as static.
|
||||
*/
|
||||
public boolean isStatic() {
|
||||
if (reflectionField != null)
|
||||
return Modifier.isStatic(reflectionField.getModifiers());
|
||||
|
||||
return Modifier.isStatic(modifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this field is declared as private.
|
||||
*/
|
||||
public boolean isPrivate() {
|
||||
if (reflectionField != null)
|
||||
return Modifier.isPrivate(reflectionField.getModifiers());
|
||||
|
||||
return Modifier.isPrivate(modifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this field is declared as protected.
|
||||
*/
|
||||
public boolean isProtected() {
|
||||
if (reflectionField != null)
|
||||
return Modifier.isProtected(reflectionField.getModifiers());
|
||||
|
||||
return Modifier.isProtected(modifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this field is declared as public.
|
||||
*/
|
||||
public boolean isPublic() {
|
||||
if (reflectionField != null)
|
||||
return Modifier.isPublic(reflectionField.getModifiers());
|
||||
|
||||
return Modifier.isPublic(modifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw modifiers value.
|
||||
*/
|
||||
public int getModifiers() {
|
||||
if (reflectionField != null)
|
||||
return reflectionField.getModifiers();
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reflection field, if available.
|
||||
*/
|
||||
public java.lang.reflect.Field getReflectionField() {
|
||||
return reflectionField;
|
||||
}
|
||||
|
||||
// ==================== TYPE INFERENCE ====================
|
||||
|
||||
/**
|
||||
* Get the inferred type for this field.
|
||||
* This is set when a variable declared as "any" has its type refined through
|
||||
* assignment analysis or JSDoc comments.
|
||||
*/
|
||||
public TypeInfo getInferredType() {
|
||||
return inferredType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the inferred type for this field.
|
||||
* Used when type inference determines a more specific type than the declared type.
|
||||
* @param inferredType The refined type based on assignments or JSDoc
|
||||
*/
|
||||
public void setVarArg(boolean isVarArg) { this.isVarArg = isVarArg; }
|
||||
public boolean isVarArg() { return isVarArg; }
|
||||
|
||||
public void setInferredType(TypeInfo inferredType) {
|
||||
this.inferredType = inferredType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this field has a type that can be refined (currently "any" type).
|
||||
*/
|
||||
public boolean canInferType() {
|
||||
return declaredType != null && "any".equals(declaredType.getFullName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective type for this field, considering inference.
|
||||
* Returns inferredType if available, otherwise declaredType.
|
||||
*/
|
||||
public TypeInfo getEffectiveType() {
|
||||
if (inferredType != null) {
|
||||
return inferredType;
|
||||
}
|
||||
return declaredType;
|
||||
}
|
||||
|
||||
// ==================== BASIC GETTERS ====================
|
||||
|
||||
public String getName() { return name; }
|
||||
public Scope getScope() { return scope; }
|
||||
public TypeInfo getDeclaredType() { return declaredType; }
|
||||
public TypeInfo getTypeInfo() {
|
||||
// Return the effective type (inferred if available, otherwise declared)
|
||||
return getEffectiveType();
|
||||
}
|
||||
public int getDeclarationOffset() { return declarationOffset; }
|
||||
public boolean isResolved() {
|
||||
// Check if we have any resolved type (declared or inferred)
|
||||
TypeInfo effectiveType = getEffectiveType();
|
||||
return effectiveType != null && effectiveType.isResolved();
|
||||
}
|
||||
public MethodInfo getContainingMethod() { return containingMethod; }
|
||||
public String getDocumentation() { return documentation; }
|
||||
public JSDocInfo getJSDocInfo() { return jsDocInfo; }
|
||||
public void setJSDocInfo(JSDocInfo jsDocInfo) { this.jsDocInfo = jsDocInfo; }
|
||||
public int getInitStart() { return initStart; }
|
||||
public int getInitEnd() { return initEnd; }
|
||||
public boolean hasInitializer() { return initStart >= 0 && initEnd > initStart; }
|
||||
|
||||
public boolean isGlobal() { return scope == Scope.GLOBAL; }
|
||||
public boolean isLocal() { return scope == Scope.LOCAL; }
|
||||
public boolean isParameter() { return scope == Scope.PARAMETER; }
|
||||
|
||||
/**
|
||||
* Check if a reference at the given position can see this field.
|
||||
* For local variables, they're only visible after their declaration.
|
||||
*/
|
||||
public boolean isVisibleAt(int position) {
|
||||
if (scopeInfo != null && !scopeInfo.containsPosition(position)) {
|
||||
return false;
|
||||
}
|
||||
if (scope == Scope.GLOBAL || scope == Scope.PARAMETER) {
|
||||
return true; // Always visible in their scope
|
||||
}
|
||||
// Local variables are only visible after declaration
|
||||
return position >= declarationOffset;
|
||||
}
|
||||
|
||||
public ScopeInfo getScopeInfo() { return scopeInfo; }
|
||||
public void setScopeInfo(ScopeInfo scopeInfo) { this.scopeInfo = scopeInfo; }
|
||||
|
||||
/**
|
||||
* Get the appropriate TokenType for highlighting this field.
|
||||
*/
|
||||
public TokenType getTokenType() {
|
||||
if (!resolved) {
|
||||
return TokenType.UNDEFINED_VAR;
|
||||
}
|
||||
switch (scope) {
|
||||
case GLOBAL:
|
||||
// Static final fields get special highlighting
|
||||
if (isStatic() && isFinal()) {
|
||||
return TokenType.STATIC_FINAL_FIELD;
|
||||
}
|
||||
return TokenType.GLOBAL_FIELD;
|
||||
case LOCAL:
|
||||
return TokenType.LOCAL_FIELD;
|
||||
case PARAMETER:
|
||||
return TokenType.PARAMETER;
|
||||
case ENUM_CONSTANT:
|
||||
return TokenType.ENUM_CONSTANT;
|
||||
default:
|
||||
return TokenType.VARIABLE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FieldInfo{" + name + ", " + scope + ", type=" + declaredType + ", final=" + isFinal() + "}";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
FieldInfo fieldInfo = (FieldInfo) o;
|
||||
return name.equals(fieldInfo.name) && scope == fieldInfo.scope;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode() * 31 + scope.ordinal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class that holds both FieldInfo and MethodCallInfo for a variable usage.
|
||||
* This is used when a variable is used as an argument to a method call.
|
||||
*/
|
||||
public static class ArgInfo {
|
||||
public final FieldInfo fieldInfo;
|
||||
public final MethodCallInfo methodCallInfo;
|
||||
|
||||
public ArgInfo(FieldInfo fieldInfo, MethodCallInfo methodCallInfo) {
|
||||
this.fieldInfo = fieldInfo;
|
||||
this.methodCallInfo = methodCallInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.hover;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Gui;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import noppes.npcs.client.ClientProxy;
|
||||
import noppes.npcs.client.gui.util.GuiUtil;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.token.TokenType;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Renders gutter icons for method inheritance (override/implements) in the script editor.
|
||||
*
|
||||
* Renders IntelliJ-style tooltips when hovering over gutter icons:
|
||||
* - Background + border matching TokenHoverRenderer style
|
||||
* - Colored declaration showing which class/interface the method overrides/implements
|
||||
* - Uses the same token highlighting system for type names
|
||||
*/
|
||||
public class GutterIconRenderer {
|
||||
|
||||
// ==================== CONSTANTS ====================
|
||||
|
||||
/** Texture resource for script icons (64x32: first 32x32 = override, second 32x32 = implements) */
|
||||
private static final ResourceLocation SCRIPT_ICONS = new ResourceLocation("customnpcs",
|
||||
"textures/gui/script/icons.png");
|
||||
|
||||
/** Icon size when rendered in the gutter (scaled from 32x32) */
|
||||
private static final int GUTTER_ICON_SIZE = 10;
|
||||
|
||||
/** Extra gutter width to accommodate inheritance icons */
|
||||
public static final int ICON_GUTTER_WIDTH = 12;
|
||||
|
||||
/** Padding inside the tooltip box */
|
||||
private static final int PADDING = 6;
|
||||
|
||||
/** Line spacing between rows */
|
||||
private static final int LINE_SPACING = 2;
|
||||
|
||||
/** Vertical offset from mouse */
|
||||
private static final int VERTICAL_OFFSET = 10;
|
||||
|
||||
// ==================== COLORS ====================
|
||||
|
||||
/** Background color (dark gray like IntelliJ) */
|
||||
private static final int BG_COLOR = 0xF0313335;
|
||||
|
||||
/** Border color */
|
||||
private static final int BORDER_COLOR = 0xFF3C3F41;
|
||||
|
||||
/** Info text color */
|
||||
private static final int INFO_COLOR = 0xFFA9B7C6;
|
||||
|
||||
// ==================== RENDERING ====================
|
||||
|
||||
/**
|
||||
* Render gutter icons for a range of lines.
|
||||
*
|
||||
* @param lineHeight Height of each line
|
||||
* @param gutterX X position for the icon in the gutter
|
||||
* @param gutterY Base Y position of the gutter
|
||||
* @param renderStart First line index to render
|
||||
* @param renderEnd Last line index to render
|
||||
* @param scrolledLine Current scroll position
|
||||
* @param stringYOffset Y offset for text positioning
|
||||
* @param methods All methods in the document
|
||||
* @param lines Line data for position calculation
|
||||
* @param xMouse Mouse X position
|
||||
* @param yMouse Mouse Y position
|
||||
* @param fracPixels Fractional scroll offset
|
||||
* @return The hovered method, or null if no icon is hovered
|
||||
*/
|
||||
public static MethodInfo renderIcons(
|
||||
int lineHeight,
|
||||
int gutterX,
|
||||
int gutterY,
|
||||
int renderStart,
|
||||
int renderEnd,
|
||||
int scrolledLine,
|
||||
int stringYOffset,
|
||||
List<MethodInfo> methods,
|
||||
List<?> lines,
|
||||
int xMouse,
|
||||
int yMouse,
|
||||
float fracPixels) {
|
||||
|
||||
if (methods == null || methods.isEmpty())
|
||||
return null;
|
||||
|
||||
MethodInfo hoveredMethod = null;
|
||||
float adjustedMouseY = yMouse + fracPixels;
|
||||
|
||||
for (int lineIndex = renderStart; lineIndex <= renderEnd; lineIndex++) {
|
||||
MethodInfo method = getMethodAtLine(lineIndex, methods, lines);
|
||||
if (method == null || !method.hasInheritanceMarker())
|
||||
continue;
|
||||
|
||||
int posY = gutterY + (lineIndex - scrolledLine) * lineHeight + stringYOffset;
|
||||
int iconY = posY + (lineHeight - GUTTER_ICON_SIZE) / 2 - 1;
|
||||
|
||||
// Draw the icon
|
||||
renderIcon(gutterX, iconY, method.isOverride());
|
||||
|
||||
// Check for hover
|
||||
int iconScaleOffsetX = -4;
|
||||
int screenPosY = gutterY + (lineIndex - scrolledLine) * lineHeight;
|
||||
if (xMouse >= gutterX + iconScaleOffsetX && xMouse < gutterX + GUTTER_ICON_SIZE + iconScaleOffsetX &&
|
||||
adjustedMouseY >= screenPosY && adjustedMouseY < screenPosY + lineHeight) {
|
||||
hoveredMethod = method;
|
||||
}
|
||||
}
|
||||
|
||||
return hoveredMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single gutter icon.
|
||||
*/
|
||||
private static void renderIcon(int x, int y, boolean isOverride) {
|
||||
int iconU = isOverride ? 0 : 32;
|
||||
|
||||
Minecraft.getMinecraft().renderEngine.bindTexture(SCRIPT_ICONS);
|
||||
GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
|
||||
GL11.glPushMatrix();
|
||||
float scale = 2.0f, scaleOffsetX = -4, scaleOffsetY = -3.25f;
|
||||
GL11.glScalef(scale, scale, scale);
|
||||
GL11.glTranslatef(scaleOffsetX, scaleOffsetY, 0);
|
||||
GuiUtil.drawScaledTexturedRect(
|
||||
(int) (x / scale), (int) (y / scale),
|
||||
iconU, 0, 32, 32,
|
||||
GUTTER_ICON_SIZE, GUTTER_ICON_SIZE,
|
||||
64, 32
|
||||
);
|
||||
GL11.glPopMatrix();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the tooltip for a hovered gutter icon.
|
||||
*/
|
||||
public static void renderTooltip(MethodInfo method, int mouseX, int mouseY, int viewportX, int viewportWidth,
|
||||
int viewportY, int viewportHeight) {
|
||||
if (method == null)
|
||||
return;
|
||||
|
||||
// Build tooltip content
|
||||
List<TextSegment> segments = buildTooltipContent(method);
|
||||
if (segments.isEmpty())
|
||||
return;
|
||||
|
||||
int lineHeight = ClientProxy.Font.height();
|
||||
|
||||
// Calculate dimensions
|
||||
int contentWidth = calculateContentWidth(segments);
|
||||
int contentHeight = lineHeight;
|
||||
int boxWidth = contentWidth + PADDING * 2;
|
||||
int boxHeight = contentHeight + PADDING * 2;
|
||||
|
||||
// Position tooltip
|
||||
int tooltipX = mouseX + VERTICAL_OFFSET;
|
||||
int tooltipY = mouseY - 5;
|
||||
|
||||
// Clamp to viewport
|
||||
int rightBound = viewportX + viewportWidth;
|
||||
int bottomBound = viewportY + viewportHeight;
|
||||
|
||||
if (tooltipX + boxWidth > rightBound) {
|
||||
tooltipX = mouseX - boxWidth - 5;
|
||||
}
|
||||
if (tooltipX < viewportX) {
|
||||
tooltipX = viewportX;
|
||||
}
|
||||
|
||||
if (tooltipY + boxHeight > bottomBound) {
|
||||
tooltipY = bottomBound - boxHeight;
|
||||
}
|
||||
if (tooltipY < viewportY) {
|
||||
tooltipY = viewportY;
|
||||
}
|
||||
|
||||
// Render tooltip box
|
||||
renderTooltipBox(tooltipX, tooltipY, boxWidth, boxHeight, segments);
|
||||
}
|
||||
|
||||
// ==================== PRIVATE HELPERS ====================
|
||||
|
||||
/**
|
||||
* Get the method that starts on the given line.
|
||||
*/
|
||||
private static MethodInfo getMethodAtLine(int lineIndex, List<MethodInfo> methods, List<?> lines) {
|
||||
if (lineIndex < 0 || lineIndex >= lines.size())
|
||||
return null;
|
||||
|
||||
Object lineObj = lines.get(lineIndex);
|
||||
int lineStart, lineEnd;
|
||||
|
||||
try {
|
||||
java.lang.reflect.Field startField = lineObj.getClass().getField("start");
|
||||
java.lang.reflect.Field endField = lineObj.getClass().getField("end");
|
||||
lineStart = startField.getInt(lineObj);
|
||||
lineEnd = endField.getInt(lineObj);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (MethodInfo method : methods) {
|
||||
if (!method.hasInheritanceMarker())
|
||||
continue;
|
||||
int nameOffset = method.getNameOffset();
|
||||
if (nameOffset >= lineStart && nameOffset < lineEnd) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the tooltip content as colored text segments.
|
||||
*/
|
||||
private static List<TextSegment> buildTooltipContent(MethodInfo method) {
|
||||
List<TextSegment> segments = new ArrayList<>();
|
||||
|
||||
if (method.isOverride()) {
|
||||
TypeInfo overridesFrom = method.getOverridesFrom();
|
||||
segments.add(new TextSegment("Overrides method in ", INFO_COLOR));
|
||||
|
||||
if (overridesFrom != null) {
|
||||
int color = TokenType.getColor(overridesFrom);
|
||||
segments.add(new TextSegment(overridesFrom.getSimpleName(), color));
|
||||
} else {
|
||||
segments.add(new TextSegment("parent class", INFO_COLOR));
|
||||
}
|
||||
} else if (method.isImplements()) {
|
||||
TypeInfo implementsFrom = method.getImplementsFrom();
|
||||
segments.add(new TextSegment("Implements method from ", INFO_COLOR));
|
||||
|
||||
if (implementsFrom != null) {
|
||||
int color = TokenType.getColor(implementsFrom);
|
||||
segments.add(new TextSegment(implementsFrom.getSimpleName(), color));
|
||||
} else {
|
||||
segments.add(new TextSegment("interface", INFO_COLOR));
|
||||
}
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the width needed for the segments.
|
||||
*/
|
||||
private static int calculateContentWidth(List<TextSegment> segments) {
|
||||
int totalWidth = 0;
|
||||
for (TextSegment segment : segments) {
|
||||
totalWidth += ClientProxy.Font.width(segment.text);
|
||||
}
|
||||
return totalWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the tooltip box with background, border, and content.
|
||||
*/
|
||||
private static void renderTooltipBox(int x, int y, int width, int height, List<TextSegment> segments) {
|
||||
GL11.glDisable(GL11.GL_SCISSOR_TEST);
|
||||
|
||||
// Draw background
|
||||
int paddedHeight = y + height - 4;
|
||||
Gui.drawRect(x, y, x + width, paddedHeight, BG_COLOR);
|
||||
|
||||
// Draw border
|
||||
Gui.drawRect(x, y, x + width, y + 1, BORDER_COLOR); // Top
|
||||
Gui.drawRect(x, paddedHeight - 1, x + width, paddedHeight, BORDER_COLOR); // Bottom
|
||||
Gui.drawRect(x, y, x + 1, paddedHeight, BORDER_COLOR); // Left
|
||||
Gui.drawRect(x + width - 1, y, x + width, paddedHeight, BORDER_COLOR); // Right
|
||||
|
||||
// Draw text segments
|
||||
int currentX = x + PADDING;
|
||||
int currentY = y + PADDING;
|
||||
|
||||
for (TextSegment segment : segments) {
|
||||
ClientProxy.Font.drawString(segment.text, currentX, currentY, 0xFF000000 | segment.color);
|
||||
currentX += ClientProxy.Font.width(segment.text);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== DATA CLASSES ====================
|
||||
|
||||
/**
|
||||
* A colored text segment.
|
||||
*/
|
||||
private static class TextSegment {
|
||||
final String text;
|
||||
final int color;
|
||||
|
||||
TextSegment(String text, int color) {
|
||||
this.text = text;
|
||||
this.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.hover;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.ScriptLine;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.token.Token;
|
||||
|
||||
/**
|
||||
* Manages the hover state for token tooltips in the script editor.
|
||||
*
|
||||
* Tracks:
|
||||
* - Current mouse position
|
||||
* - Hovered token (if any)
|
||||
* - Hover duration for delayed tooltip display
|
||||
* - Whether tooltip should be visible
|
||||
*
|
||||
* Implements a 500ms delay before showing tooltips, resetting when
|
||||
* the mouse moves to a different token.
|
||||
*/
|
||||
public class HoverState {
|
||||
|
||||
/** Minimum hover time (ms) before showing tooltip */
|
||||
private static final long HOVER_DELAY_MS = 100;
|
||||
|
||||
// ==================== STATE ====================
|
||||
|
||||
/** Currently hovered token (null if none) */
|
||||
private Token hoveredToken;
|
||||
|
||||
/** Time when hover on current token started */
|
||||
private long hoverStartTime;
|
||||
|
||||
/** Whether the tooltip should currently be displayed */
|
||||
private boolean tooltipVisible;
|
||||
|
||||
/** Cached hover info for the current token */
|
||||
private TokenHoverInfo hoverInfo;
|
||||
|
||||
/** Last known mouse position */
|
||||
private int lastMouseX;
|
||||
private int lastMouseY;
|
||||
|
||||
/** Position of the hovered token (for tooltip positioning) */
|
||||
private int tokenScreenX;
|
||||
private int tokenScreenY;
|
||||
private int tokenWidth;
|
||||
|
||||
/** Tooltip bounding box (set after each render frame, used for mouse-over detection) */
|
||||
private int tooltipBoxX, tooltipBoxY, tooltipBoxWidth, tooltipBoxHeight;
|
||||
|
||||
/** Also store the actual tooltip panel rect (not including the token-gap extension) */
|
||||
private int tooltipPanelX, tooltipPanelY, tooltipPanelW, tooltipPanelH;
|
||||
|
||||
/** Scrollbar thumb rect (set each render frame, used for drag/hover detection) */
|
||||
private int scrollbarThumbX, scrollbarThumbY, scrollbarThumbH;
|
||||
private int scrollbarX;
|
||||
private int scrollbarTrackTop;
|
||||
private int scrollbarTrackHeight;
|
||||
private boolean isDraggingScrollbar;
|
||||
private int dragStartMouseY;
|
||||
private int dragStartScrollOffset;
|
||||
|
||||
/** Tooltip panel drag state */
|
||||
private boolean isDraggingTooltip;
|
||||
private int tooltipDragOffsetX, tooltipDragOffsetY;
|
||||
private boolean tooltipPositionOverridden;
|
||||
private int overriddenTooltipX, overriddenTooltipY;
|
||||
|
||||
/** Tooltip panel resize state */
|
||||
private boolean isResizingTooltip;
|
||||
private int resizeInitMouseX, resizeInitMouseY;
|
||||
private int resizeInitW, resizeInitH;
|
||||
private boolean tooltipSizeOverridden;
|
||||
private int overriddenTooltipW, overriddenTooltipH;
|
||||
/** Minimum tooltip dimensions when user-resized */
|
||||
private static final int MIN_TOOLTIP_W = 80;
|
||||
private static final int MIN_TOOLTIP_H = 30;
|
||||
|
||||
/** Smooth scroll: target offset (pixels), current is animated toward it each frame */
|
||||
private float targetScrollOffset;
|
||||
/** Vertical scroll offset for tooltip content (pixels) */
|
||||
private float tooltipScrollOffsetF;
|
||||
|
||||
/** Maximum scroll offset = totalContentHeight - visibleContentHeight (pixels) */
|
||||
private int tooltipMaxScroll;
|
||||
/** Pinned token (set when user clicks a token to keep tooltip open) */
|
||||
private Token pinnedToken;
|
||||
private TokenHoverInfo pinnedHoverInfo;
|
||||
|
||||
/** Whether click-to-pin behaviour is enabled for this hover state. */
|
||||
private boolean clickToPinEnabled = true;
|
||||
|
||||
// ==================== UPDATE ====================
|
||||
|
||||
/**
|
||||
* Update the hover state based on current mouse position.
|
||||
* Should be called every frame from drawTextBox.
|
||||
*
|
||||
* @param mouseX Current mouse X position
|
||||
* @param mouseY Current mouse Y position
|
||||
* @param document The script document
|
||||
* @param viewportX X position of the text viewport (after gutter)
|
||||
* @param viewportY Y position of the text viewport
|
||||
* @param viewportWidth Width of the text viewport
|
||||
* @param viewportHeight Height of the text viewport
|
||||
* @param scrollOffset Current vertical scroll offset (in lines)
|
||||
* @param lineHeight Height of each line in pixels
|
||||
* @param gutterWidth Width of the line number gutter
|
||||
*/
|
||||
public void update(int mouseX, int mouseY, ScriptDocument document,
|
||||
int viewportX, int viewportY, int viewportWidth, int viewportHeight,
|
||||
float scrollOffset, int lineHeight, int gutterWidth) {
|
||||
|
||||
lastMouseX = mouseX;
|
||||
lastMouseY = mouseY;
|
||||
|
||||
// Check if mouse is within the text viewport
|
||||
if (mouseX < viewportX || mouseX > viewportX + viewportWidth ||
|
||||
mouseY < viewportY || mouseY > viewportY + viewportHeight) {
|
||||
clearHover();
|
||||
return;
|
||||
}
|
||||
|
||||
if (document == null) {
|
||||
clearHover();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate which line the mouse is over
|
||||
int relativeY = mouseY - viewportY;
|
||||
int lineIndex = (int) (scrollOffset + (relativeY / (float) lineHeight));
|
||||
|
||||
// Get the line
|
||||
ScriptLine line = null;
|
||||
for (ScriptLine l : document.getLines()) {
|
||||
if (l.getLineIndex() == lineIndex) {
|
||||
line = l;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (line == null) {
|
||||
clearHover();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate character position within the line
|
||||
int relativeX = mouseX - viewportX;
|
||||
int globalPos = line.getGlobalStart() + getCharacterIndexAtX(line, relativeX);
|
||||
|
||||
// Find the token at this position
|
||||
Token token = line.getTokenAt(globalPos);
|
||||
|
||||
if (token == null) {
|
||||
clearHover();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a new token
|
||||
if (token != hoveredToken) {
|
||||
// New token - reset timer
|
||||
hoveredToken = token;
|
||||
hoverStartTime = System.currentTimeMillis();
|
||||
tooltipVisible = false;
|
||||
hoverInfo = null;
|
||||
|
||||
resetTooltipScroll();
|
||||
// Calculate token screen position for tooltip
|
||||
calculateTokenPosition(line, token, viewportX, viewportY, scrollOffset, lineHeight);
|
||||
} else {
|
||||
// Same token - check if delay has elapsed
|
||||
long elapsed = System.currentTimeMillis() - hoverStartTime;
|
||||
if (elapsed >= HOVER_DELAY_MS && !tooltipVisible) {
|
||||
tooltipVisible = true;
|
||||
hoverInfo = TokenHoverInfo.fromToken(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the hover state with a specific token.
|
||||
* Simplified version for when the token has already been found.
|
||||
*
|
||||
* @param mouseX Current mouse X position
|
||||
* @param mouseY Current mouse Y position
|
||||
* @param token The token at the mouse position (or null if none)
|
||||
* @param tokenX Screen X position of the token
|
||||
* @param tokenY Screen Y position of the token
|
||||
* @param tokenW Width of the token in pixels
|
||||
*/
|
||||
public void update(int mouseX, int mouseY, Token token, int tokenX, int tokenY, int tokenW) {
|
||||
lastMouseX = mouseX;
|
||||
lastMouseY = mouseY;
|
||||
clickToPinEnabled=false;
|
||||
// If a token has been pinned by click, ignore mouse movement updates
|
||||
if (pinnedToken != null) {
|
||||
// keep pinned tooltip visible
|
||||
tooltipVisible = true;
|
||||
hoverInfo = pinnedHoverInfo;
|
||||
return;
|
||||
}
|
||||
|
||||
if (token == null) {
|
||||
clearHover();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a new token
|
||||
if (token != hoveredToken) {
|
||||
// New token - reset timer
|
||||
hoveredToken = token;
|
||||
hoverStartTime = System.currentTimeMillis();
|
||||
tooltipVisible = false;
|
||||
hoverInfo = null;
|
||||
|
||||
resetTooltipScroll();
|
||||
// Store token position
|
||||
tokenScreenX = tokenX;
|
||||
tokenScreenY = tokenY;
|
||||
tokenWidth = tokenW;
|
||||
} else {
|
||||
// Same token - check if delay has elapsed
|
||||
long elapsed = System.currentTimeMillis() - hoverStartTime;
|
||||
if (elapsed >= HOVER_DELAY_MS && !tooltipVisible) {
|
||||
tooltipVisible = true;
|
||||
hoverInfo = TokenHoverInfo.fromToken(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the current hover state.
|
||||
*/
|
||||
public void clearHover() {
|
||||
if (hoveredToken != null) {
|
||||
hoveredToken = null;
|
||||
hoverStartTime = 0;
|
||||
// Do not clear tooltipInfo here if pinned; if not pinned, hide tooltip.
|
||||
if (pinnedToken == null) {
|
||||
tooltipVisible = false;
|
||||
hoverInfo = null;
|
||||
tooltipBoxX = tooltipBoxY = tooltipBoxWidth = tooltipBoxHeight = 0;
|
||||
tooltipPanelX = tooltipPanelY = tooltipPanelW = tooltipPanelH = 0;
|
||||
isDraggingScrollbar = false;
|
||||
isDraggingTooltip = false;
|
||||
tooltipPositionOverridden = false;
|
||||
isResizingTooltip = false;
|
||||
tooltipSizeOverridden = false;
|
||||
tooltipScrollOffsetF = 0;
|
||||
targetScrollOffset = 0;
|
||||
tooltipMaxScroll = 0;
|
||||
isDraggingScrollbar = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force the tooltip to hide (e.g., when clicking).
|
||||
*/
|
||||
public void hideTooltip() {
|
||||
tooltipVisible = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable click-to-pin behaviour.
|
||||
*/
|
||||
public void setClickToPinEnabled(boolean enabled) {
|
||||
this.clickToPinEnabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isClickToPinEnabled() {
|
||||
return clickToPinEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a token so its tooltip stays visible until explicitly unpinned.
|
||||
*/
|
||||
public void pinToken(Token token, int tokenX, int tokenY, int tokenW) {
|
||||
if (token == null) return;
|
||||
this.pinnedToken = token;
|
||||
this.pinnedHoverInfo = TokenHoverInfo.fromToken(token);
|
||||
this.tooltipVisible = pinnedHoverInfo != null && pinnedHoverInfo.hasContent();
|
||||
this.tokenScreenX = tokenX;
|
||||
this.tokenScreenY = tokenY;
|
||||
this.tokenWidth = tokenW;
|
||||
// ensure hoveredToken reflects pinned token
|
||||
this.hoveredToken = token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin any pinned token and hide tooltip.
|
||||
*/
|
||||
public void unpin() {
|
||||
this.pinnedToken = null;
|
||||
this.pinnedHoverInfo = null;
|
||||
this.tooltipVisible = false;
|
||||
this.hoverInfo = null;
|
||||
}
|
||||
|
||||
public boolean isPinned() { return pinnedToken != null; }
|
||||
|
||||
// ==================== POSITION CALCULATION ====================
|
||||
|
||||
/**
|
||||
* Get the character index within a line at the given X pixel position.
|
||||
*/
|
||||
private int getCharacterIndexAtX(ScriptLine line, int x) {
|
||||
String text = line.getText();
|
||||
if (text == null || text.isEmpty()) return 0;
|
||||
|
||||
int accumWidth = 0;
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
int charWidth = noppes.npcs.client.ClientProxy.Font.width(String.valueOf(text.charAt(i)));
|
||||
if (accumWidth + charWidth / 2 > x) {
|
||||
return i;
|
||||
}
|
||||
accumWidth += charWidth;
|
||||
}
|
||||
return text.length();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the screen position of a token for tooltip positioning.
|
||||
*/
|
||||
private void calculateTokenPosition(ScriptLine line, Token token,
|
||||
int viewportX, int viewportY,
|
||||
float scrollOffset, int lineHeight) {
|
||||
// X position: calculate width of text before the token
|
||||
String lineText = line.getText();
|
||||
int tokenLocalStart = token.getGlobalStart() - line.getGlobalStart();
|
||||
tokenLocalStart = Math.max(0, Math.min(tokenLocalStart, lineText.length()));
|
||||
|
||||
String textBefore = lineText.substring(0, tokenLocalStart);
|
||||
tokenScreenX = viewportX + noppes.npcs.client.ClientProxy.Font.width(textBefore);
|
||||
|
||||
// Y position: line position minus scroll
|
||||
int lineY = line.getLineIndex();
|
||||
tokenScreenY = viewportY + (int) ((lineY - scrollOffset) * lineHeight);
|
||||
|
||||
// Token width
|
||||
tokenWidth = noppes.npcs.client.ClientProxy.Font.width(token.getText());
|
||||
}
|
||||
|
||||
public void setTooltipBounds(int x, int y, int width, int height) {
|
||||
tooltipBoxX = x;
|
||||
tooltipBoxY = y;
|
||||
tooltipBoxWidth = width;
|
||||
tooltipBoxHeight = height;
|
||||
}
|
||||
|
||||
public void setTooltipMaxScroll(int maxScroll) {
|
||||
this.tooltipMaxScroll = Math.max(0, maxScroll);
|
||||
}
|
||||
|
||||
public boolean isMouseOverTooltip(int mouseX, int mouseY) {
|
||||
if (!tooltipVisible || tooltipBoxWidth <= 0) return false;
|
||||
return mouseX >= tooltipBoxX && mouseX <= tooltipBoxX + tooltipBoxWidth
|
||||
&& mouseY >= tooltipBoxY && mouseY <= tooltipBoxY + tooltipBoxHeight;
|
||||
}
|
||||
|
||||
public void scrollTooltip(int wheelDelta) {
|
||||
// LWJGL wheelDelta: positive = scrolled up (content should move up = offset increases)
|
||||
// Divide by 2 for sensitivity; negate to match expected direction
|
||||
targetScrollOffset = Math.max(0, Math.min(tooltipMaxScroll, targetScrollOffset - wheelDelta / 5.0f));
|
||||
}
|
||||
|
||||
public int getTooltipScrollOffset() {
|
||||
return (int) tooltipScrollOffsetF;
|
||||
}
|
||||
|
||||
public void resetTooltipScroll() {
|
||||
tooltipScrollOffsetF = 0;
|
||||
targetScrollOffset = 0;
|
||||
}
|
||||
|
||||
/** Lerp current offset toward target. Call every frame while tooltip is visible. */
|
||||
public void updateSmoothScroll() {
|
||||
float diff = targetScrollOffset - tooltipScrollOffsetF;
|
||||
if (Math.abs(diff) < 0.5f) {
|
||||
tooltipScrollOffsetF = targetScrollOffset;
|
||||
} else {
|
||||
tooltipScrollOffsetF += diff * 0.04f;
|
||||
}
|
||||
}
|
||||
|
||||
/** Called by renderer each frame to store thumb bounds for drag/hover detection. */
|
||||
public void setScrollbarThumb(int barX, int thumbY, int thumbH, int trackTop, int trackHeight) {
|
||||
scrollbarX = barX;
|
||||
scrollbarThumbX = barX;
|
||||
scrollbarThumbY = thumbY;
|
||||
scrollbarThumbH = thumbH;
|
||||
scrollbarTrackTop = trackTop;
|
||||
scrollbarTrackHeight = trackHeight;
|
||||
}
|
||||
|
||||
public boolean isMouseOverScrollbarThumb(int mx, int my) {
|
||||
if (!tooltipVisible || scrollbarThumbH <= 0) return false;
|
||||
return mx >= scrollbarX && mx <= scrollbarX + 3
|
||||
&& my >= scrollbarThumbY && my <= scrollbarThumbY + scrollbarThumbH;
|
||||
}
|
||||
|
||||
public void startScrollbarDrag(int mouseY) {
|
||||
isDraggingScrollbar = true;
|
||||
dragStartMouseY = mouseY;
|
||||
dragStartScrollOffset = (int) tooltipScrollOffsetF;
|
||||
}
|
||||
|
||||
public void updateScrollbarDrag(int mouseY) {
|
||||
if (!isDraggingScrollbar || scrollbarTrackHeight <= 0 || tooltipMaxScroll <= 0) return;
|
||||
float scrollRatio = (float) (scrollbarTrackHeight) / Math.max(1, scrollbarTrackHeight + tooltipMaxScroll);
|
||||
int thumbH = Math.max(6, (int)(scrollbarTrackHeight * scrollRatio));
|
||||
int effectiveTrack = scrollbarTrackHeight - thumbH;
|
||||
if (effectiveTrack <= 0) return;
|
||||
int deltaY = mouseY - dragStartMouseY;
|
||||
float scrollDelta = (float) deltaY / effectiveTrack * tooltipMaxScroll;
|
||||
targetScrollOffset = Math.max(0, Math.min(tooltipMaxScroll, dragStartScrollOffset + scrollDelta));
|
||||
tooltipScrollOffsetF = targetScrollOffset; // snap during drag
|
||||
}
|
||||
|
||||
public void releaseScrollbarDrag() {
|
||||
isDraggingScrollbar = false;
|
||||
}
|
||||
|
||||
// ==================== TOOLTIP PANEL DRAG ====================
|
||||
|
||||
/** Record the actual rendered tooltip panel rect (used for drag hit-testing). */
|
||||
public void setTooltipPanel(int x, int y, int w, int h) {
|
||||
tooltipPanelX = x;
|
||||
tooltipPanelY = y;
|
||||
tooltipPanelW = w;
|
||||
tooltipPanelH = h;
|
||||
}
|
||||
|
||||
public boolean isMouseOverTooltipPanel(int mx, int my) {
|
||||
if (!tooltipVisible || tooltipPanelW <= 0) return false;
|
||||
return mx >= tooltipPanelX && mx <= tooltipPanelX + tooltipPanelW
|
||||
&& my >= tooltipPanelY && my <= tooltipPanelY + tooltipPanelH;
|
||||
}
|
||||
|
||||
public int getTooltipPanelX() { return tooltipPanelX; }
|
||||
public int getTooltipPanelY() { return tooltipPanelY; }
|
||||
|
||||
/** Begin dragging the tooltip; offsets are from mouse to tooltip top-left. */
|
||||
public void startTooltipDrag(int mouseX, int mouseY) {
|
||||
isDraggingTooltip = true;
|
||||
tooltipDragOffsetX = mouseX - tooltipPanelX;
|
||||
tooltipDragOffsetY = mouseY - tooltipPanelY;
|
||||
}
|
||||
|
||||
/** Called every frame while M1 is held to move the tooltip. */
|
||||
public void updateTooltipDrag(int mouseX, int mouseY) {
|
||||
if (!isDraggingTooltip) return;
|
||||
overriddenTooltipX = mouseX - tooltipDragOffsetX;
|
||||
overriddenTooltipY = mouseY - tooltipDragOffsetY;
|
||||
tooltipPositionOverridden = true;
|
||||
}
|
||||
|
||||
public void releaseTooltipDrag() {
|
||||
isDraggingTooltip = false;
|
||||
}
|
||||
|
||||
public boolean isDraggingTooltip() { return isDraggingTooltip; }
|
||||
public boolean hasOverriddenPosition() { return tooltipPositionOverridden; }
|
||||
public int getOverriddenTooltipX() { return overriddenTooltipX; }
|
||||
public int getOverriddenTooltipY() { return overriddenTooltipY; }
|
||||
|
||||
// ==================== TOOLTIP PANEL RESIZE ====================
|
||||
|
||||
/** Returns true when the mouse is over the resize handle (bottom-right corner). */
|
||||
public boolean isMouseOverResizeHandle(int mx, int my) {
|
||||
if (!tooltipVisible || tooltipPanelW <= 0) return false;
|
||||
int rx = tooltipPanelX + tooltipPanelW - 8;
|
||||
int ry = tooltipPanelY + tooltipPanelH - 8;
|
||||
return mx >= rx && mx <= tooltipPanelX + tooltipPanelW
|
||||
&& my >= ry && my <= tooltipPanelY + tooltipPanelH;
|
||||
}
|
||||
|
||||
/** Begin resizing; captures current panel dimensions as the baseline. */
|
||||
public void startTooltipResize(int mouseX, int mouseY) {
|
||||
isResizingTooltip = true;
|
||||
resizeInitMouseX = mouseX;
|
||||
resizeInitMouseY = mouseY;
|
||||
resizeInitW = tooltipSizeOverridden ? overriddenTooltipW : tooltipPanelW;
|
||||
resizeInitH = tooltipSizeOverridden ? overriddenTooltipH : tooltipPanelH;
|
||||
}
|
||||
|
||||
/** Called every frame while M1 is held to resize the tooltip panel. */
|
||||
public void updateTooltipResize(int mouseX, int mouseY) {
|
||||
if (!isResizingTooltip) return;
|
||||
overriddenTooltipW = Math.max(MIN_TOOLTIP_W, resizeInitW + (mouseX - resizeInitMouseX));
|
||||
overriddenTooltipH = Math.max(MIN_TOOLTIP_H, resizeInitH + (mouseY - resizeInitMouseY));
|
||||
tooltipSizeOverridden = true;
|
||||
}
|
||||
|
||||
public void releaseTooltipResize() {
|
||||
isResizingTooltip = false;
|
||||
}
|
||||
|
||||
public boolean isResizingTooltip() { return isResizingTooltip; }
|
||||
public boolean hasOverriddenSize() { return tooltipSizeOverridden; }
|
||||
public int getOverriddenTooltipW() { return overriddenTooltipW; }
|
||||
public int getOverriddenTooltipH() { return overriddenTooltipH; }
|
||||
|
||||
public boolean isDraggingScrollbar() {
|
||||
return isDraggingScrollbar;
|
||||
}
|
||||
|
||||
public int getScrollbarThumbY() { return scrollbarThumbY; }
|
||||
public int getScrollbarThumbH() { return scrollbarThumbH; }
|
||||
public int getScrollbarX() { return scrollbarX; }
|
||||
|
||||
// ==================== GETTERS ====================
|
||||
|
||||
public boolean isTooltipVisible() {
|
||||
return tooltipVisible && hoverInfo != null && hoverInfo.hasContent();
|
||||
}
|
||||
|
||||
public TokenHoverInfo getHoverInfo() {
|
||||
return hoverInfo;
|
||||
}
|
||||
|
||||
public Token getHoveredToken() {
|
||||
return hoveredToken;
|
||||
}
|
||||
|
||||
public int getTokenScreenX() {
|
||||
return tokenScreenX;
|
||||
}
|
||||
|
||||
public int getTokenScreenY() {
|
||||
return tokenScreenY;
|
||||
}
|
||||
|
||||
public int getTokenWidth() {
|
||||
return tokenWidth;
|
||||
}
|
||||
|
||||
public int getLastMouseX() {
|
||||
return lastMouseX;
|
||||
}
|
||||
|
||||
public int getLastMouseY() {
|
||||
return lastMouseY;
|
||||
}
|
||||
public void setLastMousePosition(int x, int y) {
|
||||
lastMouseX = x;
|
||||
lastMouseY = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the progress (0.0 to 1.0) of the hover delay.
|
||||
* Can be used for fade-in animation.
|
||||
*/
|
||||
public float getHoverProgress() {
|
||||
if (hoveredToken == null) return 0f;
|
||||
long elapsed = System.currentTimeMillis() - hoverStartTime;
|
||||
return Math.min(1.0f, elapsed / (float) HOVER_DELAY_MS);
|
||||
}
|
||||
}
|
||||
+1787
File diff suppressed because it is too large.
Load diff
+744
@@ -0,0 +1,744 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.hover;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Gui;
|
||||
import net.minecraft.client.gui.ScaledResolution;
|
||||
import noppes.npcs.client.ClientProxy;
|
||||
import noppes.npcs.client.gui.util.GuiNPCInterface;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Renders hover tooltips for tokens in the script editor.
|
||||
*
|
||||
* Renders IntelliJ-style tooltips with:
|
||||
* - Package name in gray
|
||||
* - Icon indicator (C/I/E/m/f/v/p)
|
||||
* - Colored declaration line
|
||||
* - Documentation/Javadoc
|
||||
* - Error messages in red
|
||||
*
|
||||
* Uses a unified approach for width/height calculation based on max available width.
|
||||
*/
|
||||
public class TokenHoverRenderer {
|
||||
|
||||
// ==================== CONSTANTS ====================
|
||||
|
||||
/** Padding inside the tooltip box */
|
||||
private static final int PADDING = 6;
|
||||
|
||||
/** Line spacing between rows */
|
||||
private static final int LINE_SPACING = 2;
|
||||
|
||||
/** Separator line height */
|
||||
private static final int SEPARATOR_HEIGHT = 1;
|
||||
|
||||
/** Spacing above and below separator */
|
||||
private static final int SEPARATOR_SPACING = 5;
|
||||
|
||||
/** Vertical offset from token */
|
||||
private static final int VERTICAL_OFFSET = 4;
|
||||
|
||||
/** Maximum tooltip width as percentage of viewport */
|
||||
private static final float MAX_WIDTH_RATIO = 0.9f;
|
||||
|
||||
/** Minimum tooltip width */
|
||||
private static final int MIN_WIDTH = 50;
|
||||
|
||||
// ==================== COLORS ====================
|
||||
|
||||
/** Background color (dark gray like IntelliJ) */
|
||||
private static final int BG_COLOR = 0xF0313335;
|
||||
|
||||
/** Border color */
|
||||
private static final int BORDER_COLOR = 0xFF3C3F41;
|
||||
|
||||
/** Package text color */
|
||||
private static int PACKAGE_COLOR = 0xFF6490e2;
|
||||
|
||||
/** Error text color */
|
||||
private static final int ERROR_COLOR = 0xFFFF6B68;
|
||||
|
||||
/** Info text color */
|
||||
private static final int INFO_COLOR = 0xFF808080;
|
||||
|
||||
/** Documentation text color */
|
||||
private static final int DOC_COLOR = 0xFFA9B7C6;
|
||||
|
||||
/** Background fill for ``` code blocks */
|
||||
private static final int CODE_BG_COLOR = 0xFF1E1F22;
|
||||
/** Left accent bar color for code blocks */
|
||||
private static final int CODE_ACCENT_COLOR = 0xFF4FC1FF;
|
||||
/** Horizontal padding inside code block */
|
||||
private static final int CODE_INDENT = 10;
|
||||
/** Vertical padding above/below code block content */
|
||||
private static final int CODE_VPAD = 6;
|
||||
/** Fixed visual gap between last content line and the panel bottom (always visible, regardless of scroll) */
|
||||
private static final int BOTTOM_GAP = 4;
|
||||
|
||||
// ==================== RENDERING ====================
|
||||
|
||||
/**
|
||||
* Render the hover tooltip.
|
||||
*/
|
||||
public static void render(HoverState hoverState, int viewportX, int viewportWidth, int viewportY, int viewportHeight, GuiNPCInterface gui) {
|
||||
if (!hoverState.isTooltipVisible()) return;
|
||||
hoverState.updateSmoothScroll();
|
||||
|
||||
TokenHoverInfo info = hoverState.getHoverInfo();
|
||||
if (info == null || !info.hasContent()) return;
|
||||
|
||||
int lineHeight = ClientProxy.Font.height();
|
||||
int tokenX = hoverState.getTokenScreenX();
|
||||
int tokenY = hoverState.getTokenScreenY();
|
||||
|
||||
// Screen dimensions for clamping when the panel has been dragged outside the editor viewport
|
||||
Minecraft mc2 = Minecraft.getMinecraft();
|
||||
ScaledResolution sr2 = new ScaledResolution(mc2, mc2.displayWidth, mc2.displayHeight);
|
||||
int screenW = sr2.getScaledWidth();
|
||||
int screenH = sr2.getScaledHeight();
|
||||
boolean positionOverridden = hoverState.hasOverriddenPosition();
|
||||
// Use screen bounds whenever the panel has been dragged OR resized — both escape the editor viewport
|
||||
boolean useScreenBounds = positionOverridden || hoverState.hasOverriddenSize();
|
||||
|
||||
// Calculate max available width for content
|
||||
int maxContentWidth = getMaxContentWidth(viewportX, viewportWidth, tokenX);
|
||||
|
||||
// Calculate actual content width needed (clamped to max)
|
||||
int contentWidth = calculateContentWidth(info, maxContentWidth);
|
||||
|
||||
// Cap visible height at 60% of viewport
|
||||
int totalContentHeight = calculateContentHeight(info, contentWidth);
|
||||
int maxVisibleContentHeight = (int)((useScreenBounds ? screenH : viewportHeight) * 0.60f);
|
||||
if (totalContentHeight > maxVisibleContentHeight) {
|
||||
// Scrollbar will appear, reducing effective wrap width by 6px — re-measure for accurate height
|
||||
totalContentHeight = calculateContentHeight(info, contentWidth - 6);
|
||||
}
|
||||
int visibleContentHeight = Math.min(totalContentHeight, maxVisibleContentHeight);
|
||||
|
||||
// Box dimensions — use visibleContentHeight so positioning matches rendering
|
||||
int boxWidth = contentWidth + PADDING * 2;
|
||||
int boxHeight = visibleContentHeight + PADDING * 2;
|
||||
|
||||
// Apply user size override when the panel has been resized
|
||||
if (hoverState.hasOverriddenSize()) {
|
||||
int overW = Math.max(MIN_WIDTH + PADDING * 2, hoverState.getOverriddenTooltipW());
|
||||
int overH = Math.max(lineHeight + PADDING * 2, hoverState.getOverriddenTooltipH());
|
||||
boxWidth = overW;
|
||||
visibleContentHeight = overH - PADDING * 2;
|
||||
boxHeight = overH;
|
||||
// Re-measure with new wrap width so scrollbar detection is accurate
|
||||
int newContentW = boxWidth - PADDING * 2;
|
||||
totalContentHeight = calculateContentHeight(info, newContentW);
|
||||
if (totalContentHeight > visibleContentHeight) {
|
||||
totalContentHeight = calculateContentHeight(info, newContentW - 6);
|
||||
}
|
||||
}
|
||||
|
||||
// Position the tooltip — use dragged position if user has panned the panel
|
||||
int tooltipX = positionOverridden ? hoverState.getOverriddenTooltipX() : tokenX;
|
||||
int tooltipY = positionOverridden ? hoverState.getOverriddenTooltipY() : tokenY + lineHeight + VERTICAL_OFFSET;
|
||||
|
||||
// Clamp position — use screen bounds when user has dragged the panel outside the editor viewport
|
||||
int leftBound = useScreenBounds ? 0 : viewportX;
|
||||
int rightBound = useScreenBounds ? screenW : viewportX + viewportWidth;
|
||||
int topBound = useScreenBounds ? 0 : viewportY;
|
||||
int bottomBound = useScreenBounds ? screenH : viewportY + viewportHeight;
|
||||
|
||||
if (tooltipX + boxWidth > rightBound) {
|
||||
tooltipX = rightBound - boxWidth;
|
||||
}
|
||||
if (tooltipX < leftBound) {
|
||||
tooltipX = leftBound;
|
||||
}
|
||||
|
||||
// If box still doesn't fit horizontally, shrink it
|
||||
int availableWidth = rightBound - tooltipX;
|
||||
if (boxWidth > availableWidth) {
|
||||
boxWidth = availableWidth;
|
||||
}
|
||||
|
||||
if (tooltipY + boxHeight > bottomBound) {
|
||||
if (useScreenBounds) {
|
||||
tooltipY = bottomBound - boxHeight;
|
||||
} else {
|
||||
tooltipY = tokenY - boxHeight - VERTICAL_OFFSET; // try above token
|
||||
}
|
||||
}
|
||||
if (tooltipY < topBound) {
|
||||
tooltipY = topBound;
|
||||
}
|
||||
|
||||
// Record actual panel rect for drag hit-testing
|
||||
hoverState.setTooltipPanel(tooltipX, tooltipY, boxWidth, boxHeight);
|
||||
|
||||
// Mouse-over detection bounds:
|
||||
// When dragged, use panel rect only — no gap extension (token is unrelated to panel position)
|
||||
// When default, union the token bottom and the tooltip rect so both directions are covered
|
||||
if (hoverState.hasOverriddenPosition()) {
|
||||
hoverState.setTooltipBounds(tooltipX, tooltipY, boxWidth, boxHeight);
|
||||
} else {
|
||||
int boundsTopY = Math.min(tokenY + lineHeight, tooltipY);
|
||||
int boundsBottomY = Math.max(tooltipY + boxHeight, tokenY + lineHeight);
|
||||
hoverState.setTooltipBounds(tooltipX, boundsTopY, boxWidth, boundsBottomY - boundsTopY);
|
||||
}
|
||||
|
||||
// Render the tooltip
|
||||
renderTooltipBox(tooltipX, tooltipY, boxWidth, maxContentWidth, info, hoverState,
|
||||
totalContentHeight, visibleContentHeight, gui);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the maximum content width based on viewport and token position.
|
||||
*/
|
||||
private static int getMaxContentWidth(int viewportX, int viewportWidth, int tokenX) {
|
||||
// Max width is based on viewport size (80% by default)
|
||||
// This scales naturally with viewport size - no artificial hard cap
|
||||
int maxWidth = (int)(viewportWidth * MAX_WIDTH_RATIO);
|
||||
|
||||
// The tooltip can expand across the entire viewport width
|
||||
// (positioning logic will shift it left if needed)
|
||||
return Math.max(MIN_WIDTH, maxWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the tooltip box with all content.
|
||||
*/
|
||||
private static void renderTooltipBox(int x, int y, int boxWidth, int wrapWidth, TokenHoverInfo info,
|
||||
HoverState hoverState, int totalContentHeight, int visibleContentHeight, GuiNPCInterface gui) {
|
||||
|
||||
boolean hasScrollbar = totalContentHeight > visibleContentHeight;
|
||||
// Wrap at the actual box content area width, not the max allowed width.
|
||||
// Using maxContentWidth here caused lines to never wrap at wide viewports (fullscreen).
|
||||
int contentWrapWidth = boxWidth - PADDING * 2;
|
||||
int effectiveWrapWidth = hasScrollbar ? contentWrapWidth - 6 : contentWrapWidth;
|
||||
|
||||
int boxHeight = visibleContentHeight + PADDING * 2;
|
||||
|
||||
// Store scroll metadata into HoverState
|
||||
hoverState.setTooltipMaxScroll(Math.max(0, totalContentHeight - visibleContentHeight + BOTTOM_GAP));
|
||||
|
||||
// Draw background
|
||||
Gui.drawRect(x, y, x + boxWidth, y + boxHeight, BG_COLOR);
|
||||
|
||||
// Draw border
|
||||
Gui.drawRect(x, y, x + boxWidth, y + 1, BORDER_COLOR);
|
||||
Gui.drawRect(x, y + boxHeight - 1, x + boxWidth, y + boxHeight, BORDER_COLOR);
|
||||
Gui.drawRect(x, y, x + 1, y + boxHeight, BORDER_COLOR);
|
||||
Gui.drawRect(x + boxWidth - 1, y, x + boxWidth, y + boxHeight, BORDER_COLOR);
|
||||
|
||||
Minecraft mc = Minecraft.getMinecraft();
|
||||
ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
||||
int sf = sr.getScaleFactor();
|
||||
double panX = gui != null ? gui.getPanX() : 0;
|
||||
double panY = gui != null ? gui.getPanY() : 0;
|
||||
int clipX = (int)((x + 1 - panX) * sf);
|
||||
int clipY = (int)((sr.getScaledHeight() - (y - panY + boxHeight - 1 - BOTTOM_GAP)) * sf);
|
||||
int clipW = (hasScrollbar ? boxWidth - PADDING - 1 : boxWidth - 2) * sf;
|
||||
int clipH = (boxHeight - 2 - BOTTOM_GAP) * sf;
|
||||
GL11.glEnable(GL11.GL_SCISSOR_TEST);
|
||||
GL11.glScissor(clipX, clipY, clipW, clipH);
|
||||
|
||||
int textX = x + PADDING;
|
||||
int currentY = y + PADDING - hoverState.getTooltipScrollOffset();
|
||||
int lineHeight = ClientProxy.Font.height();
|
||||
|
||||
String packageName = info.getPackageName();
|
||||
List<TokenHoverInfo.TextSegment> declaration = info.getDeclaration();
|
||||
List<String> docs = info.getDocumentation();
|
||||
List<String> additionalInfo = info.getAdditionalInfo();
|
||||
|
||||
// Draw errors first
|
||||
List<String> errors = info.getErrors();
|
||||
if (!errors.isEmpty()) {
|
||||
for (String error : errors) {
|
||||
List<String> wrappedLines = wrapText(error, effectiveWrapWidth);
|
||||
for (String line : wrappedLines) {
|
||||
drawText(textX, currentY, line, ERROR_COLOR);
|
||||
currentY += lineHeight + LINE_SPACING;
|
||||
}
|
||||
}
|
||||
boolean onlyErrors = errors != null && !errors.isEmpty() && (packageName == null || packageName.isEmpty()) && (declaration == null ||
|
||||
declaration.isEmpty()) && (docs == null || docs.isEmpty()) && (additionalInfo == null || additionalInfo.isEmpty());
|
||||
|
||||
if (!onlyErrors) { //Add error separator line
|
||||
currentY += SEPARATOR_HEIGHT + SEPARATOR_SPACING - 5;
|
||||
Gui.drawRect(textX, currentY, x + boxWidth - PADDING, currentY + SEPARATOR_HEIGHT, BORDER_COLOR);
|
||||
currentY += SEPARATOR_HEIGHT + SEPARATOR_SPACING;
|
||||
}
|
||||
currentY += LINE_SPACING;
|
||||
}
|
||||
|
||||
// Draw package name
|
||||
if (packageName != null && !packageName.isEmpty()) {
|
||||
String packageText = "\u25CB " + packageName;
|
||||
List<String> wrappedLines = wrapText(packageText, effectiveWrapWidth);
|
||||
for (String line : wrappedLines) {
|
||||
drawText(textX, currentY, line, PACKAGE_COLOR);
|
||||
currentY += lineHeight + LINE_SPACING;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw declaration (colored segments with wrapping)
|
||||
if (!declaration.isEmpty()) {
|
||||
currentY = drawWrappedSegments(textX, currentY, effectiveWrapWidth, declaration);
|
||||
currentY += LINE_SPACING;
|
||||
}
|
||||
|
||||
// Draw documentation (plain text)
|
||||
if (!docs.isEmpty()) {
|
||||
// Draw separator line before documentation
|
||||
Gui.drawRect(textX, currentY, x + boxWidth - PADDING, currentY + SEPARATOR_HEIGHT, BORDER_COLOR);
|
||||
currentY += SEPARATOR_HEIGHT + SEPARATOR_SPACING;
|
||||
for (String doc : docs) {
|
||||
List<String> wrappedLines = wrapText(doc, effectiveWrapWidth);
|
||||
for (String line : wrappedLines) {
|
||||
drawText(textX, currentY, line, DOC_COLOR);
|
||||
currentY += lineHeight + LINE_SPACING;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw JSDoc-formatted documentation with colored segments
|
||||
List<TokenHoverInfo.DocumentationLine> jsDocLines = info.getJSDocLines();
|
||||
if (!jsDocLines.isEmpty()) {
|
||||
// Draw separator line if there was plain documentation or declaration
|
||||
if (docs.isEmpty() && !declaration.isEmpty()) {
|
||||
Gui.drawRect(textX, currentY, x + boxWidth - PADDING, currentY + SEPARATOR_HEIGHT, BORDER_COLOR);
|
||||
currentY += SEPARATOR_HEIGHT + SEPARATOR_SPACING;
|
||||
}
|
||||
|
||||
boolean previousVisibleJSDocWasCode = false;
|
||||
for (int di = 0; di < jsDocLines.size(); di++) {
|
||||
TokenHoverInfo.DocumentationLine docLine = jsDocLines.get(di);
|
||||
|
||||
if (docLine.isCodeBlockFirst) {
|
||||
// Lookahead: measure full block height
|
||||
int blockH = CODE_VPAD;
|
||||
for (int k = di; k < jsDocLines.size() && jsDocLines.get(k).isCodeLine; k++) {
|
||||
TokenHoverInfo.DocumentationLine cl = jsDocLines.get(k);
|
||||
if (!cl.isEmpty()) {
|
||||
int clIndent = cl.codeLeadingSpaces * ClientProxy.Font.width(" ");
|
||||
int clWrapW = Math.max(10, effectiveWrapWidth - CODE_INDENT - clIndent);
|
||||
blockH += calculateSegmentsHeight(clWrapW, cl.segments) + LINE_SPACING;
|
||||
} else {
|
||||
blockH += lineHeight / 2;
|
||||
}
|
||||
}
|
||||
blockH -= LINE_SPACING * 2; // subtract trailing LINE_SPACING (present inside bg) + one more to visually balance top/bottom
|
||||
blockH += CODE_VPAD;
|
||||
// Draw background
|
||||
Gui.drawRect(textX , currentY - CODE_VPAD,
|
||||
textX + effectiveWrapWidth + 4, currentY - CODE_VPAD + blockH,
|
||||
CODE_BG_COLOR);
|
||||
// Left accent bar
|
||||
Gui.drawRect(textX, currentY - CODE_VPAD,
|
||||
textX+2, currentY - CODE_VPAD + blockH,
|
||||
CODE_ACCENT_COLOR);
|
||||
}
|
||||
|
||||
if (docLine.isCodeLine) {
|
||||
if (!docLine.isEmpty()) {
|
||||
int explicitIndent = docLine.codeLeadingSpaces * ClientProxy.Font.width(" ");
|
||||
int codeX = textX + CODE_INDENT + explicitIndent;
|
||||
int codeWrapW = Math.max(10, effectiveWrapWidth - CODE_INDENT - explicitIndent);
|
||||
currentY = drawWrappedSegments(codeX, currentY, codeWrapW, docLine.segments);
|
||||
currentY += LINE_SPACING;
|
||||
previousVisibleJSDocWasCode = true;
|
||||
} else {
|
||||
currentY += lineHeight / 2;
|
||||
}
|
||||
} else if (!docLine.isEmpty()) {
|
||||
if (previousVisibleJSDocWasCode) {
|
||||
currentY += LINE_SPACING;
|
||||
}
|
||||
currentY = drawWrappedSegments(textX, currentY, effectiveWrapWidth, docLine.segments);
|
||||
currentY += LINE_SPACING;
|
||||
previousVisibleJSDocWasCode = false;
|
||||
} else {
|
||||
currentY += (di > 0 && jsDocLines.get(di - 1).isCodeLine) ? LINE_SPACING : lineHeight / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw additional info
|
||||
if (!additionalInfo.isEmpty()) {
|
||||
currentY += LINE_SPACING;
|
||||
for (String infoLine : additionalInfo) {
|
||||
List<String> wrappedLines = wrapText(infoLine, effectiveWrapWidth);
|
||||
for (String line : wrappedLines) {
|
||||
drawText(textX, currentY, line, INFO_COLOR);
|
||||
currentY += lineHeight + LINE_SPACING;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore scissor state before drawing scrollbar (scrollbar must not be clipped)
|
||||
GL11.glDisable(GL11.GL_SCISSOR_TEST);
|
||||
|
||||
// Draw scrollbar outside scissor rect so it is never clipped
|
||||
if (hasScrollbar) {
|
||||
int scrollbarX = x + boxWidth - PADDING;
|
||||
int scrollbarTrackTop = y + PADDING;
|
||||
int scrollbarTrackHeight = visibleContentHeight - 4; // leave room for resize handle
|
||||
// Track
|
||||
Gui.drawRect(scrollbarX, scrollbarTrackTop, scrollbarX + 3,
|
||||
scrollbarTrackTop + scrollbarTrackHeight, 0x40888888);
|
||||
// Thumb
|
||||
float scrollRatio = (float) visibleContentHeight / (totalContentHeight + BOTTOM_GAP);
|
||||
int thumbHeight = Math.max(6, (int)(scrollbarTrackHeight * scrollRatio));
|
||||
int effectiveMaxScroll = totalContentHeight - visibleContentHeight + BOTTOM_GAP;
|
||||
// Clamp scrollProgress to [0,1] — guards against stale offset after resize shrinks content
|
||||
float scrollProgress = effectiveMaxScroll > 0
|
||||
? Math.min(1f, (float) hoverState.getTooltipScrollOffset() / effectiveMaxScroll) : 0f;
|
||||
int thumbY = scrollbarTrackTop + (int)((scrollbarTrackHeight - thumbHeight) * scrollProgress);
|
||||
// Clamp thumb within track bounds defensively
|
||||
thumbY = Math.max(scrollbarTrackTop, Math.min(scrollbarTrackTop + scrollbarTrackHeight - thumbHeight, thumbY));
|
||||
// Store thumb bounds in HoverState for drag/hover detection
|
||||
hoverState.setScrollbarThumb(scrollbarX, thumbY, thumbHeight, scrollbarTrackTop, scrollbarTrackHeight);
|
||||
// Highlight thumb when hovered or dragging (darker = active, like AutocompleteMenu)
|
||||
int mouseX = hoverState.getLastMouseX();
|
||||
int mouseY = hoverState.getLastMouseY();
|
||||
boolean thumbActive = hoverState.isDraggingScrollbar()
|
||||
|| hoverState.isMouseOverScrollbarThumb(mouseX, mouseY);
|
||||
int thumbColor = thumbActive ? 0xFFCCCCCC : 0xFF808080;
|
||||
Gui.drawRect(scrollbarX, thumbY, scrollbarX + 3, thumbY + thumbHeight, thumbColor);
|
||||
}
|
||||
|
||||
// Draw resize handle at bottom-right corner (outside scissor, same as scrollbar)
|
||||
{
|
||||
int mouseX = hoverState.getLastMouseX();
|
||||
int mouseY = hoverState.getLastMouseY();
|
||||
boolean resizeActive = hoverState.isResizingTooltip()
|
||||
|| hoverState.isMouseOverResizeHandle(mouseX, mouseY);
|
||||
int dotColor = resizeActive ? 0xFFCCCCCC : 0x80888888;
|
||||
int rhX = x + boxWidth - 2;
|
||||
int rhY = y + boxHeight - 2;
|
||||
// Three-dot diagonal pattern (◢)
|
||||
Gui.drawRect(rhX - 1, rhY - 1, rhX, rhY, dotColor);
|
||||
Gui.drawRect(rhX - 3, rhY - 1, rhX - 2, rhY, dotColor);
|
||||
Gui.drawRect(rhX - 1, rhY - 3, rhX, rhY - 2, dotColor);
|
||||
Gui.drawRect(rhX - 5, rhY - 1, rhX - 4, rhY, dotColor);
|
||||
Gui.drawRect(rhX - 3, rhY - 3, rhX - 2, rhY - 2, dotColor);
|
||||
Gui.drawRect(rhX - 1, rhY - 5, rhX, rhY - 4, dotColor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw colored text segments with word wrapping.
|
||||
* Returns the Y position after drawing.
|
||||
*/
|
||||
private static int drawWrappedSegments(int startX, int startY, int maxWidth, List<TokenHoverInfo.TextSegment> segments) {
|
||||
int lineHeight = ClientProxy.Font.height();
|
||||
int currentX = startX;
|
||||
int currentY = startY;
|
||||
|
||||
for (TokenHoverInfo.TextSegment segment : segments) {
|
||||
String text = segment.text;
|
||||
int color = 0xFF000000 | segment.color;
|
||||
|
||||
// Split segment into words for wrapping
|
||||
String[] words = text.split("(?<=\\s)|(?=\\s)"); // Keep whitespace as separate tokens
|
||||
|
||||
for (String word : words) {
|
||||
int wordWidth = ClientProxy.Font.width(word);
|
||||
|
||||
// Check if word fits on current line
|
||||
if (currentX + wordWidth > startX + maxWidth && currentX > startX) {
|
||||
// Wrap to next line
|
||||
currentY += lineHeight + LINE_SPACING;
|
||||
currentX = startX;
|
||||
|
||||
// Skip leading whitespace on new line
|
||||
if (word.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
drawText(currentX, currentY, word, color);
|
||||
currentX += wordWidth;
|
||||
}
|
||||
}
|
||||
|
||||
return currentY + lineHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap text to fit within maxWidth.
|
||||
* Returns list of lines.
|
||||
*/
|
||||
private static List<String> wrapText(String text, int maxWidth) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
|
||||
if (text == null || text.isEmpty()) {
|
||||
return lines;
|
||||
}
|
||||
|
||||
// First split by explicit newlines
|
||||
String[] paragraphs = text.split("\n", -1); // -1 to preserve trailing empty lines
|
||||
|
||||
for (String paragraph : paragraphs) {
|
||||
if (paragraph.isEmpty()) {
|
||||
lines.add("");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Then apply word wrapping to each paragraph
|
||||
String[] words = paragraph.split(" ");
|
||||
StringBuilder currentLine = new StringBuilder();
|
||||
|
||||
for (String word : words) {
|
||||
String testLine = currentLine.length() == 0 ? word : currentLine + " " + word;
|
||||
int testWidth = ClientProxy.Font.width(testLine);
|
||||
|
||||
if (testWidth > maxWidth && currentLine.length() > 0) {
|
||||
lines.add(currentLine.toString());
|
||||
currentLine = new StringBuilder(word);
|
||||
} else {
|
||||
if (currentLine.length() > 0) {
|
||||
currentLine.append(" ");
|
||||
}
|
||||
currentLine.append(word);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLine.length() > 0) {
|
||||
lines.add(currentLine.toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.isEmpty()) {
|
||||
lines.add(text);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw text using the font renderer.
|
||||
*/
|
||||
private static void drawText(int x, int y, String text, int color) {
|
||||
ClientProxy.Font.drawString(text, x, y, color);
|
||||
}
|
||||
|
||||
// ==================== DIMENSION CALCULATION ====================
|
||||
|
||||
/**
|
||||
* Calculate the width needed for the content, clamped to maxWidth.
|
||||
* Returns the width of the longest wrapped line.
|
||||
*/
|
||||
private static int calculateContentWidth(TokenHoverInfo info, int maxWidth) {
|
||||
int longestLineWidth = 0;
|
||||
|
||||
// Package name
|
||||
String packageName = info.getPackageName();
|
||||
if (packageName != null && !packageName.isEmpty()) {
|
||||
String packageText = "\u25CB " + packageName;
|
||||
List<String> wrappedLines = wrapText(packageText, maxWidth);
|
||||
for (String line : wrappedLines) {
|
||||
longestLineWidth = Math.max(longestLineWidth, ClientProxy.Font.width(line));
|
||||
}
|
||||
}
|
||||
|
||||
// Declaration - wrap segments and find longest line
|
||||
if (!info.getDeclaration().isEmpty()) {
|
||||
int declarationLongestLine = calculateSegmentsLongestLine(maxWidth, info.getDeclaration());
|
||||
longestLineWidth = Math.max(longestLineWidth, declarationLongestLine);
|
||||
}
|
||||
|
||||
// Errors
|
||||
for (String error : info.getErrors()) {
|
||||
List<String> wrappedLines = wrapText(error, maxWidth);
|
||||
for (String line : wrappedLines) {
|
||||
longestLineWidth = Math.max(longestLineWidth, ClientProxy.Font.width(line));
|
||||
}
|
||||
}
|
||||
|
||||
// Documentation
|
||||
for (String doc : info.getDocumentation()) {
|
||||
List<String> wrappedLines = wrapText(doc, maxWidth);
|
||||
for (String line : wrappedLines) {
|
||||
longestLineWidth = Math.max(longestLineWidth, ClientProxy.Font.width(line));
|
||||
}
|
||||
}
|
||||
|
||||
// JSDoc-formatted documentation lines
|
||||
List<TokenHoverInfo.DocumentationLine> jsDocLines = info.getJSDocLines();
|
||||
if (jsDocLines != null) {
|
||||
for (TokenHoverInfo.DocumentationLine docLine : jsDocLines) {
|
||||
if (!docLine.isEmpty()) {
|
||||
int explicitIndent = docLine.isCodeLine
|
||||
? CODE_INDENT + docLine.codeLeadingSpaces * ClientProxy.Font.width(" ")
|
||||
: 0;
|
||||
int lineLongest = calculateSegmentsLongestLine(maxWidth - explicitIndent, docLine.segments) + explicitIndent;
|
||||
longestLineWidth = Math.max(longestLineWidth, lineLongest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Additional info
|
||||
for (String line : info.getAdditionalInfo()) {
|
||||
List<String> wrappedLines = wrapText(line, maxWidth);
|
||||
for (String wrappedLine : wrappedLines) {
|
||||
longestLineWidth = Math.max(longestLineWidth, ClientProxy.Font.width(wrappedLine));
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure minimum width
|
||||
return Math.max(50, longestLineWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the height needed for the content given the available width.
|
||||
* Uses the same wrapping logic as rendering.
|
||||
*/
|
||||
private static int calculateContentHeight(TokenHoverInfo info, int contentWidth) {
|
||||
int lineHeight = ClientProxy.Font.height();
|
||||
int totalHeight = 0;
|
||||
|
||||
String packageName = info.getPackageName();
|
||||
List<TokenHoverInfo.TextSegment> declaration = info.getDeclaration();
|
||||
List<String> docs = info.getDocumentation();
|
||||
List<TokenHoverInfo.DocumentationLine> jsDocLines = info.getJSDocLines();
|
||||
List<String> additionalInfo = info.getAdditionalInfo();
|
||||
|
||||
// Errors
|
||||
List<String> errors = info.getErrors();
|
||||
if (!errors.isEmpty()) {
|
||||
for (String error : errors) {
|
||||
List<String> wrappedLines = wrapText(error, contentWidth);
|
||||
totalHeight += wrappedLines.size() * (lineHeight + LINE_SPACING);
|
||||
}
|
||||
|
||||
boolean onlyErrors = errors != null && !errors.isEmpty() && (packageName == null || packageName.isEmpty()) && (declaration == null ||
|
||||
declaration.isEmpty()) && (docs == null || docs.isEmpty()) && (jsDocLines == null || jsDocLines.isEmpty()) && (additionalInfo == null || additionalInfo.isEmpty());
|
||||
|
||||
if (!onlyErrors) //Add error separator height
|
||||
totalHeight += (SEPARATOR_SPACING + SEPARATOR_HEIGHT) * 2 - 5;
|
||||
|
||||
totalHeight += LINE_SPACING; // Extra space after errors
|
||||
}
|
||||
|
||||
// Package name
|
||||
if (packageName != null && !packageName.isEmpty()) {
|
||||
String packageText = "\u25CB " + packageName;
|
||||
List<String> wrappedLines = wrapText(packageText, contentWidth);
|
||||
totalHeight += wrappedLines.size() * (lineHeight + LINE_SPACING);
|
||||
}
|
||||
|
||||
// Declaration (colored segments with wrapping)
|
||||
if (!declaration.isEmpty()) {
|
||||
totalHeight += calculateSegmentsHeight(contentWidth, declaration);
|
||||
totalHeight += LINE_SPACING;
|
||||
}
|
||||
|
||||
// Documentation (plain text)
|
||||
if (!docs.isEmpty()) {
|
||||
// Add space for separator line and spacing
|
||||
totalHeight += SEPARATOR_SPACING + SEPARATOR_HEIGHT;
|
||||
for (String doc : docs) {
|
||||
List<String> wrappedLines = wrapText(doc, contentWidth);
|
||||
totalHeight += wrappedLines.size() * (lineHeight + LINE_SPACING);
|
||||
}
|
||||
}
|
||||
|
||||
// JSDoc-formatted documentation lines
|
||||
if (jsDocLines != null && !jsDocLines.isEmpty()) {
|
||||
if (docs.isEmpty() && !declaration.isEmpty()) {
|
||||
totalHeight += SEPARATOR_HEIGHT + SEPARATOR_SPACING;
|
||||
}
|
||||
|
||||
boolean previousVisibleJSDocWasCode = false;
|
||||
for (int i = 0; i < jsDocLines.size(); i++) {
|
||||
TokenHoverInfo.DocumentationLine docLine = jsDocLines.get(i);
|
||||
if (!docLine.isEmpty()) {
|
||||
int explicitIndent = docLine.isCodeLine
|
||||
? docLine.codeLeadingSpaces * ClientProxy.Font.width(" ")
|
||||
: 0;
|
||||
int wrapW = Math.max(10, contentWidth - (docLine.isCodeLine ? CODE_INDENT : 0) - explicitIndent);
|
||||
if (!docLine.isCodeLine && previousVisibleJSDocWasCode) {
|
||||
totalHeight += LINE_SPACING;
|
||||
}
|
||||
totalHeight += calculateSegmentsHeight(wrapW, docLine.segments);
|
||||
totalHeight += LINE_SPACING;
|
||||
previousVisibleJSDocWasCode = docLine.isCodeLine;
|
||||
} else {
|
||||
totalHeight += (i > 0 && jsDocLines.get(i - 1).isCodeLine) ? LINE_SPACING : lineHeight / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Additional info
|
||||
if (!additionalInfo.isEmpty()) {
|
||||
totalHeight += LINE_SPACING;
|
||||
for (String infoLine : additionalInfo) {
|
||||
List<String> wrappedLines = wrapText(infoLine, contentWidth);
|
||||
totalHeight += wrappedLines.size() * (lineHeight + LINE_SPACING);
|
||||
}
|
||||
}
|
||||
return Math.max(lineHeight, Math.max(0, totalHeight - LINE_SPACING))-2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate height needed for wrapped segments.
|
||||
*/
|
||||
private static int calculateSegmentsHeight(int maxWidth, List<TokenHoverInfo.TextSegment> segments) {
|
||||
int lineHeight = ClientProxy.Font.height();
|
||||
int currentLineWidth = 0;
|
||||
int lineCount = 1;
|
||||
|
||||
for (TokenHoverInfo.TextSegment segment : segments) {
|
||||
String text = segment.text;
|
||||
String[] words = text.split("(?<=\\s)|(?=\\s)");
|
||||
|
||||
for (String word : words) {
|
||||
int wordWidth = ClientProxy.Font.width(word);
|
||||
|
||||
if (currentLineWidth + wordWidth > maxWidth && currentLineWidth > 0) {
|
||||
lineCount++;
|
||||
currentLineWidth = word.trim().isEmpty() ? 0 : wordWidth;
|
||||
} else {
|
||||
currentLineWidth += wordWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lineCount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Total height = (lines * lineHeight) + (spacing between lines)
|
||||
return (lineCount * lineHeight) + ((lineCount - 1) * LINE_SPACING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the width of the longest line when segments are wrapped.
|
||||
*/
|
||||
private static int calculateSegmentsLongestLine(int maxWidth, List<TokenHoverInfo.TextSegment> segments) {
|
||||
int currentLineWidth = 0;
|
||||
int longestLineWidth = 0;
|
||||
|
||||
for (TokenHoverInfo.TextSegment segment : segments) {
|
||||
String text = segment.text;
|
||||
String[] words = text.split("(?<=\\s)|(?=\\s)");
|
||||
|
||||
for (String word : words) {
|
||||
int wordWidth = ClientProxy.Font.width(word);
|
||||
|
||||
if (currentLineWidth + wordWidth > maxWidth && currentLineWidth > 0) {
|
||||
// Line break - record current line width and start new line
|
||||
longestLineWidth = Math.max(longestLineWidth, currentLineWidth);
|
||||
currentLineWidth = word.trim().isEmpty() ? 0 : wordWidth;
|
||||
} else {
|
||||
currentLineWidth += wordWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last line
|
||||
longestLineWidth = Math.max(longestLineWidth, currentLineWidth);
|
||||
|
||||
return longestLineWidth;
|
||||
}
|
||||
}
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.js_parser;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class DTSJSDocParser {
|
||||
|
||||
private static final Pattern JSDOC_BLOCK_PATTERN = Pattern.compile(
|
||||
"/\\*\\*\\s*(.*?)\\*/", Pattern.DOTALL);
|
||||
|
||||
private static final Pattern TAG_PATTERN = Pattern.compile(
|
||||
"@(\\w+)(?:\\s+\\{([^}]+)\\})?(?:\\s+(.*))?");
|
||||
|
||||
private static final Pattern PARAM_PATTERN = Pattern.compile(
|
||||
"@param(?:\\s+\\{([^}]+)\\})?(?:\\s+(\\w+))?(?:\\s+(.*))?");
|
||||
|
||||
private static final Pattern RETURN_PATTERN = Pattern.compile(
|
||||
"@returns?\\s+(?:\\{([^}]+)\\}\\s*)?(.*)");
|
||||
|
||||
private static final Pattern TYPE_PATTERN = Pattern.compile(
|
||||
"@type(?:\\s+\\{([^}]+)\\})?(?:\\s+(.*))?");
|
||||
|
||||
public static JSDocInfo parseJSDocBlock(String jsDocContent) {
|
||||
if (jsDocContent == null || jsDocContent.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
JSDocInfo info = new JSDocInfo(jsDocContent, -1, -1);
|
||||
|
||||
String[] lines = jsDocContent.split("\\r?\\n");
|
||||
StringBuilder descriptionBuilder = new StringBuilder();
|
||||
boolean foundFirstTag = false;
|
||||
JSDocTag lastTag = null;
|
||||
|
||||
for (String line : lines) {
|
||||
line = cleanLine(line);
|
||||
if (line.startsWith("@")) {
|
||||
foundFirstTag = true;
|
||||
lastTag = parseTag(line, info);
|
||||
} else if (!foundFirstTag) {
|
||||
if (line.isEmpty()) {
|
||||
// Preserve blank lines as paragraph gaps (only if description has started)
|
||||
if (descriptionBuilder.length() > 0) {
|
||||
descriptionBuilder.append("\n");
|
||||
}
|
||||
} else {
|
||||
if (descriptionBuilder.length() > 0) {
|
||||
descriptionBuilder.append("\n");
|
||||
}
|
||||
descriptionBuilder.append(line);
|
||||
}
|
||||
} else if (lastTag != null) {
|
||||
if (!line.isEmpty()) {
|
||||
String existing = lastTag.getDescription();
|
||||
if (existing == null || existing.isEmpty()) {
|
||||
lastTag.setDescription(line);
|
||||
} else {
|
||||
lastTag.setDescription(existing + "\n" + line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (descriptionBuilder.length() > 0) {
|
||||
info.setDescription(descriptionBuilder.toString().trim());
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private static String cleanLine(String line) {
|
||||
// Strip outer leading whitespace (before the * marker) and all trailing whitespace
|
||||
line = line.trim();
|
||||
if (line.startsWith("/**")) {
|
||||
line = line.substring(3).trim();
|
||||
}
|
||||
if (line.endsWith("*/")) {
|
||||
line = line.substring(0, line.length() - 2);
|
||||
// Strip trailing whitespace between content and */ but preserve leading spaces
|
||||
int end = line.length();
|
||||
while (end > 0 && Character.isWhitespace(line.charAt(end - 1))) end--;
|
||||
line = line.substring(0, end);
|
||||
}
|
||||
if (line.startsWith("*")) {
|
||||
line = line.substring(1); // strip the leading *
|
||||
// Consume exactly ONE space after * (the JSDoc conventional separator).
|
||||
// Remaining leading spaces are intentional indentation (e.g. inside code fences).
|
||||
if (!line.isEmpty() && line.charAt(0) == ' ') {
|
||||
line = line.substring(1);
|
||||
}
|
||||
// Strip trailing whitespace
|
||||
int end = line.length();
|
||||
while (end > 0 && Character.isWhitespace(line.charAt(end - 1))) end--;
|
||||
line = line.substring(0, end);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
private static JSDocTag parseTag(String line, JSDocInfo info) {
|
||||
Matcher paramMatcher = PARAM_PATTERN.matcher(line);
|
||||
if (paramMatcher.find()) {
|
||||
String type = paramMatcher.group(1);
|
||||
String name = paramMatcher.group(2);
|
||||
String desc = paramMatcher.group(3);
|
||||
if (desc == null && type == null && name != null) {
|
||||
desc = name;
|
||||
name = null;
|
||||
}
|
||||
if (desc != null) {
|
||||
desc = desc.trim();
|
||||
if (desc.isEmpty()) {
|
||||
desc = null;
|
||||
}
|
||||
}
|
||||
JSDocParamTag tag = JSDocParamTag.create(-1, -1, -1,
|
||||
type, null, -1, -1, name, -1, -1, desc);
|
||||
info.addParamTag(tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
Matcher returnMatcher = RETURN_PATTERN.matcher(line);
|
||||
if (returnMatcher.find()) {
|
||||
String type = returnMatcher.group(1);
|
||||
String desc = returnMatcher.group(2);
|
||||
if (desc != null) {
|
||||
desc = desc.trim();
|
||||
if (desc.isEmpty()) {
|
||||
desc = null;
|
||||
}
|
||||
}
|
||||
JSDocReturnTag tag = JSDocReturnTag.create("return", -1, -1, -1,
|
||||
type, null, -1, -1, desc);
|
||||
info.setReturnTag(tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
Matcher typeMatcher = TYPE_PATTERN.matcher(line);
|
||||
if (typeMatcher.find()) {
|
||||
String type = typeMatcher.group(1);
|
||||
String desc = typeMatcher.group(2);
|
||||
if (desc != null) {
|
||||
desc = desc.trim();
|
||||
if (desc.isEmpty()) {
|
||||
desc = null;
|
||||
}
|
||||
}
|
||||
JSDocTypeTag tag = JSDocTypeTag.create(-1, -1, -1,
|
||||
type, null, -1, -1, desc);
|
||||
info.setTypeTag(tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
Matcher tagMatcher = TAG_PATTERN.matcher(line);
|
||||
if (tagMatcher.find()) {
|
||||
String tagName = tagMatcher.group(1);
|
||||
String typeName = tagMatcher.group(2);
|
||||
String rest = tagMatcher.group(3);
|
||||
|
||||
switch (tagName) {
|
||||
case "see":
|
||||
JSDocSeeTag seeTag = JSDocSeeTag.createSimple(rest != null ? rest.trim() : "");
|
||||
info.addSeeTag(seeTag);
|
||||
return seeTag;
|
||||
default:
|
||||
JSDocTag genericTag = new JSDocTag(tagName, -1, -1, -1);
|
||||
if (typeName != null) {
|
||||
typeName = typeName.trim();
|
||||
genericTag.setType(typeName, null, -1, -1);
|
||||
}
|
||||
genericTag.setDescription(rest != null ? rest.trim() : "");
|
||||
info.addTag(genericTag);
|
||||
return genericTag;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String extractJSDocBefore(String content, int elementStart) {
|
||||
if (elementStart <= 0) return null;
|
||||
|
||||
String searchArea = content.substring(0, elementStart);
|
||||
|
||||
int lastJSDocEnd = searchArea.lastIndexOf("*/");
|
||||
if (lastJSDocEnd < 0) return null;
|
||||
|
||||
int jsDocStart = searchArea.lastIndexOf("/**", lastJSDocEnd);
|
||||
if (jsDocStart < 0) return null;
|
||||
|
||||
String between = searchArea.substring(lastJSDocEnd + 2).trim();
|
||||
if (between.isEmpty() || isOnlyWhitespaceOrModifiers(between)) {
|
||||
return searchArea.substring(jsDocStart, lastJSDocEnd + 2);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isOnlyWhitespaceOrModifiers(String text) {
|
||||
String cleaned = text.replaceAll("\\s+", " ").trim();
|
||||
return cleaned.isEmpty()
|
||||
|| cleaned.matches("^(export\\s*)?(readonly\\s*)?(interface|class|type|function)?\\s*$");
|
||||
}
|
||||
|
||||
public static List<JSDocBlock> extractAllJSDocBlocks(String content) {
|
||||
List<JSDocBlock> blocks = new ArrayList<>();
|
||||
Matcher matcher = JSDOC_BLOCK_PATTERN.matcher(content);
|
||||
|
||||
while (matcher.find()) {
|
||||
int start = matcher.start();
|
||||
int end = matcher.end();
|
||||
String docContent = matcher.group(0);
|
||||
blocks.add(new JSDocBlock(start, end, docContent));
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
public static class JSDocBlock {
|
||||
public final int start;
|
||||
public final int end;
|
||||
public final String content;
|
||||
|
||||
public JSDocBlock(int start, int end, String content) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public JSDocInfo parse() {
|
||||
return parseJSDocBlock(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.js_parser;
|
||||
|
||||
import cpw.mods.fml.common.Loader;
|
||||
import cpw.mods.fml.common.ModContainer;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
final class DtsModScanner {
|
||||
|
||||
private static final Pattern MOD_DTS_PATH_PATTERN = Pattern.compile("^assets/([^/]+)/api/(.+\\.d\\.ts)$");
|
||||
|
||||
private static final List<String> DOMAIN_PRIORITY = Arrays.asList("customnpcs", "npcdbc");
|
||||
|
||||
private DtsModScanner() {}
|
||||
|
||||
static List<DtsFileRef> collectDtsFilesFromMods() {
|
||||
List<DtsFileRef> dtsFiles = new ArrayList<>();
|
||||
try {
|
||||
for (ModContainer mod : Loader.instance().getModList()) {
|
||||
File source = mod.getSource();
|
||||
if (source == null || !source.exists()) {
|
||||
continue;
|
||||
}
|
||||
if (source.isDirectory()) {
|
||||
scanDirectoryForModDts(source, mod.getModId(), dtsFiles);
|
||||
} else if (source.getName().endsWith(".jar") || source.getName().endsWith(".zip")) {
|
||||
scanJarForModDts(source, mod.getModId(), dtsFiles);
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// Headless/non-FML environment: fall back to resource scanning.
|
||||
}
|
||||
return dtsFiles;
|
||||
}
|
||||
|
||||
static void sortDtsFiles(List<DtsFileRef> dtsFiles) {
|
||||
Collections.sort(dtsFiles, new DtsFileRefComparator());
|
||||
}
|
||||
|
||||
static void logSummary(List<DtsFileRef> dtsFiles) {
|
||||
Map<String, Integer> domainCounts = new LinkedHashMap<>();
|
||||
for (DtsFileRef ref : dtsFiles) {
|
||||
domainCounts.put(ref.getDomain(), domainCounts.getOrDefault(ref.getDomain(), 0) + 1);
|
||||
}
|
||||
System.out.println("[JSTypeRegistry] Found " + dtsFiles.size() + " .d.ts files across " + domainCounts.size() + " domains");
|
||||
for (Map.Entry<String, Integer> entry : domainCounts.entrySet()) {
|
||||
System.out.println("[JSTypeRegistry] Domain " + entry.getKey() + ": " + entry.getValue() + " files");
|
||||
}
|
||||
}
|
||||
|
||||
private static void scanDirectoryForModDts(File baseDir, String modId, List<DtsFileRef> dtsFiles) {
|
||||
scanDirectoryForModDts(baseDir, baseDir, modId, dtsFiles);
|
||||
}
|
||||
|
||||
private static void scanDirectoryForModDts(File baseDir, File directory, String modId, List<DtsFileRef> dtsFiles) {
|
||||
File[] files = directory.listFiles();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
scanDirectoryForModDts(baseDir, file, modId, dtsFiles);
|
||||
continue;
|
||||
}
|
||||
if (!file.getName().endsWith(".d.ts")) {
|
||||
continue;
|
||||
}
|
||||
String relativePath = baseDir.toURI().relativize(file.toURI()).getPath();
|
||||
if (relativePath == null) {
|
||||
continue;
|
||||
}
|
||||
String normalized = relativePath.replace('\\', '/');
|
||||
Matcher matcher = MOD_DTS_PATH_PATTERN.matcher(normalized);
|
||||
if (matcher.matches()) {
|
||||
String domain = matcher.group(1);
|
||||
String apiPath = matcher.group(2);
|
||||
dtsFiles.add(DtsFileRef.forFile(modId, domain, apiPath, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void scanJarForModDts(File jarFile, String modId, List<DtsFileRef> dtsFiles) {
|
||||
try {
|
||||
JarFile jar = new JarFile(jarFile);
|
||||
Enumeration<JarEntry> entries = jar.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
String entryName = entry.getName();
|
||||
Matcher matcher = MOD_DTS_PATH_PATTERN.matcher(entryName);
|
||||
if (matcher.matches()) {
|
||||
String domain = matcher.group(1);
|
||||
String apiPath = matcher.group(2);
|
||||
dtsFiles.add(DtsFileRef.forJar(modId, domain, apiPath, jarFile, entryName));
|
||||
}
|
||||
}
|
||||
jar.close();
|
||||
} catch (Exception e) {
|
||||
System.err.println("[JSTypeRegistry] Error scanning mod jar for .d.ts files: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
static final class DtsFileRef {
|
||||
private final String modId;
|
||||
private final String domain;
|
||||
private final String relativePath;
|
||||
private final File file;
|
||||
private final File jarFile;
|
||||
private final String jarEntryName;
|
||||
|
||||
private DtsFileRef(String modId, String domain, String relativePath, File file, File jarFile, String jarEntryName) {
|
||||
this.modId = modId;
|
||||
this.domain = domain;
|
||||
this.relativePath = relativePath;
|
||||
this.file = file;
|
||||
this.jarFile = jarFile;
|
||||
this.jarEntryName = jarEntryName;
|
||||
}
|
||||
|
||||
static DtsFileRef forFile(String modId, String domain, String relativePath, File file) {
|
||||
return new DtsFileRef(modId, domain, relativePath, file, null, null);
|
||||
}
|
||||
|
||||
static DtsFileRef forJar(String modId, String domain, String relativePath, File jarFile, String jarEntryName) {
|
||||
return new DtsFileRef(modId, domain, relativePath, null, jarFile, jarEntryName);
|
||||
}
|
||||
|
||||
InputStream openStream() throws IOException {
|
||||
if (file != null) {
|
||||
return new FileInputStream(file);
|
||||
}
|
||||
if (jarFile != null && jarEntryName != null) {
|
||||
final JarFile jar = new JarFile(jarFile);
|
||||
JarEntry entry = jar.getJarEntry(jarEntryName);
|
||||
if (entry == null) {
|
||||
jar.close();
|
||||
return null;
|
||||
}
|
||||
InputStream is = jar.getInputStream(entry);
|
||||
return new FilterInputStream(is) {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
super.close();
|
||||
jar.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String getDomain() {
|
||||
return domain;
|
||||
}
|
||||
|
||||
String getRelativePath() {
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
String getOrigin() {
|
||||
return modId + ":" + domain + ":" + relativePath;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DtsFileRefComparator implements Comparator<DtsFileRef> {
|
||||
@Override
|
||||
public int compare(DtsFileRef a, DtsFileRef b) {
|
||||
int domainCompare = Integer.compare(getDomainRank(a.getDomain()), getDomainRank(b.getDomain()));
|
||||
if (domainCompare != 0) {
|
||||
return domainCompare;
|
||||
}
|
||||
int domainNameCompare = a.getDomain().compareTo(b.getDomain());
|
||||
if (domainNameCompare != 0) {
|
||||
return domainNameCompare;
|
||||
}
|
||||
int filePriorityCompare = Integer.compare(getFilePriority(a.getRelativePath()), getFilePriority(b.getRelativePath()));
|
||||
if (filePriorityCompare != 0) {
|
||||
return filePriorityCompare;
|
||||
}
|
||||
return a.getRelativePath().compareTo(b.getRelativePath());
|
||||
}
|
||||
|
||||
private int getDomainRank(String domain) {
|
||||
int idx = DOMAIN_PRIORITY.indexOf(domain);
|
||||
return idx >= 0 ? idx : DOMAIN_PRIORITY.size();
|
||||
}
|
||||
|
||||
private int getFilePriority(String relativePath) {
|
||||
if (relativePath.endsWith("hooks.d.ts")) {
|
||||
return 0;
|
||||
}
|
||||
if (relativePath.endsWith("index.d.ts")) {
|
||||
return 1;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.js_parser;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.GenericContext;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver;
|
||||
/**
|
||||
* Represents a field/property in a TypeScript interface.
|
||||
*/
|
||||
public class JSFieldInfo {
|
||||
|
||||
private final String name;
|
||||
private final String type; // Raw type string for display
|
||||
private TypeInfo typeInfo; // Resolved TypeInfo
|
||||
private final boolean readonly;
|
||||
private boolean isStatic;
|
||||
private JSDocInfo jsDocInfo;
|
||||
private JSTypeInfo containingType; // The type that contains this field
|
||||
|
||||
public JSFieldInfo(String name, String type, boolean readonly) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.readonly = readonly;
|
||||
}
|
||||
|
||||
public JSFieldInfo setJsDocInfo(JSDocInfo jsDocInfo) {
|
||||
this.jsDocInfo = jsDocInfo;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isStatic() { return isStatic; }
|
||||
|
||||
public JSFieldInfo setStatic(boolean isStatic) {
|
||||
this.isStatic = isStatic;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the containing type (the JSTypeInfo that owns this field).
|
||||
*/
|
||||
public void setContainingType(JSTypeInfo containingType) {
|
||||
this.containingType = containingType;
|
||||
}
|
||||
|
||||
public void setTypeInfo(TypeInfo typeInfo) {
|
||||
this.typeInfo = typeInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolved type, with fallback resolution using type parameters.
|
||||
* @param contextType The TypeInfo context for resolving type parameters
|
||||
* @return The resolved TypeInfo for the field type
|
||||
*/
|
||||
public TypeInfo getResolvedType(TypeInfo contextType) {
|
||||
TypeResolver resolver = TypeResolver.getInstance();
|
||||
|
||||
// Cache only the raw resolved type; substitutions depend on the receiver context.
|
||||
TypeInfo resolved = typeInfo != null ? typeInfo : resolver.resolveJSType(type);
|
||||
if (typeInfo == null) {
|
||||
typeInfo = resolved;
|
||||
}
|
||||
|
||||
if (contextType != null) {
|
||||
GenericContext ctx = GenericContext.forReceiver(contextType);
|
||||
return ctx.substituteType(resolved, type, resolver);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getName() { return name; }
|
||||
public String getType() { return type; }
|
||||
public TypeInfo getTypeInfo() { return typeInfo; }
|
||||
public boolean isReadonly() { return readonly; }
|
||||
public JSDocInfo getJsDocInfo() { return jsDocInfo; }
|
||||
public JSTypeInfo getContainingType() { return containingType; }
|
||||
|
||||
/**
|
||||
* Get documentation string (backward compatibility).
|
||||
* Extracts description from JSDocInfo if available.
|
||||
*/
|
||||
public String getDocumentation() {
|
||||
return jsDocInfo != null ? jsDocInfo.getDescription() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name - uses resolved TypeInfo display name if available,
|
||||
* including generic type arguments like List<String>.
|
||||
*/
|
||||
public String getDisplayType() {
|
||||
if (typeInfo != null && typeInfo.isResolved()) {
|
||||
return typeInfo.getDisplayName();
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build hover info HTML for this field.
|
||||
*/
|
||||
public String buildHoverInfo() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (readonly) {
|
||||
sb.append("<i>readonly</i> ");
|
||||
}
|
||||
sb.append("<b>").append(name).append("</b>: <i>").append(type).append("</i>");
|
||||
|
||||
if (jsDocInfo != null && jsDocInfo.getDescription() != null && !jsDocInfo.getDescription().isEmpty()) {
|
||||
sb.append("<br><br>").append(jsDocInfo.getDescription());
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return (readonly ? "readonly " : "") + name + ": " + type;
|
||||
}
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.js_parser;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.GenericContext;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Represents a method signature parsed from .d.ts files.
|
||||
*/
|
||||
public class JSMethodInfo {
|
||||
|
||||
private final String name;
|
||||
private final String returnType; // Raw type string for display, e.g., "number", "IEntity", "void"
|
||||
private TypeInfo returnTypeInfo; // Resolved TypeInfo (set during Phase 2)
|
||||
private final List<JSParameterInfo> parameters;
|
||||
private boolean isStatic;
|
||||
private JSDocInfo jsDocInfo;
|
||||
private JSTypeInfo containingType; // The type that contains this method
|
||||
|
||||
public JSMethodInfo(String name, String returnType, List<JSParameterInfo> parameters) {
|
||||
this.name = name;
|
||||
this.returnType = returnType;
|
||||
this.parameters = parameters != null ? new ArrayList<>(parameters) : new ArrayList<>();
|
||||
}
|
||||
|
||||
public JSMethodInfo setJsDocInfo(JSDocInfo jsDocInfo) {
|
||||
this.jsDocInfo = jsDocInfo;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isStatic() { return isStatic; }
|
||||
|
||||
public JSMethodInfo setStatic(boolean isStatic) {
|
||||
this.isStatic = isStatic;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the resolved return type info (called during Phase 2 type resolution).
|
||||
*/
|
||||
public void setReturnTypeInfo(TypeInfo typeInfo) {
|
||||
this.returnTypeInfo = typeInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the containing type (the JSTypeInfo that owns this method).
|
||||
*/
|
||||
public void setContainingType(JSTypeInfo containingType) {
|
||||
this.containingType = containingType;
|
||||
// Also set for all parameters
|
||||
for (JSParameterInfo param : parameters)
|
||||
param.setContainingMethod(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolved return type, with type parameter substitution based on receiver context.
|
||||
*
|
||||
* For parameterized receivers like List<String>, this will substitute type variables:
|
||||
* - If return type is "T" and receiver is List<String>, returns TypeInfo(String)
|
||||
* - Handles nested generics like List<Map<K,V>> with proper substitution
|
||||
*
|
||||
* @param contextType The TypeInfo context for resolving type parameters (e.g., List<String> to resolve E → String)
|
||||
* @return The resolved TypeInfo for the return type, with type variables substituted
|
||||
*/
|
||||
public TypeInfo getResolvedReturnType(TypeInfo contextType) {
|
||||
TypeResolver resolver = TypeResolver.getInstance();
|
||||
TypeInfo resolved = resolver.resolveJSType(returnType);
|
||||
|
||||
if (contextType != null) {
|
||||
GenericContext ctx = GenericContext.forReceiver(contextType);
|
||||
resolved = ctx.substituteType(resolved, returnType, resolver);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getName() { return name; }
|
||||
public String getReturnType() { return returnType; }
|
||||
public TypeInfo getReturnTypeInfo() { return returnTypeInfo; }
|
||||
public List<JSParameterInfo> getParameters() { return parameters; }
|
||||
public JSDocInfo getJsDocInfo() { return jsDocInfo; }
|
||||
public int getParameterCount() { return parameters.size(); }
|
||||
public JSTypeInfo getContainingType() { return containingType; }
|
||||
|
||||
/**
|
||||
* Get documentation string (backward compatibility).
|
||||
* Extracts description from JSDocInfo if available.
|
||||
*/
|
||||
public String getDocumentation() {
|
||||
return jsDocInfo != null ? jsDocInfo.getDescription() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a formatted signature string for display.
|
||||
*/
|
||||
public String getSignature() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(name).append("(");
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
if (i > 0) sb.append(", ");
|
||||
JSParameterInfo param = parameters.get(i);
|
||||
sb.append(param.getName()).append(": ").append(param.getType());
|
||||
}
|
||||
sb.append("): ").append(returnType);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build hover info HTML for this method.
|
||||
*/
|
||||
public String buildHoverInfo() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<b>").append(name).append("</b>(");
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
if (i > 0) sb.append(", ");
|
||||
JSParameterInfo param = parameters.get(i);
|
||||
sb.append(param.getName()).append(": <i>").append(param.getType()).append("</i>");
|
||||
}
|
||||
sb.append("): <i>").append(returnType).append("</i>");
|
||||
|
||||
if (jsDocInfo != null && jsDocInfo.getDescription() != null && !jsDocInfo.getDescription().isEmpty()) {
|
||||
sb.append("<br><br>").append(jsDocInfo.getDescription());
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getSignature();
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a parameter in a method signature.
|
||||
*/
|
||||
public static class JSParameterInfo {
|
||||
private final String name;
|
||||
private final String type;
|
||||
private TypeInfo typeInfo; // Resolved TypeInfo (set during Phase 2)
|
||||
private JSMethodInfo containingMethod; // The type that contains the method with this parameter
|
||||
private boolean isVarArg = false;
|
||||
|
||||
public JSParameterInfo(String name, String type) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public void setVarArg(boolean isVarArg) { this.isVarArg = isVarArg; }
|
||||
public boolean isVarArg() { return isVarArg; }
|
||||
|
||||
public void setContainingMethod(JSMethodInfo containingMethod) {
|
||||
this.containingMethod = containingMethod;
|
||||
}
|
||||
|
||||
public void setTypeInfo(TypeInfo typeInfo) {
|
||||
this.typeInfo = typeInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolved type, with type parameter substitution based on receiver context.
|
||||
*
|
||||
* For parameterized receivers like Consumer<IAction>, this will substitute type variables:
|
||||
* - If param type is "T" and receiver is Consumer<IAction>, returns TypeInfo(IAction)
|
||||
*
|
||||
* @param contextType The TypeInfo context for resolving type parameters
|
||||
* @return The resolved TypeInfo for the parameter type
|
||||
*/
|
||||
public TypeInfo getResolvedType(TypeInfo contextType) {
|
||||
TypeResolver resolver = TypeResolver.getInstance();
|
||||
TypeInfo resolved = resolver.resolveJSType(type);
|
||||
|
||||
if (contextType != null) {
|
||||
GenericContext ctx = GenericContext.forReceiver(contextType);
|
||||
resolved = ctx.substituteType(resolved, type, resolver);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public String getName() { return name; }
|
||||
public String getType() { return type; }
|
||||
public TypeInfo getTypeInfo() {return typeInfo;}
|
||||
public JSMethodInfo getContainingMethod() {return containingMethod;}
|
||||
|
||||
/**
|
||||
* Get display name - uses resolved TypeInfo display name if available,
|
||||
* including generic type arguments like List<String>.
|
||||
*/
|
||||
public String getDisplayType() {
|
||||
if (typeInfo != null && typeInfo.isResolved()) {
|
||||
return typeInfo.getDisplayName();
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name + ": " + type;
|
||||
}
|
||||
}
|
||||
}
|
||||
+554
@@ -0,0 +1,554 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.js_parser;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.ScriptLine;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.token.TokenType;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
import scala.annotation.meta.field;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.*;
|
||||
|
||||
/**
|
||||
* Analyzes JavaScript scripts to produce syntax highlighting marks and type information.
|
||||
* Uses the JSTypeRegistry to resolve types and validate member access.
|
||||
*
|
||||
* @deprecated This class is deprecated. All JavaScript analysis is now handled by the
|
||||
* unified pipeline in {@link ScriptDocument}. JavaScript and Java scripts now use the
|
||||
* same methods and data structures:
|
||||
* <ul>
|
||||
* <li>{@link ScriptDocument#parseMethodDeclarations()} - for both Java methods and JS functions</li>
|
||||
* <li>{@link ScriptDocument#parseLocalVariables()} - for both Java and JS local variables</li>
|
||||
* <li>{@link ScriptDocument#parseGlobalFields()} - for both Java and JS global variables</li>
|
||||
* <li>{@link ScriptDocument#buildMarks(List)} - for mark building (both languages)</li>
|
||||
* </ul>
|
||||
* Type information is available via the unified data structures: {@code getMethods()},
|
||||
* {@code getMethodLocals()}, and {@code getGlobalFields()}.
|
||||
*/
|
||||
@Deprecated
|
||||
public class JSScriptAnalyzer {
|
||||
|
||||
private final ScriptDocument document;
|
||||
private final String text;
|
||||
private final JSTypeRegistry registry;
|
||||
|
||||
// Variable tracking: varName -> inferred type name
|
||||
private final Map<String, String> variableTypes = new HashMap<>();
|
||||
|
||||
// Function parameter types: funcName -> paramName -> typeName
|
||||
private final Map<String, Map<String, String>> functionParams = new HashMap<>();
|
||||
|
||||
// Excluded ranges (strings, comments)
|
||||
private final List<int[]> excludedRanges = new ArrayList<>();
|
||||
|
||||
// Patterns
|
||||
private static final Pattern STRING_PATTERN = Pattern.compile("\"(?:[^\"\\\\]|\\\\.)*\"|'(?:[^'\\\\]|\\\\.)*'");
|
||||
private static final Pattern COMMENT_PATTERN = Pattern.compile("//.*?$|/\\*.*?\\*/", Pattern.MULTILINE | Pattern.DOTALL);
|
||||
private static final Pattern FUNCTION_PATTERN = Pattern.compile("function\\s+(\\w+)\\s*\\(([^)]*)\\)");
|
||||
private static final Pattern VAR_DECL_PATTERN = Pattern.compile("(?:var|let|const)\\s+(\\w+)(?:\\s*=\\s*([^;]+))?");
|
||||
private static final Pattern ASSIGNMENT_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*([^;]+)");
|
||||
private static final Pattern METHOD_CALL_PATTERN = Pattern.compile("(\\w+(?:\\.\\w+)*)\\s*\\(");
|
||||
private static final Pattern MEMBER_ACCESS_PATTERN = Pattern.compile("(\\w+)(?:\\.(\\w+))+");
|
||||
private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b");
|
||||
|
||||
private static final Pattern JS_KEYWORD_PATTERN = Pattern.compile(
|
||||
"\\b(function|var|let|const|if|else|for|while|do|switch|case|break|continue|return|" +
|
||||
"try|catch|finally|throw|new|typeof|instanceof|in|of|this|null|undefined|true|false|" +
|
||||
"class|extends|import|export|default|async|await|yield)\\b");
|
||||
|
||||
private static final Pattern NUMBER_PATTERN = Pattern.compile("\\b\\d+\\.?\\d*\\b");
|
||||
|
||||
public JSScriptAnalyzer(ScriptDocument document) {
|
||||
this.document = document;
|
||||
this.text = document.getText();
|
||||
this.registry = JSTypeRegistry.getInstance();
|
||||
// Ensure registry is initialized
|
||||
if (!registry.isInitialized()) {
|
||||
registry.initializeFromResources();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze the script and produce marks.
|
||||
*/
|
||||
public List<ScriptLine.Mark> analyze() {
|
||||
List<ScriptLine.Mark> marks = new ArrayList<>();
|
||||
|
||||
// First pass: find excluded regions
|
||||
findExcludedRanges();
|
||||
|
||||
// Mark comments and strings
|
||||
addPatternMarks(marks, COMMENT_PATTERN, TokenType.COMMENT);
|
||||
addPatternMarks(marks, STRING_PATTERN, TokenType.STRING);
|
||||
|
||||
// Mark keywords
|
||||
addPatternMarks(marks, JS_KEYWORD_PATTERN, TokenType.KEYWORD);
|
||||
|
||||
// Mark numbers
|
||||
addPatternMarks(marks, NUMBER_PATTERN, TokenType.LITERAL);
|
||||
|
||||
// Parse functions and infer parameter types from hooks
|
||||
parseFunctions(marks);
|
||||
|
||||
// Parse variable declarations
|
||||
parseVariables(marks);
|
||||
|
||||
// Mark member accesses with type validation
|
||||
markMemberAccesses(marks);
|
||||
|
||||
// Mark method calls
|
||||
markMethodCalls(marks);
|
||||
|
||||
// Mark standalone identifiers (parameters and variables in function bodies)
|
||||
markIdentifiers(marks);
|
||||
|
||||
return marks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find strings and comments to exclude from analysis.
|
||||
*/
|
||||
private void findExcludedRanges() {
|
||||
Matcher m = STRING_PATTERN.matcher(text);
|
||||
while (m.find()) {
|
||||
excludedRanges.add(new int[]{m.start(), m.end()});
|
||||
}
|
||||
|
||||
m = COMMENT_PATTERN.matcher(text);
|
||||
while (m.find()) {
|
||||
excludedRanges.add(new int[]{m.start(), m.end()});
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isExcluded(int pos) {
|
||||
for (int[] range : excludedRanges) {
|
||||
if (pos >= range[0] && pos < range[1]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse function declarations and infer parameter types from hook signatures.
|
||||
*/
|
||||
private void parseFunctions(List<ScriptLine.Mark> marks) {
|
||||
Matcher m = FUNCTION_PATTERN.matcher(text);
|
||||
while (m.find()) {
|
||||
if (isExcluded(m.start())) continue;
|
||||
|
||||
String funcName = m.group(1);
|
||||
String params = m.group(2);
|
||||
|
||||
// Mark function name
|
||||
int nameStart = m.start(1);
|
||||
int nameEnd = m.end(1);
|
||||
|
||||
// Check if this is a known hook
|
||||
if (registry.isHook(funcName)) {
|
||||
// Create a unified MethodInfo for the hook
|
||||
List<JSTypeRegistry.HookSignature> sigs = registry.getHookSignatures(funcName);
|
||||
if (!sigs.isEmpty()) {
|
||||
JSTypeRegistry.HookSignature sig = sigs.get(0);
|
||||
|
||||
// Create unified MethodInfo for hover info
|
||||
TypeInfo paramTypeInfo = resolveJSType(sig.paramType);
|
||||
List<FieldInfo> methodParams = new ArrayList<>();
|
||||
methodParams.add(FieldInfo.reflectionParam(sig.paramName, paramTypeInfo));
|
||||
|
||||
MethodInfo hookMethod = MethodInfo.declaration(
|
||||
funcName, null, TypeInfo.fromPrimitive("void"), methodParams,
|
||||
nameStart, nameStart, nameStart, -1, -1, 0, sig.doc
|
||||
);
|
||||
|
||||
marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_DECL, hookMethod));
|
||||
|
||||
// Infer parameter types from hook
|
||||
if (!params.isEmpty()) {
|
||||
// Parse parameter names from function
|
||||
String[] paramNames = params.split(",");
|
||||
if (paramNames.length > 0) {
|
||||
String paramName = paramNames[0].trim();
|
||||
String paramType = sig.paramType;
|
||||
|
||||
// Store in function params and variable types
|
||||
Map<String, String> funcParamMap = new HashMap<>();
|
||||
funcParamMap.put(paramName, paramType);
|
||||
functionParams.put(funcName, funcParamMap);
|
||||
variableTypes.put(paramName, paramType);
|
||||
|
||||
// Mark the parameter with unified FieldInfo
|
||||
int paramStart = m.start(2) + params.indexOf(paramName);
|
||||
int paramEnd = paramStart + paramName.length();
|
||||
FieldInfo paramFieldInfo = FieldInfo.parameter(
|
||||
paramName, paramTypeInfo, paramStart, hookMethod
|
||||
);
|
||||
marks.add(new ScriptLine.Mark(paramStart, paramEnd, TokenType.PARAMETER, paramFieldInfo));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_DECL, funcName));
|
||||
}
|
||||
} else {
|
||||
marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_DECL, funcName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse variable declarations and infer types.
|
||||
*/
|
||||
private void parseVariables(List<ScriptLine.Mark> marks) {
|
||||
Matcher m = VAR_DECL_PATTERN.matcher(text);
|
||||
while (m.find()) {
|
||||
if (isExcluded(m.start())) continue;
|
||||
|
||||
String varName = m.group(1);
|
||||
String initializer = m.group(2);
|
||||
|
||||
// Infer type from initializer
|
||||
String inferredType = null;
|
||||
if (initializer != null) {
|
||||
inferredType = inferTypeFromExpression(initializer.trim());
|
||||
}
|
||||
|
||||
if (inferredType != null) {
|
||||
variableTypes.put(varName, inferredType);
|
||||
}
|
||||
|
||||
// Mark variable declaration with unified FieldInfo
|
||||
int varStart = m.start(1);
|
||||
int varEnd = m.end(1);
|
||||
TypeInfo typeInfo = resolveJSType(inferredType != null ? inferredType : "any");
|
||||
FieldInfo varFieldInfo = FieldInfo.localField(varName, typeInfo, varStart, null);
|
||||
marks.add(new ScriptLine.Mark(varStart, varEnd, TokenType.LOCAL_FIELD, varFieldInfo));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer type from an expression.
|
||||
*/
|
||||
private String inferTypeFromExpression(String expr) {
|
||||
if (expr == null || expr.isEmpty()) return null;
|
||||
|
||||
// String literal
|
||||
if (expr.startsWith("\"") || expr.startsWith("'")) {
|
||||
return "string";
|
||||
}
|
||||
|
||||
// Number literal
|
||||
if (expr.matches("\\d+\\.?\\d*")) {
|
||||
return "number";
|
||||
}
|
||||
|
||||
// Boolean literal
|
||||
if (expr.equals("true") || expr.equals("false")) {
|
||||
return "boolean";
|
||||
}
|
||||
|
||||
// null/undefined
|
||||
if (expr.equals("null")) return "null";
|
||||
if (expr.equals("undefined")) return "undefined";
|
||||
|
||||
// Array literal
|
||||
if (expr.startsWith("[")) {
|
||||
return "any[]";
|
||||
}
|
||||
|
||||
// Object literal
|
||||
if (expr.startsWith("{")) {
|
||||
return "object";
|
||||
}
|
||||
|
||||
// Method call: something.method() - infer from method return type
|
||||
if (expr.contains(".") && expr.contains("(")) {
|
||||
return inferTypeFromMethodCall(expr);
|
||||
}
|
||||
|
||||
// Variable reference
|
||||
if (variableTypes.containsKey(expr)) {
|
||||
return variableTypes.get(expr);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer type from a method call chain.
|
||||
*/
|
||||
private String inferTypeFromMethodCall(String expr) {
|
||||
// Remove trailing parentheses and args for analysis
|
||||
int parenIndex = expr.indexOf('(');
|
||||
if (parenIndex > 0) {
|
||||
expr = expr.substring(0, parenIndex);
|
||||
}
|
||||
|
||||
String[] parts = expr.split("\\.");
|
||||
if (parts.length < 2) return null;
|
||||
|
||||
// Start with the receiver type
|
||||
String currentType = variableTypes.get(parts[0]);
|
||||
if (currentType == null) return null;
|
||||
|
||||
// Walk the chain
|
||||
for (int i = 1; i < parts.length; i++) {
|
||||
JSTypeInfo typeInfo = registry.getType(currentType);
|
||||
if (typeInfo == null) return null;
|
||||
|
||||
String member = parts[i];
|
||||
|
||||
// Check if it's a method
|
||||
JSMethodInfo method = typeInfo.getMethod(member);
|
||||
if (method != null) {
|
||||
currentType = method.getReturnType();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if it's a field
|
||||
JSFieldInfo f = typeInfo.getField(member);
|
||||
if (f != null) {
|
||||
currentType = f.getType();
|
||||
continue;
|
||||
}
|
||||
|
||||
return null; // Unknown member
|
||||
}
|
||||
|
||||
return currentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark member accesses (x.y.z) with type validation.
|
||||
*/
|
||||
private void markMemberAccesses(List<ScriptLine.Mark> marks) {
|
||||
Matcher m = MEMBER_ACCESS_PATTERN.matcher(text);
|
||||
while (m.find()) {
|
||||
if (isExcluded(m.start())) continue;
|
||||
|
||||
String fullAccess = m.group(0);
|
||||
String[] parts = fullAccess.split("\\.");
|
||||
|
||||
if (parts.length < 2) continue;
|
||||
|
||||
// Get receiver type
|
||||
String receiverName = parts[0];
|
||||
String currentType = variableTypes.get(receiverName);
|
||||
|
||||
int pos = m.start();
|
||||
|
||||
// Mark receiver with unified FieldInfo
|
||||
if (currentType != null) {
|
||||
TypeInfo unifiedType = resolveJSType(currentType);
|
||||
FieldInfo receiverField = FieldInfo.localField(receiverName, unifiedType, pos, null);
|
||||
marks.add(new ScriptLine.Mark(pos, pos + receiverName.length(), TokenType.LOCAL_FIELD, receiverField));
|
||||
}
|
||||
|
||||
pos += receiverName.length() + 1; // +1 for the dot
|
||||
|
||||
// Walk the chain and mark each member
|
||||
for (int i = 1; i < parts.length; i++) {
|
||||
String member = parts[i];
|
||||
int memberStart = pos;
|
||||
int memberEnd = pos + member.length();
|
||||
|
||||
if (currentType != null) {
|
||||
JSTypeInfo jsTypeInfo = registry.getType(currentType);
|
||||
if (jsTypeInfo != null) {
|
||||
TypeInfo unifiedContainingType = TypeInfo.fromJSTypeInfo(jsTypeInfo);
|
||||
|
||||
// Check method first
|
||||
JSMethodInfo jsMethod = jsTypeInfo.getMethod(member);
|
||||
if (jsMethod != null) {
|
||||
// Convert to unified MethodInfo
|
||||
MethodInfo unifiedMethod = MethodInfo.fromJSMethod(jsMethod, unifiedContainingType);
|
||||
marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.METHOD_CALL, unifiedMethod));
|
||||
currentType = jsMethod.getReturnType();
|
||||
} else {
|
||||
// Check field
|
||||
JSFieldInfo jsField = jsTypeInfo.getField(member);
|
||||
if (jsField != null) {
|
||||
// Convert to unified FieldInfo
|
||||
FieldInfo unifiedField = FieldInfo.fromJSField(jsField, unifiedContainingType);
|
||||
marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.GLOBAL_FIELD, unifiedField));
|
||||
currentType = jsField.getType();
|
||||
} else {
|
||||
// Unknown member - mark as undefined
|
||||
marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.UNDEFINED_VAR,
|
||||
"Unknown member '" + member + "' on type " + jsTypeInfo.getFullName()));
|
||||
currentType = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
currentType = null;
|
||||
}
|
||||
}
|
||||
|
||||
pos = memberEnd + 1; // +1 for the next dot
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark method calls.
|
||||
*/
|
||||
private void markMethodCalls(List<ScriptLine.Mark> marks) {
|
||||
Matcher m = METHOD_CALL_PATTERN.matcher(text);
|
||||
while (m.find()) {
|
||||
if (isExcluded(m.start())) continue;
|
||||
|
||||
String callExpr = m.group(1);
|
||||
if (!callExpr.contains(".")) {
|
||||
// Simple function call
|
||||
int nameStart = m.start(1);
|
||||
int nameEnd = m.end(1);
|
||||
|
||||
if (registry.isHook(callExpr)) {
|
||||
// It's a known hook being called - create unified MethodInfo
|
||||
List<JSTypeRegistry.HookSignature> sigs = registry.getHookSignatures(callExpr);
|
||||
if (!sigs.isEmpty()) {
|
||||
JSTypeRegistry.HookSignature sig = sigs.get(0);
|
||||
TypeInfo paramTypeInfo = resolveJSType(sig.paramType);
|
||||
List<FieldInfo> methodParams = new ArrayList<>();
|
||||
methodParams.add(FieldInfo.reflectionParam(sig.paramName, paramTypeInfo));
|
||||
|
||||
MethodInfo hookMethod = MethodInfo.declaration(
|
||||
callExpr, null, TypeInfo.fromPrimitive("void"), methodParams,
|
||||
nameStart, nameStart, nameStart, -1, -1, 0, sig.doc
|
||||
);
|
||||
marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, hookMethod));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Chained calls are handled in markMemberAccesses
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add pattern-based marks.
|
||||
*/
|
||||
private void addPatternMarks(List<ScriptLine.Mark> marks, Pattern pattern, TokenType type) {
|
||||
Matcher m = pattern.matcher(text);
|
||||
while (m.find()) {
|
||||
marks.add(new ScriptLine.Mark(m.start(), m.end(), type));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inferred type of a variable at a position.
|
||||
*/
|
||||
public String getVariableType(String varName) {
|
||||
return variableTypes.get(varName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all inferred variable types.
|
||||
*/
|
||||
public Map<String, String> getVariableTypes() {
|
||||
return new HashMap<>(variableTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a JavaScript type name to a unified TypeInfo.
|
||||
* Handles primitives, mapped types, and custom types from the registry.
|
||||
*/
|
||||
private TypeInfo resolveJSType(String jsTypeName) {
|
||||
if (jsTypeName == null || jsTypeName.isEmpty() || "void".equals(jsTypeName)) {
|
||||
return TypeInfo.fromPrimitive("void");
|
||||
}
|
||||
|
||||
// Handle JS primitives
|
||||
switch (jsTypeName) {
|
||||
case "string":
|
||||
return TypeInfo.fromClass(String.class);
|
||||
case "number":
|
||||
return TypeInfo.fromClass(double.class);
|
||||
case "boolean":
|
||||
return TypeInfo.fromClass(boolean.class);
|
||||
case "any":
|
||||
return TypeInfo.fromClass(Object.class);
|
||||
case "void":
|
||||
return TypeInfo.fromPrimitive("void");
|
||||
}
|
||||
|
||||
// Handle array types
|
||||
if (jsTypeName.endsWith("[]")) {
|
||||
String elementType = jsTypeName.substring(0, jsTypeName.length() - 2);
|
||||
TypeInfo elementTypeInfo = resolveJSType(elementType);
|
||||
return TypeInfo.arrayOf(elementTypeInfo);
|
||||
}
|
||||
|
||||
// Try to resolve from the JS type registry
|
||||
if (registry != null) {
|
||||
JSTypeInfo jsTypeInfo = registry.getType(jsTypeName);
|
||||
if (jsTypeInfo != null) {
|
||||
return TypeInfo.fromJSTypeInfo(jsTypeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: unresolved type
|
||||
return TypeInfo.unresolved(jsTypeName, jsTypeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all identifiers in the script - variables and parameters.
|
||||
* This ensures consistent coloring for parameters throughout function bodies.
|
||||
*/
|
||||
private void markIdentifiers(List<ScriptLine.Mark> marks) {
|
||||
// Build set of positions already marked
|
||||
Set<Integer> markedPositions = new HashSet<>();
|
||||
for (ScriptLine.Mark mark : marks) {
|
||||
for (int i = mark.start; i < mark.end; i++) {
|
||||
markedPositions.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Find all identifiers
|
||||
Matcher matcher = IDENTIFIER_PATTERN.matcher(text);
|
||||
while (matcher.find()) {
|
||||
if (isExcluded(matcher.start())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int start = matcher.start();
|
||||
int end = matcher.end();
|
||||
|
||||
// Skip if already marked
|
||||
boolean alreadyMarked = false;
|
||||
for (int i = start; i < end; i++) {
|
||||
if (markedPositions.contains(i)) {
|
||||
alreadyMarked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (alreadyMarked) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String identifier = matcher.group();
|
||||
|
||||
// Check if it's a parameter
|
||||
boolean isParameter = false;
|
||||
for (Map<String, String> params : functionParams.values()) {
|
||||
if (params.containsKey(identifier)) {
|
||||
isParameter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isParameter) {
|
||||
marks.add(new ScriptLine.Mark(start, end, TokenType.PARAMETER));
|
||||
// Mark this position as used
|
||||
for (int i = start; i < end; i++) {
|
||||
markedPositions.add(i);
|
||||
}
|
||||
} else if (variableTypes.containsKey(identifier)) {
|
||||
// It's a local variable
|
||||
marks.add(new ScriptLine.Mark(start, end, TokenType.LOCAL_FIELD));
|
||||
// Mark this position as used
|
||||
for (int i = start; i < end; i++) {
|
||||
markedPositions.add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.js_parser;
|
||||
|
||||
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.JSDocReturnTag;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocTypeTag;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Represents a TypeScript interface/type parsed from .d.ts files.
|
||||
* This is the JS equivalent of TypeInfo for Java types.
|
||||
*/
|
||||
public class JSTypeInfo {
|
||||
|
||||
private final String simpleName; // e.g., "InteractEvent"
|
||||
private final String fullName; // e.g., "IPlayerEvent.InteractEvent"
|
||||
private final String namespace; // e.g., "IPlayerEvent" (parent namespace, null for top-level)
|
||||
|
||||
// Java fully-qualified name (e.g., "noppes.npcs.api.entity.IPlayer")
|
||||
private String javaFqn;
|
||||
|
||||
// Type parameters (generics)
|
||||
private final List<TypeParamInfo> typeParams = new ArrayList<>();
|
||||
|
||||
// Members
|
||||
private final Map<String, JSMethodInfo> methods = new LinkedHashMap<>();
|
||||
private final Map<String, JSFieldInfo> fields = new LinkedHashMap<>();
|
||||
|
||||
// Inheritance
|
||||
private String extendsType; // The type this interface extends
|
||||
private JSTypeInfo resolvedParent; // Resolved parent type (after registry is built)
|
||||
|
||||
// Inner types (for namespaces)
|
||||
private final Map<String, JSTypeInfo> innerTypes = new LinkedHashMap<>();
|
||||
private JSTypeInfo parentType; // The containing type (for inner types)
|
||||
|
||||
// Documentation
|
||||
private JSDocInfo jsDocInfo;
|
||||
|
||||
public JSTypeInfo(String simpleName, String namespace) {
|
||||
this.simpleName = simpleName;
|
||||
this.namespace = namespace;
|
||||
this.fullName = namespace != null ? namespace + "." + simpleName : simpleName;
|
||||
}
|
||||
|
||||
// Builder methods
|
||||
public JSTypeInfo setExtends(String extendsType) {
|
||||
this.extendsType = extendsType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JSTypeInfo setJsDocInfo(JSDocInfo jsDocInfo) {
|
||||
this.jsDocInfo = jsDocInfo;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JSTypeInfo setJavaFqn(String javaFqn) {
|
||||
this.javaFqn = javaFqn;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void addMethod(JSMethodInfo method) {
|
||||
method.setContainingType(this);
|
||||
// Handle overloads - store with index if name already exists
|
||||
String key = method.getName();
|
||||
if (methods.containsKey(key)) {
|
||||
// Find next available key for overload
|
||||
int index = 1;
|
||||
while (methods.containsKey(key + "$" + index)) {
|
||||
index++;
|
||||
}
|
||||
methods.put(key + "$" + index, method);
|
||||
} else {
|
||||
methods.put(key, method);
|
||||
}
|
||||
}
|
||||
|
||||
public void addField(JSFieldInfo field) {
|
||||
field.setContainingType(this);
|
||||
fields.put(field.getName(), field);
|
||||
}
|
||||
|
||||
public void addInnerType(JSTypeInfo inner) {
|
||||
inner.parentType = this;
|
||||
innerTypes.put(inner.getSimpleName(), inner);
|
||||
}
|
||||
|
||||
public void setResolvedParent(JSTypeInfo parent) {
|
||||
this.resolvedParent = parent;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getSimpleName() { return simpleName; }
|
||||
public String getFullName() { return fullName; }
|
||||
public String getNamespace() { return namespace; }
|
||||
public String getJavaFqn() { return javaFqn; }
|
||||
public String getExtendsType() { return extendsType; }
|
||||
public JSTypeInfo getResolvedParent() { return resolvedParent; }
|
||||
public JSDocInfo getJsDocInfo() { return jsDocInfo; }
|
||||
public JSTypeInfo getParentType() { return parentType; }
|
||||
public List<TypeParamInfo> getTypeParams() { return typeParams; }
|
||||
|
||||
public Map<String, JSMethodInfo> getMethods() { return methods; }
|
||||
public Map<String, JSFieldInfo> getFields() { return fields; }
|
||||
public Map<String, JSTypeInfo> getInnerTypes() { return innerTypes; }
|
||||
|
||||
/**
|
||||
* Get the type parameter info for a given parameter name (e.g., "T").
|
||||
* @return TypeParamInfo or null if not found
|
||||
*/
|
||||
public TypeParamInfo getTypeParam(String name) {
|
||||
for (TypeParamInfo param : typeParams) {
|
||||
if (param.getName().equals(name)) {
|
||||
return param;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void addTypeParam(TypeParamInfo param) {
|
||||
typeParams.add(param);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve all type parameters for this type.
|
||||
* Called during Phase 2 after all types are loaded into the registry.
|
||||
*/
|
||||
public void resolveTypeParameters() {
|
||||
for (TypeParamInfo param : typeParams) {
|
||||
param.resolveBoundType();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve all member types (return types, field types, parameter types).
|
||||
* Called during Phase 2 after all types are loaded into the registry.
|
||||
* This resolves types like "Java.java.io.File" to proper TypeInfo objects.
|
||||
*/
|
||||
public void resolveMemberTypes() {
|
||||
TypeResolver resolver = TypeResolver.getInstance();
|
||||
|
||||
// Resolve method return types and parameter types
|
||||
for (JSMethodInfo method : methods.values()) {
|
||||
TypeInfo returnTypeInfo = resolver.resolveJSType(method.getReturnType());
|
||||
method.setReturnTypeInfo(returnTypeInfo);
|
||||
|
||||
// Resolve parameter types
|
||||
for (JSMethodInfo.JSParameterInfo param : method.getParameters()) {
|
||||
TypeInfo paramTypeInfo = resolver.resolveJSType(param.getType());
|
||||
param.setTypeInfo(paramTypeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve field types
|
||||
for (JSFieldInfo field : fields.values()) {
|
||||
TypeInfo fieldTypeInfo = resolver.resolveJSType(field.getType());
|
||||
field.setTypeInfo(fieldTypeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public void resolveJSDocTypes() {
|
||||
TypeResolver resolver = TypeResolver.getInstance();
|
||||
|
||||
if (jsDocInfo != null) {
|
||||
resolveJSDocInfoTypes(jsDocInfo, resolver);
|
||||
}
|
||||
|
||||
for (JSMethodInfo method : methods.values()) {
|
||||
JSDocInfo methodDoc = method.getJsDocInfo();
|
||||
if (methodDoc != null) {
|
||||
resolveJSDocInfoTypes(methodDoc, resolver);
|
||||
}
|
||||
}
|
||||
|
||||
for (JSFieldInfo field : fields.values()) {
|
||||
JSDocInfo fieldDoc = field.getJsDocInfo();
|
||||
if (fieldDoc != null) {
|
||||
resolveJSDocInfoTypes(fieldDoc, resolver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void resolveJSDocInfoTypes(JSDocInfo jsDoc, TypeResolver resolver) {
|
||||
JSDocTypeTag typeTag = jsDoc.getTypeTag();
|
||||
if (typeTag != null && typeTag.hasType() && typeTag.getTypeInfo() == null) {
|
||||
TypeInfo resolved = resolver.resolveJSType(typeTag.getTypeName());
|
||||
typeTag.setType(typeTag.getTypeName(), resolved, typeTag.getTypeStart(), typeTag.getTypeEnd());
|
||||
}
|
||||
|
||||
for (JSDocParamTag paramTag : jsDoc.getParamTags()) {
|
||||
if (paramTag.hasType() && paramTag.getTypeInfo() == null) {
|
||||
TypeInfo resolved = resolver.resolveJSType(paramTag.getTypeName());
|
||||
paramTag.setType(paramTag.getTypeName(), resolved, paramTag.getTypeStart(), paramTag.getTypeEnd());
|
||||
}
|
||||
}
|
||||
|
||||
JSDocReturnTag returnTag = jsDoc.getReturnTag();
|
||||
if (returnTag != null && returnTag.hasType() && returnTag.getTypeInfo() == null) {
|
||||
TypeInfo resolved = resolver.resolveJSType(returnTag.getTypeName());
|
||||
returnTag.setType(returnTag.getTypeName(), resolved, returnTag.getTypeStart(), returnTag.getTypeEnd());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a type parameter to its bound TypeInfo.
|
||||
* For example, if this type has "T extends EntityPlayerMP", resolveTypeParam("T") returns the TypeInfo for EntityPlayerMP.
|
||||
* If no type parameter is found with that name, returns null.
|
||||
*/
|
||||
public TypeInfo resolveTypeParamToTypeInfo(String typeName) {
|
||||
TypeParamInfo param = getTypeParam(typeName);
|
||||
if (param != null) {
|
||||
return param.getBoundTypeInfo();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a type parameter to its bound type name (for backward compatibility).
|
||||
* @deprecated Use resolveTypeParamToTypeInfo instead
|
||||
*/
|
||||
@Deprecated
|
||||
public String resolveTypeParam(String typeName) {
|
||||
TypeParamInfo param = getTypeParam(typeName);
|
||||
if (param != null && param.getBoundTypeInfo() != null) {
|
||||
return param.getBoundTypeInfo().getSimpleName();
|
||||
}
|
||||
return typeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a method by name, including inherited methods.
|
||||
*/
|
||||
public JSMethodInfo getMethod(String name) {
|
||||
JSMethodInfo method = methods.get(name);
|
||||
if (method != null) return method;
|
||||
|
||||
// Check parent
|
||||
if (resolvedParent != null) {
|
||||
return resolvedParent.getMethod(name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all methods with a given name (for overloads).
|
||||
*/
|
||||
public List<JSMethodInfo> getMethodOverloads(String name) {
|
||||
List<JSMethodInfo> overloads = new ArrayList<>();
|
||||
|
||||
// Get from this type
|
||||
if (methods.containsKey(name)) {
|
||||
overloads.add(methods.get(name));
|
||||
}
|
||||
// Get numbered overloads
|
||||
int index = 1;
|
||||
while (methods.containsKey(name + "$" + index)) {
|
||||
overloads.add(methods.get(name + "$" + index));
|
||||
index++;
|
||||
}
|
||||
|
||||
// Get from parent
|
||||
if (resolvedParent != null) {
|
||||
overloads.addAll(resolvedParent.getMethodOverloads(name));
|
||||
}
|
||||
|
||||
return overloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this type has a method (including inherited).
|
||||
*/
|
||||
public boolean hasMethod(String name) {
|
||||
return getMethod(name) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a field by name, including inherited fields.
|
||||
*/
|
||||
public JSFieldInfo getField(String name) {
|
||||
JSFieldInfo field = fields.get(name);
|
||||
if (field != null) return field;
|
||||
|
||||
// Check parent
|
||||
if (resolvedParent != null) {
|
||||
return resolvedParent.getField(name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this type has a field (including inherited).
|
||||
*/
|
||||
public boolean hasField(String name) {
|
||||
return getField(name) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an inner type by name.
|
||||
*/
|
||||
public JSTypeInfo getInnerType(String name) {
|
||||
return innerTypes.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "JSTypeInfo{" + fullName + "}";
|
||||
}
|
||||
}
|
||||
+1199
File diff suppressed because it is too large.
Load diff
+68
@@ -0,0 +1,68 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.js_parser;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver;
|
||||
|
||||
/**
|
||||
* Represents a generic type parameter like "T extends Entity".
|
||||
* Stores the parameter name and both the string representation (for deferred resolution)
|
||||
* and the resolved TypeInfo for the bound.
|
||||
*/
|
||||
public class TypeParamInfo {
|
||||
private final String name; // e.g., "T"
|
||||
private final String boundType; // Simple name like "EntityPlayerMP" (may be null)
|
||||
private final String fullBoundType; // Full name like "net.minecraft.entity.player.EntityPlayerMP" (may be null)
|
||||
private TypeInfo boundTypeInfo; // Resolved TypeInfo for the bound (resolved in Phase 2)
|
||||
|
||||
/**
|
||||
* Constructor for parse-time (Phase 1) - stores string names only.
|
||||
*/
|
||||
public TypeParamInfo(String name, String boundType, String fullBoundType) {
|
||||
this.name = name;
|
||||
this.boundType = boundType;
|
||||
this.fullBoundType = fullBoundType;
|
||||
this.boundTypeInfo = null; // Will be resolved later
|
||||
}
|
||||
|
||||
public String getName() { return name; }
|
||||
|
||||
public TypeInfo getBoundTypeInfo() {
|
||||
return boundTypeInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the bound type using TypeResolver.
|
||||
* Called during Phase 2 after all types are loaded.
|
||||
*/
|
||||
public void resolveBoundType() {
|
||||
if (boundTypeInfo != null)
|
||||
return; // Already resolved
|
||||
|
||||
if (fullBoundType != null && !fullBoundType.isEmpty()) {
|
||||
// Try to load the Java class using the full name
|
||||
boundTypeInfo = TypeResolver.getInstance().resolveFullName(fullBoundType);
|
||||
} else if (boundType != null && !boundType.isEmpty()) {
|
||||
// Try to resolve using the simple name
|
||||
boundTypeInfo = TypeResolver.getInstance().resolveJSType(boundType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name for the bound type, including generic arguments.
|
||||
*/
|
||||
public String getBoundTypeName() {
|
||||
if (boundTypeInfo == null)
|
||||
return null;
|
||||
return boundTypeInfo.getDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (boundTypeInfo != null) {
|
||||
return name + " extends " + boundTypeInfo.getDisplayName();
|
||||
} else if (boundType != null || fullBoundType != null) {
|
||||
return name + " extends " + (boundType != null ? boundType : fullBoundType);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
}
|
||||
+728
@@ -0,0 +1,728 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.js_parser;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocTag;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeStringNormalizer;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.regex.*;
|
||||
import java.util.zip.*;
|
||||
|
||||
/**
|
||||
* Parses TypeScript definition files (.d.ts) to extract type information.
|
||||
* Can read from individual files, directories, or .vsix archives.
|
||||
*/
|
||||
public class TypeScriptDefinitionParser {
|
||||
|
||||
// Patterns for parsing .d.ts content
|
||||
// Updated to capture generic type parameters like: export interface IEntity<T extends Entity /* net.minecraft.entity.Entity */>
|
||||
private static final Pattern INTERFACE_PATTERN = Pattern.compile(
|
||||
"export\\s+interface\\s+(\\w+)(?:<([^>]*)>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{");
|
||||
|
||||
// Pattern for nested interfaces without export keyword
|
||||
private static final Pattern NESTED_INTERFACE_PATTERN = Pattern.compile(
|
||||
"(?<!export\\s)\\binterface\\s+(\\w+)(?:<([^>]*)>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{");
|
||||
|
||||
// Similar pattern for classes
|
||||
private static final Pattern CLASS_PATTERN = Pattern.compile(
|
||||
"export\\s+class\\s+(\\w+)(?:<([^>]*)>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{");
|
||||
|
||||
// Pattern to parse individual type parameters like: T extends EntityPlayerMP /* net.minecraft.entity.player.EntityPlayerMP */
|
||||
private static final Pattern TYPE_PARAM_PATTERN = Pattern.compile(
|
||||
"(\\w+)(?:\\s+extends\\s+(\\w+)(?:\\s*/\\*\\s*([\\w.]+)\\s*\\*/)?)?");
|
||||
|
||||
// Match both "export namespace" and plain "namespace" (for declare global blocks)
|
||||
private static final Pattern NAMESPACE_PATTERN = Pattern.compile(
|
||||
"(?:export\\s+)?namespace\\s+(\\w+)\\s*\\{");
|
||||
|
||||
// Make semicolon optional for type aliases (TypeScript doesn't require them)
|
||||
private static final Pattern TYPE_ALIAS_PATTERN = Pattern.compile(
|
||||
"export\\s+type\\s+(\\w+)\\s*=\\s*([^;\\n]+);?");
|
||||
|
||||
private static final Pattern METHOD_PATTERN = Pattern.compile(
|
||||
"(?m)^\\s*(static\\s+)?(\\w+)\\??\\s*\\((.*)\\)\\s*:\\s*([^;]+);", Pattern.MULTILINE);
|
||||
|
||||
private static final Pattern FIELD_PATTERN = Pattern.compile(
|
||||
"^\\s*(static\\s+)?(readonly\\s+)?(\\w+)\\??\\s*:\\s*([^;]+);", Pattern.MULTILINE);
|
||||
|
||||
private static final Pattern GLOBAL_FUNCTION_PATTERN = Pattern.compile(
|
||||
"function\\s+(\\w+)\\s*\\((.*)\\)\\s*:\\s*([^;]+);");
|
||||
|
||||
private static final Pattern GLOBAL_TYPE_ALIAS_PATTERN = Pattern.compile(
|
||||
"type\\s+(\\w+)\\s*=\\s*import\\(['\"]([^'\"]+)['\"]\\)\\.([\\w.]+);");
|
||||
|
||||
// Pattern for context-namespaced hooks like: declare namespace INpcEvent { ... }
|
||||
// Matches any "declare namespace <Name> {" where Name starts with I and contains Event,
|
||||
// or any other namespace pattern used for hooks
|
||||
private static final Pattern HOOKS_NAMESPACE_PATTERN = Pattern.compile(
|
||||
"declare\\s+namespace\\s+(I\\w*Event|\\w+)\\s*\\{", Pattern.MULTILINE);
|
||||
|
||||
private final JSTypeRegistry registry;
|
||||
|
||||
public TypeScriptDefinitionParser(JSTypeRegistry registry) {
|
||||
this.registry = registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse all .d.ts files from a VSIX archive.
|
||||
*/
|
||||
public void parseVsixArchive(File vsixFile) throws IOException {
|
||||
try (ZipFile zip = new ZipFile(vsixFile)) {
|
||||
Enumeration<? extends ZipEntry> entries = zip.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
if (entry.getName().endsWith(".d.ts")) {
|
||||
try (InputStream is = zip.getInputStream(entry);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
|
||||
String content = readFully(reader);
|
||||
String fileName = entry.getName();
|
||||
parseDefinitionFile(content, fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse all .d.ts files from a directory recursively.
|
||||
*/
|
||||
public void parseDirectory(File directory) throws IOException {
|
||||
if (!directory.isDirectory()) {
|
||||
throw new IllegalArgumentException("Not a directory: " + directory);
|
||||
}
|
||||
parseDirectoryRecursive(directory);
|
||||
}
|
||||
|
||||
private void parseDirectoryRecursive(File dir) throws IOException {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) return;
|
||||
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
parseDirectoryRecursive(file);
|
||||
} else if (file.getName().endsWith(".d.ts")) {
|
||||
parseFile(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single .d.ts file.
|
||||
*/
|
||||
public void parseFile(File file) throws IOException {
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
|
||||
String content = readFully(reader);
|
||||
parseDefinitionFile(content, file.getName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a .d.ts file content.
|
||||
*/
|
||||
public void parseDefinitionFile(String content, String fileName) {
|
||||
// Special handling for hooks.d.ts - extract function signatures
|
||||
if (fileName.contains("hooks.d.ts")) {
|
||||
// Commented out as hooks are now parsed through ScriptHookController which is the universal source of truth for registered hooks.
|
||||
// parseHooksFile(content);
|
||||
return;
|
||||
}
|
||||
|
||||
// Special handling for index.d.ts - extract global type aliases
|
||||
if (fileName.contains("index.d.ts")) {
|
||||
parseIndexFile(content);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse regular interface files
|
||||
String packageName = derivePackageName(fileName);
|
||||
parseInterfaceFile(content, null, packageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse hooks.d.ts to extract function signatures for JS hooks.
|
||||
*
|
||||
* Hooks are organized by their parent event interface, with the interface name
|
||||
* used directly as the namespace. Any mod can register its own event interfaces
|
||||
* and they will be automatically parsed:
|
||||
*
|
||||
* declare namespace INpcEvent {
|
||||
* function interact(event: INpcEvent.InteractEvent): void;
|
||||
* function init(event: INpcEvent.InitEvent): void;
|
||||
* }
|
||||
*
|
||||
* declare namespace IPlayerEvent {
|
||||
* function interact(event: IPlayerEvent.InteractEvent): void;
|
||||
* }
|
||||
*
|
||||
* declare namespace IDBCEvent {
|
||||
* function customHook(event: IDBCEvent.CustomEvent): void;
|
||||
* }
|
||||
*
|
||||
* The namespace name is stored as a string, allowing dynamic registration
|
||||
* without requiring enum modifications.
|
||||
*/
|
||||
private void parseHooksFile(String content) {
|
||||
// Parse namespaced hooks (e.g., declare namespace INpcEvent { ... })
|
||||
// The namespace name is used directly - any event interface can register hooks
|
||||
Matcher namespaceMatcher = HOOKS_NAMESPACE_PATTERN.matcher(content);
|
||||
while (namespaceMatcher.find()) {
|
||||
String namespace = namespaceMatcher.group(1); // e.g., "INpcEvent", "IPlayerEvent", "IDBCEvent"
|
||||
|
||||
// Find the body of this namespace
|
||||
int bodyStart = namespaceMatcher.end();
|
||||
int bodyEnd = findMatchingBrace(content, bodyStart - 1);
|
||||
if (bodyEnd > bodyStart) {
|
||||
String namespaceBody = content.substring(bodyStart, bodyEnd);
|
||||
|
||||
// Parse functions within this namespace
|
||||
Matcher funcMatcher = GLOBAL_FUNCTION_PATTERN.matcher(namespaceBody);
|
||||
while (funcMatcher.find()) {
|
||||
String funcName = funcMatcher.group(1);
|
||||
String params = funcMatcher.group(2);
|
||||
|
||||
// Parse parameter - format is "paramName: TypeName"
|
||||
if (!params.isEmpty()) {
|
||||
String[] parts = params.split(":\\s*", 2);
|
||||
if (parts.length == 2) {
|
||||
String paramName = parts[0].trim();
|
||||
String paramType = parts[1].trim();
|
||||
// Register hook with the namespace string directly
|
||||
registry.registerHook(namespace, funcName, paramName, paramType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse index.d.ts to extract global type aliases.
|
||||
*/
|
||||
private void parseIndexFile(String content) {
|
||||
Matcher m = GLOBAL_TYPE_ALIAS_PATTERN.matcher(content);
|
||||
while (m.find()) {
|
||||
String aliasName = m.group(1);
|
||||
String importPath = m.group(2);
|
||||
String typeName = m.group(3);
|
||||
registry.registerTypeAlias(aliasName, typeName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse interface and class definitions from content.
|
||||
*/
|
||||
private void parseInterfaceFile(String content, String parentNamespace, String packageName) {
|
||||
// IMPORTANT:
|
||||
// Many generated .d.ts files declare nested exported interfaces inside `namespace X { ... }` blocks.
|
||||
// If we scan `content` directly for `export interface` / `export class`, we'll accidentally treat
|
||||
// those nested exports as top-level for the current `parentNamespace`.
|
||||
// That pollutes the registry with incorrectly-scoped type names (e.g. "DamagedEvent" instead of
|
||||
// "INpcEvent.DamagedEvent"), and later registry merges (by @javaFqn) can prevent the correctly
|
||||
// namespaced key from ever being registered.
|
||||
//
|
||||
// To avoid this, we strip namespace blocks out of the scan input and then parse namespaces
|
||||
// explicitly via the NAMESPACE_PATTERN recursion further below.
|
||||
String scanContent = stripNamespaceBlocks(content);
|
||||
|
||||
// Find exported interfaces
|
||||
Matcher interfaceMatcher = INTERFACE_PATTERN.matcher(scanContent);
|
||||
while (interfaceMatcher.find()) {
|
||||
String interfaceName = interfaceMatcher.group(1);
|
||||
String typeParamsStr = interfaceMatcher.group(2); // e.g., "T extends EntityPlayerMP /* net.minecraft.entity.player.EntityPlayerMP */"
|
||||
String extendsClause = interfaceMatcher.group(3);
|
||||
|
||||
JSTypeInfo typeInfo = new JSTypeInfo(interfaceName, parentNamespace);
|
||||
|
||||
JSDocInfo jsDoc = extractJSDocBefore(content, interfaceMatcher.start());
|
||||
if (jsDoc != null) {
|
||||
typeInfo.setJsDocInfo(jsDoc);
|
||||
}
|
||||
|
||||
String javaFqn = findJavaFqn(jsDoc, packageName, typeInfo.getFullName());
|
||||
if (javaFqn != null && !javaFqn.isEmpty()) {
|
||||
typeInfo.setJavaFqn(javaFqn);
|
||||
}
|
||||
|
||||
// Parse type parameters
|
||||
if (typeParamsStr != null && !typeParamsStr.isEmpty()) {
|
||||
parseTypeParameters(typeParamsStr, typeInfo);
|
||||
}
|
||||
|
||||
if (extendsClause != null) {
|
||||
// Handle multiple extends (e.g., "IEntityLivingBase<T>, IAnimatable")
|
||||
// Take the first one, stripping generics
|
||||
String extendsType = extendsClause.trim();
|
||||
// Remove generic parameters like <T>
|
||||
extendsType = extendsType.replaceAll("<[^>]*>", "");
|
||||
// If multiple types (comma-separated), take the first one
|
||||
if (extendsType.contains(",")) {
|
||||
extendsType = extendsType.substring(0, extendsType.indexOf(',')).trim();
|
||||
}
|
||||
// Clean up import() syntax if present
|
||||
extendsType = cleanType(extendsType);
|
||||
typeInfo.setExtends(extendsType);
|
||||
}
|
||||
|
||||
// Find the body of this interface
|
||||
int bodyStart = interfaceMatcher.end();
|
||||
int bodyEnd = findMatchingBrace(content, bodyStart - 1);
|
||||
if (bodyEnd > bodyStart) {
|
||||
String body = content.substring(bodyStart, bodyEnd);
|
||||
parseInterfaceBody(body, typeInfo);
|
||||
|
||||
// Parse nested interfaces and type aliases within this interface body
|
||||
String fullNamespace = parentNamespace != null ?
|
||||
parentNamespace + "." + interfaceName : interfaceName;
|
||||
parseNestedTypes(body, fullNamespace, packageName);
|
||||
}
|
||||
|
||||
registry.registerType(typeInfo);
|
||||
}
|
||||
|
||||
// Find nested interfaces (without export keyword)
|
||||
Matcher nestedInterfaceMatcher = NESTED_INTERFACE_PATTERN.matcher(scanContent);
|
||||
while (nestedInterfaceMatcher.find()) {
|
||||
String interfaceName = nestedInterfaceMatcher.group(1);
|
||||
String typeParamsStr = nestedInterfaceMatcher.group(2);
|
||||
String extendsClause = nestedInterfaceMatcher.group(3);
|
||||
|
||||
JSTypeInfo typeInfo = new JSTypeInfo(interfaceName, parentNamespace);
|
||||
|
||||
JSDocInfo jsDoc = extractJSDocBefore(content, nestedInterfaceMatcher.start());
|
||||
if (jsDoc != null) {
|
||||
typeInfo.setJsDocInfo(jsDoc);
|
||||
}
|
||||
|
||||
// Parse type parameters
|
||||
if (typeParamsStr != null && !typeParamsStr.isEmpty()) {
|
||||
parseTypeParameters(typeParamsStr, typeInfo);
|
||||
}
|
||||
|
||||
if (extendsClause != null) {
|
||||
String extendsType = extendsClause.trim();
|
||||
extendsType = extendsType.replaceAll("<[^>]*>", "");
|
||||
if (extendsType.contains(",")) {
|
||||
extendsType = extendsType.substring(0, extendsType.indexOf(',')).trim();
|
||||
}
|
||||
extendsType = cleanType(extendsType);
|
||||
typeInfo.setExtends(extendsType);
|
||||
}
|
||||
|
||||
// Find the body of this nested interface
|
||||
int bodyStart = nestedInterfaceMatcher.end();
|
||||
int bodyEnd = findMatchingBrace(content, bodyStart - 1);
|
||||
if (bodyEnd > bodyStart) {
|
||||
String body = content.substring(bodyStart, bodyEnd);
|
||||
parseInterfaceBody(body, typeInfo);
|
||||
}
|
||||
|
||||
registry.registerType(typeInfo);
|
||||
}
|
||||
|
||||
// Find classes (same logic as interfaces)
|
||||
Matcher classMatcher = CLASS_PATTERN.matcher(scanContent);
|
||||
while (classMatcher.find()) {
|
||||
String className = classMatcher.group(1);
|
||||
String typeParamsStr = classMatcher.group(2);
|
||||
String extendsClause = classMatcher.group(3);
|
||||
|
||||
JSTypeInfo typeInfo = new JSTypeInfo(className, parentNamespace);
|
||||
|
||||
JSDocInfo jsDoc = extractJSDocBefore(content, classMatcher.start());
|
||||
if (jsDoc != null) {
|
||||
typeInfo.setJsDocInfo(jsDoc);
|
||||
}
|
||||
|
||||
// Parse type parameters
|
||||
if (typeParamsStr != null && !typeParamsStr.isEmpty()) {
|
||||
parseTypeParameters(typeParamsStr, typeInfo);
|
||||
}
|
||||
|
||||
if (extendsClause != null) {
|
||||
String extendsType = extendsClause.trim();
|
||||
extendsType = extendsType.replaceAll("<[^>]*>", "");
|
||||
if (extendsType.contains(",")) {
|
||||
extendsType = extendsType.substring(0, extendsType.indexOf(',')).trim();
|
||||
}
|
||||
extendsType = cleanType(extendsType);
|
||||
typeInfo.setExtends(extendsType);
|
||||
}
|
||||
|
||||
// Find the body of this class
|
||||
int bodyStart = classMatcher.end();
|
||||
int bodyEnd = findMatchingBrace(content, bodyStart - 1);
|
||||
if (bodyEnd > bodyStart) {
|
||||
String body = content.substring(bodyStart, bodyEnd);
|
||||
parseInterfaceBody(body, typeInfo);
|
||||
}
|
||||
|
||||
registry.registerType(typeInfo);
|
||||
}
|
||||
|
||||
// Find namespaces (which contain inner types)
|
||||
Matcher namespaceMatcher = NAMESPACE_PATTERN.matcher(content);
|
||||
while (namespaceMatcher.find()) {
|
||||
String namespaceName = namespaceMatcher.group(1);
|
||||
|
||||
// Find the body of this namespace
|
||||
int bodyStart = namespaceMatcher.end();
|
||||
int bodyEnd = findMatchingBrace(content, bodyStart - 1);
|
||||
if (bodyEnd > bodyStart) {
|
||||
String body = content.substring(bodyStart, bodyEnd);
|
||||
|
||||
// Parse inner types with namespace prefix
|
||||
// Namespaces contain exported types, so use parseInterfaceFile
|
||||
String fullNamespace = parentNamespace != null ?
|
||||
parentNamespace + "." + namespaceName : namespaceName;
|
||||
parseInterfaceFile(body, fullNamespace, packageName);
|
||||
// Don't call parseNestedTypes here - namespace members are all exported
|
||||
// and will be caught by parseInterfaceFile's INTERFACE_PATTERN
|
||||
}
|
||||
}
|
||||
|
||||
// Handle top-level type aliases
|
||||
if (parentNamespace == null) {
|
||||
parseTypeAliases(content, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip namespace blocks from a file-level scan to avoid incorrectly capturing nested exports.
|
||||
*
|
||||
* This replaces the entire `namespace X { ... }` span (including braces) with whitespace so that
|
||||
* match indices remain stable when we later slice `content` for bodies.
|
||||
*/
|
||||
private String stripNamespaceBlocks(String content) {
|
||||
if (content == null || content.isEmpty()) {
|
||||
return content;
|
||||
}
|
||||
|
||||
Matcher m = NAMESPACE_PATTERN.matcher(content);
|
||||
if (!m.find()) {
|
||||
return content;
|
||||
}
|
||||
|
||||
char[] chars = content.toCharArray();
|
||||
|
||||
int searchFrom = 0;
|
||||
m.reset();
|
||||
while (m.find(searchFrom)) {
|
||||
int start = m.start();
|
||||
int bodyStart = m.end();
|
||||
int bodyEnd = findMatchingBrace(content, bodyStart - 1);
|
||||
if (bodyEnd <= start) {
|
||||
searchFrom = Math.max(m.end(), searchFrom + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
int endExclusive = Math.min(chars.length, bodyEnd + 1);
|
||||
for (int i = start; i < endExclusive; i++) {
|
||||
chars[i] = ' ';
|
||||
}
|
||||
|
||||
searchFrom = endExclusive;
|
||||
}
|
||||
|
||||
return new String(chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse type parameters from a string like "T extends EntityPlayerMP /* net.minecraft.entity.player.EntityPlayerMP *`/".
|
||||
* Handles multiple parameters separated by commas.
|
||||
* Stores strings only - resolution happens in Phase 2 after all types are loaded.
|
||||
*/
|
||||
private void parseTypeParameters(String typeParamsStr, JSTypeInfo typeInfo) {
|
||||
// Split by comma, but be careful with nested generics (shouldn't happen at this level, but be safe)
|
||||
String[] params = typeParamsStr.split(",");
|
||||
for (String param : params) {
|
||||
param = param.trim();
|
||||
if (param.isEmpty()) continue;
|
||||
|
||||
Matcher m = TYPE_PARAM_PATTERN.matcher(param);
|
||||
if (m.find()) {
|
||||
String name = m.group(1);
|
||||
String boundType = m.group(2); // Simple name like "EntityPlayerMP"
|
||||
String fullBoundType = m.group(3); // Full name like "net.minecraft.entity.player.EntityPlayerMP"
|
||||
|
||||
// Store strings only - resolution happens in Phase 2
|
||||
typeInfo.addTypeParam(new TypeParamInfo(name, boundType, fullBoundType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse nested types (interfaces and type aliases) within a parent type or namespace.
|
||||
*/
|
||||
private void parseNestedTypes(String content, String namespace, String packageName) {
|
||||
// Parse nested interfaces
|
||||
Matcher nestedInterfaceMatcher = NESTED_INTERFACE_PATTERN.matcher(content);
|
||||
while (nestedInterfaceMatcher.find()) {
|
||||
String interfaceName = nestedInterfaceMatcher.group(1);
|
||||
String typeParamsStr = nestedInterfaceMatcher.group(2);
|
||||
String extendsClause = nestedInterfaceMatcher.group(3);
|
||||
|
||||
JSTypeInfo typeInfo = new JSTypeInfo(interfaceName, namespace);
|
||||
|
||||
JSDocInfo jsDoc = extractJSDocBefore(content, nestedInterfaceMatcher.start());
|
||||
if (jsDoc != null) {
|
||||
typeInfo.setJsDocInfo(jsDoc);
|
||||
}
|
||||
|
||||
String javaFqn = findJavaFqn(jsDoc, packageName, typeInfo.getFullName());
|
||||
if (javaFqn != null && !javaFqn.isEmpty()) {
|
||||
typeInfo.setJavaFqn(javaFqn);
|
||||
}
|
||||
|
||||
// Parse type parameters
|
||||
if (typeParamsStr != null && !typeParamsStr.isEmpty()) {
|
||||
parseTypeParameters(typeParamsStr, typeInfo);
|
||||
}
|
||||
|
||||
if (extendsClause != null) {
|
||||
String extendsType = extendsClause.trim();
|
||||
extendsType = extendsType.replaceAll("<[^>]*>", "");
|
||||
if (extendsType.contains(",")) {
|
||||
extendsType = extendsType.substring(0, extendsType.indexOf(',')).trim();
|
||||
}
|
||||
extendsType = cleanType(extendsType);
|
||||
typeInfo.setExtends(extendsType);
|
||||
}
|
||||
|
||||
// Find the body of this nested interface
|
||||
int bodyStart = nestedInterfaceMatcher.end();
|
||||
int bodyEnd = findMatchingBrace(content, bodyStart - 1);
|
||||
if (bodyEnd > bodyStart) {
|
||||
String body = content.substring(bodyStart, bodyEnd);
|
||||
parseInterfaceBody(body, typeInfo);
|
||||
}
|
||||
|
||||
registry.registerType(typeInfo);
|
||||
}
|
||||
|
||||
// Parse type aliases within this context
|
||||
parseTypeAliases(content, namespace);
|
||||
}
|
||||
|
||||
private String findJavaFqn(JSDocInfo jsDoc, String packageName, String typeFullName) {
|
||||
String tagged = extractJavaFqnFromJSDoc(jsDoc);
|
||||
if (tagged != null && !tagged.isEmpty()) {
|
||||
return tagged;
|
||||
}
|
||||
return buildJavaFqnFromPackage(packageName, typeFullName);
|
||||
}
|
||||
|
||||
private String extractJavaFqnFromJSDoc(JSDocInfo jsDoc) {
|
||||
if (jsDoc == null) return null;
|
||||
for (JSDocTag tag : jsDoc.getAllTags()) {
|
||||
if (tag == null) continue;
|
||||
if ("javaFqn".equals(tag.getTagName())) {
|
||||
if (tag.getDescription() != null && !tag.getDescription().trim().isEmpty()) {
|
||||
return tag.getDescription().trim();
|
||||
}
|
||||
if (tag.getTypeName() != null && !tag.getTypeName().trim().isEmpty()) {
|
||||
return tag.getTypeName().trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String buildJavaFqnFromPackage(String packageName, String typeFullName) {
|
||||
if (typeFullName == null || typeFullName.isEmpty()) return null;
|
||||
if (packageName == null || packageName.isEmpty()) return null;
|
||||
return packageName + "." + typeFullName;
|
||||
}
|
||||
|
||||
private String derivePackageName(String fileName) {
|
||||
if (fileName == null || fileName.isEmpty()) return null;
|
||||
String normalized = fileName.replace('\\', '/');
|
||||
if (normalized.endsWith(".d.ts")) {
|
||||
normalized = normalized.substring(0, normalized.length() - 5);
|
||||
}
|
||||
int lastSlash = normalized.lastIndexOf('/');
|
||||
if (lastSlash < 0) return null;
|
||||
String pkgPath = normalized.substring(0, lastSlash);
|
||||
if (pkgPath.isEmpty()) return null;
|
||||
return pkgPath.replace('/', '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse type aliases (export type X = Y).
|
||||
*/
|
||||
private void parseTypeAliases(String content, String namespace) {
|
||||
Matcher m = TYPE_ALIAS_PATTERN.matcher(content);
|
||||
while (m.find()) {
|
||||
String aliasName = m.group(1);
|
||||
String targetType = m.group(2).trim();
|
||||
|
||||
// Create a simple type that extends the target
|
||||
JSTypeInfo typeInfo = new JSTypeInfo(aliasName, namespace);
|
||||
typeInfo.setExtends(targetType);
|
||||
registry.registerType(typeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the body of an interface to extract methods and fields.
|
||||
*/
|
||||
private void parseInterfaceBody(String body, JSTypeInfo typeInfo) {
|
||||
// Parse methods
|
||||
Matcher methodMatcher = METHOD_PATTERN.matcher(body);
|
||||
while (methodMatcher.find()) {
|
||||
boolean isStatic = methodMatcher.group(1) != null;
|
||||
String methodName = methodMatcher.group(2);
|
||||
String params = methodMatcher.group(3);
|
||||
String returnType = cleanType(methodMatcher.group(4));
|
||||
|
||||
List<JSMethodInfo.JSParameterInfo> parameters = parseParameters(params);
|
||||
JSMethodInfo method = new JSMethodInfo(methodName, returnType, parameters);
|
||||
method.setStatic(isStatic);
|
||||
|
||||
JSDocInfo jsDoc = extractJSDocBefore(body, methodMatcher.start());
|
||||
if (jsDoc != null) {
|
||||
method.setJsDocInfo(jsDoc);
|
||||
}
|
||||
|
||||
typeInfo.addMethod(method);
|
||||
}
|
||||
|
||||
// Parse fields (that aren't method signatures)
|
||||
Matcher fieldMatcher = FIELD_PATTERN.matcher(body);
|
||||
while (fieldMatcher.find()) {
|
||||
boolean isStatic = fieldMatcher.group(1) != null;
|
||||
String fieldName = fieldMatcher.group(3);
|
||||
|
||||
// CNPC+ base event implementation CustomNPCsEvent exposes a public "API" field that
|
||||
// ends up polluting autocomplete on every event via ICustomNPCsEvent inheritance.
|
||||
// Keep parsing other fields, but skip this one.
|
||||
if ("ICustomNPCsEvent".equals(typeInfo.getSimpleName()) && "API".equals(fieldName))
|
||||
continue;
|
||||
|
||||
String fieldType = cleanType(fieldMatcher.group(4));
|
||||
boolean readonly = fieldMatcher.group(2) != null;
|
||||
|
||||
JSFieldInfo field = new JSFieldInfo(fieldName, fieldType, readonly);
|
||||
field.setStatic(isStatic);
|
||||
|
||||
JSDocInfo jsDoc = extractJSDocBefore(body, fieldMatcher.start());
|
||||
if (jsDoc != null) {
|
||||
field.setJsDocInfo(jsDoc);
|
||||
}
|
||||
|
||||
typeInfo.addField(field);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse parameters string into list of parameter info.
|
||||
*/
|
||||
private List<JSMethodInfo.JSParameterInfo> parseParameters(String params) {
|
||||
List<JSMethodInfo.JSParameterInfo> result = new ArrayList<>();
|
||||
if (params == null || params.trim().isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Split by comma, but be careful of nested types like Map<K, V>
|
||||
List<String> paramParts = splitParameters(params);
|
||||
|
||||
for (String part : paramParts) {
|
||||
part = part.trim();
|
||||
if (part.isEmpty()) continue;
|
||||
|
||||
// Format: name: type or name?: type or ...name: type
|
||||
int colonIndex = part.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
String name = part.substring(0, colonIndex).trim().replace("?", "");
|
||||
String type = cleanType(part.substring(colonIndex + 1).trim());
|
||||
boolean isVarArg = name.startsWith("...");
|
||||
if (isVarArg) {
|
||||
name = name.substring(3).trim();
|
||||
// Varargs in TS are typed as arrays (e.g. ...codes: number[])
|
||||
// Strip the array wrapper to get the element type since isVarArg already implies array
|
||||
if (type.endsWith("[]")) {
|
||||
type = type.substring(0, type.length() - 2).trim();
|
||||
} else if (type.startsWith("Array<") && type.endsWith(">")) {
|
||||
type = type.substring(6, type.length() - 1).trim();
|
||||
}
|
||||
}
|
||||
JSMethodInfo.JSParameterInfo param = new JSMethodInfo.JSParameterInfo(name, type);
|
||||
param.setVarArg(isVarArg);
|
||||
result.add(param);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split parameters respecting nested angle brackets.
|
||||
*/
|
||||
private List<String> splitParameters(String params) {
|
||||
List<String> result = new ArrayList<>();
|
||||
int depth = 0;
|
||||
int start = 0;
|
||||
|
||||
for (int i = 0; i < params.length(); i++) {
|
||||
char c = params.charAt(i);
|
||||
if (c == '<' || c == '(' || c == '[') {
|
||||
depth++;
|
||||
} else if (c == '>' || c == ')' || c == ']') {
|
||||
depth--;
|
||||
} else if (c == ',' && depth == 0) {
|
||||
result.add(params.substring(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (start < params.length()) {
|
||||
result.add(params.substring(start));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up a type string (remove import() syntax, trim, etc.).
|
||||
*/
|
||||
private String cleanType(String type) {
|
||||
if (type == null) return "any";
|
||||
return TypeStringNormalizer.stripImportTypeSyntax(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the matching closing brace.
|
||||
*/
|
||||
private int findMatchingBrace(String text, int openBracePos) {
|
||||
int depth = 1;
|
||||
for (int i = openBracePos + 1; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
if (c == '{') depth++;
|
||||
else if (c == '}') {
|
||||
depth--;
|
||||
if (depth == 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read entire content from reader.
|
||||
*/
|
||||
private String readFully(BufferedReader reader) throws IOException {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private JSDocInfo extractJSDocBefore(String content, int elementStart) {
|
||||
String jsDocBlock = DTSJSDocParser.extractJSDocBefore(content, elementStart);
|
||||
if (jsDocBlock != null) {
|
||||
return DTSJSDocParser.parseJSDocBlock(jsDocBlock);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
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<JSDocParamTag> paramTags = new ArrayList<>();
|
||||
private JSDocReturnTag returnTag;
|
||||
private final List<JSDocSeeTag> seeTags = new ArrayList<>();
|
||||
private String description;
|
||||
private final List<JSDocTag> 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 addSeeTag(JSDocSeeTag seeTag) {
|
||||
this.seeTags.add(seeTag);
|
||||
allTags.add(seeTag);
|
||||
}
|
||||
|
||||
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<JSDocParamTag> getParamTags() {
|
||||
return Collections.unmodifiableList(paramTags);
|
||||
}
|
||||
|
||||
public boolean hasParamTags() { return !paramTags.isEmpty(); }
|
||||
|
||||
public JSDocParamTag getParamTag(String paramName) {
|
||||
for (JSDocParamTag tag : paramTags) {
|
||||
String tagName = tag.getParamName();
|
||||
if (tagName != null && tagName.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 List<JSDocSeeTag> getSeeTags() { return Collections.unmodifiableList(seeTags); }
|
||||
public boolean hasSeeTags() { return !seeTags.isEmpty(); }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public List<JSDocTag> 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());
|
||||
if (!seeTags.isEmpty()) sb.append(", see=").append(seeTags.size());
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -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 + "}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
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*(?:\\{([^}]+)\\})?(?:\\s*-?\\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 GENERIC_TAG_PATTERN = Pattern.compile(
|
||||
"^@(\\w+)(?:\\s*\\{([^}]+)\\})?(?:\\s*-?\\s*(.*))?$", 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;
|
||||
|
||||
StringBuilder descriptionBuilder = new StringBuilder();
|
||||
boolean foundFirstTag = false;
|
||||
JSDocTag lastTag = null;
|
||||
|
||||
int position = 0;
|
||||
while (position <= content.length()) {
|
||||
int lineEnd = content.indexOf('\n', position);
|
||||
if (lineEnd == -1) {
|
||||
lineEnd = content.length();
|
||||
}
|
||||
|
||||
String rawLine = content.substring(position, lineEnd);
|
||||
if (rawLine.endsWith("\r")) {
|
||||
rawLine = rawLine.substring(0, rawLine.length() - 1);
|
||||
}
|
||||
|
||||
CleanLine cleanLine = cleanLine(rawLine, contentOffset + position);
|
||||
String line = cleanLine.text;
|
||||
|
||||
if (line.startsWith("@")) {
|
||||
foundFirstTag = true;
|
||||
lastTag = parseTagLine(line, cleanLine.offset, info);
|
||||
} else if (!foundFirstTag) {
|
||||
if (line.isEmpty()) {
|
||||
// Preserve blank lines as paragraph gaps (only if description has started)
|
||||
if (descriptionBuilder.length() > 0) {
|
||||
descriptionBuilder.append("\n");
|
||||
}
|
||||
} else {
|
||||
if (descriptionBuilder.length() > 0) {
|
||||
descriptionBuilder.append("\n");
|
||||
}
|
||||
descriptionBuilder.append(line);
|
||||
}
|
||||
} else if (lastTag != null && !line.isEmpty()) {
|
||||
String existing = lastTag.getDescription();
|
||||
if (existing == null || existing.isEmpty()) {
|
||||
lastTag.setDescription(line);
|
||||
} else {
|
||||
lastTag.setDescription(existing + "\n" + line);
|
||||
}
|
||||
}
|
||||
|
||||
if (lineEnd == content.length()) {
|
||||
break;
|
||||
}
|
||||
position = lineEnd + 1;
|
||||
}
|
||||
|
||||
if (descriptionBuilder.length() > 0) {
|
||||
info.setDescription(descriptionBuilder.toString().trim());
|
||||
}
|
||||
|
||||
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 JSDocTag parseTagLine(String line, int lineOffset, JSDocInfo info) {
|
||||
Matcher paramMatcher = PARAM_TAG_PATTERN.matcher(line);
|
||||
if (paramMatcher.matches()) {
|
||||
String typeName = paramMatcher.group(1);
|
||||
String paramName = paramMatcher.group(2);
|
||||
String description = normalizeDescription(paramMatcher.group(3));
|
||||
|
||||
int atSignOffset = lineOffset + paramMatcher.start();
|
||||
int tagNameStart = lineOffset + 1;
|
||||
int tagNameEnd = tagNameStart + "param".length();
|
||||
|
||||
int typeStart = -1;
|
||||
int typeEnd = -1;
|
||||
TypeInfo typeInfo = null;
|
||||
|
||||
if (typeName != null) {
|
||||
typeName = typeName.trim();
|
||||
typeStart = lineOffset + paramMatcher.start(1);
|
||||
typeEnd = lineOffset + paramMatcher.end(1);
|
||||
typeInfo = resolveType(typeName);
|
||||
}
|
||||
|
||||
int paramNameStart = -1;
|
||||
int paramNameEnd = -1;
|
||||
if (paramName != null) {
|
||||
paramNameStart = lineOffset + paramMatcher.start(2);
|
||||
paramNameEnd = lineOffset + paramMatcher.end(2);
|
||||
}
|
||||
|
||||
JSDocParamTag tag = JSDocParamTag.create(atSignOffset, tagNameStart, tagNameEnd,
|
||||
typeName, typeInfo, typeStart, typeEnd,
|
||||
paramName, paramNameStart, paramNameEnd,
|
||||
description);
|
||||
info.addParamTag(tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
Matcher returnMatcher = RETURN_TAG_PATTERN.matcher(line);
|
||||
if (returnMatcher.matches()) {
|
||||
String tagName = returnMatcher.group(1);
|
||||
String typeName = returnMatcher.group(2);
|
||||
String description = normalizeDescription(returnMatcher.group(3));
|
||||
|
||||
int atSignOffset = lineOffset + returnMatcher.start();
|
||||
int tagNameStart = lineOffset + returnMatcher.start(1);
|
||||
int tagNameEnd = lineOffset + returnMatcher.end(1);
|
||||
|
||||
int typeStart = -1;
|
||||
int typeEnd = -1;
|
||||
TypeInfo typeInfo = null;
|
||||
|
||||
if (typeName != null) {
|
||||
typeName = typeName.trim();
|
||||
typeStart = lineOffset + returnMatcher.start(2);
|
||||
typeEnd = lineOffset + returnMatcher.end(2);
|
||||
typeInfo = resolveType(typeName);
|
||||
}
|
||||
|
||||
JSDocReturnTag tag = JSDocReturnTag.create(tagName, atSignOffset, tagNameStart, tagNameEnd,
|
||||
typeName, typeInfo, typeStart, typeEnd,
|
||||
description);
|
||||
info.setReturnTag(tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
Matcher typeMatcher = TYPE_TAG_PATTERN.matcher(line);
|
||||
if (typeMatcher.matches()) {
|
||||
String typeName = typeMatcher.group(2);
|
||||
String description = normalizeDescription(typeMatcher.group(3));
|
||||
|
||||
int atSignOffset = lineOffset + typeMatcher.start();
|
||||
int tagNameStart = lineOffset + typeMatcher.start(1);
|
||||
int tagNameEnd = lineOffset + typeMatcher.end(1);
|
||||
|
||||
int typeStart = -1;
|
||||
int typeEnd = -1;
|
||||
TypeInfo typeInfo = null;
|
||||
|
||||
if (typeName != null) {
|
||||
typeName = typeName.trim();
|
||||
typeStart = lineOffset + typeMatcher.start(2);
|
||||
typeEnd = lineOffset + typeMatcher.end(2);
|
||||
typeInfo = resolveType(typeName);
|
||||
}
|
||||
|
||||
JSDocTypeTag tag = JSDocTypeTag.create(atSignOffset, tagNameStart, tagNameEnd,
|
||||
typeName, typeInfo, typeStart, typeEnd, description);
|
||||
info.setTypeTag(tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
Matcher genericMatcher = GENERIC_TAG_PATTERN.matcher(line);
|
||||
if (genericMatcher.matches()) {
|
||||
String tagName = genericMatcher.group(1);
|
||||
String typeName = genericMatcher.group(2);
|
||||
String description = normalizeDescription(genericMatcher.group(3));
|
||||
|
||||
int atSignOffset = lineOffset + genericMatcher.start();
|
||||
int tagNameStart = lineOffset + genericMatcher.start(1);
|
||||
int tagNameEnd = lineOffset + genericMatcher.end(1);
|
||||
|
||||
JSDocTag tag = new JSDocTag(tagName, atSignOffset, tagNameStart, tagNameEnd);
|
||||
|
||||
if (typeName != null) {
|
||||
typeName = typeName.trim();
|
||||
int typeStart = lineOffset + genericMatcher.start(2);
|
||||
int typeEnd = lineOffset + genericMatcher.end(2);
|
||||
TypeInfo typeInfo = resolveType(typeName);
|
||||
tag.setType(typeName, typeInfo, typeStart, typeEnd);
|
||||
}
|
||||
|
||||
if (description != null && !description.isEmpty()) {
|
||||
tag.setDescription(description);
|
||||
}
|
||||
|
||||
info.addTag(tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String normalizeDescription(String description) {
|
||||
if (description == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = description.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
private CleanLine cleanLine(String line, int lineStartOffset) {
|
||||
int index = 0;
|
||||
while (index < line.length() && Character.isWhitespace(line.charAt(index))) {
|
||||
index++;
|
||||
}
|
||||
|
||||
if (index + 2 < line.length() && line.startsWith("/**", index)) {
|
||||
index += 3;
|
||||
while (index < line.length() && Character.isWhitespace(line.charAt(index))) {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
if (index < line.length() && line.charAt(index) == '*') {
|
||||
index++;
|
||||
if (index < line.length() && line.charAt(index) == ' ') {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
String cleaned = line.substring(index);
|
||||
int end = cleaned.length();
|
||||
while (end > 0 && Character.isWhitespace(cleaned.charAt(end - 1))) {
|
||||
end--;
|
||||
}
|
||||
cleaned = cleaned.substring(0, end);
|
||||
|
||||
return new CleanLine(cleaned, lineStartOffset + index);
|
||||
}
|
||||
|
||||
private TypeInfo resolveType(String typeName) {
|
||||
return document.resolveType(typeName);
|
||||
}
|
||||
|
||||
private static class CleanLine {
|
||||
private final String text;
|
||||
private final int offset;
|
||||
|
||||
private CleanLine(String text, int offset) {
|
||||
this.text = text;
|
||||
this.offset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<int[]> findAllJSDocComments(String source) {
|
||||
List<int[]> positions = new ArrayList<>();
|
||||
Matcher m = JSDOC_PATTERN.matcher(source);
|
||||
while (m.find()) {
|
||||
positions.add(new int[]{m.start(), m.end()});
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
}
|
||||
+29
@@ -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 + "}" : "") + "}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.jsdoc;
|
||||
|
||||
public class JSDocSeeTag extends JSDocTag {
|
||||
|
||||
private final String reference;
|
||||
private String url;
|
||||
private String linkText;
|
||||
|
||||
public JSDocSeeTag(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, String reference) {
|
||||
super(tagName, atSignOffset, tagNameStart, tagNameEnd);
|
||||
this.reference = reference;
|
||||
this.setDescription(reference);
|
||||
parseReference(reference);
|
||||
}
|
||||
|
||||
public static JSDocSeeTag create(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd,
|
||||
String reference) {
|
||||
return new JSDocSeeTag(tagName, atSignOffset, tagNameStart, tagNameEnd, reference);
|
||||
}
|
||||
|
||||
public static JSDocSeeTag createSimple(String reference) {
|
||||
return new JSDocSeeTag("see", -1, -1, -1, reference);
|
||||
}
|
||||
|
||||
private void parseReference(String ref) {
|
||||
if (ref == null) return;
|
||||
|
||||
if (ref.contains("<a href=")) {
|
||||
int hrefStart = ref.indexOf("href=\"");
|
||||
if (hrefStart != -1) {
|
||||
hrefStart += 6;
|
||||
int hrefEnd = ref.indexOf("\"", hrefStart);
|
||||
if (hrefEnd != -1) {
|
||||
this.url = ref.substring(hrefStart, hrefEnd);
|
||||
}
|
||||
}
|
||||
int textStart = ref.indexOf(">");
|
||||
int textEnd = ref.indexOf("</a>");
|
||||
if (textStart != -1 && textEnd != -1 && textStart < textEnd) {
|
||||
this.linkText = ref.substring(textStart + 1, textEnd);
|
||||
}
|
||||
} else if (ref.contains("{@link")) {
|
||||
int linkStart = ref.indexOf("{@link");
|
||||
int linkEnd = ref.indexOf("}", linkStart);
|
||||
if (linkStart != -1 && linkEnd != -1) {
|
||||
this.linkText = ref.substring(linkStart + 6, linkEnd).trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getReference() {
|
||||
return reference;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public boolean hasUrl() {
|
||||
return url != null && !url.isEmpty();
|
||||
}
|
||||
|
||||
public String getLinkText() {
|
||||
return linkText;
|
||||
}
|
||||
|
||||
public boolean hasLinkText() {
|
||||
return linkText != null && !linkText.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "JSDocSeeTag{@see " + reference + "}";
|
||||
}
|
||||
}
|
||||
@@ -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 + "}" : "") + "}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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, String description) {
|
||||
JSDocTypeTag tag = new JSDocTypeTag(atSignOffset, tagNameStart, tagNameEnd);
|
||||
tag.setType(typeName, typeInfo, typeStart, typeEnd);
|
||||
tag.setDescription(description);
|
||||
return tag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "JSDocTypeTag{@type " + (typeName != null ? "{" + typeName + "}" : "") + "}";
|
||||
}
|
||||
}
|
||||
+458
@@ -0,0 +1,458 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.method;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.token.Token;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Stores information about a method call for argument validation.
|
||||
* This includes the method name, the arguments passed, and validation results.
|
||||
*/
|
||||
public class MethodCallInfo {
|
||||
|
||||
/**
|
||||
* Represents a single argument in a method call.
|
||||
*/
|
||||
public static class Argument {
|
||||
private final String text; // The text of the argument expression
|
||||
private final int startOffset; // Start position in source
|
||||
private final int endOffset; // End position in source
|
||||
private final TypeInfo resolvedType; // The resolved type of the argument (null if unresolved)
|
||||
private final boolean valid; // Whether this arg matches the expected parameter type
|
||||
private final String errorMessage; // Error message if invalid
|
||||
|
||||
public Argument(String text, int startOffset, int endOffset, TypeInfo resolvedType,
|
||||
boolean valid, String errorMessage) {
|
||||
this.text = text;
|
||||
this.startOffset = startOffset;
|
||||
this.endOffset = endOffset;
|
||||
this.resolvedType = resolvedType;
|
||||
this.valid = valid;
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public int getStartOffset() {
|
||||
return startOffset;
|
||||
}
|
||||
|
||||
public int getEndOffset() {
|
||||
return endOffset;
|
||||
}
|
||||
|
||||
public TypeInfo getResolvedType() {
|
||||
return resolvedType;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public boolean equals(Token t) {
|
||||
return text.equals(t.getText()) &&
|
||||
startOffset == t.getGlobalStart() &&
|
||||
endOffset == t.getGlobalEnd();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Arg{" + text + " [" + startOffset + "-" + endOffset + "]" +
|
||||
(resolvedType != null ? " :" + resolvedType.getSimpleName() : "") +
|
||||
(valid ? "" : " INVALID: " + errorMessage) + "}";
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Validation error type.
|
||||
*/
|
||||
public enum ErrorType {
|
||||
NONE,
|
||||
WRONG_ARG_COUNT, // Number of args doesn't match any overload
|
||||
WRONG_ARG_TYPE, // Specific argument has wrong type
|
||||
STATIC_ACCESS_ERROR, // Trying to call instance method statically or vice versa
|
||||
RETURN_TYPE_MISMATCH, // Return type doesn't match expected type (e.g., assignment LHS)
|
||||
UNRESOLVED_METHOD, // Method doesn't exist
|
||||
UNRESOLVED_RECEIVER // Can't resolve the receiver type
|
||||
}
|
||||
|
||||
private final String methodName;
|
||||
private final int methodNameStart; // Position of method name
|
||||
private final int methodNameEnd;
|
||||
private final int openParenOffset; // Position of '('
|
||||
private final int closeParenOffset; // Position of ')'
|
||||
private final List<Argument> arguments;
|
||||
private final TypeInfo receiverType; // The type on which this method is called (null for standalone)
|
||||
private final MethodInfo resolvedMethod; // The resolved method (null if unresolved)
|
||||
private final boolean isStaticAccess; // True if this is Class.method() style access
|
||||
private TypeInfo expectedType; // Expected return type (from assignment LHS, etc.)
|
||||
|
||||
private ErrorType errorType = ErrorType.NONE;
|
||||
private String errorMessage;
|
||||
private int errorArgIndex = -1; // Index of the problematic argument (for WRONG_ARG_TYPE)
|
||||
private List<ArgumentTypeError> argumentTypeErrors = new ArrayList<>();
|
||||
|
||||
private boolean isConstructor;
|
||||
|
||||
// For Java.type() ClassTypeInfo constructor access
|
||||
// i.e var IntArray = Java.type("int[]"); var array = new IntArray()
|
||||
public boolean isClassTypeAccess;
|
||||
|
||||
public MethodCallInfo(String methodName, int methodNameStart, int methodNameEnd,
|
||||
int openParenOffset, int closeParenOffset,
|
||||
List<Argument> arguments, TypeInfo receiverType,
|
||||
MethodInfo resolvedMethod) {
|
||||
this(methodName, methodNameStart, methodNameEnd, openParenOffset, closeParenOffset,
|
||||
arguments, receiverType, resolvedMethod, false);
|
||||
}
|
||||
|
||||
public MethodCallInfo(String methodName, int methodNameStart, int methodNameEnd,
|
||||
int openParenOffset, int closeParenOffset,
|
||||
List<Argument> arguments, TypeInfo receiverType,
|
||||
MethodInfo resolvedMethod, boolean isStaticAccess) {
|
||||
this.methodName = methodName;
|
||||
this.methodNameStart = methodNameStart;
|
||||
this.methodNameEnd = methodNameEnd;
|
||||
this.openParenOffset = openParenOffset;
|
||||
this.closeParenOffset = closeParenOffset;
|
||||
this.arguments = arguments != null ? new ArrayList<>(arguments) : new ArrayList<>();
|
||||
this.receiverType = receiverType;
|
||||
this.resolvedMethod = resolvedMethod;
|
||||
this.isStaticAccess = isStaticAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a MethodCallInfo for a constructor call.
|
||||
* Constructors are represented as method calls where the type itself is the receiver.
|
||||
*/
|
||||
public static MethodCallInfo constructor(TypeInfo typeInfo, MethodInfo constructor,
|
||||
int typeNameStart, int typeNameEnd,
|
||||
int openParenOffset, int closeParenOffset,
|
||||
List<Argument> arguments) {
|
||||
return new MethodCallInfo(
|
||||
typeInfo.getSimpleName(), // Constructor name is the type name
|
||||
typeNameStart,
|
||||
typeNameEnd,
|
||||
openParenOffset,
|
||||
closeParenOffset,
|
||||
arguments,
|
||||
typeInfo, // The type itself is the receiver
|
||||
constructor, // The constructor MethodInfo
|
||||
false // Constructors are not static access
|
||||
).setConstructor(true);
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getMethodName() {
|
||||
return methodName;
|
||||
}
|
||||
|
||||
public int getMethodNameStart() {
|
||||
return methodNameStart;
|
||||
}
|
||||
|
||||
public int getMethodNameEnd() {
|
||||
return methodNameEnd;
|
||||
}
|
||||
|
||||
public int getOpenParenOffset() {
|
||||
return openParenOffset;
|
||||
}
|
||||
|
||||
public int getCloseParenOffset() {
|
||||
return closeParenOffset;
|
||||
}
|
||||
|
||||
public List<Argument> getArguments() {
|
||||
return Collections.unmodifiableList(arguments);
|
||||
}
|
||||
|
||||
public int getArgumentCount() {
|
||||
return arguments.size();
|
||||
}
|
||||
|
||||
public TypeInfo getReceiverType() {
|
||||
return receiverType;
|
||||
}
|
||||
|
||||
public MethodInfo getResolvedMethod() {
|
||||
return resolvedMethod;
|
||||
}
|
||||
|
||||
public boolean isStaticAccess() {
|
||||
return isStaticAccess;
|
||||
}
|
||||
|
||||
public TypeInfo getExpectedType() {
|
||||
return expectedType;
|
||||
}
|
||||
|
||||
public void setExpectedType(TypeInfo expectedType) {
|
||||
this.expectedType = expectedType;
|
||||
}
|
||||
|
||||
// Dynamically resolved return type (e.g., for Java.type() which returns ClassTypeInfo)
|
||||
private TypeInfo resolvedReturnType;
|
||||
|
||||
/**
|
||||
* Get the resolved return type of this method call.
|
||||
* This may differ from resolvedMethod.getReturnType() for methods with dynamic return types.
|
||||
* @return The resolved return type, or the method's declared return type if not dynamically resolved
|
||||
*/
|
||||
public TypeInfo getResolvedReturnType() {
|
||||
if (resolvedReturnType != null) {
|
||||
return resolvedReturnType;
|
||||
}
|
||||
return resolvedMethod != null ? resolvedMethod.getReturnType() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a dynamically resolved return type.
|
||||
* Used for methods like Java.type() where the return type depends on the arguments.
|
||||
*/
|
||||
public void setResolvedReturnType(TypeInfo returnType) {
|
||||
this.resolvedReturnType = returnType;
|
||||
}
|
||||
|
||||
public boolean isConstructor() {
|
||||
return isConstructor;
|
||||
}
|
||||
|
||||
public MethodCallInfo setConstructor(boolean isConstructor) {
|
||||
this.isConstructor = isConstructor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ErrorType getErrorType() {
|
||||
return errorType;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full span of the method call including parentheses.
|
||||
* Used for underlining the entire call on arg count errors.
|
||||
*/
|
||||
public int getFullCallStart() {
|
||||
return methodNameStart;
|
||||
}
|
||||
|
||||
public int getFullCallEnd() {
|
||||
return closeParenOffset + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this method call has any validation errors.
|
||||
*/
|
||||
public boolean hasError() {
|
||||
return errorType != ErrorType.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is an arg count error (underline whole call).
|
||||
*/
|
||||
public boolean hasArgCountError() {
|
||||
return errorType == ErrorType.WRONG_ARG_COUNT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is an arg type error (underline specific arg).
|
||||
*/
|
||||
public boolean hasArgTypeError() {
|
||||
return !this.argumentTypeErrors.isEmpty();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if this is a static access error (underline method name).
|
||||
*/
|
||||
public boolean hasStaticAccessError() {
|
||||
return errorType == ErrorType.STATIC_ACCESS_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a return type mismatch error.
|
||||
*/
|
||||
public boolean hasReturnTypeMismatch() {
|
||||
return errorType == ErrorType.RETURN_TYPE_MISMATCH;
|
||||
}
|
||||
|
||||
// Setters for validation results
|
||||
public void setError(ErrorType type, String message) {
|
||||
this.errorType = type;
|
||||
this.errorMessage = message;
|
||||
}
|
||||
|
||||
public void setArgTypeError(int argIndex, String message) {
|
||||
this.argumentTypeErrors.add(new ArgumentTypeError(arguments.get(argIndex), argIndex, message));
|
||||
}
|
||||
|
||||
public List<ArgumentTypeError> getArgumentTypeErrors() {
|
||||
return argumentTypeErrors;
|
||||
}
|
||||
|
||||
public class ArgumentTypeError {
|
||||
private ErrorType type = ErrorType.WRONG_ARG_TYPE;
|
||||
private final Argument arg;
|
||||
private final int argIndex;
|
||||
private final String message;
|
||||
|
||||
public ArgumentTypeError(Argument arg, int argIndex, String message) {
|
||||
this.arg = arg;
|
||||
this.argIndex = argIndex;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public int getArgIndex() {
|
||||
return argIndex;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public Argument getArg() {
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate this method call against the resolved method signature.
|
||||
* Sets error information if validation fails.
|
||||
*/
|
||||
public void validate() {
|
||||
if (isConstructor) {
|
||||
if (resolvedMethod == null) {
|
||||
// Don't report missing JS constructors as unresolved method error, since they may be created dynamically at runtime
|
||||
boolean isJSType = receiverType != null && receiverType.isJSType();
|
||||
if (!isJSType && !isClassTypeAccess) {
|
||||
if (receiverType != null && receiverType.hasConstructors()) {
|
||||
setError(ErrorType.WRONG_ARG_COUNT,
|
||||
"No constructor in '" + receiverType.getSimpleName() + "' matches " + arguments.size() + " argument(s)");
|
||||
} else {
|
||||
setError(ErrorType.UNRESOLVED_METHOD,
|
||||
"Cannot resolve constructor for '" + methodName + "'");
|
||||
}
|
||||
}
|
||||
} else if (receiverType != null) {
|
||||
List<FieldInfo> cParams = resolvedMethod.getParameters();
|
||||
boolean ctorVarArg = !cParams.isEmpty() && cParams.get(cParams.size() - 1).isVarArg();
|
||||
boolean ctorCountOk = ctorVarArg
|
||||
? arguments.size() >= cParams.size() - 1
|
||||
: arguments.size() == resolvedMethod.getParameterCount();
|
||||
if (ctorCountOk) validateArgTypeError();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check static/instance access (skip for constructors)
|
||||
// NO LONGER CHECKED HERE, BUT DIRECTLY AT MARK CREATION
|
||||
if (!isConstructor && isStaticAccess && !resolvedMethod.isStatic()) {
|
||||
setError(ErrorType.STATIC_ACCESS_ERROR,
|
||||
"Cannot call instance method '" + methodName + "' on a class type");
|
||||
return;
|
||||
}
|
||||
|
||||
List<FieldInfo> params = resolvedMethod.getParameters();
|
||||
int expectedCount = params.size();
|
||||
int actualCount = arguments.size();
|
||||
boolean isVarArgMethod = expectedCount > 0 && params.get(expectedCount - 1).isVarArg();
|
||||
|
||||
// Check arg count
|
||||
if (isVarArgMethod) {
|
||||
if (actualCount < expectedCount - 1) {
|
||||
setError(ErrorType.WRONG_ARG_COUNT,
|
||||
"Expected at least " + (expectedCount - 1) + " argument(s) but got " + actualCount);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (actualCount != expectedCount) {
|
||||
setError(ErrorType.WRONG_ARG_COUNT,
|
||||
"Expected " + expectedCount + " argument(s) but got " + actualCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check each argument type
|
||||
validateArgTypeError();
|
||||
|
||||
// Check return type compatibility with expected type (e.g., assignment LHS)
|
||||
// ALREADY CHECKED AT AssignmentInfo LEVEL
|
||||
if (expectedType != null && resolvedMethod != null) {
|
||||
TypeInfo returnType = resolvedMethod.getReturnType();
|
||||
if (returnType != null && !TypeChecker.isTypeCompatible(expectedType, returnType)) {
|
||||
// setError(ErrorType.RETURN_TYPE_MISMATCH,
|
||||
// "Required type: " + expectedType.getSimpleName() + ", Provided: " + returnType.getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void validateArgTypeError() {
|
||||
List<FieldInfo> params = resolvedMethod.getParameters();
|
||||
int paramCount = params.size();
|
||||
boolean isVarArgMethod = paramCount > 0 && params.get(paramCount - 1).isVarArg();
|
||||
|
||||
for (int i = 0; i < arguments.size(); i++) {
|
||||
Argument arg = arguments.get(i);
|
||||
|
||||
// Surface argument-level errors (e.g., SAM conflict, ambiguous overload, bare method in Java)
|
||||
if (!arg.isValid() && arg.getErrorMessage() != null) {
|
||||
setArgTypeError(i, arg.getErrorMessage());
|
||||
continue; // Skip remaining checks for this argument
|
||||
}
|
||||
|
||||
// Varargs: all args at index >= (paramCount-1) map to the element type of the last param
|
||||
FieldInfo para;
|
||||
TypeInfo paramType;
|
||||
if (isVarArgMethod && i >= paramCount - 1) {
|
||||
para = params.get(paramCount - 1);
|
||||
TypeInfo arrType = para.getTypeInfo();
|
||||
paramType = (arrType != null && arrType.isArray()) ? arrType.getElementType() : arrType;
|
||||
} else {
|
||||
para = params.get(i);
|
||||
paramType = para.getTypeInfo();
|
||||
}
|
||||
|
||||
TypeInfo argType = arg.getResolvedType();
|
||||
if (argType != null && paramType != null) {
|
||||
if (!TypeChecker.isTypeCompatible(paramType, argType)) {
|
||||
setArgTypeError(i, "Expected " + paramType.getDisplayName() +
|
||||
" but got " + argType.getDisplayName());
|
||||
}
|
||||
} else if (paramType == null) {
|
||||
setArgTypeError(i, "Parameter type of '" + para.getName() + "' is unresolved");
|
||||
} else if (argType == null) {
|
||||
setArgTypeError(i, "Cannot resolve type of argument '" + arg.getText() + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder("MethodCallInfo{");
|
||||
sb.append(methodName).append("(");
|
||||
for (int i = 0; i < arguments.size(); i++) {
|
||||
if (i > 0)
|
||||
sb.append(", ");
|
||||
sb.append(arguments.get(i).getText());
|
||||
}
|
||||
sb.append(")");
|
||||
if (hasError()) {
|
||||
sb.append(" ERROR: ").append(errorType).append(" - ").append(errorMessage);
|
||||
}
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,746 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.method;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.*;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.bridge.DtsJavaBridge;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo;
|
||||
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.token.TokenType;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.GenericContext;
|
||||
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.TypeSubstitutor;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.GenericParameterAdapter;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Metadata for a method declaration or method call.
|
||||
* Tracks method name, parameters, return type, and containing class.
|
||||
*
|
||||
* <p>This class delegates to helper classes for specific functionality:</p>
|
||||
* <ul>
|
||||
* <li>{@link ControlFlowAnalyzer} - Control flow analysis for missing return detection</li>
|
||||
* <li>{@link CodeParser} - Code parsing utilities (brace matching, comment removal)</li>
|
||||
* <li>{@link TypeChecker} - Type compatibility checking</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class MethodInfo {
|
||||
|
||||
/**
|
||||
* Validation error types for method declarations.
|
||||
*/
|
||||
public enum ErrorType {
|
||||
NONE,
|
||||
MISSING_RETURN, // Non-void method missing return statement,
|
||||
INTERFACE_METHOD_BODY, // Interface method has a body
|
||||
MISSING_BODY, // Non-void method missing return statement
|
||||
RETURN_TYPE_MISMATCH, // Return statement type doesn't match method return type
|
||||
VOID_METHOD_RETURNS_VALUE, // Void method returns a value
|
||||
DUPLICATE_METHOD, // Method with same signature already defined in scope
|
||||
DUPLICATE_PARAMETER, // Two parameters have the same name
|
||||
PARAMETER_UNDEFINED, // Parameter type cannot be resolved
|
||||
SAM_TYPE_INCOMPATIBLE // Explicit param type incompatible with SAM context
|
||||
}
|
||||
|
||||
private final String name;
|
||||
private final TypeInfo returnType;
|
||||
private final TypeInfo containingType; // The class/interface that owns this method
|
||||
private final List<FieldInfo> parameters;
|
||||
private final int fullDeclarationOffset; // Start of full declaration (including modifiers), -1 for external
|
||||
private final int typeOffset; // Start of return type
|
||||
private final int nameOffset; // Start of method name
|
||||
private final int bodyStart; // Start of method body (after {)
|
||||
private final int bodyEnd; // End of method body (before })
|
||||
private final boolean resolved;
|
||||
private final boolean isDeclaration; // true if this is a declaration, false if it's a call
|
||||
private final int modifiers; // Java Modifier flags (e.g., Modifier.PUBLIC | Modifier.STATIC)
|
||||
private final String documentation; // Javadoc/comment documentation for this method
|
||||
private JSDocInfo jsDocInfo; // Parsed JSDoc info for this method (may be null)
|
||||
private final java.lang.reflect.Method javaMethod; // The Java reflection Method, if this was created from reflection
|
||||
|
||||
// Error tracking for method declarations
|
||||
private ErrorType errorType = ErrorType.NONE;
|
||||
private String errorMessage;
|
||||
private List<ParameterError> parameterErrors = new ArrayList<>();
|
||||
private List<ReturnStatementError> returnStatementErrors = new ArrayList<>();
|
||||
|
||||
// ==================== OVERRIDE/IMPLEMENTS TRACKING ====================
|
||||
|
||||
/**
|
||||
* The type containing the method that this method overrides (the super class).
|
||||
* Null if this method doesn't override anything.
|
||||
*/
|
||||
private TypeInfo overridesFrom;
|
||||
|
||||
/**
|
||||
* The type containing the interface method that this method implements.
|
||||
* Null if this method doesn't implement an interface method.
|
||||
*/
|
||||
private TypeInfo implementsFrom;
|
||||
|
||||
private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType,
|
||||
List<FieldInfo> parameters, int fullDeclarationOffset, int typeOffset, int nameOffset,
|
||||
int bodyStart, int bodyEnd, boolean resolved, boolean isDeclaration,
|
||||
int modifiers, String documentation, java.lang.reflect.Method javaMethod) {
|
||||
this.name = name;
|
||||
this.returnType = returnType;
|
||||
this.containingType = containingType;
|
||||
this.parameters = parameters != null ? new ArrayList<>(parameters) : new ArrayList<>();
|
||||
this.fullDeclarationOffset = fullDeclarationOffset;
|
||||
this.typeOffset = typeOffset;
|
||||
this.nameOffset = nameOffset;
|
||||
this.bodyStart = bodyStart;
|
||||
this.bodyEnd = bodyEnd;
|
||||
this.resolved = resolved;
|
||||
this.isDeclaration = isDeclaration;
|
||||
this.modifiers = modifiers;
|
||||
this.documentation = documentation;
|
||||
this.javaMethod = javaMethod;
|
||||
}
|
||||
|
||||
public static MethodInfo declaration(String name, TypeInfo containingType, TypeInfo returnType, List<FieldInfo> params,
|
||||
int fullDeclOffset, int typeOffset, int nameOffset,
|
||||
int bodyStart, int bodyEnd, int modifiers, String documentation) {
|
||||
return new MethodInfo(name, returnType, containingType, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, modifiers, documentation, null);
|
||||
}
|
||||
|
||||
public static MethodInfo call(String name, TypeInfo containingType, int paramCount) {
|
||||
boolean resolved = containingType != null && containingType.isResolved() &&
|
||||
containingType.hasMethod(name);
|
||||
List<FieldInfo> params = new ArrayList<>();
|
||||
for (int i = 0; i < paramCount; i++) {
|
||||
params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER));
|
||||
}
|
||||
return new MethodInfo(name, null, containingType, params, -1, -1, -1, -1, -1, resolved, false, 0, null, null);
|
||||
}
|
||||
|
||||
public static MethodInfo unresolvedCall(String name, int paramCount) {
|
||||
List<FieldInfo> params = new ArrayList<>();
|
||||
for (int i = 0; i < paramCount; i++) {
|
||||
params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER));
|
||||
}
|
||||
return new MethodInfo(name, null, null, params, -1, -1, -1, -1, -1, false, false, 0, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a MethodInfo for a synthetic/external method.
|
||||
* Used for built-in types like Nashorn's Java object.
|
||||
*/
|
||||
public static MethodInfo external(String name, TypeInfo returnType, TypeInfo containingType,
|
||||
List<FieldInfo> params, int modifiers, String documentation) {
|
||||
return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, false, modifiers, documentation, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a MethodInfo from reflection data.
|
||||
* Used when resolving method calls on known types.
|
||||
* Preserves generic type information from reflection.
|
||||
*/
|
||||
public static MethodInfo fromReflection(Method method, TypeInfo containingType) {
|
||||
String name = method.getName();
|
||||
// Use getGenericReturnType() to preserve generic information like List<String>
|
||||
TypeInfo returnType = TypeInfo.fromGenericType(method.getGenericReturnType());
|
||||
if (returnType == null) {
|
||||
returnType = TypeInfo.fromClass(method.getReturnType());
|
||||
}
|
||||
int modifiers = method.getModifiers();
|
||||
|
||||
// Try to find matching JSMethodInfo to bridge parameter names/docs and allow return overrides
|
||||
JSMethodInfo jsMethod = DtsJavaBridge.findMatchingMethod(method, containingType);
|
||||
if (jsMethod != null) {
|
||||
// If .d.ts provides a more specific return type, use it for editor typing.
|
||||
// (e.g., .d.ts override like IDBCPlayer -> IDBCAddon in npcdbc).
|
||||
TypeInfo overrideReturnType = DtsJavaBridge.resolveReturnTypeOverride(method, containingType, jsMethod);
|
||||
if (overrideReturnType != null && overrideReturnType.getJavaClass() != null) {
|
||||
Class<?> reflectedReturn = method.getReturnType();
|
||||
Class<?> overrideClass = overrideReturnType.getJavaClass();
|
||||
if (reflectedReturn.isAssignableFrom(overrideClass)) {
|
||||
if (reflectedReturn != overrideClass) {
|
||||
returnType = overrideReturnType;
|
||||
System.out.println("[DtsJavaBridge] Overrode return type for "
|
||||
+ method.getDeclaringClass().getName() + "." + name
|
||||
+ "(" + method.getParameterCount() + ") from "
|
||||
+ reflectedReturn.getName() + " to " + overrideClass.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the receiver is parameterized (e.g., List<String>), substitute class type variables (E -> String)
|
||||
Map<String, TypeInfo> receiverBindings = null;
|
||||
if (GenericContext.hasGenerics(containingType)) {
|
||||
receiverBindings = TypeSubstitutor.createBindingsFromReceiver(containingType);
|
||||
if (!receiverBindings.isEmpty()) {
|
||||
returnType = TypeSubstitutor.substitute(returnType, receiverBindings);
|
||||
}
|
||||
}
|
||||
List<FieldInfo> params = new ArrayList<>();
|
||||
// Use getGenericParameterTypes() to preserve generic information
|
||||
java.lang.reflect.Type[] genericParamTypes = method.getGenericParameterTypes();
|
||||
List<JSMethodInfo.JSParameterInfo> jsParams = jsMethod != null ? jsMethod.getParameters() : null;
|
||||
for (int i = 0; i < genericParamTypes.length; i++) {
|
||||
TypeInfo paramType = TypeInfo.fromGenericType(genericParamTypes[i]);
|
||||
if (paramType == null) {
|
||||
paramType = TypeInfo.fromClass(method.getParameterTypes()[i]);
|
||||
}
|
||||
|
||||
if (receiverBindings != null && !receiverBindings.isEmpty()) {
|
||||
paramType = TypeSubstitutor.substitute(paramType, receiverBindings);
|
||||
}
|
||||
|
||||
// Adapt Object params to receiver generic type args (e.g., Map.get(Object) -> Map.get(K))
|
||||
TypeInfo adapted = GenericParameterAdapter.adaptParameterType(method, containingType, i, paramType);
|
||||
if (adapted != null) {
|
||||
paramType = adapted;
|
||||
}
|
||||
String paramName = "arg" + i;
|
||||
if (jsParams != null && i < jsParams.size()) {
|
||||
String jsName = jsParams.get(i).getName();
|
||||
if (jsName != null && !jsName.isEmpty()) {
|
||||
paramName = jsName;
|
||||
}
|
||||
}
|
||||
|
||||
FieldInfo fieldInfo = FieldInfo.reflectionParam(paramName, paramType);
|
||||
boolean isVarArg = method.isVarArgs() && i == genericParamTypes.length - 1;
|
||||
if (jsParams != null && i < jsParams.size()) {
|
||||
isVarArg = isVarArg || jsParams.get(i).isVarArg();
|
||||
}
|
||||
fieldInfo.setVarArg(isVarArg);
|
||||
params.add(fieldInfo);
|
||||
}
|
||||
|
||||
String documentation = null;
|
||||
JSDocInfo jsDocInfo = null;
|
||||
if (jsMethod != null) {
|
||||
jsDocInfo = jsMethod.getJsDocInfo();
|
||||
String jsDocDesc = jsDocInfo != null ? jsDocInfo.getDescription() : null;
|
||||
documentation = jsDocDesc != null ? jsDocDesc : jsMethod.getDocumentation();
|
||||
}
|
||||
|
||||
MethodInfo methodInfo = new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, false, modifiers, documentation, method);
|
||||
if (jsDocInfo != null) {
|
||||
methodInfo.setJSDocInfo(jsDocInfo);
|
||||
}
|
||||
return methodInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a MethodInfo from a Constructor via reflection.
|
||||
* Used when resolving constructor calls on external types.
|
||||
* Preserves generic type information for constructor parameters.
|
||||
*/
|
||||
public static MethodInfo fromReflectionConstructor(Constructor<?> constructor, TypeInfo containingType) {
|
||||
String name = containingType.getSimpleName(); // Constructor name is the type name
|
||||
TypeInfo returnType = containingType; // Constructor "returns" an instance of the type
|
||||
int modifiers = constructor.getModifiers();
|
||||
|
||||
List<FieldInfo> params = new ArrayList<>();
|
||||
// Use getGenericParameterTypes() to preserve generic information
|
||||
java.lang.reflect.Type[] genericParamTypes = constructor.getGenericParameterTypes();
|
||||
for (int i = 0; i < genericParamTypes.length; i++) {
|
||||
TypeInfo paramType = TypeInfo.fromGenericType(genericParamTypes[i]);
|
||||
if (paramType == null) {
|
||||
paramType = TypeInfo.fromClass(constructor.getParameterTypes()[i]);
|
||||
}
|
||||
FieldInfo fieldInfo = FieldInfo.reflectionParam("arg" + i, paramType);
|
||||
fieldInfo.setVarArg(constructor.isVarArgs() && i == genericParamTypes.length - 1);
|
||||
params.add(fieldInfo);
|
||||
}
|
||||
|
||||
return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, true, modifiers, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a MethodInfo from a JSMethodInfo (parsed from .d.ts files).
|
||||
* Used when resolving method calls on JavaScript types.
|
||||
*
|
||||
* @param jsMethod The JavaScript method info from the type registry
|
||||
* @param containingType The TypeInfo that owns this method
|
||||
* @return A MethodInfo representing the JavaScript method
|
||||
*/
|
||||
public static MethodInfo fromJSMethod(JSMethodInfo jsMethod, TypeInfo containingType) {
|
||||
String name = jsMethod.getName();
|
||||
|
||||
// Use the new getResolvedReturnType method
|
||||
TypeInfo returnType = jsMethod.getResolvedReturnType(containingType);
|
||||
|
||||
// Convert JS parameters to FieldInfo
|
||||
List<FieldInfo> params = new ArrayList<>();
|
||||
List<JSMethodInfo.JSParameterInfo> jsParams = jsMethod.getParameters();
|
||||
|
||||
for (JSMethodInfo.JSParameterInfo param : jsParams) {
|
||||
String paramName = param.getName();
|
||||
|
||||
// Use the new getResolvedType method
|
||||
TypeInfo paramType = param.getResolvedType(containingType);
|
||||
|
||||
FieldInfo fieldInfo = FieldInfo.reflectionParam(paramName, paramType);
|
||||
fieldInfo.setVarArg(param.isVarArg());
|
||||
params.add(fieldInfo);
|
||||
}
|
||||
|
||||
// JS methods are always public (no access modifiers in .d.ts)
|
||||
int modifiers = Modifier.PUBLIC;
|
||||
if (jsMethod.isStatic()) {
|
||||
modifiers |= Modifier.STATIC;
|
||||
}
|
||||
|
||||
// Use the documentation from the method if available
|
||||
JSDocInfo jsDocInfo = jsMethod.getJsDocInfo();
|
||||
String jsDocDesc = jsDocInfo != null ? jsDocInfo.getDescription() : null;
|
||||
String documentation = jsDocDesc != null ? jsDocDesc : jsMethod.getDocumentation();
|
||||
|
||||
MethodInfo methodInfo = new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true,
|
||||
false, modifiers, documentation, null);
|
||||
methodInfo.setJSDocInfo(jsDocInfo);
|
||||
return methodInfo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Getters
|
||||
public String getName() { return name; }
|
||||
public TypeInfo getReturnType() { return returnType; }
|
||||
public TypeInfo getContainingType() { return containingType; }
|
||||
public List<FieldInfo> getParameters() { return Collections.unmodifiableList(parameters); }
|
||||
public int getParameterCount() { return parameters.size(); }
|
||||
public Method getJavaMethod() { return javaMethod; }
|
||||
/** @deprecated Use getTypeOffset() or getNameOffset() instead */
|
||||
@Deprecated
|
||||
public int getDeclarationOffset() { return typeOffset; }
|
||||
public int getFullDeclarationOffset() { return fullDeclarationOffset; }
|
||||
public int getTypeOffset() { return typeOffset; }
|
||||
public int getNameOffset() { return nameOffset; }
|
||||
public int getBodyStart() { return bodyStart; }
|
||||
public int getBodyEnd() { return bodyEnd; }
|
||||
public boolean isResolved() { return resolved; }
|
||||
public boolean isDeclaration() { return isDeclaration; }
|
||||
public boolean isCall() { return !isDeclaration; }
|
||||
public int getModifiers() { return modifiers; }
|
||||
public boolean isStatic() { return Modifier.isStatic(modifiers); }
|
||||
public boolean isFinal() { return Modifier.isFinal(modifiers); }
|
||||
public boolean isAbstract() { return Modifier.isAbstract(modifiers); }
|
||||
public boolean isSynchronized() { return Modifier.isSynchronized(modifiers); }
|
||||
public boolean isNative() { return Modifier.isNative(modifiers); }
|
||||
public boolean isPublic() { return Modifier.isPublic(modifiers); }
|
||||
public boolean isPrivate() { return Modifier.isPrivate(modifiers); }
|
||||
public boolean isProtected() { return Modifier.isProtected(modifiers); }
|
||||
public String getDocumentation() { return documentation; }
|
||||
public JSDocInfo getJSDocInfo() { return jsDocInfo; }
|
||||
public void setJSDocInfo(JSDocInfo jsDocInfo) { this.jsDocInfo = jsDocInfo; }
|
||||
|
||||
// ==================== OVERRIDE/IMPLEMENTS GETTERS/SETTERS ====================
|
||||
|
||||
/**
|
||||
* Check if this method overrides a parent class method.
|
||||
*/
|
||||
public boolean isOverride() { return overridesFrom != null; }
|
||||
|
||||
/**
|
||||
* Get the type containing the method this overrides.
|
||||
* @return The parent class TypeInfo, or null if not an override
|
||||
*/
|
||||
public TypeInfo getOverridesFrom() { return overridesFrom; }
|
||||
|
||||
/**
|
||||
* Mark this method as overriding a parent class method.
|
||||
* @param parentType The type containing the overridden method
|
||||
*/
|
||||
public void setOverridesFrom(TypeInfo parentType) { this.overridesFrom = parentType; }
|
||||
|
||||
/**
|
||||
* Check if this method implements an interface method.
|
||||
*/
|
||||
public boolean isImplements() { return implementsFrom != null; }
|
||||
|
||||
/**
|
||||
* Get the interface containing the method this implements.
|
||||
* @return The interface TypeInfo, or null if not implementing
|
||||
*/
|
||||
public TypeInfo getImplementsFrom() { return implementsFrom; }
|
||||
|
||||
/**
|
||||
* Mark this method as implementing an interface method.
|
||||
* @param interfaceType The interface containing the implemented method
|
||||
*/
|
||||
public void setImplementsFrom(TypeInfo interfaceType) { this.implementsFrom = interfaceType; }
|
||||
|
||||
/**
|
||||
* Check if this method either overrides or implements something.
|
||||
*/
|
||||
public boolean hasInheritanceMarker() { return isOverride() || isImplements(); }
|
||||
|
||||
/**
|
||||
* Check if a position is inside this method's body.
|
||||
*/
|
||||
public boolean containsPosition(int position) {
|
||||
return position >= bodyStart && position < bodyEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the end of the method declaration (closing paren position).
|
||||
* This is used for error highlighting of duplicate methods.
|
||||
*/
|
||||
public int getDeclarationEnd() {
|
||||
// The declaration ends just before the opening brace
|
||||
// We find the closing paren by searching backwards from bodyStart
|
||||
return bodyStart > 0 ? bodyStart - 1 : nameOffset + name.length();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this method has a parameter with the given name.
|
||||
*/
|
||||
public boolean hasParameter(String paramName) {
|
||||
for (FieldInfo p : parameters) {
|
||||
if (p.getName().equals(paramName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parameter info by name.
|
||||
*/
|
||||
public FieldInfo getParameter(String paramName) {
|
||||
for (FieldInfo p : parameters) {
|
||||
if (p.getName().equals(paramName)) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method signature for duplicate detection.
|
||||
* Signature = name + parameter types (ignoring parameter names).
|
||||
*/
|
||||
public MethodSignature cachedSignature;
|
||||
|
||||
public MethodSignature getSignature() {
|
||||
if (cachedSignature == null) {
|
||||
List<TypeInfo> paramTypes = new ArrayList<>();
|
||||
for (FieldInfo param : parameters)
|
||||
paramTypes.add(param.getTypeInfo());
|
||||
cachedSignature = new MethodSignature(name, paramTypes);
|
||||
}
|
||||
return cachedSignature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate TokenType for highlighting this method.
|
||||
*/
|
||||
public TokenType getTokenType() {
|
||||
if (isDeclaration) {
|
||||
return TokenType.METHOD_DECL;
|
||||
}
|
||||
return resolved ? TokenType.METHOD_CALL : TokenType.DEFAULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder("MethodInfo{");
|
||||
sb.append(name).append("(");
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
if (i > 0) sb.append(", ");
|
||||
sb.append(parameters.get(i).getName());
|
||||
}
|
||||
sb.append(")");
|
||||
if (returnType != null) {
|
||||
sb.append(" -> ").append(returnType.getSimpleName());
|
||||
}
|
||||
sb.append(", ").append(isDeclaration ? "decl" : "call");
|
||||
sb.append(", ").append(resolved ? "resolved" : "unresolved");
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
MethodInfo that = (MethodInfo) o;
|
||||
return name.equals(that.name) && parameters.size() == that.parameters.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode() * 31 + parameters.size();
|
||||
}
|
||||
|
||||
// ==================== ERROR HANDLING ====================
|
||||
|
||||
/**
|
||||
* Represents an error with a specific parameter.
|
||||
*/
|
||||
public static class ParameterError {
|
||||
private final FieldInfo parameter;
|
||||
private final int paramIndex;
|
||||
private final ErrorType errorType;
|
||||
private final String message;
|
||||
|
||||
public ParameterError(FieldInfo parameter, int paramIndex, ErrorType errorType, String message) {
|
||||
this.parameter = parameter;
|
||||
this.paramIndex = paramIndex;
|
||||
this.errorType = errorType;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public FieldInfo getParameter() { return parameter; }
|
||||
public int getParamIndex() { return paramIndex; }
|
||||
public ErrorType getErrorType() { return errorType; }
|
||||
public String getMessage() { return message; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an error with a return statement (type mismatch).
|
||||
*/
|
||||
public static class ReturnStatementError {
|
||||
private final int startOffset; // Start of "return" keyword (absolute position)
|
||||
private final int endOffset; // End of return statement (after semicolon)
|
||||
private final String message;
|
||||
private final TypeInfo expectedType;
|
||||
private final TypeInfo actualType;
|
||||
|
||||
public ReturnStatementError(int startOffset, int endOffset, String message, TypeInfo expectedType, TypeInfo actualType) {
|
||||
this.startOffset = startOffset;
|
||||
this.endOffset = endOffset;
|
||||
this.message = message;
|
||||
this.expectedType = expectedType;
|
||||
this.actualType = actualType;
|
||||
}
|
||||
|
||||
public int getStartOffset() { return startOffset; }
|
||||
public int getEndOffset() { return endOffset; }
|
||||
public String getMessage() { return message; }
|
||||
public TypeInfo getExpectedType() { return expectedType; }
|
||||
public TypeInfo getActualType() { return actualType; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this method declaration has any errors.
|
||||
*/
|
||||
public boolean hasError() {
|
||||
return errorType != ErrorType.NONE || !parameterErrors.isEmpty() || !returnStatementErrors.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this has a missing return error.
|
||||
*/
|
||||
public boolean hasMissingReturnError() {
|
||||
return errorType == ErrorType.MISSING_RETURN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this has parameter errors.
|
||||
*/
|
||||
public boolean hasParameterErrors() {
|
||||
return !parameterErrors.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this has return statement type errors.
|
||||
*/
|
||||
public boolean hasReturnStatementErrors() {
|
||||
return !returnStatementErrors.isEmpty();
|
||||
}
|
||||
|
||||
public ErrorType getErrorType() { return errorType; }
|
||||
public String getErrorMessage() { return errorMessage; }
|
||||
public List<ParameterError> getParameterErrors() { return Collections.unmodifiableList(parameterErrors); }
|
||||
public List<ReturnStatementError> getReturnStatementErrors() { return Collections.unmodifiableList(returnStatementErrors); }
|
||||
|
||||
/**
|
||||
* Set the main error for this method.
|
||||
*/
|
||||
public void setError(ErrorType type, String message) {
|
||||
this.errorType = type;
|
||||
this.errorMessage = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a parameter-specific error.
|
||||
*/
|
||||
public void addParameterError(FieldInfo param, int index, ErrorType type, String message) {
|
||||
parameterErrors.add(new ParameterError(param, index, type, message));
|
||||
}
|
||||
|
||||
public void addSamTypeError(int paramIndex, TypeInfo expectedSamType, TypeInfo actualDeclaredType) {
|
||||
if (paramIndex < 0 || paramIndex >= parameters.size()) {
|
||||
return;
|
||||
}
|
||||
FieldInfo param = parameters.get(paramIndex);
|
||||
String message = "Explicit type '" + actualDeclaredType.getSimpleName() +
|
||||
"' is incompatible with SAM parameter type '" + expectedSamType.getSimpleName() + "'";
|
||||
parameterErrors.add(new ParameterError(param, paramIndex, ErrorType.SAM_TYPE_INCOMPATIBLE, message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a return statement error.
|
||||
*/
|
||||
public void addReturnStatementError(int startOffset, int endOffset, String message, TypeInfo expectedType, TypeInfo actualType) {
|
||||
returnStatementErrors.add(new ReturnStatementError(startOffset, endOffset, message, expectedType, actualType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional interface for type resolution callback.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TypeResolver {
|
||||
TypeInfo resolveExpression(String expression, int position);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validate this method declaration with type resolution for return statements.
|
||||
* Checks for parameter errors, missing return statements, and return type mismatches.
|
||||
*
|
||||
* @param methodBodyText The text content of the method body (between { and })
|
||||
* @param typeResolver Optional callback to resolve expression types (for return type checking)
|
||||
*/
|
||||
public void validate(String methodBodyText, boolean hasBody, TypeResolver typeResolver) {
|
||||
if (!isDeclaration) return;
|
||||
|
||||
// Don't error if return type unresolved
|
||||
if(returnType != null && !returnType.isResolved())
|
||||
return;
|
||||
|
||||
// Validate parameters
|
||||
validateParameters();
|
||||
boolean interfaceMember = containingType != null && containingType.isInterface();
|
||||
if (hasBody && (interfaceMember || isAbstract() || isNative())) {
|
||||
setError(MethodInfo.ErrorType.INTERFACE_METHOD_BODY, "Interface or abstract methods cannot have a body");
|
||||
return;
|
||||
}
|
||||
|
||||
//No need to check for return types/statements
|
||||
if(interfaceMember)
|
||||
return;
|
||||
|
||||
//If no body and not interface or abstract/native
|
||||
if (bodyStart == bodyEnd && !isAbstract() && !isNative()) {
|
||||
setError(ErrorType.MISSING_BODY, "Method must have a body or be declared abstract/native.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate return statement types FIRST if we have a type resolver
|
||||
// This ensures type errors are shown even if there's also a missing return
|
||||
if (typeResolver != null) {
|
||||
validateReturnTypes(methodBodyText, typeResolver);
|
||||
}
|
||||
|
||||
// Validate return statement for non-void methods
|
||||
// This may set the main error, but return type errors are already recorded
|
||||
validateReturnStatement(methodBodyText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for duplicate and unresolved parameters.
|
||||
*/
|
||||
private void validateParameters() {
|
||||
Set<String> seenNames = new HashSet<>();
|
||||
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
FieldInfo param = parameters.get(i);
|
||||
String paramName = param.getName();
|
||||
|
||||
// Check for duplicate parameter names
|
||||
if (seenNames.contains(paramName)) {
|
||||
addParameterError(param, i, ErrorType.DUPLICATE_PARAMETER,
|
||||
"Duplicate parameter name '" + paramName + "'");
|
||||
} else {
|
||||
seenNames.add(paramName);
|
||||
}
|
||||
|
||||
// Check for unresolved parameter types
|
||||
TypeInfo paramType = param.getTypeInfo();
|
||||
if (paramType == null || !paramType.isResolved()) {
|
||||
String typeName = paramType != null ? paramType.getDisplayName() : "unknown";
|
||||
addParameterError(param, i, ErrorType.PARAMETER_UNDEFINED,
|
||||
"Cannot resolve parameter type '" + typeName + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a non-void method has a guaranteed return statement.
|
||||
* Delegates to {@link ControlFlowAnalyzer} for control flow analysis.
|
||||
*/
|
||||
private void validateReturnStatement(String bodyText) {
|
||||
if (bodyText == null || bodyText.isEmpty()) return;
|
||||
|
||||
// void methods don't need return statements
|
||||
if (TypeChecker.isVoidType(returnType)) return;
|
||||
|
||||
// Abstract/native methods don't have bodies
|
||||
if (isAbstract() || isNative()) return;
|
||||
|
||||
// Check if the method has a guaranteed return using the control flow analyzer
|
||||
if (!ControlFlowAnalyzer.hasGuaranteedReturn(bodyText)) {
|
||||
setError(ErrorType.MISSING_RETURN, "Missing return statement");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that all return statements have compatible types.
|
||||
* Delegates to {@link CodeParser} for parsing and {@link TypeChecker} for type checks.
|
||||
*/
|
||||
private void validateReturnTypes(String bodyText, TypeResolver typeResolver) {
|
||||
if (bodyText == null || bodyText.isEmpty()) return;
|
||||
|
||||
boolean isVoid = TypeChecker.isVoidType(returnType);
|
||||
String expectedTypeName = isVoid ? "void" : returnType.getDisplayName();
|
||||
|
||||
// Remove comments but keep strings (we need accurate positions)
|
||||
String cleanBody = CodeParser.removeComments(bodyText);
|
||||
|
||||
// Find all return statements
|
||||
int pos = 0;
|
||||
while (pos < cleanBody.length()) {
|
||||
// Look for "return" keyword
|
||||
int returnPos = CodeParser.findReturnKeyword(cleanBody, pos);
|
||||
if (returnPos < 0) break;
|
||||
|
||||
// Find the semicolon ending this return statement
|
||||
int semiPos = CodeParser.findReturnSemicolon(cleanBody, returnPos + 6);
|
||||
if (semiPos < 0) {
|
||||
pos = returnPos + 6;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract the return expression
|
||||
String returnExpr = cleanBody.substring(returnPos + 6, semiPos).trim();
|
||||
|
||||
// Calculate absolute position (bodyStart + 1 is after the opening brace)
|
||||
int absoluteReturnStart = bodyStart + 1 + returnPos;
|
||||
int absoluteSemiEnd = bodyStart + 1 + semiPos + 1; // +1 to include semicolon
|
||||
|
||||
// Check void method returning a value
|
||||
if (isVoid && !returnExpr.isEmpty()) {
|
||||
String message = "Cannot return a value from a method with void result type";
|
||||
addReturnStatementError(absoluteReturnStart, absoluteSemiEnd, message, returnType, null);
|
||||
}
|
||||
// Check non-void method return type compatibility
|
||||
else if (!isVoid && !returnExpr.isEmpty() && typeResolver != null) {
|
||||
// Resolve the expression type
|
||||
TypeInfo actualType = typeResolver.resolveExpression(returnExpr, absoluteReturnStart);
|
||||
|
||||
// Check type compatibility
|
||||
if (!TypeChecker.isTypeCompatible(returnType, actualType)) {
|
||||
String actualTypeName = actualType != null ? actualType.getSimpleName() : "null";
|
||||
String message = "Incompatible types.\nRequired: " + expectedTypeName + "\nFound: " + actualTypeName;
|
||||
addReturnStatementError(absoluteReturnStart, absoluteSemiEnd, message, returnType, actualType);
|
||||
}
|
||||
}
|
||||
|
||||
pos = semiPos + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.method;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a method signature for robust comparison.
|
||||
* Compares method name and parameter types as structured data, not as strings.
|
||||
*/
|
||||
public final class MethodSignature {
|
||||
private final String methodName;
|
||||
private final List<TypeInfo> parameterTypes;
|
||||
|
||||
public MethodSignature(String methodName, List<TypeInfo> parameterTypes) {
|
||||
this.methodName = methodName;
|
||||
this.parameterTypes = new ArrayList<>(parameterTypes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (!(obj instanceof MethodSignature))
|
||||
return false;
|
||||
|
||||
MethodSignature other = (MethodSignature) obj;
|
||||
|
||||
// Compare method names
|
||||
if (!this.methodName.equals(other.methodName))
|
||||
return false;
|
||||
|
||||
// Compare parameter count
|
||||
if (this.parameterTypes.size() != other.parameterTypes.size())
|
||||
return false;
|
||||
|
||||
// Compare each parameter type
|
||||
for (int i = 0; i < this.parameterTypes.size(); i++) {
|
||||
TypeInfo thisType = this.parameterTypes.get(i);
|
||||
TypeInfo otherType = other.parameterTypes.get(i);
|
||||
|
||||
// Both null = equal
|
||||
if (thisType == null && otherType == null)
|
||||
continue;
|
||||
|
||||
// One null, one not = not equal
|
||||
if (thisType == null || otherType == null)
|
||||
return false;
|
||||
|
||||
// Compare full type names
|
||||
if (!thisType.getFullName().equals(otherType.getFullName()))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = methodName.hashCode();
|
||||
for (TypeInfo paramType : parameterTypes) {
|
||||
result = 31 * result + (paramType != null ? paramType.getFullName().hashCode() : 0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder(methodName);
|
||||
sb.append("(");
|
||||
for (int i = 0; i < parameterTypes.size(); i++) {
|
||||
if (i > 0)
|
||||
sb.append(", ");
|
||||
TypeInfo type = parameterTypes.get(i);
|
||||
sb.append(type != null ? type.getFullName() : "?");
|
||||
}
|
||||
sb.append(")");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String asString(Method javaMethod) {
|
||||
StringBuilder sig = new StringBuilder(javaMethod.getName());
|
||||
sig.append("(");
|
||||
Class<?>[] paramTypes = javaMethod.getParameterTypes();
|
||||
for (int i = 0; i < paramTypes.length; i++) {
|
||||
if (i > 0)
|
||||
sig.append(", ");
|
||||
sig.append(paramTypes[i].getSimpleName());
|
||||
}
|
||||
sig.append(")");
|
||||
return sig.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* <h1>Interpreter Package - Java Syntax Highlighting System</h1>
|
||||
*
|
||||
* <p>This package provides a clean, modular rewrite of the JavaTextContainer
|
||||
* syntax highlighting system. It replaces the original implementation with
|
||||
* well-structured, atomic classes that work together like LEGO pieces.</p>
|
||||
*
|
||||
* <h2>Architecture Overview</h2>
|
||||
*
|
||||
* <pre>
|
||||
* ┌─────────────────────────────────────────────────────────────────┐
|
||||
* │ ScriptTextContainer │
|
||||
* │ (Compatibility adapter extending JavaTextContainer) │
|
||||
* │ - USE_NEW_INTERPRETER flag to toggle between systems │
|
||||
* │ - Produces JavaTextContainer.LineData/Token for GUI compat │
|
||||
* └───────────────────────┬─────────────────────────────────────────┘
|
||||
* │
|
||||
* ▼
|
||||
* ┌─────────────────────────────────────────────────────────────────┐
|
||||
* │ ScriptDocument │
|
||||
* │ (Main document container - replaces JavaTextContainer core) │
|
||||
* │ │
|
||||
* │ 7-Phase Tokenization Pipeline: │
|
||||
* │ 1. findExcludedRanges() - strings/comments │
|
||||
* │ 2. parseImports() - import statements │
|
||||
* │ 3. parseStructure() - methods, fields │
|
||||
* │ 4. buildMarks() - all highlighting marks │
|
||||
* │ 5. resolveConflicts() - priority-based resolution │
|
||||
* │ 6. buildTokens() - convert marks to tokens per line │
|
||||
* │ 7. computeIndentGuides() - visual indent guides │
|
||||
* └───────────────────────┬─────────────────────────────────────────┘
|
||||
* │
|
||||
* ▼
|
||||
* ┌─────────────────────────────────────────────────────────────────┐
|
||||
* │ ScriptLine │
|
||||
* │ (Line container with token management and rendering) │
|
||||
* │ - Token list with navigation │
|
||||
* │ - Indent guides │
|
||||
* │ - drawString() with color codes │
|
||||
* │ - drawStringHex() for direct hex color rendering │
|
||||
* └───────────────────────┬─────────────────────────────────────────┘
|
||||
* │
|
||||
* ▼
|
||||
* ┌─────────────────────────────────────────────────────────────────┐
|
||||
* │ Token │
|
||||
* │ (Atomic token unit with type-specific metadata) │
|
||||
* │ - Text, start/end offsets │
|
||||
* │ - TokenType (with hex color and priority) │
|
||||
* │ - prev()/next() navigation │
|
||||
* │ - Type-specific metadata: TypeInfo, FieldInfo, MethodInfo │
|
||||
* └─────────────────────────────────────────────────────────────────┘
|
||||
* </pre>
|
||||
*
|
||||
* <h2>Supporting Classes</h2>
|
||||
*
|
||||
* <ul>
|
||||
* <li><b>TokenType</b> - Enum with hex colors and priorities for all token types</li>
|
||||
* <li><b>TypeInfo</b> - Immutable type metadata (class/interface/enum)</li>
|
||||
* <li><b>TypeResolver</b> - Class resolution with caching (replaces ClassPathFinder)</li>
|
||||
* <li><b>ImportData</b> - Import statement tracking with resolution status</li>
|
||||
* <li><b>FieldInfo</b> - Field/variable metadata with scope (global/local/parameter)</li>
|
||||
* <li><b>MethodInfo</b> - Method declaration/call metadata with parameters</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Key Improvements Over Original</h2>
|
||||
*
|
||||
* <ol>
|
||||
* <li><b>Single-pass tokenization</b> - 7 distinct phases vs multiple loops</li>
|
||||
* <li><b>Token navigation</b> - prev()/next() methods for easy traversal</li>
|
||||
* <li><b>Type-specific metadata</b> - Rich information attached to tokens</li>
|
||||
* <li><b>Hex color support</b> - Direct RGB colors in addition to MC color codes</li>
|
||||
* <li><b>Clean separation</b> - Each class has one responsibility</li>
|
||||
* <li><b>Immutable data</b> - TypeInfo, FieldInfo, MethodInfo are immutable</li>
|
||||
* <li><b>Feature toggle</b> - USE_NEW_INTERPRETER flag for safe rollback</li>
|
||||
* </ol>
|
||||
*
|
||||
* <h2>Usage</h2>
|
||||
*
|
||||
* <pre>{@code
|
||||
* // The new system is automatically used via ScriptTextContainer
|
||||
* // To disable and use original JavaTextContainer:
|
||||
* ScriptTextContainer.USE_NEW_INTERPRETER = false;
|
||||
*
|
||||
* // To access the underlying ScriptDocument:
|
||||
* ScriptTextContainer container = (ScriptTextContainer) textArea.getContainer();
|
||||
* ScriptDocument doc = container.getDocument();
|
||||
*
|
||||
* // Token navigation example:
|
||||
* for (ScriptLine line : doc.getLines()) {
|
||||
* for (Token token : line.getTokens()) {
|
||||
* Token next = token.next();
|
||||
* Token prev = token.prev();
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* @since 2.0
|
||||
* @see ScriptTextContainer
|
||||
* @see ScriptDocument
|
||||
* @see Token
|
||||
*/
|
||||
package noppes.npcs.client.gui.util.script.interpreter;
|
||||
@@ -0,0 +1,271 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.token;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.ImportData;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.ScriptLine;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
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.method.MethodCallInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
|
||||
|
||||
/**
|
||||
* Represents a single token in the source code with its type and metadata.
|
||||
* Tokens are the atomic units of the syntax highlighting system.
|
||||
*
|
||||
* Each token knows:
|
||||
* - Its text content
|
||||
* - Its position in the global source
|
||||
* - Its type (for coloring)
|
||||
* - Type-specific metadata (resolved class info, declaration info, etc.)
|
||||
*/
|
||||
public class Token {
|
||||
|
||||
private final String text;
|
||||
private final int globalStart; // start offset in the full document
|
||||
private final int globalEnd; // end offset in the full document
|
||||
private TokenType type;
|
||||
|
||||
// Optional metadata based on token type
|
||||
private TypeInfo typeInfo; // For class references, type declarations
|
||||
private FieldInfo fieldInfo; // For field references
|
||||
private MethodInfo methodInfo; // For method calls/declarations
|
||||
private MethodCallInfo methodCallInfo; // For method calls with argument info
|
||||
private ImportData importData; // For import statements
|
||||
private FieldAccessInfo fieldAccessInfo; //For fields like Minecraft.getMinecraft.thePlayer
|
||||
private TokenErrorMessage errorMessage;
|
||||
|
||||
|
||||
// Navigation - set by ScriptLine
|
||||
private Token prev;
|
||||
private Token next;
|
||||
private ScriptLine parentLine;
|
||||
|
||||
public Token(String text, int globalStart, int globalEnd, TokenType type) {
|
||||
this.text = text;
|
||||
this.globalStart = globalStart;
|
||||
this.globalEnd = globalEnd;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
// ==================== FACTORY METHODS ====================
|
||||
|
||||
public static Token defaultToken(String text, int start, int end) {
|
||||
return new Token(text, start, end, TokenType.DEFAULT);
|
||||
}
|
||||
|
||||
public static Token keyword(String text, int start, int end) {
|
||||
return new Token(text, start, end, TokenType.KEYWORD);
|
||||
}
|
||||
|
||||
public static Token modifier(String text, int start, int end) {
|
||||
return new Token(text, start, end, TokenType.MODIFIER);
|
||||
}
|
||||
|
||||
public static Token comment(String text, int start, int end) {
|
||||
return new Token(text, start, end, TokenType.COMMENT);
|
||||
}
|
||||
|
||||
public static Token string(String text, int start, int end) {
|
||||
return new Token(text, start, end, TokenType.STRING);
|
||||
}
|
||||
|
||||
public static Token literal(String text, int start, int end) {
|
||||
return new Token(text, start, end, TokenType.LITERAL);
|
||||
}
|
||||
|
||||
public static Token typeReference(String text, int start, int end, TypeInfo info) {
|
||||
Token t = new Token(text, start, end, info != null ? info.getTokenType() : TokenType.TYPE_DECL);
|
||||
t.typeInfo = info;
|
||||
return t;
|
||||
}
|
||||
|
||||
public static Token methodDecl(String text, int start, int end, MethodInfo info) {
|
||||
Token t = new Token(text, start, end, TokenType.METHOD_DECL);
|
||||
t.methodInfo = info;
|
||||
return t;
|
||||
}
|
||||
|
||||
public static Token methodCall(String text, int start, int end, MethodInfo info, boolean resolved) {
|
||||
Token t = new Token(text, start, end, resolved ? TokenType.METHOD_CALL : TokenType.DEFAULT);
|
||||
t.methodInfo = info;
|
||||
return t;
|
||||
}
|
||||
|
||||
public static Token globalField(String text, int start, int end, FieldInfo info) {
|
||||
Token t = new Token(text, start, end, TokenType.GLOBAL_FIELD);
|
||||
t.fieldInfo = info;
|
||||
return t;
|
||||
}
|
||||
|
||||
public static Token localField(String text, int start, int end, FieldInfo info) {
|
||||
Token t = new Token(text, start, end, TokenType.LOCAL_FIELD);
|
||||
t.fieldInfo = info;
|
||||
return t;
|
||||
}
|
||||
|
||||
public static Token parameter(String text, int start, int end, FieldInfo info) {
|
||||
Token t = new Token(text, start, end, TokenType.PARAMETER);
|
||||
t.fieldInfo = info;
|
||||
return t;
|
||||
}
|
||||
|
||||
public static Token undefined(String text, int start, int end) {
|
||||
return new Token(text, start, end, TokenType.UNDEFINED_VAR);
|
||||
}
|
||||
|
||||
// ==================== GETTERS ====================
|
||||
|
||||
public String getText() { return text; }
|
||||
public int getGlobalStart() { return globalStart; }
|
||||
public int getGlobalEnd() { return globalEnd; }
|
||||
public int getLength() { return globalEnd - globalStart; }
|
||||
public TokenType getType() { return type; }
|
||||
public TypeInfo getTypeInfo() { return typeInfo; }
|
||||
public FieldInfo getFieldInfo() { return fieldInfo; }
|
||||
public MethodInfo getMethodInfo() { return methodInfo; }
|
||||
public MethodCallInfo getMethodCallInfo() { return methodCallInfo; }
|
||||
public FieldAccessInfo getFieldAccessInfo() { return fieldAccessInfo; }
|
||||
public ImportData getImportData() { return importData; }
|
||||
public TokenErrorMessage getErrorMessage() { return errorMessage; }
|
||||
public ScriptLine getParentLine() { return parentLine; }
|
||||
|
||||
// ==================== SETTERS ====================
|
||||
|
||||
public void setType(TokenType type) { this.type = type; }
|
||||
public void setTypeInfo(TypeInfo info) { this.typeInfo = info; }
|
||||
public void setFieldInfo(FieldInfo info) { this.fieldInfo = info; }
|
||||
public void setFieldAccessInfo(FieldAccessInfo info) { this.fieldAccessInfo = info; }
|
||||
public void setMethodInfo(MethodInfo info) { this.methodInfo = info; }
|
||||
public void setMethodCallInfo(MethodCallInfo info) { this.methodCallInfo = info; }
|
||||
public void setImportData(ImportData data) { this.importData = data; }
|
||||
public void setErrorMessage(TokenErrorMessage message) { this.errorMessage = message; }
|
||||
|
||||
public void setParentLine(ScriptLine line) { this.parentLine = line; }
|
||||
public void setPrev(Token prev) { this.prev = prev; }
|
||||
public void setNext(Token next) { this.next = next; }
|
||||
|
||||
// ==================== NAVIGATION ====================
|
||||
|
||||
/**
|
||||
* Get the previous token (may be on a previous line).
|
||||
*/
|
||||
public Token prev() {
|
||||
if (prev != null) return prev;
|
||||
if (parentLine == null) return null;
|
||||
|
||||
ScriptLine prevLine = parentLine.prev();
|
||||
while (prevLine != null) {
|
||||
Token last = prevLine.getLastToken();
|
||||
if (last != null) return last;
|
||||
prevLine = prevLine.prev();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next token (may be on a following line).
|
||||
*/
|
||||
public Token next() {
|
||||
if (next != null) return next;
|
||||
if (parentLine == null) return null;
|
||||
|
||||
ScriptLine nextLine = parentLine.next();
|
||||
while (nextLine != null) {
|
||||
Token first = nextLine.getFirstToken();
|
||||
if (first != null) return first;
|
||||
nextLine = nextLine.next();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the previous token on the same line only.
|
||||
*/
|
||||
public Token prevOnLine() {
|
||||
return prev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next token on the same line only.
|
||||
*/
|
||||
public Token nextOnLine() {
|
||||
return next;
|
||||
}
|
||||
|
||||
// ==================== TYPE CHECKS ====================
|
||||
|
||||
public boolean isKeyword() {
|
||||
return type == TokenType.KEYWORD || type == TokenType.MODIFIER ||
|
||||
type == TokenType.CLASS_KEYWORD || type == TokenType.IMPORT_KEYWORD;
|
||||
}
|
||||
|
||||
public boolean isTypeReference() {
|
||||
return type == TokenType.TYPE_DECL || type == TokenType.IMPORTED_CLASS ||
|
||||
type == TokenType.CLASS_DECL || type == TokenType.INTERFACE_DECL ||
|
||||
type == TokenType.ENUM_DECL;
|
||||
}
|
||||
|
||||
public boolean isEnumConstant() {
|
||||
return type == TokenType.ENUM_CONSTANT;
|
||||
}
|
||||
public boolean isResolved() {
|
||||
if (typeInfo != null) return typeInfo.isResolved();
|
||||
if (fieldInfo != null) return fieldInfo.isResolved();
|
||||
if (methodInfo != null) return methodInfo.isResolved();
|
||||
return type != TokenType.UNDEFINED_VAR;
|
||||
}
|
||||
|
||||
public boolean isIdentifier() {
|
||||
if (text.isEmpty()) return false;
|
||||
char first = text.charAt(0);
|
||||
return Character.isJavaIdentifierStart(first);
|
||||
}
|
||||
|
||||
public boolean startsWithUpperCase() {
|
||||
return !text.isEmpty() && Character.isUpperCase(text.charAt(0));
|
||||
}
|
||||
|
||||
public boolean isMethodCall() {
|
||||
return type == TokenType.METHOD_CALL ||
|
||||
(type == TokenType.DEFAULT && methodInfo != null);
|
||||
}
|
||||
|
||||
public boolean isField() {
|
||||
return type == TokenType.GLOBAL_FIELD || type == TokenType.LOCAL_FIELD ||
|
||||
type == TokenType.PARAMETER || type == TokenType.STATIC_FINAL_FIELD ||
|
||||
type == TokenType.ENUM_CONSTANT;
|
||||
}
|
||||
|
||||
// ==================== RENDERING ====================
|
||||
|
||||
/**
|
||||
* Get the hex color for this token.
|
||||
*/
|
||||
public int getHexColor() {
|
||||
return type.getHexColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Minecraft color code character.
|
||||
*/
|
||||
public char getColorCode() {
|
||||
return type.toColorCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Minecraft style prefix for this token (bold/italic codes).
|
||||
* Returns empty string if no style, otherwise §l for bold, §o for italic, or both.
|
||||
*/
|
||||
public String getStylePrefix() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (type.isBold()) sb.append('\u00A7').append('l');
|
||||
if (type.isItalic()) sb.append('\u00A7').append('o');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Token{'" + text + "', " + type + ", [" + globalStart + "-" + globalEnd + "]}";
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.token;
|
||||
|
||||
public class TokenErrorMessage {
|
||||
private final String message;
|
||||
public boolean clearOtherErrors;
|
||||
|
||||
public TokenErrorMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public static TokenErrorMessage from(String message) {
|
||||
return new TokenErrorMessage(message);
|
||||
}
|
||||
|
||||
public TokenErrorMessage clearOtherErrors() {
|
||||
this.clearOtherErrors = true;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.token;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
|
||||
/**
|
||||
* Defines all token types for syntax highlighting with hex colors and priorities.
|
||||
* Priority determines which token type wins when marks overlap.
|
||||
* Higher priority = wins conflicts.
|
||||
*/
|
||||
public enum TokenType {
|
||||
// Comments and strings have highest priority - they override everything inside them
|
||||
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
|
||||
CLASS_KEYWORD(0xFF5555, 115), // 'class', 'interface', 'enum' keywords
|
||||
IMPORT_KEYWORD(0xFFAA00, 110), // 'import' keyword
|
||||
KEYWORD(0xFF5555, 100), // control flow: if, else, for, while, etc.
|
||||
MODIFIER(0xFFAA00, 90), // public, private, static, final, etc.
|
||||
|
||||
// Type declarations and references
|
||||
INTERFACE_DECL(0x55FFFF, 85), // interface names (aqua)
|
||||
ENUM_DECL(0xFF55FF, 85), // enum names (magenta)
|
||||
ENUM_CONSTANT(0x55FFFF, 84, true, false), // enum constant values (blue, bold+italic) - like IntelliJ
|
||||
CLASS_DECL(0x00AAAA, 85), // class names in declarations
|
||||
IMPORTED_CLASS(0x00AAAA, 75), // imported class usages
|
||||
TYPE_DECL(0x00AAAA, 70), // package paths, type references
|
||||
|
||||
// Methods
|
||||
METHOD_DECL(0x00AA00, 60), // method declarations (green)
|
||||
METHOD_CALL(0x55FF55, 50), // method calls (bright green)
|
||||
|
||||
// Variables and fields
|
||||
UNDEFINED_VAR(0xAA0000, 20), // unresolved variables (dark red) - high priority
|
||||
PARAMETER(0x5555FF, 36), // method parameters (blue)
|
||||
GLOBAL_FIELD(0x55FFFF, 35), // class-level fields (aqua)
|
||||
LOCAL_FIELD(0xFFFF55, 25), // local variables (yellow)
|
||||
STATIC_FINAL_FIELD(0xFF55FF, 36, false, true), // static final fields (magenta, italic)
|
||||
|
||||
// Literals
|
||||
LITERAL(0x777777, 40), // numeric and boolean literals
|
||||
|
||||
// Default
|
||||
VARIABLE(0xFFFFFF, 30), // generic variables
|
||||
DEFAULT(0xFFFFFF, 0); // default text color (white)
|
||||
|
||||
private final int hexColor;
|
||||
private final int priority;
|
||||
private final boolean bold;
|
||||
private final boolean italic;
|
||||
|
||||
TokenType(int hexColor, int priority) {
|
||||
this(hexColor, priority, false, false);
|
||||
}
|
||||
|
||||
TokenType(int hexColor, int priority, boolean bold, boolean italic) {
|
||||
this.hexColor = hexColor;
|
||||
this.priority = priority;
|
||||
this.bold = bold;
|
||||
this.italic = italic;
|
||||
}
|
||||
|
||||
public int getHexColor() {
|
||||
return hexColor;
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
public boolean isBold() {
|
||||
return bold;
|
||||
}
|
||||
|
||||
public boolean isItalic() {
|
||||
return italic;
|
||||
}
|
||||
|
||||
public static TokenType getByType(TypeInfo typeInfo) {
|
||||
if (typeInfo == null || !typeInfo.isResolved())
|
||||
return TokenType.UNDEFINED_VAR;
|
||||
|
||||
if ("any".equals(typeInfo.getFullName()))
|
||||
return TokenType.KEYWORD;
|
||||
|
||||
// Use the TypeInfo's own token type, which handles ScriptTypeInfo correctly
|
||||
return typeInfo.getTokenType();
|
||||
}
|
||||
|
||||
public static int getColor(TypeInfo typeInfo) {
|
||||
return getByType(typeInfo).getHexColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this token type to a Minecraft color code character.
|
||||
* Used for backward compatibility with the existing rendering system.
|
||||
*/
|
||||
public char toColorCode() {
|
||||
switch (this) {
|
||||
case COMMENT:
|
||||
case LITERAL:
|
||||
case UNUSED_IMPORT:
|
||||
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
|
||||
case IMPORT_KEYWORD:
|
||||
case MODIFIER:
|
||||
return '6'; // gold
|
||||
case ENUM_DECL:
|
||||
case STATIC_FINAL_FIELD:
|
||||
return 'd'; // magenta
|
||||
case ENUM_CONSTANT:
|
||||
return '9'; // blue (same as PARAMETER)
|
||||
case INTERFACE_DECL:
|
||||
case GLOBAL_FIELD:
|
||||
return 'b'; // aqua
|
||||
case CLASS_DECL:
|
||||
case IMPORTED_CLASS:
|
||||
case TYPE_DECL:
|
||||
return '3'; // dark aqua
|
||||
case METHOD_DECL:
|
||||
return '2'; // dark green
|
||||
case METHOD_CALL:
|
||||
return 'a'; // green
|
||||
case LOCAL_FIELD:
|
||||
return 'e'; // yellow
|
||||
case PARAMETER:
|
||||
return '9'; // blue
|
||||
case UNDEFINED_VAR:
|
||||
return '4'; // dark red
|
||||
case VARIABLE:
|
||||
case DEFAULT:
|
||||
default:
|
||||
return 'f'; // white
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Example Usage of ClassIndex:
|
||||
*
|
||||
* 1. Add a single class:
|
||||
* ClassIndex.getInstance().addClass(EntityPlayer.class);
|
||||
*
|
||||
* 2. Add an entire package recursively:
|
||||
* ClassIndex.getInstance().addPackage("net.minecraft.entity.player");
|
||||
* This will scan and add ALL classes in that package and subpackages.
|
||||
*
|
||||
* 3. Find classes by prefix:
|
||||
* List<String> matches = ClassIndex.getInstance().findByPrefix("Entity", 50);
|
||||
* // Returns fully-qualified names like "net.minecraft.entity.Entity", etc.
|
||||
*
|
||||
* Benefits:
|
||||
* - No more manual string pairs! Just pass Class<?> objects.
|
||||
* - Simple name and package are extracted automatically via reflection.
|
||||
* - Can scan entire package trees recursively with one call.
|
||||
* - Cleaner, more maintainable, and easier to extend.
|
||||
*/
|
||||
@@ -0,0 +1,326 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.type;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
/**
|
||||
* Index of Java classes for autocomplete suggestions.
|
||||
* Uses reflection to automatically extract class names and packages.
|
||||
* Supports multiple classes with the same simple name (e.g., java.util.Date and java.sql.Date).
|
||||
*/
|
||||
public class ClassIndex {
|
||||
private static ClassIndex instance;
|
||||
|
||||
// Map simple names to multiple classes (handles name collisions)
|
||||
private final Map<String, List<Class<?>>> simpleNameToClasses = new LinkedHashMap<>();
|
||||
private final Set<Class<?>> registeredClasses = new HashSet<>();
|
||||
|
||||
private ClassIndex() {
|
||||
// Initialize with common classes
|
||||
initializeCommonClasses();
|
||||
}
|
||||
|
||||
public static ClassIndex getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new ClassIndex();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static void init(){
|
||||
getInstance();
|
||||
}
|
||||
/**
|
||||
* Register a single class by its Class<?> object.
|
||||
* The simple name and full name are extracted via reflection.
|
||||
* Supports multiple classes with the same simple name.
|
||||
*/
|
||||
public void addClass(Class<?> clazz) {
|
||||
if (clazz == null) return;
|
||||
|
||||
// Check if already registered using the Class<?> object itself
|
||||
if (registeredClasses.contains(clazz)) {
|
||||
return; // Already registered
|
||||
}
|
||||
|
||||
registeredClasses.add(clazz);
|
||||
String simpleName = clazz.getSimpleName();
|
||||
|
||||
// Only add if simple name is not empty (avoid inner classes with $ in name)
|
||||
if (!simpleName.isEmpty() && !simpleName.contains("$")) {
|
||||
// Add to list of classes with this simple name
|
||||
simpleNameToClasses.computeIfAbsent(simpleName, k -> new ArrayList<>()).add(clazz);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all classes in a package and all subpackages recursively.
|
||||
* Uses ClassLoader to scan the classpath.
|
||||
*/
|
||||
public void addPackage(String packageName) {
|
||||
addPackage(packageName, new String[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all classes in a package and all subpackages recursively,
|
||||
* excluding specified packages.
|
||||
* Uses ClassLoader to scan the classpath.
|
||||
*
|
||||
* @param packageName The base package to scan
|
||||
* @param excludedPackages Array of package names to exclude (including their subpackages)
|
||||
*/
|
||||
public void addPackage(String packageName, String... excludedPackages) {
|
||||
try {
|
||||
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
|
||||
String path = packageName.replace('.', '/');
|
||||
Enumeration<URL> resources = classLoader.getResources(path);
|
||||
|
||||
Set<String> classNames = new HashSet<>();
|
||||
Set<String> excludedPrefixes = new HashSet<>();
|
||||
|
||||
// Prepare excluded package prefixes for matching
|
||||
for (String excluded : excludedPackages) {
|
||||
if (excluded != null && !excluded.isEmpty()) {
|
||||
excludedPrefixes.add(excluded + ".");
|
||||
}
|
||||
}
|
||||
|
||||
while (resources.hasMoreElements()) {
|
||||
URL resource = resources.nextElement();
|
||||
|
||||
if (resource.getProtocol().equals("file")) {
|
||||
// Scan directory
|
||||
File directory = new File(resource.getFile());
|
||||
scanDirectory(directory, packageName, classNames, excludedPrefixes);
|
||||
} else if (resource.getProtocol().equals("jar")) {
|
||||
// Scan JAR file
|
||||
String jarPath = resource.getPath();
|
||||
if (jarPath.startsWith("file:")) {
|
||||
jarPath = jarPath.substring(5);
|
||||
}
|
||||
int separatorIndex = jarPath.indexOf("!");
|
||||
if (separatorIndex != -1) {
|
||||
jarPath = jarPath.substring(0, separatorIndex);
|
||||
}
|
||||
scanJar(jarPath, packageName, classNames, excludedPrefixes);
|
||||
}
|
||||
}
|
||||
|
||||
// Load all discovered classes
|
||||
for (String className : classNames) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(className, false, classLoader);
|
||||
addClass(clazz);
|
||||
} catch (ClassNotFoundException | NoClassDefFoundError | ExceptionInInitializerError e) {
|
||||
// Skip classes that can't be loaded
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Silently fail - package might not exist
|
||||
}
|
||||
}
|
||||
|
||||
private void scanDirectory(File directory, String packageName, Set<String> classNames) {
|
||||
scanDirectory(directory, packageName, classNames, new HashSet<>());
|
||||
}
|
||||
|
||||
private void scanDirectory(File directory, String packageName, Set<String> classNames, Set<String> excludedPrefixes) {
|
||||
if (!directory.exists() || !directory.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this package is excluded
|
||||
for (String excludedPrefix : excludedPrefixes) {
|
||||
if ((packageName + ".").startsWith(excludedPrefix) || packageName.equals(excludedPrefix.substring(0, excludedPrefix.length() - 1))) {
|
||||
return; // Skip excluded package
|
||||
}
|
||||
}
|
||||
|
||||
File[] files = directory.listFiles();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (File file : files) {
|
||||
String fileName = file.getName();
|
||||
|
||||
if (file.isDirectory()) {
|
||||
// Recursively scan subdirectory
|
||||
scanDirectory(file, packageName + "." + fileName, classNames, excludedPrefixes);
|
||||
} else if (fileName.endsWith(".class")) {
|
||||
// Add class name
|
||||
String className = packageName + "." + fileName.substring(0, fileName.length() - 6);
|
||||
classNames.add(className);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void scanJar(String jarPath, String packageName, Set<String> classNames) {
|
||||
scanJar(jarPath, packageName, classNames, new HashSet<>());
|
||||
}
|
||||
|
||||
private void scanJar(String jarPath, String packageName, Set<String> classNames, Set<String> excludedPrefixes) {
|
||||
try {
|
||||
JarFile jarFile = new JarFile(jarPath);
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
String packagePath = packageName.replace('.', '/');
|
||||
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
String entryName = entry.getName();
|
||||
|
||||
if (entryName.startsWith(packagePath) && entryName.endsWith(".class")) {
|
||||
// Convert path to class name
|
||||
String className = entryName
|
||||
.substring(0, entryName.length() - 6)
|
||||
.replace('/', '.');
|
||||
|
||||
// Check if this class is in an excluded package
|
||||
boolean excluded = false;
|
||||
for (String excludedPrefix : excludedPrefixes) {
|
||||
if ((className + ".").startsWith(excludedPrefix) ||
|
||||
className.startsWith(excludedPrefix.substring(0, excludedPrefix.length() - 1) + ".")) {
|
||||
excluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!excluded) {
|
||||
classNames.add(className);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jarFile.close();
|
||||
} catch (Exception e) {
|
||||
// Silently fail
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all classes whose simple name starts with the given prefix (case-insensitive).
|
||||
* Returns fully-qualified class names for all matches, including multiple classes with the same simple name.
|
||||
*/
|
||||
public List<String> findByPrefix(String prefix, int maxResults) {
|
||||
List<String> results = new ArrayList<>();
|
||||
String lowerPrefix = prefix.toLowerCase();
|
||||
|
||||
for (Map.Entry<String, List<Class<?>>> entry : simpleNameToClasses.entrySet()) {
|
||||
if (entry.getKey().toLowerCase().startsWith(lowerPrefix)) {
|
||||
// Add all classes with this simple name
|
||||
for (Class<?> clazz : entry.getValue()) {
|
||||
results.add(clazz.getName());
|
||||
|
||||
if (maxResults != -1 && results.size() >= maxResults) {
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the index with commonly used Java, Minecraft, and NPC classes.
|
||||
*/
|
||||
private void initializeCommonClasses() {
|
||||
// Java standard library
|
||||
addClass(String.class);
|
||||
addClass(Integer.class);
|
||||
addClass(Double.class);
|
||||
addClass(Float.class);
|
||||
addClass(Long.class);
|
||||
addClass(Boolean.class);
|
||||
addClass(Character.class);
|
||||
addClass(Byte.class);
|
||||
addClass(Short.class);
|
||||
addClass(Object.class);
|
||||
addClass(Math.class);
|
||||
addClass(System.class);
|
||||
addClass(Thread.class);
|
||||
addClass(Runnable.class);
|
||||
addClass(Exception.class);
|
||||
addClass(RuntimeException.class);
|
||||
addClass(Error.class);
|
||||
addClass(Throwable.class);
|
||||
|
||||
// Java collections
|
||||
addClass(List.class);
|
||||
addClass(ArrayList.class);
|
||||
addClass(LinkedList.class);
|
||||
addClass(Set.class);
|
||||
addClass(HashSet.class);
|
||||
addClass(LinkedHashSet.class);
|
||||
addClass(TreeSet.class);
|
||||
addClass(Map.class);
|
||||
addClass(HashMap.class);
|
||||
addClass(LinkedHashMap.class);
|
||||
addClass(TreeMap.class);
|
||||
addClass(Collection.class);
|
||||
addClass(Iterator.class);
|
||||
addClass(Comparator.class);
|
||||
addClass(Collections.class);
|
||||
addClass(Arrays.class);
|
||||
addClass(Queue.class);
|
||||
addClass(Deque.class);
|
||||
addClass(Stack.class);
|
||||
addClass(Vector.class);
|
||||
addClass(Hashtable.class);
|
||||
|
||||
// Java I/O
|
||||
addClass(java.io.File.class);
|
||||
addClass(java.io.InputStream.class);
|
||||
addClass(java.io.OutputStream.class);
|
||||
addClass(java.io.Reader.class);
|
||||
addClass(java.io.Writer.class);
|
||||
addClass(java.io.BufferedReader.class);
|
||||
addClass(java.io.BufferedWriter.class);
|
||||
addClass(java.io.FileReader.class);
|
||||
addClass(java.io.FileWriter.class);
|
||||
addClass(java.io.IOException.class);
|
||||
|
||||
// Java utilities
|
||||
addClass(java.util.Random.class);
|
||||
addClass(java.util.Date.class);
|
||||
addClass(java.util.Calendar.class);
|
||||
addClass(java.util.UUID.class);
|
||||
addClass(java.util.regex.Pattern.class);
|
||||
addClass(java.util.regex.Matcher.class);
|
||||
addPackage("java.util.function");
|
||||
|
||||
// Now add common Minecraft/Forge/NPC packages
|
||||
addPackage("net.minecraft.entity");
|
||||
addPackage("net.minecraft.item");
|
||||
addPackage("net.minecraft.block");
|
||||
addPackage("net.minecraft.world");
|
||||
addPackage("net.minecraft.util");
|
||||
addPackage("net.minecraft.nbt");
|
||||
addPackage("net.minecraft.potion");
|
||||
addPackage("net.minecraft.enchantment");
|
||||
addPackage("net.minecraft.inventory");
|
||||
addPackage("net.minecraft.tileentity");
|
||||
addPackage("net.minecraft.command");
|
||||
addPackage("net.minecraft.client");
|
||||
|
||||
// Forge
|
||||
addPackage("net.minecraftforge.common");
|
||||
addPackage("net.minecraftforge.event");
|
||||
addPackage("net.minecraftforge.fml.common");
|
||||
|
||||
// CustomNPCs
|
||||
addPackage("noppes.npcs");
|
||||
|
||||
// DBC (if available)
|
||||
try {
|
||||
addPackage("JinRyuu.JRMCore");
|
||||
addPackage("JinRyuu.DragonBC");
|
||||
addPackage("kamkeel.npcdbc");
|
||||
} catch (Exception e) {
|
||||
// DBC might not be available
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.type;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
|
||||
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a reference to a Java Class itself (not an instance).
|
||||
* Used for patterns like: var File = Java.type("java.io.File");
|
||||
*
|
||||
* When you have a ClassTypeInfo:
|
||||
* - Accessing members should show ONLY static members (File.listRoots())
|
||||
* - Using 'new' should return an instance of the wrapped class (new File("path"))
|
||||
* - Calling .constructor(...) should return an instance of the wrapped class
|
||||
*
|
||||
* This is distinct from a regular TypeInfo which represents an instance of that type.
|
||||
*/
|
||||
public class ClassTypeInfo extends TypeInfo {
|
||||
|
||||
private final TypeInfo instanceType; // The type you get when you do 'new' on this class
|
||||
private List<MethodInfo> constructorMethod;
|
||||
private List<FieldInfo> classField;
|
||||
|
||||
/**
|
||||
* Create a ClassTypeInfo wrapping a Java class.
|
||||
*
|
||||
* @param javaClass The Java class this represents (e.g., java.io.File.class)
|
||||
*/
|
||||
public ClassTypeInfo(Class<?> javaClass) {
|
||||
super(
|
||||
javaClass.getSimpleName(),
|
||||
javaClass.getName(),
|
||||
javaClass.getPackage() != null ? javaClass.getPackage().getName() : "",
|
||||
javaClass.isInterface() ? Kind.INTERFACE : (javaClass.isEnum() ? Kind.ENUM : Kind.CLASS),
|
||||
javaClass,
|
||||
true,
|
||||
null,
|
||||
true // subclass marker
|
||||
);
|
||||
this.instanceType = TypeInfo.fromClass(javaClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ClassTypeInfo from an existing TypeInfo.
|
||||
*/
|
||||
public ClassTypeInfo(TypeInfo instanceType) {
|
||||
super(
|
||||
instanceType.getSimpleName(),
|
||||
instanceType.getFullName(),
|
||||
instanceType.getPackageName(),
|
||||
instanceType.getKind(),
|
||||
instanceType.getJavaClass(),
|
||||
instanceType.isResolved(),
|
||||
instanceType.getEnclosingType(),
|
||||
true // subclass marker
|
||||
);
|
||||
this.instanceType = instanceType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true - this TypeInfo represents a class reference, not an instance.
|
||||
*/
|
||||
@Override
|
||||
public boolean isClassReference() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the TypeInfo for instances of this class.
|
||||
* This is what you get when you call 'new' on this class.
|
||||
*/
|
||||
public TypeInfo getInstanceType() {
|
||||
return instanceType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MethodInfo> getSyntheticMethods() {
|
||||
List<MethodInfo> base = super.getSyntheticMethods();
|
||||
|
||||
// if (constructorMethod == null) {
|
||||
constructorMethod = buildConstructorMethod();
|
||||
//}
|
||||
|
||||
if (constructorMethod.isEmpty()) {
|
||||
return base;
|
||||
}
|
||||
|
||||
List<MethodInfo> merged = new ArrayList<>(base.size() + constructorMethod.size());
|
||||
merged.addAll(base);
|
||||
merged.addAll(constructorMethod);
|
||||
return merged;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FieldInfo> getSyntheticFields() {
|
||||
List<FieldInfo> base = super.getSyntheticFields();
|
||||
|
||||
if (classField == null) {
|
||||
classField = buildClassField();
|
||||
}
|
||||
|
||||
if (classField.isEmpty()) {
|
||||
return base;
|
||||
}
|
||||
|
||||
List<FieldInfo> merged = new ArrayList<>(base.size() + classField.size());
|
||||
merged.addAll(base);
|
||||
merged.addAll(classField);
|
||||
return merged;
|
||||
}
|
||||
|
||||
private List<MethodInfo> buildConstructorMethod() {
|
||||
if (instanceType == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
FieldInfo argsParam = FieldInfo.parameter("args", TypeInfo.OBJECT, -1, null);
|
||||
argsParam.setVarArg(true);
|
||||
|
||||
List<FieldInfo> params = new ArrayList<>();
|
||||
params.add(argsParam);
|
||||
|
||||
MethodInfo ctor = MethodInfo.external(
|
||||
"constructor",
|
||||
instanceType,
|
||||
this,
|
||||
params,
|
||||
Modifier.PUBLIC | Modifier.STATIC,
|
||||
"Creates a new instance of " + instanceType.getSimpleName() + ".\n\n" +
|
||||
"Equivalent to using the 'new' operator on this class reference."
|
||||
);
|
||||
|
||||
return Collections.singletonList(ctor);
|
||||
}
|
||||
|
||||
private List<FieldInfo> buildClassField() {
|
||||
if (getJavaClass() == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
try {
|
||||
TypeInfo classType = TypeInfo.fromClass(Class.class).parameterize(instanceType);
|
||||
FieldInfo classField = FieldInfo.external("class", classType,
|
||||
"The Class object for " + getSimpleName(), Modifier.PUBLIC | Modifier.STATIC);
|
||||
return Collections.singletonList(classField);
|
||||
} catch (Exception e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Class<" + getSimpleName() + ">";
|
||||
}
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.type;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.js_parser.TypeParamInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A context for resolving and substituting declared type parameters (T, E, K, V...)
|
||||
* based on a receiver type.
|
||||
*
|
||||
* Policy:
|
||||
* - Prefer applied type arguments when present (e.g. List<String> binds E -> String).
|
||||
* - Fall back to declared bounds when no applied argument exists (e.g. T extends Entity -> Entity).
|
||||
*/
|
||||
public final class GenericContext {
|
||||
|
||||
/** Singleton context for types with no generic parameters. Used to avoid object allocation. */
|
||||
private static final GenericContext EMPTY = new GenericContext(new HashMap<>(), new HashMap<>());
|
||||
|
||||
/**
|
||||
* Map of type variable names to their applied type arguments.
|
||||
* For example, in List<String>, this contains E -> TypeInfo.STRING.
|
||||
* Applied bindings take precedence over bound fallbacks.
|
||||
*/
|
||||
private final Map<String, TypeInfo> appliedBindings;
|
||||
|
||||
/**
|
||||
* Map of type variable names to their declared upper bounds.
|
||||
* For example, in <T extends Entity>, this contains T -> TypeInfo(Entity).
|
||||
* Used as fallback when no applied argument exists.
|
||||
*/
|
||||
private final Map<String, TypeInfo> boundFallbacks;
|
||||
|
||||
private GenericContext(Map<String, TypeInfo> appliedBindings, Map<String, TypeInfo> boundFallbacks) {
|
||||
this.appliedBindings = appliedBindings != null ? appliedBindings : new HashMap<>();
|
||||
this.boundFallbacks = boundFallbacks != null ? boundFallbacks : new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a GenericContext for a receiver type.
|
||||
*
|
||||
* Fast path: returns singleton EMPTY for non-generic types (no allocation overhead).
|
||||
* Slow path: builds maps from type parameters and applied arguments for generic types.
|
||||
*
|
||||
* @param receiverType the type to extract generic bindings from (e.g., List<String>, DAO<T extends Entity>)
|
||||
* @return a context with applied bindings and bound fallbacks, or EMPTY singleton if receiverType is not generic
|
||||
*/
|
||||
public static GenericContext forReceiver(TypeInfo receiverType) {
|
||||
// Special edge case: Array.d.ts JS type, bind T to element type so Array<T> methods resolve correctly
|
||||
if (receiverType != null && receiverType.isArray() && receiverType.getElementType() != null) {
|
||||
Map<String, TypeInfo> applied = new HashMap<>();
|
||||
applied.put("T", receiverType.getElementType());
|
||||
return new GenericContext(applied, new HashMap<>());
|
||||
}
|
||||
|
||||
// Fast path: return singleton for non-generic types
|
||||
if (receiverType == null || !hasGenerics(receiverType)) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
TypeInfo rawType = receiverType.getRawType();
|
||||
List<TypeParamInfo> declaredParams = rawType != null ? rawType.getTypeParams() : null;
|
||||
List<TypeInfo> appliedArgs = receiverType.getAppliedTypeArgs();
|
||||
|
||||
Map<String, TypeInfo> applied = new HashMap<>();
|
||||
if (declaredParams != null && appliedArgs != null) {
|
||||
int count = Math.min(declaredParams.size(), appliedArgs.size());
|
||||
for (int i = 0; i < count; i++) {
|
||||
TypeParamInfo declared = declaredParams.get(i);
|
||||
TypeInfo arg = appliedArgs.get(i);
|
||||
if (declared == null || declared.getName() == null || declared.getName().isEmpty() || arg == null) {
|
||||
continue;
|
||||
}
|
||||
applied.put(declared.getName(), arg);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, TypeInfo> bounds = new HashMap<>();
|
||||
if (declaredParams != null) {
|
||||
for (TypeParamInfo declared : declaredParams) {
|
||||
if (declared == null) continue;
|
||||
String name = declared.getName();
|
||||
if (name == null || name.isEmpty()) continue;
|
||||
TypeInfo bound = declared.getBoundTypeInfo();
|
||||
if (bound != null && bound.isResolved()) {
|
||||
bounds.put(name, bound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new GenericContext(applied, bounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a type has generics that need substitution.
|
||||
*
|
||||
* Returns true if:
|
||||
* - Type is parameterized (e.g., List<String>)
|
||||
* - Type's raw form differs from the type itself
|
||||
* - Type has declared type parameters
|
||||
*
|
||||
* @param type the type to check
|
||||
* @return true if this type contains generic information that may need substitution
|
||||
*/
|
||||
public static boolean hasGenerics(TypeInfo type) {
|
||||
return type.isParameterized() ||
|
||||
type.getRawType() != type ||
|
||||
(type.getTypeParams() != null && !type.getTypeParams().isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a type variable to its bound or applied type.
|
||||
*
|
||||
* Prefers applied bindings (e.g., E -> String in List<String>) over declared bounds.
|
||||
* Falls back to bounds (e.g., T -> Entity in <T extends Entity>) if no applied argument exists.
|
||||
* Returns null if the variable is not found in either map.
|
||||
*
|
||||
* @param name the type variable name (e.g., "T", "E", "K")
|
||||
* @return the resolved TypeInfo, or null if variable not found
|
||||
*/
|
||||
public TypeInfo resolveTypeVariable(String name) {
|
||||
if (name == null || name.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TypeInfo applied = appliedBindings.get(name);
|
||||
if (applied != null) {
|
||||
return applied;
|
||||
}
|
||||
|
||||
return boundFallbacks.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitute type variables in a TypeInfo recursively.
|
||||
*
|
||||
* Handles:
|
||||
* - Direct type variables (unresolved types representing T, E, etc.)
|
||||
* - Parameterized types (recursively substitutes inside type arguments)
|
||||
* - Array types (substitutes the element type, preserves array dimensions)
|
||||
*
|
||||
* @param type the type to substitute (may contain type variables)
|
||||
* @return the substituted type, or the original type if no substitutions apply
|
||||
*/
|
||||
public TypeInfo substitute(TypeInfo type) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Direct type variable substitution (unresolved types represent type variables like T).
|
||||
if (!type.isResolved()) {
|
||||
TypeInfo substitution = resolveTypeVariable(type.getSimpleName());
|
||||
if (substitution != null) {
|
||||
return substitution;
|
||||
}
|
||||
}
|
||||
|
||||
// Parameterized types: substitute inside args.
|
||||
if (type.isParameterized()) {
|
||||
List<TypeInfo> originalArgs = type.getAppliedTypeArgs();
|
||||
List<TypeInfo> substitutedArgs = new ArrayList<>(originalArgs.size());
|
||||
boolean changed = false;
|
||||
|
||||
for (TypeInfo arg : originalArgs) {
|
||||
TypeInfo substitutedArg = substitute(arg);
|
||||
substitutedArgs.add(substitutedArg);
|
||||
if (substitutedArg != arg) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
return type.getRawType().parameterize(substitutedArgs);
|
||||
}
|
||||
}
|
||||
|
||||
// Array wrapper types are represented by a display-name suffix; we can still substitute
|
||||
// a plain "T[]" by looking up "T".
|
||||
String simple = type.getSimpleName();
|
||||
if (simple != null && simple.endsWith("[]")) {
|
||||
TypeStringNormalizer.ArraySplit split = TypeStringNormalizer.splitArraySuffixes(simple);
|
||||
String elementName = split.base;
|
||||
int dims = split.dimensions;
|
||||
TypeInfo elementSub = resolveTypeVariable(elementName);
|
||||
if (elementSub != null) {
|
||||
TypeInfo result = elementSub;
|
||||
for (int i = 0; i < dims; i++) {
|
||||
result = TypeInfo.arrayOf(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitute type variables in a type string.
|
||||
*
|
||||
* Attempts substitution in this order:
|
||||
* 1. Normalize the string (strip imports, pick union branch, remove nullable suffix, split arrays)
|
||||
* 2. Try direct type variable lookup (e.g., "T" -> resolved Entity)
|
||||
* 3. Fall back to full type resolution with substitution applied
|
||||
*
|
||||
* @param typeString the type expression as a string (e.g., "T", "T[]", "List<T>")
|
||||
* @param resolver optional TypeResolver for fallback resolution; if null, returns null on failure
|
||||
* @return the substituted type, or null if resolution fails
|
||||
*/
|
||||
public TypeInfo substituteString(String typeString, TypeResolver resolver) {
|
||||
if (typeString == null || typeString.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalized = TypeStringNormalizer.stripImportTypeSyntax(typeString);
|
||||
normalized = TypeStringNormalizer.pickPreferredUnionBranch(normalized);
|
||||
normalized = TypeStringNormalizer.stripNullableSuffix(normalized);
|
||||
|
||||
TypeStringNormalizer.ArraySplit arraySplit = TypeStringNormalizer.splitArraySuffixes(normalized);
|
||||
String baseExpr = arraySplit.base;
|
||||
int dims = arraySplit.dimensions;
|
||||
|
||||
String bareBase = GenericTypeParser.stripGenerics(baseExpr);
|
||||
TypeInfo direct = resolveTypeVariable(bareBase);
|
||||
if (direct != null) {
|
||||
TypeInfo result = direct;
|
||||
for (int i = 0; i < dims; i++) {
|
||||
result = TypeInfo.arrayOf(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (resolver == null) {
|
||||
return null;
|
||||
}
|
||||
TypeInfo resolved = resolver.resolveJSType(typeString);
|
||||
return substitute(resolved);
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitute type variables in a resolved type, with string fallback.
|
||||
*
|
||||
* Two-phase approach:
|
||||
* 1. Try substituting the resolved type directly
|
||||
* 2. If that fails, attempt substitution via the raw string as fallback
|
||||
*
|
||||
* This handles edge cases where the TypeInfo doesn't capture enough information
|
||||
* to do proper substitution (e.g., union types, complex generics).
|
||||
*
|
||||
* @param resolvedType a TypeInfo that has already been resolved (may still contain type variables)
|
||||
* @param rawTypeString the original type string before resolution (for fallback)
|
||||
* @param resolver optional TypeResolver for string-based fallback resolution
|
||||
* @return the substituted type, or the best attempt if full substitution fails
|
||||
*/
|
||||
public TypeInfo substituteType(TypeInfo resolvedType, String rawTypeString, TypeResolver resolver) {
|
||||
if (resolvedType == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TypeInfo substituted = substitute(resolvedType);
|
||||
if (substituted != null && substituted.isResolved()) {
|
||||
return substituted;
|
||||
}
|
||||
|
||||
if (rawTypeString != null && !rawTypeString.trim().isEmpty()) {
|
||||
TypeInfo fromString = substituteString(rawTypeString, resolver);
|
||||
if (fromString != null && fromString.isResolved()) {
|
||||
return fromString;
|
||||
}
|
||||
}
|
||||
|
||||
return substituted;
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.type;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.BlockingDeque;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* Adapts {@code Object}-typed method parameters to the receiver's generic type arguments
|
||||
* for editor display (autocomplete/hover).
|
||||
* <p>
|
||||
* Java collections use {@code Object} in certain method signatures for backward-compatibility
|
||||
* (e.g., {@code Map.get(Object)} instead of {@code Map.get(K)}). This adapter maps those
|
||||
* {@code Object} parameters back to the appropriate generic type argument when the receiver
|
||||
* is parameterized (e.g., {@code Map<String, Integer>}).
|
||||
* <p>
|
||||
* <b>Design rules:</b>
|
||||
* <ul>
|
||||
* <li>Only contract-level checks using {@code java.util.Map}, {@code java.util.Collection},
|
||||
* {@code java.util.List} assignability — never concrete classes like HashMap/ArrayList.</li>
|
||||
* <li>Only adapts when the reflected parameter type is exactly {@code java.lang.Object}
|
||||
* AND the receiver is parameterized with applied type args.</li>
|
||||
* <li>Table-driven: each rule is a {@link Rule} record in a static list.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class GenericParameterAdapter {
|
||||
|
||||
// ==================== Rule Model ====================
|
||||
|
||||
/**
|
||||
* A single adaptation rule.
|
||||
*
|
||||
* @see #RULES
|
||||
*/
|
||||
private static final class Rule {
|
||||
/** The contract interface (e.g., {@code java.util.Map}). */
|
||||
final Class<?> contractType;
|
||||
/** Method name to match. */
|
||||
final String methodName;
|
||||
/** Total parameter count the method must have. */
|
||||
final int paramCount;
|
||||
/** The parameter index to adapt (0-based). */
|
||||
final int paramIndex;
|
||||
/** Index into the receiver's applied type args (0-based). 0=K/E, 1=V, etc. */
|
||||
final int typeArgIndex;
|
||||
|
||||
Rule(Class<?> contractType, String methodName, int paramCount, int paramIndex, int typeArgIndex) {
|
||||
this.contractType = contractType;
|
||||
this.methodName = methodName;
|
||||
this.paramCount = paramCount;
|
||||
this.paramIndex = paramIndex;
|
||||
this.typeArgIndex = typeArgIndex;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Rule Table ====================
|
||||
|
||||
private static final List<Rule> RULES;
|
||||
|
||||
static {
|
||||
List<Rule> rules = new ArrayList<>();
|
||||
|
||||
// Map<K, V>
|
||||
rules.add(new Rule(java.util.Map.class, "get", 1, 0, 0));
|
||||
rules.add(new Rule(java.util.Map.class, "containsKey", 1, 0, 0));
|
||||
rules.add(new Rule(java.util.Map.class, "containsValue", 1, 0, 1));
|
||||
rules.add(new Rule(java.util.Map.class, "remove", 1, 0, 0));
|
||||
rules.add(new Rule(java.util.Map.class, "remove", 2, 0, 0));
|
||||
rules.add(new Rule(java.util.Map.class, "remove", 2, 1, 1));
|
||||
rules.add(new Rule(java.util.Map.class, "getOrDefault", 2, 0, 0));
|
||||
|
||||
// ConcurrentMap<K, V> (get/containsKey/containsValue inherited from Map above)
|
||||
rules.add(new Rule(java.util.concurrent.ConcurrentMap.class, "remove", 2, 0, 0));
|
||||
rules.add(new Rule(java.util.concurrent.ConcurrentMap.class, "remove", 2, 1, 1));
|
||||
rules.add(new Rule(java.util.concurrent.ConcurrentMap.class, "getOrDefault", 2, 0, 0));
|
||||
|
||||
// Collection<E>
|
||||
rules.add(new Rule(java.util.Collection.class, "contains", 1, 0, 0));
|
||||
rules.add(new Rule(java.util.Collection.class, "remove", 1, 0, 0));
|
||||
|
||||
// List<E> (contains/remove covered by Collection above)
|
||||
rules.add(new Rule(java.util.List.class, "indexOf", 1, 0, 0));
|
||||
rules.add(new Rule(java.util.List.class, "lastIndexOf", 1, 0, 0));
|
||||
|
||||
// Set<E> (contains/remove covered by Collection above)
|
||||
|
||||
// Deque<E>
|
||||
rules.add(new Rule(java.util.Deque.class, "contains", 1, 0, 0));
|
||||
rules.add(new Rule(java.util.Deque.class, "remove", 1, 0, 0));
|
||||
rules.add(new Rule(java.util.Deque.class, "removeFirstOccurrence", 1, 0, 0));
|
||||
rules.add(new Rule(java.util.Deque.class, "removeLastOccurrence", 1, 0, 0));
|
||||
|
||||
// BlockingQueue<E> (contains/remove covered by Collection above)
|
||||
rules.add(new Rule(java.util.concurrent.BlockingQueue.class, "remove", 1, 0, 0));
|
||||
rules.add(new Rule(java.util.concurrent.BlockingQueue.class, "contains", 1, 0, 0));
|
||||
|
||||
// BlockingDeque<E>
|
||||
rules.add(new Rule(java.util.concurrent.BlockingDeque.class, "remove", 1, 0, 0));
|
||||
rules.add(new Rule(java.util.concurrent.BlockingDeque.class, "contains", 1, 0, 0));
|
||||
rules.add(new Rule(java.util.concurrent.BlockingDeque.class, "removeFirstOccurrence", 1, 0, 0));
|
||||
rules.add(new Rule(java.util.concurrent.BlockingDeque.class, "removeLastOccurrence", 1, 0, 0));
|
||||
|
||||
RULES = Collections.unmodifiableList(rules);
|
||||
}
|
||||
|
||||
// ==================== Public API ====================
|
||||
|
||||
/**
|
||||
* Attempt to adapt a parameter type from {@code Object} to the receiver's generic type argument.
|
||||
* <p>
|
||||
* Returns the adapted {@link TypeInfo} if all conditions are met:
|
||||
* <ol>
|
||||
* <li>The receiver is parameterized and has applied type args.</li>
|
||||
* <li>The parameter's current type is exactly {@code java.lang.Object}.</li>
|
||||
* <li>A matching rule exists in the rule table.</li>
|
||||
* <li>The rule's required type arg index is within bounds.</li>
|
||||
* </ol>
|
||||
* Otherwise returns {@code null} (meaning: keep the original type).
|
||||
*
|
||||
* @param method the reflected Java method
|
||||
* @param receiverType the parameterized receiver TypeInfo (e.g., {@code Map<String, Integer>})
|
||||
* @param paramIndex the index of the parameter being checked
|
||||
* @param currentType the current TypeInfo for that parameter (after normal substitution)
|
||||
* @return adapted TypeInfo, or {@code null} if no adaptation applies
|
||||
*/
|
||||
public static TypeInfo adaptParameterType(Method method, TypeInfo receiverType,
|
||||
int paramIndex, TypeInfo currentType) {
|
||||
// Guard: only adapt Object params on parameterized receivers
|
||||
if (receiverType == null || !receiverType.isParameterized()) {
|
||||
return null;
|
||||
}
|
||||
if (currentType == null || currentType.getJavaClass() != Object.class) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<TypeInfo> appliedArgs = receiverType.getAppliedTypeArgs();
|
||||
if (appliedArgs == null || appliedArgs.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine the declaring class of the method
|
||||
Class<?> declaringClass = method.getDeclaringClass();
|
||||
String methodName = method.getName();
|
||||
int paramCount = method.getParameterCount();
|
||||
|
||||
// Scan rules for a match
|
||||
for (Rule rule : RULES) {
|
||||
if (rule.paramIndex != paramIndex) continue;
|
||||
if (rule.paramCount != paramCount) continue;
|
||||
if (!rule.methodName.equals(methodName)) continue;
|
||||
if (!rule.contractType.isAssignableFrom(declaringClass)) continue;
|
||||
|
||||
// Rule matches — check if the type arg index is within bounds
|
||||
if (rule.typeArgIndex < appliedArgs.size()) {
|
||||
TypeInfo adapted = appliedArgs.get(rule.typeArgIndex);
|
||||
if (adapted != null && adapted.isResolved()) {
|
||||
return adapted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private GenericParameterAdapter() {
|
||||
// Utility class — no instantiation
|
||||
}
|
||||
}
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.type;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Parser for generic type expressions with nested type arguments.
|
||||
*
|
||||
* Handles type expressions like:
|
||||
* - Simple types: "String", "IPlayer"
|
||||
* - Qualified names: "java.util.List", "IPlayerEvent.InteractEvent"
|
||||
* - Single generic: "List<String>", "Consumer<IAction>"
|
||||
* - Multiple generics: "Map<String, Integer>"
|
||||
* - Nested generics: "List<Map<String, Integer>>"
|
||||
*
|
||||
* Note: union/nullability/array semantics are handled by higher-level resolvers.
|
||||
* This parser only consumes suffix tokens where needed to correctly delimit type arguments.
|
||||
*
|
||||
* This parser produces a ParsedType tree that can be resolved into parameterized TypeInfo instances.
|
||||
*/
|
||||
public class GenericTypeParser {
|
||||
|
||||
/**
|
||||
* Represents a parsed type expression with potential generic arguments.
|
||||
*/
|
||||
public static class ParsedType {
|
||||
/** The base type name (may be qualified, e.g., "java.util.List" or "IPlayer") */
|
||||
public final String baseName;
|
||||
|
||||
/** The applied type arguments (for generics like List<String>), each is itself a ParsedType */
|
||||
public final List<ParsedType> typeArgs;
|
||||
|
||||
/** The original raw string before parsing */
|
||||
public final String rawString;
|
||||
|
||||
public ParsedType(String baseName, List<ParsedType> typeArgs, String rawString) {
|
||||
this.baseName = baseName;
|
||||
this.typeArgs = typeArgs != null ? typeArgs : new ArrayList<>();
|
||||
this.rawString = rawString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this parsed type has generic arguments.
|
||||
*/
|
||||
public boolean hasTypeArgs() {
|
||||
return !typeArgs.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display string for debugging.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder(baseName);
|
||||
if (!typeArgs.isEmpty()) {
|
||||
sb.append("<");
|
||||
for (int i = 0; i < typeArgs.size(); i++) {
|
||||
if (i > 0) sb.append(", ");
|
||||
sb.append(typeArgs.get(i).toString());
|
||||
}
|
||||
sb.append(">");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a type expression string into a ParsedType tree.
|
||||
*
|
||||
* @param typeExpr The type expression to parse (e.g., "List<Map<String, Integer>>[]")
|
||||
* @return The parsed type, or null if parsing fails
|
||||
*/
|
||||
public static ParsedType parse(String typeExpr) {
|
||||
if (typeExpr == null || typeExpr.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String expr = typeExpr.trim();
|
||||
|
||||
// Now parse the base type with potential generic arguments
|
||||
ParseResult result = parseTypeWithGenerics(expr, 0);
|
||||
if (result == null || result.type == null) {
|
||||
// Fallback: treat entire expression as a simple type
|
||||
return new ParsedType(expr, null, typeExpr);
|
||||
}
|
||||
|
||||
return result.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal result class for recursive parsing.
|
||||
*/
|
||||
private static class ParseResult {
|
||||
final ParsedType type;
|
||||
final int endIndex; // position after this type in the string
|
||||
|
||||
ParseResult(ParsedType type, int endIndex) {
|
||||
this.type = type;
|
||||
this.endIndex = endIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a type expression starting at the given index, handling nested generics.
|
||||
* Returns the parsed type and the index after this type expression.
|
||||
*/
|
||||
private static ParseResult parseTypeWithGenerics(String expr, int startIndex) {
|
||||
int i = startIndex;
|
||||
int len = expr.length();
|
||||
|
||||
// Skip leading whitespace
|
||||
while (i < len && Character.isWhitespace(expr.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i >= len) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read the base type name (can include dots for qualified names)
|
||||
StringBuilder baseNameBuilder = new StringBuilder();
|
||||
while (i < len) {
|
||||
char c = expr.charAt(i);
|
||||
if (Character.isJavaIdentifierPart(c) || c == '.') {
|
||||
baseNameBuilder.append(c);
|
||||
i++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
String baseName = baseNameBuilder.toString().trim();
|
||||
|
||||
if (baseName.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove trailing dots if any
|
||||
while (baseName.endsWith(".")) {
|
||||
baseName = baseName.substring(0, baseName.length() - 1);
|
||||
}
|
||||
|
||||
// Skip whitespace before potential <
|
||||
while (i < len && Character.isWhitespace(expr.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
|
||||
// Check for generic arguments
|
||||
List<ParsedType> typeArgs = new ArrayList<>();
|
||||
if (i < len && expr.charAt(i) == '<') {
|
||||
i++; // Skip '<'
|
||||
|
||||
// Parse type arguments (comma-separated, handling nested <>)
|
||||
while (i < len) {
|
||||
// Skip whitespace
|
||||
while (i < len && Character.isWhitespace(expr.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i >= len) break;
|
||||
|
||||
char c = expr.charAt(i);
|
||||
if (c == '>') {
|
||||
i++; // Skip '>'
|
||||
break;
|
||||
}
|
||||
|
||||
if (c == ',') {
|
||||
i++; // Skip ','
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse a type argument (may itself have generics)
|
||||
ParseResult argResult = parseTypeArgument(expr, i);
|
||||
if (argResult != null && argResult.type != null) {
|
||||
typeArgs.add(argResult.type);
|
||||
i = argResult.endIndex;
|
||||
} else {
|
||||
// Skip forward until the next ',' or '>' at the current nesting level.
|
||||
i = skipToNextTypeArgDelimiter(expr, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParsedType type = new ParsedType(baseName, typeArgs, expr.substring(startIndex, i));
|
||||
return new ParseResult(type, i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip forward until we hit ',' or '>' that would delimit a type argument.
|
||||
* This prevents producing spurious additional type args when encountering unsupported syntax.
|
||||
*/
|
||||
private static int skipToNextTypeArgDelimiter(String expr, int startIndex) {
|
||||
int i = startIndex;
|
||||
int depth = 0;
|
||||
boolean inString = false;
|
||||
char stringChar = 0;
|
||||
while (i < expr.length()) {
|
||||
char c = expr.charAt(i);
|
||||
if (inString) {
|
||||
if (c == stringChar && (i == 0 || expr.charAt(i - 1) != '\\')) {
|
||||
inString = false;
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '\'' || c == '"') {
|
||||
inString = true;
|
||||
stringChar = c;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '<' || c == '(' || c == '[') {
|
||||
depth++;
|
||||
} else if (c == '>' || c == ')' || c == ']') {
|
||||
if (depth > 0) {
|
||||
depth--;
|
||||
} else if (c == '>') {
|
||||
return i; // Let caller handle closing '>'
|
||||
}
|
||||
} else if ((c == ',' || c == '>') && depth == 0) {
|
||||
return i;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single type argument, which may include nested generics and array suffixes.
|
||||
*/
|
||||
private static ParseResult parseTypeArgument(String expr, int startIndex) {
|
||||
int i = startIndex;
|
||||
int len = expr.length();
|
||||
|
||||
// Skip leading whitespace
|
||||
while (i < len && Character.isWhitespace(expr.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i >= len) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for wildcard (? extends X, ? super X, or just ?)
|
||||
if (expr.charAt(i) == '?') {
|
||||
i++;
|
||||
// Skip whitespace
|
||||
while (i < len && Character.isWhitespace(expr.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
// Check for "extends" or "super"
|
||||
if (i < len && expr.substring(i).startsWith("extends ")) {
|
||||
i += 8; // Skip "extends "
|
||||
// Parse the bound type
|
||||
ParseResult boundResult = parseTypeWithGenerics(expr, i);
|
||||
if (boundResult != null) {
|
||||
return boundResult;
|
||||
}
|
||||
} else if (i < len && expr.substring(i).startsWith("super ")) {
|
||||
i += 6; // Skip "super "
|
||||
// Parse the bound type
|
||||
ParseResult boundResult = parseTypeWithGenerics(expr, i);
|
||||
if (boundResult != null) {
|
||||
return boundResult;
|
||||
}
|
||||
}
|
||||
// Plain wildcard, treat as Object
|
||||
return new ParseResult(new ParsedType("Object", null, "?"), i);
|
||||
}
|
||||
|
||||
// Parse regular type with potential generics
|
||||
ParseResult typeResult = parseTypeWithGenerics(expr, i);
|
||||
if (typeResult == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
i = typeResult.endIndex;
|
||||
ParsedType baseType = typeResult.type;
|
||||
|
||||
// Skip whitespace
|
||||
while (i < len && Character.isWhitespace(expr.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
|
||||
// Consume array suffixes / nullable suffix so the caller can correctly find delimiters.
|
||||
while (i + 1 < len && expr.charAt(i) == '[' && expr.charAt(i + 1) == ']') {
|
||||
i += 2;
|
||||
while (i < len && Character.isWhitespace(expr.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (i < len && expr.charAt(i) == '?') {
|
||||
i++;
|
||||
}
|
||||
|
||||
// Preserve the full raw substring (including suffixes) for higher-level resolvers.
|
||||
if (i != typeResult.endIndex) {
|
||||
return new ParseResult(new ParsedType(baseType.baseName, baseType.typeArgs, expr.substring(startIndex, i)), i);
|
||||
}
|
||||
|
||||
return new ParseResult(baseType, i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to check if a type string contains generic arguments.
|
||||
*/
|
||||
public static boolean hasGenerics(String typeExpr) {
|
||||
return typeExpr != null && typeExpr.contains("<") && typeExpr.contains(">");
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip generic arguments from a type string (for base type lookup).
|
||||
* Example: "List<String>" -> "List"
|
||||
*/
|
||||
public static String stripGenerics(String typeExpr) {
|
||||
if (typeExpr == null) return null;
|
||||
int idx = typeExpr.indexOf('<');
|
||||
if (idx > 0) {
|
||||
return typeExpr.substring(0, idx).trim();
|
||||
}
|
||||
return typeExpr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.type;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.token.Token;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a single import statement with all its metadata.
|
||||
* Tracks the full import path, resolution status, source positions, and references.
|
||||
*/
|
||||
public final class ImportData {
|
||||
|
||||
private final String fullPath; // e.g., "java.util.List" or "kamkeel.npcdbc.api.*"
|
||||
private final String simpleName; // e.g., "List" or null for wildcard
|
||||
private final boolean isWildcard; // true if ends with .*
|
||||
private final boolean isStatic; // true if 'import static'
|
||||
private final int startOffset; // start of 'import' keyword in source
|
||||
private final int endOffset; // end of import statement (before or at semicolon)
|
||||
private final int pathStartOffset; // start of the package.Class path
|
||||
private final int pathEndOffset; // end of the package.Class path (before .*)
|
||||
|
||||
private TypeInfo resolvedType; // null if wildcard or unresolved
|
||||
private boolean resolved; // whether resolution was attempted and succeeded
|
||||
|
||||
// Track usage count (simpler than tracking individual tokens)
|
||||
private int usageCount = 0;
|
||||
|
||||
// Track all tokens that reference this import (optional, for detailed analysis)
|
||||
private final List<Token> referencingTokens = new ArrayList<>();
|
||||
|
||||
public ImportData(String fullPath, String simpleName, boolean isWildcard, boolean isStatic,
|
||||
int startOffset, int endOffset, int pathStartOffset, int pathEndOffset) {
|
||||
this.fullPath = fullPath;
|
||||
this.simpleName = simpleName;
|
||||
this.isWildcard = isWildcard;
|
||||
this.isStatic = isStatic;
|
||||
this.startOffset = startOffset;
|
||||
this.endOffset = endOffset;
|
||||
this.pathStartOffset = pathStartOffset;
|
||||
this.pathEndOffset = pathEndOffset;
|
||||
this.resolved = false;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getFullPath() { return fullPath; }
|
||||
public String getSimpleName() { return simpleName; }
|
||||
public boolean isWildcard() { return isWildcard; }
|
||||
public boolean isStatic() { return isStatic; }
|
||||
public int getStartOffset() { return startOffset; }
|
||||
public int getEndOffset() { return endOffset; }
|
||||
public int getPathStartOffset() { return pathStartOffset; }
|
||||
public int getPathEndOffset() { return pathEndOffset; }
|
||||
public TypeInfo getResolvedType() { return resolvedType; }
|
||||
public boolean isResolved() { return resolved; }
|
||||
|
||||
/**
|
||||
* Set the resolved type information.
|
||||
*/
|
||||
public void setResolvedType(TypeInfo typeInfo) {
|
||||
this.resolvedType = typeInfo;
|
||||
this.resolved = typeInfo != null && typeInfo.isResolved();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this import as resolved (for wildcard imports where we confirm the package exists).
|
||||
*/
|
||||
public void markResolved(boolean resolved) {
|
||||
this.resolved = resolved;
|
||||
}
|
||||
|
||||
// ==================== REFERENCE TRACKING ====================
|
||||
|
||||
/**
|
||||
* Increment the usage count for this import.
|
||||
* Called during type resolution when this import is used.
|
||||
*/
|
||||
public void incrementUsage() {
|
||||
usageCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a token that references this import.
|
||||
* Called when a token is created that uses this import's type.
|
||||
*/
|
||||
public void addReference(Token token) {
|
||||
if (token != null && !referencingTokens.contains(token)) {
|
||||
referencingTokens.add(token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tokens that reference this import.
|
||||
*/
|
||||
public List<Token> getReferencingTokens() {
|
||||
return Collections.unmodifiableList(referencingTokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this import is used (has any references or usages).
|
||||
*/
|
||||
public boolean isUsed() {
|
||||
return usageCount > 0 || !referencingTokens.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the usage count for this import.
|
||||
*/
|
||||
public int getUsageCount() {
|
||||
return usageCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of references to this import.
|
||||
*/
|
||||
public int getReferenceCount() {
|
||||
return referencingTokens.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all references and reset usage count (used when re-tokenizing).
|
||||
*/
|
||||
public void clearReferences() {
|
||||
referencingTokens.clear();
|
||||
usageCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the package portion of the import path.
|
||||
* For "java.util.List" returns "java.util"
|
||||
* For wildcards like "java.util.*" returns "java.util"
|
||||
*/
|
||||
public String getPackagePortion() {
|
||||
if (isWildcard) {
|
||||
return fullPath;
|
||||
}
|
||||
int lastDot = fullPath.lastIndexOf('.');
|
||||
return lastDot > 0 ? fullPath.substring(0, lastDot) : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all segments of the import path.
|
||||
*/
|
||||
public String[] getPathSegments() {
|
||||
return fullPath.split("\\.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this import could provide the given simple class name.
|
||||
* For wildcard imports, always returns true (might provide any class).
|
||||
* For specific imports, checks if the simple name matches.
|
||||
*/
|
||||
public boolean couldProvide(String className) {
|
||||
if (isWildcard) {
|
||||
return true;
|
||||
}
|
||||
return className.equals(simpleName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder("import ");
|
||||
if (isStatic) sb.append("static ");
|
||||
sb.append(fullPath);
|
||||
if (isWildcard) sb.append(".*");
|
||||
sb.append(" [").append(resolved ? "resolved" : "unresolved").append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ImportData that = (ImportData) o;
|
||||
return fullPath.equals(that.fullPath) && isWildcard == that.isWildcard && isStatic == that.isStatic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return fullPath.hashCode() * 31 + (isWildcard ? 1 : 0) + (isStatic ? 2 : 0);
|
||||
}
|
||||
}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.type;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticMethod;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticParameter;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticTypeBuilder;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticType;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Registry for Nashorn built-in types and global functions.
|
||||
*
|
||||
* <p>Nashorn provides several built-in objects and functions:</p>
|
||||
* <ul>
|
||||
* <li><b>Java</b> - Object for Java interop: type(), extend(), from(), to()</li>
|
||||
* <li><b>Packages</b> - Root of package hierarchy for accessing Java packages</li>
|
||||
* <li><b>JavaImporter</b> - Creates scope with imported Java packages</li>
|
||||
* <li><b>print()</b> - Global print function</li>
|
||||
* <li><b>load()</b> - Load and execute a script</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class NashornBuiltins {
|
||||
|
||||
private static NashornBuiltins instance;
|
||||
|
||||
private final Map<String, SyntheticType> builtinTypes = new LinkedHashMap<>();
|
||||
private final Map<String, SyntheticMethod> globalFunctions = new LinkedHashMap<>();
|
||||
|
||||
private NashornBuiltins() {
|
||||
initializeBuiltins();
|
||||
}
|
||||
|
||||
public static NashornBuiltins getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new NashornBuiltins();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void initializeBuiltins() {
|
||||
// ==================== Java object ====================
|
||||
SyntheticType javaType = new SyntheticTypeBuilder("Java")
|
||||
.documentation("Nashorn's Java interop object for accessing Java types and utilities.")
|
||||
|
||||
.addMethod("type")
|
||||
.parameter("className", "String", "Fully-qualified Java class name")
|
||||
.returns("java.lang.Class")
|
||||
.returnsResolved(args -> {
|
||||
if (args != null && args.length > 0 && args[0] != null) {
|
||||
String className = args[0];
|
||||
// Remove quotes if present
|
||||
if (className.startsWith("\"") && className.endsWith("\"")) {
|
||||
className = className.substring(1, className.length() - 1);
|
||||
} else if (className.startsWith("'") && className.endsWith("'")) {
|
||||
className = className.substring(1, className.length() - 1);
|
||||
}
|
||||
// Try to load the class
|
||||
TypeInfo resolved = TypeResolver.getInstance().resolve(className);
|
||||
if (resolved != null && resolved.isResolved()) {
|
||||
return new ClassTypeInfo(resolved);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.documentation("Loads a Java class by its fully-qualified name.\n\n" +
|
||||
"**Usage:**\n" +
|
||||
"```javascript\n" +
|
||||
"var File = Java.type(\"java.io.File\");\n" +
|
||||
"var file = new File(\"path/to/file\");\n" +
|
||||
"```\n\n" +
|
||||
"@param className The fully-qualified Java class name\n" +
|
||||
"@returns The Java class reference (use with 'new' to create instances)")
|
||||
.done()
|
||||
|
||||
.addMethod("extend")
|
||||
.parameter("type", "java.lang.Class", "Java class or interface to extend/implement")
|
||||
.returns("java.lang.Class")
|
||||
.documentation("Creates a subclass or implementation of a Java class or interface.\n\n" +
|
||||
"**Usage:**\n" +
|
||||
"```javascript\n" +
|
||||
"var MyRunnable = Java.extend(Java.type(\"java.lang.Runnable\"), {\n" +
|
||||
" run: function() {\n" +
|
||||
" print(\"Running!\");\n" +
|
||||
" }\n" +
|
||||
"});\n" +
|
||||
"```\n\n" +
|
||||
"@param type The Java class or interface to extend\n" +
|
||||
"@returns A new class that extends/implements the given type")
|
||||
.done()
|
||||
|
||||
.addMethod("from")
|
||||
.parameter("javaArray", "Object", "A Java array or Collection")
|
||||
.returns("Object[]")
|
||||
.documentation("Converts a Java array or Collection to a JavaScript array.\n\n" +
|
||||
"**Usage:**\n" +
|
||||
"```javascript\n" +
|
||||
"var jsList = Java.from(javaArrayList);\n" +
|
||||
"jsList.forEach(function(item) { print(item); });\n" +
|
||||
"```\n\n" +
|
||||
"@param javaArray A Java array or java.util.Collection\n" +
|
||||
"@returns A JavaScript array containing the elements")
|
||||
.done()
|
||||
|
||||
.addMethod("to")
|
||||
.parameter("jsArray", "Object[]", "A JavaScript array")
|
||||
.parameter("javaType", "Class", "The target Java array type")
|
||||
.returns("Object")
|
||||
.documentation("Converts a JavaScript array to a Java array of the specified type.\n\n" +
|
||||
"**Usage:**\n" +
|
||||
"```javascript\n" +
|
||||
"var jsArray = [1, 2, 3];\n" +
|
||||
"var intArray = Java.to(jsArray, \"int[]\");\n" +
|
||||
"```\n\n" +
|
||||
"@param jsArray A JavaScript array\n" +
|
||||
"@param javaType The target Java array type (e.g., \"int[]\", \"java.lang.String[]\")\n" +
|
||||
"@returns A Java array of the specified type")
|
||||
.done()
|
||||
|
||||
.addMethod("super")
|
||||
.parameter("object", "Object", "A Java object created via Java.extend()")
|
||||
.returns("Object")
|
||||
.documentation("Gets a reference to the super class for calling super methods.\n\n" +
|
||||
"**Usage:**\n" +
|
||||
"```javascript\n" +
|
||||
"var MyList = Java.extend(Java.type(\"java.util.ArrayList\"), {\n" +
|
||||
" add: function(e) {\n" +
|
||||
" print(\"Adding: \" + e);\n" +
|
||||
" return Java.super(this).add(e);\n" +
|
||||
" }\n" +
|
||||
"});\n" +
|
||||
"```\n\n" +
|
||||
"@param object An extended Java object\n" +
|
||||
"@returns A reference to call super methods")
|
||||
.done()
|
||||
|
||||
.addMethod("synchronized")
|
||||
.parameter("func", "java.util.function.Function", "The function to synchronize")
|
||||
.parameter("lock", "Object", "The object to synchronize on")
|
||||
.returns("java.util.function.Function")
|
||||
.documentation("Wraps a function to execute synchronized on a given object.\n\n" +
|
||||
"**Usage:**\n" +
|
||||
"```javascript\n" +
|
||||
"var syncFunc = Java.synchronized(function() {\n" +
|
||||
" // Thread-safe code here\n" +
|
||||
"}, lockObject);\n" +
|
||||
"```\n\n" +
|
||||
"@param func The function to synchronize\n" +
|
||||
"@param lock The object to use as the monitor\n" +
|
||||
"@returns A synchronized version of the function")
|
||||
.done()
|
||||
|
||||
.build();
|
||||
builtinTypes.put("Java", javaType);
|
||||
|
||||
// ==================== Global print function ====================
|
||||
SyntheticType printFunc = new SyntheticTypeBuilder("print")
|
||||
.addMethod("print")
|
||||
.parameter("message", "Object", "The message to print")
|
||||
.returns("void")
|
||||
.documentation("Prints a message to standard output.\n\n" +
|
||||
"**Usage:**\n" +
|
||||
"```javascript\n" +
|
||||
"print(\"Hello, world!\");\n" +
|
||||
"print(myObject);\n" +
|
||||
"```")
|
||||
.done()
|
||||
.build();
|
||||
globalFunctions.put("print", printFunc.getMethod("print"));
|
||||
|
||||
// ==================== Global load function ====================
|
||||
SyntheticType loadFunc = new SyntheticTypeBuilder("load")
|
||||
.addMethod("load")
|
||||
.parameter("script", "String", "Path or URL to the script")
|
||||
.returns("Object")
|
||||
.documentation("Loads and executes a script file or URL.\n\n" +
|
||||
"**Usage:**\n" +
|
||||
"```javascript\n" +
|
||||
"load(\"./myScript.js\");\n" +
|
||||
"load(\"http://example.com/script.js\");\n" +
|
||||
"```\n\n" +
|
||||
"@param script Path to a local script or URL\n" +
|
||||
"@returns The result of the script execution")
|
||||
.done()
|
||||
.build();
|
||||
globalFunctions.put("load", loadFunc.getMethod("load"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a built-in type by name.
|
||||
* @param name The type name (e.g., "Java")
|
||||
* @return The SyntheticType or null if not found
|
||||
*/
|
||||
public SyntheticType getBuiltinType(String name) {
|
||||
return builtinTypes.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a name is a built-in type.
|
||||
*/
|
||||
public boolean isBuiltinType(String name) {
|
||||
return builtinTypes.containsKey(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a global function by name.
|
||||
*/
|
||||
public SyntheticMethod getGlobalFunction(String name) {
|
||||
return globalFunctions.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a name is a global function.
|
||||
*/
|
||||
public boolean isGlobalFunction(String name) {
|
||||
return globalFunctions.containsKey(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all built-in type names.
|
||||
*/
|
||||
public Set<String> getBuiltinTypeNames() {
|
||||
return Collections.unmodifiableSet(builtinTypes.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all global function names.
|
||||
*/
|
||||
public Set<String> getGlobalFunctionNames() {
|
||||
return Collections.unmodifiableSet(globalFunctions.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all built-in types.
|
||||
*/
|
||||
public Collection<SyntheticType> getAllBuiltinTypes() {
|
||||
return Collections.unmodifiableCollection(builtinTypes.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all global functions as SyntheticTypes.
|
||||
* Each function is wrapped in its own SyntheticType for the registry.
|
||||
*/
|
||||
public Map<String, SyntheticType> getAllGlobalFunctions() {
|
||||
Map<String, SyntheticType> result = new LinkedHashMap<>();
|
||||
|
||||
// Wrap each global function in a minimal SyntheticType
|
||||
for (Map.Entry<String, SyntheticMethod> entry : globalFunctions.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
SyntheticMethod method = entry.getValue();
|
||||
|
||||
// Create a synthetic type that just contains this one global function
|
||||
SyntheticTypeBuilder builder = new SyntheticTypeBuilder(name);
|
||||
builder.documentation("Global " + name + " function");
|
||||
|
||||
// Add the method
|
||||
SyntheticTypeBuilder.MethodBuilder methodBuilder = builder.addMethod(name);
|
||||
for (SyntheticParameter param : method.parameters) {
|
||||
methodBuilder.parameter(param.name, param.typeName, param.documentation);
|
||||
}
|
||||
methodBuilder.returns(method.returnType);
|
||||
if (method.documentation != null && !method.documentation.isEmpty()) {
|
||||
methodBuilder.documentation(method.documentation);
|
||||
}
|
||||
methodBuilder.done();
|
||||
|
||||
result.put(name, builder.build());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the return type of Java.type() given the class name argument.
|
||||
*/
|
||||
public TypeInfo resolveJavaType(String className) {
|
||||
SyntheticType javaBuiltin = builtinTypes.get("Java");
|
||||
if (javaBuiltin != null) {
|
||||
return javaBuiltin.resolveMethodReturnType("type", new String[]{className});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.type;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Helper class for selecting the best method overload from a list of candidates.
|
||||
*
|
||||
* This encapsulates the scoring-based overload selection logic that considers:
|
||||
* - Exact type matches
|
||||
* - Numeric promotion compatibility
|
||||
* - Arity (parameter count) matching
|
||||
* - Varargs handling
|
||||
* - Unknown (null) argument types
|
||||
*
|
||||
* Lower scores indicate better matches.
|
||||
*/
|
||||
public class OverloadSelector {
|
||||
|
||||
// Scoring constants
|
||||
private static final int ARITY_MISMATCH_BASE = 10000;
|
||||
private static final int VARARGS_PENALTY = 50;
|
||||
private static final int UNKNOWN_TYPE_PENALTY = 10;
|
||||
private static final int COMPATIBLE_TYPE_PENALTY = 5;
|
||||
private static final int INCOMPATIBLE_TYPE_PENALTY = 100;
|
||||
|
||||
/**
|
||||
* Select the best overload from a list of candidates based on argument types.
|
||||
*
|
||||
* Selection phases:
|
||||
* 1. Exact match (all types match exactly)
|
||||
* 2. Numeric promotion match (all args can promote to common param type)
|
||||
* 3. Scoring-based selection (handles unknown types, varargs, etc.)
|
||||
*
|
||||
* @param overloads List of candidate method overloads
|
||||
* @param argTypes Array of argument types (may contain nulls for unknown types)
|
||||
* @return The best matching overload, or the first overload if no good match found
|
||||
*/
|
||||
public static MethodInfo selectBestOverload(List<MethodInfo> overloads, TypeInfo[] argTypes) {
|
||||
if (overloads == null || overloads.isEmpty()) return null;
|
||||
|
||||
int argCount = (argTypes == null) ? 0 : argTypes.length;
|
||||
|
||||
// Fast path: If no arguments provided, try to find zero-arg method
|
||||
if (argCount == 0) {
|
||||
for (MethodInfo method : overloads) {
|
||||
if (method.getParameterCount() == 0) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
// Fall back to closest arity
|
||||
return selectClosestArity(overloads, 0);
|
||||
}
|
||||
|
||||
// Check if all arg types are known (non-null)
|
||||
boolean allArgsKnown = true;
|
||||
for (TypeInfo argType : argTypes) {
|
||||
if (argType == null) {
|
||||
allArgsKnown = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 1: Try exact match (only when all args known)
|
||||
if (allArgsKnown) {
|
||||
MethodInfo exactMatch = findExactMatch(overloads, argTypes, argCount);
|
||||
if (exactMatch != null) return exactMatch;
|
||||
}
|
||||
|
||||
// Phase 2: Try numeric promotion match (only when all args known)
|
||||
if (allArgsKnown) {
|
||||
MethodInfo numericMatch = findNumericPromotionMatch(overloads, argTypes, argCount);
|
||||
if (numericMatch != null) return numericMatch;
|
||||
}
|
||||
|
||||
// Phase 3: Scoring-based selection with arity-first preference
|
||||
// This handles unknown (null) arg types gracefully and prefers fixed-arity over varargs
|
||||
return scoringBasedSelection(overloads, argTypes, argCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an overload with exact type match for all arguments.
|
||||
*/
|
||||
private static MethodInfo findExactMatch(List<MethodInfo> overloads, TypeInfo[] argTypes, int argCount) {
|
||||
for (MethodInfo method : overloads) {
|
||||
if (method.getParameterCount() == argCount) {
|
||||
boolean exactMatch = true;
|
||||
List<FieldInfo> params = method.getParameters();
|
||||
for (int i = 0; i < argCount; i++) {
|
||||
TypeInfo paramType = params.get(i).getTypeInfo();
|
||||
TypeInfo argType = argTypes[i];
|
||||
if (paramType == null || !paramType.equals(argType)) {
|
||||
exactMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (exactMatch) return method;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an overload where all numeric arguments can promote to a common parameter type.
|
||||
* Prefers narrower numeric types (e.g., int over long over double).
|
||||
*/
|
||||
private static MethodInfo findNumericPromotionMatch(List<MethodInfo> overloads, TypeInfo[] argTypes, int argCount) {
|
||||
MethodInfo bestNumericMatch = null;
|
||||
int bestNumericRank = Integer.MAX_VALUE;
|
||||
|
||||
for (MethodInfo method : overloads) {
|
||||
if (method.getParameterCount() == argCount) {
|
||||
List<FieldInfo> params = method.getParameters();
|
||||
|
||||
// Check if all parameters and arguments are numeric primitives
|
||||
boolean allNumeric = true;
|
||||
for (int i = 0; i < argCount; i++) {
|
||||
TypeInfo paramType = params.get(i).getTypeInfo();
|
||||
TypeInfo argType = argTypes[i];
|
||||
if (paramType == null ||
|
||||
!TypeChecker.isNumericPrimitive(paramType) || !TypeChecker.isNumericPrimitive(argType)) {
|
||||
allNumeric = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allNumeric) {
|
||||
// Check if all parameters are the same numeric type
|
||||
TypeInfo commonParamType = params.get(0).getTypeInfo();
|
||||
boolean allParamsSame = true;
|
||||
for (int i = 1; i < params.size(); i++) {
|
||||
TypeInfo paramType = params.get(i).getTypeInfo();
|
||||
if (!paramType.equals(commonParamType)) {
|
||||
allParamsSame = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If all parameters are the same numeric type, check if args can promote to it
|
||||
if (allParamsSame) {
|
||||
boolean canPromote = true;
|
||||
for (int i = 0; i < argCount; i++) {
|
||||
if (!TypeChecker.canPromoteNumeric(argTypes[i], commonParamType)) {
|
||||
canPromote = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If all args can promote, check if this is the narrowest match so far
|
||||
if (canPromote) {
|
||||
int paramRank = TypeChecker.getNumericRank(commonParamType.getJavaClass());
|
||||
if (paramRank < bestNumericRank) {
|
||||
bestNumericRank = paramRank;
|
||||
bestNumericMatch = method;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestNumericMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use scoring to select the best overload when exact/numeric matches don't apply.
|
||||
*/
|
||||
private static MethodInfo scoringBasedSelection(List<MethodInfo> overloads, TypeInfo[] argTypes, int argCount) {
|
||||
MethodInfo bestCandidate = null;
|
||||
int bestScore = Integer.MAX_VALUE;
|
||||
|
||||
for (MethodInfo method : overloads) {
|
||||
int score = scoreOverload(method, argTypes, argCount);
|
||||
if (score < bestScore) {
|
||||
bestScore = score;
|
||||
bestCandidate = method;
|
||||
}
|
||||
}
|
||||
|
||||
return (bestCandidate != null) ? bestCandidate : overloads.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Score an overload for matching against provided arguments.
|
||||
* Lower score = better match.
|
||||
*
|
||||
* Scoring priorities:
|
||||
* - Arity mismatch: +10000 + |paramCount - argCount|
|
||||
* - Varargs penalty: +1000
|
||||
* - Unknown arg type (null): +10 per arg
|
||||
* - Compatible but not exact: +5 per arg
|
||||
* - Incompatible type: +100 per arg
|
||||
* - Exact type match: +0 per arg
|
||||
*
|
||||
* @param method The method overload to score
|
||||
* @param argTypes Array of argument types (may contain nulls)
|
||||
* @param argCount Number of arguments
|
||||
* @return Score (lower is better)
|
||||
*/
|
||||
public static int scoreOverload(MethodInfo method, TypeInfo[] argTypes, int argCount) {
|
||||
int score = 0;
|
||||
int paramCount = method.getParameterCount();
|
||||
|
||||
// Check if method is varargs
|
||||
boolean isVarArgs = false;
|
||||
java.lang.reflect.Method javaMethod = method.getJavaMethod();
|
||||
if (javaMethod != null) {
|
||||
isVarArgs = javaMethod.isVarArgs();
|
||||
} else {
|
||||
// JS methods from .d.ts have no backing Java method; vararg info lives on the last FieldInfo
|
||||
List<FieldInfo> checkParams = method.getParameters();
|
||||
if (!checkParams.isEmpty()) {
|
||||
isVarArgs = checkParams.get(checkParams.size() - 1).isVarArg();
|
||||
}
|
||||
}
|
||||
|
||||
// Determine arity applicability
|
||||
boolean arityApplicable;
|
||||
if (isVarArgs) {
|
||||
// Varargs methods accept argCount >= paramCount - 1
|
||||
// (the varargs array can be empty or have multiple elements)
|
||||
arityApplicable = argCount >= paramCount - 1;
|
||||
} else {
|
||||
arityApplicable = paramCount == argCount;
|
||||
}
|
||||
|
||||
if (!arityApplicable) {
|
||||
// Large penalty for arity mismatch, plus distance to encourage closest match
|
||||
score += ARITY_MISMATCH_BASE + Math.abs(paramCount - argCount);
|
||||
}
|
||||
|
||||
// Varargs penalty (fixed-arity preferred over varargs when both apply)
|
||||
if (isVarArgs) {
|
||||
score += VARARGS_PENALTY;
|
||||
}
|
||||
|
||||
// Score each argument's type compatibility
|
||||
List<FieldInfo> params = method.getParameters();
|
||||
int paramsToCheck = Math.min(argCount, paramCount);
|
||||
|
||||
for (int i = 0; i < paramsToCheck; i++) {
|
||||
TypeInfo paramType = params.get(i).getTypeInfo();
|
||||
TypeInfo argType = (argTypes != null && i < argTypes.length) ? argTypes[i] : null;
|
||||
|
||||
if (argType == null) {
|
||||
// Unknown arg type - don't disqualify, but add penalty
|
||||
score += UNKNOWN_TYPE_PENALTY;
|
||||
} else if (paramType == null) {
|
||||
// Unknown param type - add penalty
|
||||
score += UNKNOWN_TYPE_PENALTY;
|
||||
} else if (paramType.equals(argType)) {
|
||||
// Exact match - no penalty
|
||||
score += 0;
|
||||
} else if (TypeChecker.isTypeCompatible(paramType, argType)) {
|
||||
// Compatible but not exact
|
||||
score += COMPATIBLE_TYPE_PENALTY;
|
||||
} else {
|
||||
// Incompatible type
|
||||
score += INCOMPATIBLE_TYPE_PENALTY;
|
||||
}
|
||||
}
|
||||
|
||||
// For vararg methods, also score extra args (index >= paramCount) against the element type
|
||||
if (isVarArgs && argCount > paramCount && !params.isEmpty()) {
|
||||
TypeInfo varargElemType = params.get(paramCount - 1).getTypeInfo();
|
||||
for (int i = paramCount; i < argCount; i++) {
|
||||
TypeInfo argType = (argTypes != null && i < argTypes.length) ? argTypes[i] : null;
|
||||
if (argType == null || varargElemType == null) {
|
||||
score += UNKNOWN_TYPE_PENALTY;
|
||||
} else if (varargElemType.equals(argType)) {
|
||||
// exact match
|
||||
} else if (TypeChecker.isTypeCompatible(varargElemType, argType)) {
|
||||
score += COMPATIBLE_TYPE_PENALTY;
|
||||
} else {
|
||||
score += INCOMPATIBLE_TYPE_PENALTY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Penalize extra args that don't match params (for non-varargs)
|
||||
if (!isVarArgs && argCount > paramCount) {
|
||||
score += (argCount - paramCount) * INCOMPATIBLE_TYPE_PENALTY;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the overload with closest arity to the target.
|
||||
* Tie-breaker: lower parameter count wins.
|
||||
*
|
||||
* @param overloads List of candidate overloads
|
||||
* @param targetArity The desired number of parameters
|
||||
* @return The overload with closest arity, or first overload if list is empty
|
||||
*/
|
||||
public static MethodInfo selectClosestArity(List<MethodInfo> overloads, int targetArity) {
|
||||
if (overloads == null || overloads.isEmpty()) return null;
|
||||
|
||||
MethodInfo best = null;
|
||||
int bestDistance = Integer.MAX_VALUE;
|
||||
int bestParamCount = Integer.MAX_VALUE;
|
||||
|
||||
for (MethodInfo method : overloads) {
|
||||
int paramCount = method.getParameterCount();
|
||||
int distance = Math.abs(paramCount - targetArity);
|
||||
|
||||
// Prefer smaller distance; tie-break by lower paramCount
|
||||
if (distance < bestDistance || (distance == bestDistance && paramCount < bestParamCount)) {
|
||||
best = method;
|
||||
bestDistance = distance;
|
||||
bestParamCount = paramCount;
|
||||
}
|
||||
}
|
||||
|
||||
return (best != null) ? best : overloads.get(0);
|
||||
}
|
||||
}
|
||||
+945
@@ -0,0 +1,945 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.type;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
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.TokenType;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Represents a type defined within the script itself (not from Java classpath).
|
||||
* This handles classes, interfaces, and enums declared in the user's script.
|
||||
*
|
||||
* Unlike TypeInfo which wraps a Java Class<?>, ScriptTypeInfo stores all metadata
|
||||
* about fields, methods, and other type information parsed from the script.
|
||||
*/
|
||||
public class ScriptTypeInfo extends TypeInfo {
|
||||
|
||||
private final String scriptClassName;
|
||||
private final int declarationOffset;
|
||||
private final int bodyStart;
|
||||
private final int bodyEnd;
|
||||
private final int modifiers; // Java reflection modifiers (public, static, final, etc.)
|
||||
|
||||
// Script-defined members
|
||||
private final Map<String, FieldInfo> fields = new HashMap<>();
|
||||
private final Map<String, List<MethodInfo>> methods = new HashMap<>(); // name -> list of overloads
|
||||
private final List<MethodInfo> constructors = new ArrayList<>(); // List of constructors
|
||||
private final List<ScriptTypeInfo> innerClasses = new ArrayList<>();
|
||||
|
||||
// Enum constants (for enum types only) - name -> EnumConstantInfo
|
||||
private final Map<String, EnumConstantInfo> enumConstants = new HashMap<>();
|
||||
|
||||
// Parent class reference (for inner class resolution)
|
||||
private ScriptTypeInfo outerClass;
|
||||
|
||||
// ==================== INHERITANCE ====================
|
||||
|
||||
/**
|
||||
* The parent/super class for this type (from "extends ParentClass").
|
||||
* Can be a resolved TypeInfo or ScriptTypeInfo, or unresolved TypeInfo if the parent is not found.
|
||||
* Null if this type doesn't extend anything (or extends Object implicitly).
|
||||
*/
|
||||
private TypeInfo superClass;
|
||||
|
||||
/**
|
||||
* The raw string name of the super class as written in the script (e.g., "ParentClass").
|
||||
* Stored for display purposes even when the type couldn't be resolved.
|
||||
*/
|
||||
private String superClassName;
|
||||
|
||||
/**
|
||||
* All implemented interfaces (from "implements Interface1, Interface2, ...").
|
||||
* Each can be resolved or unresolved. The list order matches the declaration order.
|
||||
*/
|
||||
private final List<TypeInfo> implementedInterfaces = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The raw string names of implemented interfaces as written in the script.
|
||||
* Stored for display purposes even when types couldn't be resolved.
|
||||
*/
|
||||
private final List<String> implementedInterfaceNames = new ArrayList<>();
|
||||
|
||||
private ScriptTypeInfo(String simpleName, String fullName, Kind kind,
|
||||
int declarationOffset, int bodyStart, int bodyEnd, int modifiers) {
|
||||
super(simpleName, fullName, "", kind, null, true, null, true);
|
||||
this.scriptClassName = simpleName;
|
||||
this.declarationOffset = declarationOffset;
|
||||
this.bodyStart = bodyStart;
|
||||
this.bodyEnd = bodyEnd;
|
||||
this.modifiers = modifiers;
|
||||
}
|
||||
|
||||
// Factory method
|
||||
public static ScriptTypeInfo create(String simpleName, Kind kind,
|
||||
int declarationOffset, int bodyStart, int bodyEnd, int modifiers) {
|
||||
return new ScriptTypeInfo(simpleName, simpleName, kind, declarationOffset, bodyStart, bodyEnd, modifiers);
|
||||
}
|
||||
|
||||
public static ScriptTypeInfo createInner(String simpleName, Kind kind, ScriptTypeInfo outer,
|
||||
int declarationOffset, int bodyStart, int bodyEnd, int modifiers) {
|
||||
String fullName = outer.getFullName() + "$" + simpleName;
|
||||
ScriptTypeInfo inner = new ScriptTypeInfo(simpleName, fullName, kind, declarationOffset, bodyStart, bodyEnd, modifiers);
|
||||
inner.outerClass = outer;
|
||||
outer.innerClasses.add(inner);
|
||||
return inner;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getScriptClassName() { return scriptClassName; }
|
||||
public int getDeclarationOffset() { return declarationOffset; }
|
||||
public int getBodyStart() { return bodyStart; }
|
||||
public int getBodyEnd() { return bodyEnd; }
|
||||
public int getModifiers() { return modifiers; }
|
||||
public ScriptTypeInfo getOuterClass() { return outerClass; }
|
||||
public List<ScriptTypeInfo> getInnerClasses() { return innerClasses; }
|
||||
|
||||
// ==================== INHERITANCE ====================
|
||||
|
||||
/**
|
||||
* Get the super class (from "extends"). Can be resolved or unresolved.
|
||||
* @return The parent class TypeInfo, or null if no extends clause
|
||||
*/
|
||||
public TypeInfo getSuperClass() { return superClass; }
|
||||
|
||||
/**
|
||||
* Get the raw super class name as written in the script.
|
||||
* @return The super class name string, or null if no extends clause
|
||||
*/
|
||||
public String getSuperClassName() { return superClassName; }
|
||||
|
||||
/**
|
||||
* Set the super class info. Call this after resolving types.
|
||||
* @param superClass The resolved or unresolved TypeInfo for the parent class
|
||||
* @param superClassName The raw class name as written in the script
|
||||
*/
|
||||
public void setSuperClass(TypeInfo superClass, String superClassName) {
|
||||
this.superClass = superClass;
|
||||
this.superClassName = superClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this type has a super class (extends something).
|
||||
*/
|
||||
public boolean hasSuperClass() { return superClass != null || superClassName != null; }
|
||||
|
||||
/**
|
||||
* Get all implemented interfaces. Each can be resolved or unresolved.
|
||||
* @return Unmodifiable list of implemented interface TypeInfos
|
||||
*/
|
||||
public List<TypeInfo> getImplementedInterfaces() {
|
||||
return new ArrayList<>(implementedInterfaces);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw interface names as written in the script.
|
||||
* @return Unmodifiable list of interface name strings
|
||||
*/
|
||||
public List<String> getImplementedInterfaceNames() {
|
||||
return new ArrayList<>(implementedInterfaceNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an implemented interface. Call this after resolving types.
|
||||
* @param interfaceType The resolved or unresolved TypeInfo for the interface
|
||||
* @param interfaceName The raw interface name as written in the script
|
||||
*/
|
||||
public void addImplementedInterface(TypeInfo interfaceType, String interfaceName) {
|
||||
implementedInterfaces.add(interfaceType);
|
||||
implementedInterfaceNames.add(interfaceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this type implements any interfaces.
|
||||
*/
|
||||
public boolean hasImplementedInterfaces() { return !implementedInterfaces.isEmpty(); }
|
||||
|
||||
/**
|
||||
* Check if this type implements a specific interface (by simple name).
|
||||
*/
|
||||
public boolean implementsInterface(String interfaceName) {
|
||||
for (String name : implementedInterfaceNames) {
|
||||
if (name.equals(interfaceName)) return true;
|
||||
}
|
||||
for (TypeInfo ti : implementedInterfaces) {
|
||||
if (ti.getSimpleName().equals(interfaceName)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a position is inside this type's body.
|
||||
*/
|
||||
public boolean containsPosition(int position) {
|
||||
return position >= bodyStart && position < bodyEnd;
|
||||
}
|
||||
|
||||
// ==================== FIELD MANAGEMENT ====================
|
||||
|
||||
public void addField(FieldInfo field) {
|
||||
fields.put(field.getName(), field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasField(String fieldName) {
|
||||
if (fields.containsKey(fieldName))
|
||||
return true;
|
||||
|
||||
if (enumConstants.containsKey(fieldName))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FieldInfo getFieldInfo(String fieldName) {
|
||||
if (fields.containsKey(fieldName))
|
||||
return fields.get(fieldName);
|
||||
|
||||
if (enumConstants.containsKey(fieldName)) {
|
||||
EnumConstantInfo enumConst = enumConstants.get(fieldName);
|
||||
if (enumConst != null)
|
||||
return enumConst.getFieldInfo();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Map<String, FieldInfo> getFields() {
|
||||
return new HashMap<>(fields);
|
||||
}
|
||||
|
||||
// ==================== ENUM CONSTANT MANAGEMENT ====================
|
||||
|
||||
/**
|
||||
* Add an enum constant to this enum type.
|
||||
* Only valid for enum types.
|
||||
*/
|
||||
public void addEnumConstant(EnumConstantInfo constant) {
|
||||
if (isEnum()) {
|
||||
enumConstants.put(constant.getFieldInfo().getName(), constant);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this enum has a constant with the given name.
|
||||
*/
|
||||
public boolean hasEnumConstant(String constantName) {
|
||||
return enumConstants.containsKey(constantName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an enum constant by name.
|
||||
*/
|
||||
public EnumConstantInfo getEnumConstant(String constantName) {
|
||||
return enumConstants.get(constantName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all enum constants.
|
||||
*/
|
||||
public Map<String, EnumConstantInfo> getEnumConstants() {
|
||||
return new HashMap<>(enumConstants);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is an enum type and has any constants.
|
||||
*/
|
||||
public boolean hasEnumConstants() {
|
||||
return isEnum() && !enumConstants.isEmpty();
|
||||
}
|
||||
|
||||
// ==================== METHOD MANAGEMENT ====================
|
||||
|
||||
public void addMethod(MethodInfo method) {
|
||||
methods.computeIfAbsent(method.getName(), k -> new ArrayList<>()).add(method);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMethod(String methodName) {
|
||||
return methods.containsKey(methodName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMethod(String methodName, int paramCount) {
|
||||
List<MethodInfo> overloads = methods.get(methodName);
|
||||
if (overloads == null) return false;
|
||||
for (MethodInfo m : overloads) {
|
||||
if (m.getParameterCount() == paramCount) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodInfo getMethodInfo(String methodName) {
|
||||
List<MethodInfo> overloads = methods.get(methodName);
|
||||
return (overloads != null && !overloads.isEmpty()) ? overloads.get(0) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all method overloads with the given name.
|
||||
*/
|
||||
public List<MethodInfo> getAllMethodOverloads(String methodName) {
|
||||
return methods.getOrDefault(methodName, new ArrayList<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a method with specific parameter count.
|
||||
*/
|
||||
public MethodInfo getMethodWithParamCount(String methodName, int paramCount) {
|
||||
List<MethodInfo> overloads = methods.get(methodName);
|
||||
if (overloads == null) return null;
|
||||
for (MethodInfo m : overloads) {
|
||||
if (m.getParameterCount() == paramCount) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Map<String, List<MethodInfo>> getMethods() {
|
||||
return new HashMap<>(methods);
|
||||
}
|
||||
|
||||
public List<MethodInfo> getAllMethodsFlat() {
|
||||
List<MethodInfo> allMethods = new ArrayList<>();
|
||||
for (List<MethodInfo> overloads : methods.values()) {
|
||||
allMethods.addAll(overloads);
|
||||
}
|
||||
return allMethods;
|
||||
}
|
||||
|
||||
// ==================== CONSTRUCTOR MANAGEMENT ====================
|
||||
|
||||
public void addConstructor(MethodInfo constructor) {
|
||||
constructors.add(constructor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MethodInfo> getConstructors() {
|
||||
return new ArrayList<>(constructors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasConstructors() {
|
||||
return !constructors.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the best matching constructor for the given argument count.
|
||||
* Returns null if no constructor matches.
|
||||
*/
|
||||
@Override
|
||||
public MethodInfo findConstructor(int argCount) {
|
||||
for (MethodInfo constructor : constructors) {
|
||||
if (constructor.getParameterCount() == argCount) {
|
||||
return constructor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public MethodInfo findConstructor(TypeInfo[] argTypes) {
|
||||
for (MethodInfo constructor : constructors) {
|
||||
if (constructor.getParameterCount() == argTypes.length) {
|
||||
boolean match = true;
|
||||
List<FieldInfo> params = constructor.getParameters();
|
||||
for (int i = 0; i < argTypes.length; i++) {
|
||||
TypeInfo paramType = params.get(i).getTypeInfo();
|
||||
if (!TypeChecker.isTypeCompatible(paramType, argTypes[i])) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match)
|
||||
return constructor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== INHERITANCE HIERARCHY SEARCH ====================
|
||||
|
||||
/**
|
||||
* Check if this type or any of its parent classes has a field with the given name.
|
||||
* This recursively searches up the inheritance tree.
|
||||
*/
|
||||
public boolean hasFieldInHierarchy(String fieldName) {
|
||||
// Check this class first
|
||||
if (hasField(fieldName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check parent class recursively
|
||||
if (superClass != null && superClass.isResolved()) {
|
||||
if (superClass instanceof ScriptTypeInfo) {
|
||||
return ((ScriptTypeInfo) superClass).hasFieldInHierarchy(fieldName);
|
||||
} else {
|
||||
// For Java classes, hasField already checks inheritance via reflection
|
||||
return superClass.hasField(fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field info from this type or any of its parent classes.
|
||||
* This recursively searches up the inheritance tree.
|
||||
*/
|
||||
public FieldInfo getFieldInfoInHierarchy(String fieldName) {
|
||||
// Check this class first
|
||||
FieldInfo field = getFieldInfo(fieldName);
|
||||
if (field != null) {
|
||||
return field;
|
||||
}
|
||||
|
||||
// Check parent class recursively
|
||||
if (superClass != null && superClass.isResolved()) {
|
||||
if (superClass instanceof ScriptTypeInfo) {
|
||||
return ((ScriptTypeInfo) superClass).getFieldInfoInHierarchy(fieldName);
|
||||
} else {
|
||||
// For Java classes, getFieldInfo already checks inheritance via reflection
|
||||
return superClass.getFieldInfo(fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this type or any of its parent classes has a method with the given name.
|
||||
* This recursively searches up the inheritance tree.
|
||||
*/
|
||||
public boolean hasMethodInHierarchy(String methodName) {
|
||||
// Check this class first
|
||||
if (hasMethod(methodName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check parent class recursively
|
||||
if (superClass != null && superClass.isResolved()) {
|
||||
if (superClass instanceof ScriptTypeInfo) {
|
||||
return ((ScriptTypeInfo) superClass).hasMethodInHierarchy(methodName);
|
||||
} else {
|
||||
// For Java classes, hasMethod already checks inheritance via reflection
|
||||
return superClass.hasMethod(methodName);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this type or any of its parent classes has a method with the given name and parameter count.
|
||||
* This recursively searches up the inheritance tree.
|
||||
*/
|
||||
public boolean hasMethodInHierarchy(String methodName, int paramCount) {
|
||||
// Check this class first
|
||||
if (hasMethod(methodName, paramCount)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check parent class recursively
|
||||
if (superClass != null && superClass.isResolved()) {
|
||||
if (superClass instanceof ScriptTypeInfo) {
|
||||
return ((ScriptTypeInfo) superClass).hasMethodInHierarchy(methodName, paramCount);
|
||||
} else {
|
||||
// For Java classes, hasMethod already checks inheritance via reflection
|
||||
return superClass.hasMethod(methodName, paramCount);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method info from this type or any of its parent classes.
|
||||
* This recursively searches up the inheritance tree.
|
||||
*/
|
||||
public MethodInfo getMethodInfoInHierarchy(String methodName) {
|
||||
// Check this class first
|
||||
MethodInfo method = getMethodInfo(methodName);
|
||||
if (method != null) {
|
||||
return method;
|
||||
}
|
||||
|
||||
// Check parent class recursively
|
||||
if (superClass != null && superClass.isResolved()) {
|
||||
if (superClass instanceof ScriptTypeInfo) {
|
||||
return ((ScriptTypeInfo) superClass).getMethodInfoInHierarchy(methodName);
|
||||
} else {
|
||||
// For Java classes, getMethodInfo already checks inheritance via reflection
|
||||
return superClass.getMethodInfo(methodName);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method info with specific parameter count from this type or any of its parent classes.
|
||||
* This recursively searches up the inheritance tree.
|
||||
*/
|
||||
public MethodInfo getMethodWithParamCountInHierarchy(String methodName, int paramCount) {
|
||||
// Check this class first
|
||||
MethodInfo method = getMethodWithParamCount(methodName, paramCount);
|
||||
if (method != null) {
|
||||
return method;
|
||||
}
|
||||
|
||||
// Check parent class recursively
|
||||
if (superClass != null && superClass.isResolved()) {
|
||||
if (superClass instanceof ScriptTypeInfo) {
|
||||
return ((ScriptTypeInfo) superClass).getMethodWithParamCountInHierarchy(methodName, paramCount);
|
||||
} else {
|
||||
// For Java classes, reflection already handles inheritance
|
||||
// We need to manually search for matching method
|
||||
List<MethodInfo> overloads = superClass.getAllMethodOverloads(methodName);
|
||||
for (MethodInfo m : overloads) {
|
||||
if (m.getParameterCount() == paramCount) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all method overloads with the given name from this type or any of its parent classes.
|
||||
* This recursively searches up the inheritance tree and returns all matching overloads.
|
||||
*/
|
||||
public List<MethodInfo> getAllMethodOverloadsInHierarchy(String methodName) {
|
||||
List<MethodInfo> result = new ArrayList<>();
|
||||
|
||||
// Get overloads from this class
|
||||
result.addAll(getAllMethodOverloads(methodName));
|
||||
|
||||
// Get overloads from parent class recursively
|
||||
if (superClass != null && superClass.isResolved()) {
|
||||
if (superClass instanceof ScriptTypeInfo) {
|
||||
result.addAll(((ScriptTypeInfo) superClass).getAllMethodOverloadsInHierarchy(methodName));
|
||||
} else {
|
||||
// For Java classes, getAllMethodOverloads already checks inheritance via reflection
|
||||
result.addAll(superClass.getAllMethodOverloads(methodName));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override getBestMethodOverload to search the inheritance hierarchy.
|
||||
* This ensures that method overload resolution considers parent class methods.
|
||||
*/
|
||||
@Override
|
||||
public MethodInfo getBestMethodOverload(String methodName, TypeInfo[] argTypes) {
|
||||
// Get all overloads from this class and parent classes
|
||||
List<MethodInfo> allOverloads = getAllMethodOverloadsInHierarchy(methodName);
|
||||
if (allOverloads.isEmpty())
|
||||
return null;
|
||||
|
||||
// If no arguments provided, try to find zero-arg method
|
||||
if (argTypes == null || argTypes.length == 0) {
|
||||
for (MethodInfo method : allOverloads) {
|
||||
if (method.getParameterCount() == 0) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
// Fall back to first overload if no zero-arg found
|
||||
return allOverloads.get(0);
|
||||
}
|
||||
|
||||
// Phase 1: Try exact match
|
||||
for (MethodInfo method : allOverloads) {
|
||||
if (method.getParameterCount() == argTypes.length) {
|
||||
boolean exactMatch = true;
|
||||
List<FieldInfo> params = method.getParameters();
|
||||
for (int i = 0; i < argTypes.length; i++) {
|
||||
TypeInfo paramType = params.get(i).getTypeInfo();
|
||||
TypeInfo argType = argTypes[i];
|
||||
if (paramType == null || argType == null || !paramType.equals(argType)) {
|
||||
exactMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (exactMatch)
|
||||
return method;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Try compatible type match (assignability)
|
||||
for (MethodInfo method : allOverloads) {
|
||||
if (method.getParameterCount() == argTypes.length) {
|
||||
boolean compatible = true;
|
||||
List<FieldInfo> params = method.getParameters();
|
||||
for (int i = 0; i < argTypes.length; i++) {
|
||||
TypeInfo paramType = params.get(i).getTypeInfo();
|
||||
TypeInfo argType = argTypes[i];
|
||||
if (paramType == null || argType == null) {
|
||||
continue; // Allow null types (unresolved)
|
||||
}
|
||||
// Check if argType can be assigned to paramType
|
||||
if (!noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker.isTypeCompatible(paramType,
|
||||
argType)) {
|
||||
compatible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (compatible)
|
||||
return method;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Fall back to first overload with matching parameter count
|
||||
for (MethodInfo method : allOverloads) {
|
||||
if (method.getParameterCount() == argTypes.length) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
|
||||
// No match found, return first overload
|
||||
return allOverloads.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override getBestMethodOverload with return type expectation to search the inheritance hierarchy.
|
||||
*/
|
||||
@Override
|
||||
public MethodInfo getBestMethodOverload(String methodName, TypeInfo expectedReturnType) {
|
||||
List<MethodInfo> allOverloads = getAllMethodOverloadsInHierarchy(methodName);
|
||||
if (allOverloads.isEmpty())
|
||||
return null;
|
||||
|
||||
// If no expected return type, return first overload
|
||||
if (expectedReturnType == null) {
|
||||
return allOverloads.get(0);
|
||||
}
|
||||
|
||||
// First pass: look for return type compatible overload
|
||||
for (MethodInfo method : allOverloads) {
|
||||
TypeInfo returnType = method.getReturnType();
|
||||
if (returnType != null && noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker.isTypeCompatible(
|
||||
expectedReturnType, returnType)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
// Second pass: return any overload (first one)
|
||||
return allOverloads.get(0);
|
||||
}
|
||||
|
||||
// ==================== INNER CLASS LOOKUP ====================
|
||||
|
||||
/**
|
||||
* Get the dot-separated display name for this type.
|
||||
* Top-level: "MyClass"
|
||||
* Inner: "Outer.Inner"
|
||||
* Deeply nested: "Outer.Middle.Inner"
|
||||
*
|
||||
* This is what users write in type annotations and what resolveType() should accept.
|
||||
* Recursively walks the outerClass chain to build the full user-facing qualified name.
|
||||
*/
|
||||
public String getDotSeparatedName() {
|
||||
if (outerClass == null) {
|
||||
return getSimpleName();
|
||||
}
|
||||
// Recurse up the nesting hierarchy: each level prepends "Parent." to the name
|
||||
return outerClass.getDotSeparatedName() + "." + getSimpleName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an inner class by name.
|
||||
*/
|
||||
public ScriptTypeInfo getInnerClass(String name) {
|
||||
for (ScriptTypeInfo inner : innerClasses) {
|
||||
if (inner.getSimpleName().equals(name)) {
|
||||
return inner;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Script types are always considered resolved since they're defined in the script
|
||||
@Override
|
||||
public boolean isResolved() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Script types don't have a Java class
|
||||
@Override
|
||||
public Class<?> getJavaClass() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ScriptTypeInfo{" + scriptClassName + ", " + getKind() +
|
||||
", fields=" + fields.size() + ", methods=" + methods.size() + "}";
|
||||
}
|
||||
|
||||
// ==================== ERROR HANDLING ====================
|
||||
|
||||
/**
|
||||
* Error types for ScriptTypeInfo validation.
|
||||
*/
|
||||
public enum ErrorType {
|
||||
NONE,
|
||||
MISSING_INTERFACE_METHOD, // Class doesn't implement all interface methods
|
||||
MISSING_CONSTRUCTOR_MATCH, // Class extends parent but has no matching constructor
|
||||
UNRESOLVED_PARENT, // Parent class cannot be resolved
|
||||
UNRESOLVED_INTERFACE // Implemented interface cannot be resolved
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a missing interface method error.
|
||||
*/
|
||||
|
||||
// Error tracking
|
||||
private ErrorType errorType = ErrorType.NONE;
|
||||
private String errorMessage;
|
||||
private final List<MissingMethodError> missingMethodErrors = new ArrayList<>();
|
||||
private final List<ConstructorMismatchError> constructorMismatchErrors = new ArrayList<>();
|
||||
|
||||
// Error getters
|
||||
public ErrorType getErrorType() { return errorType; }
|
||||
public String getErrorMessage() { return errorMessage; }
|
||||
public boolean hasError() { return errorType != ErrorType.NONE || !missingMethodErrors.isEmpty() || !constructorMismatchErrors.isEmpty(); }
|
||||
public List<MissingMethodError> getMissingMethodErrors() { return new ArrayList<>(missingMethodErrors); }
|
||||
public List<ConstructorMismatchError> getConstructorMismatchErrors() { return new ArrayList<>(constructorMismatchErrors); }
|
||||
|
||||
/**
|
||||
* Set a general error on this type.
|
||||
*/
|
||||
public void setError(ErrorType type, String message) {
|
||||
this.errorType = type;
|
||||
this.errorMessage = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a missing interface method error.
|
||||
*/
|
||||
public void addMissingMethodError(TypeInfo interfaceType, String signature) {
|
||||
missingMethodErrors.add(new MissingMethodError(interfaceType, signature));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a constructor mismatch error.
|
||||
*/
|
||||
public void addConstructorMismatchError(TypeInfo parentType, String parentConstructorSignature) {
|
||||
constructorMismatchErrors.add(new ConstructorMismatchError(parentType, parentConstructorSignature));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all errors on this type.
|
||||
*/
|
||||
public void clearErrors() {
|
||||
errorType = ErrorType.NONE;
|
||||
errorMessage = null;
|
||||
missingMethodErrors.clear();
|
||||
constructorMismatchErrors.clear();
|
||||
}
|
||||
|
||||
public static class MissingMethodError {
|
||||
private final TypeInfo interfaceType;
|
||||
private final String signature;
|
||||
|
||||
public MissingMethodError(TypeInfo interfaceType, String signature) {
|
||||
this.interfaceType = interfaceType;
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return "Class must implement method '" + signature + "' from interface " + interfaceType.getSimpleName();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a constructor mismatch error.
|
||||
*/
|
||||
public static class ConstructorMismatchError {
|
||||
private final TypeInfo parentType;
|
||||
private final String parentConstructorSignature;
|
||||
|
||||
public ConstructorMismatchError(TypeInfo parentType, String parentConstructorSignature) {
|
||||
this.parentType = parentType;
|
||||
this.parentConstructorSignature = parentConstructorSignature;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return "Class extends " + parentType.getSimpleName() + " but has no constructor matching " + parentConstructorSignature;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== VALIDATION ====================
|
||||
|
||||
/**
|
||||
* Validate this script type for missing interface methods and constructor matching.
|
||||
*/
|
||||
@Override
|
||||
public void validate() {
|
||||
// Check that extending class has a matching constructor
|
||||
if (hasSuperClass()) {
|
||||
TypeInfo superClass = getSuperClass();
|
||||
if (superClass == null || !superClass.isResolved()) {
|
||||
setError(ErrorType.UNRESOLVED_PARENT,
|
||||
"Cannot resolve parent class " + getSuperClassName());
|
||||
} else {
|
||||
validateConstructorChain(superClass);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip validation for interfaces - they don't implement methods
|
||||
if (getKind() == Kind.INTERFACE)
|
||||
return;
|
||||
|
||||
// Check that all interface methods are implemented
|
||||
if (hasImplementedInterfaces()) {
|
||||
for (TypeInfo iface : getImplementedInterfaces()) {
|
||||
if (iface == null || !iface.isResolved()) {
|
||||
// Mark unresolved interface error
|
||||
setError(ErrorType.UNRESOLVED_INTERFACE, "Cannot resolve interface");
|
||||
continue;
|
||||
}
|
||||
validateInterfaceImplementation(iface);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that this script type implements all methods from an interface.
|
||||
*/
|
||||
private void validateInterfaceImplementation(TypeInfo iface) {
|
||||
// Handle Java interfaces
|
||||
Class<?> javaClass = iface.getJavaClass();
|
||||
if (javaClass != null && javaClass.isInterface()) {
|
||||
try {
|
||||
for (Method javaMethod : javaClass.getMethods()) {
|
||||
// Skip static and default methods
|
||||
if (Modifier.isStatic(javaMethod.getModifiers()))
|
||||
continue;
|
||||
|
||||
// Check if this type has a matching method
|
||||
String methodName = javaMethod.getName();
|
||||
int paramCount = javaMethod.getParameterCount();
|
||||
|
||||
boolean found = false;
|
||||
List<MethodInfo> overloads = getAllMethodOverloads(methodName);
|
||||
for (MethodInfo method : overloads) {
|
||||
if (parameterTypesMatch(method, javaMethod)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//Limit to one error at a time to not spam all missing methods
|
||||
if (!found && missingMethodErrors.isEmpty())
|
||||
addMissingMethodError(iface, MethodSignature.asString(javaMethod));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle script-defined interfaces (ScriptType)
|
||||
if (iface instanceof ScriptTypeInfo) {
|
||||
ScriptTypeInfo ifaceType = (ScriptTypeInfo) iface;
|
||||
|
||||
// Check all methods declared in the interface
|
||||
for (MethodInfo ifaceMethod : ifaceType.getAllMethodsFlat()) {
|
||||
MethodSignature ifaceSignature = ifaceMethod.getSignature();
|
||||
String methodName = ifaceMethod.getName();
|
||||
|
||||
boolean found = false;
|
||||
List<MethodInfo> overloads = getAllMethodOverloads(methodName);
|
||||
for (MethodInfo method : overloads) {
|
||||
if (method.getSignature().equals(ifaceSignature)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//Limit to one error at a time to not spam all missing methods
|
||||
if (!found && missingMethodErrors.isEmpty())
|
||||
addMissingMethodError(iface, ifaceSignature.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that this script type has a constructor compatible with its parent class.
|
||||
* This checks that for each parent constructor, there's a matching constructor in the child.
|
||||
*/
|
||||
private void validateConstructorChain(TypeInfo superClass) {
|
||||
// If no constructors defined in script type, it has an implicit default constructor
|
||||
// Check if parent has a no-arg constructor
|
||||
if (!hasConstructors()) {
|
||||
boolean parentHasNoArg = false;
|
||||
|
||||
if (superClass instanceof ScriptTypeInfo) {
|
||||
ScriptTypeInfo parentScript = (ScriptTypeInfo) superClass;
|
||||
if (!parentScript.hasConstructors() || parentScript.findConstructor(0) != null) {
|
||||
parentHasNoArg = true;
|
||||
}
|
||||
} else {
|
||||
Class<?> javaClass = superClass.getJavaClass();
|
||||
if (javaClass != null) {
|
||||
try {
|
||||
for (java.lang.reflect.Constructor<?> ctor : javaClass.getConstructors()) {
|
||||
if (ctor.getParameterCount() == 0) {
|
||||
parentHasNoArg = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Security error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!parentHasNoArg) {
|
||||
addConstructorMismatchError(superClass, superClass.getSimpleName());
|
||||
}
|
||||
}
|
||||
// If script type has constructors, we'd need to check super() calls - that's more complex
|
||||
// For now, we just validate the implicit default constructor case
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a MethodInfo's parameter types match a Java reflection Method's parameter types.
|
||||
*/
|
||||
private boolean parameterTypesMatch(MethodInfo methodInfo, Method javaMethod) {
|
||||
List<FieldInfo> params = methodInfo.getParameters();
|
||||
Class<?>[] javaParams = javaMethod.getParameterTypes();
|
||||
|
||||
if (params.size() != javaParams.length)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < params.size(); i++) {
|
||||
TypeInfo paramType = params.get(i).getTypeInfo();
|
||||
if (paramType == null)
|
||||
continue; // Unresolved param, skip check
|
||||
|
||||
Class<?> javaParamClass = javaParams[i];
|
||||
String javaParamName = javaParamClass.getName();
|
||||
|
||||
// Compare type names
|
||||
if (!paramType.getFullName().equals(javaParamName) &&
|
||||
!paramType.getSimpleName().equals(javaParamClass.getSimpleName())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user