opencode-qml-lsp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.qmllint.ini ADDED
@@ -0,0 +1,6 @@
1
+ [General]
2
+ MaxWarnings=-1
3
+
4
+ [Warnings]
5
+ UnqualifiedAccess=disable
6
+ BlockScopeVarDeclaration=disable
@@ -0,0 +1,203 @@
1
+ # PLAN: opencode-qml-lsp Plugin
2
+
3
+ > A plugin + config generator that gives OpenCode first-class QML language server support.
4
+ > Installable via npm, works for any QML/PySide6 project.
5
+
6
+ ---
7
+
8
+ ## Goal
9
+
10
+ Create `opencode-qml-lsp` — an npm-distributable plugin that:
11
+ 1. **Generates** the correct `lsp` config for `opencode.json` (auto-detects qmlls + PySide6 paths)
12
+ 2. **Generates** a `.qmlint` config to suppress noisy warnings
13
+ 3. **Provides** a custom tool `qml_fix_warnings` for auto-fixing lint issues
14
+ 4. **Notifies** via toast when QML issues are detected
15
+
16
+ **Plugin limitations (OpenCode API constraints):**
17
+ - Plugins **cannot** dynamically inject LSP config at runtime — LSP is set via `opencode.json`
18
+ - Plugins **cannot** intercept/modify `lsp.client.diagnostics` — they can only listen
19
+ - So the plugin works in two modes:
20
+ - **Setup mode**: generates config files the user adds to their project
21
+ - **Runtime mode**: provides custom tools + notifications during sessions
22
+
23
+ ---
24
+
25
+ ## Architecture
26
+
27
+ ```
28
+ opencode-qml-lsp/
29
+ ├── package.json # @opencode-ai/plugin dependency
30
+ ├── src/
31
+ │ ├── index.js # Plugin entry point
32
+ │ ├── setup.js # Config generator (qmlls detection, .qmlint, lsp config)
33
+ │ ├── fix-tool.js # Custom tool for auto-fixing warnings
34
+ │ └── notify.js # Diagnostic listener + toast notifications
35
+ └── README.md
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Phase 1: Setup Command + Config Generator
41
+
42
+ **What:** A command that detects the QML environment and generates config files.
43
+
44
+ **How:**
45
+ - Plugin registers a custom command `/qml-setup`
46
+ - When run, it:
47
+ 1. Searches for `qmlls` binary (PATH, common locations, PySide6 venv)
48
+ 2. Detects PySide6 installation path for QML import resolution
49
+ 3. Scans project for `.qml` files to confirm QML project
50
+ 4. Generates two files:
51
+ - `.qmlint` — suppresses `unqualified` and `block-scope-var-declaration`
52
+ - `opencode-qml-lsp.config.json` — the `lsp` block to merge into `opencode.json`
53
+ 5. Prints instructions for the user to merge the config
54
+
55
+ **Hooks used:**
56
+ - `command.executed` — detect `/qml-setup` command
57
+ - `tui.toast.show` — notify user of setup results
58
+
59
+ **Generated `.qmlint`:**
60
+ ```ini
61
+ [unqualified]
62
+ disable=true
63
+
64
+ [block-scope-var-declaration]
65
+ disable=true
66
+ ```
67
+
68
+ **Generated `lsp` config snippet:**
69
+ ```json
70
+ {
71
+ "lsp": {
72
+ "qmlls": {
73
+ "command": ["/path/to/qmlls", "-E", "-I/path/to/PySide6/Qt/qml"],
74
+ "extensions": [".qml", ".qrc"]
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ **Success criteria:**
81
+ - User runs `/qml-setup` → gets two config files
82
+ - User merges lsp config into `opencode.json` → qmlls starts working
83
+ - `.qmlint` in place → noise warnings suppressed
84
+
85
+ ---
86
+
87
+ ## Phase 2: Auto-Fix Custom Tool
88
+
89
+ **What:** Custom tool that the AI can call to fix QML lint warnings.
90
+
91
+ **How:**
92
+ - Register custom tool via plugin `tool` hook
93
+ - Tool reads file, parses qmllint output, applies fixes
94
+ - Fixes:
95
+ - `var` → `let`/`const` in JS blocks
96
+ - Remove unused imports
97
+ - Qualify unqualified property accesses with `root.` prefix
98
+ - Verifies fix by running `qmllint` on the file
99
+
100
+ **Tool definition:**
101
+ ```js
102
+ qml_fix_warnings: tool({
103
+ description: "Fix QML lint warnings in a file",
104
+ args: {
105
+ filePath: tool.schema.string(),
106
+ fixTypes: tool.schema.array(tool.schema.enum([
107
+ "var-to-let",
108
+ "qualify-access",
109
+ "remove-unused-imports"
110
+ ]))
111
+ },
112
+ async execute(args, context) {
113
+ // Read file → parse qmllint output → apply fixes → verify
114
+ }
115
+ })
116
+ ```
117
+
118
+ **Success criteria:**
119
+ - AI calls `qml_fix_warnings({ filePath: "canvas.qml", fixTypes: ["var-to-let"] })`
120
+ - File is fixed, qmllint confirms clean
121
+
122
+ ---
123
+
124
+ ## Phase 3: Diagnostic Notifications
125
+
126
+ **What:** Listen to `lsp.client.diagnostics` events and show toast summaries.
127
+
128
+ **How:**
129
+ - Hook `lsp.client.diagnostics` — listen for QML file diagnostics
130
+ - When a `.qml` file has diagnostics, show toast:
131
+ - `"canvas.qml: 3 warnings, 0 errors"`
132
+ - Hook `file.edited` — when a `.qml` file is saved, trigger re-lint via bash
133
+
134
+ **Hooks used:**
135
+ - `lsp.client.diagnostics` — listen for diagnostic events
136
+ - `file.edited` — trigger re-lint on save
137
+ - `tui.toast.show` — show summary notifications
138
+
139
+ **Success criteria:**
140
+ - Open a QML file with issues → toast shows count
141
+ - Fix a QML file → toast updates
142
+
143
+ ---
144
+
145
+ ## Phase 4: npm Package + Distribution
146
+
147
+ **What:** Publish to npm so anyone can install.
148
+
149
+ **How:**
150
+ - `package.json` with name `opencode-qml-lsp`
151
+ - `@opencode-ai/plugin` as dependency
152
+ - Entry point exports plugin function
153
+ - README with install instructions
154
+
155
+ **Install for users:**
156
+ ```json
157
+ // opencode.json
158
+ {
159
+ "plugin": ["opencode-qml-lsp"]
160
+ }
161
+ ```
162
+
163
+ Then run `/qml-setup` in any QML project.
164
+
165
+ ---
166
+
167
+ ## Implementation Order
168
+
169
+ 1. **Phase 1** — Setup command + config generator (foundation, testable immediately)
170
+ 2. **Phase 2** — Auto-fix custom tool (highest value feature)
171
+ 3. **Phase 3** — Diagnostic notifications (polish)
172
+ 4. **Phase 4** — npm packaging (distribution)
173
+
174
+ ---
175
+
176
+ ## Files to Create
177
+
178
+ | File | Purpose |
179
+ |---|---|
180
+ | `plugins/qml-lsp/package.json` | Plugin package definition |
181
+ | `plugins/qml-lsp/src/index.js` | Plugin entry, hooks registration |
182
+ | `plugins/qml-lsp/src/setup.js` | qmlls detection + config generation |
183
+ | `plugins/qml-lsp/src/fix-tool.js` | Custom auto-fix tool |
184
+ | `plugins/qml-lsp/src/notify.js` | Diagnostic listener + toast |
185
+ | `plugins/qml-lsp/README.md` | Usage docs |
186
+
187
+ ---
188
+
189
+ ## Testing Strategy
190
+
191
+ 1. **Local testing** — place in `.opencode/plugins/` during development
192
+ 2. **Test project** — use `my_canvas_app/views/canvas.qml` (374 warnings → ~5 after .qmlint)
193
+ 3. **Verify setup** — run `/qml-setup`, check generated files
194
+ 4. **Verify fix tool** — call `qml_fix_warnings`, check lsp_diagnostics
195
+ 5. **Distribution test** — `npm link` to simulate npm install
196
+
197
+ ---
198
+
199
+ ## Risks
200
+
201
+ - **qmlls not on PATH** — setup command searches common locations: `~/.local/share/qmlls/`, PySide6 venv, system PATH
202
+ - **PySide6 import paths vary** — setup auto-detects by finding `python -c "import PySide6; print(PySide6.__path__)"`
203
+ - **Custom command registration** — OpenCode plugins register commands via `tui.executeCommand`, not a dedicated command hook. May need to use a different approach (e.g., detect user typing `/qml-setup` in prompt)
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # opencode-qml-lsp
2
+
3
+ QML language server support for [OpenCode](https://opencode.ai).
4
+
5
+ ## Features
6
+
7
+ - **Auto-detect qmlls** — finds qmlls binary and PySide6 QML import paths
8
+ - **Generate .qmlint** — suppresses noisy warnings (unqualified access, block-scope-var)
9
+ - **Custom LSP config** — generates the correct `lsp` block for your `opencode.json`
10
+ - **Auto-fix tool** — `qml_fix_warnings` fixes common lint issues automatically
11
+
12
+ ## Installation
13
+
14
+ ### 1. Add to your `opencode.json`
15
+
16
+ ```json
17
+ {
18
+ "plugin": ["opencode-qml-lsp"]
19
+ }
20
+ ```
21
+
22
+ ### 2. Run setup in your QML project
23
+
24
+ Open OpenCode in your QML project directory and ask the AI to run:
25
+
26
+ ```
27
+ run qml_setup
28
+ ```
29
+
30
+ Or invoke the `qml_setup` tool directly. This generates two files:
31
+ - `.qmlint` — suppresses noisy warnings
32
+ - `opencode-qml-lsp.config.json` — the `lsp` config block to merge into `opencode.json`
33
+
34
+ ### 3. Merge the LSP config
35
+
36
+ Copy the `lsp` block from `opencode-qml-lsp.config.json` into your `opencode.json`:
37
+
38
+ ```json
39
+ {
40
+ "$schema": "https://opencode.ai/config.json",
41
+ "lsp": {
42
+ "qmlls": {
43
+ "command": ["/path/to/qmlls", "-E", "-I/path/to/PySide6/Qt/qml"],
44
+ "extensions": [".qml", ".qrc"]
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ ## Custom Tools
51
+
52
+ ### `qml_setup`
53
+
54
+ Detects `qmlls`, finds PySide6 QML import paths, and generates `.qmlint` + LSP config. No arguments needed.
55
+
56
+ ### `qml_fix_warnings`
57
+
58
+ Fixes QML lint warnings in a file:
59
+
60
+ ```
61
+ qml_fix_warnings({
62
+ filePath: "my_canvas_app/views/canvas.qml",
63
+ fixTypes: ["var-to-let", "remove-unused-imports", "qualify-access"]
64
+ })
65
+ ```
66
+
67
+ ### Fix Types
68
+
69
+ | Fix Type | What it does |
70
+ |---|---|
71
+ | `var-to-let` | Replaces `var` with `let` in JS blocks |
72
+ | `remove-unused-imports` | Removes unused `import` statements |
73
+ | `qualify-access` | Adds `root.` prefix to unqualified property accesses |
74
+
75
+ ## Development
76
+
77
+ ```bash
78
+ # Clone
79
+ git clone https://github.com/your-username/opencode-qml-lsp.git
80
+ cd opencode-qml-lsp
81
+
82
+ # Install dependencies
83
+ npm install
84
+
85
+ # Link locally for testing
86
+ ln -s $(pwd) ~/.config/opencode/plugins/opencode-qml-lsp
87
+
88
+ # Or use npm link
89
+ npm link
90
+ ```
91
+
92
+ ## License
93
+
94
+ MIT
package/opencode.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "model": "opencode/qwen3.6-plus-free",
4
+ "plugin": [
5
+ "oh-my-openagent@latest"
6
+ ]
7
+ }
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "opencode-qml-lsp",
3
+ "version": "0.1.0",
4
+ "description": "QML language server support for OpenCode — auto-detect qmlls, filter diagnostics, auto-fix warnings",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "echo \"No tests yet\""
9
+ },
10
+ "keywords": ["opencode", "qml", "qmlls", "pyside6", "lsp", "plugin"],
11
+ "license": "MIT",
12
+ "dependencies": {
13
+ "@opencode-ai/plugin": "^1.0.0"
14
+ }
15
+ }
@@ -0,0 +1,99 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { readFileSync, writeFileSync } from "fs";
3
+
4
+ export function QmlFixTool({ client, $, directory }) {
5
+ async function fixVarToLet(content) {
6
+ return content.replace(
7
+ /\bvar\s+(\w+)/g,
8
+ (match, name) => {
9
+ return `let ${name}`;
10
+ }
11
+ );
12
+ }
13
+
14
+ async function fixRemoveUnusedImports(content) {
15
+ const lines = content.split("\n");
16
+ const importLines = [];
17
+ const usedModules = new Set();
18
+
19
+ lines.forEach((line, i) => {
20
+ const importMatch = line.match(/^import\s+(\S+)/);
21
+ if (importMatch) {
22
+ importLines.push({ index: i, module: importMatch[1] });
23
+ }
24
+ });
25
+
26
+ for (const { module } of importLines) {
27
+ const cleanModule = module.replace(/^["']|["']$/g, "");
28
+ if (cleanModule === "QtQuick") continue;
29
+ const pattern = new RegExp(`\\b${cleanModule.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
30
+ const contentWithoutImports = lines.filter((_, i) => !importLines.some(imp => imp.index === i)).join("\n");
31
+ if (pattern.test(contentWithoutImports)) {
32
+ usedModules.add(cleanModule);
33
+ }
34
+ }
35
+
36
+ const unusedImports = importLines.filter(({ module }) => {
37
+ const cleanModule = module.replace(/^["']|["']$/g, "");
38
+ return cleanModule !== "QtQuick" && !usedModules.has(cleanModule);
39
+ });
40
+
41
+ if (unusedImports.length === 0) return content;
42
+
43
+ const removeIndices = new Set(unusedImports.map(imp => imp.index));
44
+ return lines.filter((_, i) => !removeIndices.has(i)).join("\n");
45
+ }
46
+
47
+ async function fixQualifyAccess(content, rootId = "root") {
48
+ const propertiesToQualify = [
49
+ "zoomLevel", "panX", "panY", "artboardWidth", "artboardHeight",
50
+ "isSpacePressed", "isMultiDragging", "multiDragLastX", "multiDragLastY",
51
+ "multiDragInitiatorId"
52
+ ];
53
+
54
+ let result = content;
55
+ for (const prop of propertiesToQualify) {
56
+ const pattern = new RegExp(`(?<!\\.)\\b${prop}\\b(?!\\s*:)(?!\\.)`, "g");
57
+ result = result.replace(pattern, `${rootId}.${prop}`);
58
+ }
59
+ return result;
60
+ }
61
+
62
+ const fixers = {
63
+ "var-to-let": fixVarToLet,
64
+ "remove-unused-imports": fixRemoveUnusedImports,
65
+ "qualify-access": fixQualifyAccess,
66
+ };
67
+
68
+ const qmlFixTool = tool({
69
+ description: "Fix QML lint warnings in a file. Supports: var-to-let, remove-unused-imports, qualify-access",
70
+ args: {
71
+ filePath: tool.schema.string().describe("Path to the QML file to fix"),
72
+ fixTypes: tool.schema.array(tool.schema.enum(["var-to-let", "remove-unused-imports", "qualify-access"])).describe("Types of fixes to apply"),
73
+ },
74
+ async execute({ filePath, fixTypes }) {
75
+ try {
76
+ const content = readFileSync(filePath, "utf-8");
77
+ let result = content;
78
+
79
+ for (const fixType of fixTypes) {
80
+ const fixer = fixers[fixType];
81
+ if (fixer) {
82
+ result = await fixer(result);
83
+ }
84
+ }
85
+
86
+ if (result === content) {
87
+ return `No changes needed for ${filePath}.`;
88
+ }
89
+
90
+ writeFileSync(filePath, result);
91
+ return `Applied fixes to ${filePath}: ${fixTypes.join(", ")}`;
92
+ } catch (error) {
93
+ return `Error fixing file ${filePath}: ${error.message}`;
94
+ }
95
+ },
96
+ });
97
+
98
+ return { tool: qmlFixTool };
99
+ }
package/src/index.js ADDED
@@ -0,0 +1,44 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { QmlLspSetup } from "./setup.js";
3
+ import { QmlFixTool } from "./fix-tool.js";
4
+
5
+ /**
6
+ * opencode-qml-lsp — QML language server support for OpenCode
7
+ * @type {import('@opencode-ai/plugin').Plugin}
8
+ */
9
+ export const QmlLspPlugin = async (ctx) => {
10
+ const { project, client, $, directory } = ctx;
11
+
12
+ const setup = QmlLspSetup({ client, $, directory });
13
+ const fixTool = QmlFixTool({ client, $, directory });
14
+
15
+ return {
16
+ tool: {
17
+ qml_setup: tool({
18
+ description: "Run QML project setup: detect qmlls, generate .qmlint and LSP config for the current project",
19
+ args: {},
20
+ async execute(_args, context) {
21
+ const result = await setup.run();
22
+ if (!result.success) {
23
+ return `Setup failed: ${result.message}`;
24
+ }
25
+ return [
26
+ `QML setup complete!`,
27
+ ``,
28
+ `Found: ${result.qmllsPath}`,
29
+ `PySide6 QML import: ${result.qmlImportPath || "not found (using defaults)"}`,
30
+ `.qml files detected: ${result.qmlFileCount}`,
31
+ ``,
32
+ `Generated files:`,
33
+ result.generatedFiles.map(f => ` - ${f}`).join("\n"),
34
+ ``,
35
+ `Next step: Merge the "lsp" block from opencode-qml-lsp.config.json into your opencode.json`,
36
+ ].join("\n");
37
+ },
38
+ }),
39
+ qml_fix_warnings: fixTool.tool,
40
+ },
41
+ };
42
+ };
43
+
44
+ export default QmlLspPlugin;
package/src/setup.js ADDED
@@ -0,0 +1,108 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { execSync } from "child_process";
4
+
5
+ const QMLLINT_CONTENT = `[General]
6
+ MaxWarnings=-1
7
+
8
+ [Warnings]
9
+ UnqualifiedAccess=disable
10
+ BlockScopeVarDeclaration=disable
11
+ `;
12
+
13
+ const COMMON_QMLLS_PATHS = [
14
+ "~/.local/share/qmlls/files/qmlls",
15
+ "/usr/bin/qmlls",
16
+ "/usr/local/bin/qmlls",
17
+ ];
18
+
19
+ export function QmlLspSetup({ client, $$, directory }) {
20
+ function findQmlls() {
21
+ for (const path of COMMON_QMLLS_PATHS) {
22
+ const expanded = path.replace("~", process.env.HOME || "");
23
+ if (existsSync(expanded)) return expanded;
24
+ }
25
+
26
+ try {
27
+ const stdout = execSync("which qmlls", { encoding: "utf-8" }).trim();
28
+ if (stdout) return stdout;
29
+ } catch {}
30
+
31
+ try {
32
+ const stdout = execSync("python3 -c \"import PySide6; import os; print(os.path.join(os.path.dirname(PySide6.__file__), 'Qt', 'libexec', 'qmlls'))\"", { encoding: "utf-8" }).trim();
33
+ if (existsSync(stdout)) return stdout;
34
+ } catch {}
35
+
36
+ return null;
37
+ }
38
+
39
+ function findPySide6QmlPath() {
40
+ try {
41
+ const stdout = execSync("python3 -c \"import PySide6; import os; print(os.path.join(os.path.dirname(PySide6.__file__), 'Qt', 'qml'))\"", { encoding: "utf-8" }).trim();
42
+ if (existsSync(stdout)) return stdout;
43
+ } catch {}
44
+ return null;
45
+ }
46
+
47
+ function findQmlFiles() {
48
+ try {
49
+ const stdout = execSync(`find ${directory} -name "*.qml" -not -path "*/node_modules/*" -not -path "*/.git/*"`, { encoding: "utf-8" });
50
+ return stdout.trim().split("\n").filter(Boolean);
51
+ } catch {
52
+ return [];
53
+ }
54
+ }
55
+
56
+ function generateQmlint() {
57
+ return QMLLINT_CONTENT;
58
+ }
59
+
60
+ function generateLspConfig(qmllsPath, qmlImportPath) {
61
+ const importArgs = [];
62
+ if (qmlImportPath) importArgs.push(`-I${qmlImportPath}`);
63
+ importArgs.push(`-I${directory}`);
64
+
65
+ return {
66
+ lsp: {
67
+ qmlls: {
68
+ command: [qmllsPath, "-E", ...importArgs],
69
+ extensions: [".qml", ".qrc"],
70
+ },
71
+ },
72
+ };
73
+ }
74
+
75
+ function run() {
76
+ const qmlFiles = findQmlFiles();
77
+ if (qmlFiles.length === 0) {
78
+ return { success: false, message: "No .qml files found in this project." };
79
+ }
80
+
81
+ const qmllsPath = findQmlls();
82
+ if (!qmllsPath) {
83
+ return { success: false, message: "qmlls not found. Install PySide6 or qmlls first." };
84
+ }
85
+
86
+ const qmlImportPath = findPySide6QmlPath();
87
+
88
+ const qmllintIniPath = join(directory, ".qmllint.ini");
89
+ if (!existsSync(qmllintIniPath)) {
90
+ writeFileSync(qmllintIniPath, generateQmlint());
91
+ }
92
+
93
+ const lspConfig = generateLspConfig(qmllsPath, qmlImportPath);
94
+ const configPath = join(directory, "opencode-qml-lsp.config.json");
95
+ writeFileSync(configPath, JSON.stringify(lspConfig, null, 2));
96
+
97
+ return {
98
+ success: true,
99
+ qmllsPath,
100
+ qmlImportPath,
101
+ qmlFileCount: qmlFiles.length,
102
+ generatedFiles: [".qmllint.ini", "opencode-qml-lsp.config.json"],
103
+ instructions: `Merge the "lsp" block from opencode-qml-lsp.config.json into your opencode.json`,
104
+ };
105
+ }
106
+
107
+ return { run, findQmlls, findQmlFiles };
108
+ }