Skip to content

Commit db3a705

Browse files
author
merge-queue-bot
committed
Merge PR #414: Add plans 214 and 215 for Obsidian editor integration
2 parents 91021d5 + e41a9bc commit db3a705

3 files changed

Lines changed: 578 additions & 0 deletions

File tree

PLAN.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ footer: |
140140
| 212 | 🔲 | opus | [`mdsmith extract` projects paragraph inline spans as data](plan/212_extract-inline-spans.md) |
141141
| 213 | 🔲 | opus | [Built-in `no-llm-tells` convention with append-mode forbidden lists](plan/213_anti-slop-convention.md) |
142142
| 214 | 🔳 | sonnet | [MDS019 catalog: CUE-expression row templates](plan/214_catalog-cue-row-expressions.md) |
143+
| 214 | 🔲 | opus | [Obsidian plugin via hand-rolled LSP bridge](plan/214_obsidian-plugin.md) |
143144
| 215 | 🔲 | opus | [Audit AST-walking rules and rewrite the ones that only need f.Lines](plan/215_lines-only-rule-audit.md) |
145+
| 215 | 🔲 | opus | [WASM mdsmith for mobile Obsidian](plan/215_obsidian-wasm-mobile.md) |
144146
| 216 | 🔲 | opus | [Per-document parse cache for the LSP, keyed by version](plan/216_lsp-parse-cache.md) |
145147
<?/catalog?>

plan/214_obsidian-plugin.md

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
---
2+
id: 214
3+
title: Obsidian plugin via hand-rolled LSP bridge
4+
status: "🔲"
5+
model: opus
6+
summary: >-
7+
Ship a desktop-only Obsidian plugin under
8+
`editors/obsidian/` that spawns `mdsmith lsp`, drives a
9+
hand-rolled JSON-RPC client, and renders diagnostics
10+
plus quick-fixes inside Obsidian's CodeMirror 6 editor.
11+
depends-on: [121, 168]
12+
---
13+
# Obsidian plugin via hand-rolled LSP bridge
14+
15+
## Goal
16+
17+
Surface mdsmith inside Obsidian. A writer opening a
18+
`.md` file sees inline squiggles. A "Fix" lightbulb
19+
applies the matching quick-fix. Fix-on-save runs
20+
`fixAll` on the active buffer. The plugin reuses
21+
the LSP server from
22+
[plan 121](121_vscode-integration.md) unchanged.
23+
24+
## Background
25+
26+
Plan 121 shipped `mdsmith lsp` and a VS Code client.
27+
Plan 168 added the `obsidian` convention. Together
28+
they cover the rules. The gap is the editor wiring.
29+
30+
Obsidian runs on Electron. Plugins are bundled to
31+
one `main.js` plus `manifest.json`, loaded from
32+
`<vault>/.obsidian/plugins/<id>/`. Desktop plugins
33+
have Node access. So spawning `mdsmith lsp` via
34+
`child_process` works. No `vscode-languageclient`
35+
analog exists in the Obsidian ecosystem. The plugin
36+
hand-rolls JSON-RPC framing and dispatch.
37+
38+
[Mobile Obsidian][mob] runs no Node. It cannot
39+
spawn binaries. The manifest sets
40+
`isDesktopOnly: true`. Mobile is the scope of
41+
[plan 215](215_obsidian-wasm-mobile.md).
42+
43+
Ship via GitHub Release only. The zip holds
44+
`main.js`, `manifest.json`, and `styles.css`. No
45+
PR to [obsidian-releases][cp].
46+
47+
[mob]: https://help.obsidian.md/mobile
48+
[cp]: https://github.com/obsidianmd/obsidian-releases
49+
50+
## Design
51+
52+
### Layout
53+
54+
`editors/obsidian/` mirrors `editors/vscode/`:
55+
56+
```text
57+
editors/obsidian/
58+
manifest.json
59+
package.json
60+
tsconfig.json
61+
build.ts
62+
src/
63+
main.ts
64+
lsp-client.ts
65+
diagnostics.ts
66+
actions.ts
67+
settings.ts
68+
binary.ts
69+
*.test.ts
70+
styles.css
71+
README.md
72+
```
73+
74+
The build emits one CommonJS bundle to
75+
`dist/main.js`. Obsidian requires CommonJS. Static
76+
files are copied as-is. The release zip holds
77+
those three files plus the staged binaries.
78+
79+
### Binary resolution
80+
81+
Reuse the VS Code bundle path. The shared
82+
`@mdsmith/cli` shim maps host to target. `build.ts`
83+
copies `npm/mdsmith/bin/mdsmith.js` and stages each
84+
platform binary under `dist/cli/@mdsmith/<target>/`.
85+
`binary.ts` loads the shim and calls
86+
`resolveBinary(process.platform, process.arch, …)`.
87+
88+
The resolver falls back to a `mdsmith.path` setting,
89+
then `$PATH`. The fallback notice points at the
90+
[releases page][rel].
91+
92+
[rel]: https://github.com/jeduden/mdsmith/releases
93+
94+
### JSON-RPC client
95+
96+
`lsp-client.ts` owns the JSON-RPC surface. It
97+
spawns the binary, frames messages with
98+
`Content-Length` headers, and dispatches replies
99+
by id. The framing is around 80 lines. No
100+
third-party package is used.
101+
102+
The client exposes:
103+
104+
- `spawn(binary, args, cwd)` and `kill()`.
105+
- `request(method, params): Promise<unknown>`.
106+
- `notify(method, params): void`.
107+
- `onNotification(method, handler)`.
108+
- Lifecycle: `initialize``initialized`
109+
requests → `shutdown``exit`.
110+
111+
Methods the plugin sends or receives:
112+
113+
| Direction | Method | Why |
114+
| --------- | --------------------------------- | ---------------------------------------- |
115+
| → server | `initialize` | Handshake |
116+
| → server | `textDocument/didOpen` | Buffer opened |
117+
| → server | `textDocument/didChange` | Debounced edit |
118+
| → server | `textDocument/didSave` | Triggers fix |
119+
| → server | `textDocument/didClose` | Buffer closed |
120+
| → server | `textDocument/codeAction` | Quick fixes plus `source.fixAll.mdsmith` |
121+
| ← server | `textDocument/publishDiagnostics` | Squiggles |
122+
| ← server | `window/showMessage` | `new Notice(...)` |
123+
124+
Hover, completion, rename, and navigation are
125+
deferred. Obsidian exposes those through different
126+
surfaces. They warrant their own plan.
127+
128+
### Diagnostics in CodeMirror 6
129+
130+
Obsidian's source and live-preview editors both
131+
use [CodeMirror 6][cm6]. The plugin adds a CM6
132+
`StateField<DecorationSet>`. It holds active
133+
diagnostics per file. Decorations apply a
134+
severity-themed underline. Classes live in
135+
`styles.css`.
136+
137+
Hover uses `hoverTooltip` from `@codemirror/view`.
138+
The tooltip shows `code + message`. The footer
139+
holds a "Fix" link. The link runs the same
140+
code-action flow as the lightbulb.
141+
142+
[cm6]: https://codemirror.net/
143+
144+
A "mdsmith Diagnostics" [`ItemView`][iv] lists
145+
workspace-wide diagnostics as a sortable table.
146+
Click jumps to the source location.
147+
148+
[iv]: https://docs.obsidian.md/Plugins/User+interface/Views
149+
150+
### Code actions and fix-on-save
151+
152+
Obsidian has no lightbulb. Three surfaces stand in:
153+
154+
1. The hover tooltip "Fix" link.
155+
2. Per-line palette commands. Each active
156+
diagnostic on the cursor line registers a
157+
transient `mdsmith: Fix — {code}` command. The
158+
set clears on cursor move.
159+
3. The `mdsmith: Fix file` command. It sends
160+
`textDocument/codeAction` filtered to kind
161+
`source.fixAll.mdsmith` and applies the
162+
returned `WorkspaceEdit` — mirroring VS Code.
163+
164+
`fixOnSave` (off by default) debounces
165+
`vault.on('modify')` and triggers `Fix file` 200 ms
166+
after the last save. The command path is shared
167+
with the palette entry.
168+
169+
### Settings
170+
171+
A `PluginSettingTab` renders five controls:
172+
173+
| Setting | Default | Purpose |
174+
| ------------- | ---------- | -------------------------- |
175+
| `binaryPath` | `""` | Override resolver |
176+
| `configPath` | `""` | Pass `-c` to the server |
177+
| `runMode` | `"onSave"` | `onType`/`onSave`/`off` |
178+
| `fixOnSave` | `false` | Run `fixAll` after save |
179+
| `traceServer` | `"off"` | `off`/`messages`/`verbose` |
180+
181+
Settings round-trip via `loadData` and `saveData`.
182+
Changing `binaryPath` or `configPath` restarts the
183+
server. Changing `runMode` or `fixOnSave`
184+
reconfigures listeners without a restart.
185+
186+
### Lifecycle
187+
188+
`onload` reads settings, resolves the binary,
189+
spawns the server, registers the CM6 extension,
190+
registers commands and the diagnostics view, and
191+
attaches vault listeners. `onunload` sends
192+
`shutdown` and `exit`, kills the child after 1 s,
193+
disposes views, and removes listeners. A
194+
`mdsmith: Restart server` command exists for the
195+
same reason VS Code has one.
196+
197+
### Build, test, release
198+
199+
`bun run build.ts --production` writes `dist/`,
200+
stages the platform binaries, and zips the
201+
artifact. CI runs `bun test`, then the build,
202+
then attaches the zip to the GitHub Release.
203+
The release pipeline picks it up the same way it
204+
picks up the `.vsix`.
205+
206+
### Docs
207+
208+
A new `docs/guides/editors/obsidian.md` covers
209+
install, settings, and troubleshooting. Update the
210+
[linter comparison][lc] to cite the new plugin in
211+
its Obsidian row. Add a note to the
212+
[conventions reference][conv] that
213+
`convention: obsidian` pairs with this plugin.
214+
Mention the artifact in [github-releases.md][gh].
215+
216+
[lc]: ../docs/background/markdown-linters.md
217+
[conv]: ../docs/reference/conventions.md
218+
[gh]: ../docs/development/release-channels/github-releases.md
219+
220+
## Tasks
221+
222+
1. Scaffold `editors/obsidian/`: `package.json`,
223+
`tsconfig.json`, `manifest.json` with
224+
`isDesktopOnly: true`, `build.ts`, a stub
225+
`src/main.ts` extending `Plugin`, and a
226+
`README.md` that passes default rules.
227+
2. Implement `lsp-client.ts`. Cover framing,
228+
request correlation, notification fan-out, and
229+
the `initialize`/`shutdown` cycle. Unit-test
230+
against an in-process `Duplex`.
231+
3. Implement `binary.ts`. Load the `@mdsmith/cli`
232+
shim. Fall back to setting, then `$PATH`. Match
233+
the test surface of the VS Code module.
234+
4. Implement `diagnostics.ts`. Add the CM6
235+
`StateField`, the effect type, and a
236+
`hoverTooltip` provider rendering code, message,
237+
and a Fix link.
238+
5. Implement `actions.ts`. Add per-line palette
239+
commands from active diagnostics, the
240+
`Fix file` command via `executeCommand`, and the
241+
debounced `vault.on('modify')` handler.
242+
6. Implement `settings.ts`. Wire the five controls,
243+
the `loadData`/`saveData` round-trip, and the
244+
restart-on-change for `binaryPath` and
245+
`configPath`.
246+
7. Wire `main.ts`. `onload` spawns the server,
247+
registers the CM6 extension, the diagnostics
248+
view, the commands, and the settings tab.
249+
`onunload` cleans up.
250+
8. Add `styles.css` for severity underlines and
251+
tooltip styling.
252+
9. Add a `.github/workflows/` step that builds the
253+
plugin and uploads the zip as a release
254+
artifact, mirroring the existing `vscode` job.
255+
10. Write `docs/guides/editors/obsidian.md`.
256+
Update the conventions reference, the
257+
linter-comparison page, and the GitHub
258+
Releases page.
259+
11. Run `mdsmith fix .` and confirm `mdsmith check
260+
.` passes against the updated `PLAN.md`.
261+
262+
## Acceptance Criteria
263+
264+
- [ ] `editors/obsidian/` builds with `bun run
265+
build.ts --production`. The output is
266+
`dist/main.js`, `manifest.json`, and
267+
`styles.css`.
268+
- [ ] `bun test` passes. The suite covers framing,
269+
binary resolution, diagnostics decoration,
270+
and settings round-trip.
271+
- [ ] Loading the plugin in a vault that holds an
272+
`MDS001` violation shows a wavy underline
273+
within 500 ms of opening the file. Manual
274+
smoke step.
275+
- [ ] The hover tooltip shows the rule code and
276+
message. The "Fix" link applies the
277+
quick-fix.
278+
- [ ] `mdsmith: Fix file` produces the same buffer
279+
as `mdsmith fix` on the same input.
280+
- [ ] Toggling `fixOnSave: true` runs `Fix file`
281+
after each save without a plugin restart.
282+
- [ ] Editing `.mdsmith.yml` re-lints open files
283+
without a restart. The
284+
`didChangeWatchedFiles` event is forwarded.
285+
- [ ] `manifest.json` has `isDesktopOnly: true`.
286+
Mobile Obsidian treats the plugin as absent,
287+
not as a crash.
288+
- [ ] CI attaches `mdsmith-obsidian-<version>.zip`
289+
to the release artifacts.
290+
- [ ] `docs/guides/editors/obsidian.md` exists.
291+
The linter-comparison page cites the new
292+
plugin in its Obsidian row.
293+
- [ ] `mdsmith check .` passes against the
294+
updated `PLAN.md`.
295+
296+
## Non-Goals
297+
298+
- LSP hover, completion, rename, and symbol
299+
navigation. Each goes in its own follow-up.
300+
- Mobile support. That is plan 215.
301+
- Submission to the Obsidian Community Plugins
302+
catalog. The chosen channel is GitHub Releases.
303+
- Live-preview rendering changes.
304+
- New rule bindings beyond what the `obsidian`
305+
convention activates.
306+
307+
## See also
308+
309+
- [Plan 121: VS Code via LSP](121_vscode-integration.md)
310+
- [Plan 215: WASM for mobile](215_obsidian-wasm-mobile.md)
311+
- [Linter comparison][lc]

0 commit comments

Comments
 (0)