From 13e37816f84bf3c5ef15f4124614f588dc11d10c Mon Sep 17 00:00:00 2001 From: Pierre Gironde Date: Wed, 20 May 2026 23:08:18 +0200 Subject: [PATCH] feat: add virtual sensor support with navigation commands and YAML parsing --- vscode-appdaemon/package.json | 10 + vscode-appdaemon/src/extension.ts | 215 ++++++++++++++++++- vscode-appdaemon/vscode-appdaemon-0.1.0.vsix | Bin 26238 -> 29052 bytes 3 files changed, 223 insertions(+), 2 deletions(-) diff --git a/vscode-appdaemon/package.json b/vscode-appdaemon/package.json index 6460e94..add5e54 100644 --- a/vscode-appdaemon/package.json +++ b/vscode-appdaemon/package.json @@ -96,10 +96,20 @@ } ], "keybindings": [ + { + "command": "appdaemon.goToVirtualSensor", + "key": "ctrl+shift+v", + "mac": "cmd+shift+v" + }, { "command": "appdaemon.goToApp", "key": "ctrl+shift+a", "mac": "cmd+shift+a" + }, + { + "command": "appdaemon.goToVirtualSensor", + "key": "ctrl+shift+v", + "mac": "cmd+shift+v" } ] }, diff --git a/vscode-appdaemon/src/extension.ts b/vscode-appdaemon/src/extension.ts index 5002c71..6d670b0 100644 --- a/vscode-appdaemon/src/extension.ts +++ b/vscode-appdaemon/src/extension.ts @@ -75,6 +75,122 @@ async function findAllApps(): Promise { + const lines = text.split('\n'); + const entries: Array<{ sensorId: string; line: number; appName: string }> = []; + + let currentApp = ''; + let isVSApp = false; // module: virtualsensors + let vsSection: VSSectionType = ''; // active sub-section in VirtualSensorsApp at indent=2 + let inVSKey = false; // in virtual_sensors: sub-block of another app + let nestedSection: VSSectionType = ''; // active sub-section in virtual_sensors: at indent=4 + + for (let i = 0; i < lines.length; i++) { + const raw = lines[i]; + const trimmed = raw.trimStart(); + if (!trimmed || trimmed.startsWith('#')) { continue; } + const indent = raw.length - trimmed.length; + + if (indent === 0) { + const m = raw.match(/^([a-z][a-z0-9_]*):\s*$/); + currentApp = m ? m[1] : ''; + isVSApp = false; vsSection = ''; inVSKey = false; nestedSection = ''; + continue; + } + + if (indent === 2) { + vsSection = ''; inVSKey = false; nestedSection = ''; + if (/^\s+module:\s*virtualsensors/.test(raw)) { + isVSApp = true; + } else if (isVSApp) { + vsSection = parseVSSectionName(raw, 2); + } else { + if (/^\s+virtual_sensors:\s*$/.test(raw)) { inVSKey = true; } + } + continue; + } + + if (indent === 4) { + nestedSection = ''; + if (isVSApp && vsSection) { + const km = raw.match(/^\s+([a-z][a-z0-9_.]+):/); + if (km) { + const sensorId = mapVirtualSensorId(km[1], vsSection); + if (sensorId) { entries.push({ sensorId, line: i, appName: currentApp }); } + } + } else if (inVSKey) { + nestedSection = parseVSSectionName(raw, 4); + } + continue; + } + + if (indent === 6 && inVSKey && nestedSection) { + const km = raw.match(/^\s+([a-z][a-z0-9_.]+):/); + if (km) { + const sensorId = mapVirtualSensorId(km[1], nestedSection); + if (sensorId) { entries.push({ sensorId, line: i, appName: currentApp }); } + } + } + } + return entries; +} + +async function findAllVirtualSensors(): Promise { + const yamlFiles = await vscode.workspace.findFiles('**/*.yaml', '**/node_modules/**'); + const results: VirtualSensorEntry[] = []; + for (const uri of yamlFiles) { + let doc: vscode.TextDocument; + try { doc = await vscode.workspace.openTextDocument(uri); } catch { continue; } + const fileName = vscode.workspace.asRelativePath(uri); + for (const e of parseVirtualSensorsFromText(doc.getText())) { + results.push({ ...e, uri, fileName }); + } + } + return results; +} + // ── Symbol Providers ───────────────────────────────────────────────────────── class ADDocumentSymbolProvider implements vscode.DocumentSymbolProvider { @@ -97,9 +213,9 @@ class ADDocumentSymbolProvider implements vscode.DocumentSymbolProvider { class ADWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider { async provideWorkspaceSymbols(query: string): Promise { - const apps = await findAllApps(); const lq = query.toLowerCase(); - return apps + const [apps, sensors] = await Promise.all([findAllApps(), findAllVirtualSensors()]); + const appSymbols = apps .filter(a => !lq || a.appName.toLowerCase().includes(lq)) .map(a => new vscode.SymbolInformation( a.appName, @@ -107,6 +223,62 @@ class ADWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider { `module: ${a.moduleName}`, new vscode.Location(a.uri, new vscode.Position(a.line, 0)) )); + const sensorSymbols = sensors + .filter(s => !lq || s.sensorId.toLowerCase().includes(lq)) + .map(s => new vscode.SymbolInformation( + s.sensorId, + vscode.SymbolKind.Variable, + s.appName, + new vscode.Location(s.uri, new vscode.Position(s.line, 0)) + )); + return [...appSymbols, ...sensorSymbols]; + } +} + +class ADVirtualSensorDefinitionProvider implements vscode.DefinitionProvider { + // Simple cache: reset on save + private cache: VirtualSensorEntry[] | undefined; + private cacheExpiry = 0; + + constructor(private haClient: HAClient) {} + + invalidate() { this.cache = undefined; } + + private async getSensors(): Promise { + const now = Date.now(); + if (this.cache && now < this.cacheExpiry) { return this.cache; } + this.cache = await findAllVirtualSensors(); + this.cacheExpiry = now + 5000; // 5 s TTL + return this.cache; + } + + async provideDefinition( + document: vscode.TextDocument, + position: vscode.Position + ): Promise { + // Match any entity ID pattern (same as hover provider) + const wordRange = document.getWordRangeAtPosition( + position, + /[a-z][a-z_]*\.[a-z0-9][a-z0-9_]*/ + ); + if (!wordRange) { return undefined; } + const entityId = document.getText(wordRange); + + // 1. Check virtual sensor index (parsed from YAML) + const sensors = await this.getSensors(); + const vsMatch = sensors.find(s => s.sensorId === entityId); + if (vsMatch) { + return new vscode.Location(vsMatch.uri, new vscode.Position(vsMatch.line, 0)); + } + + // 2. Fallback: check HA entity's ad_app attribute → navigate to that app + const entity = this.haClient.getEntity(entityId); + const adApp = entity?.attributes?.ad_app as string | undefined; + if (!adApp) { return undefined; } + const apps = await findAllApps(); + const appMatch = apps.find(a => a.appName === adApp); + if (!appMatch) { return undefined; } + return new vscode.Location(appMatch.uri, new vscode.Position(appMatch.line, 0)); } } @@ -166,6 +338,15 @@ export async function activate(context: vscode.ExtensionContext) { { scheme: 'file', language: 'python' } ]; + const virtualSensorDefProvider = new ADVirtualSensorDefinitionProvider(haClient); + + // Invalidate definition cache on save so edits are picked up immediately + context.subscriptions.push( + vscode.workspace.onDidSaveTextDocument(doc => { + if (doc.languageId === 'yaml') { virtualSensorDefProvider.invalidate(); } + }) + ); + context.subscriptions.push( vscode.languages.registerCompletionItemProvider( selector, @@ -182,12 +363,42 @@ export async function activate(context: vscode.ExtensionContext) { ), vscode.languages.registerWorkspaceSymbolProvider( new ADWorkspaceSymbolProvider() + ), + vscode.languages.registerDefinitionProvider( + { scheme: 'file', language: 'yaml' }, + virtualSensorDefProvider ) ); // ── Commands ───────────────────────────────────────────────────────────── context.subscriptions.push( + vscode.commands.registerCommand('appdaemon.goToVirtualSensor', async () => { + const sensors = await findAllVirtualSensors(); + if (sensors.length === 0) { + vscode.window.showWarningMessage('AppDaemon: No virtual sensors found in workspace'); + return; + } + type SensorItem = vscode.QuickPickItem & { sensor: VirtualSensorEntry }; + const items: SensorItem[] = sensors.map(s => ({ + label: `$(circuit-board) ${s.sensorId}`, + description: s.fileName, + detail: `in ${s.appName}`, + sensor: s + })); + const picked = await vscode.window.showQuickPick(items, { + placeHolder: 'Go to virtual sensor…', + matchOnDescription: true, + matchOnDetail: true + }); + if (!picked) { return; } + const doc = await vscode.workspace.openTextDocument(picked.sensor.uri); + const editor = await vscode.window.showTextDocument(doc); + const pos = new vscode.Position(picked.sensor.line, 0); + editor.selection = new vscode.Selection(pos, pos); + editor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.InCenter); + }), + vscode.commands.registerCommand('appdaemon.goToApp', async () => { const apps = await findAllApps(); if (apps.length === 0) { diff --git a/vscode-appdaemon/vscode-appdaemon-0.1.0.vsix b/vscode-appdaemon/vscode-appdaemon-0.1.0.vsix index b233536fbe80ff531a0a68024a8ae35ae01457db..99eb3b60a165ecd44a6c6b7b1a570c573f217ece 100644 GIT binary patch delta 9924 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 F{|}BJ+Fk$v delta 7085 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 RggrjU#vL7&#PXk>{{pyJU7!E}