Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/notebook-run-lenses.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `sysml-lsp` offers *Run* and *Evaluate* code lenses only on a model file on disk (a `file:` URI), the only document the `sysml` runner can read: a notebook cell, which the kernel runs, and a document of another scheme keep their reference-count lenses but no longer show a run button that could only fail.
8 changes: 5 additions & 3 deletions docs/guide/08-editors.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,9 @@ LSP client; only the syntax highlighting is specific to VS Code.
### Running an element from the editor

Above each executable `action`, `state`, `calc`, `constraint` and `requirement` declared
outside another behavior the server offers a *Run* or *Evaluate* code lens naming it, and above
each definition a reference count that opens the references peek. Clicking *Run* or *Evaluate*
outside another behavior in a model file on disk the server offers a *Run* or *Evaluate* code
lens naming it, and above each definition a reference count that opens the references peek — in
a notebook cell too, which the kernel runs and so has no *Run* or *Evaluate* lens. Clicking *Run* or *Evaluate*
saves every unsaved model file and runs the matching `sysml` check over the workspace folders —
`sysml -action Demo::Charge <folder>`, `-state`, `-calc`, `-constraint` or `-requirement` — as
a task, so the result appears in the task terminal and a name another file declares without an
Expand Down Expand Up @@ -424,7 +425,8 @@ same way, so its imports of sibling files resolve rather than being reported unr
(`editor.action.showReferences`, whose URI, position and locations the VS Code extension
converts from their protocol form), and a *Run* (action, state) or *Evaluate* (calc,
constraint, requirement) command, titled with the element's name, on each executable
declaration that is not a step or substate of another behavior. The command is
declaration that is not a step or substate of another behavior, in a document with a `file:`
URI only (not in a notebook cell, which the kernel runs). The command is
`opensysml.runElement` with the document URI, the element's qualified name and its kind;
the VS Code extension implements it, and another client registers a command of that name
- ✅ Inlay hints (`textDocument/inlayHint`): after the name of a feature declared without a
Expand Down
4 changes: 3 additions & 1 deletion docs/guide/12-jupyter.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@ imports are followed to the files beside it as a loaded file's are.

With the [VS Code extension](08-editors.md) installed, a notebook on the `sysml` kernel is also a
language-server client: the cells have the diagnostics, hover, completion, go to definition,
references, code lenses, inlay hints, semantic highlighting and formatting a `.sysml` file has.
references, reference-count code lenses, inlay hints, semantic highlighting and formatting a
`.sysml` file has. A cell has no *Run* or *Evaluate* lens: those run a model file on disk with
`sysml`, and a cell is run by the kernel.
The server reads the notebook as **one model**, the code cells in notebook order, the way
`%load wheels.ipynb` reads a notebook into a session: a later cell sees what an earlier cell
declares, each cell's `%` command lines and bare expressions are left out rather than parsed
Expand Down
4 changes: 3 additions & 1 deletion docs/reference/lsp.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ the cell; positions in an answer — hover and lens ranges, locations, text and
document symbols, inlay hints, semantic tokens — are the cell's, and a location in another
open notebook's document names that notebook's cell. `textDocument/documentSymbol`,
`codeLens` and `semanticTokens` on a cell answer that cell's part of the notebook;
`textDocument/formatting` on a cell formats that cell's lines.
`textDocument/formatting` on a cell formats that cell's lines. A cell's code lenses are its
reference counts only: the kernel runs a cell, so the *Run* and *Evaluate* lenses, which hand
`sysml` a model file on disk, are offered on a document with a `file:` URI alone.

## Strict conformance (setting)

Expand Down
11 changes: 7 additions & 4 deletions editors/vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,9 @@ each cell the kernel's language, so the grammar colors it, and the language clie
synchronizes the notebook with `sysml-lsp` as one document of its code cells in notebook
order: a later cell resolves what an earlier one declares, `%` kernel commands and bare
expressions are left out rather than parsed, and the cells have diagnostics, hover,
completion, navigation, references, code lenses, inlay hints, semantic highlighting and
formatting. The editor reads the cells top to bottom while the kernel holds what has been run,
completion, navigation, references, reference-count code lenses, inlay hints, semantic
highlighting and formatting; a cell is run by the kernel, so it has no *Run* or *Evaluate*
lens. The editor reads the cells top to bottom while the kernel holds what has been run,
so a reference to a later cell is a warning and a cell run before an edit above it is not what
the editor shows; the guide's
[Editing a notebook in VS Code](../../docs/guide/12-jupyter.md#editing-a-notebook-in-vs-code)
Expand All @@ -188,8 +189,10 @@ lists the differences.
## Running an element from a code lens

Above each executable `action`, `state`, `calc`, `constraint` and `requirement`
the server offers a *Run* or *Evaluate* lens (and above each definition a
reference count, which opens the references peek). Clicking *Run* or *Evaluate*
in a model file on disk the server offers a *Run* or *Evaluate* lens (and above
each definition a reference count, which opens the references peek, in a
notebook cell too: a cell is run by the kernel, so it has no *Run* or *Evaluate*
lens). Clicking *Run* or *Evaluate*
saves every unsaved model file and runs the matching `sysml` check over the
workspace folders — `sysml -action Demo::Charge <folder>`, `-state`, `-calc`,
`-constraint` or `-requirement` — as a task, so the result appears in the task
Expand Down
13 changes: 8 additions & 5 deletions internal/frontend/lsp/codelens.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package lsp
import (
"context"
"fmt"
"strings"

"go.lsp.dev/protocol"
"go.lsp.dev/uri"
Expand Down Expand Up @@ -37,10 +38,8 @@ type codeLensData struct {
Name int `json:"name"`
}

// CodeLens answers the lenses of a document: a reference count on each
// definition, resolved on demand, and a Run or Evaluate command on each
// executable action, state, calc, constraint and requirement declaration that
// is not a step of another behavior.
// CodeLens answers a document's lenses: a reference count on each definition,
// and in a file on disk a Run or Evaluate command on each top-level behavior.
func (s *Server) CodeLens(ctx context.Context, params *protocol.CodeLensParams) ([]protocol.CodeLens, error) {
name := uriToName(params.TextDocument.URI)
if s.ws.IsLibraryDocument(name) {
Expand All @@ -52,6 +51,7 @@ func (s *Server) CodeLens(ctx context.Context, params *protocol.CodeLensParams)
}
pos := positionsOf(doc)
docURI := params.TextDocument.URI
runnable := onDisk(docURI)
out := []protocol.CodeLens{}
var encodeErr error
seen := map[*symbols.Symbol]bool{}
Expand All @@ -76,7 +76,7 @@ func (s *Server) CodeLens(ctx context.Context, params *protocol.CodeLensParams)
out = append(out, protocol.CodeLens{Range: rng, Data: data})
}
// `sysml` runs an element by name, so a name declared twice runs nothing.
if kind, ok := executableKind(sym); ok && len(s.ws.LookupQualified(fqn)) == 1 {
if kind, ok := executableKind(sym); runnable && ok && len(s.ws.LookupQualified(fqn)) == 1 {
command, err := runCommand(docURI, fqn, sym.Name, kind)
if err != nil {
encodeErr = err
Expand Down Expand Up @@ -185,6 +185,9 @@ func (s *Server) definitionNamed(doc, fqn string, nameAt int) *symbols.Symbol {
return nil
}

// onDisk reports whether u names a file `sysml` can read to run an element in it.
func onDisk(u uri.URI) bool { return strings.EqualFold(u.Scheme(), "file") }

// runCommand is the lens command running or evaluating the named element.
func runCommand(docURI uri.URI, fqn, name, kind string) (protocol.Command, error) {
verb := "Evaluate"
Expand Down
31 changes: 31 additions & 0 deletions internal/frontend/lsp/editor_facts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,37 @@ func TestCodeLensResolveCountsReferences(t *testing.T) {
}
}

// `sysml` reads the element's file from disk, so a document of another scheme
// keeps its reference counts and offers no Run or Evaluate lens.
func TestCodeLensRunsOnlyFilesOnDisk(t *testing.T) {
s := NewServer(model.NewWorkspace())
u := uri.URI("untitled:Untitled-1")
if err := s.DidOpen(context.Background(), &protocol.DidOpenTextDocumentParams{
TextDocument: protocol.TextDocumentItem{URI: u, Version: 1, Text: editorFactsModel},
}); err != nil {
t.Fatalf("DidOpen: %v", err)
}
lenses, err := s.CodeLens(context.Background(), &protocol.CodeLensParams{TextDocument: protocol.TextDocumentIdentifier{URI: u}})
if err != nil {
t.Fatalf("CodeLens: %v", err)
}
counts := map[string]bool{}
for _, lens := range lenses {
if lens.Command.Command != "" {
t.Errorf("lens %q (%s) on a document not on disk", lens.Command.Title, lens.Command.Command)
continue
}
if data, ok := lensData(&lens); ok && data.URI == u {
counts[data.Element] = true
}
}
for _, fqn := range []string{"Demo::Fall", "Demo::Rover", "Demo::Charge", "Demo::Mission", "Demo::Positive"} {
if !counts[fqn] {
t.Errorf("no reference lens on %s; got %v", fqn, counts)
}
}
}

func TestCodeLensSkipsLibraryDocuments(t *testing.T) {
s, _ := openEditorFactsModel(t)
lenses, err := s.CodeLens(context.Background(), &protocol.CodeLensParams{
Expand Down
3 changes: 2 additions & 1 deletion internal/frontend/lsp/notebook_requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ func (s *Server) cellRequest(ctx context.Context, req *jsonrpc2.Request, nb *not
lenses, err := s.CodeLens(ctx, p)
var own []protocol.CodeLens
for _, lens := range lenses {
if inCell(lens.Range) {
// The kernel runs a cell; a Run lens would hand `sysml` the notebook file.
if inCell(lens.Range) && lens.Command.Command != runElementCommand {
own = append(own, lens)
}
}
Expand Down
37 changes: 37 additions & 0 deletions internal/frontend/lsp/notebook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,40 @@ func TestNotebookLanguageChangeReparsesUnchangedText(t *testing.T) {
t.Errorf("part def in a KerML notebook: want KerML syntax diagnostics, got %s", asJSON(diags))
}
}

// A cell is run by the kernel, not by `sysml` from disk: its lenses count
// references, resolved in the cell's terms, and none runs or evaluates.
func TestNotebookCellLensesCountReferencesAndRunNothing(t *testing.T) {
nb := openCells(t, []cellSpec{
{"sysml", "package P {\n part def A;\n action def Go;\n calc def Area;\n}\n"},
{"sysml", "part a : P::A;\naction go : P::Go;\n"},
})
nb.awaitDiagnostics(nb.cells[0])
lenses, _ := nb.call("textDocument/codeLens", fmt.Sprintf(`{"textDocument":{"uri":%q}}`, nb.cells[0])).([]any)
if strings.Contains(asJSON(lenses), runElementCommand) {
t.Errorf("a cell offers a Run or Evaluate lens: %s", asJSON(lenses))
}
var partLens any
counted := map[string]bool{}
for _, lens := range lenses {
fields, _ := lens.(map[string]any)
data, _ := fields["data"].(map[string]any)
element, _ := data["element"].(string)
counted[element] = true
if element == "P::A" {
partLens = lens
}
}
for _, fqn := range []string{"P::A", "P::Go", "P::Area"} {
if !counted[fqn] {
t.Errorf("no reference lens on %s in the cell: %s", fqn, asJSON(lenses))
}
}
if partLens == nil {
t.Fatalf("no reference lens on P::A: %s", asJSON(lenses))
}
resolved := asJSON(nb.call("codeLens/resolve", asJSON(partLens)))
if !strings.Contains(resolved, `"title":"1 reference"`) || !strings.Contains(resolved, nb.cells[1]) {
t.Errorf("resolved reference lens on P::A: %s", resolved)
}
}
Loading