|
| 1 | +# effect-error-pretty.nvim |
| 2 | + |
| 3 | +Effect-first TypeScript diagnostic formatter for Neovim. Turns walls of `Effect<A, E, R>` assignability errors into something you can actually read — with dedicated handling for the Effect ecosystem's pain points. |
| 4 | + |
| 5 | +TypeScript errors in general also get the treatment, but Effect is the headline act. |
| 6 | + |
| 7 | +## Why |
| 8 | + |
| 9 | +When you're learning Effect, half the type errors look like this: |
| 10 | + |
| 11 | +``` |
| 12 | +Type 'Effect<void, DbError, Database | Logger>' is not assignable to type 'Effect<void, DbError, never>'. |
| 13 | +``` |
| 14 | + |
| 15 | +VSCode and Zed render that as one wrapped line in a tooltip. The actual information — "you forgot to provide `Database | Logger`" — is buried. |
| 16 | + |
| 17 | +This plugin parses the error, figures out which channel (A, E, or R) actually diverged, and renders: |
| 18 | + |
| 19 | +``` |
| 20 | +╭─ ◈ Effect — Missing Services |
| 21 | +│ |
| 22 | +│ ◈ Forgot to provide: Database | Logger |
| 23 | +│ ⚡ Hint: .pipe(Effect.provide(SomeLayer)) |
| 24 | +│ |
| 25 | +│ Got: Effect<void, DbError, Database | Logger> |
| 26 | +│ Expected: Effect<void, DbError, never> |
| 27 | +╰─ |
| 28 | +``` |
| 29 | + |
| 30 | +## Features |
| 31 | + |
| 32 | +**Effect-specific** |
| 33 | +- `Effect<A, E, R>` / `Effect.Effect<A, E, R>` / `Stream<A, E, R>` / `Layer<ROut, E, RIn>` — all parsed, channel-labeled, and diffed |
| 34 | +- **Missing services** (forgot `Effect.provide`) — compact box naming the exact services |
| 35 | +- **Unhandled errors** (forgot `Effect.catchAll` / `catchTags` / `orDie`) — compact box listing the leftover `E` members |
| 36 | +- **Wrong success type** — compact box with Got/Expected for `A` (or `ROut` for Layer) |
| 37 | +- **Scope-required** — detects `Scope` in got.R and suggests `Effect.scoped` |
| 38 | +- **`Context.Tag<"Id", Service>` unwrapping** — shows just the service name in diffs |
| 39 | +- **`YieldWrap<...>` unwrapping** — Effect.gen yields render as plain Effect mismatches |
| 40 | +- **Short signatures** — `Effect<A>` and `Effect<A, E>` default missing params to `never` |
| 41 | +- **Multi-channel view** — falls back to a full tri-channel table when 2+ channels diverge |
| 42 | +- **Type signature at the bottom** of every compact box so you see the full `Effect<...>` context |
| 43 | + |
| 44 | +**General TypeScript** |
| 45 | +- Type mismatches (TS2322 / TS2345) with multi-line wrapping for big types |
| 46 | +- Missing property (TS2741), Unknown property (TS2339) |
| 47 | +- Cannot find name / module / exported member (TS2304 / TS2307 / TS2305) |
| 48 | +- Implicit any (TS7006), Uninitialized variable (TS2454), Nullish (TS2531 / TS18048) |
| 49 | +- Argument count (TS2554), Const reassignment, Not callable (TS2349) |
| 50 | +- Deprecated symbol messages |
| 51 | +- Optional fallback to [`format-ts-errors`](https://www.npmjs.com/package/@0no-co/format-ts-errors) when no pattern matches |
| 52 | + |
| 53 | +## Install |
| 54 | + |
| 55 | +Requires Neovim 0.10+. |
| 56 | + |
| 57 | +### lazy.nvim |
| 58 | + |
| 59 | +```lua |
| 60 | +{ |
| 61 | + "rashedInt32/effect-error-pretty.nvim", |
| 62 | + event = { "BufReadPre", "BufNewFile" }, |
| 63 | + opts = { |
| 64 | + float = true, -- auto-patch vim.diagnostic.config.float.format |
| 65 | + effect = true, |
| 66 | + format_ts_errors_fallback = true, |
| 67 | + }, |
| 68 | +} |
| 69 | +``` |
| 70 | + |
| 71 | +### Manual wiring |
| 72 | + |
| 73 | +If you manage `vim.diagnostic.config` yourself, don't set `float = true`. Call the formatter directly: |
| 74 | + |
| 75 | +```lua |
| 76 | +local pretty = require("effect-error-pretty") |
| 77 | +pretty.setup({ effect = true }) |
| 78 | + |
| 79 | +vim.diagnostic.config({ |
| 80 | + float = { |
| 81 | + format = function(diagnostic) |
| 82 | + return pretty.float_format(diagnostic) or diagnostic.message |
| 83 | + end, |
| 84 | + }, |
| 85 | +}) |
| 86 | +``` |
| 87 | + |
| 88 | +### tiny-inline-diagnostic.nvim |
| 89 | + |
| 90 | +```lua |
| 91 | +{ |
| 92 | + "rachartier/tiny-inline-diagnostic.nvim", |
| 93 | + opts = { |
| 94 | + options = { |
| 95 | + format = function(diagnostic) |
| 96 | + return require("effect-error-pretty").inline_format(diagnostic) or diagnostic.message |
| 97 | + end, |
| 98 | + }, |
| 99 | + }, |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +## Options |
| 104 | + |
| 105 | +| Option | Default | Description | |
| 106 | +|------------------------------|---------|-----------------------------------------------------------------------------------------------------------| |
| 107 | +| `effect` | `true` | Recognize `Effect` / `Stream` / `Layer` mismatches as first-class. | |
| 108 | +| `float` | `false` | Automatically patch `vim.diagnostic.config.float.format` on `setup()`. | |
| 109 | +| `sources` | `{ typescript=true, ts=true, vtsls=true }` | Diagnostic sources this plugin handles. Exact match only. | |
| 110 | +| `format_ts_errors_fallback` | `true` | When no pattern matches, try `format-ts-errors` if installed. | |
| 111 | +| `extra_patterns` | `nil` | List of `function(msg) -> {kind, ...}` parsers run after builtins. Let you add custom shapes. | |
| 112 | + |
| 113 | +## Public API |
| 114 | + |
| 115 | +```lua |
| 116 | +local pretty = require("effect-error-pretty") |
| 117 | + |
| 118 | +-- Float box (multi-line) — returns nil if unhandled; caller should fall back. |
| 119 | +pretty.float_format(diagnostic) |
| 120 | + |
| 121 | +-- Inline one-line — returns nil if unhandled. |
| 122 | +pretty.inline_format(diagnostic) |
| 123 | + |
| 124 | +-- Low-level: parse a raw TS diagnostic message into a structured kind. |
| 125 | +-- Returns nil if no pattern matched. |
| 126 | +require("effect-error-pretty.parse").parse(message, { effect = true }) |
| 127 | +``` |
| 128 | + |
| 129 | +## Extending |
| 130 | + |
| 131 | +Add a parser for a custom rule you care about: |
| 132 | + |
| 133 | +```lua |
| 134 | +require("effect-error-pretty").setup({ |
| 135 | + extra_patterns = { |
| 136 | + function(msg) |
| 137 | + local rule = msg:match("^eslint%-plugin%-effect: (.+)$") |
| 138 | + if rule then |
| 139 | + return { kind = "custom_lint", rule = rule } |
| 140 | + end |
| 141 | + end, |
| 142 | + }, |
| 143 | +}) |
| 144 | +``` |
| 145 | + |
| 146 | +Render logic for custom `kind`s isn't wired in yet — for now, `parse()` returns the structured result and you render it yourself. If there's demand, we'll expose a renderer registry. |
| 147 | + |
| 148 | +## Tests |
| 149 | + |
| 150 | +```sh |
| 151 | +nvim --headless -c "PlenaryBustedDirectory tests/" -c "qa!" |
| 152 | +``` |
| 153 | + |
| 154 | +Requires [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) on the runtimepath. |
| 155 | + |
| 156 | +## License |
| 157 | + |
| 158 | +MIT |
0 commit comments