| // Copyright 2024 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as vscode from 'vscode'; |
| import { Ffx, FuchsiaDevice } from './ffx'; |
| import { DebugTarget } from './zxdb'; |
| |
| class Moniker { |
| constructor(readonly value: string) { } |
| |
| get basename(): string { |
| const index = this.value.lastIndexOf('/'); |
| if (index === -1) { |
| return this.value; |
| } else { |
| return this.value.slice(index + 1); |
| } |
| } |
| |
| get parent(): string | null { |
| const index = this.value.lastIndexOf('/'); |
| if (index === -1) { |
| return null; |
| } else { |
| return this.value.slice(0, index); |
| } |
| } |
| } |
| |
| class ComponentInfo implements DebugTarget { |
| parent: ComponentInfo | null = null; |
| children: Array<ComponentInfo> = []; |
| |
| constructor( |
| readonly moniker: Moniker, |
| readonly url: string, |
| readonly isResolved: boolean, |
| readonly isRunning: boolean, |
| ) { } |
| |
| setParent(info: ComponentInfo) { |
| if (this.parent) { |
| let index = this.parent.children.indexOf(this); |
| if (index !== -1) { |
| this.parent.children.splice(index, 1); |
| } |
| } |
| this.parent = info; |
| info.children.push(this); |
| } |
| |
| get attachDescriptor() { return this.moniker.value; } |
| |
| static fromJSON(object: any): ComponentInfo { |
| let moniker = new Moniker(object['moniker'] as string); |
| let url = object['url'] as string; |
| let isResolved = !!object['resolved_info']; |
| let isRunning = isResolved && !!object['resolved_info']['execution_info']; |
| return new ComponentInfo(moniker, url, isResolved, isRunning); |
| } |
| } |
| |
| class ComponentTree { |
| readonly map: Map<string, ComponentInfo> = new Map(); |
| readonly roots: Array<ComponentInfo> = []; |
| |
| constructor(object: any) { |
| for (const entry of object) { |
| let info = ComponentInfo.fromJSON(entry); |
| this.map.set(info.moniker.value, info); |
| } |
| for (const info of this.map.values()) { |
| let parentMoniker = info.moniker.parent; |
| if (parentMoniker) { |
| let parent = this.map.get(parentMoniker); |
| if (parent) { |
| info.setParent(parent); |
| } else { |
| this.roots.push(info); |
| } |
| } else { |
| this.roots.push(info); |
| } |
| } |
| } |
| } |
| |
| export class ComponentExplorerDataProvider implements vscode.TreeDataProvider<ComponentInfo> { |
| private currentDevice: FuchsiaDevice | null = null; |
| private tree: ComponentTree | null = null; |
| |
| constructor( |
| private readonly ffx: Ffx |
| ) { |
| this.ffx.onSetTarget((device: FuchsiaDevice | null) => { |
| if (this.currentDevice?.nodeName !== device?.nodeName) { |
| this.currentDevice = device; |
| this.refresh(); |
| } |
| }); |
| } |
| |
| private _onDidChangeTreeData: vscode.EventEmitter<ComponentInfo | undefined | null | void> = |
| new vscode.EventEmitter<ComponentInfo | undefined | null | void>(); |
| readonly onDidChangeTreeData: vscode.Event<ComponentInfo | undefined | null | void> = |
| this._onDidChangeTreeData.event; |
| |
| refresh(): void { |
| this.tree = null; |
| this._onDidChangeTreeData.fire(); |
| } |
| |
| getTreeItem(element: ComponentInfo): vscode.TreeItem { |
| let collapsibleState = element.children.length > 0 ? |
| vscode.TreeItemCollapsibleState.Expanded : |
| vscode.TreeItemCollapsibleState.None; |
| return new ComponentTreeItem(element, collapsibleState); |
| } |
| |
| async getChildren(element?: ComponentInfo | undefined): Promise<ComponentInfo[]> { |
| if (element) { |
| return element.children; |
| } else if (this.currentDevice) { |
| if (!this.tree) { |
| this.tree = new ComponentTree(await this.ffx.listComponents(this.currentDevice)); |
| } |
| return this.tree.roots; |
| } else { |
| return []; |
| } |
| } |
| |
| getParent?(element: ComponentInfo): vscode.ProviderResult<ComponentInfo> { |
| return element.parent; |
| } |
| } |
| |
| class ComponentTreeItem extends vscode.TreeItem { |
| static readonly icon = new vscode.ThemeIcon('symbol-interface'); |
| |
| constructor( |
| public readonly info: ComponentInfo, |
| public readonly collapsibleState: vscode.TreeItemCollapsibleState |
| ) { |
| super(info.moniker.basename, collapsibleState); |
| this.description = info.url; |
| this.tooltip = `Moniker: ${info.moniker.value}\nURL: ${info.url}\nComponent State: ${info.isResolved ? 'Resolved' : 'Unresolved'}\nExecution State: ${info.isRunning ? 'Running' : 'Stopped'}`; |
| this.iconPath = ComponentTreeItem.icon; |
| this.contextValue = info.isRunning ? 'running' : 'stopped'; |
| this.command = { |
| command: 'fuchsia.component.show', |
| title: 'Show details', |
| arguments: [info.moniker.value], |
| }; |
| } |
| } |
| |
| /** |
| * open a text document with the output of `ffx component show` for the given |
| * component moniker |
| */ |
| export async function showComponent(ffx: Ffx, moniker: string) { |
| const contents = await ffx.runFfx(['component', 'show', moniker]); |
| const document = await vscode.workspace.openTextDocument({ |
| language: 'plaintext', |
| content: contents |
| }); |
| await vscode.window.showTextDocument(document); |
| } |