## Linked issue
n/a: no Linear ticket, at the operator's request. Builds on #30563 (the
binder is built by the caller and `ProjectArtifacts.binder()` serves the
current snapshot).
## At a glance
```prisma
// customer.prisma
namespace catalog {
model Product { id Int @id }
}
// order.prisma
namespace catalog { … }
model Order {
productId Int
product catalog.Product @relation(fields: [productId], references: [id])
}
```
`textDocument/definition` in the language server:
| Cursor on | Result |
|---|---|
| `Product` in `catalog.Product` | `model Product` in `customer.prisma`
|
| `catalog` in `catalog.Product` | both `namespace catalog { … }`
blocks, one per file |
| `productId` in `fields: [...]` | the `productId` field |
| a declaration's own name, a contributed type, a cross-space reference,
an attribute name, an unresolved name | `null` |
## Decision
1. **The binder resolves the qualifier of a qualified type reference.**
In `ns.Name`, the qualifier identifier gets its own resolution:
`namespace`, or `contributedNamespace` for a contributed one. The
resolution on the whole `QualifiedName` is unchanged. This is binding
only, with no new diagnostics.
2. **The language server answers `textDocument/definition`.** It finds
the token at the cursor, walks up to the nearest node the binder
resolved, and returns that symbol's declaration nodes. A namespace
returns one target per `namespace` block. The response is
`LocationLink[]` when the client declares `linkSupport`, and
`Location[]` otherwise.
3. **The playground navigates across files.** `apps/lsp-playground`
switches the editor and sidebar to the target file when a definition is
in another scratch file.
## Reviewer notes
- **Walking up from the cursor** is shared with hover (`resolvedNodeAt`
in `cursor-resolution.ts`): it stops at the nearest node the binder
resolves, or with nothing at a declaration node (whitespace inside a
model, argument keys). A cursor on a declaration's own name returns
`null`: since #30569 the binder records the declaration on its name
node, and the provider returns `null` when the resolved node is the
target's own name.
- **Qualifiers everywhere.** The qualifier of every qualified name the
binder resolves gets its own resolution: type references, named-type
bases, and entity references in attribute arguments and block values
(`@@extends(auth.User)`, `on = auth.User`). Definition on the qualifier
lists the namespace blocks, hover shows `namespace auth`, and semantic
tokens mark it `namespace`. The qualifier is looked up once and passed
to `qualifiedMember`.
- **Between two tokens**, the identifier on the right is preferred, then
the one on the left, so `auth|.User` targets `auth` and `User|` targets
`User`.
- **In `app.Missing`**, `app` still resolves to the namespace while the
whole name stays unresolved. In a cross-space reference
(`supabase:auth.User`) the qualifier gets no resolution.
- **The family `create-binder` tests** list every resolution in a
schema, so they now include the qualifier entries (`eba8b8078f`).
- **Playground:** it uses monaco-languageclient's
`viewsConfig.openEditorFunc` hook. The library calls it only when the
target is in a file the editor isn't showing. The hook selects that file
through the sidebar's queued path, keeping the lazy `didOpen`. An
existing `.playground/scratch/` is never re-seeded; delete it to get the
new seed with `catalog.Product`.
- **Semantic tokens** classify a qualifier from the binder's qualifier
resolution instead of a scope lookup (`1d8ce44f59`). A cross-space
qualifier and the qualifier of a dotted function call in an expression
(`@default(app.fn())`) no longer get a `namespace` token, because the
binder records nothing for them. The remaining scope lookups in the
language server are completion candidates on partial input.
## Behavior changes & evidence
- **Go-to-definition** for type references, qualifiers, `fieldRef` /
`referencedFieldRef` / `entityRef` arguments in attributes, and
`entityRef` in block values, with targets in other files of the project.
- Implementation:
[`definition.ts`](packages/1-framework/3-tooling/language-server/src/definition.ts),
[`server.ts`](packages/1-framework/3-tooling/language-server/src/server.ts).
- Evidence:
[`definition.test.ts`](packages/1-framework/3-tooling/language-server/test/definition.test.ts),
[`server.test.ts`](packages/1-framework/3-tooling/language-server/test/server.test.ts).
- **Qualifier resolution in the binder.**
- Implementation:
[`binder.ts`](packages/1-framework/2-authoring/psl-parser/src/binder.ts).
- Evidence:
[`binder.test.ts`](packages/1-framework/2-authoring/psl-parser/test/binder.test.ts).
## Skill update
n/a: an editor feature with no CLI, config, public API or diagnostic
change.
## Testing performed
- `pnpm build`, typecheck of changed packages and their dependents
(170/170), `pnpm lint:deps`
- `@internal/psl-parser`: 1263 tests; `@internal/language-server`: 912
tests; `@internal/sql-contract-psl`: 629;
`@internal/mongo-contract-psl`: 305; lint clean
- `test/integration`, `test/authoring`: 226 tests
- Playground: build, typecheck and lint. Smoke check through `prisma lsp
--stdio` with the playground seed: the cross-file model link, two links
for the reopened-namespace qualifier, and a same-file field link.
- **Not yet done:** the manual browser check (playground README, Manual
QA step 9).
## Follow-ups
- Find references, which builds on the same reference-to-declaration
mapping.
## Alternatives considered
- **The server resolves the qualifier by inspecting `QualifiedName`
segments itself.** Rejected: that is a name lookup outside the binder.
With the qualifier bound, the server only reads `symbolForNode`.
- **Only `Location[]`.** Rejected: `LocationLink` gives peek the whole
declaration and the jump the declaration's name. The `Location` fallback
covers clients without `linkSupport`.
- **The cursor on a declaration's own name returns the declaration
itself.** Deferred to find references, where that position becomes
useful.
## Checklist
- [x] All commits are signed off (`git commit -s`) per the
[DCO](../CONTRIBUTING.md#developer-certificate-of-origin-dco).
- [x] I read [CONTRIBUTING.md](../CONTRIBUTING.md) and the change is
scoped to one logical concern.
- [x] Tests are updated.
- [ ] The PR title is in `TML-NNNN: <sentence-case title>` form. No
Linear ticket, at the operator's request.
- [x] The **Skill update** section above is filled in.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added go-to-definition support for models, fields, types, blocks, and
namespaces, including navigation across files and to multiple namespace
declarations.
* Namespace qualifiers now resolve independently and receive
namespace-aware highlighting and hover information.
* **Documentation**
* Updated language-server and playground guides with navigation details,
cross-file examples, and manual QA steps.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Signed-off-by: Steven McClankerton <tatarintsev@prisma.io>
Co-authored-by: Steven McClankerton <tatarintsev@prisma.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>