Skip to main content
πŸ“’ New post: Editor Superpowers for Phobos β€” Introducing the Language Server and VS Code Extension β€” Read more β†’

Editor Superpowers for Phobos β€” Introducing the Language Server and VS Code Extension

Β· 9 min read
Gregory McDaniel

Writing a Phobos pipeline template used to be a "save, run, read the error, fix, repeat" loop. Today we're shipping the Phobos Language Server and VS Code extension β€” a pair of releases that close that loop in your editor. Type-mismatch on a variable, an unresolved reference to a sibling task, a deprecated namespace, a missing plugin block β€” every one of these surfaces as you type, with a one-click fix wherever the LSP can synthesise one.

This post is a tour of what the language server and extension actually do, what's new in this release (notably first-class support for .pbvars variables files), and how to get started.

What you get​

Diagnostics that catch problems before the pipeline runs​

The language server runs the same validation Phobos itself runs β€” parse errors, missing required attributes, schema violations, cross-field constraints (interval requires attempts, deployments require environment, mount_point volumes must be declared), stage_order correctness, duplicate names, reserved keywords β€” plus a dozen Phobos-specific checks the editor is uniquely positioned to surface:

  • Unresolved var.<X>, jwt.<X>, vcs_token.<X>, volume.<X> references.
  • Structural typos in pipeline.* / this.* / phobos.* paths anchored to the specific bad segment (pipeline.stage.s.task.t.outp.x flags outp directly, not the whole expression).
  • Missing plugin "<name>" {} configuration blocks for every action that needs one β€” including the bundled exec plugin.
  • Plugin-alias resolution: action { plugin = <type>.<alias> } traversals that don't resolve, plus the "you must pick one" ambiguity case.
  • Dead action references (this.action.<X>.outputs.* where <X> isn't declared in the same task).
  • Unused variable, jwt, vcs_token, volume declarations.
  • Deprecated phobos.* and action_outputs.* references, rendered with strikethrough.

Every diagnostic is anchored at the precise expression range so a long file doesn't bury the cursor under a wall of text.

Context-aware completion​

Block types, attribute names, enumerated string values, HCL built-in functions, and the named-value namespaces (var., jwt., vcs_token., system., pipeline., this.) all complete with type hints and inline documentation. Inside dependencies = [...] you get the names of sibling tasks, deployments, and nested pipelines in the same stage and phase. Inside action { plugin = <cursor> } you get every declared <type>.<alias> qualified name.

Hover documentation​

Hover any block, attribute, action label, named value, or output reference to see the schema docs (with type, allowed values, and limits), the description from the plugin schema (for action attributes), or the resolved target β€” pipeline.stage.<s>.task.<t>.outputs.<o> follows the output "<o>" block back to its declaration.

Code actions and quick-fixes​

When the LSP knows how to fix a diagnostic, it surfaces a Ctrl+. action:

  • Install plugin β€” runs the right phobos plugin install command, including any --version constraint pinned in plugin_requirements.
  • Declare missing plugin "<name>" {} block β€” inserts the block at the natural anchor (after plugin_requirements if present, else after the last plugin block, else at top of file).
  • Declare aliased plugin block β€” when an action references an undeclared plugin = <type>.<alias>.
  • Set plugin = <type>.<alias> β€” one action per declared alias when the type has only aliased instances and the action must pick one explicitly.
  • Migrate deprecation β€” rewrites legacy phobos.* / action_outputs.* references to the current pipeline.* / this.action.* shape. Bridging output "<o>" blocks are inserted atomically when needed.
  • Extract to variable / inline variable β€” refactors that move a literal in or out of a variable block.
  • Source-fix-all β€” the editor's "fix everything" key applies every preferred deprecation fix in one undoable edit.

Inlay hints and other niceties​

Inlay hints add quiet annotations the cursor never lands on: variable types, variable defaults, per-task action + output counts (: 2 actions, 1 output), per-stage children counts (: 3 tasks, : 2 tasks, 1 pipeline), deployment environments, and per-action plugin-instance badges (: docker.builder when the action sets plugin = docker.builder). Each category is independently toggleable.

The extension also wires up:

  • Pipeline Graph webview β€” a layered DAG of stages, tasks, deployments, and dependencies = [...] edges. Click a node to jump to its declaration.
  • Evaluated Context panel β€” a live tree of every namespace the evaluator exposes at the cursor.
  • Templates tree β€” every pipeline.hcl / release_lifecycle.hcl in the workspace expanded to its outline.
  • Installed Plugins tree β€” the LSP's plugin inventory, with a right-click "Browse Online" to open the plugin's registry page.
  • Unified Phobos status chip β€” one glanceable token consolidating language-server state, CLI presence + version, plugin health, diagnostic counts, and active-file kind.

Snippets​

Built-in snippets cover the most common shapes β€” full templates (pipeline, multipipe), block scaffolds (stage, task, taskdeps, taskretry, pre, post, action, output, var, plugins, jwt, vcs, nested), and gates (approval for when = "manual" + approval_rules).

New in this release: .pbvars files​

Phobos has always supported file-based variable input (analogous to Terraform's .tfvars); this release wires that file format into the LSP as a first-class document type. Open a prod.pbvars next to a pipeline.hcl and you get:

  • Diagnostics against the sibling template β€” unknown variable names error out, type mismatches against the declared type produce friendly messages (variable "replicas" has type number but was assigned a value of type string), and unresolved references inside values surface HCL's own evaluation diagnostic.
  • Snippet-based completion β€” declared variable names from the sibling are filtered by typed prefix and inserted as typed snippets shaped to each variable's declared type. string inserts quotes-with-cursor-between, number inserts a 0 placeholder selected for one-keystroke overwrite, bool inserts a true/false choice dropdown, object({...}) types pre-populate every declared field as its own tab stop with = aligned. Already-assigned variables are filtered out so you don't accidentally re-declare them.
  • Hover β€” surface the sibling variable's description, declared type, and default-state.

The cross-file analysis is live β€” edits to the sibling template propagate to open .pbvars diagnostics, completions, and hover without saving first.

pipeline.hcl
variable "environment" {
type = string
default = "staging"
}

variable "replicas" {
type = number
}

variable "service" {
type = object({
name = string
timeout = number
})
}
prod.pbvars
environment = "production"
replicas = 5
service = {
name = "checkout"
timeout = 30
}

The CLI's loading precedence (env vars β†’ phobos.pbvars β†’ *.auto.pbvars β†’ --pb-var-file β†’ --pb-var) hasn't changed; see Variables for the full ordering.

Mixed-tool workspaces​

A common workspace has Phobos templates living next to Terraform, Nomad, or Packer files β€” all of them .hcl. The extension is intentionally narrow about which .hcl files it claims:

  • pipeline.hcl and release_lifecycle.hcl β€” claimed by exact filename, always.
  • *.pbvars β€” claimed by extension, always.
  • Any other *.hcl β€” not claimed by default.

If you want every .hcl file to be treated as Phobos (e.g. a workspace that's exclusively Phobos), flip phobos.editor.associateAllHclFiles to true. Toggling on takes effect immediately for every currently-open .hcl document; toggling off only affects future opens.

Installation​

VS Code, VSCodium, Cursor, and other VS Code-compatible editors​

The extension is published to two registries β€” pick the one your editor uses:

  • VS Code Marketplace β€” for VS Code itself, plus Cursor, Kiro, and any other Microsoft-licensed VS Code distribution.
  • Open VSX Registry β€” for VSCodium, Code OSS, Gitpod, Theia, and any other Eclipse-licensed VS Code fork that can't legally ship the Microsoft Marketplace.

Both registries publish the same VSIX. From the editor's Extensions view, search Phobos and install. Marketplace and Open VSX VSIXes ship without the language-server binary to keep the download small β€” on first launch the extension prompts:

Phobos language server not found. Install the latest version? [Install Latest] [Set Path…]

Choose Install Latest and the extension downloads, SHA-256-verifies, and installs the binary into your editor's global storage. You can rerun or upgrade from the command palette (Phobos: Install or Update Language Server).

For air-gapped setups, GitLab releases ship platform-specific VSIXes with the language-server bundled β€” install with code --install-extension mc-phobos-<version>-<platform>.vsix (or your editor's equivalent).

See the VS Code Extension guide for the full install matrix and configuration reference.

Neovim, Helix, Zed, Emacs, and other editors​

The language server is a standalone Go binary that speaks LSP over stdio. Install it from the language-server releases page (each release ships a sha256sums.txt manifest), or build from source with make build.

Quick install
VERSION=0.1.0
PLATFORM=linux_amd64
BASE="https://gitlab.com/infor-cloud/martian-cloud/phobos/phobos-language-server/-/releases/${VERSION}/downloads"
curl -L -o phobos-language-server "${BASE}/phobos-language-server_${VERSION}_${PLATFORM}"
chmod +x phobos-language-server
sudo mv phobos-language-server /usr/local/bin/

Per-editor configuration snippets are in the Language Server guide β€” Neovim with nvim-lspconfig, Helix's languages.toml, Eglot for Emacs.

Under the hood​

Both the language server and extension are open source under MPL 2.0:

  • phobos-language-server β€” Go, ~25k LoC. The same pkg/hcl/config schema constants Phobos itself uses; a reflection-driven schema tree means new HCL-tagged fields on PipelineTemplate / LifecycleTemplate show up in diagnostics, completion, and hover automatically.
  • vscode-phobos β€” TypeScript, hand-rolled LSP client + status-bar, tree views, and webviews. SHA-256-verified bundled binary on platform-specific releases.

The plugin-aware features (action-body diagnostics, completion, hover for installed plugins) work via the same gRPC Schema() endpoint the CLI uses; schemas are cached on disk so cold starts stay fast even when a plugin binary is gone or failing to spawn. A fsnotify watcher on ~/.phobos.d/plugins debounced at 500 ms means newly-installed plugins light up in the editor without any explicit refresh.

What's next​

Roadmap items we're already prototyping:

  • Code-action quick fix for "not declared" .pbvars diagnostics that adds the variable "<name>" { ... } block to the sibling template.
  • Range-restricted type diagnostics β€” point at the offending sub-expression instead of the whole RHS for nested type errors inside object literals.

If you have a feature request, please open an issue on either repo. MRs welcome.

Get started​