I've been building a VS Code extension for spec management over the past 3 months (full disclosure: I'm the creator, it's called SPECLAN — free side project). The idea is that specifications need the same structure we give source code: hierarchy, types, lifecycle tracking. So the extension organizes specs as a tree of Markdown files with YAML frontmatter — goals break down into features, features into sub-features, sub-features into requirements. Each file has a status lifecycle (draft → review → approved → in-development → released) so you always know what's specced, what's being built, and what needs to change.
The interesting VS Code challenge: making this usable for non-technical people. Product managers and business analysts define what to build, but they won't write raw Markdown with YAML frontmatter. So I needed a WYSIWYG editor inside a webview that round-trips cleanly to Markdown — same file in Git, two editing experiences.
That editor ate about 40% of total development effort. Here's what I learned.
The stack:
- Quill 2.x in a VS Code webview (rich text editing)
- remark + remark-gfm for Markdown → HTML on load
- turndown + turndown-plugin-gfm for HTML → Markdown on save
- gray-matter for YAML frontmatter — strips on load, reattaches on save
- quill-table-up for GFM tables (Quill has no native table support)
What actually hurt:
Round-trip fidelity. The pipeline is Markdown → HTML → Quill Delta → HTML → Markdown. Every step is lossy. Links, emphasis, nested lists — they all drift across conversions. I spent weeks writing custom turndown rules to keep Markdown output stable. If you're building something similar: start with the save pipeline, not the editor. The round-trip is the constraint that shapes everything.
Frontmatter is invisible but critical. Each spec file has 10+ YAML fields — status, entity ID, parent references, timestamps. The editor only sees the Markdown body, but the file is meaningless without its frontmatter. gray-matter handles parsing, but you need to be careful that editor changes don't conflict with frontmatter values (e.g., someone editing a title in the body that's also in the YAML).
Tables. Quill doesn't do tables. quill-table-up adds them, but serializing table HTML through turndown into GFM pipe tables has edge cases everywhere — empty cells, inline formatting in cells, nested content.
Webview communication. Everything between the editor (iframe) and the extension host is a postMessage call — load, save, dirty state, undo, external file change detection. I ended up building a structured message protocol with typed handlers on both sides.
console.login the webview doesn't show up anywhere useful, so I added a logging bridge that routes webview logs to the extension's output channel.Custom editor API. Using
CustomTextEditorProvidermeans the document model is VS Code'sTextDocumentbut the visual state is Quill's Delta. Keeping these in sync — especially during concurrent edits or Git operations that change the file underneath — required careful event sequencing.
What worked well:
-
The file-system-as-data-model approach. Directories ARE the spec hierarchy.
speclan/features/F-1234-auth/requirements/R-5678-login/R-5678-login.md— any tool (or AI agent) can understand the structure by reading the file system. No database, no server. - Snapshot testing the conversion pipeline. Take a Markdown file, push it through the full round-trip, diff the output. Catches regressions fast.
- Tree views for navigation. VS Code's TreeDataProvider is excellent. The spec tree (goals → features → requirements) renders as a native sidebar with status icons, drag-and-drop reordering, and context menus. Much less effort than the WYSIWYG editor.
Happy to answer questions about webviews, the conversion pipeline, or the spec structure approach.
Top comments (0)