feat: add 'Go to App' command and enhance YAML app parsing

This commit is contained in:
2026-04-17 19:21:17 +02:00
parent da282058d8
commit abdc962500
3 changed files with 133 additions and 12 deletions

View File

@@ -51,6 +51,11 @@
} }
}, },
"commands": [ "commands": [
{
"command": "appdaemon.goToApp",
"title": "AppDaemon: Go to App",
"icon": "$(symbol-class)"
},
{ {
"command": "appdaemon.restartCurrentFileApps", "command": "appdaemon.restartCurrentFileApps",
"title": "AppDaemon: Restart Apps in Current File", "title": "AppDaemon: Restart Apps in Current File",
@@ -85,6 +90,13 @@
"command": "appdaemon.clearErrors", "command": "appdaemon.clearErrors",
"title": "AppDaemon: Clear Error Diagnostics" "title": "AppDaemon: Clear Error Diagnostics"
} }
],
"keybindings": [
{
"command": "appdaemon.goToApp",
"key": "ctrl+shift+a",
"mac": "cmd+shift+a"
}
] ]
}, },
"scripts": { "scripts": {

View File

@@ -17,28 +17,101 @@ const NON_APP_KEYS = new Set([
'appdaemon', 'http', 'hadashboard', 'admin', 'api', 'plugins' 'appdaemon', 'http', 'hadashboard', 'admin', 'api', 'plugins'
]); ]);
/** // ── App parsing helpers ───────────────────────────────────────────────────────
* Parse AppDaemon app names from a YAML document.
* Returns only top-level keys whose block contains a `module:` line. type AppEntry = { appName: string; line: number; moduleName: string };
*/
function parseAppsFromDocument(doc: vscode.TextDocument): string[] { function parseAppEntries(text: string): AppEntry[] {
if (doc.languageId !== 'yaml') { return []; } const lines = text.split('\n');
const lines = doc.getText().split('\n'); const entries: AppEntry[] = [];
const apps: string[] = [];
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const m = lines[i].match(/^([a-z][a-z0-9_]*):\s*$/); const m = lines[i].match(/^([a-z][a-z0-9_]*):\s*$/);
if (!m) { continue; } if (!m) { continue; }
const key = m[1]; const key = m[1];
if (NON_APP_KEYS.has(key)) { continue; } 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++) { 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/)) { break; }
if (lines[j].match(/^\s+module\s*:/)) { apps.push(key); 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<Array<{
appName: string;
uri: vscode.Uri;
line: number;
fileName: string;
moduleName: string;
}>> {
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<vscode.SymbolInformation[]> {
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) { function updateContextualButton(editor: vscode.TextEditor | undefined) {
if (!editor) { statusBar.updateContextualApps([]); return; } if (!editor) { statusBar.updateContextualApps([]); return; }
const apps = parseAppsFromDocument(editor.document); const apps = parseAppsFromDocument(editor.document);
@@ -102,12 +175,48 @@ export async function activate(context: vscode.ExtensionContext) {
vscode.languages.registerHoverProvider( vscode.languages.registerHoverProvider(
selector, selector,
new EntityHoverProvider(haClient) new EntityHoverProvider(haClient)
),
vscode.languages.registerDocumentSymbolProvider(
{ scheme: 'file', language: 'yaml' },
new ADDocumentSymbolProvider()
),
vscode.languages.registerWorkspaceSymbolProvider(
new ADWorkspaceSymbolProvider()
) )
); );
// ── Commands ───────────────────────────────────────────────────────────── // ── Commands ─────────────────────────────────────────────────────────────
context.subscriptions.push( 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 () => { vscode.commands.registerCommand('appdaemon.restartCurrentFileApps', async () => {
const editor = vscode.window.activeTextEditor; const editor = vscode.window.activeTextEditor;
if (!editor) { if (!editor) {