Skip to content

Latest commit

 

History

History
267 lines (244 loc) · 13.2 KB

File metadata and controls

267 lines (244 loc) · 13.2 KB

Editor Module Interface Projection

splash-lsp can receive a small description of host-defined mod.* paths when an editor starts it or through a later configuration refresh. This is authoring metadata only. It does not make Splash a package loader, create a Rust adapter, or prove that a module exists in the runtime selected for a document.

Initialization and refresh format

Pass initializationOptions.splash.moduleCatalog as an array of full module paths. Each descriptor accepts path, an optional description, and an optional callMode of "synchronous" or "deferred", plus an optional callShape of "single_json", plus optional inputFields and outputFields; unknown unrelated fields are ignored. callMode is advisory presentation metadata for an exact leaf method path, not a runtime declaration. callShape explicitly says that a direct method has one JSON-compatible argument; it must appear with callMode and never creates a runtime contract. inputFields is a compact literal-record view for that one argument, while outputFields is the compact view for the declared JSON result. Both require callShape: "single_json". An inputFields or outputFields entry whose type is object may contain one optional fields array describing its direct object children; a child entry cannot itself contain fields. A path with callMode must have at least three segments (mod.<module>.<method>) and cannot also be a parent of another catalog path.

{
  "splash": {
    "moduleCatalog": [
      {
        "path": "mod.app.weather",
        "description": "Host-provided weather module."
      },
      {
        "path": "mod.app.weather.current",
        "description": "Returns the current forecast data.",
        "callMode": "deferred",
        "callShape": "single_json",
        "inputFields": [
          {
            "name": "location",
            "type": "string",
            "required": true,
            "description": "Canonical location to query."
          },
          {"name": "units", "type": "string", "required": false},
          {
            "name": "filters",
            "type": "object",
            "required": false,
            "fields": [
              {"name": "station", "type": "string", "required": false}
            ]
          }
        ],
        "outputFields": [
          {
            "name": "summary",
            "type": "object",
            "required": true,
            "description": "Current forecast summary.",
            "fields": [
              {
                "name": "temperature",
                "type": "number",
                "required": true,
                "description": "Current temperature in the selected units."
              }
            ]
          }
        ]
      }
    ]
  }
}

Every path must start with mod, have at least one following canonical Splash identifier, contain at most 16 segments, and fit in 256 bytes. The LSP keeps at most 256 descriptors, 1,024 aggregate input fields (including retained input children), 1,024 aggregate output fields (including retained output children), and 512 KiB of retained path, description, call-mode, call-shape, and field bytes. A descriptor description and a field description each cap at 4 KiB. Every input or output field requires a canonical Splash identifier up to 128 bytes, one of the fixed any, null, boolean, number, integer, string, array, or object types, and an explicit Boolean required value. fields is valid only on a top-level object input or output field and retains at most one child level; duplicates at either retained level, fields on a non-object field, a deeper fields list, or any other malformed recognized callMode, callShape, inputFields, or outputFields discards the catalog as a whole; completion at a matching site then returns no candidates with isIncomplete: true.

The fixed mod.tool namespace is excluded from this metadata format. A path whose first segment after mod is tool is rejected rather than treated as a host-defined interface descriptor.

When a host configures a runtime direct capability module, it can pass CapabilityRuntime::module_interface_catalog() directly as this projection. For example, a reviewed runtime binding for use mod.arithmetic and arithmetic.add(...) produces mod.arithmetic and mod.arithmetic.add entries; a direct method includes its host-selected callMode and callShape: "single_json". When its executable input or output schema is an explicit object with a properties map whose declared property and required names use canonical Splash identifiers, it also projects inputFields or outputFields with the schema field type, required bit, and optional plain-text description. For an input or output property explicitly typed as object with its own complete properties map, the runtime also projects that one direct child level as fields; deeper object structure is omitted. Array, scalar, missing-properties, noncanonical-key, and otherwise incomplete retained record shapes omit the corresponding view rather than exposing a partial one. The runtime module is still configured separately during host setup; this returned list is only a bounded snapshot for editor completion, hover, and explicit signature metadata, not runtime discovery or authority.

A host can replace the complete projection later through workspace/didChangeConfiguration using the same array under settings.splash.moduleCatalog:

{
  "settings": {
    "splash": {
      "moduleCatalog": [
        {
          "path": "mod.app.weather",
          "description": "Host-provided weather module."
        }
      ]
    }
  }
}

An omitted moduleCatalog key preserves the prior projection; JSON null explicitly clears it. A malformed, duplicate, or over-limit replacement makes only module completion unavailable rather than retaining stale paths. A valid empty array is a complete empty projection. Module refreshes do not alter toolCatalog or the atomic workflow-data pair. A malformed settings value or non-object settings.splash clears all advisory catalogs.

Completion and hover behavior

The LSP can complete the current segment in a direct statement-position import such as use mod. or use mod.app.. It also completes immediate static children after a direct, visible imported module binding or a stable exact local root alias, including a bounded chain of catalog paths below that binding:

use mod.app.weather
let weather_api = weather
weather_api.
weather_api.current.

Only immediate children at the selected catalog path are exposed. Intermediate namespaces inferred from a deeper path have no borrowed leaf description. The LSP replaces exactly the current path or member segment and renders descriptions as plain text. An exact callMode: "deferred" leaf is labeled as returning a promise and documents that the generated call needs await(); a synchronous leaf is labeled as synchronous. The LSP never inserts await() or changes source beyond the selected identifier segment. A chain has at most 16 identifier segments. It must begin at the visible binding from a direct use mod.* statement or a qualifying exact root alias.

This is an editor-only, source-only alias rule. An alias such as let weather_api = weather is accepted only through exact root let alias = binding edges, for at most 16 hops, with complete lexical/import/alias metadata. Other than the active queried receiver, every reference in the resolved import-alias group must remain an exact group alias or direct member call; writes, member extraction, parenthesized/computed edges, and other escapes make completion, hover, input-key completion/hover, result-field metadata, and signature help fail closed. It never resolves a module, evaluates source, or creates authority. The fixed mod.tool API deliberately does not use this alias rule. When retained alias edges are capped, a local-alias completion returns no catalog metadata and marks the completion incomplete; a direct import remains independent of that alias report.

Hovering an exact catalog leaf reached through the same visible import or qualifying alias path returns its canonical catalog path, any plain-text description or call-mode note, any compact input- and output-record field lists, and the advisory authority boundary. Inferred namespaces and unresolved, shadowed, or non-direct paths have no catalog hover.

For an exact visible leaf with callShape: "single_json" and inputFields, the server also completes an undeclared root key or one direct object-child key while the cursor is in the first direct literal-record argument, such as weather.current({loc}), weather_api.current({loc}), or weather.current({filters: {sta}}). It replaces only that key identifier and does not insert an object, a value, or await(). Hovering an exact known root or direct child key presents the same plain-text advisory field metadata. The recognizer rejects a path below that direct child level, second argument, string or comment cursor, mismatched/deep delimiters, duplicate prior key, truncated import metadata, shadowed receiver, malformed record prefix, or unknown/unshaped leaf. It does not evaluate JSON Schema, infer a value or deeper/arbitrary nested shape, read a runtime, validate a contract, or grant a capability.

For an exact source binding on that same shaped leaf, the LSP can also use outputFields for a result member: a synchronous leaf must appear exactly as let result = weather.current(input) or let result = weather_api.current(input), while a deferred leaf uses the same exact direct form ending in .await(). At result.field, it completes projected root fields and hovers known fields with plain-text metadata. For an explicit object root field with retained fields, it also completes and hovers one direct child path such as result.summary.temperature. Exact local let alias = result chains of at most 16 hops receive the same metadata, so alias.summary.temperature is equally eligible. The complete result-alias group, including aliases declared after the queried member, must remain stable; a capped alias report makes output completion empty and incomplete. The recognizer accepts exactly one completed balanced argument and one direct imported member call. It rejects zero or multiple arguments, parenthesized or computed initializers or aliases, deeper alias chains, other postfix chains, non-alias bare uses, mutations, possible escapes, let selected = result.field, deeper result paths such as result.summary.temperature.unit, shadowed imports, truncated metadata, and source beyond the first diagnostic. This is not result-type inference or runtime inspection; an output suggestion does not validate a result, load a module, or grant a capability.

The server also advertises textDocument/signatureHelp. An exact visible leaf through the same import-or-qualifying-alias rule, with both callMode and callShape: "single_json", has a one-argument input signature and labels its result as either a JSON value or a promise of one. Mode-only metadata remains useful for completion and hover, but gets no invented arity or value contract. The scanner is bounded by the source and canonical nesting limits, accepts an in-progress string argument, and refuses a cursor inside a comment, mismatched/deep delimiters, truncated scope or import metadata, shadowed receivers, and unknown or unshaped paths. Signature help uses the same plain-text advisory description and compact input/output field lists as hover; it does not resolve a module, inspect a runtime, validate an adapter contract, or authorize a call. The separate input-key completion and hover are limited to root keys and one direct object-child literal-record position. The separate output-field feature is limited to the exact result binding and bounded local alias chain described above, plus one explicit object-child path; it never follows arbitrary member chains. Neither feature performs JSON Schema evaluation, runtime value inspection, or contract validation.

mod.tool remains a fixed language surface: only a direct visible use mod.tool binding offers call, call_json, start, and start_json, regardless of this projection. The projection is also refused for a shadowed binding, a receiver that does not begin at a visible import or qualifying alias, comments, strings, or source after the first syntax diagnostic.

Security and authority

This bounded catalog lookup is not general imported-module resolution or type inference. The LSP does not read module files, URIs, the environment, a Rust registry, a capability runtime, or a live catalog. It does not validate an imported path, load a module, inspect exports, infer general record fields, or authorize a tool. The metadata, including callMode, callShape, and inputFields and outputFields, is client-supplied, potentially stale, and advisory even when an integration generated it from trusted host configuration. Those field lists are static presentation metadata, not a JSON Schema payload or contract proof. Configuration refresh only replaces editor metadata; it never validates a live runtime.

Runtime module binding and all capability decisions remain host-owned. In particular, a suggested mod.tool call target is still checked against the current runtime catalog and active capability lease.