diff --git a/vscode-appdaemon/package.json b/vscode-appdaemon/package.json index dc69b92..f45feec 100644 --- a/vscode-appdaemon/package.json +++ b/vscode-appdaemon/package.json @@ -51,6 +51,11 @@ } }, "commands": [ + { + "command": "appdaemon.goToApp", + "title": "AppDaemon: Go to App", + "icon": "$(symbol-class)" + }, { "command": "appdaemon.restartCurrentFileApps", "title": "AppDaemon: Restart Apps in Current File", @@ -85,6 +90,13 @@ "command": "appdaemon.clearErrors", "title": "AppDaemon: Clear Error Diagnostics" } + ], + "keybindings": [ + { + "command": "appdaemon.goToApp", + "key": "ctrl+shift+a", + "mac": "cmd+shift+a" + } ] }, "scripts": { diff --git a/vscode-appdaemon/src/extension.ts b/vscode-appdaemon/src/extension.ts index 02b7fb3..2c585d7 100644 --- a/vscode-appdaemon/src/extension.ts +++ b/vscode-appdaemon/src/extension.ts @@ -17,28 +17,101 @@ const NON_APP_KEYS = new Set([ 'appdaemon', 'http', 'hadashboard', 'admin', 'api', 'plugins' ]); -/** - * Parse AppDaemon app names from a YAML document. - * Returns only top-level keys whose block contains a `module:` line. - */ -function parseAppsFromDocument(doc: vscode.TextDocument): string[] { - if (doc.languageId !== 'yaml') { return []; } - const lines = doc.getText().split('\n'); - const apps: string[] = []; +// ── App parsing helpers ─────────────────────────────────────────────────────── + +type AppEntry = { appName: string; line: number; moduleName: string }; + +function parseAppEntries(text: string): AppEntry[] { + const lines = text.split('\n'); + const entries: AppEntry[] = []; for (let i = 0; i < lines.length; i++) { const m = lines[i].match(/^([a-z][a-z0-9_]*):\s*$/); if (!m) { continue; } const key = m[1]; if (NON_APP_KEYS.has(key)) { continue; } - // Scan the indented block below for a `module:` key + let moduleName = ''; for (let j = i + 1; j < Math.min(i + 20, lines.length); j++) { - if (lines[j].match(/^\S/)) { break; } // end of block - if (lines[j].match(/^\s+module\s*:/)) { apps.push(key); break; } + if (lines[j].match(/^\S/)) { break; } + const mod = lines[j].match(/^\s+module\s*:\s*(\S+)/); + if (mod) { moduleName = mod[1]; entries.push({ appName: key, line: i, moduleName }); break; } } } - return apps; + return entries; } +/** + * Parse AppDaemon app names from a YAML document. + */ +function parseAppsFromDocument(doc: vscode.TextDocument): string[] { + if (doc.languageId !== 'yaml') { return []; } + return parseAppEntries(doc.getText()).map(e => e.appName); +} + +/** + * Scan all YAML files in the workspace and return app definitions with location. + */ +async function findAllApps(): Promise> { + const yamlFiles = await vscode.workspace.findFiles('**/*.yaml', '**/node_modules/**'); + const results: Array<{ appName: string; uri: vscode.Uri; line: number; fileName: string; moduleName: string }> = []; + + 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 entry of parseAppEntries(doc.getText())) { + results.push({ ...entry, uri, fileName }); + } + } + return results; +} + +// ── Symbol Providers ───────────────────────────────────────────────────────── + +class ADDocumentSymbolProvider implements vscode.DocumentSymbolProvider { + provideDocumentSymbols(doc: vscode.TextDocument): vscode.DocumentSymbol[] { + if (doc.languageId !== 'yaml') { return []; } + return parseAppEntries(doc.getText()).map(entry => { + const pos = new vscode.Position(entry.line, 0); + const range = new vscode.Range(pos, pos); + const sym = new vscode.DocumentSymbol( + entry.appName, + `module: ${entry.moduleName}`, + vscode.SymbolKind.Class, + range, + range + ); + return sym; + }); + } +} + +class ADWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider { + async provideWorkspaceSymbols(query: string): Promise { + const apps = await findAllApps(); + const lq = query.toLowerCase(); + return apps + .filter(a => !lq || a.appName.toLowerCase().includes(lq)) + .map(a => new vscode.SymbolInformation( + a.appName, + vscode.SymbolKind.Class, + `module: ${a.moduleName}`, + new vscode.Location(a.uri, new vscode.Position(a.line, 0)) + )); + } +} + +// ───────────────────────────────────────────────────────────────────────────── + function updateContextualButton(editor: vscode.TextEditor | undefined) { if (!editor) { statusBar.updateContextualApps([]); return; } const apps = parseAppsFromDocument(editor.document); @@ -102,12 +175,48 @@ export async function activate(context: vscode.ExtensionContext) { vscode.languages.registerHoverProvider( selector, new EntityHoverProvider(haClient) + ), + vscode.languages.registerDocumentSymbolProvider( + { scheme: 'file', language: 'yaml' }, + new ADDocumentSymbolProvider() + ), + vscode.languages.registerWorkspaceSymbolProvider( + new ADWorkspaceSymbolProvider() ) ); // ── Commands ───────────────────────────────────────────────────────────── context.subscriptions.push( + vscode.commands.registerCommand('appdaemon.goToApp', async () => { + const apps = await findAllApps(); + if (apps.length === 0) { + vscode.window.showWarningMessage('AppDaemon: No app definitions found in workspace'); + return; + } + + type AppItem = vscode.QuickPickItem & { app: typeof apps[0] }; + const items: AppItem[] = apps.map(app => ({ + label: `$(symbol-class) ${app.appName}`, + description: app.fileName, + detail: `Line ${app.line + 1}`, + app + })); + + const picked = await vscode.window.showQuickPick(items, { + placeHolder: 'Go to AppDaemon app…', + matchOnDescription: true, + matchOnDetail: false + }); + + if (!picked) { return; } + const doc = await vscode.workspace.openTextDocument(picked.app.uri); + const editor = await vscode.window.showTextDocument(doc); + const pos = new vscode.Position(picked.app.line, 0); + editor.selection = new vscode.Selection(pos, pos); + editor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.InCenter); + }), + vscode.commands.registerCommand('appdaemon.restartCurrentFileApps', async () => { const editor = vscode.window.activeTextEditor; if (!editor) { diff --git a/vscode-appdaemon/vscode-appdaemon-0.1.0.vsix b/vscode-appdaemon/vscode-appdaemon-0.1.0.vsix index dc9d822..e40e917 100644 Binary files a/vscode-appdaemon/vscode-appdaemon-0.1.0.vsix and b/vscode-appdaemon/vscode-appdaemon-0.1.0.vsix differ