diff --git a/package.json b/package.json index a084e03..433da92 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "onView:fallout.build", "onView:fallout.buildExplorer", "onView:fallout.deployment", + "onView:fallout.runConfig", "workspaceContains:**/.fallout/temp/build-graph.json", "workspaceContains:**/.nuke/temp/build-graph.json" ], @@ -59,6 +60,13 @@ "name": "Deployment", "icon": "media/fallout.svg", "contextualTitle": "Fallout" + }, + { + "id": "fallout.runConfig", + "name": "Run Configuration", + "type": "webview", + "icon": "media/fallout.svg", + "contextualTitle": "Fallout" } ], "explorer": [ diff --git a/src/extension.ts b/src/extension.ts index f802055..aa5dcad 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import { BuildGraph, GraphSource, Relation, Target, checkCompatibility, findGraphFile, loadGraph } from './model'; import { GraphPanel } from './graphPanel'; import { goToTarget } from './goToTarget'; +import { RunConfigStore, RunConfigViewProvider } from './runConfig'; const RELATION_LABELS: Record = { dependsOn: 'depends on', @@ -130,6 +131,7 @@ function runInTerminal(root: string, args: string): void { export function activate(context: vscode.ExtensionContext): void { const extensionVersion: string = context.extension.packageJSON.version; const provider = new FalloutTargetsProvider(extensionVersion); + const runConfig = new RunConfigStore(context); const runTarget = (name: string) => { const root = provider.source?.root; @@ -161,6 +163,7 @@ export function activate(context: vscode.ExtensionContext): void { vscode.window.registerTreeDataProvider('fallout.build', provider), vscode.window.registerTreeDataProvider('fallout.buildExplorer', provider), vscode.window.registerTreeDataProvider('fallout.deployment', new DeploymentProvider()), + vscode.window.registerWebviewViewProvider('fallout.runConfig', new RunConfigViewProvider(runConfig, context.subscriptions)), watcher, vscode.commands.registerCommand('fallout.refreshTargets', refreshAll), vscode.commands.registerCommand('fallout.runTarget', (item?: TargetItem) => { diff --git a/src/runConfig.ts b/src/runConfig.ts new file mode 100644 index 0000000..eb2f7e6 --- /dev/null +++ b/src/runConfig.ts @@ -0,0 +1,163 @@ +import * as vscode from 'vscode'; + +/** A build parameter passed to a local run as `--name value`. */ +export interface Parameter { + name: string; + value: string; +} + +const PARAMS_KEY = 'fallout.parameters'; + +/** + * Persists the local run configuration. Parameters live in workspace state — they are + * per-workspace by nature (a configuration for *this* build), and plain enough to sit + * in plugin storage. They are rendered as CLI args (`--name value`) for a local run. + */ +export class RunConfigStore { + private readonly onDidChangeEmitter = new vscode.EventEmitter(); + readonly onDidChange = this.onDidChangeEmitter.event; + + constructor(private readonly context: vscode.ExtensionContext) {} + + getParameters(): Parameter[] { + return this.context.workspaceState.get(PARAMS_KEY, []); + } + + async setParameter(name: string, value: string): Promise { + const params = this.getParameters().filter(p => p.name !== name); + params.push({ name, value }); + params.sort((a, b) => a.name.localeCompare(b.name)); + await this.context.workspaceState.update(PARAMS_KEY, params); + this.onDidChangeEmitter.fire(); + } + + async removeParameter(name: string): Promise { + await this.context.workspaceState.update(PARAMS_KEY, this.getParameters().filter(p => p.name !== name)); + this.onDidChangeEmitter.fire(); + } + + /** Parameters rendered as a CLI argument string, e.g. `--configuration Release`. */ + buildArgs(): string { + return this.getParameters() + .map(p => `--${p.name} ${quoteArg(p.value)}`) + .join(' '); + } +} + +function quoteArg(value: string): string { + return /\s/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value; +} + +/** The "Run Configuration" webview view — a form for build parameters. */ +export class RunConfigViewProvider implements vscode.WebviewViewProvider { + private view: vscode.WebviewView | undefined; + + constructor( + private readonly store: RunConfigStore, + disposables: vscode.Disposable[], + ) { + disposables.push(this.store.onDidChange(() => this.postState())); + } + + resolveWebviewView(webviewView: vscode.WebviewView): void { + this.view = webviewView; + webviewView.webview.options = { enableScripts: true }; + webviewView.webview.html = this.html(webviewView.webview); + + webviewView.webview.onDidReceiveMessage(async (message: any) => { + switch (message?.type) { + case 'ready': + this.postState(); + break; + case 'setParameter': + if (message.name) { await this.store.setParameter(String(message.name), String(message.value ?? '')); } + break; + case 'removeParameter': + await this.store.removeParameter(String(message.name)); + break; + } + }); + } + + private postState(): void { + void this.view?.webview.postMessage({ + type: 'state', + parameters: this.store.getParameters(), + }); + } + + private html(webview: vscode.Webview): string { + const nonce = getNonce(); + return /* html */ ` + + + + + + + +

Parameters

+
Passed to local runs as --name value.
+
+
+ + + +
+ + + +`; + } +} + +function getNonce(): string { + let text = ''; + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 32; i++) { + text += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return text; +}