IntelliJ Tooling plugin - #838
Draft
devnatan wants to merge 23 commits into
Draft
Conversation
Adds a new intellij-plugin Gradle module (IntelliJ Platform Gradle Plugin 2.11.0, kept on Gradle 8.x since 2.12+ requires Gradle 9) with a minimal plugin.xml and a no-op FileEditorProvider, as the foundation for a live inventory preview panel that renders next to open View classes.
Adds UAST-based detection (ViewDetector) that recognizes View subclasses and inline Views.rows/type/builder chains across Java and Kotlin, and wires a TextEditorWithPreview split editor showing a placeholder preview panel next to the source. FileEditor is implemented directly against UserDataHolderBase since FileEditorBase no longer exists on this platform version (confirmed empirically against the 2026.1 build 261 distribution).
Adds ViewExtractor (UAST-based) to pull ViewType, size, title, and layout() chars from ViewConfigBuilder/ViewBuilder call chains, using a hardcoded rows/columns table per view type since actual runtime evaluation of the user's ViewType constants is out of scope for the static analysis pass. The preview panel now paints an actual slot grid sized and shaped from the extracted model instead of a placeholder label.
Adds ItemExtractor to resolve withItem(new ItemStack(Material.X)) calls (directly or through a local variable initializer) to a slot/material, covering both the slot(index, item)-style sugar methods and the slot(index).withItem(item) chained builder style, plus layoutSlot char bindings expanded against the extracted layout grid. renderWith/onRender calls are marked as dynamic placeholders since their output depends on runtime state. The preview panel now paints a deterministic color+label per material and a distinct marker for dynamic slots.
ItemExtractor.kt was untracked and got skipped by the previous pathspec-limited commit, which only picks up already-tracked paths for new files. Adding it here so the module actually compiles as described.
Registers a PsiTreeChangeListener scoped to the open file, debounced 300ms via Alarm and disposed alongside the FileEditor, so the preview re-extracts and repaints as the user edits the view's config/render methods instead of only rendering once at open time.
Views.rows(n) is sugar for builder().type(CHEST).size(n) inside Views itself, so that .type()/.size() call never appears in the user's own source for the extractor to see - special-cased here since it's the primary documented entry point for the inline API. Also adds a maxSize per view type so irregular containers (furnace/smoker, whose 2x2 grid only has 3 real slots) don't render a phantom extra slot, and clarifies the empty-state message now that "no view detected" can no longer reach this panel (the file editor only opens once a view is already detected).
… tab ViewDetector.isViewFile used InheritanceUtil.isInheritor and method resolution, both of which hit the stub index and throw IndexNotReadyException while the project is still indexing; accept() is invoked even for non-DumbAware providers in this platform version, so the exception was surfacing as a PluginException. Now short-circuits via DumbService.isDumb() with an IndexNotReadyException catch as a fallback for the race window. Also switches the editor policy from PLACE_AFTER_DEFAULT_EDITOR to HIDE_DEFAULT_EDITOR: since our editor already embeds the full text editor inside TextEditorWithPreview, the previous policy was adding a redundant second "Inventory Preview" tab alongside the plain text tab instead of replacing it with one always-visible split view.
HIDE_DEFAULT_EDITOR requires the platform to trust the provider's accept() during dumb mode, which it now can since ViewDetector already short-circuits to false while indexing instead of throwing.
The initial refreshPreview() in init{} could run while the project was
still indexing (e.g. restoring a previously-open view file on IDE
startup), and ViewExtractor's PSI resolution has no dumb-mode guard, so
it threw, got caught, and cached a permanent null model with nothing
ever retrying it. DumbService.runWhenSmart() covers both cases: it runs
immediately if already smart, or queues a single retry once indexing
finishes.
render.firstRow()/lastRow()/row(n)/firstColumn()/lastColumn()/column(n) return the "next available slot" in that row/column, not the whole row/column - but the idiomatic usage (see RowColumnSample, which loops columnsCount times calling firstRow()) is to call them repeatedly to fill the entire row/column. Static analysis can't detect loop iteration counts from a single call site, so each occurrence is now treated as "fill the whole row/column" - wrong for genuine single-slot usage, but renders the common case instead of showing nothing, which is what was happening for RowColumnSample.java before this change.
render.firstRow((pos, slot) -> slot.withItem(item)) calls withItem on the lambda's builder parameter rather than chaining it directly onto the row/column call, so the existing receiver-chain check in resolveChainTarget never matched it. Adds resolveFactoryCall to recognize the factory-argument overloads directly and search the lambda body for its withItem call, reusing the same row/column "fill the whole line" indices as the chain-style handling.
Adds chest-1.png..chest-6.png (one per row count) under resources/assets/sprites and uses them as the chest inventory background instead of a hand-drawn grid of rectangles. Slot content (material color+label, dynamic marker, layout fill) is overlaid at the exact pixel offsets derived from the sprites' geometry (7px side border, 17px top title area, 18px/slot, scaled 2x with nearest-neighbor interpolation to keep the pixel art crisp). Non-chest view types still fall back to the drawn grid since there's no sprite for them.
Recreating this file - a prior commit adding it was dropped by a branch rebase. Updated to reflect the new sprite-based chest rendering (frame is a real bundled sprite now, item icons are still placeholders).
render.firstRow(((pos, slot) -> slot.withItem(item)))) - note the extra parens around the lambda - resolved to a UParenthesizedExpression wrapping the ULambdaExpression rather than the lambda directly, so `lambdaArg as? ULambdaExpression` silently failed and the slot was never placed. render.lastColumn(...) worked because it happened to have no redundant parens. Applies skipParenthesizedExprDown() at every point an argument expression gets downcast to a specific UAST subtype (lambda, call receiver, reference, item expression), since redundant parens are valid anywhere in Java/Kotlin, not just around this one lambda.
Records the source TextRange that produced each slot (the outer call expression - slot()/withItem()/the row-column factory call - depending on which extraction path resolved it) directly on PreviewSlot. The preview editor listens for caret movement and highlights the slot whose range contains the caret offset; clicking a filled slot in the preview moves the caret to (and scrolls/focuses) its source location. Ranges are plain TextRange (two ints), not held PSI/UAST references, so they stay safe to keep on the model between read actions - though they can go stale for the ~300ms between an edit and the next debounced re-extraction, same tradeoff the live-refresh feature already accepts.
firstRow()/lastRow()/column(n)/etc. produce multiple slots that all share the same PreviewSlot (same sourceRange, same item), but updateHighlightForCaret() used firstOrNull(), so only one of them ever lit up when the caret was inside that call. Switched the highlight state from a single nullable index to a Set<Int> and filter() instead of firstOrNull() to collect every matching slot.
Adds a LocalInspectionTool that walks call expressions and simple name references (via UastHintedVisitorAdapter, so it works across Java and Kotlin like the rest of the plugin) and flags anything whose resolved declaration - or its containing class, since inventory-framework marks whole classes like View/ViewBuilder rather than every member - carries @ApiStatus.Experimental or @ApiStatus.Internal. Uses ProblemHighlightType.LIKE_DEPRECATED (same strikethrough treatment as @deprecated usage) since both convey "this may change or disappear."
The grid/sprite was always drawn at a fixed top-left offset regardless of the panel's actual size, so it sat pinned to the corner in a wider split-editor pane instead of centered. gridOrigin() now computes the origin from the panel's live width/height (falling back to a fixed margin when the panel is smaller than the content), and both painting and click hit-testing derive their coordinates from it so they can't drift out of sync as the panel resizes.
… highlight Both used ProblemHighlightType.LIKE_DEPRECATED before, so an @internal usage - which really shouldn't happen at all - looked the same as an @experimental one, which is just "may change." Internal now uses GENERIC_ERROR_OR_WARNING (the ordinary yellow warning underline); Experimental uses WEAK_WARNING (the lighter gray dotted underline IntelliJ reserves for softer suggestions).
WEAK_WARNING was still in the yellow family, just thinner. Trying POSSIBLE_PROBLEM for a genuinely different color per user request; LIKE_UNKNOWN_SYMBOL is the other candidate to compare against if this one isn't right either - registerProblem only accepts the built-in ProblemHighlightType set, so picking among these is the extent of control available without switching to an Annotator.
…spection Experimental stays WEAK_WARNING (lighter dotted underline); Internal moved to GENERIC_ERROR (a stricter red squiggly, since using it at all usually indicates a mistake) - settled on after comparing a few ProblemHighlightType options. Comment updated to match; it previously still described the POSSIBLE_PROBLEM experiment.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
IntelliJ Tooling plugin will help developers preview inventories. In this first release only static analysis is done. I'll bring fully interactive inventory right in the IDE later.
Derived from #262
Closes #259