coc-gustave
v0.2.0
Published
Contract hovers for gustave specs in coc.nvim, including under tsgo
Maintainers
Readme
coc-gustave
Contract hovers for gustave specs in coc.nvim — at call sites, across files, and under tsgo.
const divide: (a: number, b: number) => number ← from your language server
Division ← appended by this extension
require:
• divisor is nonzero
ensure:
• quotient times divisor equals dividendWhy this exists
gustave ships gustave/ts-plugin, which does the same job. But a TypeScript
language service plugin needs a language service to host it, and the two setups
a coc user is likely to be running can't:
- tsgo / TypeScript 7 has no plugin API at all.
- coc-tsserver doesn't pass
--allowLocalPluginLoads, so tsserver won't resolve a plugin from the project's owntsconfig.json.
This extension answers the same question out of process. It registers a second
hover provider; coc's HoverManager runs every registered provider and
concatenates the results, so whatever your language server said stands and the
contract is appended beneath it. It doesn't replace, wrap, or proxy your
language server, and it works the same whether that's tsgo, tsserver, or
nothing at all.
Install
:CocInstall coc-gustaveThat's it. gustave itself is a dependency of the extension, so it works even
in a project that doesn't have it installed.
Confirm with :CocList extensions.
From a checkout
For hacking on the extension, put the folder on your runtimepath instead.
With lazy.nvim:
{ dir = "/absolute/path/to/gustave/coc-extension", name = "coc-gustave" }vim-plug, or by hand:
Plug '/absolute/path/to/gustave/coc-extension'
" or
set runtimepath+=/absolute/path/to/gustave/coc-extensionIt appears in :CocList extensions marked [RTP]. Build it once, linking the
sibling gustave rather than the published one:
cd .. && npm link
cd coc-extension && npm install --ignore-scripts && npm link gustave && npm run build(The npm link dance is only needed while the gustave version this depends
on is unpublished; after that a plain npm install does it.)
If you also use VS Code on the same project, keep the gustave/ts-plugin entry
in tsconfig.json. The two never both apply: nothing loads the plugin in the
setups this extension is for.
How the lookup works
Syntactic, not type-directed. On hover it finds the identifier under the
cursor, follows imports and re-exports with ts.resolveModuleName — so your
tsconfig.json paths and baseUrl are honored — parses the single file that
declares the spec, walks the spec<...>() chain, and collects the condition
names.
No Program, no type checker, no second tsserver-sized process. If you moved to
tsgo for speed, this doesn't hand the cost back: it's one file parse per hop,
and open buffers are read from the buffer rather than from disk, so hovers are
right on unsaved edits.
One thing it does better than the type-directed plugin: factory-form conditions.
.require((from, to, cents) => ({
"amount is positive": () => cents > 0,
"accounts are distinct": () => from.id !== to.id,
}))Those names never reach the Contracted brand, so the ts-plugin can't show
them and describe() can't list them without a live call. A parse reads them
straight off the object literal.
Contracts from other packages
A published package ships dist, not sources, so there is no chain to walk.
Declaration emit writes the condition names into the Contracted brand as
literal types:
export declare const settleInvoice: Contracted<(cents: number) => number, {
pre: "amount is positive" | "amount is an integer";
post: "never settles more than it was given";
}>;So when resolution lands on a .d.ts, the names are read off the type
annotation instead — the same thing the ts-plugin reads, reached by parse
rather than by checker. A shared contracts library hovers like local code. The
one thing that doesn't survive the boundary is .named("..."), which the brand
doesn't carry, so those contracts report their conditions without a heading.
What's left as a miss is anything a parse can't follow: a spec reached through something other than a direct binding or import. That shows no contract rather than a guess, and your language server's hover is untouched.
Auditing the whole project
:CocCommand gustave.checkProjectAnswers, once, whether anything in the project provably violates a
precondition. It audits the nearest tsconfig.json above the current buffer
(pass a path to override), fills the quickfix list so you can walk the hits
with :cnext, and says nothing more than a message when it's clean — a window
that opens to announce the absence of problems is its own problem.
The work happens in a subprocess. coc's extension host is single-threaded, so building a TypeScript program inside it would freeze the editor for as long as the project takes, and the first time that happens is the last time anyone runs the command.
The summary is a ratio on purpose: "no violations provable across 316
contracted call sites" is a much weaker claim than "316 calls are correct".
Most call sites pass arguments no static analysis can pin down, and those are
skipped silently. It's the same engine as the gustave/eslint rule, and the
same gustave check you'd run in CI — which is where this question is worth
asking automatically rather than on demand.
Configuration
| Setting | Default | Effect |
| --- | --- | --- |
| gustave.hover.enable | true | Append contract conditions to hovers. |
| gustave.hover.showName | true | Head the hover with the contract's .named("..."). |
