|
| 1 | +# qDup Language Server |
| 2 | + |
| 3 | +A Language Server Protocol (LSP) implementation for qDup YAML scripts, providing IDE support for command completion, diagnostics, and hover documentation. |
| 4 | + |
| 5 | +## Building |
| 6 | + |
| 7 | +```bash |
| 8 | +# Build the fat JAR (includes all dependencies) |
| 9 | +mvn -pl qDup-lsp package -DskipTests |
| 10 | + |
| 11 | +# Run unit tests |
| 12 | +mvn -pl qDup-lsp test |
| 13 | +``` |
| 14 | + |
| 15 | +The fat JAR is produced at `qDup-lsp/target/qDup-lsp-0.11.1-SNAPSHOT.jar`. |
| 16 | + |
| 17 | +## Running |
| 18 | + |
| 19 | +The server communicates via stdin/stdout using the JSON-RPC protocol defined by LSP. |
| 20 | + |
| 21 | +### Using JBang (recommended) |
| 22 | + |
| 23 | +The easiest way to run the server is with [JBang](https://www.jbang.dev/). No build step required — JBang resolves the dependency and launches the server directly: |
| 24 | + |
| 25 | +```bash |
| 26 | +jbang qDup-lsp/qdup-lsp.java |
| 27 | +``` |
| 28 | + |
| 29 | +Install JBang if you don't have it: |
| 30 | + |
| 31 | +```bash |
| 32 | +curl -Ls https://sh.jbang.dev | bash -s - app setup |
| 33 | +``` |
| 34 | + |
| 35 | +For development against a local SNAPSHOT build, install the artifact to your local Maven repository first: |
| 36 | + |
| 37 | +```bash |
| 38 | +mvn -pl qDup-lsp install -DskipTests |
| 39 | +jbang qDup-lsp/qdup-lsp.java |
| 40 | +``` |
| 41 | + |
| 42 | +### Using the fat JAR |
| 43 | + |
| 44 | +```bash |
| 45 | +java -jar qDup-lsp/target/qDup-lsp-0.11.1-SNAPSHOT.jar |
| 46 | +``` |
| 47 | + |
| 48 | +## Editor Setup |
| 49 | + |
| 50 | +### Neovim (nvim-lspconfig) |
| 51 | + |
| 52 | +Add a custom server configuration in your Neovim config. You can use either the JBang script or the fat JAR: |
| 53 | + |
| 54 | +```lua |
| 55 | +local lspconfig = require('lspconfig') |
| 56 | +local configs = require('lspconfig.configs') |
| 57 | + |
| 58 | +-- Option 1: Using JBang |
| 59 | +configs.qdup = { |
| 60 | + default_config = { |
| 61 | + cmd = { 'jbang', '/path/to/qdup-lsp.java' }, |
| 62 | + filetypes = { 'yaml' }, |
| 63 | + root_dir = lspconfig.util.find_git_ancestor, |
| 64 | + settings = {}, |
| 65 | + }, |
| 66 | +} |
| 67 | + |
| 68 | +-- Option 2: Using the fat JAR |
| 69 | +-- configs.qdup = { |
| 70 | +-- default_config = { |
| 71 | +-- cmd = { 'java', '-jar', '/path/to/qDup-lsp-0.11.1-SNAPSHOT.jar' }, |
| 72 | +-- filetypes = { 'yaml' }, |
| 73 | +-- root_dir = lspconfig.util.find_git_ancestor, |
| 74 | +-- settings = {}, |
| 75 | +-- }, |
| 76 | +-- } |
| 77 | + |
| 78 | +lspconfig.qdup.setup({}) |
| 79 | +``` |
| 80 | + |
| 81 | +To limit activation to qDup files only, you can use an `on_attach` or `autocommand` that checks for qDup-specific top-level keys (`scripts:`, `roles:`, `hosts:`). |
| 82 | + |
| 83 | +### VS Code |
| 84 | + |
| 85 | +Install a generic LSP client extension such as [vscode-languageclient](https://github.com/AKosyak/vscode-glspc) or create a minimal extension with a `package.json`: |
| 86 | + |
| 87 | +```json |
| 88 | +{ |
| 89 | + "name": "qdup-lsp", |
| 90 | + "displayName": "qDup Language Support", |
| 91 | + "version": "0.1.0", |
| 92 | + "engines": { "vscode": "^1.75.0" }, |
| 93 | + "activationEvents": ["onLanguage:yaml"], |
| 94 | + "main": "./extension.js", |
| 95 | + "contributes": { |
| 96 | + "configuration": { |
| 97 | + "properties": { |
| 98 | + "qdup.lsp.path": { |
| 99 | + "type": "string", |
| 100 | + "default": "", |
| 101 | + "description": "Path to the qDup LSP fat JAR" |
| 102 | + } |
| 103 | + } |
| 104 | + } |
| 105 | + } |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +With an `extension.js`: |
| 110 | + |
| 111 | +```javascript |
| 112 | +const { LanguageClient, TransportKind } = require('vscode-languageclient/node'); |
| 113 | + |
| 114 | +let client; |
| 115 | + |
| 116 | +function activate(context) { |
| 117 | + const jarPath = vscode.workspace.getConfiguration('qdup').get('lsp.path'); |
| 118 | + const serverOptions = { |
| 119 | + command: 'java', |
| 120 | + args: ['-jar', jarPath], |
| 121 | + transport: TransportKind.stdio, |
| 122 | + }; |
| 123 | + const clientOptions = { |
| 124 | + documentSelector: [{ scheme: 'file', language: 'yaml' }], |
| 125 | + }; |
| 126 | + client = new LanguageClient('qdup', 'qDup Language Server', serverOptions, clientOptions); |
| 127 | + client.start(); |
| 128 | +} |
| 129 | + |
| 130 | +function deactivate() { |
| 131 | + return client?.stop(); |
| 132 | +} |
| 133 | + |
| 134 | +module.exports = { activate, deactivate }; |
| 135 | +``` |
| 136 | +
|
| 137 | +### Emacs (eglot) |
| 138 | +
|
| 139 | +```elisp |
| 140 | +;; Using JBang |
| 141 | +(with-eval-after-load 'eglot |
| 142 | + (add-to-list 'eglot-server-programs |
| 143 | + '(yaml-mode . ("jbang" "/path/to/qdup-lsp.java")))) |
| 144 | +
|
| 145 | +;; Or using the fat JAR |
| 146 | +;; (with-eval-after-load 'eglot |
| 147 | +;; (add-to-list 'eglot-server-programs |
| 148 | +;; '(yaml-mode . ("java" "-jar" "/path/to/qDup-lsp-0.11.1-SNAPSHOT.jar")))) |
| 149 | +``` |
| 150 | +
|
| 151 | +### Helix |
| 152 | +
|
| 153 | +Add to `~/.config/helix/languages.toml`: |
| 154 | +
|
| 155 | +```toml |
| 156 | +[[language]] |
| 157 | +name = "yaml" |
| 158 | +language-servers = ["qdup-lsp"] |
| 159 | + |
| 160 | +# Using JBang |
| 161 | +[language-server.qdup-lsp] |
| 162 | +command = "jbang" |
| 163 | +args = ["/path/to/qdup-lsp.java"] |
| 164 | + |
| 165 | +# Or using the fat JAR |
| 166 | +# [language-server.qdup-lsp] |
| 167 | +# command = "java" |
| 168 | +# args = ["-jar", "/path/to/qDup-lsp-0.11.1-SNAPSHOT.jar"] |
| 169 | +``` |
| 170 | +
|
| 171 | +## Features |
| 172 | +
|
| 173 | +### Completion |
| 174 | +
|
| 175 | +The server provides context-aware completions for: |
| 176 | +
|
| 177 | +| Context | Completions | |
| 178 | +|---|---| |
| 179 | +| Top-level keys | `name`, `scripts`, `hosts`, `roles`, `states`, `globals` | |
| 180 | +| Script commands | All 32+ qDup commands (`sh`, `regex`, `set-state`, etc.) | |
| 181 | +| Command modifiers | `then`, `else`, `watch`, `with`, `timer`, `on-signal`, `silent`, etc. | |
| 182 | +| Command parameters | Command-specific keys (e.g., `command`, `prompt` for `sh`) | |
| 183 | +| Host configuration | 21 host config keys (`hostname`, `username`, `port`, `identity`, etc.) | |
| 184 | +| Role properties | `hosts`, `setup-scripts`, `run-scripts`, `cleanup-scripts` | |
| 185 | +| Script references | Script names defined in the `scripts:` section | |
| 186 | +
|
| 187 | +### Diagnostics |
| 188 | +
|
| 189 | +The server validates documents and reports: |
| 190 | +
|
| 191 | +- **Errors:** Unknown top-level keys, unknown command names, unknown host config keys, unknown role keys |
| 192 | +- **Warnings:** Undefined script references in roles, undefined host references in roles |
| 193 | +- **Info:** Unused scripts not referenced by any role, unused hosts not referenced by any role |
| 194 | +
|
| 195 | +### Hover |
| 196 | +
|
| 197 | +Hovering over qDup elements shows documentation: |
| 198 | +
|
| 199 | +- **Commands** — description and usage from the qDup reference docs |
| 200 | +- **Command parameters** — per-parameter documentation (e.g., `sh.command`, `regex.pattern`) |
| 201 | +- **Modifiers** — description of `then`, `watch`, `timer`, `on-signal`, etc. |
| 202 | +- **Host config keys** — description of `hostname`, `port`, `identity`, etc. |
| 203 | +- **Top-level keys** — description of `scripts`, `roles`, `hosts`, etc. |
| 204 | +- **Role keys** — description of `setup-scripts`, `run-scripts`, etc. |
| 205 | +
|
| 206 | +## Architecture |
| 207 | +
|
| 208 | +``` |
| 209 | +io.hyperfoil.tools.qdup.lsp |
| 210 | +├── QDupLspLauncher # Entry point (stdin/stdout JSON-RPC) |
| 211 | +├── QDupLanguageServer # LanguageServer impl, declares capabilities |
| 212 | +├── QDupTextDocumentService # Completion, hover, diagnostics wiring |
| 213 | +├── QDupWorkspaceService # Stub |
| 214 | +├── QDupDocument # Parsed document model (text + SnakeYAML Node tree) |
| 215 | +├── YamlContext # Enum of cursor context types |
| 216 | +├── CursorContextResolver # Determines YamlContext from position + Node tree |
| 217 | +├── CompletionProvider # Produces CompletionItems based on context |
| 218 | +├── DiagnosticsProvider # Validates document, produces Diagnostics |
| 219 | +├── HoverProvider # Produces Hover content based on context |
| 220 | +└── CommandRegistry # Extracts command metadata from qDup-core Parser |
| 221 | +``` |
| 222 | +
|
| 223 | +The `CommandRegistry` loads command metadata from the qDup-core `Parser` at startup using reflection, giving the LSP access to the same command definitions used by the qDup runtime. When the `Parser` is not available (e.g., classpath issues), it falls back to a hardcoded command list. |
| 224 | +
|
| 225 | +Document parsing uses SnakeYAML's `compose()` method to produce a `Node` tree with line/column positions. When `compose()` fails on broken YAML, a line-based fallback determines context from indentation and parent key patterns. |
0 commit comments