diff --git a/vscode/common-src/experimentWebViewApi.ts b/vscode/common-src/experimentWebViewApi.ts new file mode 100644 index 0000000..896d5c6 --- /dev/null +++ b/vscode/common-src/experimentWebViewApi.ts @@ -0,0 +1,33 @@ +export type WebViewMessage = VisualizeImpactScenarioCommand; + +export interface VisualizeImpactScenarioCommand { + command: "visualizeImpactScenario"; + args: ImpactScenario; +} + +export interface ImpactScenario { + scenarioName: string; + mutations: MutationInfo[]; + impactedTimelines: TimelineInfo[]; +} + +export interface MutationInfo { + mutationId: string; + timelineId: string; + timelineName: string; + segmentId: SegmentId; +} + +// This matches the one in the backend api +export interface SegmentId { + workspace_version_id: string; + rule_name: string; + segment_name: string; +} + +export interface TimelineInfo { + timelineName: string; + severity: number; + events: string[]; + detailsHtml: string; +} diff --git a/vscode/common-src/transitionGraphWebViewApi.ts b/vscode/common-src/transitionGraphWebViewApi.ts index e6cb667..09f97eb 100644 --- a/vscode/common-src/transitionGraphWebViewApi.ts +++ b/vscode/common-src/transitionGraphWebViewApi.ts @@ -26,7 +26,7 @@ export interface LogSelectedNodesCommand { export interface NodeData extends cytoscape.NodeDataDefinition { label?: string; - labelvalign?: "top" | "center"; + labelvalign: "top" | "center"; filepath?: string; timeline?: string; timelineName?: string; diff --git a/vscode/package-lock.json b/vscode/package-lock.json index 8b7fd41..f39f21d 100644 --- a/vscode/package-lock.json +++ b/vscode/package-lock.json @@ -28,6 +28,7 @@ "devDependencies": { "@types/cytoscape": "^3.19", "@types/cytoscape-context-menus": "^4.1.3", + "@types/glob": "^8.1.0", "@types/jquery": "^3.5.29", "@types/mocha": "^9.1.0", "@types/node": "^16.11.7", @@ -624,6 +625,16 @@ "@types/cytoscape": "*" } }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "dev": true, + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, "node_modules/@types/jquery": { "version": "3.5.29", "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.29.tgz", @@ -644,6 +655,12 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, "node_modules/@types/mocha": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", diff --git a/vscode/package.json b/vscode/package.json index 70b8e65..977f292 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -1032,6 +1032,7 @@ "devDependencies": { "@types/cytoscape": "^3.19", "@types/cytoscape-context-menus": "^4.1.3", + "@types/glob": "^8.1.0", "@types/jquery": "^3.5.29", "@types/mocha": "^9.1.0", "@types/node": "^16.11.7", diff --git a/vscode/src/config.ts b/vscode/src/config.ts index 594698b..4edb612 100644 --- a/vscode/src/config.ts +++ b/vscode/src/config.ts @@ -57,9 +57,9 @@ export async function modalityUrl(): Promise { /** * Extra environment variables to use for all command invocations, including the LSP server. */ -function extraEnv(): null | object { +function extraEnv(): object | undefined { const auxonConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("auxon"); - return auxonConfig.get("extraEnv"); + return auxonConfig.get("extraEnv"); } /** @@ -85,14 +85,18 @@ export async function allowInsecureHttps(): Promise { */ export async function toolEnv(): Promise> { const env: Record = { - MODALITY_AUTH_TOKEN: await userAuthToken(), // TODO implement this in the CLI MODALITY_ALLOW_INSECURE_TLS: (await allowInsecureHttps()).toString(), MODALITY_URL: (await modalityUrlV1()).toString(), }; + const auth_token = await userAuthToken(); + if (auth_token != null) { + env["MODALITY_AUTH_TOKEN"] = auth_token; + } + const extra = extraEnv(); - if (extra) { + if (extra != null) { for (const [k, v] of Object.entries(extra)) { env[k] = v.toString(); } @@ -123,7 +127,7 @@ export async function toolDebugEnv(): Promise> { export function toolPath(tool_name: string): string { const auxonConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("auxon"); const toolDir = auxonConfig.get("tooldir"); - let toolPath: string; + let toolPath: string | undefined; if (process.platform == "win32") { let customPath = null; @@ -167,18 +171,22 @@ export function toolPath(tool_name: string): string { export function extraCliArgs(command: string): string[] { const auxonConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("auxon"); const argsMap = auxonConfig.get<{ [key: string]: string[] }>("extraCliArgs"); + if (argsMap == null) { + return []; + } + const args = argsMap[command]; - if (args) { - return args; - } else { + if (args == null) { return []; } + + return args; } /** * Return the first given path which exists, or null if none of them do. */ -function firstExistingPath(...paths: string[]): string | null { +function firstExistingPath(...paths: (string | null)[]): string | undefined { for (let i = 0; i < paths.length; i++) { const path = paths[i]; if (path == null) { @@ -189,6 +197,4 @@ function firstExistingPath(...paths: string[]): string | null { return path; } } - - return null; } diff --git a/vscode/src/deviantCommands.ts b/vscode/src/deviantCommands.ts index 4d2dd05..9ac381e 100644 --- a/vscode/src/deviantCommands.ts +++ b/vscode/src/deviantCommands.ts @@ -35,8 +35,12 @@ export async function runDeviantExperimentCreateCommand(args: ExperimentCreateCo try { const res = await execFile(deviantPath, commandArgs, { encoding: "utf8" }); const _dont_wait = vscode.window.showInformationMessage(res.stdout); - } catch (e) { - vscode.window.showErrorMessage(e.stderr.trim()); + } catch (e: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) { + if (Object.prototype.hasOwnProperty.call(e, "stderr")) { + vscode.window.showErrorMessage(e.stderr.trim()); + } else { + vscode.window.showErrorMessage(e.toString()); + } } } @@ -64,8 +68,12 @@ async function runDeviantMutationClearCommand(args: MutationClearCommandArgs) { try { const res = await execFile(deviantPath, commandArgs, { encoding: "utf8" }); const _dont_wait = vscode.window.showInformationMessage(res.stdout); - } catch (e) { - vscode.window.showErrorMessage(e.stderr.trim()); + } catch (e: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) { + if (Object.prototype.hasOwnProperty.call(e, "stderr")) { + vscode.window.showErrorMessage(e.stderr.trim()); + } else { + vscode.window.showErrorMessage(e.toString()); + } } vscode.commands.executeCommand("auxon.mutations.refresh"); @@ -102,15 +110,23 @@ async function runDeviantMutationCreateCommand(args: MutationCreateCommandArgs) try { const res = await execFile(deviantPath, commandArgs, { encoding: "utf8" }); - const output = JSON.parse(res.stdout) as string; - const _dont_wait = vscode.window.showInformationMessage(`Created mutation '${output["mutation_id"]}'`); - } catch (e) { - vscode.window.showErrorMessage(e.stderr.trim()); + const output = JSON.parse(res.stdout) as MutationCreateOutput; + const _dont_wait = vscode.window.showInformationMessage(`Created mutation '${output.mutation_id}'`); + } catch (e: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) { + if (Object.prototype.hasOwnProperty.call(e, "stderr")) { + vscode.window.showErrorMessage(e.stderr.trim()); + } else { + vscode.window.showErrorMessage(e.toString()); + } } vscode.commands.executeCommand("auxon.mutations.refresh"); } +interface MutationCreateOutput { + mutation_id: string; +} + // TODO - add linked experiment option async function runCreateMutationWizard(mutator: Mutator) { const title = `Create a mutation for mutator '${mutator.name}'`; @@ -138,7 +154,7 @@ async function runCreateMutationWizard(mutator: Mutator) { title: `${title} (${step}/${maxSteps})`, placeHolder: `Enter the parameter value for '${param.name}' or leave blank for Deviant-suggested values`, ignoreFocusOut: true, - validateInput: (input) => validateParameter(input, param), + validateInput: (input: string) => validateParameter(input, param), }; let paramValue = await vscode.window.showInputBox(options); if (paramValue === undefined) { @@ -194,18 +210,24 @@ function validateParameter(input: string, param: MutatorParameter): string | nul } } -function isFloat(val) { +function isFloat(val: string) { const floatRegex = /^-?\d+(?:[.]\d*?)?$/; - if (!floatRegex.test(val)) return false; + if (!floatRegex.test(val)) { + return false; + } - val = parseFloat(val); - if (isNaN(val)) return false; + const fVal = parseFloat(val); + if (isNaN(fVal)) { + return false; + } return true; } -function isInt(val) { +function isInt(val: string) { const intRegex = /^-?\d+$/; - if (!intRegex.test(val)) return false; + if (!intRegex.test(val)) { + return false; + } const intVal = parseInt(val, 10); return parseFloat(val) == intVal && !isNaN(intVal); diff --git a/vscode/src/events.ts b/vscode/src/events.ts index fe721ec..8d8317a 100644 --- a/vscode/src/events.ts +++ b/vscode/src/events.ts @@ -1,29 +1,30 @@ import * as lodash from "lodash"; import * as vscode from "vscode"; import * as api from "./modalityApi"; +import * as modalityLog from "./modalityLog"; +import * as workspaceState from "./workspaceState"; + import * as commonNotebookCells from "./notebooks/common.json"; import * as eventTimingNotebookCells from "./notebooks/eventTiming.json"; import * as eventAttributeValuesNotebookCells from "./notebooks/eventAttributeValues.json"; import * as eventMultiAttributeValuesNotebookCells from "./notebooks/eventMultiAttributeValues.json"; -import * as modalityLog from "./modalityLog"; +type JupyterNotebookCell = (typeof commonNotebookCells.cells)[0]; export class EventsTreeDataProvider implements vscode.TreeDataProvider { selectedTimelineId?: api.TimelineId = undefined; selectedTimelineName?: string = undefined; view: vscode.TreeView; - // Need these to generate the Jupyter notebooks - activeWorkspaceVersionId: string; - activeSegments: api.WorkspaceSegmentId[]; - private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - constructor(private readonly apiClient: api.Client) {} - - register(context: vscode.ExtensionContext) { + constructor( + private readonly apiClient: api.Client, + private wss: workspaceState.WorkspaceAndSegmentState, + context: vscode.ExtensionContext + ) { this.view = vscode.window.createTreeView("auxon.events", { treeDataProvider: this, canSelectMany: true, @@ -53,6 +54,8 @@ export class EventsTreeDataProvider implements vscode.TreeDataProvider { const varMap = this.templateVariableMap(); - varMap["eventName"] = eventName; + if (varMap == null) { + return; + } + + varMap.eventName = eventName; const cells = lodash.cloneDeep(eventMultiAttributeValuesNotebookCells.cells.slice(0, 2)); const srcCell = eventMultiAttributeValuesNotebookCells.cells[2]; const figShowCell = eventMultiAttributeValuesNotebookCells.cells[3]; @@ -162,14 +183,14 @@ export class EventsTreeDataProvider implements vscode.TreeDataProvider { + return "'" + seg.segment_name + "'"; + }) + .join(","); + } + } + + if (this.selectedTimelineId == null || this.selectedTimelineName == null) { + vscode.window.showWarningMessage(`A timeline must be selected to generate a notebook`); + return; + } + return { - workspaceVersionId: this.activeWorkspaceVersionId, - segments: this.activeSegments - .map((seg) => { - return "'" + seg.segment_name + "'"; - }) - .join(","), + workspaceVersionId: this.wss.activeWorkspaceVersionId, + segments, timelineId: "%" + this.selectedTimelineId.replace(/-/g, ""), timelineName: this.selectedTimelineName, }; } } +interface TemplateVariableMap { + workspaceVersionId: string; + segments: string; + timelineId: string; + timelineName: string; + eventName?: string; + eventAttribute?: string; + eventAttributes?: string; + + [k: string]: unknown; +} + export type EventsTreeItemData = EventNameTreeItemData | EventAttributeTreeItemData; export type EventsTreeItem = EventNameTreeItem | EventAttributeTreeItem; diff --git a/vscode/src/experiments.ts b/vscode/src/experiments.ts index 7d53866..0a6aa47 100644 --- a/vscode/src/experiments.ts +++ b/vscode/src/experiments.ts @@ -1,11 +1,12 @@ import * as vscode from "vscode"; import * as api from "./modalityApi"; -import * as cliConfig from "./cliConfig"; import * as config from "./config"; import * as tmp from "tmp-promise"; import { promises as fs } from "fs"; import { getNonce } from "./webviewUtil"; import { AssignNodeProps } from "./transitionGraph"; +import * as workspaceState from "./workspaceState"; +import * as experimentWebViewApi from "../common-src/experimentWebViewApi"; class ExperimentsTreeMemento { constructor(private readonly memento: vscode.Memento) {} @@ -24,24 +25,21 @@ export class ExperimentsTreeDataProvider implements vscode.TreeDataProvider = this._onDidChangeTreeData.event; - workspaceState?: ExperimentsTreeMemento = undefined; + uiState: ExperimentsTreeMemento; view: vscode.TreeView; - // Data scope related - dataScope: ExperimentDataScope; - - constructor(private readonly apiClient: api.Client, private readonly extensionContext: vscode.ExtensionContext) { - this.dataScope = new ExperimentDataScope(undefined, undefined, []); - } - - register(context: vscode.ExtensionContext) { - this.workspaceState = new ExperimentsTreeMemento(context.workspaceState); + constructor( + private readonly apiClient: api.Client, + private wss: workspaceState.WorkspaceAndSegmentState, + private readonly extensionContext: vscode.ExtensionContext + ) { + this.uiState = new ExperimentsTreeMemento(extensionContext.workspaceState); this.view = vscode.window.createTreeView("auxon.experiments", { treeDataProvider: this, canSelectMany: false, }); - context.subscriptions.push( + extensionContext.subscriptions.push( this.view, vscode.commands.registerCommand("auxon.experiments.refresh", () => this.refresh()), vscode.commands.registerCommand("auxon.experiments.showResults", () => this.showResults(true)), @@ -55,7 +53,8 @@ export class ExperimentsTreeDataProvider implements vscode.TreeDataProvider this.visualizeImpactScenario(args) ), - vscode.tasks.onDidEndTaskProcess((ev) => this.onDidEndTaskProcess(ev)) + vscode.tasks.onDidEndTaskProcess((ev) => this.onDidEndTaskProcess(ev)), + this.wss.onDidChangeUsedSegments(() => this.refresh()) ); this.refresh(); @@ -65,34 +64,19 @@ export class ExperimentsTreeDataProvider implements vscode.TreeDataProvider m.segmentId); const assignNodeProps = new AssignNodeProps(); @@ -221,37 +208,17 @@ export class ExperimentsTreeDataProvider implements vscode.TreeDataProvider { const experiment = await this.apiClient.experiment(name).get(); - return new NamedExperimentTreeItemData(experiment, this.dataScope); + return new NamedExperimentTreeItemData(experiment, this.wss); }) ); const { compare } = Intl.Collator("en-US"); return items.sort((a, b) => compare(a.name, b.name)); } else { - return await element.children(this.apiClient, this.workspaceState); + return await element.children(this.apiClient, this.uiState); } } } -interface ImpactScenario { - scenarioName: string; - mutations: [ - { - mutationId: string; - timelineId: string; - timelineName: string; - segmentId: api.WorkspaceSegmentId; - } - ]; - impactedTimelines: [ - { - timelineName: string; - events: [string]; - severity: number; - detailsHtml: string; - } - ]; -} - abstract class ExperimentsTreeItemData { abstract contextValue: string; @@ -302,7 +269,7 @@ abstract class ExperimentsTreeItemData { export class NamedExperimentTreeItemData extends ExperimentsTreeItemData { contextValue = "experiment"; - constructor(public experiment: api.Experiment, private dataScope: ExperimentDataScope) { + constructor(public experiment: api.Experiment, private wss: workspaceState.WorkspaceAndSegmentState) { super(experiment.name); this.iconPath = new vscode.ThemeIcon("beaker"); } @@ -344,26 +311,28 @@ export class NamedExperimentTreeItemData extends ExperimentsTreeItemData { } if (workspaceState.getShowResults()) { let results = undefined; - switch (this.dataScope.usedSegmentConfig.type) { - case "All": + switch (this.wss.activeSegments.type) { case "WholeWorkspace": - if (this.dataScope.activeWorkspaceVersionId) { + if (this.wss.activeWorkspaceVersionId) { results = await apiClient - .workspace(this.dataScope.activeWorkspaceVersionId) + .workspace(this.wss.activeWorkspaceVersionId) .experimentResults(this.experiment.name); } break; - case "Latest": - case "Set": - if (this.dataScope.activeSegments.length != 0) { + case "Explicit": + if (this.wss.activeSegments.isAllSegments) { + results = await apiClient + .workspace(this.wss.activeWorkspaceVersionId) + .experimentResults(this.experiment.name); + } else if (this.wss.activeSegments.segmentIds.length != 0) { results = await apiClient - .segment(this.dataScope.activeSegments[0]) + .segment(this.wss.activeSegments.segmentIds[0]) .experimentResults(this.experiment.name); // Merge results of remaining segments, ExperimentResults will sort them out - for (let i = 1; i < this.dataScope.activeSegments.length; i++) { + for (let i = 1; i < this.wss.activeSegments.segmentIds.length; i++) { const segRes = await apiClient - .segment(this.dataScope.activeSegments[i]) + .segment(this.wss.activeSegments.segmentIds[i]) .experimentResults(this.experiment.name); results.mutations.push(...segRes.mutations); results.mutators.push(...segRes.mutators); @@ -604,7 +573,15 @@ export class ExperimentMutationsTreeItemData extends ExperimentsTreeItemData { for (const mutation of this.mutations) { children.push(new ExperimentMutationTreeItemData(mutation)); } - children.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + children.sort((a, b) => { + if (!a?.createdAt) { + return 1; + } + if (!b?.createdAt) { + return -1; + } + return b.createdAt.getTime() - a.createdAt.getTime(); + }); return children; } } @@ -649,9 +626,7 @@ class ExperimentResults { } const mutations: Map = new Map(); - for (const mutationAndChecklist of results.mutations) { - const mutation = mutationAndChecklist[0]; - const _overall_checklist = mutationAndChecklist[1]; // using the per-region-checklist + for (const [mutation, _overallChecklist] of results.mutations) { mutations.set(mutation.mutation_id, mutation); } @@ -662,7 +637,7 @@ class ExperimentResults { for (const [_mutationId, mutation] of mutations) { if (mutationIdsToChecklist.has(mutation.mutation_id)) { const checklist = mutationIdsToChecklist.get(mutation.mutation_id); - if (mutation.linked_experiment == experimentName && checklist.proposed_for_the_selected_experiment) { + if (mutation.linked_experiment == experimentName && checklist?.proposed_for_the_selected_experiment) { let mutatorName = ""; const mutatorDef = mutators.find((m) => m.mutator_id == mutation.mutator_id); if (mutatorDef) { @@ -689,11 +664,3 @@ class ExperimentMutation { this.createdAt.setUTCSeconds(mutation.created_at_utc_seconds); } } - -class ExperimentDataScope { - constructor( - public usedSegmentConfig?: cliConfig.ContextSegment, - public activeWorkspaceVersionId?: string, - public activeSegments?: api.WorkspaceSegmentId[] - ) {} -} diff --git a/vscode/src/main.ts b/vscode/src/main.ts index cbfbb96..c9daa09 100644 --- a/vscode/src/main.ts +++ b/vscode/src/main.ts @@ -22,6 +22,7 @@ import * as mutators from "./mutators"; import * as mutations from "./mutations"; import * as deviantCommands from "./deviantCommands"; import * as experiments from "./experiments"; +import * as workspaceState from "./workspaceState"; export let log: vscode.OutputChannel; let lspClient: LanguageClient; @@ -49,6 +50,7 @@ export async function activate(context: vscode.ExtensionContext) { } const apiClient = new api.Client(apiUrl.toString(), token, allowInsecure); + const wss = await workspaceState.WorkspaceAndSegmentState.create(apiClient); terminalLinkProvider.register(context); modalityLog.register(context); @@ -58,70 +60,20 @@ export async function activate(context: vscode.ExtensionContext) { deviantCommands.register(context); experimentFileCommands.register(context); - const specCoverageProvider = new specCoverage.SpecCoverageProvider(apiClient); - await specCoverageProvider.initialize(context); + const specCoverageProvider = new specCoverage.SpecCoverageProvider(apiClient, context); - const workspacesTreeDataProvider = new workspaces.WorkspacesTreeDataProvider(apiClient); - const segmentsTreeDataProvider = new segments.SegmentsTreeDataProvider(apiClient, specCoverageProvider); - const timelinesTreeDataProvider = new timelines.TimelinesTreeDataProvider(apiClient); - const eventsTreeDataProvider = new events.EventsTreeDataProvider(apiClient); - const specsTreeDataProvider = new specs.SpecsTreeDataProvider(apiClient, specCoverageProvider); - const mutatorsTreeDataProvider = new mutators.MutatorsTreeDataProvider(apiClient); - const mutationsTreeDataProvider = new mutations.MutationsTreeDataProvider(apiClient); - const experimentsTreeDataProvider = new experiments.ExperimentsTreeDataProvider(apiClient, context); + // The tree view providers all register themselves with the window + // when we new them up, so we don't need to hold on to them. + new workspaces.WorkspacesTreeDataProvider(apiClient, wss, context); + new segments.SegmentsTreeDataProvider(apiClient, specCoverageProvider, wss, context); + new timelines.TimelinesTreeDataProvider(apiClient, wss, context); + new events.EventsTreeDataProvider(apiClient, wss, context); - workspacesTreeDataProvider.onDidChangeActiveWorkspace(async (ws_ver) => { - log.appendLine(`Active workspace change! ${ws_ver}`); - const wsDef = await apiClient.workspace(ws_ver).definition(); + new specs.SpecsTreeDataProvider(apiClient, specCoverageProvider, wss, context); - segmentsTreeDataProvider.activeWorkspaceVersionId = ws_ver; - segmentsTreeDataProvider.refresh(); - - timelinesTreeDataProvider.activeWorkspaceVersionId = ws_ver; - timelinesTreeDataProvider.refresh(); - - eventsTreeDataProvider.activeWorkspaceVersionId = ws_ver; - eventsTreeDataProvider.refresh(); - - mutatorsTreeDataProvider.setWorkspaceMutatorGroupingAttrs(wsDef.mutator_grouping_attrs); - mutatorsTreeDataProvider.setActiveWorkspace(ws_ver); - - mutationsTreeDataProvider.setActiveWorkspace(ws_ver); - - experimentsTreeDataProvider.setActiveWorkspace(ws_ver); - }); - - segmentsTreeDataProvider.onDidChangeUsedSegments((ev) => { - specsTreeDataProvider.setActiveSegmentIds(ev.activeSegmentIds); - - timelinesTreeDataProvider.usedSegmentConfig = ev.usedSegmentConfig; - timelinesTreeDataProvider.activeSegments = ev.activeSegmentIds; - timelinesTreeDataProvider.refresh(); - - eventsTreeDataProvider.activeSegments = ev.activeSegmentIds; - eventsTreeDataProvider.refresh(); - - mutatorsTreeDataProvider.setActiveSegmentIds(ev.usedSegmentConfig, ev.activeSegmentIds); - - mutationsTreeDataProvider.setActiveSegmentIds(ev.usedSegmentConfig, ev.activeSegmentIds); - - experimentsTreeDataProvider.setActiveSegmentIds(ev.usedSegmentConfig, ev.activeSegmentIds); - }); - - workspacesTreeDataProvider.register(context); - segmentsTreeDataProvider.register(context); - timelinesTreeDataProvider.register(context); - eventsTreeDataProvider.register(context); - specsTreeDataProvider.register(context); - mutatorsTreeDataProvider.register(context); - mutationsTreeDataProvider.register(context); - experimentsTreeDataProvider.register(context); - - // Explicitly load views that are referenceable across views - await workspacesTreeDataProvider.getChildren(); - await mutatorsTreeDataProvider.getChildren(); - await mutationsTreeDataProvider.getChildren(); - await specsTreeDataProvider.getChildren(); + new mutators.MutatorsTreeDataProvider(apiClient, wss, context); + new mutations.MutationsTreeDataProvider(apiClient, wss, context); + new experiments.ExperimentsTreeDataProvider(apiClient, wss, context); } export function deactivate(): Thenable | undefined { diff --git a/vscode/src/modalityApi.ts b/vscode/src/modalityApi.ts index ce6ca80..dd8ed79 100644 --- a/vscode/src/modalityApi.ts +++ b/vscode/src/modalityApi.ts @@ -4,10 +4,19 @@ import createClient from "openapi-fetch"; // See https://github.com/ajaishankar/openapi-typescript-fetch#server-side-usage import fetch, { Headers, Request, Response } from "node-fetch"; import { Uri } from "vscode"; + +// @ts-ignore if (!globalThis.fetch) { + // @ts-ignore globalThis.fetch = fetch; + + // @ts-ignore globalThis.Headers = Headers; + + // @ts-ignore globalThis.Request = Request; + + // @ts-ignore globalThis.Response = Response; } @@ -157,7 +166,8 @@ export class WorkspacesClient { async list(): Promise { const res = await this.client.get("/v2/workspaces", {}); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } } @@ -170,7 +180,8 @@ export class WorkspaceClient { path: { workspace_version_id: this.workspaceVersionId }, }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async segments(): Promise { @@ -179,7 +190,8 @@ export class WorkspaceClient { path: { workspace_version_id: this.workspaceVersionId }, }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async timelines(): Promise { @@ -188,7 +200,8 @@ export class WorkspaceClient { path: { workspace_version_id: this.workspaceVersionId }, }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async groupedTimelines(groupBy: string[]): Promise { @@ -203,7 +216,8 @@ export class WorkspaceClient { query: groupBy.map((gb) => ["group_by", gb]), }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async timelineAttrKeys(): Promise { @@ -212,7 +226,8 @@ export class WorkspaceClient { path: { workspace_version_id: this.workspaceVersionId }, }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async mutators(): Promise { @@ -223,7 +238,8 @@ export class WorkspaceClient { query: [], }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async groupedMutators(groupBy: string[]): Promise { @@ -234,7 +250,8 @@ export class WorkspaceClient { query: groupBy.map((gb) => ["group_by", gb]), }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async mutations(mutatorId?: MutatorId): Promise { @@ -249,7 +266,8 @@ export class WorkspaceClient { query: q, }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async experimentResults(experimentName: string): Promise { @@ -261,7 +279,8 @@ export class WorkspaceClient { }, }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } } @@ -275,7 +294,8 @@ export class SegmentClient { params: { path: this.segmentId }, } ); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async groupedTimelines(groupBy: string[]): Promise { @@ -294,7 +314,8 @@ export class SegmentClient { }, } ); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async timelineAttrKeys(): Promise { @@ -304,7 +325,8 @@ export class SegmentClient { params: { path: this.segmentId }, } ); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async groupedGraph(groupBy: string[]): Promise { @@ -323,7 +345,8 @@ export class SegmentClient { } ); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async specCoverage( @@ -365,7 +388,8 @@ export class SegmentClient { } ); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async specSummary(spec_filter?: string): Promise { @@ -389,7 +413,8 @@ export class SegmentClient { } ); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async mutators(): Promise { @@ -400,7 +425,8 @@ export class SegmentClient { query: [], }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async groupedMutators(groupBy: string[]): Promise { @@ -414,7 +440,8 @@ export class SegmentClient { }, } ); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async mutations(mutatorId?: MutatorId): Promise { @@ -429,7 +456,8 @@ export class SegmentClient { query: q, }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async experimentResults(experimentName: string): Promise { @@ -446,7 +474,8 @@ export class SegmentClient { }, } ); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } } @@ -467,7 +496,8 @@ export class TimelinesClient { query, }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } } @@ -478,7 +508,8 @@ export class TimelineClient { const res = await this.client.get("/v2/timelines/{timeline_id}", { params: { path: { timeline_id: this.timelineId } }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } } @@ -489,7 +520,8 @@ export class EventsClient { const res = await this.client.get("/v2/events/{timeline_id}/summary", { params: { path: { timeline_id: timelineId } }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } } @@ -498,7 +530,8 @@ export class SpecsClient { async list(): Promise { const res = await this.client.get("/v2/specs", {}); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } } @@ -513,21 +546,24 @@ export class SpecClient { const res = await this.client.get("/v2/specs/{spec_name}", { params: { path: { spec_name: this.specName } }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async structure(): Promise { const res = await this.client.get("/v2/specs/{spec_name}/structure", { params: { path: { spec_name: this.specName } }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async versions(): Promise { const res = await this.client.get("/v2/specs/{spec_name}/versions", { params: { path: { spec_name: this.specName } }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } } @@ -547,7 +583,8 @@ export class SpecVersionClient { }, }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async structure(): Promise { @@ -559,7 +596,8 @@ export class SpecVersionClient { }, }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async results(): Promise { @@ -571,7 +609,8 @@ export class SpecVersionClient { }, }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } } @@ -585,7 +624,8 @@ export class MutatorsClient { query: [], }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } async groupedMutators(groupBy: string[]): Promise { @@ -599,7 +639,8 @@ export class MutatorsClient { query: groupBy.map((gb) => ["group_by", gb]), }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } } @@ -615,7 +656,8 @@ export class MutatorClient { query: q, }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } } @@ -633,7 +675,8 @@ export class MutationsClient { query: q, }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } } @@ -642,7 +685,8 @@ export class ExperimentsClient { async list(): Promise { const res = await this.client.get("/v2/experiments", {}); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } } @@ -657,7 +701,8 @@ export class ExperimentClient { }, }, }); - return unwrapData(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return unwrapData(res); } } @@ -665,9 +710,12 @@ export class ExperimentClient { * Convert a repsonse to just the data; if it's an error, throw the error. */ function unwrapData(res: { data: T; error?: never } | { data?: never; error: E }): T { - if (res.error) { + if (res.data != null) { + return res.data; + } else if (res.error != null) { throw new Error(res.error.toString()); } else { - return res.data; + // Unreachable, but I can't convince typescript of that fact + throw new Error("Unknown api error"); } } diff --git a/vscode/src/mutations.ts b/vscode/src/mutations.ts index 990981e..2bfec92 100644 --- a/vscode/src/mutations.ts +++ b/vscode/src/mutations.ts @@ -1,7 +1,7 @@ import * as vscode from "vscode"; -import * as cliConfig from "./cliConfig"; import * as api from "./modalityApi"; import * as modalityLog from "./modalityLog"; +import * as workspaceState from "./workspaceState"; class MutationsTreeMemento { constructor(private readonly memento: vscode.Memento) {} @@ -37,20 +37,17 @@ export class MutationsTreeDataProvider implements vscode.TreeDataProvider = this._onDidChangeTreeData.event; - workspaceState?: MutationsTreeMemento = undefined; + uiState: MutationsTreeMemento; data: MutationsTreeItemData[] = []; view: vscode.TreeView; selectedMutatorId?: api.MutatorId = undefined; - // Data scope related - usedSegmentConfig?: cliConfig.ContextSegment = undefined; - activeWorkspaceVersionId?: string = undefined; - activeSegments: api.WorkspaceSegmentId[] = []; - - constructor(private readonly apiClient: api.Client) {} - - register(context: vscode.ExtensionContext) { - this.workspaceState = new MutationsTreeMemento(context.workspaceState); + constructor( + private readonly apiClient: api.Client, + private wss: workspaceState.WorkspaceAndSegmentState, + context: vscode.ExtensionContext + ) { + this.uiState = new MutationsTreeMemento(context.workspaceState); this.view = vscode.window.createTreeView("auxon.mutations", { treeDataProvider: this, canSelectMany: false, @@ -84,7 +81,8 @@ export class MutationsTreeDataProvider implements vscode.TreeDataProvider this.viewLogFromMutation(itemData) - ) + ), + this.wss.onDidChangeUsedSegments(() => this.refresh()) ); this.refresh(); @@ -94,69 +92,60 @@ export class MutationsTreeDataProvider implements vscode.TreeDataProvider { - // This is an 'uninitialized' condition - if (!this.usedSegmentConfig) { - return []; - } - - if (this.workspaceState.getFilterBySelectedMutator() && this.selectedMutatorId == null) { + if (this.uiState.getFilterBySelectedMutator() && this.selectedMutatorId == null) { // Need a selected mutator to populate with return []; } else if (!element) { let mutations = []; this.data = []; - switch (this.usedSegmentConfig.type) { - case "All": - mutations = await this.apiClient.mutations().list(this.selectedMutatorId); - break; + switch (this.wss.activeSegments.type) { case "WholeWorkspace": - if (!this.activeWorkspaceVersionId) { - return []; - } mutations = await this.apiClient - .workspace(this.activeWorkspaceVersionId) + .workspace(this.wss.activeWorkspaceVersionId) .mutations(this.selectedMutatorId); break; - case "Latest": - case "Set": - if (this.activeSegments.length === 0) { - return []; - } - for (const segmentId of this.activeSegments) { - const segMutations = await this.apiClient.segment(segmentId).mutations(this.selectedMutatorId); - mutations.push(...segMutations); + case "Explicit": + if (this.wss.activeSegments.isAllSegments) { + mutations = await this.apiClient.mutations().list(this.selectedMutatorId); + } else { + for (const segmentId of this.wss.activeSegments.segmentIds) { + const segMutations = await this.apiClient + .segment(segmentId) + .mutations(this.selectedMutatorId); + mutations.push(...segMutations); + } } break; } mutations = mutations.map((m) => new Mutation(m)); - if (!this.workspaceState.getShowClearedMutations()) { + if (!this.uiState.getShowClearedMutations()) { mutations = mutations.filter((m) => !m.wasCleared()); } - if (this.workspaceState.getGroupByMutatorName()) { + if (this.uiState.getGroupByMutatorName()) { const root = new MutationsGroupByNameTreeItemData("", []); for (const m of mutations) { root.insertNode(m); @@ -183,7 +172,7 @@ export class MutationsTreeDataProvider implements vscode.TreeDataProvider { - if (this.workspaceState.getGroupByMutatorName()) { + if (this.uiState.getGroupByMutatorName()) { for (const group of this.data) { if (!(group instanceof MutationsGroupByNameTreeItemData)) { throw new Error("Internal error: mutations tree node not of expected type"); @@ -198,12 +187,12 @@ export class MutationsTreeDataProvider implements vscode.TreeDataProvider = this._onDidChangeTreeData.event; - workspaceState?: MutatorsTreeMemento = undefined; + uiState: MutatorsTreeMemento; data: MutatorsTreeItemData[] = []; view: vscode.TreeView; workspaceMutatorGroupingAttrs: string[] = []; - // Data scope related - usedSegmentConfig?: cliConfig.ContextSegment = undefined; - activeWorkspaceVersionId?: string = undefined; - activeSegments: api.WorkspaceSegmentId[] = []; - - constructor(private readonly apiClient: api.Client) {} - - register(context: vscode.ExtensionContext) { + constructor( + private readonly apiClient: api.Client, + private wss: workspaceState.WorkspaceAndSegmentState, + context: vscode.ExtensionContext + ) { this.data = []; this.workspaceMutatorGroupingAttrs = []; - this.workspaceState = new MutatorsTreeMemento(context.workspaceState); + this.uiState = new MutatorsTreeMemento(context.workspaceState); this.view = vscode.window.createTreeView("auxon.mutators", { treeDataProvider: this, canSelectMany: false, @@ -89,7 +86,8 @@ export class MutatorsTreeDataProvider implements vscode.TreeDataProvider { this.createMutation(itemData); - }) + }), + this.wss.onDidChangeUsedSegments(() => this.refresh()) ); this.refresh(); @@ -99,13 +97,13 @@ export class MutatorsTreeDataProvider implements vscode.TreeDataProvider { let groups = []; - if (this.workspaceState.getFilterByDataScope()) { - switch (this.usedSegmentConfig.type) { - case "All": + if (this.uiState.getFilterByDataScope()) { + switch (this.wss.activeSegments.type) { case "WholeWorkspace": - if (!this.activeWorkspaceVersionId) { - return []; - } groups = await this.apiClient - .workspace(this.activeWorkspaceVersionId) + .workspace(this.wss.activeWorkspaceVersionId) .groupedMutators(this.workspaceMutatorGroupingAttrs); break; - case "Latest": - case "Set": - if (this.activeSegments.length === 0) { - return []; - } - for (const segmentId of this.activeSegments) { - const segGroups = await this.apiClient - .segment(segmentId) + case "Explicit": + if (this.wss.activeSegments.isAllSegments) { + groups = await this.apiClient + .workspace(this.wss.activeWorkspaceVersionId) .groupedMutators(this.workspaceMutatorGroupingAttrs); - groups.push(...segGroups); + } else { + for (const segmentId of this.wss.activeSegments.segmentIds) { + const segGroups = await this.apiClient + .segment(segmentId) + .groupedMutators(this.workspaceMutatorGroupingAttrs); + groups.push(...segGroups); + } } break; } @@ -173,25 +154,19 @@ export class MutatorsTreeDataProvider implements vscode.TreeDataProvider { let mutators = []; - if (this.workspaceState.getFilterByDataScope()) { - switch (this.usedSegmentConfig.type) { - case "All": - mutators = await this.apiClient.mutators().list(); - break; + if (this.uiState.getFilterByDataScope()) { + switch (this.wss.activeSegments.type) { case "WholeWorkspace": - if (!this.activeWorkspaceVersionId) { - return []; - } - mutators = await this.apiClient.workspace(this.activeWorkspaceVersionId).mutators(); + mutators = await this.apiClient.mutators().list(); break; - case "Latest": - case "Set": - if (this.activeSegments.length === 0) { - return []; - } - for (const segmentId of this.activeSegments) { - const segMutators = await this.apiClient.segment(segmentId).mutators(); - mutators.push(...segMutators); + case "Explicit": + if (this.wss.activeSegments.isAllSegments) { + mutators = await this.apiClient.workspace(this.wss.activeWorkspaceVersionId).mutators(); + } else { + for (const segmentId of this.wss.activeSegments.segmentIds) { + const segMutators = await this.apiClient.segment(segmentId).mutators(); + mutators.push(...segMutators); + } } break; } @@ -202,15 +177,10 @@ export class MutatorsTreeDataProvider implements vscode.TreeDataProvider { - // This is an 'uninitialized' condition - if (!this.usedSegmentConfig && this.workspaceState.getFilterByDataScope()) { - return []; - } - if (!element) { this.data = []; - if (this.workspaceState.getGroupByWorkspaceAttrs()) { + if (this.uiState.getGroupByWorkspaceAttrs()) { if (this.workspaceMutatorGroupingAttrs.length == 0) { // No workspace attrs yet return []; @@ -226,7 +196,7 @@ export class MutatorsTreeDataProvider implements vscode.TreeDataProvider m.mutator_state === "Available"); available_groups.push(available_group); - if (this.workspaceState.getShowUnavailable()) { + if (this.uiState.getShowUnavailable()) { const unavailable_group = { ...group }; unavailable_group.mutators = unavailable_group.mutators.filter( (m) => m.mutator_state !== "Available" @@ -238,7 +208,7 @@ export class MutatorsTreeDataProvider implements vscode.TreeDataProvider g.mutators.length != 0); unavailable_groups = unavailable_groups.filter((g) => g.mutators.length != 0); - if (this.workspaceState.getShowUnavailable()) { + if (this.uiState.getShowUnavailable()) { let items = []; items = available_groups.map((mut_group) => new MutatorsGroupTreeItemData(mut_group)); if (items.length !== 0) { @@ -260,7 +230,7 @@ export class MutatorsTreeDataProvider implements vscode.TreeDataProvider m.mutator_state !== "Available"); let items = []; - if (this.workspaceState.getShowUnavailable()) { + if (this.uiState.getShowUnavailable()) { items = this.generateMutatorsSubTree(available_mutators); if (items.length !== 0) { this.data.push(new MutatorsParentGroupTreeItemData("Available Mutators", items)); @@ -283,7 +253,7 @@ export class MutatorsTreeDataProvider implements vscode.TreeDataProvider; params: MutatorParameter[]; - constructor(private mutator: api.Mutator) { + constructor(mutator: api.Mutator) { this.id = mutator.mutator_id; this.state = mutator.mutator_state; this.orgMetadataAttrs = new Map(); @@ -739,10 +709,12 @@ export class Mutator { } else if (key.startsWith("mutator.params")) { const pk = key.replace("mutator.params.", ""); const pnamePrefix = pk.split(".", 1)[0]; - if (!paramAttrsByPrefix.has(pnamePrefix)) { - paramAttrsByPrefix.set(pnamePrefix, new Map()); + let paramAttrs = paramAttrsByPrefix.get(pnamePrefix); + if (paramAttrs == null) { + paramAttrs = new Map(); + paramAttrsByPrefix.set(pnamePrefix, paramAttrs); } - const paramAttrs = paramAttrsByPrefix.get(pnamePrefix); + paramAttrs.set(pk.replace(`${pnamePrefix}.`, ""), mutator.mutator_attributes[key]); } else { // Remaining are organization_custom_metadata attributes @@ -761,7 +733,7 @@ export class Mutator { export class MutatorParameter { name = ""; description?: string = undefined; - valueType: string; + valueType?: string; attrs: Map; constructor(private paramAttrs: Map) { diff --git a/vscode/src/segments.ts b/vscode/src/segments.ts index 00dce05..9eab123 100644 --- a/vscode/src/segments.ts +++ b/vscode/src/segments.ts @@ -1,21 +1,12 @@ import * as vscode from "vscode"; -import * as util from "util"; -import { isDeepStrictEqual } from "util"; -import * as child_process from "child_process"; import * as api from "./modalityApi"; -import * as cliConfig from "./cliConfig"; -import * as config from "./config"; import * as specCoverage from "./specCoverage"; import * as transitionGraph from "./transitionGraph"; +import * as workspaceState from "./workspaceState"; import { ModalityLogCommandArgs } from "./modalityLog"; -const execFile = util.promisify(child_process.execFile); - export class SegmentsTreeDataProvider implements vscode.TreeDataProvider { - activeWorkspaceVersionId: string; - usedSegmentConfig: cliConfig.ContextSegment; - activeSegmentIds: api.WorkspaceSegmentId[]; modalityView: vscode.TreeView; conformView: vscode.TreeView; deviantView: vscode.TreeView; @@ -26,12 +17,12 @@ export class SegmentsTreeDataProvider implements vscode.TreeDataProvider = this._onDidChangeTreeData.event; - private _onDidChangeUsedSegments: vscode.EventEmitter = new vscode.EventEmitter(); - readonly onDidChangeUsedSegments: vscode.Event = this._onDidChangeUsedSegments.event; - - constructor(private readonly apiClient: api.Client, private readonly cov: specCoverage.SpecCoverageProvider) {} - - register(context: vscode.ExtensionContext) { + constructor( + private readonly apiClient: api.Client, + private readonly cov: specCoverage.SpecCoverageProvider, + private wss: workspaceState.WorkspaceAndSegmentState, + context: vscode.ExtensionContext + ) { this.modalityView = vscode.window.createTreeView("auxon.modality_segments", { treeDataProvider: this, canSelectMany: true, @@ -87,7 +78,8 @@ export class SegmentsTreeDataProvider implements vscode.TreeDataProvider this.transitionGraph(itemData) - ) + ), + wss.onDidChangeUsedSegments(() => this.refresh()) ); } @@ -100,138 +92,57 @@ export class SegmentsTreeDataProvider implements vscode.TreeDataProvider { - if (element) { - return; - } - if (!this.activeWorkspaceVersionId) { - return; - } - - const usedSegmentConfig = await cliConfig.usedSegments(); - - let activeSegmentIds: api.WorkspaceSegmentId[]; - if (usedSegmentConfig.type == "Latest" || usedSegmentConfig.type == "Set") { - activeSegmentIds = (await cliConfig.activeSegments()).map((meta) => meta.id); + // only the root has children + if (element != null) { + return []; } - const workspaceSegments = await this.apiClient.workspace(this.activeWorkspaceVersionId).segments(); + const workspaceSegments = await this.apiClient.workspace(this.wss.activeWorkspaceVersionId).segments(); const items = []; for (const segment of workspaceSegments) { - let isActive = false; - switch (usedSegmentConfig.type) { - case "All": - isActive = true; - break; - - case "WholeWorkspace": - break; - - case "Latest": - case "Set": - isActive = activeSegmentIds.some((active_seg_id) => isDeepStrictEqual(active_seg_id, segment.id)); - break; - } - - items.push(new SegmentTreeItemData(segment, isActive)); + items.push(new SegmentTreeItemData(segment, this.wss.isSegmentActive(segment.id))); } items.sort((a, b) => { - if (a === null || a.name === null) { + if (a?.segment?.latest_receive_time == null) { return 1; } - if (b === null || b.name === null) { + if (b?.segment?.latest_receive_time == null) { return -1; } return a.segment.latest_receive_time - b.segment.latest_receive_time; }); - if ( - !isDeepStrictEqual(usedSegmentConfig, this.usedSegmentConfig) || - !isDeepStrictEqual(activeSegmentIds, this.activeSegmentIds) - ) { - this.usedSegmentConfig = usedSegmentConfig; - this.activeSegmentIds = activeSegmentIds; - this._onDidChangeUsedSegments.fire( - new UsedSegmentsChangeEvent(this.usedSegmentConfig, this.activeSegmentIds) - ); - } - - if (usedSegmentConfig.type == "WholeWorkspace") { + if (this.wss.isWholeWorkspaceActive()) { this.activeView.message = "The whole workspace is currently active as a single universe, without any segmentation applied."; return []; } else { - this.activeView.message = null; + this.activeView.message = undefined; return items; } } async setActiveCommand(item: SegmentTreeItemData) { - const modality = config.toolPath("modality"); - const args = [ - "segment", - "use", - "--segmentation-rule", - item.segment.id.rule_name, - item.segment.id.segment_name, - ...config.extraCliArgs("modality segment use"), - ]; - await execFile(modality, args); - this.refresh(); + await this.wss.setActiveSegments([item.segment.id]); } async setActiveFromSelectionCommand() { - const args = ["segment", "use"]; - let ruleName: string; - for (const item of this.activeView.selection) { - if (!ruleName) { - ruleName = item.segment.id.rule_name; - args.push("--segmentation-rule", item.segment.id.rule_name); - } else if (item.segment.id.rule_name != ruleName) { - // TODO can we make this possible? Might just be a cli limitation. - throw new Error("Segments from different segmentation rules cannot be used together."); - } - - args.push(item.segment.id.segment_name); - } - - for (const extra of config.extraCliArgs("modality segment use")) { - args.push(extra); - } - - await execFile(config.toolPath("modality"), args); - this.refresh(); + const segmentIds = this.activeView.selection.map((item) => item.segment.id); + this.wss.setActiveSegments(segmentIds); } async setLatestActiveCommand() { - await execFile(config.toolPath("modality"), [ - "segment", - "use", - "--latest", - ...config.extraCliArgs("modality segment use"), - ]); - this.refresh(); + await this.wss.useLatestSegment(); } async setAllActiveCommand() { - await execFile(config.toolPath("modality"), [ - "segment", - "use", - "--all-segments", - ...config.extraCliArgs("modality segment use"), - ]); - this.refresh(); + await this.wss.setAllActiveSegments(); } async setWholeWorkspaceActiveCommand() { - await execFile(config.toolPath("modality"), [ - "segment", - "use", - "--whole-workspace", - ...config.extraCliArgs("modality segment use"), - ]); - this.refresh(); + await this.wss.setWholeWorkspaceActive(); } async showSpecCoverageForSegment(item: SegmentTreeItemData) { @@ -245,13 +156,6 @@ export class SegmentsTreeDataProvider implements vscode.TreeDataProvider; - constructor(private readonly apiClient: api.Client) {} - - async initialize(context: vscode.ExtensionContext) { + private template: HandlebarsTemplateDelegate; + constructor(private readonly apiClient: api.Client, context: vscode.ExtensionContext) { const templateUri = vscode.Uri.joinPath(context.extensionUri, "resources", "specCoverage.handlebars.html"); const templateText = fs.readFileSync(templateUri.fsPath, "utf8"); @@ -206,11 +204,16 @@ function behaviorViewModel(bhCov: api.BehaviorCoverage): BehaviorViewModel { } } + let triggerCount = undefined; + if (bhCov.triggered_n_times != null) { + triggerCount = bhCov.triggered_n_times; + } + return { name: bhCov.name, executed: bhCov.test_counts.ever_executed, passed: !bhCov.test_counts.ever_failed, - triggerCount: bhCov.triggered_n_times, + triggerCount, style, isTriggered: style == "triggered", isGlobal: style == "global", diff --git a/vscode/src/specs.ts b/vscode/src/specs.ts index 001233c..c920541 100644 --- a/vscode/src/specs.ts +++ b/vscode/src/specs.ts @@ -6,6 +6,7 @@ import * as child_process from "child_process"; import * as config from "./config"; import * as specCoverage from "./specCoverage"; import * as cliConfig from "./cliConfig"; +import * as workspaceState from "./workspaceState"; const execFile = util.promisify(child_process.execFile); @@ -34,15 +35,17 @@ export class SpecsTreeDataProvider implements vscode.TreeDataProvider = this._onDidChangeTreeData.event; - workspaceState?: SpecsTreeMemento = undefined; - activeSegmentId?: api.WorkspaceSegmentId = undefined; + uiState: SpecsTreeMemento; view: vscode.TreeView; data: SpecsTreeItemData[] = []; - constructor(private readonly apiClient: api.Client, private readonly cov: specCoverage.SpecCoverageProvider) {} - - register(context: vscode.ExtensionContext) { - this.workspaceState = new SpecsTreeMemento(context.workspaceState); + constructor( + private readonly apiClient: api.Client, + private readonly cov: specCoverage.SpecCoverageProvider, + private wss: workspaceState.WorkspaceAndSegmentState, + context: vscode.ExtensionContext + ) { + this.uiState = new SpecsTreeMemento(context.workspaceState); this.view = vscode.window.createTreeView("auxon.specs", { treeDataProvider: this, canSelectMany: true, @@ -95,7 +98,8 @@ export class SpecsTreeDataProvider implements vscode.TreeDataProvider this.refresh()) ); this.refresh(); @@ -105,27 +109,18 @@ export class SpecsTreeDataProvider implements vscode.TreeDataProvider i.contextValue == "spec" && i.name == specName); if (item) { @@ -134,27 +129,31 @@ export class SpecsTreeDataProvider implements vscode.TreeDataProvider { if (!element) { this.data = []; const specs = await this.apiClient.specs().list(); - let evalSummaries: api.SpecSegmentEvalOutcomeSummary[] = []; - const showResultsOrVersions = this.workspaceState.getShowResults() || this.workspaceState.getShowVersions(); - if (!showResultsOrVersions && this.activeSegmentId) { - evalSummaries = await this.apiClient.segment(this.activeSegmentId).specSummary(); + const evalSummaries: api.SpecSegmentEvalOutcomeSummary[] = []; + const showResultsOrVersions = this.uiState.getShowResults() || this.uiState.getShowVersions(); + if (!showResultsOrVersions && this.wss.activeSegments) { + if (this.wss.activeSegments.type == "Explicit") { + for (const seg of this.wss.activeSegments.segmentIds) { + evalSummaries.push(...(await this.apiClient.segment(seg).specSummary())); + } + } } const items = await Promise.all( specs.map(async (spec) => { @@ -168,7 +167,7 @@ export class SpecsTreeDataProvider implements vscode.TreeDataProvider compare(a.name, b.name)); return this.data; } else { - return await element.children(this.apiClient, this.workspaceState); + return await element.children(this.apiClient, this.uiState); } } @@ -276,7 +275,7 @@ export class SpecsTreeDataProvider implements vscode.TreeDataProvider new SpecResultTreeItemData(result)); } + } else { + return []; } } } @@ -544,9 +545,11 @@ export class BehaviorTreeItemData extends SpecsTreeItemData { children.push(new UntilTreeItemData(name, attributes)); } - for (const [name, type, attrs] of this.structure.cases) { - removeNamesFromAttrMap(attrs); - children.push(new CaseTreeItemData(name, type, attrs)); + if (this.structure.cases != null) { + for (const [name, type, attrs] of this.structure.cases) { + removeNamesFromAttrMap(attrs); + children.push(new CaseTreeItemData(name, type, attrs)); + } } return children; diff --git a/vscode/src/speqtrLinkProvider.ts b/vscode/src/speqtrLinkProvider.ts index 75dcf74..d807ab5 100644 --- a/vscode/src/speqtrLinkProvider.ts +++ b/vscode/src/speqtrLinkProvider.ts @@ -13,6 +13,10 @@ export class SpeqtrLinkProvider implements vscode.DocumentLinkProvider { provideDocumentLinks(document: vscode.TextDocument): vscode.ProviderResult { const links: vscode.DocumentLink[] = []; for (const m of document.getText().matchAll(EVENT_AT_TIMELINE_RE)) { + if (m.index == null) { + continue; + } + const link = new vscode.DocumentLink( new vscode.Range(document.positionAt(m.index), document.positionAt(m.index + m[0].length)) ); diff --git a/vscode/src/terminalLinkProvider.ts b/vscode/src/terminalLinkProvider.ts index 955d7f3..5b74671 100644 --- a/vscode/src/terminalLinkProvider.ts +++ b/vscode/src/terminalLinkProvider.ts @@ -34,24 +34,33 @@ const EVENT_COORDS_TERMINAL_LINK_PROVIDER: vscode.TerminalLinkProvider { - activeWorkspaceVersionId: string; - usedSegmentConfig: cliConfig.ContextSegment; - activeSegments: api.WorkspaceSegmentId[]; view: vscode.TreeView; - workspaceState?: TimelinesTreeMemento; + uiState: TimelinesTreeMemento; private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - constructor(private readonly apiClient: api.Client) {} - - register(context: vscode.ExtensionContext) { - this.workspaceState = new TimelinesTreeMemento(context.workspaceState); + constructor( + private readonly apiClient: api.Client, + private wss: workspaceState.WorkspaceAndSegmentState, + context: vscode.ExtensionContext + ) { + this.uiState = new TimelinesTreeMemento(context.workspaceState); this.view = vscode.window.createTreeView("auxon.timelines", { treeDataProvider: this, canSelectMany: true, @@ -93,41 +92,27 @@ export class TimelinesTreeDataProvider implements vscode.TreeDataProvider { - // This is an 'uninitialized' condition - if (!this.usedSegmentConfig) { - return []; - } - if (element) { return element.children(); } // root element - const groupingAttrKeys = this.workspaceState.getGroupingAttrKeys(); - const groupByTimelineNameComponents = this.workspaceState.getGroupByTimelineNameComponents(); + const groupingAttrKeys = this.uiState.getGroupingAttrKeys(); + const groupByTimelineNameComponents = this.uiState.getGroupByTimelineNameComponents(); if (groupingAttrKeys.length > 0) { let groups: api.TimelineGroup[] = []; - switch (this.usedSegmentConfig.type) { - case "All": + switch (this.wss.activeSegments.type) { case "WholeWorkspace": - if (!this.activeWorkspaceVersionId) { - return []; - } groups = await this.apiClient - .workspace(this.activeWorkspaceVersionId) + .workspace(this.wss.activeWorkspaceVersionId) .groupedTimelines(groupingAttrKeys); break; - case "Latest": - case "Set": - if (this.activeSegments) { - for (const segmentId of this.activeSegments) { - const api_groups = await this.apiClient - .segment(segmentId) - .groupedTimelines(groupingAttrKeys); - for (const tl_group of api_groups) { - groups.push(tl_group); - } + case "Explicit": + for (const segmentId of this.wss.activeSegments.segmentIds) { + const api_groups = await this.apiClient.segment(segmentId).groupedTimelines(groupingAttrKeys); + for (const tl_group of api_groups) { + groups.push(tl_group); } } break; @@ -137,31 +122,24 @@ export class TimelinesTreeDataProvider implements vscode.TreeDataProvider { - if (a === null || a.name === null) { + if (!a?.name) { return 1; } - if (b === null || b.name === null) { + if (!b?.name) { return -1; } return a.name.localeCompare(b.name); @@ -170,7 +148,7 @@ export class TimelinesTreeDataProvider implements vscode.TreeDataProvider { const picked = groupingAttrKeys.find((el) => el == tlAttr) !== undefined; const label = tlAttr; @@ -241,33 +219,28 @@ export class TimelinesTreeDataProvider implements vscode.TreeDataProvider pi.label).sort()); + this.uiState.setGroupingAttrKeys(pickedItems.map((pi) => pi.label).sort()); this.refresh(); } } groupTimelinesByNameComponents() { - this.workspaceState.setGroupingAttrKeys([]); - this.workspaceState.setGroupByTimelineNameComponents(true); + this.uiState.setGroupingAttrKeys([]); + this.uiState.setGroupByTimelineNameComponents(true); this.refresh(); } async getAvailableTimelineAttrKeys(): Promise { - switch (this.usedSegmentConfig.type) { - case "All": + switch (this.wss.activeSegments.type) { case "WholeWorkspace": - if (!this.activeWorkspaceVersionId) { - return []; - } - return await this.apiClient.workspace(this.activeWorkspaceVersionId).timelineAttrKeys(); + return await this.apiClient.workspace(this.wss.activeWorkspaceVersionId).timelineAttrKeys(); - case "Latest": - case "Set": - if (!this.activeSegments) { - return []; + case "Explicit": { + if (this.wss.activeSegments.isAllSegments) { + return await this.apiClient.workspace(this.wss.activeWorkspaceVersionId).timelineAttrKeys(); } else { const keys = new Set(); - for (const segmentId of this.activeSegments) { + for (const segmentId of this.wss.activeSegments.segmentIds) { for (const key of await this.apiClient.segment(segmentId).timelineAttrKeys()) { keys.add(key); } @@ -275,6 +248,7 @@ export class TimelinesTreeDataProvider implements vscode.TreeDataProvider 0) { + } else if (this.uiState.getGroupingAttrKeys().length > 0) { groupingMode = TimelinesGroupingMode.ByAttributes; } vscode.commands.executeCommand("setContext", "auxon.timelinesGroupingMode", groupingMode); @@ -337,7 +311,7 @@ abstract class TimelineTreeItemData { } getTimelineIds(): api.TimelineId[] { - const ids = []; + const ids: api.TimelineId[] = []; this.postwalk((n: TimelineTreeItemData) => { if (n.timelineId) { ids.push(n.timelineId); @@ -378,11 +352,11 @@ export class TimelineGroupByNameTreeItemData extends TimelineTreeItemData { } insertNode(timeline: api.TimelineOverview, timelineNamePath: string[]) { - if (timelineNamePath.length == 0) { + const nextNodeName = timelineNamePath.shift(); + + if (!nextNodeName) { this.childItems.push(new TimelineLeafTreeItemData(timeline)); } else { - const nextNodeName = timelineNamePath.shift(); - let nextNodeIndex = this.childItems.findIndex((item) => item.name == nextNodeName); if (nextNodeIndex == -1) { this.childItems.push(new TimelineGroupByNameTreeItemData(nextNodeName, [])); @@ -435,20 +409,21 @@ export class TimelineGroupTreeItemData extends TimelineTreeItemData { override children(): TimelineTreeItemData[] { const timelines = this.timeline_group.timelines.sort((a, b) => { - if (a === null || a.name === null) { + if (!a?.name) { return 1; } - if (b === null || b.name === null) { + if (!b?.name) { return -1; } return a.name.localeCompare(b.name); }); + return timelines.map((timeline_overview) => new TimelineLeafTreeItemData(timeline_overview)); } } export class TimelineLeafTreeItemData extends TimelineTreeItemData { - name = ""; + name = ""; contextValue = "timeline"; iconPath = new vscode.ThemeIcon("git-commit"); @@ -456,11 +431,10 @@ export class TimelineLeafTreeItemData extends TimelineTreeItemData { super(); this.timelineId = this.timeline_overview.id; - let label = this.timeline_overview.name; - if (label === null) { - label = ""; + const label = this.timeline_overview.name; + if (label) { + this.name = label; } - this.name = label; this.description = this.timeline_overview.id; let tooltip = `- **Timeline Name**: ${this.timeline_overview.name}`; diff --git a/vscode/src/transitionGraph.ts b/vscode/src/transitionGraph.ts index 0da7228..ae3596b 100644 --- a/vscode/src/transitionGraph.ts +++ b/vscode/src/transitionGraph.ts @@ -50,7 +50,7 @@ export interface TimelineParams { type: "timelines"; title?: string; timelines: string[]; - groupBy?: string[]; + groupBy: string[]; assignNodeProps?: AssignNodeProps; } @@ -58,7 +58,7 @@ export interface SegmentParams { type: "segment"; title?: string; segmentIds: [api.WorkspaceSegmentId]; - groupBy?: string[]; + groupBy: string[]; assignNodeProps?: AssignNodeProps; } @@ -166,11 +166,11 @@ export function promptForGraphGrouping(picked: (groupBy: string[]) => void) { step1(); } -export function showGraphForTimelines(timelineIds: string[], groupBy?: string[]) { +export function showGraphForTimelines(timelineIds: string[], groupBy: string[]) { showGraph({ type: "timelines", timelines: timelineIds, groupBy }); } -export function showGraphForSegment(segmentId: api.WorkspaceSegmentId, groupBy?: string[]) { +export function showGraphForSegment(segmentId: api.WorkspaceSegmentId, groupBy: string[]) { showGraph({ type: "segment", segmentIds: [segmentId], groupBy }); } @@ -269,21 +269,24 @@ export class TransitionGraph { private async generateGraph(params: TransitionGraphParams): Promise { let res: api.GroupedGraph; - if (params.type == "timelines") { - res = await this.apiClient.timelines().groupedGraph(params.timelines, params.groupBy); - } else if (params.type == "segment") { - if (params.segmentIds.length == 1) { - res = await this.apiClient.segment(params.segmentIds[0]).groupedGraph(params.groupBy); - } else { - const timelineIds: api.TimelineId[] = []; - // Not ideal, but okay for now #2714 - for (const segmentId of params.segmentIds) { - for (const tl of await this.apiClient.segment(segmentId).timelines()) { - timelineIds.push(tl.id); + switch (params.type) { + case "timelines": + res = await this.apiClient.timelines().groupedGraph(params.timelines, params.groupBy); + break; + case "segment": + if (params.segmentIds.length == 1) { + res = await this.apiClient.segment(params.segmentIds[0]).groupedGraph(params.groupBy); + } else { + const timelineIds: api.TimelineId[] = []; + // Not ideal, but okay for now #2714 + for (const segmentId of params.segmentIds) { + for (const tl of await this.apiClient.segment(segmentId).timelines()) { + timelineIds.push(tl.id); + } } + res = await this.apiClient.timelines().groupedGraph(timelineIds, params.groupBy); } - res = await this.apiClient.timelines().groupedGraph(timelineIds, params.groupBy); - } + break; } const hideSelfEdges = @@ -316,7 +319,9 @@ export class TransitionGraph { } const graphNode = new Node(i); - graphNode.count = node.count; + if (node.count) { + graphNode.count = node.count; + } graphNode.label = title; if (params.assignNodeProps) { @@ -329,15 +334,16 @@ export class TransitionGraph { const dataProps = params.assignNodeProps.getDataProps(title); if (dataProps) { - for (const [key, value] of Object.entries(dataProps)) { - graphNode[key] = value; - } + Object.assign(graphNode, dataProps); } } const timelineIdIdx = res.attr_keys.indexOf("timeline.id"); if (timelineIdIdx != -1) { - graphNode.timelineId = node.attr_vals[timelineIdIdx]["TimelineId"]; + const timelineIdAttrVal = node.attr_vals[timelineIdIdx]; + if (timelineIdAttrVal && isTimelineId(timelineIdAttrVal)) { + graphNode.timelineId = timelineIdAttrVal.TimelineId; + } } const timelineNameIdx = res.attr_keys.indexOf("timeline.name"); if (timelineNameIdx != -1) { @@ -357,14 +363,15 @@ export class TransitionGraph { continue; } - const sourceOccurCount = res.nodes[edge.source].count; - const percent = (edge.count / sourceOccurCount) * 100; - const label = `${percent.toFixed(1)}% (${edge.count})`; - const graphEdge = new Edge(edgeIdx, edge.source, edge.destination); - graphEdge.label = label; graphEdge.count = edge.count; + const sourceOccurCount = res.nodes[edge.source]?.count; + if (sourceOccurCount) { + const percent = (edge.count / sourceOccurCount) * 100; + graphEdge.label = `${percent.toFixed(1)}% (${edge.count})`; + } + directedGraph.edges.push(graphEdge); edgeIdx++; } @@ -373,6 +380,10 @@ export class TransitionGraph { } } +function isTimelineId(value: api.AttrVal): value is { TimelineId?: api.TimelineId } { + return Object.prototype.hasOwnProperty.call(value, "TimelineId"); +} + function postNodesAndEdges(webview: vscode.Webview, graph: DirectedGraph) { if (graph === undefined) { return; @@ -418,9 +429,9 @@ class Node { } toCytoscapeObject(): cytoscape.NodeDefinition { - const data: transitionGraphWebViewApi.NodeData = { id: this.id.toString() }; + const data: transitionGraphWebViewApi.NodeData = { id: this.id.toString(), labelvalign: "center" }; - const label = this.label.replace("'", "\\'"); + const label = this.label?.replace("'", "\\'"); if (label !== undefined && label !== "") { data.label = label; } else { @@ -433,8 +444,6 @@ class Node { if (this.hasChildren) { data.labelvalign = "top"; - } else { - data.labelvalign = "center"; } // We use this to indicate nodes that can be logged from the graph context menu diff --git a/vscode/src/workspaceState.ts b/vscode/src/workspaceState.ts new file mode 100644 index 0000000..6ec28cd --- /dev/null +++ b/vscode/src/workspaceState.ts @@ -0,0 +1,231 @@ +import * as vscode from "vscode"; +import * as util from "util"; +import * as child_process from "child_process"; + +import * as cliConfig from "./cliConfig"; +import * as api from "./modalityApi"; +import * as config from "./config"; + +const execFile = util.promisify(child_process.execFile); + +/// Track the current state of the active workspace and the used segments; dispatch +/// events related to them changing. +export class WorkspaceAndSegmentState { + private _onDidChangeActiveWorkspace: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeActiveWorkspace: vscode.Event = + this._onDidChangeActiveWorkspace.event; + + private _onDidChangeUsedSegments: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeUsedSegments: vscode.Event = this._onDidChangeUsedSegments.event; + + constructor( + private apiClient: api.Client, + public activeWorkspaceName: string, + public activeWorkspaceVersionId: string, + public usedSegmentConfig: cliConfig.ContextSegment, + public activeSegments: ActiveSegments + ) {} + + static async create(apiClient: api.Client): Promise { + const activeWorkspaceName = await cliConfig.activeWorkspaceName(); + + const allWorkspaces = await apiClient.workspaces().list(); + const activeWorkspaceVersionId = allWorkspaces.find((ws) => ws.name == activeWorkspaceName)?.version_id; + + if (activeWorkspaceVersionId == null) { + if (activeWorkspaceName == "default") { + throw new Error("Cannot find workspace version for default workspace"); + } else { + vscode.window.showWarningMessage( + `Cannot find workspace with name '${activeWorkspaceName}'.\nReverting to the default workspace.` + ); + await _setActiveWorkspaceByName("default"); + return WorkspaceAndSegmentState.create(apiClient); + } + } + + const usedSegmentConfig = await cliConfig.usedSegments(); + let activeSegments: ActiveSegments; + switch (usedSegmentConfig.type) { + case "WholeWorkspace": + activeSegments = { type: "WholeWorkspace" }; + break; + + case "Set": + activeSegments = { type: "Explicit", segmentIds: usedSegmentConfig.set, isAllSegments: false }; + break; + + case "All": { + const workspaceSegments = await apiClient.workspace(activeWorkspaceVersionId).segments(); + activeSegments = { + type: "Explicit", + segmentIds: workspaceSegments.map((seg) => seg.id), + isAllSegments: true, + }; + break; + } + + case "Latest": + usedSegmentConfig.type; + activeSegments = { + type: "Explicit", + segmentIds: (await cliConfig.activeSegments()).map((meta) => meta.id), + isAllSegments: false, + }; + break; + + default: + activeSegments = { type: "Explicit", segmentIds: [], isAllSegments: false }; + break; + } + + let resetToLatestWorkspace = false; + if (activeSegments.type == "Explicit") { + for (const seg of activeSegments.segmentIds) { + if (seg.workspace_version_id != activeWorkspaceVersionId) { + resetToLatestWorkspace = true; + break; + } + } + } + if (resetToLatestWorkspace) { + vscode.window.showWarningMessage(`Active segment is for a different workspace; reverting to latest.`); + await _useLatestSegment(); + activeSegments = { + type: "Explicit", + segmentIds: (await cliConfig.activeSegments()).map((meta) => meta.id), + isAllSegments: false, + }; + } + + return new WorkspaceAndSegmentState( + apiClient, + activeWorkspaceName, + activeWorkspaceVersionId, + usedSegmentConfig, + activeSegments + ); + } + + async refresh() { + const s = await WorkspaceAndSegmentState.create(this.apiClient); + if ( + s.activeWorkspaceName != this.activeWorkspaceName || + s.activeWorkspaceVersionId != this.activeWorkspaceVersionId + ) { + this.activeWorkspaceName = s.activeWorkspaceName; + this.activeWorkspaceVersionId = s.activeWorkspaceVersionId; + + this._onDidChangeActiveWorkspace.fire(this); + } + + if ( + !util.isDeepStrictEqual(s.usedSegmentConfig, this.usedSegmentConfig) || + !util.isDeepStrictEqual(s.activeSegments, this.activeSegments) + ) { + this.usedSegmentConfig = s.usedSegmentConfig; + this.activeSegments = s.activeSegments; + this._onDidChangeUsedSegments.fire(this); + } + } + + async setActiveWorkspaceByName(workspaceName: string) { + await _setActiveWorkspaceByName(workspaceName); + this.refresh(); + } + + isWholeWorkspaceActive(): boolean { + return this.activeSegments.type == "WholeWorkspace"; + } + + isSegmentActive(segment: api.WorkspaceSegmentId): boolean { + switch (this.activeSegments.type) { + case "Explicit": + return this.activeSegments.segmentIds.findIndex((s) => util.isDeepStrictEqual(s, segment)) != -1; + case "WholeWorkspace": + return false; + } + } + + async setActiveSegments(segments: api.WorkspaceSegmentId[]) { + const args = ["segment", "use"]; + let ruleName: string | undefined = undefined; + for (const segment of segments) { + if (ruleName === undefined) { + ruleName = segment.rule_name; + args.push("--segmentation-rule", segment.rule_name); + } else if (segment.rule_name != ruleName) { + // TODO can we make this possible? Might just be a cli limitation. + vscode.window.showWarningMessage("Segments from different segmentation rules cannot be used together."); + return; + } + + args.push(segment.segment_name); + } + + for (const extra of config.extraCliArgs("modality segment use")) { + args.push(extra); + } + + await execFile(config.toolPath("modality"), args); + this.refresh(); + } + + async setAllActiveSegments() { + await execFile(config.toolPath("modality"), [ + "segment", + "use", + "--all-segments", + ...config.extraCliArgs("modality segment use"), + ]); + this.refresh(); + } + + async useLatestSegment() { + await _useLatestSegment(); + this.refresh(); + } + + async setWholeWorkspaceActive() { + await execFile(config.toolPath("modality"), [ + "segment", + "use", + "--whole-workspace", + ...config.extraCliArgs("modality segment use"), + ]); + this.refresh(); + } +} + +async function _useLatestSegment() { + await execFile(config.toolPath("modality"), [ + "segment", + "use", + "--latest", + ...config.extraCliArgs("modality segment use"), + ]); +} + +async function _setActiveWorkspaceByName(workspaceName: string) { + const modality = config.toolPath("modality"); + await execFile(modality, ["workspace", "use", workspaceName, ...config.extraCliArgs("modality workspace use")]); + await execFile(modality, ["segment", "use", "--latest", ...config.extraCliArgs("modality segment use")]); +} + +export type ActiveSegments = ExplicitActiveSegments | WholeWorkspaceActiveSegments; + +/// We're in an active segment mode where we can use the segments (Set, Latest, or All) +export interface ExplicitActiveSegments { + type: "Explicit"; + segmentIds: api.WorkspaceSegmentId[]; + + /** + * Is this all of the segments in the workspace? + */ + isAllSegments: boolean; +} + +/// We're in 'the whole workspace as one segment' mode +export interface WholeWorkspaceActiveSegments { + type: "WholeWorkspace"; +} diff --git a/vscode/src/workspaces.ts b/vscode/src/workspaces.ts index 81e8b40..6046465 100644 --- a/vscode/src/workspaces.ts +++ b/vscode/src/workspaces.ts @@ -1,28 +1,20 @@ import * as vscode from "vscode"; -import * as util from "util"; -import * as child_process from "child_process"; import * as cliConfig from "./cliConfig"; -import * as config from "./config"; import * as api from "./modalityApi"; - -const execFile = util.promisify(child_process.execFile); +import * as workspaceState from "./workspaceState"; export class WorkspacesTreeDataProvider implements vscode.TreeDataProvider { - activeWorkspaceVersionId: string; - activeWorkspaceName: string; - private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - private _onDidChangeActiveWorkspace: vscode.EventEmitter = new vscode.EventEmitter(); - readonly onDidChangeActiveWorkspace: vscode.Event = this._onDidChangeActiveWorkspace.event; - - constructor(private readonly apiClient: api.Client) {} - - register(context: vscode.ExtensionContext) { + constructor( + private readonly apiClient: api.Client, + private wss: workspaceState.WorkspaceAndSegmentState, + context: vscode.ExtensionContext + ) { context.subscriptions.push( vscode.window.createTreeView("auxon.modality_workspaces", { treeDataProvider: this, @@ -36,7 +28,8 @@ export class WorkspacesTreeDataProvider implements vscode.TreeDataProvider this.refresh()), vscode.commands.registerCommand("auxon.workspaces.setActive", (itemData) => this.setActiveWorkspaceCommand(itemData) - ) + ), + wss.onDidChangeActiveWorkspace(() => this.refresh()) ); } @@ -49,46 +42,26 @@ export class WorkspacesTreeDataProvider implements vscode.TreeDataProvider { - this.activeWorkspaceName = await cliConfig.activeWorkspaceName(); const usedSegments = await cliConfig.usedSegments(); const workspaces = await this.apiClient.workspaces().list(); const children = []; - let changed = false; for (const workspace of workspaces) { children.push( new WorkspaceTreeItemData( workspace, - workspace.name == this.activeWorkspaceName, + workspace.name == this.wss.activeWorkspaceName, usedSegments.type == "WholeWorkspace" ) ); - if (workspace.name == this.activeWorkspaceName) { - if (this.activeWorkspaceVersionId != workspace.version_id) { - this.activeWorkspaceVersionId = workspace.version_id; - changed = true; - } - } - } - - if (changed) { - this._onDidChangeActiveWorkspace.fire(this.activeWorkspaceVersionId); } return children; } async setActiveWorkspaceCommand(itemData: WorkspaceTreeItemData) { - const modality = config.toolPath("modality"); - // TODO use workspace version id for this - await execFile(modality, [ - "workspace", - "use", - itemData.workspace.name, - ...config.extraCliArgs("modality workspace use"), - ]); - await execFile(modality, ["segment", "use", "--latest", ...config.extraCliArgs("modality segment use")]); - this.refresh(); + // TODO use workspace version id for this? + await this.wss.setActiveWorkspaceByName(itemData.workspace.name); } } diff --git a/vscode/tsconfig.base.json b/vscode/tsconfig.base.json index a70067f..f797abf 100644 --- a/vscode/tsconfig.base.json +++ b/vscode/tsconfig.base.json @@ -19,6 +19,7 @@ "resolveJsonModule": true, "esModuleInterop": true, "skipLibCheck": true, + "strict": true, "plugins": [ { "name": "typescript-eslint-language-service" diff --git a/vscode/webview-src/cytoscape-cose-bilkent.d.ts b/vscode/webview-src/cytoscape-cose-bilkent.d.ts new file mode 100644 index 0000000..6229fe2 --- /dev/null +++ b/vscode/webview-src/cytoscape-cose-bilkent.d.ts @@ -0,0 +1 @@ +declare module "cytoscape-cose-bilkent"; diff --git a/vscode/webview-src/experimentImpactWebView.ts b/vscode/webview-src/experimentImpactWebView.ts index 089acce..4aa31b5 100644 --- a/vscode/webview-src/experimentImpactWebView.ts +++ b/vscode/webview-src/experimentImpactWebView.ts @@ -1,49 +1,90 @@ import * as vw from "vscode-webview"; +import * as experimentWebViewApi from "../common-src/experimentWebViewApi"; + const vscode: vw.WebviewApi = acquireVsCodeApi(); function wrapElement(el: Element, outer_tag: string) { const outerEl = document.createElement(outer_tag); + + if (el.parentNode == null) { + throw new Error("Cannot wrap element with no parent"); + } el.parentNode.insertBefore(outerEl, el); + outerEl.appendChild(el); return outerEl; } +function getExpectedAttr(el: Element, name: string): string | null { + const attr = el.attributes.getNamedItem(name); + if (attr == null) { + console.warn(`Missing expected attribute ${name}`); + return null; + } + return attr.nodeValue; +} + document.querySelectorAll("[data-scenario-name]").forEach((scenarioElement) => { - const scenarioName = scenarioElement.attributes["data-scenario-name"].nodeValue; + const scenarioName = getExpectedAttr(scenarioElement, "data-scenario-name") || "Unnamed"; // Gather the mutations, and timelines at which each occurred - const mutations = []; + const mutations: experimentWebViewApi.MutationInfo[] = []; scenarioElement.querySelectorAll("[data-mutation]").forEach((mutationElement) => { - const mutationId = mutationElement.attributes["data-mutation-id"].nodeValue; - const timelineId = mutationElement.attributes["data-timeline-id"].nodeValue; - const timelineName = mutationElement.attributes["data-timeline-name"].nodeValue; - const segmentId = { - workspace_version_id: mutationElement.attributes["data-segment-workspace-version-id"].nodeValue, - rule_name: mutationElement.attributes["data-segment-rule-name"].nodeValue, - segment_name: mutationElement.attributes["data-segment-name"].nodeValue, - }; - mutations.push({ mutationId, timelineId, timelineName, segmentId }); + const mutationId = getExpectedAttr(mutationElement, "data-mutation-id"); + const timelineId = getExpectedAttr(mutationElement, "data-timeline-id"); + const timelineName = getExpectedAttr(mutationElement, "data-timeline-name"); + const workspace_version_id = getExpectedAttr(mutationElement, "data-segment-workspace-version-id"); + const rule_name = getExpectedAttr(mutationElement, "data-segment-rule-name"); + const segment_name = getExpectedAttr(mutationElement, "data-segment-name"); + + if ( + mutationId != null && + timelineId != null && + timelineName != null && + workspace_version_id != null && + rule_name != null && + segment_name != null + ) { + mutations.push({ + mutationId, + timelineId, + timelineName, + segmentId: { workspace_version_id, rule_name, segment_name }, + }); + } }); // Gather the impacted timelines, and what was impacted at each one - const impactedTimelines = []; + const impactedTimelines: experimentWebViewApi.TimelineInfo[] = []; scenarioElement.querySelectorAll("[data-impact]").forEach((impactElement) => { - const timelineName = impactElement.attributes["data-timeline-name"].nodeValue; - const severity = impactElement.attributes["data-timeline-severity"].nodeValue; - const events = []; + const events: string[] = []; impactElement.querySelectorAll("[data-event-name]").forEach((eventElement) => { - const eventName = eventElement.attributes["data-event-name"].nodeValue; - events.push(eventName); + const eventName = getExpectedAttr(eventElement, "data-event-name"); + if (eventName != null) { + events.push(eventName); + } }); + + const timelineName = getExpectedAttr(impactElement, "data-timeline-name"); + const severityString = getExpectedAttr(impactElement, "data-timeline-severity"); const detailsHtml = impactElement.innerHTML; - impactedTimelines.push({ timelineName, severity, events, detailsHtml }); + + if (timelineName != null && severityString != null) { + const severity = Number(severityString); + if (!isNaN(severity)) { + impactedTimelines.push({ timelineName, severity, events, detailsHtml }); + } else { + console.warn("Could not parse data-timeline-severity as a number"); + } + } }); const scenarioTitleElement = scenarioElement.querySelector(".scenario-name"); - const anchorElement = wrapElement(scenarioTitleElement, "a") as HTMLLinkElement; - - const args = { scenarioName, mutations, impactedTimelines }; - const msg = { command: "visualizeImpactScenario", args }; - anchorElement.onclick = () => vscode.postMessage(msg); - anchorElement.href = "#"; + if (scenarioTitleElement != null) { + const anchorElement = wrapElement(scenarioTitleElement, "a") as HTMLLinkElement; + const args = { scenarioName, mutations, impactedTimelines }; + const msg: experimentWebViewApi.VisualizeImpactScenarioCommand = { command: "visualizeImpactScenario", args }; + anchorElement.onclick = () => vscode.postMessage(msg); + anchorElement.href = "#"; + } }); diff --git a/vscode/webview-src/transitionGraphWebView.ts b/vscode/webview-src/transitionGraphWebView.ts index 710d202..a4c5f17 100644 --- a/vscode/webview-src/transitionGraphWebView.ts +++ b/vscode/webview-src/transitionGraphWebView.ts @@ -18,29 +18,45 @@ cytoscape.use(contextMenus); const defaultZoom = 1.25; -const loadingDiv = document.getElementById("loading"); -const cyContainerDiv = document.getElementById("cy"); -const layoutDropdown = document.getElementById("layoutDropdown"); -const modeDropdown = document.getElementById("modeDropdown"); -const toolbarSave = document.getElementById("toolbarSave"); -const toolbarRefresh = document.getElementById("toolbarRefresh"); +function getRequiredElement(id: string): HTMLElement { + const el = document.getElementById(id); + if (el == null) { + throw new Error("Missing required html element, id=" + id); + } + return el; +} + +const loadingDiv = getRequiredElement("loading"); +const cyContainerDiv = getRequiredElement("cy"); +const layoutDropdown = getRequiredElement("layoutDropdown"); +const modeDropdown = getRequiredElement("modeDropdown"); +const toolbarSave = getRequiredElement("toolbarSave"); +const toolbarRefresh = getRequiredElement("toolbarRefresh"); +const detailsGrid = getRequiredElement("detailsGrid"); +const impactDetailsContainer = getRequiredElement("impactDetailsContainer"); +$(impactDetailsContainer).hide(); + const txtCanvas = document.createElement("canvas"); const txtCtx = txtCanvas.getContext("2d"); -const detailsGrid = document.getElementById("detailsGrid"); -const impactDetailsContainer = document.getElementById("impactDetailsContainer"); -$(impactDetailsContainer).hide(); const vscode: vw.WebviewApi = acquireVsCodeApi(); -// The cytoscape interface -let cy: cytoscape.Core = undefined; +// Initialize with a placeholder cytoscape; this is replaced in constructGraph +let cy: cytoscape.Core = cytoscape({}); -const validLayouts = ["cose-bilkent", "breadthfirst", "cose", "circle", "grid"]; -type LayoutType = (typeof validLayouts)[number]; +type LayoutType = "cose-bilkent" | "breadthfirst" | "cose" | "concentric" | "circle" | "grid" | "random"; +const validLayouts = ["cose-bilkent", "breadthfirst", "cose", "concentric", "circle", "grid", "random"]; function isLayout(s: string): s is LayoutType { return !!validLayouts.find((l) => s === l); } +type SelectionMode = + | "manual" + | "bidirectional-neighbors" + | "upstream-neighbors" + | "downstream-neighbors" + | "causal-descendants" + | "causal-ancestors"; const validSelectionModes = [ "manual", "bidirectional-neighbors", @@ -49,7 +65,6 @@ const validSelectionModes = [ "causal-descendants", "causal-ancestors", ]; -type SelectionMode = (typeof validSelectionModes)[number]; function isSelectionMode(s: string): s is SelectionMode { return !!validSelectionModes.find((m) => s === m); } @@ -172,7 +187,7 @@ function updateLayoutDropdown() { if (i.getAttribute("value") === persistentState.selectedLayout) { i.setAttribute("class", "selected"); layoutDropdown.setAttribute("activedescendant", `option-${j + 1}`); - layoutDropdown.setAttribute("current-value", i.getAttribute("value")); + layoutDropdown.setAttribute("current-value", persistentState.selectedLayout); break; } } @@ -208,7 +223,7 @@ function changeLayout(newLayout: LayoutType) { } } -function changeSelectionMode(newMode) { +function changeSelectionMode(newMode: SelectionMode) { if (newMode != persistentState.selectionMode) { persistentState.selectionMode = newMode; if (cy) { @@ -229,7 +244,7 @@ function updateSelectionDetails() { .filter((e) => e.hasClass("selected")) .map((e) => e.data() as transitionGraphWebViewApi.EdgeData); - let newEls = []; + let newEls: JQuery[] = []; const nodesWithEventName = selectedNodes.filter((nodeData) => nodeData.eventName !== undefined); if (nodesWithEventName?.length > 0) { @@ -278,7 +293,7 @@ function updateSelectionDetails() { } } -function eventDetailsRows(nodesWithEventName: transitionGraphWebViewApi.NodeData[]) { +function eventDetailsRows(nodesWithEventName: transitionGraphWebViewApi.NodeData[]): JQuery[] { const newEls = []; const header = $("
", { class: "vsc-grid-row header", style: "grid-column: span 3" }); @@ -297,7 +312,7 @@ function eventDetailsRows(nodesWithEventName: transitionGraphWebViewApi.NodeData return newEls; } -function timelineDetailsRows(nodesWithTimelineId: transitionGraphWebViewApi.NodeData[]) { +function timelineDetailsRows(nodesWithTimelineId: transitionGraphWebViewApi.NodeData[]): JQuery[] { const newEls = []; const header = $("
", { class: "vsc-grid-row header", style: "grid-column: span 3" }); @@ -305,7 +320,7 @@ function timelineDetailsRows(nodesWithTimelineId: transitionGraphWebViewApi.Node $("
", { class: "vsc-grid-cell column-header", text: "Timeline Id" }).appendTo(header); newEls.push(header); - const unique = []; + const unique: transitionGraphWebViewApi.NodeData[] = []; for (const n of nodesWithTimelineId) { if (!unique.some((u) => u.timeline == n.timeline && u.timelineId == n.timelineId)) { unique.push(n); @@ -318,7 +333,7 @@ function timelineDetailsRows(nodesWithTimelineId: transitionGraphWebViewApi.Node $("
", { class: "vsc-grid-cell", style: "grid-column: span 2", - text: formatTimelineId(n.timeline), + text: formatTimelineId(n.timeline || ""), }).appendTo(row); newEls.push(row); } @@ -326,7 +341,7 @@ function timelineDetailsRows(nodesWithTimelineId: transitionGraphWebViewApi.Node return newEls; } -function interactionDetailsRows(selectedEdges: transitionGraphWebViewApi.EdgeData[]) { +function interactionDetailsRows(selectedEdges: transitionGraphWebViewApi.EdgeData[]): JQuery[] { const newEls = []; const header = $("
", { class: "vsc-grid-row header", style: "grid-column: span 3" }); @@ -352,7 +367,7 @@ function interactionDetailsRows(selectedEdges: transitionGraphWebViewApi.EdgeDat return newEls; } -function cellTextForNode(node: transitionGraphWebViewApi.NodeData) { +function cellTextForNode(node: transitionGraphWebViewApi.NodeData): string { if (node.eventName && node.timelineName) { return `${node.eventName}@${node.timelineName}`; } else if (node.timelineName && node.timeline) { @@ -364,7 +379,7 @@ function cellTextForNode(node: transitionGraphWebViewApi.NodeData) { } } -function formatTimelineId(timelineId: string) { +function formatTimelineId(timelineId: string): string { let s = timelineId.replaceAll("-", ""); if (!s.startsWith("%")) { s = "%" + s; @@ -470,54 +485,85 @@ function constructGraph() { calculateLabelHeightsAndWidths(); - let layout: cytoscape.LayoutOptions = undefined; + let layout: cytoscape.LayoutOptions; if (Object.keys(persistentState.nodeCoordinates).length > 0) { - const l: cytoscape.PresetLayoutOptions = { + layout = { name: "preset", animate: false, positions: persistentState.nodeCoordinates, - }; - layout = l; - } else if (persistentState.selectedLayout === "breadthfirst") { - const l: cytoscape.BreadthFirstLayoutOptions = { - name: "breadthfirst", - directed: true, - grid: true, - spacingFactor: 1, - }; - layout = l; - } else if (persistentState.selectedLayout === "cose-bilkent") { - const l: cytoscapeExtTypes.CoseBilkentLayoutOptions = { - name: "cose-bilkent", - animate: false, - nodeDimensionsIncludeLabels: true, - nodeRepulsion: 1000000, - numIter: 5000, - }; - layout = l; - } else if (persistentState.selectedLayout === "cose") { - const l: cytoscape.CoseLayoutOptions = { - name: "cose", - animate: false, - nodeDimensionsIncludeLabels: true, - randomize: true, - gravity: 1, - nestingFactor: 1.2, - nodeRepulsion: function () { - return 1000000; - }, - nodeOverlap: 5, - componentSpacing: 5, - numIter: 5000, - }; - layout = l; - } else if (persistentState.selectedLayout === "circle" || persistentState.selectedLayout === "grid") { - const l: cytoscape.ShapedLayoutOptions = { - name: persistentState.selectedLayout, - spacingFactor: 0.5, - padding: 1, - }; - layout = l; + } as cytoscape.PresetLayoutOptions; + } else { + switch (persistentState.selectedLayout) { + case "breadthfirst": + layout = { + name: "breadthfirst", + directed: true, + grid: true, + spacingFactor: 1, + } as cytoscape.BreadthFirstLayoutOptions; + break; + + case "cose-bilkent": + layout = { + name: "cose-bilkent", + animate: false, + nodeDimensionsIncludeLabels: true, + nodeRepulsion: 1000000, + numIter: 5000, + } as cytoscapeExtTypes.CoseBilkentLayoutOptions; + break; + + case "cose": + layout = { + name: "cose", + animate: false, + nodeDimensionsIncludeLabels: true, + randomize: true, + gravity: 1, + nestingFactor: 1.2, + nodeRepulsion: function () { + return 1000000; + }, + nodeOverlap: 5, + componentSpacing: 5, + numIter: 5000, + } as cytoscape.CoseLayoutOptions; + break; + + case "concentric": + layout = { + name: "concentric", + animate: false, + nodeDimensionsIncludeLabels: true, + randomize: true, + gravity: 1, + nestingFactor: 1.2, + nodeRepulsion: function () { + return 1000000; + }, + nodeOverlap: 5, + componentSpacing: 5, + numIter: 5000, + } as cytoscape.ConcentricLayoutOptions; + break; + + case "circle": + case "grid": + layout = { + name: persistentState.selectedLayout, + spacingFactor: 0.5, + padding: 1, + } as cytoscape.ShapedLayoutOptions; + break; + + case "random": + layout = { + name: "random", + spacingFactor: 0.5, + padding: 1, + } as cytoscape.ShapedLayoutOptions; + break; + } } cy = cytoscape({ @@ -614,11 +660,12 @@ function constructGraph() { coreAsWell: false, show: false, onClickFunction: function () { - const thingsToLog = cy + const thingsToLog: string[] = cy .nodes() .filter((n) => n.hasClass("selected")) .map((n) => thingToLogForNodeData(n.data())) - .filter((thingToLog) => !!thingToLog); + // notNullOrUndefined has to be a standalone type-predicate-style function for this to typecheck + .filter(notNullOrUndefined); const msg: transitionGraphWebViewApi.LogSelectedNodesCommand = { command: "logSelectedNodes", thingsToLog, @@ -641,10 +688,17 @@ function constructGraph() { }); } +function notNullOrUndefined(value: T): value is NonNullable { + return value != null; +} + // Copied from https://github.com/CoderAllan/vscode-dgmlviewer // Copyright (c) 2021 Allan Simonsen // See the license file third_party_licenses/LICENSE_vscode-dgmlviewr function calculateLabelHeightsAndWidths() { + if (txtCtx == null) { + throw new Error("Failed to freate canvas for text metrics"); + } persistentState.nodeElements.forEach((node) => { if (node.data.label && node.data.label.length > 0) { let labelText = node.data.label;