Skip to content

Fix IncorrectOperationException and support smart URI rewriting on file move#178

Open
danteay wants to merge 11 commits into
apple:mainfrom
danteay:fix/refactor-file-paths
Open

Fix IncorrectOperationException and support smart URI rewriting on file move#178
danteay wants to merge 11 commits into
apple:mainfrom
danteay:fix/refactor-file-paths

Conversation

@danteay
Copy link
Copy Markdown
Contributor

@danteay danteay commented Mar 27, 2026

What

Fix a series of crashes and missing behaviors triggered when moving
.pkl files via IntelliJ's refactoring engine (drag-and-drop or
Refactor > Move).

Why

The default PsiReferenceBase.bindToElement unconditionally throws
IncorrectOperationException. Because every Pkl reference class
(PklModuleUriReference, PklUnqualifiedAccessReference,
PklSuperAccessReference, PklModuleNameReference,
PklSimpleTypeNameReference, PklModuleReference) inherits this
default, any file move that touched a Pkl source file would crash the
refactoring processor.

Beyond stopping the crash, moves across PKL project boundaries need
special URI handling: a plain relative path would be wrong once the
file lands in a different project, so the URI must use the
@<depName>/<path> project-dependency format instead.

Changes

Stop the crash

  • Override bindToElement on all six PsiReferenceBase subclasses.
  • Five of them (non-URI references) use a no-op that returns element
    unchanged — these references are unaffected by file location.
  • PklModuleUriReference gets a full implementation (see below).

Smart URI rewriting in PklModuleUriReference.bindToElement

  • file: URIs → replaced with the new absolute VFS URL.
  • Relative URIs, same PKL project → recomputed as a plain relative
    path from the new source location.
  • Relative URIs, crossing PKL project boundaries →
    1. If the source project already declares the target project as a
      dependency, rewrite as @<depName>/<path-from-dep-root>.
    2. Otherwise derive the dep name from the target project's
      packageUri (e.g. package://localhost:0/tests@1.0.0
      "tests"), insert a new ["tests"] = import("…/PklProject")
      entry into the source PklProject file, and rewrite as
      @tests/<path>.
    3. If neither project has a PklProject ancestor, fall back to a
      plain relative path.
    4. URIs with a non-file scheme (pkl:, package:, https:,
      modulepath:) are left unchanged.
  • After inserting a new dependency entry, schedule
    pklProjectService.syncProjects() via invokeLater so
    PklProject.deps.json is regenerated without requiring a manual
    Sync click. A project-level flag deduplicates multiple syncs when
    several references are updated in one refactoring pass.

Refresh highlights after sync

  • Add DaemonCodeAnalyzer.restart() (via invokeLater) at the end of
    PklProjectService.syncProjects(). Without this, @dep/path
    imports kept showing "unresolved" warnings in open editors even after
    the sync had fully populated the new dependency, because incrementing
    a custom ModificationTracker does not automatically re-trigger
    IntelliJ's daemon code analyzer.

Checklist

  • Follows conventions of surrounding code
  • Build passes (./gradlew build)
  • Spotless formatting applied

danteay and others added 11 commits March 27, 2026 15:05
When a referenced file is moved via IntelliJ's refactoring, the engine calls
bindToElement on all references resolving to that file. PsiReferenceBase throws
IncorrectOperationException by default. Implement bindToElement to recompute
relative-path and file: URIs to the target's new location; other schemes
(pkl:, package:, https:, modulepath:) are not affected by file moves.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When bindToElement is called for a file move and the importing file lives
outside the PKL project that owns the moved file, rewrite the import as
@<depName>/<path-from-project-root> if the source project declares the
target's project as a local dependency. Falls back to a plain relative
path when no matching dependency is found or both files share the same
PKL project.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three fixes to computeNewRelativeUri:
- Use enclosingModule.dependencies() as the primary dependency source so
  that files inside PKL packages (isInPackage=true) are handled correctly,
  not just files in local projects.
- Fall back to iterating pklProjects.values by URL when module dependencies
  are unavailable, rather than relying on getPklProject(dir) key lookup
  which can silently miss projects.
- Compare dependency roots by VirtualFile.url (string) instead of VirtualFile
  identity (==) to avoid mismatches from path-canonicalization differences.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a file is moved across PKL project boundaries and the source project
has not yet declared the target project as a dependency, fall back to the
target project's own package name (derived from its packageUri) to build
the @<name>/<relPath> import. This produces the correct import format even
before the user adds the dependency declaration to their PklProject.

Strategy order:
1. Look up a matching declared dependency in the source module (existing)
2. Find target project's package name via pklProjectService packageUri.path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ss projects

When a file is moved into a PKL project whose packageUri has a name (e.g.
"tests"), and the source PklProject does not yet declare that project as a
dependency, automatically insert the dependency entry into the source
PklProject file and use @<name>/<relPath> as the new import URI.

Behaviour summary:
- Strategy 1 (already declared dep): find by root URL match → @name/path, no file change
- Strategy 2 (undeclared dep): derive name from target packageUri, write
  ["name"] = import("rel/path/PklProject") into source PklProject (skipped
  if entry already present), return @name/path
- Relative path fallback: only when either side has no PklProject ancestor
- null (import unchanged): both sides have PklProjects but target has no
  packageUri so no dep name can be determined

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When moving a Pkl file across project boundaries causes a new dependency
entry to be written into the source PklProject file, automatically
schedule `syncProjects()` (pkl project resolve → PklProject.deps.json)
via invokeLater so the IDE picks up the new dep without a manual Sync
click. A project-level Key flag deduplicates multiple sync requests that
can arise when several references are updated in a single refactoring
pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After syncProjects() updates pklProjects, IntelliJ's daemon does not
automatically re-run annotators because custom ModificationTracker
changes do not trigger daemon restarts. This left @dep/path imports
showing "unresolved" warnings in open editors even after the sync had
fully resolved the new dependency.

Add a DaemonCodeAnalyzer.restart() call via invokeLater at the end of
syncProjects() so all open editors re-evaluate their import highlights
once the updated project state is available.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All PsiReferenceBase subclasses that don't handle file moves threw
IncorrectOperationException from the default bindToElement when a move
refactoring was performed (e.g. PklUnqualifiedAccessReference,
PklSuperAccessReference, PklModuleNameReference, PklSimpleTypeNameReference,
PklModuleReference). Add a no-op override that returns the element
unchanged, so the refactoring engine can proceed without crashing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract three focused private methods from the monolithic
addDependencyToPklProject and computeNewRelativeUri:

- findDeclaredDepName: encapsulates strategy-1 dep lookup
- findTargetPackageName: encapsulates strategy-2 package name derivation
- scheduleSyncIfNeeded: isolates the dedup + invokeLater sync scheduling

computeNewRelativeUri now reads as a clear two-strategy decision tree.
addDependencyToPklProject is purely about editing the PklProject document.
No behavior changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update copyright year to 2024-2026 and reformat code to satisfy
the project's spotless/ktfmt style checker.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove redundant inline comments added in the move-refactoring commits
that restate what the code already expresses clearly. Preserve all
pre-existing comments. Remove @throws annotations and now-unused
IncorrectOperationException imports from the no-op bindToElement
overrides.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@danteay danteay force-pushed the fix/refactor-file-paths branch from 08b98e3 to 05ebdeb Compare March 27, 2026 21:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant