Skip to content

Commit 71e280b

Browse files
committed
Add qDup Language Server Protocol (LSP) module
Introduce a new qDup-lsp module that provides editor-agnostic IDE support for qDup YAML scripts via the Language Server Protocol. The server offers context-aware completion for commands, modifiers, host config keys, role keys, and script references; diagnostics for unknown keys, undefined references, and unused definitions; and hover documentation sourced from the qDup reference docs. Includes a JBang launcher script for easy startup. Document symbols providing a hierarchical outline of scripts, hosts, roles, and states sections Support go-to-definition for script references, hover, and completion for ${{variable}} patterns in command values. Variables defined under states:/globals: are resolved within the current document and across workspace files.
1 parent 0fcdf5d commit 71e280b

34 files changed

Lines changed: 6960 additions & 0 deletions

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<modules>
1313
<module>qDup-core</module>
1414
<module>qDup</module>
15+
<module>qDup-lsp</module>
1516
</modules>
1617

1718

qDup-lsp/README.md

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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.

qDup-lsp/example.qdup.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: example qDup script
2+
scripts:
3+
test-script:
4+
- sh: echo "hello"
5+
- set-state: greeting
6+
hosts:
7+
local: me@localhost
8+
roles:
9+
test-role:
10+
hosts:
11+
- local
12+
run-scripts:
13+
- test-script

qDup-lsp/pom.xml

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>io.hyperfoil.tools</groupId>
9+
<artifactId>qDup-parent</artifactId>
10+
<version>0.11.1-SNAPSHOT</version>
11+
</parent>
12+
13+
<artifactId>qDup-lsp</artifactId>
14+
<name>qDup Language Server</name>
15+
<description>Language Server Protocol implementation for qDup YAML scripts</description>
16+
17+
<properties>
18+
<maven.compiler.source>17</maven.compiler.source>
19+
<maven.compiler.target>17</maven.compiler.target>
20+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
21+
<version.lsp4j>0.23.1</version.lsp4j>
22+
</properties>
23+
24+
<dependencies>
25+
<dependency>
26+
<groupId>io.hyperfoil.tools</groupId>
27+
<artifactId>qDup-core</artifactId>
28+
<version>${project.version}</version>
29+
</dependency>
30+
<dependency>
31+
<groupId>org.eclipse.lsp4j</groupId>
32+
<artifactId>org.eclipse.lsp4j</artifactId>
33+
<version>${version.lsp4j}</version>
34+
</dependency>
35+
<dependency>
36+
<groupId>org.yaml</groupId>
37+
<artifactId>snakeyaml</artifactId>
38+
<version>2.2</version>
39+
</dependency>
40+
<dependency>
41+
<groupId>junit</groupId>
42+
<artifactId>junit</artifactId>
43+
<scope>test</scope>
44+
</dependency>
45+
</dependencies>
46+
47+
<build>
48+
<plugins>
49+
<plugin>
50+
<groupId>org.apache.maven.plugins</groupId>
51+
<artifactId>maven-compiler-plugin</artifactId>
52+
<version>${compiler-plugin.version}</version>
53+
<configuration>
54+
<source>17</source>
55+
<target>17</target>
56+
</configuration>
57+
</plugin>
58+
<plugin>
59+
<groupId>org.apache.maven.plugins</groupId>
60+
<artifactId>maven-shade-plugin</artifactId>
61+
<version>3.5.1</version>
62+
<executions>
63+
<execution>
64+
<phase>package</phase>
65+
<goals>
66+
<goal>shade</goal>
67+
</goals>
68+
<configuration>
69+
<transformers>
70+
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
71+
<mainClass>io.hyperfoil.tools.qdup.lsp.QDupLspLauncher</mainClass>
72+
</transformer>
73+
</transformers>
74+
<filters>
75+
<filter>
76+
<artifact>*:*</artifact>
77+
<excludes>
78+
<exclude>META-INF/*.SF</exclude>
79+
<exclude>META-INF/*.DSA</exclude>
80+
<exclude>META-INF/*.RSA</exclude>
81+
</excludes>
82+
</filter>
83+
</filters>
84+
</configuration>
85+
</execution>
86+
</executions>
87+
</plugin>
88+
<plugin>
89+
<groupId>org.apache.maven.plugins</groupId>
90+
<artifactId>maven-surefire-plugin</artifactId>
91+
<version>${version.surefire-plugin}</version>
92+
</plugin>
93+
</plugins>
94+
</build>
95+
</project>

qDup-lsp/qdup-lsp.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
///usr/bin/env jbang "$0" "$@" ; exit $?
2+
//JAVA 17+
3+
//DEPS io.hyperfoil.tools:qDup-lsp:0.11.1-SNAPSHOT
4+
5+
import io.hyperfoil.tools.qdup.lsp.QDupLspLauncher;
6+
7+
class qduplsp {
8+
9+
public static void main(String... args) {
10+
QDupLspLauncher.main(args);
11+
}
12+
13+
}

0 commit comments

Comments
 (0)