Skip to content

feat(projectional-editor): support node attributes - #602

Open
slisson wants to merge 7 commits into
mainfrom
feat/node-attributes
Open

slisson wants to merge 7 commits into
mainfrom
feat/node-attributes

Conversation

@slisson

@slisson slisson commented Oct 23, 2026

Copy link
Copy Markdown
Member

What

Node attributes (jetbrains.mps.lang.core.structure.NodeAttribute) are now rendered and editable in the projectional editor.

  • Rendering as layers. A node's cell is wrapped by its node attributes, the first one outermost as in MPS. An attribute's notation says where the attributed node goes with the new attributed cell (AttributedCellTemplate, DSL attributed { attributedConcept(...); multiple(...) }).
  • No hidden context. Each layer is an ordinary CellCreationCall (AttributedNodeCellCreationCall). The attributed cell works out the next layer inward from the model alone (the attribute's parent and the next attribute after it), the way a child cell would. ChildSpecReference now holds a CellCreationCall, so adding or removing an attribute only rebuilds the layers next to it.
  • Fallbacks.
    • An attribute whose notation has no attributed cell is not rendered, so it can't hide the node, and its check messages stay on the attributed node.
    • An attribute without any notation uses the reflective editor, with an attributed node: line.
    • The reflective editor no longer lists smodelAttribute as a child role.
  • Adding and removing without intentions.
    • Typing the first constant before attributed in front of a node adds the attribute; typing the first constant after it behind the node does the same.
    • The caret position decides how the new attribute nests among existing ones.
    • The attributed concepts and multiple from @attribute info are respected.
    • Where several nodes start at the same position, the attribute goes on the outermost one.
    • Deleting the attribute's own text removes it and keeps the node.
  • Notation language. New AttributedCell concept with behavior, a canBeChild constraint (node-attribute notations only), an editor and a generator rule that reads @attribute info. ReviewMigration in the jetbrains.mps.lang.core notation now uses it (@Review(reason) above the node).

Separate fix in the first commit

updateCodeCompletionActions rebuilt the menu entries from the anchor cell's substitute actions. When the menu had been opened as a side transform, this dropped all of its entries on the second keystroke, so no side transform longer than one character could be completed by typing it. It now reuses the menu that was opened, as hasCodeCompletionActions already did. This affects all multi-character side transforms, not only attributes.

Notes for reviewers

  • IConceptEditorRegistry gained getAllConceptEditors(), implemented in LanguageAspectsFromMPSModules; the editor uses it to find attribute notations.
  • Attributes are read through allChildren rather than the smodelAttribute link, because a model may store that role by name or by ID, and reading it one way registers an incremental dependency that misses changes stored the other way.
  • CodeCompletionTest's list of known duplicates lost its <smodelAttribute> tokens because of the reflective-editor change.

Tests

  • NodeAttributeTest (kernelf, 14 tests): rendering and nesting order, both fallbacks, adding by typing before, after, nested and key by key, the multiple and concept restrictions, and delete to remove.
  • BaseLanguageTests (MPS-backed): @Review renders above a method, typing it adds it, deleting it removes it. All 31 tests in that module pass.
  • The JVM suites of projectional-editor and kernelf-editor pass; the JS targets compile; the headless MPS build (:mps:assembleMpsModules) generates and compiles the notation modules.

🤖 Generated with Claude Code

slisson and others added 2 commits October 23, 2026 15:45
…ttern changes

A completion menu opened by typing next to a cell offers the side transforms
of that border. When the pattern changed, the entries were rebuilt from the
substitute actions of the anchor cell instead, so the side transforms were
lost after the first key and a transform longer than one character could
never be completed by typing it. The update now reuses the providers of the
menu that was opened, as hasCodeCompletionActions already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node attributes (jetbrains.mps.lang.core.structure.NodeAttribute) wrap the
cell of the node they are attached to, the first attribute outermost as in
MPS. The notation of an attribute says where that node goes with the new
`attributed` cell.

Each layer is an ordinary cell creation call. The `attributed` cell derives
the next layer inward from the model (the attribute's parent and its next
attribute), so nothing is handed down while rendering and adding or removing
an attribute only rebuilds the layers next to it. An attribute whose notation
has no `attributed` cell is not rendered, and its check messages stay on the
attributed node. Without any notation the reflective editor shows it around
the node. The reflective editor no longer lists `smodelAttribute` as a child
role.

Adding an attribute needs no intention: typing the first constant in front of
the `attributed` cell in front of a node adds it, and the first constant
behind it behind the node. Where the caret is decides how it nests with the
existing ones, the attributed concepts and `multiple` of the attribute info
are respected, and where several nodes start at the same position the
outermost one gets it. Deleting the attribute's own text removes it and keeps
the node.

In the notation language, `AttributedCell` is generated to
`attributed { attributedConcept(...); multiple(...) }` from the concept's
`@attribute info`. ReviewMigration gets a notation that uses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

623 tests   610 ✅  10m 57s ⏱️
 95 suites   13 💤
 95 files      0 ❌

Results for commit 743131a.

♻️ This comment has been updated with latest results.

slisson and others added 5 commits October 30, 2026 13:37
Rendering the editor after a model change ran the typesystem and constraints
checks of the whole root node while holding the incremental engine's lock.
An edit like pressing Enter had to wait for it, which took more than two
seconds in larger root nodes.

The editor now only reads the messages published by the last completed check.
The checks run in the background in an incremental engine of their own and in
a read action that a write action cancels. The messages are published per
node, so a new check result only re-renders the cells of the affected nodes.

IncrementalModelChecker can now be cancelled. MPS executes model access in a
non-cancelable section, so the cancellation is checked explicitly instead of
through the ProgressManager. Cancelled checks are re-executed after
retryCancelledChecks() instead of staying cached as failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… is open

Services are disposed inside a write action. Stopping the Ktor server there
waited for the Netty event loops, without any timeout, while a request handler
on one of them waited for a read action. MPS could only be killed.

The servers are now stopped when the application is about to close or the
plugin is about to be unloaded, which is before that write action. Stopping
them in dispose() remains as a fallback and gives up after a bounded time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reference cell generated from an MPS notation writes its target as
NodePresentationUtil.presentation, which ends in SNode.getPresentation().
ModelixNodeAsMPSNode answered that with the stored name and otherwise with the
toString() of the modelix node. A target whose name is derived by a property
getter in the constraints, like an optionally named BlockInstance, has no
stored name and was written as `MPSWritableNode(node=(instance of ...))`.

getPresentation() now does what MPS's SNode does: it calls the getPresentation
behavior, which reads the name through SNodeAccessUtil and so applies the
property getter. The behavior runs on the wrapper instead of the unwrapped
SNode, so every read it makes is still dependency tracked and the cell follows
changes of the name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nces on wrapped nodes

ModelixNodeAsMPSNode left some SNode methods to SNodeBase, which throws, and
reduced others to what modelix can resolve:

- putUserObject threw and getUserObject always returned null. Behaviors of
  iets3 and other languages cache data in user objects, and since the
  presentation of a node is computed by its behavior, they also run on
  wrapped nodes now. User objects are kept on the MPS node, or on the wrapper
  for a node of another kind.
- setReference with an SNodeReference, a ResolveInfo or an SReference threw.
  The node reference is stored through modelix. A ResolveInfo that holds a
  node is set as that node, so that a wrapper never ends up in an MPS node,
  and the other ones are passed to the MPS node. An SReference is stored as
  what it points to, because MPS only accepts its own references of the node.
- getReference returned null for a reference whose target cannot be resolved,
  and the target node reference of a reference was computed by resolving it.
  MPS code finds broken references as ones that exist without a target and
  writes them with their resolve info, so the reference now exists as long as
  it points somewhere, says where it points to without resolving, and has the
  resolve info of the MPS reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The link of a super concept is found by its name. The test failed only
because of a concept id that doesn't exist, not because of the lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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