From abdc962500031fb4e310e19cefd43857ae00def0 Mon Sep 17 00:00:00 2001 From: Pierre Gironde Date: Fri, 17 Apr 2026 19:21:17 +0200 Subject: [PATCH] feat: add 'Go to App' command and enhance YAML app parsing --- vscode-appdaemon/package.json | 12 ++ vscode-appdaemon/src/extension.ts | 133 +++++++++++++++++-- vscode-appdaemon/vscode-appdaemon-0.1.0.vsix | Bin 24519 -> 25936 bytes 3 files changed, 133 insertions(+), 12 deletions(-) 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 dc9d822f9c884250711004c906ccf2e4a5255b85..e40e917dc60527f9af22e3d30f0c2d28f0f120a0 100644 GIT binary patch delta 7714 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 ZMTpdu5&sar008!%Px;SIf1Cf6{ui}=DZ~H( delta 6304 zcmaKRbx>T*v-YyM1b270V8PutI0OyBT@#$ZLSRF1S=?oDXG0)3OOW8fB|r%7?)D|` zckiwDcmKFur|Qhp(=**Ob!NKjIpc#!{hdf;+8|^UVgLXG1K{YIXht35=iv_o0APt^ zT;Rxp2^bbC=Tr@y$$Y~0t?16D_+QiYJd0|U;D=F6n6@{a>lMDdrmxIO6CCbcZWKET zDpL7Qf53+0?G`@O%U+IX@XgGQm7%s39H7hZ;&Aw?NH_N|y3Tx{!h~>;QGdlS$FL>+ z#u8JS$M!Jh#Zba#$vV(kzprqu@y(S94Nvd{9nP0cg%dPct^72Io7nn}jz}*62>cdb zw6C0xE8sH6vt3w-uZ+37+~l8lNGd_9eoNA#11uzq(ZPmmlDqGaO_-u)@oE3|vpP^h zwgj!sHF6|sw53Uh;8Lv2x@i2^(p->@=sk{DmWRq9k%3V~Q>kdEG2`b&bjv;zco{ZZry_Cgn&lua$RL_j)pim`($$(W0b+dGFwsRk@D3KJFN)^vrye_ zku=T)A&McvVEP!R*xlh?;Kv+CwmG4Tbf1fkv<{#!UP=P1R4HR|o)?3#E=1NKx$G$H z{M@<<9}E_DpwaGUe3P%F@sJ38L31V5CCc)IdT#9WBa}8a(4l4R**8;YSdxs+{=!pG zsFM*pF%ef$HG^xmB?W&rz@F zhl_%5e$FP2IpR11CNrR7u4B0VJNQJ5v@JO&_F=YlK>{6}9Su|(VKu~(8e1LfxMrcN z?i8At9s-ZqDF;p9N`*VD9-oq|r+qi;ld+4%*O;F<8LAvw4+GK{FGp3u*Ud(3^5aft z^`W?DKSgJAUPg+wIrVh{{{4|byMQ}DEmvqIGACHaeUcwPc%@dXQj8s#=`WqXMri1^Hg)JltVez>FY8VlkBc-f={B zFGuSpoP=;4c(W5-&I(v~V#MOv&785_y&-iTnxbRL?$5?OxnjuHE&J*F(TYV#IPuJ# zcfgynb{#%-R;10ZmM4CbV~_lP%;}~`2@|%S&T%73uW+q z=B#1^y;_(DOQJ*02K1L@F_;D4fPq;2L+~V+?&%wC3TV{yE`u>qfb~-Yy-pqc4(N^JHAUUBBL17pkZT=>PZ{ zZ4@_Kl`7p;d}Sw{qdtS`V4)-1HJ3I;HGaSU2ahlQ+? z|A?I6^hNGg=~;Ot9a~&s71~7eodtt7(3G^6Oj_p3fYwUvz>XQ|8y2;pT z17Xav`MXcc^Yi`tHM64G0lHiyn9eRoZT7(ud-oZVc!ql?R=L@qjZR(f`Cn=#t@;RN z4@Cq*y%9+lza70_+_xCH{K{#6!^YOh5*Rpgv0I~tljVIX0on|s=sW(qGzyTH~^MKmBFkeHM<9P+qj;@WCKSf&9dcga#G(6tX!^r(7iT9r)=klI< z>Bv%{(qZ}Z-B9d#N^4L^_Fata1Zvfa$>f(_nIL$!_|DH&YiT^Bi8dWONcK^;$;^N+ z(z4$)NN)wb@-Sz|g(a+lW1^dqV|7PWIEF%Vq<{})j&;9Hc>M=pyQUZTsEnvl=us`d z@6~n4vfqdf17pT?QiP9*t==3LCS2zH<}4ygBbiVs_sT`>YoHo+PX;z16<@BKHpm6q zd3~s2qCvM&4rQL+(gP*PA0SAq5sv$mez)I(E1Dw{KPLd+E1SFr8xn5>u%-rR#E3PI zM;m_HcHTAOL$wVB#pQOi^~PM7r5aRtp)*9At?x$is#bmJdQxSsNS03~8&`{dN%fjy za&Wtkbdm=mIv`Kasj5UrSjE(=0Q|&=*)WpKxgmb?OpHz)wQ`AMJ2$q1{;86Ypb>RV zW8=P8H^YgL3+MTFFpz+r&F6$tu||(-OvNT=J?>fk7o}J4TLF(s+dAXFf=%h{f@Mf) zle9HM+qZmA1uC_vg{1B~3A`Kua@33&7PAqP)*b63^u_7k2=WYt*Ql_Gxf5>b7V0I~2H`jN2cAvy(qz)coVL_mNq5XI zq@uOit8Mb3UPJNuEUL&Uc=J@3-~sg0;)xaYV~SGNw~Ta-2Nu- z6!VPuP|iNW!I7#M?M5DRf0RTb7C9{ZZae@1pXjA;GqFQGmu=$_`Fn#SH(gY! z)XYUJeZ3^HFsc5UYgV2$TIVtVnhrv!ajnYR=?#dIe71P_tZV>n|1Nge{>Y2Vsu1Bh z7#Aw}ihc980$WmKUZ(?#lSXS-q|DB1)a#u@ijS)zSQL*4!u4Pukh~b$tDDE?aMk_o z=qI)QZa+T6dZ)kvlgaq~^flwYKcbRgCzAsLj2A_hUjSy=@kWIDnyO1CpL3(g#bWCS z(8{L*{T;$`={8$iLGlFaiJ1s*&TKI}F%4D||BGA-SDh+F9jPH%wI}M7RiE%^{g0JJ z{91$Hr(UPqj<0WWte{%+~|G}%LKVpJj4>$=X_fzpSThn<9tw{`77>L$A16!QQ+x6{z zIL(15g!q0A(v&Gt!P6t@ZN#y=WaQ?3`pj&6xCa8rRUHm6b)xvnYz`3(_(k=)ti&1{ ztr+=SSQJYGuF*8jki*uBeUHmCy1j|Py>zQb06Am-H$yeqV>n{8%3yjy8lA!6EOEQ2 zpsuP>J#xY8r|;UkdhF|}qoC{WEZ(?2#c$Zx0p@3!m!+qB2D>il-Oh1$>CDj1N3_3* zqd3)R^&2Vx;7|2`B@W8}pE#hkY-HeRlcTqbB7{$erABuGBpd8rN$zZh7wkR)--R9p z>jQJ**5C|;N#u!uctG{7*~s~%RF_5`zt-DUz*9)F)sWgENSG3YLK?l)d+6@V;Jy(R zHepP>UA+|RoZ7u%o8Vb;6RjGanBE1Dl!_&OfA(y=bS3i$!Pgn$D|y?6d;nggm_v5Y zj+Rx?^=m*^!?U{%UWP0_>oOEVbHAiU3qx9GTwhZ?uEQ1r!e1-My$v@*71i~~^$3zA zW6OByU*eAI!5gj1BN(6V>&w-VWk(VD83Ou3Pni0H)`gW_+%Q65*cvxTaDNQ03MY_W z!xF?-eICc^uN=0dYrziriVEgE4bPgF-sawt>T$! zw?S*SiNwUtv_0ijo1JGI-fSKFv$~{q$=d4#pIJpyCsLO3;q&so2wQN@u3DwWcbc{& z;pd6(PNX_@nmd-;eSjXDhAkTepk~HP;j6yWw^U54H@$AwR@{Qe4nE47(R z@C;*BpX{7iJ2%h!dxg57HLiFs4s03|1Ta&X;FEbdtECoou^jPIjB378HuaK%8Pnk0 z+rL$UFz76PS5zWECdOyQ!IIA__)*KTjiw!3&8Bepau%XQvvy{8e&Mpe%ZBcJnW zuqY6C%(yz;T0t|j_t@UtZ z6TL2cAF{oOpRIJJ}f)fJ}?mZ%WAl-5f0Aed6x(BggQA`mC9ZK$&LiuHslx2XD!e& z#gQSs()%@|E_Rtbb(*J^8#9|rg5+bo^*Lw7aS+U_Y zc?r@<-##aTW@}t$jVtVxq|)+?D-htszY@w$-_;Y$&9Dqxvs>0MQW&p`zs zCt6Y!$=~(aRCmqxF;x1|T#G@ntBig)V?HN)O&$Z_^C^|9bBFz#N%Nlk*Dyzu&-#4} zX9})kSEUl5!ExvCvK94@*b^w$SnG39ooK!nb54+msWkGB->M`lXnHRg%eZ0$;{8L5`GmWz@2(eXuRmIceZx0m7a1iv)sg1d zd^&PBt!>%)fb|OM{vg&Dmrd4qUJqLH=Dg?eEbH!32`gCXP&)EbMdJAed5aTq5qor} z!47YaWr7UbaoQrW5&z_)qSLz&7u%5Lr~wdxckzs4gpmEboL?}+nfJ%|wnSGp`elEo zAmj2&_Z&3-bQCx~zr5Txf%D{3r|4MHm zJu1h4-kRBvZ{(AL^$W1~z_!M|IYnbJUu|h~d^>i=$h5$gq)ZfryF<%(gK|-%S zHIQz2!Ha>b6y;o#_M86Cb@Gwt)v^;3xDH~9VCdOMo}YHRQP~=Id2vJvu2omqO!~$_ zKQxq{}SW}Ua-TZ z13x??66tsPQG)Z4#0c}A+$_0K-yV|4MU`heWoIE-;2I`5v>h^uGbCUt%sjQ1X=m#< zF3an@4~-&hb&F%gd+EBB!Qbh9#bI^h`op%1oX9&)2PbzYl(y%4Y;GamQ#oe0+D25r zq-+Fi6xJ}q!!G28c+Nw(Yca?TZiNN^pzkhhqV;x~+9O8ysOKe5rcTZ~QG*Gol)cpD zzDYcd-Bg?Z8d!WUl|^cz(dl0JyTshInkx*wyJ*dK8|Fo-!B3Iz!SY-rcU$u-Las%i zC|59f!9m-4v;kJTAv{LQYVl5uWw9bVfq8omJb!CBvWABxyfj&gRGNJbj@EB>54$1> zpuIra4*G(`ZdE@$wlHH=9_-fhuF})|Fm!v`rl91lIbept5K9WdQF-UV>?#+FA^Ut{ zcju=_Ic9p^5^oA3>s5nnz@P=l=*dEOf_PZT#%A={T`_g{3&PD0_n`}pimN3;E~ACF zoYj&xO+BrqmoI>KP2bLG!NvE6arZ~uGQ;%oZn3t~0~lGvD4{Jz?lb@f3DyykG%7Ee zqOxa&TsZ4oK;C`}EQx)-<~$>$x>ivo4+0{aw|UogCo~^=(hEXe9|OIjTWb>nbiyW) ze>?ace;+qqOJ?WEQn<}VeoGUR9xr}#r%(7<8JGlJBuD~oJeYi7gPtDqG7>G-DA{OJyfPB)n;)S06}x-uvH zcd6C3S_=O7^^vmIF&`rK)<}_Ob`Y6V6Y7R^gBJT>5}v6&cS~x)7v|2N^L9vpq2GMDpMp^ zIH52b>K{e|E+MXr`S%^Bfby5UfJdqo|AYDUNdD_6CTD2C;WKbBBAOKM;> z5IPPd;nD^n0cDVh{}ACA0Ifev_`jJH(tmL$u+Vg9o+S(Y-}yEa$k@^Uuq{~60D!-o z%6|(ilm#8iZ$+JCg-`d_4$C5^Km-i{$Rz;){+7`HEwE5OD4!L_-$+cXsDYRGP(LfK ze?|~YO3*rC003qb0KlK;e*_lF3`JP6|Kt7NCHsGRpQeRkS#$qm^zYsKPuCmF&63apc0`!=+A^UIDa+dHq^jFDQJco1Juw4|F6dJkLFzA zA5EDJ&Oc5se#prVqW}OYL;wKI|HS_1uR>^+22@s$?teQ)vZV$#=>F-Uf{NSXquH7M H9rOPHAwQ}J