Norq

Editor Setup

Norq includes a Language Server Protocol (LSP) server that provides editor intelligence for .md and .json template files inside notification directories.

Features

Feature Description
Diagnostics Real-time lint errors and warnings as you type
Completions Variable names from data.schema.yaml, directive names, pipe names
Hover Type info and descriptions from the schema on hover
Null safety Warnings when nullable fields are used without :::if guards
Code actions Quick fixes (e.g., wrap nullable access in :::if guard)
Deliverability Real-time email deliverability and WCAG contrast checking
Document formatting Format Document applies canonical indentation (same rules as norq fmt)
On-type formatting Auto-indent on Enter inside directives (2-space indent, dedent on ::: closer)

VS Code

Install the Norq extension from the VS Code marketplace. It bundles the LSP server and provides:

  • Syntax highlighting for .md template files
  • LSP features (diagnostics, completions, hover, code actions)
  • Inline preview panel
  • Channel switcher in the status bar

The extension automatically detects projects with norq.config.yaml and activates.

Neovim

vim.api.nvim_create_autocmd('FileType', {
  pattern = 'markdown',
  callback = function()
    vim.lsp.start({
      name = 'norq',
      cmd = { 'norq', 'lsp' },
      root_dir = vim.fs.root(0, 'norq.config.yaml'),
    })
  end,
})

With nvim-lspconfig

local lspconfig = require("lspconfig")
 
lspconfig.norq.setup({
  cmd = { "norq", "lsp" },
  filetypes = { "markdown" },
  root_dir = lspconfig.util.root_pattern("norq.config.yaml"),
})

With coc.nvim

Add to coc-settings.json:

{
  "languageserver": {
    "norq": {
      "command": "norq",
      "args": ["lsp"],
      "filetypes": ["markdown"],
      "rootPatterns": ["norq.config.yaml"]
    }
  }
}

Vim

Vim does not have a built-in LSP client. Use the vim-lsp plugin.

Add to your .vimrc:

if executable('norq')
  au User lsp_setup call lsp#register_server({
    \ 'name': 'norq',
    \ 'cmd': {server_info->['norq', 'lsp']},
    \ 'allowlist': ['markdown'],
    \ 'root_uri': {server_info->lsp#utils#path_to_uri(
    \   lsp#utils#find_nearest_parent_file_directory(
    \     lsp#utils#get_buffer_path(), 'norq.config.yaml'))},
    \ })
endif

Helix

Add to languages.toml:

[[language]]
name = "markdown"
language-servers = ["norq-lsp"]
 
[language-server.norq-lsp]
command = "norq"
args = ["lsp"]

Zed

Install the Norq extension from the Zed extension registry, or load the zed/ directory as a dev extension:

  1. Open Zed
  2. Go to Extensions > Install Dev Extension
  3. Select the zed/ directory in the norq repo

The extension automatically finds norq on your PATH and launches the LSP.

Sublime Text

Install the LSP package, then add to LSP settings:

{
  "clients": {
    "norq": {
      "command": ["norq", "lsp"],
      "selector": "text.html.markdown",
      "initializationOptions": {},
      "settings": {}
    }
  }
}

Emacs (lsp-mode)

(with-eval-after-load 'lsp-mode
  (add-to-list 'lsp-language-id-configuration '(markdown-mode . "markdown"))
  (lsp-register-client
   (make-lsp-client
    :new-connection (lsp-stdio-connection '("norq" "lsp"))
    :activation-fn (lsp-activate-on "markdown")
    :server-id 'norq-lsp)))

How the LSP works

The LSP server starts via norq lsp on stdin/stdout. It:

  1. Discovers the project root by finding norq.config.yaml
  2. Loads all data.schema.yaml files for completion and type info
  3. Watches for file changes to update diagnostics in real time
  4. Provides scoped completions based on the current context (directive names after :::, variable paths inside {{, pipe names after |)

The LSP only activates for files inside a notifications/ directory that is part of a Norq project.

Completions

The LSP provides context-aware completions:

  • After ::: – directive names (header, footer, action, highlight, centered, table, etc.)
  • After :::directive (space, no brace) – {attr=} attribute snippets for the directive under the cursor
  • Inside {{ – variable paths from data.schema.yaml with dot-path traversal
  • After :::if and :::each – variable path completions from data.schema.yaml
  • After | inside {{ – pipe names with argument snippets (e.g., truncate $1, currency "$1")
  • Inside frontmatter – known frontmatter keys for the current channel
  • After { on a directive opening line – context-aware attribute completions (align=, bg=, color=, padding=, size=, weight=, spacing=, plus directive-specific attrs like url= for :::hero, width=/valign= for :::col)
  • After {{> – partial template names from all _shared/ directories in the hierarchy (local, category, type, global), with scope labels
  • After {{> partial-name – parameter names (key=) extracted from the partial’s {{@key}} references
  • After {{@ inside a _shared/ file – parameter names used in the current partial
  • Inside {...} aliases in brand.yaml – brand section names (colors, spacing, radii, typography, fonts); after the dot, the actual token names from the in-memory document. Tolerates unterminated strings during typing (regex fallback when YAML is too broken to parse)

Completion trigger characters

The LSP registers the following characters as completion triggers: :, {, >, |, ., (space), @.

The { character triggers context-aware attribute completions on directive opening lines. After typing ::: hero {, the LSP suggests attributes valid for that directive (url=, align=, bg=, color=, padding=, size=, weight=, spacing=). Directive-specific attrs like width= and valign= appear only for :::col. The > and @ characters trigger partial name and parameter completions inside {{ }}.

Directive completions

The following directives appear in completions after ::::

Directive Description
header Branded page header
footer Footer with small text
action Call-to-action button(s)
callout Highlighted callout box
hero Full-width image or banner
fields Key-value data table
media Image, video, or file link
columns Multi-column layout
list Ordered or unordered list
highlight Dark card with brand color background
centered Center-aligned text block
table Iterable data table (rows from array variable)
section Single-column section (shorthand for :::columns + :::col)
col Column inside :::columns
if / else / each Control flow
raw Raw passthrough block

Directive attribute completions

After typing { on a directive opening line, the LSP offers context-aware attribute completions. The offered attributes depend on which directive is being edited:

Universal attributes (all directives):

  • align=left, center, right
  • bg=brand, card, highlight, #hex
  • color=brand, muted, danger, success, warning, #hex
  • padding=none, compact, normal, spacious
  • size=xs, sm, md, lg, xl, 2xl, 3xl, 4xl
  • weight=light, normal, medium, semibold, bold
  • spacing=tight, normal, relaxed, loose

Directive-specific attributes (only shown for the relevant directive):

  • url=:::hero only
  • width=, valign=:::col only
  • variant=:::action only
  • flush, mobile=:::columns, :::section only

Example: typing ::: col { triggers suggestions for width=, valign=, bg=, and all universal attributes.

Diagnostics

Diagnostics appear in real time as you edit:

  • Error: Parse failures, missing required frontmatter
  • Warning: Nullable access without :::if guard or | default pipe, raw expressions
  • Info: Native JSON mode suggestion, unused variables

Code Actions

The LSP offers quick fixes triggered by diagnostics.

Wrap in :::if guard

When the linter warns about nullable access (template/nullable-access), the LSP offers a one-click fix to wrap the expression in an :::if guard. You can also silence the warning by using a | default pipe instead.

Before:

Hello {{user.last_name}}

Diagnostic: Variable 'user.last_name' is nullable -- wrap in :::if guard or use | default "..."

Fix with code action (wraps in :::if guard):

:::if user.last_name
Hello {{user.last_name}}
:::

Fix with | default pipe:

Hello {{user.last_name | default "friend"}}

In VS Code, click the lightbulb or press Ctrl+. / Cmd+. on the warning to apply.

Formatting

The LSP provides two formatting capabilities:

Document formatting

Trigger Format Document (e.g., Shift+Alt+F in VS Code) to format the entire template with canonical indentation and spacing. Uses the same rules as norq fmt:

  • 2-space indent inside directives, +2 for each nesting level
  • One blank line between block-level elements
  • No trailing blank lines before ::: closers
  • Frontmatter and code blocks preserved as-is

On-type formatting

When you press Enter inside a template, the LSP auto-adjusts indentation:

  • After a :::directive opener: indent +2 spaces
  • After a ::: closer: dedent to match the opener’s level
  • After :::else: indent +2 spaces (same as opener)
  • Regular lines: maintain previous line’s indent level

This keeps directive bodies consistently indented as you type.

Config file support

The LSP provides intelligence for norq.config.yaml in addition to template files:

  • Diagnostics – unknown keys, missing required fields, invalid channel names in routing
  • Completions – top-level keys (notifications, brand, providers, routing, email, codegen, strict), brand-token sections inside brand.yaml (tokens.colors, tokens.typography, tokens.spacing, tokens.radii, styles, defaults, modes, fonts), channel names in routing values
  • Hover – descriptions and default values for each config key

The config schema is defined in crates/core/src/schema/config.rs. Adding a new config key means adding one ConfigField entry.