Skip to content

[enhancement] frontmatter completion in iwes from the bound document schema #393

Closed

Description

@kiil

Summary

iwes already completes links (trigger [, configured under [completion]).
This asks for the same treatment for frontmatter: complete field names and enum
values from the Document Schema that .iwe/config.toml already binds to the
document's path.

Motivation

In a workspace with path-bound schemas:

[schemas.article]
match = "data/stories/*"

.iwe/schemas/article.yaml already declares the vocabulary:

stage:
  enum: [pitch, assigned, research, draft, review, published, shelved]
  description: where the story sits in the pipeline
format:
  enum: [nyhed, analyse, kommentar, interview, reportage, feature, faktatjek, andet]

While editing data/stories/2026-08-31-example.md, none of that is available at
the cursor. You type stage: from memory and find out at
iwe schema validate — or at commit time — whether the value was in the enum.
The schema is bound, loaded, and knows the answer; it just isn't reachable while
writing.

Why this can't be solved editor-side

The obvious workaround is yaml-language-server with the schema mapped to the
file. It doesn't apply: LSP servers attach per buffer filetype, and frontmatter
lives inside a markdown buffer, so a YAML server never attaches. Treesitter
injection highlights the region but does not route it to another server.

That leaves the markdown server as the only one in position to answer. For a
schema-carrying workspace, iwes is the only candidate.

Why it should fit the architecture

From the discussion in #374, Document Schema validation is deliberately local —
every schema node validates the node it matched, with no shared state and no
second pass.

Frontmatter completion has the same property. The completion at a cursor is
derived from the schema node matched at that frontmatter path, for the schema
already bound by path glob. No symbol table, no cross-document resolution, no
accumulated state — the same locality that makes validation composable. It also
strengthens an existing path ([completion]) rather than opening a new one.

Possible scope, smallest first

  1. Key completion — at a frontmatter path, offer the properties of the
    matched schema node, with required fields ranked first.
  2. Enum value completion — after field: , offer that field's enum.
  3. Hover — surface the schema's description for the field under the
    cursor.

Tier 2 alone would carry most of the value; a mistyped enum is the common error
and the one validation reports last.

Notes

Happy to test against a real workspace — this comes from daily use of a
schema-heavy one (a newsroom graph with article, beat, source, task,
plan and hub types, all path-bound).

Nothing here asks for new state in the server: the schemas and the parsed
frontmatter are already in the process.

Activity

  1. kiil commented on Aug 31, 2026

    @kiil
    Author

    Correcting myself: the "this can't be solved editor-side" section above is
    wrong. yaml-language-server can be pointed at markdown, and frontmatter
    completion does work. I tested it after filing this.

    What works, in Neovim:

    filetypes = { "yaml", "markdown" },   -- the part I claimed was impossible
    settings = { yaml = {
      format = { enable = false },
      schemas = {
        ["/abs/path/.iwe/schemas/json/article.json"] = "data/stories/*.md",
      },
    } }

    Verified against a real workspace: key completion offers the schema's unset
    properties, value completion offers stage's enum (pitch, assigned, research, draft, review, published, shelved), and an invalid value reports
    Value is not accepted. Valid values: ... on the frontmatter line.

    So this issue is no longer "there is no way". It's "the way costs five moving
    parts, and four of them exist only because the server on the other side isn't
    IWE."

    The Document Schema has to be split. .iwe/schemas/*.yaml is not JSON
    Schema — the frontmatter node sits under a frontmatter: key beside
    sections:. Using it with yamlls means generating standalone JSON Schema files
    from that one node and regenerating them whenever a schema changes. That is a
    second source of truth that drifts silently.

    Path bindings get maintained twice. [schemas.article] match = "data/stories/*" in .iwe/config.toml, and the same glob again in the editor
    config. Nothing keeps them in sync.

    The prose becomes YAML. yamlls parses the whole file as a YAML stream, so
    the body produces junk: *italics* is read as an alias
    (Unexpected alias at node end), and the rest collapses into one
    Unexpected scalar token in YAML stream spanning the document. Usable only
    with a diagnostic filter that drops everything below the closing ---.

    Formatting is a live hazard. yamlls advertises documentFormatting on the
    markdown buffer. With format-on-save enabled — the default in several
    distributions — a YAML formatter is one keystroke from rewriting a prose
    document. It has to be turned off deliberately, and nothing warns you.

    Only frontmatter is covered. sections: and block structure get nothing,
    so iwe schema validate stays the authority regardless.

    Every editor redoes it. The above is ~110 lines of Neovim Lua, including
    the fence-aware diagnostic filter and workspace-root resolution for the schema
    paths. None of it transfers to another editor.

    iwes has the schemas, the path bindings and the parsed frontmatter already,
    and it knows where the frontmatter ends — so none of these five costs exist on
    that side. That is the argument for tier 2 (enum value completion) more than
    the original "no alternative" framing was.

    Happy to share the full Neovim config if it's useful as a reference for what
    the native version would replace.

  2. locked and limited conversation to collaborators on Aug 31, 2026
  3. converted this issue into a discussion #394 on Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions