blob: 0028774a89c67f32f67626bf5d515d6a1231f1b2 [file] [log] [blame]
// Copyright 2022 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.
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';
import { initAnalytics, setUpAnalyticsEvents } from './analytics/setup';
import { spawn } from 'child_process';
import { ToolFinder } from './tool_finder';
import { Ffx } from './ffx';
import { Fx } from './fx';
import * as logger from './logger';
import { TargetStatusBarItem } from './target_status_bar_item';
import { FuchsiaDevice } from './ffx';
import { setUpZxdb } from './zxdb';
import { setUpComponentInteraction } from './components';
import { setUpTestController } from './test_controller/test_controller';
import { setUpFuchsiaTaskProvider } from './workflow/task_provider';
import { setUpWorkflowInteraction } from './workflow';
import { LogView } from './log_view';
import { registerCommandWithAnalyticsEvent } from './analytics/vscode_events';
import { FuchsiaDocumentLinkProvider } from './doc_provider';
import { setUpGitHelper } from './git_helper';
/**
* contains common bits needed by the various setup functions below
*/
export class Setup {
/**
* a ToolFinder instance
*/
toolFinder: ToolFinder;
/**
* a handle to interact with ffx
*/
ffx: Ffx;
/**
* a handle to interact with fx
*/
fx: Fx;
constructor() {
this.toolFinder = new ToolFinder();
this.ffx = this.toolFinder.ffx;
this.fx = this.toolFinder.fx;
}
}
/**
* called automatically by vscode when activating the plugin
* (see package.json for activation points)
*/
export async function activate(ctx: vscode.ExtensionContext) {
// Clean up old $FUCHSIA_NODENAME terminal environment variable contributions.
// Do this early since this extension's `activate()` call can race against
// terminal spawns on IDE startup.
// See https://fxbug.dev/409568762 for more information.
ctx.environmentVariableCollection.delete('FUCHSIA_NODENAME');
// Initialize the logging first so it can be used by all other code.
const log = vscode.window.createOutputChannel('Fuchsia Extension', { log: true });
logger.initLogger(log);
// Selects extension channel to provide languageID later
const fuchsiaOutput: vscode.DocumentSelector = {
scheme: 'output',
pattern: 'fuchsia-authors.vscode-fuchsia.Fuchsia Extension',
};
// Init analytics before possible calls to other Fuchsia tools (e.g. ffx),
// such that the extension could display the analytics notices properly.
try {
await initAnalytics(ctx);
} catch (err) {
// if analytics fails to initialize, it's not catastrophic,
// so just continue on after logging
logger.error('Unable to set up analytics, boldly continuing', undefined, err);
};
const setup = new Setup();
// TODO(fxbug.dev/98651): we're currently doing this every program
// start -- we should find a better activation point
ctx.subscriptions.push(
vscode.commands.registerCommand('fuchsia.sendFeedback', () => {
// The code you place here will be executed every time your command is executed
void /* in bg */ vscode.env.openExternal(vscode.Uri.parse(
'https://bugs.fuchsia.dev/p/fuchsia/issues/entry?template=Fuchsia+Editor+Tooling'));
}),
registerCommandWithAnalyticsEvent('fuchsia.showOutput', () => {
logger.show();
}),
vscode.languages.registerDocumentLinkProvider(fuchsiaOutput, new FuchsiaDocumentLinkProvider(),
),
);
setUpContext(ctx, setup);
setUpLogView(ctx, setup);
setUpEmulatorInteraction(ctx, setup);
setUpTargetInteraction(ctx, setup);
setUpZxdb(ctx, setup);
setUpComponentInteraction(ctx, setup);
setUpTestController(ctx, setup);
setUpWorkflowInteraction(ctx, setup);
setUpFuchsiaTaskProvider();
setUpGitHelper(ctx);
setUpAnalyticsEvents(ctx);
}
/**
* called automatically by vscode when deactivating the plugin
*/
export function deactivate() {
// Currently noop
}
/**
* initialize the logging view & related commands
*/
function setUpLogView(ctx: vscode.ExtensionContext, setup: Setup) {
const logView = new LogView(setup.ffx);
ctx.subscriptions.push(
registerCommandWithAnalyticsEvent('fuchsia.viewLogs', () => {
logView.output?.show();
}),
);
}
/**
* initialize the target interaction commands (reboot, set target, etc)
* and status bar item.
*/
function setUpEmulatorInteraction(ctx: vscode.ExtensionContext, setup: Setup) {
const registerSimpleEmulatorCommand = (commandName: string,
command: () => Promise<string>) => {
ctx.subscriptions.push(
registerCommandWithAnalyticsEvent(commandName,
() => {
const action = commandName.includes('start') ? 'starting' : 'stopping';
void vscode.window.withProgress({
location: vscode.ProgressLocation.Window,
title: `${action} emulator...`,
}, async () => {
try {
const output = await command();
logger.info(output, 'fuchsia');
} catch (err: unknown) {
logger.error(`${String(err)}`);
void vscode.window.showErrorMessage(
`error with ${commandName}, see output for details`);
}
});
}),
);
};
registerSimpleEmulatorCommand('fuchsia.emu.start',
() => setup.ffx.startEmulator(false));
registerSimpleEmulatorCommand('fuchsia.emu.startHeadless',
() => setup.ffx.startEmulator(true));
registerSimpleEmulatorCommand('fuchsia.emu.stop',
() => setup.ffx.stopEmulator());
}
/**
* initialize the target interaction commands (reboot, set target, etc)
* and status bar item.
*/
function setUpTargetInteraction(ctx: vscode.ExtensionContext, setup: Setup) {
const registerSimpleTargetCommand = (
commandName: string,
message: (device?: FuchsiaDevice) => string,
command: (device?: FuchsiaDevice) => Promise<string>,
) => ctx.subscriptions.push(
registerCommandWithAnalyticsEvent(commandName,
(device?: FuchsiaDevice) => {
void vscode.window.withProgress({
location: vscode.ProgressLocation.Window,
title: message(device),
}, async () => {
try {
const output = await command(device);
logger.info(output, commandName);
} catch (err: unknown) {
logger.error(`${String(err)}`, commandName);
void vscode.window.showErrorMessage(
`error with ${commandName}, see output for details`);
}
});
}),
);
// set up commands...
registerSimpleTargetCommand(
'fuchsia.target.refresh',
() => 'Refreshing targets...',
async () => JSON.stringify(await setup.ffx.refreshTargets()),
);
registerSimpleTargetCommand(
'fuchsia.target.show',
device => `Dumping ${device?.nodeName ?? 'target'} info to Fuchsia Output...`,
device => setup.ffx.showTarget(device),
);
registerSimpleTargetCommand(
'fuchsia.target.reboot',
device => `Rebooting ${device?.nodeName ?? 'target'}...`,
device => setup.ffx.rebootTarget(device),
);
registerSimpleTargetCommand(
'fuchsia.target.powerOff',
device => `Powering off ${device?.nodeName ?? 'target'}...`,
device => setup.ffx.poweroffTarget(device),
);
ctx.subscriptions.push(
registerCommandWithAnalyticsEvent('fuchsia.target.snapshot',
async (device?: FuchsiaDevice) => {
const path = await setup.ffx.exportSnapshotToCWD(device);
const msg = `snapshot exported to ${path}`;
logger.info(msg, 'fuchsia');
if (await vscode.window.showInformationMessage(msg, 'Open folder')) {
spawn('xdg-open', [path]);
}
}),
);
ctx.subscriptions.push(
vscode.commands.registerCommand('fuchsia.internal.target.switch',
(device?: FuchsiaDevice) => {
if (!device) {
void vscode.window.showErrorMessage('A device must be selected to set as the default target.');
return;
}
setup.ffx.targetDevice = device;
}),
);
// ...and the status bar item
new TargetStatusBarItem(ctx.subscriptions, setup.toolFinder);
}
/**
* Sets up the `fuchsia.isFuchsiaProject` context key.
*/
function setUpContext(ctx: vscode.ExtensionContext, setup: Setup) {
// eslint-disable-next-line prefer-const
let disposable: vscode.Disposable;
const updateFuchsiaContext = () => {
if (setup.toolFinder.fx.fuchsiaDir) {
void vscode.commands.executeCommand('setContext', 'fuchsia.isFuchsiaProject', true);
disposable?.dispose();
}
};
disposable = setup.toolFinder.onDidUpdateFfx(() => updateFuchsiaContext());
ctx.subscriptions.push(disposable);
updateFuchsiaContext();
}