Language Server
The Phobos Language Server (phobos-language-server) is a standalone Go binary implementing the Language Server Protocol for Phobos HCL pipeline templates, release lifecycle templates, and Phobos variables files. It powers the Phobos VS Code extension but works with any LSP-capable editor β Neovim, Helix, Zed, Emacs, Sublime Text, or any custom client.
If you're using VS Code or a fork (VSCodium, Cursor, Kiro), install the Phobos VS Code extension instead β it manages the language server for you (bundled in GitLab-releases VSIXes, downloaded on first launch for marketplace installs) and adds activity-bar views, status-bar integration, and webviews on top. This page is for editors that don't have a dedicated Phobos extension.
What it providesβ
Every feature below works on pipeline.hcl and release_lifecycle.hcl files at the template level β no plugin installation required. When local plugins are installed via phobos plugin install, action-body features light up for the configured actions too. Variables files (*.pbvars) get a tighter set of cross-file features against their sibling template β see Variables files.
LSP methodsβ
| Area | LSP methods |
|---|---|
| Lifecycle | initialize, initialized, shutdown, exit |
| Sync | textDocument/didOpen, didChange (incremental), didClose |
| Diagnostics | textDocument/publishDiagnostics (push), textDocument/diagnostic + workspace/diagnostic (pull β LSP 3.17+) |
| Completion | textDocument/completion, textDocument/signatureHelp |
| Navigation | textDocument/definition, textDocument/references, textDocument/documentHighlight, textDocument/documentSymbol, workspace/symbol |
| Call hierarchy | textDocument/prepareCallHierarchy, callHierarchy/incomingCalls, callHierarchy/outgoingCalls |
| Rename | textDocument/prepareRename, textDocument/rename (with cross-file propagation for workspace-level declarations) |
| Hover | textDocument/hover |
| Structure | textDocument/foldingRange, textDocument/selectionRange |
| Decoration | textDocument/documentLink, textDocument/semanticTokens/full, textDocument/inlayHint |
| Formatting | textDocument/formatting, textDocument/rangeFormatting, textDocument/onTypeFormatting (triggered on }) |
| Code actions | textDocument/codeAction (install-plugin, declare-missing-plugin-block, declare-aliased-plugin-block, set-plugin-ref per declared alias, deprecation rewrites, extract-to-variable / inline-variable refactors, source.fixAll) |
Diagnosticsβ
- Parse errors, missing required attributes, disallowed enum values, reserved-keyword / name-format violations, block count limits, duplicate stage / task / action names.
- Cross-field constraints:
intervalrequiresattempts,pipeline_type = "deployment"requiresenvironment,mount_pointvolumes must be declared. stage_ordercorrectness (drift betweenstage_orderentries and declared stages).- Return-type validation of HCL built-in functions against the field they populate.
- Dependency-name validation β warns on
dependencies = ["X"]whenXisn't a sibling task / deployment / nested pipeline in the same stage and phase. - Dead action-reference validation β warns on
this.action.<X>.outputs.*whenXisn't declared inside the same task. Covers every expression inside the task body, including task-level attributes likesuccess_condition,if, andwhen. - Undefined-reference validation β errors on
var.<X>,jwt.<X>,vcs_token.<X>,volume.<X>when the named value isn't declared, and on any traversal whose root isn't a known Phobos namespace (catches typos likepipelne.stage.*). Local bindings fromdynamicblocks are exempted. - Namespace-structure validation β walks
pipeline.*,this.*, andphobos.*traversals segment-by-segment against a fixed grammar. Typos in structural keywords (pipeline.stage.<s>.task.<t>.outp.<o>whereoutpshould beoutputs) produce a targeted diagnostic on the specific wrong segment. - Plugin-configuration-required validation β errors when a task uses
action "<plugin>_<act>"but no matchingplugin "<plugin>" { ... }block is declared at the template root. Every plugin includingexecrequires a declaration. - Plugin-alias validation β when a template declares aliased plugin instances:
- The
pluginattribute on an action is a symbolic traversal expression (e.g.plugin = tharsis.primary).action { plugin = <type>.<alias> }refs that don't resolve to a declared plugin instance produce an error with a concrete "declareplugin \"<type>\" { alias = \"<alias>\" }or changepluginto match a declared name" hint. - When a type has only aliased instances (no unaliased default), actions of that type MUST set
plugin = .... The diagnostic lists every available qualified name so the fix is a copy-paste.
- The
- Deprecation markers on
phobos.*andaction_outputs.*references (asDiagnosticTag.Deprecated); hovers and completion items point at thepipeline.*/this.action.*replacements. - Unused-declaration diagnostics for
variable,jwt,vcs_token, andvolume. Default severity isInformation; clients can opt intoHintvia configuration. - Enum-typed attributes (
on_error,when,pipeline_type, ...) accept variable references without false positives β the value resolves at runtime and is enforced there.
Completionβ
Context-aware completion for:
- Block types, attribute names, and enumerated string values.
- HCL built-in function names and signatures.
- Named-value namespaces:
var.*,jwt.*,vcs_token.*,system.*,pipeline.*,this.*. - All four output-reference shapes, including
this.alias.*and typed-output field navigation. - Sibling task / deployment / nested-pipeline names inside
dependencies = [...]. - Declared plugin-instance names (qualified
<type>.<alias>) insideaction { plugin = <cursor> }.
Variables files (*.pbvars)β
Phobos variables files supply concrete inputs to a sibling pipeline or release-lifecycle template β the analog of Terraform's .tfvars. The CLI loads them in a defined precedence order at pipeline-creation time (see Variables guide); the language server adds editor support on top.
- Body shape β top-level attribute assignments only (
name = value). Any block at this scope fires an error pointing at the block keyword (a clear sign the author confused a vars file for a template). - Cross-file diagnostics against the sibling template's
variable "<name>"declarations, found by walking the.pbvarsfile's directory forpipeline.hclthenrelease_lifecycle.hcl:- Unknown variable name β
variable "x" is not declared in the sibling template; remove the assignment or add a 'variable "x" { ... }' block to the template. - Type mismatch β
variable "replicas" has type number but was assigned a value of type string. The check parses the variable'stype = ...expression viatypeexpr.TypeConstraintand runsconvert.Convert, so structured types (object({...}),list(...), etc.) are checked field-by-field. - Unresolved references inside a value β HCL's own evaluation diagnostic (e.g.
Variables not allowed: β¦). Vars files don't carry an eval context, so references are diagnosed where they appear. - Stays silent when the directory has no canonical sibling β false-flagging every attribute as "unknown" would be hostile.
- Unknown variable name β
- Completion β declared variable names from the sibling, filtered by typed prefix and suppressed for already-assigned names. Each item inserts a typed snippet shaped to the variable's declared
cty.Type:stringβname = "${1}"(cursor between the quotes)numberβname = ${1:0}(placeholder selected for one-keystroke overwrite)boolβname = ${1|true,false|}(choice dropdown)list/set/tupleβname = [${1}]object({...})β multi-line, one tab stop per declared attribute, sorted alphabetically with=aligned and each field's value sized to its declared typemap(...)βname = {\n ${1}\n}(single tab stop, since keys are dynamic)
- Hover β surfaces the sibling variable's
description, declaredtype, and default-state ("Has a defaultβ¦" vs "No default declaredβ¦"). - Live cross-file β the editor integration plumbs an in-memory document store into the LSP so unsaved sibling buffers propagate to
.pbvarsdiagnostics, completions, and hover live, without the user saving first. Vars-file analysis bypasses the analysis cache for this reason.
Plugin-aware featuresβ
When a Phobos plugin is installed at ~/.phobos.d/plugins/<host>/<org>/<name>/<version>/<os>_<arch>/, the language server fetches the plugin's schema and extends diagnostics, completion, and hover to cover action bodies for every action the plugin exposes.
- Built-in schemas β The
execplugin's schema is bundled, soaction "exec_command"works without installing anything. - Schema cache β Plugin schemas are cached on disk (
<binpath_dir>/schema.cache.json) so the next cold start is fast even when the plugin binary is missing. - Live reload β A filesystem watcher on
~/.phobos.d/pluginspicks up newly-installed plugins (debounced at 500 ms) without any explicit editor action. - Install quick-fix β The
Plugin not installeddiagnostic carries structured data so editors can emit a "runphobos plugin install β¦" code action, including any--versionconstraint the template pins.
Installationβ
Pre-built binaries for the common platforms are published on the releases page. Each release ships with a sha256sums.txt manifest alongside the binaries.
Linux / macOSβ
# Replace VERSION with the release tag and PLATFORM with one of:
# linux_amd64, linux_arm64, linux_arm, linux_386,
# darwin_amd64, darwin_arm64
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}"
curl -L -o sha256sums.txt "${BASE}/sha256sums.txt"
# Verify before installing.
sha256sum -c --ignore-missing sha256sums.txt
chmod +x phobos-language-server
sudo mv phobos-language-server /usr/local/bin/
Windowsβ
Download phobos-language-server_<version>_windows_amd64.exe (or windows_386.exe) from the releases page, verify its SHA-256 against the manifest, and place it on your PATH.
Building from sourceβ
git clone https://gitlab.com/infor-cloud/martian-cloud/phobos/phobos-language-server.git
cd phobos-language-server
make build
# Produces bin/phobos-language-server
Requires Go 1.26 or newer.
Runningβ
The server speaks LSP over stdio by default β every mainstream LSP client uses stdio, so the usual phobos-language-server command suffices.
phobos-language-server # stdio (default)
phobos-language-server --version
phobos-language-server --port 7777 # TCP mode, for dev / debugging
TCP mode is primarily for language-server developers β run the server in one terminal, connect from an editor that supports TCP LSP transport. It's equally usable from any LSP client, but the stdio default is simpler and is what every editor defaults to.
Editor setupβ
The language server activates on pipeline.hcl, release_lifecycle.hcl, and any *.pbvars file. Configure your editor to:
- Associate those filename patterns with a Phobos-specific language ID (we use
phobosin the VS Code extension; other editors can pick any ID). - Launch
phobos-language-servervia stdio.
Example snippets below are sketches β refer to each client's own documentation for the authoritative configuration format.
Neovim (nvim-lspconfig)β
vim.filetype.add({
filename = {
['pipeline.hcl'] = 'phobos',
['release_lifecycle.hcl'] = 'phobos',
},
pattern = {
['.*%.pbvars'] = 'phobos',
},
})
require('lspconfig.configs').phobos = {
default_config = {
cmd = { 'phobos-language-server' },
filetypes = { 'phobos' },
root_dir = require('lspconfig').util.root_pattern('pipeline.hcl', 'release_lifecycle.hcl', '.git'),
},
}
require('lspconfig').phobos.setup({})
Helixβ
[[language]]
name = "phobos"
scope = "source.hcl"
file-types = ["hcl"] # narrow further via roots if you share HCL with other tools
language-servers = ["phobos-language-server"]
[language-server.phobos-language-server]
command = "phobos-language-server"
Zedβ
Zed's extension API exposes LSP integrations as custom extensions. The server launches via stdio the same way β see the Zed extension docs for the current wrapper format.
Emacs (eglot / lsp-mode)β
(add-to-list 'auto-mode-alist '("pipeline\\.hcl\\'" . phobos-mode))
(add-to-list 'auto-mode-alist '("release_lifecycle\\.hcl\\'" . phobos-mode))
(add-to-list 'auto-mode-alist '("\\.pbvars\\'" . phobos-mode))
(add-to-list 'eglot-server-programs
'(phobos-mode . ("phobos-language-server")))
Configurationβ
Feature toggles are sent to the server via LSP initializationOptions (on initialize) and workspace/didChangeConfiguration (at runtime). The VS Code extension sends these automatically from phobos.* settings; other clients must wire them through in their own configuration.
| Option | Default | Description |
|---|---|---|
inlayHints.enabled | true | Master switch for inlay hints. |
inlayHints.variableTypes | true | Render : string, : number, etc. after untyped variable defaults. |
inlayHints.variableDefaults | true | Render the resolved default value next to var.<name> references where the default is a literal. |
inlayHints.actionPlugins | true | Render the resolved plugin instance next to each action "<plugin>_<name>" label β : docker.builder when plugin = docker.builder is set, : docker otherwise. |
inlayHints.actionCounts | true | Render per-task action + output counts (: 2 actions, 1 output). The outputs half is hidden when the task has none. |
inlayHints.deployments | true | Render environment = "β¦" after deployment blocks in lifecycle templates. |
inlayHints.stages | true | Render a children-count summary after each stage "<name>" label (: 3 tasks, : 2 tasks, 1 pipeline). Main-phase direct children only. |
documentLinks.enabled | true | Master switch for document links. |
documentLinks.pluginDocs | true | Turn plugin source strings in plugin_requirements into clickable registry links. |
semanticTokens.enabled | true | Enable Phobos semantic tokens layered on top of the client's TextMate grammar. |
diagnostics.unusedAsHint | false | Render unused-declaration diagnostics as Hint instead of Information. Quieter but less discoverable. |
Example initializationOptions payload:
{
"inlayHints": { "enabled": true, "actionPlugins": false },
"diagnostics": { "unusedAsHint": true }
}
Workspace trustβ
The server spawns plugin binaries under ~/.phobos.d/plugins/ to introspect their schemas. When running the server on behalf of a workspace whose content you don't trust β e.g. a client's untrusted-workspace mode β set the PHOBOS_LSP_DISABLE_PLUGINS environment variable to 1 before launching:
PHOBOS_LSP_DISABLE_PLUGINS=1 phobos-language-server
With the variable set, the server skips plugin discovery and schema introspection entirely. Syntax-level diagnostics, template-level diagnostics, completion, hover, formatting, and every other non-plugin-aware feature remain available. Action-body completion and validation silently degrade to "no plugin schemas".
Custom workspace/executeCommand endpointsβ
The server exposes several custom commands through workspace/executeCommand. Editor implementors can call these to light up integrations the VS Code extension uses.
| Command | Purpose |
|---|---|
phobos.reloadPlugins | Re-run plugin discovery and republish diagnostics. Rate-limited to one run per 750 ms. |
phobos.listPlugins | Return the plugin inventory (names, versions, sources, built-in flag). |
phobos.getPluginSchema | Fetch one plugin's schema on demand. |
phobos.pluginHealth | Per-plugin ready / failed report for status-bar indicators. |
phobos.migrateDeprecations | Compile a WorkspaceEdit applying every preferred deprecation fix in a document. |
phobos.evalSnapshot | Return the evaluator namespace tree at a position β what VS Code's Evaluated Context panel renders. |
phobos.documentKind | Return "pipeline" or "lifecycle" for a document. |
phobos.pipelineGraph | Return a layered-DAG structure (stages + child nodes + edges) for a template. |
phobos.runShellCommand | Client-side target for install quick-fixes β the server emits Command descriptors, the client runs the shell. |
Troubleshootingβ
The server crashes on startupβ
Check the client's language-server log output. The server panics are caught per-request (recoverHandler logs the stack and returns an error instead of killing the process), but a crash during startup β before the handler wrapper installs β surfaces in the client's transport log. Most startup crashes are bad Go build flags; a clean make build or a fresh release binary fixes them.
Plugin schemas aren't loadingβ
- Verify the plugin is installed at
~/.phobos.d/plugins/<host>/<org>/<name>/<version>/<os>_<arch>/and that the binary is a regular file (not a symlink β the server rejects symlinks for security). - Check the Phobos Output channel or equivalent for
subprocess failed/timed outmessages. Plugin subprocesses have a 10-secondSchema()deadline. - Try
phobos.reloadPlugins(via your client's command surface) or restart the language server. - If
PHOBOS_LSP_DISABLE_PLUGINS=1is set in the server's environment, plugin discovery is disabled by design β clear the variable to re-enable it.
Diagnostics appear twice in the Problems panelβ
Your client may be listening on both push (publishDiagnostics) and pull (textDocument/diagnostic) channels without deduplication. The server suppresses push when the client declares pull support during initialize, so check that your client is sending textDocument: { diagnostic: {...} } in its capabilities payload. Otherwise, disable pull support in the client to fall back to push-only.
source.fixAll doesn't trigger on saveβ
Make sure the client advertises textDocument.codeAction.codeActionLiteralSupport.codeActionKind.valueSet covering source.fixAll, and that the editor's on-save action invokes the source.fixAll kind specifically. VS Code's editor.codeActionsOnSave uses this pattern; other clients vary.
Relatedβ
- VS Code Extension β managed-server UX (bundled or runtime-installed) for VS Code and compatible editors.
- HCL Reference β template syntax the language server validates.
- Plugin reference β plugin schemas that drive action-body features.