blob: ecd3f9fe97f3c5717dc326999d72efa5ee916aed [file] [log] [blame] [edit]
// 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;
}
}