Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/unreleased/drawio-import-rename.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `sysml -import-diagram`, the LSP `opensysml/importDiagram` request, the MCP `import_diagram` tool and the VS Code *Import Diagram* command read three more kinds of edit back from a draw.io file: an element renamed by retyping its cell's text (applied as a rename, so its declaration and every reference move, through the path a `Layout` of the cell takes), a note whose text was edited, and a picture whose image source was changed to a file the model can name. A label edited beyond its name, a name SysML does not accept, a rename the model refuses, copies of one element renamed differently, an emptied note and a picture turned into embedded data are reported and left alone.
11 changes: 8 additions & 3 deletions cmd/sysml/import_diagram.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@ func runImportDiagram(files []string) error {
if err != nil {
return fmt.Errorf("%s: %w", importDiagramPath, err)
}
doc := drawioImportDocument(imp, files)
result, err := modeledit.ImportDrawio(imp, false, func(ops []modeledit.Operation) (*model.EditResult, error) {
return sess.ApplyModelEdit(doc, ops)
})
reportDrawioImport(imp)
if len(imp.Edits) == 0 {
fmt.Fprintf(os.Stderr, "%s: nothing differs from the rendering of %s; nothing to import\n", importDiagramPath, file.View)
return nil
}
ops := modeledit.DrawioOperations(imp, false)
doc := drawioImportDocument(imp, files)
result, err := sess.ApplyModelEdit(doc, ops)
if err != nil {
var refusal *modeledit.Error
if errors.As(err, &refusal) {
Expand Down Expand Up @@ -170,6 +171,10 @@ func stageEdited(target string, content []byte, perm fs.FileMode) (string, error
// reportDrawioImport lists the edits an import plans and what it leaves alone.
func reportDrawioImport(imp *view.DrawioImport) {
for _, e := range imp.Edits {
if e.Kind == view.DrawioEditRename {
fmt.Fprintf(os.Stdout, "%s %s to %s\n", e.Kind, drawioEditTarget(e), e.NewName)
continue
}
fmt.Fprintf(os.Stdout, "%s about %s\n", e.Kind, drawioEditTarget(e))
}
for _, name := range imp.Unmatched {
Expand Down
2 changes: 1 addition & 1 deletion cmd/sysml/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,7 @@ func registerFlags(fs *flag.FlagSet) {

fs.StringVar(&renderView, "render", "", "Render this view of the model instead of running it, in the form its render member states; #<kind> renders every file loaded and #<kind>:<element> one element, kind being tree, interconnection, state, action, case, mixed, sequence, table or matrix, without a declared view")
fs.StringVar(&renderAllDir, "render-all", "", "Render every declared view into this directory")
fs.StringVar(&importDiagramPath, "import-diagram", "", "Import this diagrams.net (draw.io) file, written by -render-form drawio and edited in draw.io, back into the model: what moved, resized, folded, re-routed, recoloured or re-sized the page becomes DiagramLayout metadata in the view's body, written through the model editor; cells the rendering lacks are reported, never created or deleted")
fs.StringVar(&importDiagramPath, "import-diagram", "", "Import this diagrams.net (draw.io) file, written by -render-form drawio and edited in draw.io, back into the model: what moved, resized, folded, re-routed, recoloured or re-sized the page becomes DiagramLayout metadata in the view's body, a retyped name renames the element and its references, a retyped note or a picture pointed at another file updates the annotation, all written through the model editor; cells the rendering lacks, a label edited beyond its name, an invalid or conflicting name, an emptied note and a picture source no file under the view's directory carries are reported, never created, deleted or guessed")
fs.StringVar(&importView, "view", "", "The view -import-diagram imports, when the file has several pages; default the one page's view")
fs.BoolVar(&dryRun, "dry-run", false, "With -import-diagram, print the planned edits and a unified diff of each file they change, writing nothing")
fs.StringVar(&graphsSubject, "graphs", "", "Write the lowered graph of this action or state machine, and of every behavior it performs, as the canonical graphs:1 JSON an external analysis engine is sent, to stdout or -output")
Expand Down
12 changes: 8 additions & 4 deletions docs/guide/08-editors.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,14 @@ alone (<kbd>Ctrl</kbd>+<kbd>Alt</kbd> toggles them when `editor.inlayHints.enabl
`SysML: Import layout from draw.io…` (beside *Export Diagram* in the title bar's `…` menu and the
right-click menu) reads a `.drawio` file that *Export Diagram* wrote and you edited in draw.io
desktop, the diagrams.net editor or the VS Code draw.io extension, and writes what you moved,
resized, collapsed, re-routed or recoloured — and a changed page size or a moved note — back into
the model as `DiagramLayout` metadata in the view's body, exactly as dragging a node in the
diagram panel does, in one undoable edit. Cells that name no element, elements the rendering no
longer has and cells you deleted are reported, not created or deleted. The same import is
resized, collapsed, re-routed or recoloured — and a changed page size, a moved or retyped note, a
picture pointed at another image file — back into the model as `DiagramLayout` metadata in the
view's body, exactly as dragging a node in the diagram panel does, in one undoable edit; a node,
port or edge whose text you retyped with only its name changed renames the element and every
reference to it. Cells that name no element, elements the rendering no longer has and cells you
deleted are reported, not created or deleted, as are a label edited beyond its name, a name that
is not valid or already taken, an emptied note and a picture given embedded data or a file outside
the view's directory. The same import is
`sysml model.sysml -import-diagram view.drawio -dry-run` on the command line, which prints the
planned edits and a diff first.

Expand Down
5 changes: 4 additions & 1 deletion docs/project/diagram-layout-annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,10 @@ exactly the bytes it produced before; the existing rendering goldens pin that.
`import_diagram` tool or the VS Code *Import layout from draw.io…* command, which diff it
against a fresh export of the same view and write what moved as `setLayout`, `setRoute`,
`setStyle`, `setCanvas`, `setNote` and `setPicture` operations — the same edits the panel
writes, view-local by default ([the round trip](view-rendering-forms.md#round-trip)).
writes, view-local by default — a retyped note or a re-pointed picture as a `setNote` or
`setPicture` with the new text or location, and a cell retyped with only its name changed as
a `rename` of the element the cell's qualified path resolves to, so a member drawn under an
inheritor renames its declaration ([the round trip](view-rendering-forms.md#round-trip)).
- **Editors.** The LSP fields are what a graphical client (the VS Code diagram panel, a
SysON-style editor) reads to place nodes where the model says. Writing positions back
is `opensysml/applyModelEdit` with `setLayout`, `setRoute`, `setCanvas`, `setStyle`,
Expand Down
26 changes: 24 additions & 2 deletions docs/project/view-rendering-forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -1562,7 +1562,20 @@ drag in the diagram panel is: a moved, resized or collapsed node is a `Layout` (
coordinates, as the notation reads them; a container the model did not place is pinned with its
children so a child moved inside it lands where the file has it), a re-routed edge a `Route` (a
straightened one clears it), a recoloured cell a `Style`, a changed page size a `Canvas`, a moved
note or picture a `Note` or `Picture`. Nodes and ports are matched by the `fqn` the writer stamps,
or retyped note a `Note` with the text the file has (line breaks kept; a note formatted in draw.io
is decoded from the HTML it saves), a moved picture or one pointed at another file a `Picture`
whose location names that file as the writer would, relative to the view's document (a path, an
absolute path or a `file:` URL, each under that directory: the import reads no file outside it). A node, port or named edge whose text was
retyped is a rename when the new text is the writer's own label with exactly the declared name
replaced by one SysML name — an identifier or a quoted `'…'` name, as the lexer reads it. The
rename goes through the model editor's `rename`, so the declaration and every reference to it move,
across documents — the declaration may be in a document other than the view's — and in the view's
own `expose`, `about` and layout lines; a member renamed together with its owner is renamed first,
by the path the file still names it with; its target is the
qualified path the import gives a `Layout` of the same cell, so a member drawn under an inheritor
(`Loop::sensor::waterOut`) renames the declaration that path resolves to, not a usage of it. A
cell moved and retyped at once imports as one `Layout` and one rename, the rename applied last so
the layout still resolves. Nodes and ports are matched by the `fqn` the writer stamps,
edges by their ends, kind and order, notes and pictures by position; positions within half a
pixel are unchanged, so an untouched export imports nothing and the routes the writer computed
itself are never written back unless the user moved them. The compressed form draw.io desktop
Expand All @@ -1571,7 +1584,16 @@ view named. What the import cannot carry is reported, not written: cells naming
(nothing is created), elements the rendering lacks (renamed or gone), cells the file lost
(nothing is deleted), a port recoloured (ports are drawn by direction), a note re-anchored, a
label moved, and an edge re-routed that no `Route` steers yet, such as a typing edge drawn beside
its membership edge. `-dry-run` prints the planned edits and a unified diff and writes nothing.
its membership edge. So is a retyping the rename rule does not cover, each with a one-line reason
naming the cell: a label edited beyond its name (a keyword, `: Type`, multiplicity, parameter
line or compartment row), a new name that is not one SysML name (empty, a keyword, two words, an
unterminated quote), a name an existing member already holds (the rename check's own message), a
cell whose name is synthesized and so has none to rename, and an element the view draws twice
(an inherited member in a tree) whose copies disagree. A note emptied is reported, since a `Note`
with no text is not one, and so is a picture given embedded `data:` content, a URL, a file outside
the view's directory, or a file that does not read as an image — no `Picture` location names those. Retyping a name to the same
text imports nothing and reports nothing. `-dry-run` prints the planned edits and a unified diff
of every document they change and writes nothing.

## Limiting what a view draws

Expand Down
14 changes: 10 additions & 4 deletions docs/reference/lsp.md
Original file line number Diff line number Diff line change
Expand Up @@ -678,15 +678,21 @@ edited in draw.io, as the `DiagramLayout` edits `opensysml/applyModelEdit` would
The server renders the view, exports it again as the writer would, and compares the file with
that export cell by cell: only what differs becomes an edit, matched to the model by the `fqn`
each cell carries (edges by their ends and kind, notes and pictures by position). The result lists
the planned `edits` (`kind`, `target`, `label`, `cell`), what is reported rather than imported —
`unmatched` elements the rendering lacks, `unknown` cells naming no element, `deleted` cells, and
`notices` for changes no annotation carries yet — and either `edit`, a `WorkspaceEdit` the client
the planned `edits` (`kind`, `target`, `label`, `cell`, and `newName` on a `Rename`, which a cell
retyped with only its name changed amounts to; a retyped note is a `Note`, a picture pointed at
another file a `Picture`), what is reported rather than imported — `unmatched` elements the
rendering lacks, `unknown` cells naming no element, `deleted` cells, and `notices` for changes no
annotation carries yet and retypings the rename rule does not cover (a label edited beyond its
name, an invalid or taken name, an emptied note, a picture given embedded data or a file outside
the view's directory) — and either
`edit`, a `WorkspaceEdit` the client
applies as it applies `opensysml/applyModelEdit`'s, or `refused`, the planned edit the model
editor refused and why. An untouched export answers no edits and no `edit`.

```json
{ "view": "PlantViews::loopView", "dryRun": true, "version": 3,
"edits": [{ "kind": "Layout", "target": "Plant::Loop::pump", "label": "«part»\npump : Pump", "cell": "n2" }],
"edits": [{ "kind": "Layout", "target": "Plant::Loop::pump", "label": "«part»\npump : Pump", "cell": "n2" },
{ "kind": "Rename", "target": "Plant::Loop::tank", "newName": "reservoir", "label": "«part»\nreservoir : Tank", "cell": "n3" }],
"edit": { "documentChanges": [ … ] } }
```

Expand Down
14 changes: 9 additions & 5 deletions docs/reference/sysml-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ without this page.
| `documents` | List the document definitions (doc def) declared across the workspace, which render_document renders to Markdown. | no |
| `format` | Format a SysML v2 / KerML document, or only the lines of a range, in the canonical style. | yes |
| `hover` | Explain the element at a position: its kind, qualified name, type, value and documentation, as Markdown. | no |
| `import_diagram` | Import a draw.io file render_view wrote with form drawio and a user edited, as DiagramLayout annotations in the view's body; a dry run until apply. | yes |
| `import_diagram` | Import a draw.io file render_view wrote with form drawio and a user edited: moves, routes, styles, the page, note text and picture sources as DiagramLayout annotations in the view's body, a retyped name as a rename; the rest is reported; a dry run until apply. | yes |
| `inlay_hints` | List the inferred types and values the editor would show inline for the features of a document or range. | no |
| `open_document` | Hold a document open with the given text, as an editor buffer the server analyzes instead of the file on disk, so text can be checked (diagnostics, hover, render) before it is written. | yes |
| `references` | Find every place in the workspace that refers to the element at a position. | no |
Expand Down Expand Up @@ -357,11 +357,15 @@ without this page.
- **Round-tripping a diagram** is `render_view` with `form: "drawio"`, an edit in draw.io, then
`import_diagram` with the edited file (`file` on the host or `content` as text): the tool
renders the view again, diffs the file against that export and answers the `DiagramLayout`
annotations the differences amount to — `Layout`, `Route`, `Style`, `Canvas`, `Note`,
`Picture` — as the text edits `apply_model_edit` would make, per document. Call it first
annotations the differences amount to — `Layout`, `Route`, `Style`, `Canvas`, `Note` (a moved
or retyped note), `Picture` (a moved picture or one pointed at another image file) — and, for a
cell whose text was retyped with only its name changed, the `rename` of that element and every
reference to it, as the text edits `apply_model_edit` would make, per document. Call it first
without `apply` to read the plan and what it reports rather than imports (cells naming no
element, elements the rendering lacks, deleted cells, and changes no annotation carries yet,
such as a re-routed typing edge), then with `"apply": true` to write the files.
element, elements the rendering lacks, deleted cells, a label edited beyond its name, a name
that is not one or that an existing member holds, an emptied note, a picture given embedded
data, a URL or a file outside the view's directory, and changes no annotation carries yet, such as a re-routed typing edge), then
with `"apply": true` to write the files.
- **Writing** happens only on request: a tool marked as writing (`format`, `rename`,
`apply_model_edit`, `session_save`, ...) returns the edit or the file it would write and
touches the disk only with `"apply": true`. The session tools that write change the session's
Expand Down
32 changes: 32 additions & 0 deletions internal/check/edit/drawio.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package edit

import (
"errors"
"fmt"

"github.com/Open-MBEE/OpenSysML/internal/ir/view"
)

Expand Down Expand Up @@ -30,6 +33,8 @@ func DrawioOperations(imp *view.DrawioImport, inline bool) []Operation {
op = SetNoteAt(e.Origin.Span, e.Note).DeclaredIn(e.Origin.Doc)
case view.DrawioEditPicture:
op = SetPictureAt(e.Origin.Span, e.Picture).DeclaredIn(e.Origin.Doc)
case view.DrawioEditRename:
op = Rename(e.FQN, e.NewName).DeclaredIn(e.Origin.Doc)
default:
continue
}
Expand All @@ -46,3 +51,30 @@ func drawioTargeted(e view.DrawioEdit, named, declared Operation) Operation {
}
return declared.DeclaredIn(e.Origin.Doc)
}

// ImportDrawio applies an import's edits through apply, the editor of the
// document the view is declared in. A rename the editor refuses — the name is
// taken, a reference would be captured — is reported in the import's notices
// and the other edits are applied again, so one refused rename holds none of
// them back; any other refusal is the caller's. The import's edits are left
// as what was applied.
func ImportDrawio[R any](imp *view.DrawioImport, inline bool, apply func([]Operation) (R, error)) (R, error) {
for {
var none R
if len(imp.Edits) == 0 {
return none, nil
}
result, err := apply(DrawioOperations(imp, inline))
var refusal *Error
if !errors.As(err, &refusal) {
return result, err
}
i := refusal.OperationIndex
if i < 0 || i >= len(imp.Edits) || imp.Edits[i].Kind != view.DrawioEditRename {
return result, err
}
e := imp.Edits[i]
imp.Notices = append(imp.Notices, fmt.Sprintf("%s: not renamed to %q: %s", e.FQN, e.NewName, refusal.Message))
imp.Edits = append(imp.Edits[:i:i], imp.Edits[i+1:]...)
}
}
Loading
Loading