|
| 1 | +# Proposal: Multi-Project Tree View |
| 2 | + |
| 3 | +**Status:** Draft |
| 4 | +**Date:** 2026-04-06 |
| 5 | + |
| 6 | +## Motivation |
| 7 | + |
| 8 | +Mendix solutions increasingly span multiple applications: a frontend and a backend, or a microservices landscape (product catalog, orders, fulfillment). Another use case is monolith-to-multi-app refactoring — start with one large app and use AI to split it into smaller apps. |
| 9 | + |
| 10 | +Today the VS Code extension assumes a single `.mpr` project per workspace. Supporting multiple projects in the tree view is the first step toward multi-app development workflows. |
| 11 | + |
| 12 | +## Current State |
| 13 | + |
| 14 | +The single-project assumption runs through five layers: |
| 15 | + |
| 16 | +| Layer | File | Assumption | |
| 17 | +|-------|------|-----------| |
| 18 | +| **Setting** | `package.json` | `mdl.mprPath` is a single `string` | |
| 19 | +| **Discovery** | `extension.ts` `findMprPath()` | Returns first `.mpr` only | |
| 20 | +| **Tree provider** | `projectTreeProvider.ts` | One `mprPath`, one `treeData[]` | |
| 21 | +| **CLI** | `project_tree.go` | Accepts one `-p` flag, returns flat array | |
| 22 | +| **LSP** | `lsp.go` | `mprPath string` on server struct | |
| 23 | + |
| 24 | +## Design |
| 25 | + |
| 26 | +### Principle: project as root node |
| 27 | + |
| 28 | +Each `.mpr` project becomes a collapsible root node in the tree. Modules, domain models, and documents nest underneath their project. This mirrors Studio Pro's project explorer for multi-app solutions. |
| 29 | + |
| 30 | +``` |
| 31 | +Mendix Projects |
| 32 | +├── ProductCatalog (ProductCatalog.mpr) |
| 33 | +│ ├── Settings |
| 34 | +│ ├── Navigation |
| 35 | +│ ├── Project Security |
| 36 | +│ ├── Catalog |
| 37 | +│ │ ├── Domain Model |
| 38 | +│ │ ├── Pages |
| 39 | +│ │ └── Microflows |
| 40 | +│ └── ... |
| 41 | +├── OrderService (OrderService.mpr) |
| 42 | +│ ├── Settings |
| 43 | +│ ├── Navigation |
| 44 | +│ ├── Project Security |
| 45 | +│ ├── Orders |
| 46 | +│ │ ├── Domain Model |
| 47 | +│ │ └── Microflows |
| 48 | +│ └── ... |
| 49 | +└── Fulfillment (Fulfillment.mpr) |
| 50 | + └── ... |
| 51 | +``` |
| 52 | + |
| 53 | +When the workspace has only one `.mpr` file, the project root node is omitted and the tree looks exactly like today (no visual change for the common case). |
| 54 | + |
| 55 | +### Changes by layer |
| 56 | + |
| 57 | +#### 1. Project discovery (`extension.ts`) |
| 58 | + |
| 59 | +Replace `findMprPath()` (returns first match) with `findAllMprPaths()` (returns all `.mpr` files): |
| 60 | + |
| 61 | +```typescript |
| 62 | +async function findAllMprPaths(): Promise<string[]> { |
| 63 | + const config = vscode.workspace.getConfiguration('mdl'); |
| 64 | + const configured = config.get<string[]>('mprPaths', []); |
| 65 | + if (configured.length > 0) { |
| 66 | + return configured; |
| 67 | + } |
| 68 | + // Auto-discover: find all .mpr files, one level deep per workspace folder |
| 69 | + const files = await vscode.workspace.findFiles('*/*.mpr', '**/node_modules/**', 20); |
| 70 | + return files.map(f => f.fsPath); |
| 71 | +} |
| 72 | +``` |
| 73 | + |
| 74 | +Discovery looks for `<workspace>/<app-dir>/<name>.mpr` — one level deep matches the expected layout where each project has its own subdirectory. |
| 75 | + |
| 76 | +#### 2. Setting (`package.json`) |
| 77 | + |
| 78 | +Add a new array setting alongside the existing one (backward compatible): |
| 79 | + |
| 80 | +```jsonc |
| 81 | +"mdl.mprPaths": { |
| 82 | + "type": "array", |
| 83 | + "items": { "type": "string" }, |
| 84 | + "default": [], |
| 85 | + "description": "Paths to Mendix .mpr files. If empty, auto-discovers in workspace." |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +The existing `mdl.mprPath` (singular) continues to work for single-project workspaces. |
| 90 | + |
| 91 | +#### 3. Tree provider (`projectTreeProvider.ts`) |
| 92 | + |
| 93 | +The tree provider manages multiple projects: |
| 94 | + |
| 95 | +```typescript |
| 96 | +interface ProjectTree { |
| 97 | + mprPath: string; |
| 98 | + name: string; // derived from .mpr filename |
| 99 | + treeData: MendixTreeNode[]; |
| 100 | +} |
| 101 | + |
| 102 | +class MendixProjectTreeProvider { |
| 103 | + private projects: ProjectTree[] = []; |
| 104 | + |
| 105 | + async refresh(): Promise<void> { |
| 106 | + const paths = await findAllMprPaths(); |
| 107 | + this.projects = []; |
| 108 | + for (const mprPath of paths) { |
| 109 | + const treeData = await this.loadProjectTree(mprPath); |
| 110 | + this.projects.push({ |
| 111 | + mprPath, |
| 112 | + name: path.basename(mprPath, '.mpr'), |
| 113 | + treeData, |
| 114 | + }); |
| 115 | + } |
| 116 | + this._onDidChangeTreeData.fire(undefined); |
| 117 | + } |
| 118 | + |
| 119 | + getChildren(element?: MendixTreeNode): MendixTreeNode[] { |
| 120 | + if (!element) { |
| 121 | + // Root level |
| 122 | + if (this.projects.length === 1) { |
| 123 | + // Single project: flat (same as today) |
| 124 | + return this.projects[0].treeData; |
| 125 | + } |
| 126 | + // Multiple projects: project root nodes |
| 127 | + return this.projects.map(p => ({ |
| 128 | + label: p.name, |
| 129 | + type: 'project', |
| 130 | + qualifiedName: p.mprPath, |
| 131 | + children: p.treeData, |
| 132 | + })); |
| 133 | + } |
| 134 | + return element.children ?? []; |
| 135 | + } |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +**Key property**: Each tree node gains an implicit project context through its ancestor project node. Commands look up the tree to find which project a node belongs to. |
| 140 | + |
| 141 | +#### 4. Command context |
| 142 | + |
| 143 | +Tree item click handlers (DESCRIBE, context menu commands) need the project path. Store it on the node: |
| 144 | + |
| 145 | +```typescript |
| 146 | +interface MendixTreeNode { |
| 147 | + label: string; |
| 148 | + type: string; |
| 149 | + qualifiedName?: string; |
| 150 | + children?: MendixTreeNode[]; |
| 151 | + projectPath?: string; // NEW: which .mpr this node belongs to |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +The `projectPath` is set during tree construction and passed as `-p` when invoking `mxcli` commands. |
| 156 | + |
| 157 | +#### 5. CLI (`project_tree.go`) |
| 158 | + |
| 159 | +No changes needed. The extension calls `mxcli project-tree -p <path>` once per project and assembles the combined tree client-side. This avoids coupling the CLI to multi-project concerns. |
| 160 | + |
| 161 | +#### 6. LSP server (future, out of scope) |
| 162 | + |
| 163 | +The LSP server currently binds to one project. For multi-project, the extension could spawn one LSP server per project (each with its own `-p` flag). This is a larger change and is deferred — the tree view works without LSP changes since it uses `mxcli project-tree` directly. |
| 164 | + |
| 165 | +## Implementation Plan |
| 166 | + |
| 167 | +### Phase 1: Multi-project tree view |
| 168 | + |
| 169 | +1. Add `findAllMprPaths()` to `extension.ts` |
| 170 | +2. Add `mdl.mprPaths` array setting to `package.json` |
| 171 | +3. Refactor `MendixProjectTreeProvider` to hold `ProjectTree[]` |
| 172 | +4. Add `projectPath` to `MendixTreeNode` interface |
| 173 | +5. Update `getChildren()` for single-project-flat vs multi-project-nested |
| 174 | +6. Update command handlers to read `projectPath` from tree node context |
| 175 | +7. Add `project` type icon (`symbol-namespace`) |
| 176 | + |
| 177 | +### Phase 2: Project-aware commands (future) |
| 178 | + |
| 179 | +8. Context menu "Open in Terminal" scoped to project directory |
| 180 | +9. DESCRIBE / SHOW commands pass correct `-p` per project |
| 181 | +10. MDL script execution targets the right project |
| 182 | + |
| 183 | +### Phase 3: Multi-project LSP (future) |
| 184 | + |
| 185 | +11. Spawn separate LSP server per project |
| 186 | +12. Route diagnostics/completions to correct server based on file location |
| 187 | + |
| 188 | +### Phase 4: Cross-project catalog queries (future) |
| 189 | + |
| 190 | +13. Use SQLite `ATTACH DATABASE` to mount each project's catalog under an alias |
| 191 | +14. Enable cross-project queries: `SELECT * FROM orders.catalog.entities JOIN catalog.catalog.entities ON ...` |
| 192 | +15. `SHOW CALLERS OF Module.Microflow ACROSS PROJECTS` for cross-project dependency analysis |
| 193 | + |
| 194 | +SQLite natively supports attaching multiple databases to a single connection. Each project's catalog (built by `REFRESH CATALOG`) is a standalone `.db` file. By attaching them with project-scoped aliases, the existing `SELECT ... FROM CATALOG.*` syntax extends naturally to cross-project joins without a new query engine. |
| 195 | + |
| 196 | +```sql |
| 197 | +-- Attach project catalogs |
| 198 | +ATTACH 'orders/.mxcli/catalog.db' AS orders; |
| 199 | +ATTACH 'fulfillment/.mxcli/catalog.db' AS fulfillment; |
| 200 | + |
| 201 | +-- Cross-project query: which entities exist in both? |
| 202 | +SELECT o.name, f.name |
| 203 | +FROM orders.entities o |
| 204 | +JOIN fulfillment.entities f ON o.name = f.name; |
| 205 | +``` |
| 206 | + |
| 207 | +### Phase 5: Cross-project operations (future) |
| 208 | + |
| 209 | +16. MOVE entity/microflow between projects |
| 210 | +17. Dependency visualization (which project calls which) |
| 211 | +18. Shared module extraction |
| 212 | + |
| 213 | +## Single-Project Backward Compatibility |
| 214 | + |
| 215 | +When only one `.mpr` is found: |
| 216 | +- Tree looks identical to today (no wrapping project node) |
| 217 | +- `mdl.mprPath` (singular) still works |
| 218 | +- All commands behave exactly as before |
| 219 | +- No new UI elements shown |
| 220 | + |
| 221 | +## Scope Exclusions |
| 222 | + |
| 223 | +- **Multi-project LSP**: Out of scope — deferred to Phase 3 |
| 224 | +- **Cross-project references**: Out of scope — deferred to Phase 4 |
| 225 | +- **Shared module management**: Out of scope |
| 226 | +- **Multi-project MDL scripts**: Out of scope (scripts already target one `-p` at a time) |
| 227 | + |
| 228 | +## Effort Estimate |
| 229 | + |
| 230 | +- Phase 1: Medium — ~200 lines TypeScript, no Go changes |
| 231 | +- Phase 2: Small — command handler updates |
| 232 | +- Phase 3: Medium — LSP spawning and routing |
| 233 | +- Phase 4: Large — new CLI commands and BSON operations |
0 commit comments