diff --git a/README.md b/README.md index c489bc7..517479a 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,24 @@ repos: # - id: gmat-script-format-check # or: check only, never write (CI) ``` +## Editor tooling + +[![VS Marketplace](https://img.shields.io/visual-studio-marketplace/v/astro-tools.gmat-script?label=VS%20Marketplace&color=311B92)](https://marketplace.visualstudio.com/items?itemName=astro-tools.gmat-script) +[![Open VSX](https://img.shields.io/open-vsx/v/astro-tools/gmat-script?label=Open%20VSX&color=311B92)](https://open-vsx.org/extension/astro-tools/gmat-script) + +The same engine drives an editor experience — highlighting, hover docs, live diagnostics, completion, +go-to-definition, an outline, and format-on-save. + +- **VS Code** — the **GMAT Script** extension, on the + [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=astro-tools.gmat-script) + and [Open VSX](https://open-vsx.org/extension/astro-tools/gmat-script). Highlighting works on + install; the richer features come from the language server (`pip install "gmat-script[lsp]"`). +- **Neovim, Emacs, Helix, and the rest** — a Language Server Protocol server (`gmat-script-lsp`, + from the `lsp` extra) backs any LSP-capable editor. + +See the [VS Code extension guide](https://astro-tools.github.io/gmat-script/vscode/) and the +[language server guide](https://astro-tools.github.io/gmat-script/lsp/). + ## The grammar surface The tree-sitter grammar parses GMAT scripts (`.script`) and GmatFunctions (`.gmf`) — the same @@ -112,6 +130,11 @@ parsing is effectively version-independent — scripts from other releases parse *do* vary by release (valid field names, enums, defaults) belong to the linter and are scoped to R2026a. +Those semantics live in a **field catalogue** reflected from R2026a (102 resource types, 2614 +fields) and shipped as data inside the wheel, so the linter and editor tooling need no GMAT install. +It is version-pinned and provenance-stamped, selectable per release, and adding another GMAT version +is a data file — not a code change. See [the field catalogue](https://astro-tools.github.io/gmat-script/catalogue/). + ## What gmat-script is not - **Not a propagator or astrodynamics engine.** It reads and transforms script *text*; it computes @@ -123,7 +146,8 @@ to R2026a. ## Documentation Full documentation — getting started, the grammar surface, the typed AST and editing guides, the -formatter, the linter, the command-line tool, the error-reporting model, and the API reference — is at +formatter, the linter and the field catalogue, the command-line tool, the language server and VS Code +extension, the error-reporting model, and the API reference — is at **[astro-tools.github.io/gmat-script](https://astro-tools.github.io/gmat-script/)**. ## License diff --git a/docs/api.md b/docs/api.md index 9f8414f..e5e19d9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3,10 +3,12 @@ The public surface of `gmat_script`. It is deliberately minimal and additive: the `parse` entry point and the `Tree` it returns, the `ErrorNode` / `Position` records that describe syntax errors, the typed `Script` overlay with its mutation API (`ObjectRef`, `RawValue`, `MutationError`), the -canonical `format` pretty-printer, and the `lint` checker with its `Diagnostic` / `Severity` records. -Each layer is re-exported here as it lands. +canonical `format` pretty-printer, the `lint` checker with its `Diagnostic` / `Severity` records, and +the `Catalog` field catalogue (`load_catalog`, `FieldSpec`, `TypeSpec`). Each layer is re-exported +here as it lands. For a worked introduction see [Getting started](getting-started.md); for the error model see -[Error reporting](errors.md); for the linter see [Linting](lint.md). +[Error reporting](errors.md); for the linter see [Linting](lint.md); for the catalogue see +[The field catalogue](catalogue.md). ::: gmat_script diff --git a/docs/catalogue.md b/docs/catalogue.md new file mode 100644 index 0000000..0f8743a --- /dev/null +++ b/docs/catalogue.md @@ -0,0 +1,120 @@ +# The field catalogue + +The field catalogue is the knowledge base behind everything that reasons about GMAT *semantics*: the +[linter](lint.md), and the [language server](lsp.md)'s hover docs and completion. It records, for +every GMAT resource type, the fields that type defines — each field's type, allowed enumeration, +reference target, default, and unit — so a tool can tell that `Spacecraft.SMA` wants a number, that +`ImpulsiveBurn.Axes` is one of four values, or that `Spacecraft.Tanks` points at a `FuelTank`, +without running GMAT. + +It is shipped as **data**, not derived at runtime. The wheel carries a precompiled JSON catalogue; +loading it imports no GMAT and touches no `gmatpy`. + +## Where it comes from — and the GMAT-free guarantee + +The catalogue is reflected from a real GMAT install at build time, by a single generator, and then +frozen into the package. Two halves, deliberately kept apart: + +- **Generation (build/CI time, GMAT required).** `gmat_script.tools.gen_catalog` is the *only* + GMAT-touching code in the project. It imports `gmatpy`, walks GMAT's object factory and each + object's parameter metadata, normalises the types, and writes + `gmat_script/data/fields-.json`. +- **Loading (runtime, no GMAT).** `gmat_script.catalog` reads that JSON and answers queries. It + **never imports `gmatpy`** — so a plain `pip install gmat-script` carries the full catalogue with + no GMAT install anywhere. + +This split is the GMAT-free guarantee in practice: GMAT's knowledge is captured once, offline, and +travels with the package as a data file. (See the [design decisions](design/decisions.md).) + +The shipped catalogue is reflected from **GMAT R2026a** and records its 102 resource types and 2614 +fields. + +## Reading the catalogue + +`load_catalog()` returns a cached, queryable `Catalog`. It and its records are part of the public +API: + +```python +from gmat_script import load_catalog + +cat = load_catalog() + +cat.gmat_version # 'R2026a' — the release this was reflected from +cat.generated # '2026-06-08' — the ISO date it was generated + +cat.has_type("Spacecraft") # True +cat.field_type("Spacecraft", "SMA") # 'real' +cat.enum_values("ImpulsiveBurn", "Axes") +# ('VNB', 'LVLH', 'MJ2000Eq', 'SpacecraftBody') +cat.ref_target("Spacecraft", "Tanks") # 'FuelTank' +``` + +A type name that GMAT spells differently in scripts resolves through an alias (`Propagator` -> +`PropSetup`, `ODEModel` -> `ForceModel`), so `cat.has_type("Propagator")` is `True`. Any query for an +unknown type or field returns `None` (or `[]`) rather than raising — the linter and editor leans on +this to degrade to "no finding" wherever the catalogue is silent. + +### What a field carries + +`cat.field(type, name)` returns a `FieldSpec`: + +| Attribute | Meaning | +|-----------|---------| +| `type` | The normalised catalogue type: `real`, `integer`, `string`, `bool`, `enum`, `object`, `object_array`, `string_array`, `real_array`, `matrix`, `filename`, `on_off`, `color`, `gmat_time`, … | +| `gmat_type` | GMAT's own raw type label, kept verbatim (e.g. `Real`, `Rmatrix`). | +| `read_only` | Whether GMAT exposes the field as output-only (not settable from a script). | +| `allowed` | The enum's allowed values, where GMAT exposes them — else `None`. | +| `ref_target` | The target GMAT type for an object-reference field — else `None`. | +| `default` | The default for a settable scalar field — else `None`. | +| `unit` | The field's unit, where GMAT reports one — else `None`. | + +`cat.type_spec(type)` returns a `TypeSpec` carrying the type's GMAT object-type `category` and its +`fields` mapping. The lower-level `Catalog.load(target_version=...)` builds an uncached catalogue; +`load_catalog` is the convenience entry point most consumers want. + +## Versioning + +The semantics a script must satisfy — valid field names, enums, defaults — vary by GMAT release, so +the catalogue is **version-pinned and provenance-stamped**: every catalogue carries the GMAT version +it was reflected from and the date it was generated. The grammar, by contrast, never enumerates types +or keywords and is effectively version-independent; the catalogue is the one version-coupled artifact. + +`load_catalog()` defaults to the newest shipped catalogue. Pass `target_version` to pin a specific +release: + +```python +load_catalog("R2026a") # an explicit release +load_catalog() # the newest available (currently R2026a) +``` + +The linter exposes the same selector — `lint(source, target_version="R2026a")` — and the language +server uses the default. Requesting a version that is not shipped raises `ValueError` listing what is +available. + +Supporting another GMAT release is **additive**: a new `fields-.json` is dropped into +`gmat_script/data/`, and `load_catalog()`'s "newest" default and the `target_version` selector pick +it up with no code change. There is no per-version code fork. + +## Regenerating the catalogue (the version-bump process) + +Regenerating is only needed when targeting a new GMAT release or picking up a point-release metadata +change. It needs a real GMAT install — `gmatpy` is imported from it directly and is never +pip-installed. + +```console +# Regenerate and overwrite the shipped file, against a GMAT install: +$ python -m gmat_script.tools.gen_catalog +# Regenerate in memory and fail on any drift from the committed catalogue (the CI check): +$ python -m gmat_script.tools.gen_catalog --check +``` + +The generator locates GMAT from `--gmat-root`, else the `GMAT_ROOT` environment variable, else a set +of platform-standard install paths. A dedicated CI job runs the `--check` form on a schedule (and on +demand) against a freshly provisioned GMAT install, so a catalogue that drifts from the current GMAT +metadata is surfaced rather than silently rotting. The generation date is ignored in the drift +comparison, so re-running on a different day is not spurious drift. + +To bump to a new release: regenerate against that GMAT version (so the file is written as +`fields-.json`), commit the new data file alongside the existing one, and the loader's +"newest" default begins serving it. Keeping the prior file lets callers pin the older release through +`target_version`. diff --git a/docs/index.md b/docs/index.md index e0f2f9b..63ed880 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,9 +12,9 @@ tree.has_errors # False tree.to_source() # round-trips byte-for-byte to the input ``` -gmat-script ships the parser, a typed AST with a mutation API, and a canonical formatter, with a -`gmat-script` command-line tool over the same engine. The linter and editor tooling build on top of -the same tree as they land. +gmat-script ships the parser, a typed AST with a mutation API, a canonical formatter, and a static +linter, with a `gmat-script` command-line tool over the same engine — plus a language server and a +VS Code extension that bring it all to your editor. ## What it is @@ -22,8 +22,7 @@ the same tree as they land. full R2026a sample corpus and re-emits it byte-for-byte. - A Python library that loads that grammar from a **vendored, precompiled** binding — so `pip install gmat-script` needs no C or Node toolchain, and never GMAT. -- A `gmat-script` command-line tool that parses and formats scripts from the shell or CI (with - linting to follow as it lands). +- A `gmat-script` command-line tool that parses, formats, and lints scripts from the shell or CI. ## What it is not @@ -41,7 +40,11 @@ the same tree as they land. - **[Typed AST](typed-ast.md)** — typed resources and dict-like field access over the tree. - **[Editing](editing.md)** — set fields, rename resources, and splice commands. - **[Formatter](formatting.md)** — canonical, idempotent re-emission. -- **[CLI](cli.md)** — the `parse` syntax gate and the `format` command. +- **[Linter](lint.md)** — structural checks against the bundled field catalogue. +- **[Field catalogue](catalogue.md)** — the version-pinned knowledge base behind the linter and editor. +- **[CLI](cli.md)** — the `parse` syntax gate and the `format` and `lint` commands. +- **[Language server](lsp.md)** — diagnostics, hover, and completion in any LSP editor. +- **[VS Code extension](vscode.md)** — highlighting, diagnostics, and format-on-save in VS Code. - **[Error reporting](errors.md)** — how malformed input is surfaced. - **[API reference](api.md)** — the public Python surface. - **[Design decisions](design/decisions.md)** — the grammar scope, CST node taxonomy, and the diff --git a/docs/lsp.md b/docs/lsp.md index 95e7cc6..0297e0d 100644 --- a/docs/lsp.md +++ b/docs/lsp.md @@ -69,12 +69,9 @@ vim.api.nvim_create_autocmd("FileType", { ### VS Code -Install the [**GMAT Script**](https://marketplace.visualstudio.com/items?itemName=astro-tools.gmat-script) -extension (also on [Open VSX](https://open-vsx.org/extension/astro-tools/gmat-script)). It bundles a -client that launches this server for you, and adds standalone syntax highlighting that works even -before the server is installed. Add the server with `pip install "gmat-script[lsp]"` — the extension -picks up `gmat-script-lsp` from your `PATH`, or point `gmatScript.server.pythonPath` at the Python -environment that has it. Format-on-save is enabled for GMAT files by default. +The [**GMAT Script**](vscode.md) extension bundles a client that launches this server for you, plus +standalone syntax highlighting that works before the server is installed. See the [VS Code extension +guide](vscode.md) for install (Marketplace / Open VSX), features, format-on-save, and settings. ## How it fits together diff --git a/docs/vscode.md b/docs/vscode.md new file mode 100644 index 0000000..def540a --- /dev/null +++ b/docs/vscode.md @@ -0,0 +1,71 @@ +# The VS Code extension + +**GMAT Script** brings the grammar, linter, and [language server](lsp.md) to Visual Studio Code: +syntax highlighting, hover docs, live diagnostics, completion, go-to-definition, an outline, and +format-on-save for `.script` and `.gmf` files. + +## Installing + +Install **GMAT Script** from either marketplace: + +- [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=astro-tools.gmat-script) + — search *GMAT Script* in the Extensions view, or run + `code --install-extension astro-tools.gmat-script`. +- [Open VSX](https://open-vsx.org/extension/astro-tools/gmat-script) — for VSCodium, Cursor, Gitpod, + and other Open VSX–based editors. + +The extension bundles **syntax highlighting** as a TextMate grammar, so colouring works the moment +it is installed — no Python, no further setup. + +Everything richer — hover, diagnostics, completion, definition, references, the outline, and +formatting — is served by the `gmat-script` language server, which runs on Python. Install it into a +Python environment the extension can reach: + +```console +$ pip install "gmat-script[lsp]" +``` + +This puts a `gmat-script-lsp` command on your `PATH`, which the extension launches automatically. If +it is not found, highlighting still works and the extension points you here. To use a specific +interpreter or virtual environment, set `gmatScript.server.pythonPath` (see +[Settings](#settings)). + +## Features + +| Feature | Needs the server? | +|---------|:-----------------:| +| Syntax highlighting — resources, commands, fields, control flow, solver blocks, GmatFunction headers | no | +| Hover — a field's type, default, allowed values, units, and reference target | yes | +| Live diagnostics as you type — syntax errors plus linter findings | yes | +| Completion — resource names, the fields valid for the resource under the cursor, and enum values | yes | +| Go to definition / find all references for resources | yes | +| Document outline — every `Create`'d resource and GmatFunction header | yes | +| Format on save — canonical, idempotent re-emission | yes | + +The server-backed features are exactly those of the [language server](lsp.md); the extension is the +VS Code client that launches and talks to it. A half-typed buffer never breaks them — the +error-recovering parse keeps hover and completion working while you edit. + +## Format on save + +Format-on-save is **enabled for GMAT files by default**: the extension sets `editor.formatOnSave` +for the `gmat` language, so saving a `.script` or `.gmf` rewrites it into canonical form via the same +formatter the [CLI](cli.md) and library use. Turn it off per-language if you prefer: + +```jsonc +"[gmat]": { "editor.formatOnSave": false } +``` + +## Settings + +| Setting | Default | Description | +|---|---|---| +| `gmatScript.server.path` | `gmat-script-lsp` | Command used to launch the language server. Set an absolute path if it is not on your `PATH`. | +| `gmatScript.server.pythonPath` | _(empty)_ | A Python interpreter with `gmat-script[lsp]` installed; when set, the server runs as ` -m gmat_script.lsp` and takes precedence over `server.path`. Point it at a virtual environment. | +| `gmatScript.server.args` | `[]` | Extra arguments passed to the server on launch. | +| `gmatScript.trace.server` | `off` | Trace JSON-RPC traffic to the output channel (`off` / `messages` / `verbose`) when debugging. | + +## Other editors + +The same server powers any LSP-capable editor — Neovim, Emacs, Helix, and the rest. See the +[language server guide](lsp.md) for editor-agnostic setup. diff --git a/editors/vscode/images/README.md b/editors/vscode/images/README.md index a4fe859..2cda16d 100644 --- a/editors/vscode/images/README.md +++ b/editors/vscode/images/README.md @@ -1,8 +1,48 @@ # Marketing assets `demo.gif` — a short animated capture of the extension in action (highlighting + live diagnostics + -hover on a stock GMAT sample), shown at the top of the extension's marketplace listing. +hover + format-on-save), shown at the top of the extension's marketplace listing and referenced from +`../README.md` (the reference is commented out until the file exists). -To add it: record a `.script` editing session in VS Code (type into a sample, trigger a diagnostic, -hover a field), export an optimized GIF (≈800 px wide, a few seconds, looping), save it here as -`demo.gif`, and uncomment the image reference in `../README.md`. +## Recording `demo.gif` + +A repeatable ~8-second capture. Keep it small and looping. + +**Setup** + +1. Install the extension (run the *Extension Development Host* from this folder, or install the + packaged `.vsix`) and `pip install "gmat-script[lsp]"` into the interpreter VS Code uses, so the + language server is live. +2. Use a clean window: dark theme, no minimap, font size ~16, a single editor column ~80 columns + wide. Hide the sidebar and status-bar clutter. +3. Create a new file `demo.script` and leave it empty. + +**The take** (type at a natural pace; let each step settle for ~1 s) + +1. Type the snippet below. Highlighting colours resources, fields, the burn axis, and commands as you + go: + + ```text + Create Spacecraft Sat + Sat.SMA = 7000 + + Create ImpulsiveBurn TOI + TOI.Axes = VNB + + BeginMissionSequence + Maneuver TOI(Sat) + ``` + +2. Change `Sat.SMA = 7000` to `Sat.SMA = 'high'`. A red squiggle appears under `'high'`; hover it to + show the `type-mismatch` diagnostic ("field 'SMA' expects a number, got a quoted string"). +3. Hover `Axes` to show the field doc card (type + allowed values `VNB, LVLH, MJ2000Eq, + SpacecraftBody`). +4. Fix `Sat.SMA` back to `7000`; the squiggle clears. +5. Mangle the spacing (e.g. `GMAT Sat.SMA=7000;`) and press **Save** — format-on-save snaps it back to + canonical form. + +**Export** + +Capture the editor region only, export an optimized looping GIF (≈800 px wide, a few seconds), save +it here as `demo.gif`, and uncomment the `![GMAT Script in VS Code](images/demo.gif)` line in +`../README.md`. diff --git a/examples/README.md b/examples/README.md index 2e4d898..e961c0e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -13,8 +13,10 @@ python examples/edit_field.py | [`edit_field.py`](edit_field.py) | Read a resource field, then set it — `script.spacecraft["Sat"]["SMA"] = 8000`. | | [`rename_resource.py`](rename_resource.py) | Rename a resource with and without rewriting its references. | | [`format_in_place.py`](format_in_place.py) | Format a messy script into canonical form, written back to the file. | +| [`lint_script.py`](lint_script.py) | Lint a flawed script — type, field, reference-target, and enum findings — then show it clean. | For the concepts behind these, see the documentation: [the typed AST](https://astro-tools.github.io/gmat-script/typed-ast/), -[editing](https://astro-tools.github.io/gmat-script/editing/), and -[the formatter](https://astro-tools.github.io/gmat-script/formatting/). +[editing](https://astro-tools.github.io/gmat-script/editing/), +[the formatter](https://astro-tools.github.io/gmat-script/formatting/), and +[the linter](https://astro-tools.github.io/gmat-script/lint/). diff --git a/examples/lint_script.py b/examples/lint_script.py new file mode 100644 index 0000000..5e91182 --- /dev/null +++ b/examples/lint_script.py @@ -0,0 +1,65 @@ +"""Lint a GMAT script: print the diagnostics for a flawed script, then show it clean. + +Run with: python examples/lint_script.py + +The linter checks a script's *structure* against the bundled field catalogue — unknown types and +fields, type / enum / reference-target mismatches, duplicate names, and unused or undeclared +references. It never runs the mission and needs no GMAT install. +""" + +from __future__ import annotations + +from gmat_script import lint + +# A syntactically valid script with several seeded *structural* problems the linter catches. +FLAWED = """\ +Create Spacecraft Sat +Sat.SMA = 'high' +Sat.Naem = 7000 + +Create Thruster Thr +Sat.Tanks = {Thr} + +Create ImpulsiveBurn TOI +TOI.Axes = Sideways + +BeginMissionSequence +Maneuver TOI(Sat) +""" + +# The same mission with each finding addressed: a numeric SMA, the misspelled field corrected, a +# FuelTank in the tank slot, and a valid burn axis. The linter reports nothing. +CLEAN = """\ +Create Spacecraft Sat +Sat.SMA = 7000 +Sat.Id = 'SAT-1' + +Create FuelTank Tank +Sat.Tanks = {Tank} + +Create ImpulsiveBurn TOI +TOI.Axes = VNB + +BeginMissionSequence +Maneuver TOI(Sat) +""" + + +def report(label: str, source: str) -> None: + print(f"--- {label} ---") + diagnostics = lint(source) + if not diagnostics: + print("no findings") + return + for d in diagnostics: + print(f"{d.start.line}:{d.start.column} {d.severity} {d.rule}: {d.message}") + + +def main() -> None: + report("flawed script", FLAWED) + print() + report("cleaned script", CLEAN) + + +if __name__ == "__main__": + main() diff --git a/mkdocs.yml b/mkdocs.yml index 51add51..4ac10c8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,8 +40,11 @@ nav: - Editing: editing.md - Formatter: formatting.md - Linter: lint.md + - Field catalogue: catalogue.md - CLI: cli.md - - Language server: lsp.md + - Editor tooling: + - Language server: lsp.md + - VS Code extension: vscode.md - Error reporting: errors.md - API reference: api.md - Design: