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 +6 -0
- package/.sisyphus/plans/qml-lsp-plugin.md +203 -0
- package/README.md +94 -0
- package/opencode.json +7 -0
- package/package.json +15 -0
- package/src/fix-tool.js +99 -0
- package/src/index.js +44 -0
- package/src/setup.js +108 -0
package/.qmllint.ini
ADDED
|
@@ -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
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
|
+
}
|
package/src/fix-tool.js
ADDED
|
@@ -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
|
+
}
|