Mark Highlight — Xcode-style
|
| Setting | Default | Description |
|---|---|---|
markComments.enabled |
true |
Master switch for both features. |
markComments.keyword |
"MARK: -" |
Keyword that starts a mark. A trailing - is optional when matching, so // MARK: - Title, // MARK:- Title, and // MARK: Title all match the default. |
markComments.languages |
["*"] |
Language IDs to process. "*" covers every language that declares comment syntax, each parsed with its own comment tokens (# for Python, -- for Lua/SQL, <!-- --> for HTML, ...). |
markComments.borderColor |
"rgba(128,128,128,0.45)" |
Rule color (any CSS color); also used for the overview-ruler tick. |
markComments.borderStyle |
"solid" |
solid, dashed, dotted, or double. |
markComments.borderWidth |
"1px 0 0 0" |
CSS border-width (top right bottom left) — the default draws a top rule only. Top-only rules thicker than 1px are drawn as a band in the blank space above the mark line, so they never paint over the mark text (decorations are overlays and cannot push text down). |
markComments.overviewRuler |
true |
Show a tick per mark in the right-hand overview ruler. |
All settings apply live — no reload needed.
How it works
Both features key off a single pure function, findMarks(text, keyword) in
src/core/findMarks.ts, so they can never disagree about what counts as a
mark. That module imports nothing from vscode, which is what lets the Vitest
unit tests run without an extension host. A thin adapter maps its results to
vscode.Ranges.
- Outline (TS/TSX/JS/JSX): a bundled tsserver plugin (
src/tsPlugin/index.ts, shipped indist/ts-plugin/and installed into the extension'snode_modules/on activation — the only location tsserver loads plugins from) proxiesgetNavigationTreeand injects one node per mark into TypeScript's own symbol tree. Because the marks live inside the TypeScript provider's results, the Outline stays a single interleaved list — a separateDocumentSymbolProviderwould render as its own tree next to "TypeScript" (microsoft/vscode#60641). Marks nest into the node that contains them (a mark inside a class body appears under the class), like Xcode. - Outline (other configured languages): a regular
DocumentSymbolProvideremits flatSymbolKind.Keysymbols. For languages with no other symbol provider (plain SQL, shell scripts, ...) the marks simply are the outline. For languages that do have one (CSS/HTML built-ins, Pylance for Python, ...), VSCode renders one Outline tree per provider and cannot merge them (#60641) — marks there appear as their own "MARK" group next to the language's tree. A true merge is only possible where a language server exposes a plugin API, which today is TypeScript only.Cmd+Shift+Oalways navigates marks and symbols merged, regardless of language. - Per-language comment syntax (
src/commentSyntax.ts): each language's line/block comment tokens are discovered from thelanguage-configuration.jsonits contributing extension ships — the same data VSCode uses for toggle-comment — so any installed language works without configuration. Marks match a line comment with only whitespace before it, or a block comment that opens and closes on its own line. Languages that declare no comments (plaintext, markdown, ...) are skipped. - Rule: a single
TextEditorDecorationTypewithisWholeLine: trueand a configurable top border, applied to the mark lines of every visible editor. Decoration updates are debounced ~150 ms while typing and applied immediately on editor switches and settings changes.
Why the plugin config travels through a file
VSCode runs two tsserver processes and routes navtree (document symbols) to
the syntax server, while the configurePlugin API request only ever
reaches the semantic server. Settings therefore reach the plugin through a
config.json written next to the installed plugin, which it re-reads
(mtime-cached) on every request; configurePlugin is still sent as a
secondary channel. Two more cache layers are handled on settings changes: the
TS extension's per-document navtree response cache is evicted via a throwaway
symbol request, and VSCode's outline-model cache is invalidated by
re-registering our symbol provider (which is registered for TS languages too —
returning no symbols there — precisely so it participates in that cache key).
Scope notes (deliberate choices)
- Whole-line comments only. A mark matches a line comment with nothing but whitespace before it (
// MARK: x,# MARK: x,-- MARK: x), or a block comment occupying the whole line (/* MARK: x */,<!-- MARK: x -->). Trailing comments (foo(); // MARK: x) and multi-line block comments are not matched — this also keeps"// MARK:"inside string literals from producing false positives. - Keyword matching is case-sensitive, like Xcode.
- Empty titles (
// MARK: -) still draw a rule; in the Outline they show asMARK. - Mark icon: in TS-family files marks show the Variable icon — tsserver plugins can only emit
ScriptElementKindstrings, and the TS extension maps everything outside its known set (module/class/enum/interface/method/property/variable/function/constructor) toSymbolKind.Variable. Non-TS languages use the key icon (SymbolKind.Key). - Workspace-specific settings across multiple windows: the tsserver plugin reads one shared config file per extension install, so two windows whose workspaces configure different keywords will share the most recently written one. User-level settings behave as expected.
- No minimap text labels from this extension. The public VSCode API has no way for an extension to draw text into the minimap; the overview-ruler tick is the supported substitute. That said, VSCode itself (1.85+) natively renders
// MARK:comments as minimap section headers wheneditor.minimap.showMarkSectionHeadersis on (the default) — so with the default keyword you get minimap labels anyway, courtesy of the editor. - Outline interleaving assumes the Outline's default Sort By: Position; sorting by name or category groups the marks together instead.
Development
npm install
npm test # Vitest unit tests for the parser
npm run test:coverage # + v8 coverage (coverage/index.html)
npm run typecheck # tsc --noEmit
npm run build # esbuild → dist/extension.js
npm run package # vsce package → .vsix (not published)
Regenerating the README screenshots
docs/*.png are generated, not hand-made. scripts/screenshots.mjs launches
the @vscode/test-electron-managed VSCode with the extension in development
mode; a scene driver (scripts/screenshotScenes.cjs, injected via
--extensionTestsPath) opens the example files, arranges the layout, and
tweaks settings per scene; the window is then captured and trimmed with
ImageMagick.
xvfb-run -a npm run screenshots # Linux/CI (needs xvfb + imagemagick)
npm run screenshots # macOS (visible window, uses screencapture)
Press F5 to launch the Extension Development Host with the examples/
folder open — one demo.* file per language (TypeScript, JavaScript, Python,
Go, Rust, Java, C++, C#, Swift, Ruby, Shell, SQL, HTML, CSS, YAML), each using
its own comment syntax. They double as manual test fixtures and as material
for marketplace screenshots.
The tsserver plugin builds to dist/ts-plugin/ (what the vsix ships) and is
also copied to node_modules/vscode-mark-highlight-ts-plugin/ — the only
location tsserver loads plugins from. The build does this copy for the F5 dev
host; in an installed extension the activation code does it (and restarts the
TS server once when it had to). After changing src/tsPlugin/, rebuild and
run TypeScript: Restart TS Server in the dev host — the plugin is loaded
once at server start.



