| // 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 { Setup } from './extension'; |
| import { Ffx, FuchsiaDevice } from './ffx'; |
| import { DebugTarget } from './zxdb'; |
| import { registerCommandWithAnalyticsEvent } from './analytics/vscode_events'; |
| |
| type CommandOptions = { |
| args: Array<string>, |
| requiresURL?: boolean, |
| refresh?: boolean |
| }; |
| |
| // Setup commands to handle component lifecycle |
| const componentCommands: { [key: string]: CommandOptions } = { |
| 'fuchsia.component.create': { |
| args: ['component', 'create'], |
| requiresURL: true, |
| refresh: true, |
| }, |
| 'fuchsia.component.destroy': { |
| args: ['component', 'destroy'], |
| refresh: true, |
| }, |
| 'fuchsia.component.recreate': { |
| args: ['component', 'run', '--recreate'], |
| requiresURL: true, |
| }, |
| 'fuchsia.component.reload': { |
| args: ['component', 'reload'], |
| }, |
| 'fuchsia.component.run': { |
| args: ['component', 'run'], |
| requiresURL: true, |
| refresh: true, |
| }, |
| 'fuchsia.component.start': { |
| args: ['component', 'start'], |
| }, |
| 'fuchsia.component.stop': { |
| args: ['component', 'stop'], |
| }, |
| }; |
| |
| /** |
| * initialize the component interaction commands (explorer, etc) |
| */ |
| export function setUpComponentInteraction(ctx: vscode.ExtensionContext, setup: Setup) { |
| const componentExplorer = new ComponentExplorerDataProvider(setup.ffx); |
| vscode.window.registerTreeDataProvider('vscode-fuchsia.componentExplorer', componentExplorer); |
| vscode.commands.registerCommand('fuchsia.refreshComponentExplorer', () => |
| componentExplorer.refresh() |
| ); |
| |
| registerCommandWithAnalyticsEvent('fuchsia.component.show', async (moniker?: string) => { |
| if (!moniker) { |
| moniker = await vscode.window.showInputBox({ |
| placeHolder: '(e.g. core/ui/scenic)', |
| value: '', |
| prompt: 'Enter a component moniker' |
| }); |
| } |
| if (!moniker) { |
| return; |
| } |
| await showComponent(setup.ffx, moniker); |
| }); |
| |
| for (const command in componentCommands) { |
| const commandOptions = componentCommands[command]; |
| registerCommandWithAnalyticsEvent( |
| command, async (info?: ComponentInfo) => { |
| const args = [...commandOptions.args]; |
| let moniker: string | undefined = info?.moniker.value ?? ''; |
| let url: string | undefined = info?.url ?? ''; |
| |
| if (!moniker) { |
| moniker = await vscode.window.showInputBox({ |
| placeHolder: '(e.g. core/ui/scenic)', |
| value: '', |
| prompt: 'Enter a component moniker' |
| }); |
| } |
| if (!moniker) { |
| return; |
| } |
| if (!url && commandOptions.requiresURL) { |
| url = await vscode.window.showInputBox({ |
| placeHolder: '(e.g. fuchsia-pkg://fuchsia.com/hello-world-cpp#meta/hello-world-cpp.cm)', |
| value: '', |
| prompt: 'Enter a component URL' |
| }); |
| } |
| if (!url && commandOptions.requiresURL) { |
| return; |
| } |
| |
| commandOptions.requiresURL ? args.push(moniker!, url!) : args.push(moniker!); |
| await setup.ffx.runFfx(args); |
| |
| if (commandOptions.refresh) { |
| componentExplorer.refresh(); |
| } |
| }); |
| } |
| |
| const taskExplorer = new TaskExplorerDataProvider(setup.ffx); |
| vscode.window.registerTreeDataProvider('vscode-fuchsia.taskExplorer', taskExplorer); |
| vscode.commands.registerCommand('fuchsia.refreshTaskExplorer', () => |
| taskExplorer.refresh() |
| ); |
| } |
| |
| 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['instances']) { |
| 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); |
| } |
| |
| class TaskInfo implements DebugTarget { |
| parent: TaskInfo | null = null; |
| children: Array<TaskInfo> = []; |
| |
| constructor( |
| readonly koid: number, |
| readonly parentKoid: number, |
| readonly type: string, |
| readonly name: string, |
| ) { } |
| |
| setParent(info: TaskInfo) { |
| 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.koid.toString(); } |
| |
| static fromJSON(object: any): TaskInfo { |
| let koid = object['koid'] as number; |
| let parentKoid = object['parent_koid'] as number; |
| let type = object['type'] as string; |
| let name = object['name'] as string; |
| return new TaskInfo(koid, parentKoid, type, name); |
| } |
| } |
| |
| class TaskTree { |
| readonly map: Map<number, TaskInfo> = new Map(); |
| readonly roots: Array<TaskInfo> = []; |
| |
| constructor(object: any) { |
| for (const entry of object['Tasks']) { |
| let info = TaskInfo.fromJSON(entry); |
| this.map.set(info.koid, info); |
| } |
| for (const info of this.map.values()) { |
| let parentKoid = info.parentKoid; |
| let parent = this.map.get(parentKoid); |
| if (parent) { |
| info.setParent(parent); |
| } else { |
| this.roots.push(info); |
| } |
| } |
| } |
| } |
| |
| export class TaskExplorerDataProvider implements vscode.TreeDataProvider<TaskInfo> { |
| private currentDevice: FuchsiaDevice | null = null; |
| private tree: TaskTree | 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<TaskInfo | undefined | null | void> = |
| new vscode.EventEmitter<TaskInfo | undefined | null | void>(); |
| readonly onDidChangeTreeData: vscode.Event<TaskInfo | undefined | null | void> = |
| this._onDidChangeTreeData.event; |
| |
| refresh(): void { |
| this.tree = null; |
| this._onDidChangeTreeData.fire(); |
| } |
| |
| getTreeItem(element: TaskInfo): vscode.TreeItem { |
| let collapsibleState = vscode.TreeItemCollapsibleState.None; |
| if (element.type === 'job') { |
| collapsibleState = vscode.TreeItemCollapsibleState.Expanded; |
| } else if (element.type === 'process') { |
| collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; |
| } |
| return new TaskTreeItem(element, collapsibleState); |
| } |
| |
| async getChildren(element?: TaskInfo | undefined): Promise<TaskInfo[]> { |
| if (element) { |
| return element.children; |
| } else if (this.currentDevice) { |
| if (!this.tree) { |
| this.tree = new TaskTree(await this.ffx.listProcesses(this.currentDevice)); |
| } |
| return this.tree.roots; |
| } else { |
| return []; |
| } |
| } |
| |
| getParent?(element: TaskInfo): vscode.ProviderResult<TaskInfo> { |
| return element.parent; |
| } |
| } |
| |
| class TaskTreeItem extends vscode.TreeItem { |
| static readonly jobIcon = new vscode.ThemeIcon('symbol-folder'); |
| static readonly processIcon = new vscode.ThemeIcon('server-process'); |
| static readonly threadIcon = new vscode.ThemeIcon('circle-outline'); |
| |
| constructor( |
| public readonly info: TaskInfo, |
| public readonly collapsibleState: vscode.TreeItemCollapsibleState |
| ) { |
| super(info.name, collapsibleState); |
| this.description = info.koid.toString(); |
| this.tooltip = `Type: ${info.type}\nKoid: ${info.koid}\nName: ${info.name}`; |
| if (info.type === 'job') { |
| this.iconPath = TaskTreeItem.jobIcon; |
| } else if (info.type === 'process') { |
| this.iconPath = TaskTreeItem.processIcon; |
| } else if (info.type === 'thread') { |
| this.iconPath = TaskTreeItem.threadIcon; |
| } |
| this.contextValue = info.type; |
| } |
| } |