diff --git a/vscode/generated/src/modality-api.ts b/vscode/generated/src/modality-api.ts
index eec799b..44b688c 100644
--- a/vscode/generated/src/modality-api.ts
+++ b/vscode/generated/src/modality-api.ts
@@ -17,6 +17,27 @@ export interface paths {
*/
get: operations["get_events_summary_for_timeline"];
};
+ "/v2/mutations": {
+ /**
+ * List all mutations
+ * @description List all mutations
+ */
+ get: operations["list_mutations"];
+ };
+ "/v2/mutations/{workspace_version_id}/segments/{rule_name}/{segment_name}": {
+ /**
+ * List all mutations for the given segment
+ * @description List all mutations for the given segment
+ */
+ get: operations["list_segment_mutations"];
+ };
+ "/v2/mutators": {
+ /**
+ * List mutators
+ * @description List mutators
+ */
+ get: operations["list_mutators"];
+ };
"/v2/specs": {
/**
* List all specs
@@ -255,7 +276,7 @@ export interface components {
percentage_specs_passing: number;
};
EventCoordinate: {
- opaque_event_id?: (number)[];
+ id?: (number)[];
timeline_id?: components["schemas"]["TimelineId"];
};
EventSummary: {
@@ -319,11 +340,85 @@ export interface components {
MaybeAttrVal: OneOf<["None", {
Some: components["schemas"]["AttrVal"];
}]>;
+ Mutation: {
+ /** Format: int64 */
+ created_at_utc_seconds: number;
+ linked_experiment?: string | null;
+ mutation_id: components["schemas"]["MutationId"];
+ mutator_attributes: {
+ [key: string]: components["schemas"]["AttrVal"] | undefined;
+ };
+ mutator_id: components["schemas"]["MutatorId"];
+ params: {
+ [key: string]: components["schemas"]["AttrVal"] | undefined;
+ };
+ region_details_summary?: components["schemas"]["MutationRegionDetailsSummary"] | null;
+ };
+ /** Format: uuid */
+ MutationId: string;
+ MutationRegionDetails: {
+ /**
+ * @description Maps to the "modality.mutation.clear_communicated" event name,
+ * and the `modality.mutation.success` attribute on such an event
+ */
+ clear_communicated_and_success?: ((components["schemas"]["EventCoordinate"] & (boolean | null))[]) | null;
+ /**
+ * @description Maps to the "modality.mutation.command_communicated" event name,
+ * and the `modality.mutation.success` attribute on such an event
+ */
+ command_communicated_and_success?: ((components["schemas"]["EventCoordinate"] & (boolean | null))[]) | null;
+ /**
+ * @description Maps to the "modality.mutation.injected" event name,
+ * and the `modality.mutation.success` attribute on such an event
+ */
+ inject_attempted_and_success?: ((components["schemas"]["EventCoordinate"] & (boolean | null))[]) | null;
+ };
+ MutationRegionDetailsSummary: {
+ overall: components["schemas"]["MutationRegionDetails"];
+ regions: ((components["schemas"]["RegionKind"] & components["schemas"]["MutationRegionDetails"])[])[];
+ };
+ /** @description Mutations operation errors */
+ MutationsError: OneOf<["InvalidMutatorId", {
+ /** @description Workspace not found */
+ WorkspaceNotFound: string;
+ }, "SegmentNotFound", {
+ /** @description Internal Server Error */
+ Internal: string;
+ }]>;
+ Mutator: {
+ mutator_attributes: {
+ [key: string]: components["schemas"]["AttrVal"] | undefined;
+ };
+ mutator_id: components["schemas"]["MutatorId"];
+ mutator_state: components["schemas"]["MutatorState"];
+ };
+ /** Format: uuid */
+ MutatorId: string;
+ /** @enum {string} */
+ MutatorState: "Available" | "Retired" | "TimedOut" | "Disconnected";
+ /** @description Mutator operation errors */
+ MutatorsError: OneOf<[{
+ /** @description Invalid mutator filter expression */
+ InvalidMutatorFilter: string;
+ }, {
+ /** @description Internal Server Error */
+ Internal: string;
+ }]>;
Nanoseconds: number;
+ RegionKind: OneOf<[{
+ WholeWorkspace: components["schemas"]["WholeWorkspaceRegionKind"];
+ }, {
+ Segment: components["schemas"]["SegmentRegionKind"];
+ }]>;
SegmentCoverage: {
coverage_aggregates: components["schemas"]["CoverageAggregates"];
spec_coverages: (components["schemas"]["SpecCoverage"])[];
};
+ SegmentRegionKind: {
+ id: components["schemas"]["WorkspaceSegmentId"];
+ timeline_filter?: components["schemas"]["UnstructuredTimelineFilter"] | null;
+ workspace_name: components["schemas"]["WorkspaceName"];
+ };
SegmentationRuleName: string;
SpecContent: {
metadata: components["schemas"]["SpecVersionMetadata"];
@@ -425,10 +520,21 @@ export interface components {
/** @description Internal Server Error */
Internal: string;
}]>;
+ /**
+ * @description Stringy representation of an unparsed, unstructured DSL for expressing how to filter timelines,
+ * likely through attribute evaluation.
+ */
+ UnstructuredTimelineFilter: string;
+ WholeWorkspaceRegionKind: {
+ timeline_filter?: components["schemas"]["UnstructuredTimelineFilter"] | null;
+ workspace_name: components["schemas"]["WorkspaceName"];
+ workspace_version_id: components["schemas"]["WorkspaceVersionId"];
+ };
Workspace: {
name: string;
version_id: components["schemas"]["WorkspaceVersionId"];
};
+ WorkspaceName: string;
/** @description A specific segment of a workspace. */
WorkspaceSegmentId: {
rule_name: components["schemas"]["SegmentationRuleName"];
@@ -497,6 +603,126 @@ export interface operations {
};
};
};
+ /**
+ * List all mutations
+ * @description List all mutations
+ */
+ list_mutations: {
+ parameters: {
+ query: {
+ /** @description Mutator ID */
+ mutator_id?: string | null;
+ /** @description Experiment name */
+ experiment?: string | null;
+ };
+ };
+ responses: {
+ /** @description List mutations successfully */
+ 200: {
+ content: {
+ "application/json": (components["schemas"]["Mutation"])[];
+ };
+ };
+ /** @description Invalid mutator_id */
+ 400: {
+ content: {
+ "application/json": components["schemas"]["MutationsError"];
+ };
+ };
+ /** @description Operation not authorized */
+ 403: never;
+ /** @description Internal Server Error */
+ 500: {
+ content: {
+ "application/json": components["schemas"]["MutationsError"];
+ };
+ };
+ };
+ };
+ /**
+ * List all mutations for the given segment
+ * @description List all mutations for the given segment
+ */
+ list_segment_mutations: {
+ parameters: {
+ query: {
+ /** @description Mutator ID */
+ mutator_id?: string | null;
+ /** @description Experiment name */
+ experiment?: string | null;
+ };
+ path: {
+ /** @description Workspace Version Id */
+ workspace_version_id: components["schemas"]["WorkspaceVersionId"];
+ /** @description Segmentation Rule Name */
+ rule_name: components["schemas"]["SegmentationRuleName"];
+ /** @description Segment Name */
+ segment_name: components["schemas"]["WorkspaceSegmentName"];
+ };
+ };
+ responses: {
+ /** @description List mutations successfully */
+ 200: {
+ content: {
+ "application/json": (components["schemas"]["Mutation"])[];
+ };
+ };
+ /** @description Invalid workspace_version_id */
+ 400: {
+ content: {
+ "application/json": components["schemas"]["MutationsError"];
+ };
+ };
+ /** @description Operation not authorized */
+ 403: never;
+ /** @description Workspace or segment not found */
+ 404: {
+ content: {
+ "application/json": components["schemas"]["MutationsError"];
+ };
+ };
+ /** @description Internal Server Error */
+ 500: {
+ content: {
+ "application/json": components["schemas"]["MutationsError"];
+ };
+ };
+ };
+ };
+ /**
+ * List mutators
+ * @description List mutators
+ */
+ list_mutators: {
+ parameters: {
+ query: {
+ /** @description Mutator filter expression */
+ mutator_filter?: string | null;
+ };
+ };
+ responses: {
+ /** @description List mutators successfully */
+ 200: {
+ content: {
+ "application/json": (components["schemas"]["Mutator"])[];
+ };
+ };
+ /** @description Invalid mutator_filter */
+ 400: {
+ content: {
+ "application/json": components["schemas"]["MutatorsError"];
+ };
+ };
+ /** @description Operation not authorized */
+ 403: never;
+ /** @description Internal Server Error */
+ 500: {
+ content: {
+ "application/json": components["schemas"]["MutatorsError"];
+ };
+ };
+ };
+ };
/**
* List all specs
* @description List all specs
diff --git a/vscode/images/Conform_symbol-whitesquare.svg b/vscode/images/Conform_symbol-whitesquare.svg
new file mode 100644
index 0000000..823e203
--- /dev/null
+++ b/vscode/images/Conform_symbol-whitesquare.svg
@@ -0,0 +1,48 @@
+
+
+
+
diff --git a/vscode/images/Deviant_symbol-whitesquare.svg b/vscode/images/Deviant_symbol-whitesquare.svg
new file mode 100644
index 0000000..8cdf2d1
--- /dev/null
+++ b/vscode/images/Deviant_symbol-whitesquare.svg
@@ -0,0 +1,51 @@
+
+
+
+
diff --git a/vscode/images/Modality_symbol-whitesquare.svg b/vscode/images/Modality_symbol-whitesquare.svg
new file mode 100644
index 0000000..28d36e4
--- /dev/null
+++ b/vscode/images/Modality_symbol-whitesquare.svg
@@ -0,0 +1,57 @@
+
+
+
+
diff --git a/vscode/package-lock.json b/vscode/package-lock.json
index ec5b154..e463be1 100644
--- a/vscode/package-lock.json
+++ b/vscode/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "speqtr",
- "version": "0.3.0",
+ "version": "0.5.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "speqtr",
- "version": "0.3.0",
+ "version": "0.5.1",
"dependencies": {
"@types/lodash": "^4.14.200",
"@viz-js/viz": "^3.0.1",
diff --git a/vscode/package.json b/vscode/package.json
index 621cceb..b4971aa 100644
--- a/vscode/package.json
+++ b/vscode/package.json
@@ -65,20 +65,30 @@
"viewsContainers": {
"activitybar": [
{
- "id": "auxon-explorer",
- "title": "Auxon",
- "icon": "images/Auxon_symbol-whitesquare.svg"
+ "id": "modality-explorer",
+ "title": "Auxon Modality",
+ "icon": "images/Modality_symbol-whitesquare.svg"
+ },
+ {
+ "id": "conform-explorer",
+ "title": "Auxon Conform",
+ "icon": "images/Conform_symbol-whitesquare.svg"
+ },
+ {
+ "id": "deviant-explorer",
+ "title": "Auxon Deviant",
+ "icon": "images/Deviant_symbol-whitesquare.svg"
}
]
},
"views": {
- "auxon-explorer": [
+ "modality-explorer": [
{
- "id": "auxon.workspaces",
+ "id": "auxon.modality_workspaces",
"name": "Workspaces"
},
{
- "id": "auxon.segments",
+ "id": "auxon.modality_segments",
"name": "Segments"
},
{
@@ -88,11 +98,39 @@
{
"id": "auxon.events",
"name": "Events"
+ }
+ ],
+ "conform-explorer": [
+ {
+ "id": "auxon.conform_workspaces",
+ "name": "Workspaces"
+ },
+ {
+ "id": "auxon.conform_segments",
+ "name": "Segments"
},
{
"id": "auxon.specs",
"name": "Specs"
}
+ ],
+ "deviant-explorer": [
+ {
+ "id": "auxon.deviant_workspaces",
+ "name": "Workspaces"
+ },
+ {
+ "id": "auxon.deviant_segments",
+ "name": "Segments"
+ },
+ {
+ "id": "auxon.mutators",
+ "name": "Mutators"
+ },
+ {
+ "id": "auxon.mutations",
+ "name": "Mutations"
+ }
]
},
"menus": {
@@ -126,29 +164,29 @@
"view/item/context": [
{
"command": "auxon.workspaces.setActive",
- "when": "view == auxon.workspaces && viewItem == workspace",
+ "when": "view =~ /auxon\\.[a-zA-Z]*_workspaces/ && viewItem == workspace",
"group": "navigation@1"
},
{
"command": "auxon.modality.log",
- "when": "view == auxon.segments || view == auxon.timelines || (view == auxon.events && viewItem == event)",
+ "when": "view =~ /auxon\\.[a-zA-Z]*_segments/ || view == auxon.timelines || (view == auxon.events && viewItem == event)",
"group": "inline"
},
{
"command": "auxon.segments.setActive",
- "when": "(view == auxon.segments && viewItem == segment) && !listMultiSelection"
+ "when": "(view =~ /auxon\\.[a-zA-Z]*_segments/ && viewItem == segment) && !listMultiSelection"
},
{
"command": "auxon.segments.setActiveFromSelection",
- "when": "(view == auxon.segments && viewItem == segment) && listMultiSelection"
+ "when": "(view =~ /auxon\\.[a-zA-Z]*_segments/ && viewItem == segment) && listMultiSelection"
},
{
"command": "auxon.segments.transitionGraph",
- "when": "view == auxon.segments"
+ "when": "view =~ /auxon\\.[a-zA-Z]*_segments/"
},
{
"command": "auxon.segments.specCoverage",
- "when": "view == auxon.segments"
+ "when": "view =~ /auxon\\.[a-zA-Z]*_segments/"
},
{
"command": "auxon.timelines.inspect",
@@ -287,32 +325,47 @@
"command": "auxon.specs.deleteManyResults",
"when": "view == auxon.specs && viewItem == specResult && listMultiSelection && false",
"group": "2_context@2"
+ },
+ {
+ "command": "auxon.mutators.createMutation",
+ "when": "view == auxon.mutators && viewItem == mutator",
+ "group": "inline"
+ },
+ {
+ "command": "auxon.mutations.clearMutation",
+ "when": "view == auxon.mutations && viewItem == mutation",
+ "group": "inline"
+ },
+ {
+ "command": "auxon.mutations.viewLogFromMutation",
+ "when": "view == auxon.mutations && viewItem == mutationCoordinate",
+ "group": "inline"
}
],
"view/title": [
{
"command": "auxon.workspaces.refresh",
- "when": "view == auxon.workspaces",
+ "when": "view =~ /auxon\\.[a-zA-Z]*_workspaces/",
"group": "navigation@1"
},
{
"command": "auxon.segments.refresh",
- "when": "view == auxon.segments",
+ "when": "view =~ /auxon\\.[a-zA-Z]*_segments/",
"group": "navigation@1"
},
{
"command": "auxon.segments.setLatestActive",
- "when": "view == auxon.segments",
+ "when": "view =~ /auxon\\.[a-zA-Z]*_segments/",
"group": "7_modification@1"
},
{
"command": "auxon.segments.setAllActive",
- "when": "view == auxon.segments",
+ "when": "view =~ /auxon\\.[a-zA-Z]*_segments/",
"group": "7_modification@2"
},
{
"command": "auxon.segments.setWholeWorkspaceActive",
- "when": "view == auxon.segments",
+ "when": "view =~ /auxon\\.[a-zA-Z]*_segments/",
"group": "7_modification@3"
},
{
@@ -369,6 +422,71 @@
"command": "auxon.specs.hideResults",
"when": "view == auxon.specs && auxon.specs.results == 'SHOW'",
"group": "7_modification@2"
+ },
+ {
+ "command": "auxon.deviant.clearAllMutations",
+ "when": "view == auxon.mutations",
+ "group": "7_modification@1"
+ },
+ {
+ "command": "auxon.mutators.refresh",
+ "when": "view == auxon.mutators",
+ "group": "navigation@1"
+ },
+ {
+ "command": "auxon.mutators.showUnavailable",
+ "when": "view == auxon.mutators && auxon.mutators.unavailable == 'HIDE'",
+ "group": "7_modification@1"
+ },
+ {
+ "command": "auxon.mutators.hideUnavailable",
+ "when": "view == auxon.mutators && auxon.mutators.unavailable == 'SHOW'",
+ "group": "7_modification@1"
+ },
+ {
+ "command": "auxon.mutators.groupMutatorsByName",
+ "when": "view == auxon.mutators && auxon.mutators.groupBy != 'MUTATOR_NAME'",
+ "group": "7_modification@3"
+ },
+ {
+ "command": "auxon.mutators.disableMutatorGrouping",
+ "when": "view == auxon.mutators && auxon.mutators.groupBy == 'MUTATOR_NAME'",
+ "group": "7_modification@3"
+ },
+ {
+ "command": "auxon.mutations.refresh",
+ "when": "view == auxon.mutations",
+ "group": "navigation@1"
+ },
+ {
+ "command": "auxon.mutations.groupMutationsByName",
+ "when": "view == auxon.mutations && auxon.mutations.groupBy != 'MUTATOR_NAME'",
+ "group": "7_modification@3"
+ },
+ {
+ "command": "auxon.mutations.disableMutationGrouping",
+ "when": "view == auxon.mutations && auxon.mutations.groupBy == 'MUTATOR_NAME'",
+ "group": "7_modification@3"
+ },
+ {
+ "command": "auxon.mutations.filterBySelectedMutator",
+ "when": "view == auxon.mutations && auxon.mutations.filterBy != 'MUTATOR_ID'",
+ "group": "7_modification@3"
+ },
+ {
+ "command": "auxon.mutations.disableMutationFiltering",
+ "when": "view == auxon.mutations && auxon.mutations.filterBy == 'MUTATOR_ID'",
+ "group": "7_modification@3"
+ },
+ {
+ "command": "auxon.mutations.showCleared",
+ "when": "view == auxon.mutations && auxon.mutations.cleared == 'HIDE'",
+ "group": "7_modification@1"
+ },
+ {
+ "command": "auxon.mutations.hideCleared",
+ "when": "view == auxon.mutations && auxon.mutations.cleared == 'SHOW'",
+ "group": "7_modification@1"
}
]
},
@@ -607,11 +725,104 @@
"command": "auxon.modality.log",
"title": "View Log",
"icon": "$(open-preview)"
+ },
+ {
+ "command": "auxon.deviant.clearMutation",
+ "title": "Clear Mutation"
+ },
+ {
+ "command": "auxon.deviant.clearAllMutations",
+ "title": "Clear All Mutations"
+ },
+ {
+ "command": "auxon.deviant.createMutation",
+ "title": "Create Mutation"
+ },
+ {
+ "command": "auxon.deviant.runCreateMutationWizard",
+ "title": "Create a Mutation"
+ },
+ {
+ "command": "auxon.mutators.refresh",
+ "title": "Refresh Mutator List",
+ "icon": "$(refresh)"
+ },
+ {
+ "command": "auxon.mutators.setSelectedMutator",
+ "title": "Sets the selected mutator in the mutators tree view"
+ },
+ {
+ "command": "auxon.mutators.showUnavailable",
+ "title": "Show Unavailable"
+ },
+ {
+ "command": "auxon.mutators.hideUnavailable",
+ "title": "✓ Show Unavailable"
+ },
+ {
+ "command": "auxon.mutators.groupMutatorsByName",
+ "title": "Group By Mutator Name"
+ },
+ {
+ "command": "auxon.mutators.disableMutatorGrouping",
+ "title": "✓ Group By Mutator Name"
+ },
+ {
+ "command": "auxon.mutators.createMutation",
+ "title": "Create a Mutation",
+ "icon": "$(zap)"
+ },
+ {
+ "command": "auxon.mutations.refresh",
+ "title": "Refresh Mutation List",
+ "icon": "$(refresh)"
+ },
+ {
+ "command": "auxon.mutations.setSelectedMutator",
+ "title": "Set the selected mutator in the mutations tree view"
+ },
+ {
+ "command": "auxon.mutations.setSelectedMutation",
+ "title": "Sets the selected mutation in the mutations tree view"
+ },
+ {
+ "command": "auxon.mutations.groupMutationsByName",
+ "title": "Group By Mutator Name"
+ },
+ {
+ "command": "auxon.mutations.disableMutationGrouping",
+ "title": "✓ Group By Mutator Name"
+ },
+ {
+ "command": "auxon.mutations.filterBySelectedMutator",
+ "title": "Filter By Selected Mutator"
+ },
+ {
+ "command": "auxon.mutations.disableMutationFiltering",
+ "title": "✓ Filter By Selected Mutator"
+ },
+ {
+ "command": "auxon.mutations.showCleared",
+ "title": "Show Cleared Mutations"
+ },
+ {
+ "command": "auxon.mutations.hideCleared",
+ "title": "✓ Show Cleared Mutations"
+ },
+ {
+ "command": "auxon.mutations.clearMutation",
+ "title": "Clear Mutation",
+ "icon": "$(notebook-state-error)"
+ },
+ {
+ "command": "auxon.mutations.viewLogFromMutation",
+ "title": "View Log from this Coordinate",
+ "icon": "$(open-preview)"
}
],
"configuration": {
"type": "object",
- "title": "Auxon SpeQTr",
+ "title": "Auxon",
"properties": {
"auxon.tooldir": {
"type": [
diff --git a/vscode/src/deviantCommands.ts b/vscode/src/deviantCommands.ts
new file mode 100644
index 0000000..b51d61a
--- /dev/null
+++ b/vscode/src/deviantCommands.ts
@@ -0,0 +1,179 @@
+import * as vscode from "vscode";
+import * as config from "./config";
+import * as util from "util";
+import * as child_process from "child_process";
+import { Mutator, MutatorParameter } from "./mutators";
+
+const execFile = util.promisify(child_process.execFile);
+
+export function register(context: vscode.ExtensionContext) {
+ context.subscriptions.push(
+ vscode.commands.registerCommand("auxon.deviant.clearMutation", runDeviantMutationClearCommand),
+ vscode.commands.registerCommand("auxon.deviant.clearAllMutations", clearAllMutations),
+ vscode.commands.registerCommand("auxon.deviant.createMutation", runDeviantMutationCreateCommand),
+ vscode.commands.registerCommand("auxon.deviant.runCreateMutationWizard", runCreateMutationWizard)
+ );
+}
+
+export type MutationClearCommandArgs = {
+ mutationId?: string;
+};
+
+async function clearAllMutations() {
+ await runDeviantMutationClearCommand({});
+}
+
+async function runDeviantMutationClearCommand(args: MutationClearCommandArgs) {
+ const deviantPath = config.toolPath("deviant");
+
+ const commandArgs = ["mutation", "clear"];
+
+ if (args.mutationId) {
+ commandArgs.push("--mutation-id", args.mutationId);
+ }
+
+ 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());
+ }
+
+ vscode.commands.executeCommand("auxon.mutations.refresh");
+}
+
+export type MutationCreateCommandArgs = {
+ mutatorId?: string;
+ params?: string[];
+ experimentName?: string;
+};
+
+async function runDeviantMutationCreateCommand(args: MutationCreateCommandArgs) {
+ const deviantPath = config.toolPath("deviant");
+
+ const commandArgs = ["mutation", "create", "--format", "json"];
+
+ if (args.mutatorId) {
+ commandArgs.push("--mutator-id", args.mutatorId);
+ }
+
+ if (args.experimentName) {
+ commandArgs.push("--experiment", args.experimentName);
+ }
+
+ if (args.params) {
+ for (const paramKeyValuePair of args.params) {
+ commandArgs.push("--params", paramKeyValuePair);
+ }
+ }
+
+ 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());
+ }
+
+ vscode.commands.executeCommand("auxon.mutations.refresh");
+}
+
+// TODO - add linked experiment option
+async function runCreateMutationWizard(mutator: Mutator) {
+ const title = `Create a mutation for mutator '${mutator.name}'`;
+
+ if (mutator.params.length == 0) {
+ const options = {
+ title,
+ placeHolder: "This mutator doesn't have any parameters",
+ ignoreFocusOut: true,
+ validateInput: validateParameterLess,
+ };
+ const result = await vscode.window.showInputBox(options);
+ if (result !== undefined) {
+ vscode.commands.executeCommand("auxon.deviant.createMutation", {
+ mutatorId: mutator.id,
+ });
+ }
+ } else {
+ let step = 1;
+ const maxSteps = mutator.params.length;
+ const params = [];
+
+ for (const param of mutator.params) {
+ const options = {
+ 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),
+ };
+ let paramValue = await vscode.window.showInputBox(options);
+ if (paramValue === undefined) {
+ // User canceled
+ return;
+ } else if (paramValue !== "") {
+ // Normalize so the CLI parses as a float
+ if (param.valueType === "Float" && !paramValue.includes(".")) {
+ paramValue = `${paramValue}.0`;
+ }
+
+ params.push(`${param.name}=${paramValue}`);
+ }
+
+ step += 1;
+ }
+
+ vscode.commands.executeCommand("auxon.deviant.createMutation", {
+ mutatorId: mutator.id,
+ params,
+ });
+ }
+}
+
+function validateParameterLess(input: string): string | null {
+ if (input.length === 0) {
+ return null;
+ } else {
+ return "This mutator is parameter-less. Leave the field empty to continue.";
+ }
+}
+
+function validateParameter(input: string, param: MutatorParameter): string | null {
+ if (input === "") {
+ // Empty string is allowed, means we'll use a deviant-suggested value
+ return null;
+ }
+
+ if (param.valueType === "Float") {
+ if (isFloat(input)) {
+ return null;
+ } else {
+ return "Value must be a float type";
+ }
+ } else if (param.valueType === "Integer") {
+ if (isInt(input)) {
+ return null;
+ } else {
+ return "Value must be an integer type";
+ }
+ } else {
+ return null;
+ }
+}
+
+function isFloat(val) {
+ const floatRegex = /^-?\d+(?:[.]\d*?)?$/;
+ if (!floatRegex.test(val)) return false;
+
+ val = parseFloat(val);
+ if (isNaN(val)) return false;
+ return true;
+}
+
+function isInt(val) {
+ const intRegex = /^-?\d+$/;
+ if (!intRegex.test(val)) return false;
+
+ const intVal = parseInt(val, 10);
+ return parseFloat(val) == intVal && !isNaN(intVal);
+}
diff --git a/vscode/src/main.ts b/vscode/src/main.ts
index 08a419f..7b00d8f 100644
--- a/vscode/src/main.ts
+++ b/vscode/src/main.ts
@@ -17,12 +17,15 @@ import * as specFileCommands from "./specFileCommands";
import * as transitionGraph from "./transitionGraph";
import * as config from "./config";
import * as speqtrLinkProvider from "./speqtrLinkProvider";
+import * as mutators from "./mutators";
+import * as mutations from "./mutations";
+import * as deviantCommands from "./deviantCommands";
export let log: vscode.OutputChannel;
let lspClient: LanguageClient;
export async function activate(context: vscode.ExtensionContext) {
- log = vscode.window.createOutputChannel("Auxon SpeQTr");
+ log = vscode.window.createOutputChannel("Auxon");
// If this is a fresh install, prompt for new first user creation
await user.handleNewUserCreation();
@@ -50,6 +53,7 @@ export async function activate(context: vscode.ExtensionContext) {
transitionGraph.register(context, apiClient);
specFileCommands.register(context);
speqtrLinkProvider.register(context);
+ deviantCommands.register(context);
const specCoverageProvider = new specCoverage.SpecCoverageProvider(apiClient);
await specCoverageProvider.initialize(context);
@@ -59,6 +63,8 @@ export async function activate(context: vscode.ExtensionContext) {
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);
workspacesTreeDataProvider.onDidChangeActiveWorkspace((ws_ver) => {
log.appendLine(`Active workspace change! ${ws_ver}`);
@@ -70,6 +76,9 @@ export async function activate(context: vscode.ExtensionContext) {
eventsTreeDataProvider.activeWorkspaceVersionId = ws_ver;
eventsTreeDataProvider.refresh();
+
+ mutatorsTreeDataProvider.refresh();
+ mutationsTreeDataProvider.refresh();
});
segmentsTreeDataProvider.onDidChangeUsedSegments((ev) => {
@@ -81,6 +90,9 @@ export async function activate(context: vscode.ExtensionContext) {
eventsTreeDataProvider.activeSegments = ev.activeSegmentIds;
eventsTreeDataProvider.refresh();
+
+ mutatorsTreeDataProvider.refresh();
+ mutationsTreeDataProvider.setActiveSegmentIds(ev.activeSegmentIds);
});
workspacesTreeDataProvider.register(context);
@@ -88,6 +100,8 @@ export async function activate(context: vscode.ExtensionContext) {
timelinesTreeDataProvider.register(context);
eventsTreeDataProvider.register(context);
specsTreeDataProvider.register(context);
+ mutatorsTreeDataProvider.register(context);
+ mutationsTreeDataProvider.register(context);
}
export function deactivate(): Thenable | undefined {
diff --git a/vscode/src/modality-api.json b/vscode/src/modality-api.json
index 09f2614..c31bad1 100644
--- a/vscode/src/modality-api.json
+++ b/vscode/src/modality-api.json
@@ -73,6 +73,257 @@
]
}
},
+ "/v2/mutations": {
+ "get": {
+ "tags": ["mutations"],
+ "summary": "List all mutations",
+ "description": "List all mutations",
+ "operationId": "list_mutations",
+ "parameters": [
+ {
+ "name": "mutator_id",
+ "in": "query",
+ "description": "Mutator ID",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "nullable": true
+ },
+ "explode": true
+ },
+ {
+ "name": "experiment",
+ "in": "query",
+ "description": "Experiment name",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "nullable": true
+ },
+ "explode": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "List mutations successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Mutation"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid mutator_id",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MutationsError"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Operation not authorized"
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MutationsError"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "ApiKeyAuth": ["access"]
+ }
+ ]
+ }
+ },
+ "/v2/mutations/{workspace_version_id}/segments/{rule_name}/{segment_name}": {
+ "get": {
+ "tags": ["mutations"],
+ "summary": "List all mutations for the given segment",
+ "description": "List all mutations for the given segment",
+ "operationId": "list_segment_mutations",
+ "parameters": [
+ {
+ "name": "workspace_version_id",
+ "in": "path",
+ "description": "Workspace Version Id",
+ "required": true,
+ "schema": {
+ "$ref": "#/components/schemas/WorkspaceVersionId"
+ }
+ },
+ {
+ "name": "rule_name",
+ "in": "path",
+ "description": "Segmentation Rule Name",
+ "required": true,
+ "schema": {
+ "$ref": "#/components/schemas/SegmentationRuleName"
+ }
+ },
+ {
+ "name": "segment_name",
+ "in": "path",
+ "description": "Segment Name",
+ "required": true,
+ "schema": {
+ "$ref": "#/components/schemas/WorkspaceSegmentName"
+ }
+ },
+ {
+ "name": "mutator_id",
+ "in": "query",
+ "description": "Mutator ID",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "nullable": true
+ },
+ "explode": true
+ },
+ {
+ "name": "experiment",
+ "in": "query",
+ "description": "Experiment name",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "nullable": true
+ },
+ "explode": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "List mutations successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Mutation"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid workspace_version_id",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MutationsError"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Operation not authorized"
+ },
+ "404": {
+ "description": "Workspace or segment not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MutationsError"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MutationsError"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "ApiKeyAuth": ["access"]
+ }
+ ]
+ }
+ },
+ "/v2/mutators": {
+ "get": {
+ "tags": ["mutators"],
+ "summary": "List mutators",
+ "description": "List mutators",
+ "operationId": "list_mutators",
+ "parameters": [
+ {
+ "name": "mutator_filter",
+ "in": "query",
+ "description": "Mutator filter expression",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "nullable": true
+ },
+ "explode": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "List mutators successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Mutator"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid mutator_filter",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MutatorsError"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Operation not authorized"
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MutatorsError"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "ApiKeyAuth": ["access"]
+ }
+ ]
+ }
+ },
"/v2/specs": {
"get": {
"tags": ["specs"],
@@ -2032,7 +2283,7 @@
"EventCoordinate": {
"type": "object",
"properties": {
- "opaque_event_id": {
+ "id": {
"type": "array",
"items": {
"type": "integer",
@@ -2219,9 +2470,237 @@
],
"description": "A serialization helper type, for when you actually want Option. (We're\nnot allowed to implement ToSchema on that...)"
},
+ "Mutation": {
+ "type": "object",
+ "required": ["mutation_id", "mutator_id", "params", "mutator_attributes", "created_at_utc_seconds"],
+ "properties": {
+ "created_at_utc_seconds": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "linked_experiment": {
+ "type": "string",
+ "nullable": true
+ },
+ "mutation_id": {
+ "$ref": "#/components/schemas/MutationId"
+ },
+ "mutator_attributes": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/components/schemas/AttrVal"
+ }
+ },
+ "mutator_id": {
+ "$ref": "#/components/schemas/MutatorId"
+ },
+ "params": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/components/schemas/AttrVal"
+ }
+ },
+ "region_details_summary": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/MutationRegionDetailsSummary"
+ }
+ ],
+ "nullable": true
+ }
+ }
+ },
+ "MutationId": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "MutationRegionDetails": {
+ "type": "object",
+ "properties": {
+ "clear_communicated_and_success": {
+ "type": "array",
+ "items": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/EventCoordinate"
+ },
+ {
+ "type": "boolean",
+ "nullable": true
+ }
+ ]
+ },
+ "description": "Maps to the \"modality.mutation.clear_communicated\" event name,\nand the `modality.mutation.success` attribute on such an event",
+ "nullable": true
+ },
+ "command_communicated_and_success": {
+ "type": "array",
+ "items": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/EventCoordinate"
+ },
+ {
+ "type": "boolean",
+ "nullable": true
+ }
+ ]
+ },
+ "description": "Maps to the \"modality.mutation.command_communicated\" event name,\nand the `modality.mutation.success` attribute on such an event",
+ "nullable": true
+ },
+ "inject_attempted_and_success": {
+ "type": "array",
+ "items": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/EventCoordinate"
+ },
+ {
+ "type": "boolean",
+ "nullable": true
+ }
+ ]
+ },
+ "description": "Maps to the \"modality.mutation.injected\" event name,\nand the `modality.mutation.success` attribute on such an event",
+ "nullable": true
+ }
+ }
+ },
+ "MutationRegionDetailsSummary": {
+ "type": "object",
+ "required": ["overall", "regions"],
+ "properties": {
+ "overall": {
+ "$ref": "#/components/schemas/MutationRegionDetails"
+ },
+ "regions": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/RegionKind"
+ },
+ {
+ "$ref": "#/components/schemas/MutationRegionDetails"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "MutationsError": {
+ "oneOf": [
+ {
+ "type": "string",
+ "enum": ["InvalidMutatorId"]
+ },
+ {
+ "type": "object",
+ "required": ["WorkspaceNotFound"],
+ "properties": {
+ "WorkspaceNotFound": {
+ "type": "string",
+ "description": "Workspace not found"
+ }
+ }
+ },
+ {
+ "type": "string",
+ "enum": ["SegmentNotFound"]
+ },
+ {
+ "type": "object",
+ "required": ["Internal"],
+ "properties": {
+ "Internal": {
+ "type": "string",
+ "description": "Internal Server Error"
+ }
+ }
+ }
+ ],
+ "description": "Mutations operation errors"
+ },
+ "Mutator": {
+ "type": "object",
+ "required": ["mutator_id", "mutator_attributes", "mutator_state"],
+ "properties": {
+ "mutator_attributes": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/components/schemas/AttrVal"
+ }
+ },
+ "mutator_id": {
+ "$ref": "#/components/schemas/MutatorId"
+ },
+ "mutator_state": {
+ "$ref": "#/components/schemas/MutatorState"
+ }
+ }
+ },
+ "MutatorId": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "MutatorState": {
+ "type": "string",
+ "enum": ["Available", "Retired", "TimedOut", "Disconnected"]
+ },
+ "MutatorsError": {
+ "oneOf": [
+ {
+ "type": "object",
+ "required": ["InvalidMutatorFilter"],
+ "properties": {
+ "InvalidMutatorFilter": {
+ "type": "string",
+ "description": "Invalid mutator filter expression"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": ["Internal"],
+ "properties": {
+ "Internal": {
+ "type": "string",
+ "description": "Internal Server Error"
+ }
+ }
+ }
+ ],
+ "description": "Mutator operation errors"
+ },
"Nanoseconds": {
"type": "integer"
},
+ "RegionKind": {
+ "oneOf": [
+ {
+ "type": "object",
+ "required": ["WholeWorkspace"],
+ "properties": {
+ "WholeWorkspace": {
+ "$ref": "#/components/schemas/WholeWorkspaceRegionKind"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": ["Segment"],
+ "properties": {
+ "Segment": {
+ "$ref": "#/components/schemas/SegmentRegionKind"
+ }
+ }
+ }
+ ]
+ },
"SegmentCoverage": {
"type": "object",
"required": ["spec_coverages", "coverage_aggregates"],
@@ -2237,6 +2716,26 @@
}
}
},
+ "SegmentRegionKind": {
+ "type": "object",
+ "required": ["workspace_name", "id"],
+ "properties": {
+ "id": {
+ "$ref": "#/components/schemas/WorkspaceSegmentId"
+ },
+ "timeline_filter": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/UnstructuredTimelineFilter"
+ }
+ ],
+ "nullable": true
+ },
+ "workspace_name": {
+ "$ref": "#/components/schemas/WorkspaceName"
+ }
+ }
+ },
"SegmentationRuleName": {
"type": "string"
},
@@ -2563,6 +3062,30 @@
],
"description": "Timelines operation errors"
},
+ "UnstructuredTimelineFilter": {
+ "type": "string",
+ "description": "Stringy representation of an unparsed, unstructured DSL for expressing how to filter timelines,\nlikely through attribute evaluation."
+ },
+ "WholeWorkspaceRegionKind": {
+ "type": "object",
+ "required": ["workspace_name", "workspace_version_id"],
+ "properties": {
+ "timeline_filter": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/UnstructuredTimelineFilter"
+ }
+ ],
+ "nullable": true
+ },
+ "workspace_name": {
+ "$ref": "#/components/schemas/WorkspaceName"
+ },
+ "workspace_version_id": {
+ "$ref": "#/components/schemas/WorkspaceVersionId"
+ }
+ }
+ },
"Workspace": {
"type": "object",
"required": ["name", "version_id"],
@@ -2575,6 +3098,9 @@
}
}
},
+ "WorkspaceName": {
+ "type": "string"
+ },
"WorkspaceSegmentId": {
"type": "object",
"description": "A specific segment of a workspace.",
diff --git a/vscode/src/modalityApi.ts b/vscode/src/modalityApi.ts
index 5574497..c8ab27a 100644
--- a/vscode/src/modalityApi.ts
+++ b/vscode/src/modalityApi.ts
@@ -24,6 +24,7 @@ export type CaseCoverage = gen.components["schemas"]["CaseCoverage"];
export type CoverageAggregates = gen.components["schemas"]["CoverageAggregates"];
export type LogicalTime = gen.components["schemas"]["LogicalTime"];
export type Nanoseconds = gen.components["schemas"]["Nanoseconds"];
+export type EventCoordinate = gen.components["schemas"]["EventCoordinate"];
export type SegmentationRuleName = gen.components["schemas"]["SegmentationRuleName"];
export type SpecContent = gen.components["schemas"]["SpecContent"];
export type SpecStructure = gen.components["schemas"]["SpecStructure"];
@@ -45,6 +46,12 @@ export type WorkspaceSegmentId = gen.components["schemas"]["WorkspaceSegmentId"]
export type WorkspaceSegmentMetadata = gen.components["schemas"]["WorkspaceSegmentMetadata"];
export type WorkspaceSegmentName = gen.components["schemas"]["WorkspaceSegmentName"];
export type WorkspaceVersionId = gen.components["schemas"]["WorkspaceVersionId"];
+export type MutatorId = gen.components["schemas"]["MutatorId"];
+export type MutatorState = gen.components["schemas"]["MutatorState"];
+export type Mutator = gen.components["schemas"]["Mutator"];
+export type MutationId = gen.components["schemas"]["MutatorId"];
+export type Mutation = gen.components["schemas"]["Mutation"];
+export type MutationRegionDetails = gen.components["schemas"]["MutationRegionDetails"];
type InternalClient = ReturnType>;
@@ -112,6 +119,18 @@ export class Client {
spec(specName: string): SpecClient {
return new SpecClient(this.client, specName);
}
+
+ mutators(): MutatorsClient {
+ return new MutatorsClient(this.client);
+ }
+
+ mutator(mutatorId: MutatorId): MutatorClient {
+ return new MutatorClient(this.client, mutatorId);
+ }
+
+ mutations(): MutationsClient {
+ return new MutationsClient(this.client);
+ }
}
export class WorkspacesClient {
@@ -295,6 +314,21 @@ export class SegmentClient {
return unwrapData(res);
}
+
+ async mutations(mutatorId?: MutatorId): Promise {
+ const q = [];
+ if (typeof mutatorId !== "undefined") {
+ q.push(["mutator_id", mutatorId]);
+ }
+ const res = await this.client.get("/v2/mutations/{workspace_version_id}/segments/{rule_name}/{segment_name}", {
+ params: {
+ path: this.segmentId,
+ // @ts-ignore
+ query: q,
+ },
+ });
+ return unwrapData(res);
+ }
}
export class TimelinesClient {
@@ -422,6 +456,50 @@ export class SpecVersionClient {
}
}
+export class MutatorsClient {
+ constructor(private readonly client: InternalClient) {}
+
+ async list(): Promise {
+ const res = await this.client.get("/v2/mutators", {
+ params: {
+ // @ts-ignore
+ query: [],
+ },
+ });
+ return unwrapData(res);
+ }
+}
+
+export class MutatorClient {
+ constructor(private readonly client: InternalClient, private readonly mutatorId: MutatorId) {}
+
+ async mutations(): Promise {
+ const q = [];
+ q.push(["mutator_id", this.mutatorId]);
+ const res = await this.client.get("/v2/mutations", {
+ params: {
+ // @ts-ignore
+ query: q,
+ },
+ });
+ return unwrapData(res);
+ }
+}
+
+export class MutationsClient {
+ constructor(private readonly client: InternalClient) {}
+
+ async list(): Promise {
+ const res = await this.client.get("/v2/mutations", {
+ params: {
+ // @ts-ignore
+ query: [],
+ },
+ });
+ return unwrapData(res);
+ }
+}
+
/**
* Convert a repsonse to just the data; if it's an error, throw the error.
*/
diff --git a/vscode/src/mutations.ts b/vscode/src/mutations.ts
new file mode 100644
index 0000000..d580bf0
--- /dev/null
+++ b/vscode/src/mutations.ts
@@ -0,0 +1,548 @@
+import * as vscode from "vscode";
+import * as api from "./modalityApi";
+import * as modalityLog from "./modalityLog";
+
+class MutationsTreeMemento {
+ constructor(private readonly memento: vscode.Memento) {}
+
+ getGroupByMutatorName(): boolean {
+ return this.memento.get("mutationsTree_groupByMutatorName", false);
+ }
+
+ async setGroupByMutatorName(val: boolean): Promise {
+ return this.memento.update("mutationsTree_groupByMutatorName", val);
+ }
+
+ getFilterBySelectedMutator(): boolean {
+ return this.memento.get("mutationsTree_filterBySelectedMutator", false);
+ }
+
+ async setFilterBySelectedMutator(val: boolean): Promise {
+ return this.memento.update("mutationsTree_filterBySelectedMutator", val);
+ }
+
+ getShowClearedMutations(): boolean {
+ return this.memento.get("mutationsTree_showClearedMutations", false);
+ }
+
+ async setShowClearedMutations(val: boolean): Promise {
+ return this.memento.update("mutationsTree_showClearedMutations", val);
+ }
+}
+
+export class MutationsTreeDataProvider implements vscode.TreeDataProvider {
+ private _onDidChangeTreeData: vscode.EventEmitter =
+ new vscode.EventEmitter();
+ readonly onDidChangeTreeData: vscode.Event =
+ this._onDidChangeTreeData.event;
+
+ workspaceState?: MutationsTreeMemento;
+ data: MutationsTreeItemData[];
+ view: vscode.TreeView;
+ selectedMutatorId?: api.MutatorId = undefined;
+ activeSegmentId?: api.WorkspaceSegmentId = undefined;
+
+ constructor(private readonly apiClient: api.Client) {}
+
+ register(context: vscode.ExtensionContext) {
+ this.workspaceState = new MutationsTreeMemento(context.workspaceState);
+ this.view = vscode.window.createTreeView("auxon.mutations", {
+ treeDataProvider: this,
+ canSelectMany: false,
+ });
+
+ context.subscriptions.push(
+ this.view,
+ vscode.commands.registerCommand("auxon.mutations.refresh", () => this.refresh()),
+ vscode.commands.registerCommand("auxon.mutations.setSelectedMutator", (mutatorId) => {
+ this.setSelectedMutator(mutatorId);
+ }),
+ vscode.commands.registerCommand("auxon.mutations.setSelectedMutation", (mutationId) => {
+ this.setSelectedMutation(mutationId);
+ }),
+ vscode.commands.registerCommand("auxon.mutations.disableMutationGrouping", () => {
+ this.disableMutationGrouping();
+ }),
+ vscode.commands.registerCommand("auxon.mutations.groupMutationsByName", () => {
+ this.groupMutationsByName();
+ }),
+ vscode.commands.registerCommand("auxon.mutations.disableMutationFiltering", () => {
+ this.disableMutationFiltering();
+ }),
+ vscode.commands.registerCommand("auxon.mutations.filterBySelectedMutator", () => {
+ this.filterBySelectedMutator();
+ }),
+ vscode.commands.registerCommand("auxon.mutations.showCleared", () => this.showClearedMutations(true)),
+ vscode.commands.registerCommand("auxon.mutations.hideCleared", () => this.showClearedMutations(false)),
+ vscode.commands.registerCommand("auxon.mutations.clearMutation", (itemData) => {
+ this.clearMutation(itemData);
+ }),
+ vscode.commands.registerCommand("auxon.mutations.viewLogFromMutation", (itemData) =>
+ this.viewLogFromMutation(itemData)
+ )
+ );
+
+ this.refresh();
+ }
+
+ refresh(): void {
+ vscode.commands.executeCommand(
+ "setContext",
+ "auxon.mutations.groupBy",
+ this.workspaceState.getGroupByMutatorName() ? "MUTATOR_NAME" : "NONE"
+ );
+ vscode.commands.executeCommand(
+ "setContext",
+ "auxon.mutations.filterBy",
+ this.workspaceState.getFilterBySelectedMutator() ? "MUTATOR_ID" : "NONE"
+ );
+ vscode.commands.executeCommand(
+ "setContext",
+ "auxon.mutations.cleared",
+ this.workspaceState.getShowClearedMutations() ? "SHOW" : "HIDE"
+ );
+
+ this._onDidChangeTreeData.fire(undefined);
+ }
+
+ getTreeItem(element: MutationsTreeItemData): vscode.TreeItem {
+ return element.treeItem(this.workspaceState);
+ }
+
+ async getChildren(element?: MutationsTreeItemData): Promise {
+ if (!this.activeSegmentId) {
+ return [];
+ }
+
+ if (this.workspaceState.getFilterBySelectedMutator() && this.selectedMutatorId == null) {
+ // Need a selected mutator to populate with
+ return [];
+ } else if (!element) {
+ let mutations = [];
+
+ if (this.workspaceState.getFilterBySelectedMutator()) {
+ mutations = await this.apiClient.segment(this.activeSegmentId).mutations(this.selectedMutatorId);
+ } else {
+ mutations = await this.apiClient.segment(this.activeSegmentId).mutations();
+ }
+
+ mutations = mutations.map((m) => new Mutation(m));
+ if (!this.workspaceState.getShowClearedMutations()) {
+ mutations = mutations.filter((m) => !m.wasCleared());
+ }
+
+ this.data = [];
+ if (this.workspaceState.getGroupByMutatorName()) {
+ const root = new MutationsGroupByNameTreeItemData("", []);
+ for (const m of mutations) {
+ root.insertNode(m);
+ }
+ root.updateDescriptions();
+ root.sortMutationsByCreatedAt();
+ const { compare } = Intl.Collator("en-US");
+ this.data = await root.children(this.apiClient);
+ this.data.sort((a, b) => compare(a.name, b.name));
+ } else {
+ this.data = mutations.map((m) => new NamedMutationTreeItemData(m));
+ this.data.sort((a, b) => {
+ if (!(a instanceof NamedMutationTreeItemData) || !(b instanceof NamedMutationTreeItemData)) {
+ throw new Error("Internal error: mutations tree node not of expected type");
+ }
+ // Most recent first
+ return b.mutation.createdAt.getTime() - a.mutation.createdAt.getTime();
+ });
+ }
+ return this.data;
+ } else {
+ return await element.children(this.apiClient);
+ }
+ }
+
+ getParent(element: MutationsTreeItemData): vscode.ProviderResult {
+ if (this.workspaceState.getGroupByMutatorName()) {
+ for (const group of this.data) {
+ if (!(group instanceof MutationsGroupByNameTreeItemData)) {
+ throw new Error("Internal error: mutations tree node not of expected type");
+ }
+ if (group.childItems.includes(element)) {
+ return group;
+ }
+ }
+ }
+ return undefined;
+ }
+
+ setActiveSegmentIds(segmentIds?: api.WorkspaceSegmentId[]) {
+ if (segmentIds && segmentIds.length == 1) {
+ this.activeSegmentId = segmentIds[0];
+ } else {
+ this.activeSegmentId = undefined;
+ }
+ this.refresh();
+ }
+
+ // Set the selected mutator when grouping by mutator name or only showing a single mutator
+ setSelectedMutator(mutatorId: api.MutatorId) {
+ if (this.workspaceState.getFilterBySelectedMutator()) {
+ if (this.selectedMutatorId != mutatorId) {
+ this.selectedMutatorId = mutatorId;
+ this.refresh();
+ }
+ } else if (this.workspaceState.getGroupByMutatorName() && this.selectedMutatorId != mutatorId) {
+ this.selectedMutatorId = mutatorId;
+
+ for (const group of this.data) {
+ if (!(group instanceof MutationsGroupByNameTreeItemData)) {
+ throw new Error("Internal error: mutations tree node not of expected type");
+ }
+ const item = group.childItems.find((i) => i.mutatorId == mutatorId);
+ if (item) {
+ // Just reveal the parent group
+ this.view.reveal(group, { focus: true, select: true, expand: 1 });
+ return;
+ }
+ }
+ }
+ }
+
+ setSelectedMutation(_mutationId: api.MutationId) {
+ // TODO - add this when experiments are added
+ }
+
+ disableMutationGrouping() {
+ this.workspaceState.setGroupByMutatorName(false);
+ this.selectedMutatorId = null;
+ this.refresh();
+ }
+
+ groupMutationsByName() {
+ this.workspaceState.setGroupByMutatorName(true);
+ this.selectedMutatorId = null;
+ this.refresh();
+ }
+
+ disableMutationFiltering() {
+ this.workspaceState.setFilterBySelectedMutator(false);
+ this.selectedMutatorId = null;
+ this.refresh();
+ }
+
+ filterBySelectedMutator() {
+ this.workspaceState.setFilterBySelectedMutator(true);
+ this.selectedMutatorId = null;
+ this.refresh();
+ }
+
+ showClearedMutations(show: boolean) {
+ this.workspaceState.setShowClearedMutations(show);
+ this.refresh();
+ }
+
+ clearMutation(item: MutationsTreeItemData) {
+ if (!(item instanceof NamedMutationTreeItemData)) {
+ throw new Error("Internal error: mutations tree node not of expected type");
+ }
+ vscode.commands.executeCommand("auxon.deviant.clearMutation", { mutationId: item.mutationId });
+ }
+
+ viewLogFromMutation(item: MutationsTreeItemData) {
+ if (item instanceof MutationCoordinateTreeItemData) {
+ // Encode the opaque_event_id as a string for the log command
+ let eventIdStr = "";
+ const opaque_event_id = item.coordinate.id;
+ for (const octet of opaque_event_id) {
+ if (octet != 0) {
+ eventIdStr += octet.toString(16).padStart(2, "0");
+ }
+ }
+ const literalTimelineId = "%" + item.coordinate.timeline_id.replace(/-/g, "");
+ vscode.commands.executeCommand(
+ "auxon.modality.log",
+ new modalityLog.ModalityLogCommandArgs({
+ from: `${literalTimelineId}:${eventIdStr}`,
+ })
+ );
+ }
+ }
+}
+
+// This is the base of all the tree item data classes
+abstract class MutationsTreeItemData {
+ abstract contextValue: string;
+
+ id?: string = undefined;
+ mutatorId?: api.MutatorId = undefined;
+ mutationId?: api.MutationId = undefined;
+ description?: string = undefined;
+ tooltip?: vscode.MarkdownString = undefined;
+ iconPath?: vscode.ThemeIcon = undefined;
+
+ constructor(public name: string) {}
+
+ treeItem(workspaceData: MutationsTreeMemento): vscode.TreeItem {
+ let state = vscode.TreeItemCollapsibleState.Collapsed;
+ if (!this.canHaveChildren()) {
+ state = vscode.TreeItemCollapsibleState.None;
+ }
+
+ const item = new vscode.TreeItem(this.name, state);
+ item.contextValue = this.contextValue;
+ item.description = this.description;
+ item.tooltip = this.tooltip;
+ item.iconPath = this.iconPath;
+
+ // Mutator selection populates mutations view
+ if (this.contextValue == "mutation" && !workspaceData.getFilterBySelectedMutator()) {
+ const command = {
+ title: "Sets the selected mutator in the mutators tree view",
+ command: "auxon.mutators.setSelectedMutator",
+ arguments: [this.mutatorId],
+ };
+ item.command = command;
+ }
+
+ return item;
+ }
+
+ canHaveChildren(): boolean {
+ return false;
+ }
+
+ async children(_apiClient: api.Client): Promise {
+ return [];
+ }
+}
+
+export class MutationsGroupByNameTreeItemData extends MutationsTreeItemData {
+ contextValue = "mutationsGroup";
+ constructor(public name: string, public childItems: MutationsTreeItemData[]) {
+ super(name);
+ super.iconPath = new vscode.ThemeIcon("replace-all");
+ const tooltip = `- **Mutator Name**: ${name}`;
+ this.tooltip = new vscode.MarkdownString(tooltip);
+ }
+
+ override canHaveChildren(): boolean {
+ return true;
+ }
+
+ override async children(_apiClient: api.Client): Promise {
+ return this.childItems;
+ }
+
+ insertNode(mutation: Mutation) {
+ let nextNodeIndex = this.childItems.findIndex((item) => item.name == mutation.mutatorName);
+ if (nextNodeIndex == -1) {
+ this.childItems.push(new MutationsGroupByNameTreeItemData(mutation.mutatorName, []));
+ nextNodeIndex = this.childItems.length - 1;
+ }
+
+ const nextNode = this.childItems[nextNodeIndex];
+ if (!(nextNode instanceof MutationsGroupByNameTreeItemData)) {
+ throw new Error("Internal error: mutations tree node not of expected type");
+ }
+ nextNode.childItems.push(new NamedMutationTreeItemData(mutation));
+ }
+
+ updateDescriptions() {
+ for (const group of this.childItems) {
+ if (group instanceof MutationsGroupByNameTreeItemData && group.childItems.length > 0) {
+ const mutationCount = group.childItems.length;
+ const mutatorIds = new Set();
+ for (const item of group.childItems) {
+ mutatorIds.add(item.mutatorId);
+ }
+ group.description = `${mutationCount} mutation`;
+ if (mutationCount > 1) {
+ group.description += "s";
+ }
+ group.description += `, ${mutatorIds.size} mutator`;
+ if (mutatorIds.size > 1) {
+ group.description += "s";
+ }
+ }
+ }
+ }
+
+ sortMutationsByCreatedAt() {
+ for (const group of this.childItems) {
+ if (group instanceof MutationsGroupByNameTreeItemData && group.childItems.length > 0) {
+ group.childItems.sort((a, b) => {
+ if (!(a instanceof NamedMutationTreeItemData) || !(b instanceof NamedMutationTreeItemData)) {
+ throw new Error("Internal error: mutations tree node not of expected type");
+ }
+ // Most recent first
+ return b.mutation.createdAt.getTime() - a.mutation.createdAt.getTime();
+ });
+ }
+ }
+ }
+}
+
+export class NamedMutationTreeItemData extends MutationsTreeItemData {
+ contextValue = "mutation";
+ constructor(public mutation: Mutation) {
+ super(mutation.mutatorName);
+ this.id = `${mutation.mutationId}`;
+ this.mutatorId = mutation.mutatorId;
+ this.mutationId = mutation.mutationId;
+ this.description = mutation.mutationId;
+ let tooltip = `- **Mutator Name**: ${mutation.mutatorName}`;
+ tooltip += `\n- **Mutator Id**: ${mutation.mutatorId}`;
+ tooltip += `\n- **Mutation Id**: ${mutation.mutationId}`;
+ this.tooltip = new vscode.MarkdownString(tooltip);
+ super.iconPath = new vscode.ThemeIcon("zap");
+ }
+
+ override canHaveChildren(): boolean {
+ return true;
+ }
+
+ override async children(apiClient: api.Client): Promise {
+ const children = [];
+ children.push(new MutationDetailLeafTreeItemData(`Created At: ${this.mutation.createdAt}`));
+ if (this.mutation.experimentName) {
+ children.push(new MutationDetailLeafTreeItemData(`Experiment: ${this.mutation.experimentName}`));
+ }
+
+ if (this.mutation.regionDetailsSummary && this.mutation.regionDetailsSummary.command_communicated_and_success) {
+ const coord = this.mutation.regionDetailsSummary.command_communicated_and_success[0];
+ const maybeSuccess = this.mutation.regionDetailsSummary.command_communicated_and_success[1];
+ const timeline = await apiClient.timeline(coord.timeline_id).get();
+ let timelineStr = `${coord.timeline_id}`;
+ if (Object.prototype.hasOwnProperty.call(timeline.attributes, "timeline.name")) {
+ timelineStr = timeline.attributes["timeline.name"] as string;
+ }
+ children.push(
+ new MutationCoordinateTreeItemData(`Communicated Timeline: ${timelineStr}`, coord, maybeSuccess)
+ );
+ }
+ if (this.mutation.regionDetailsSummary && this.mutation.regionDetailsSummary.inject_attempted_and_success) {
+ const coord = this.mutation.regionDetailsSummary.inject_attempted_and_success[0];
+ const maybeSuccess = this.mutation.regionDetailsSummary.inject_attempted_and_success[1];
+ const timeline = await apiClient.timeline(coord.timeline_id).get();
+ let timelineStr = `${coord.timeline_id}`;
+ if (Object.prototype.hasOwnProperty.call(timeline.attributes, "timeline.name")) {
+ timelineStr = timeline.attributes["timeline.name"] as string;
+ }
+ children.push(new MutationCoordinateTreeItemData(`Injected Timeline: ${timelineStr}`, coord, maybeSuccess));
+ }
+ if (this.mutation.regionDetailsSummary && this.mutation.regionDetailsSummary.clear_communicated_and_success) {
+ const coord = this.mutation.regionDetailsSummary.clear_communicated_and_success[0];
+ const maybeSuccess = this.mutation.regionDetailsSummary.clear_communicated_and_success[1];
+ const timeline = await apiClient.timeline(coord.timeline_id).get();
+ let timelineStr = `${coord.timeline_id}`;
+ if (Object.prototype.hasOwnProperty.call(timeline.attributes, "timeline.name")) {
+ timelineStr = timeline.attributes["timeline.name"] as string;
+ }
+ children.push(new MutationCoordinateTreeItemData(`Cleared Timeline: ${timelineStr}`, coord, maybeSuccess));
+ }
+
+ if (this.mutation.params.size != 0) {
+ children.push(new MutationParametersTreeItemData(this.mutation));
+ }
+ return children;
+ }
+}
+
+export class MutationParametersTreeItemData extends MutationsTreeItemData {
+ contextValue = "mutationParameters";
+ constructor(public mutation: Mutation) {
+ super("Parameters");
+ super.iconPath = new vscode.ThemeIcon("output");
+ }
+
+ override canHaveChildren(): boolean {
+ return true;
+ }
+
+ override async children(_apiClient: api.Client): Promise {
+ const children = [];
+ for (const [paramName, paramValue] of this.mutation.params) {
+ children.push(new MutationDetailLeafTreeItemData(`${paramName}: ${paramValue}`));
+ }
+ return children;
+ }
+}
+
+export class MutationCoordinateTreeItemData extends MutationsTreeItemData {
+ contextValue = "mutationCoordinate";
+ constructor(public name: string, public coordinate: api.EventCoordinate, public maybeSuccess?: boolean) {
+ super(name);
+ this.iconPath = new vscode.ThemeIcon("git-commit");
+ }
+
+ override canHaveChildren(): boolean {
+ return true;
+ }
+
+ override async children(_apiClient: api.Client): Promise {
+ const children = [];
+
+ let msg = "NA";
+ let icon = new vscode.ThemeIcon("question", new vscode.ThemeColor("testing.iconQueued"));
+ if (this.maybeSuccess === true) {
+ msg = "Yes";
+ icon = new vscode.ThemeIcon("verified-filled", new vscode.ThemeColor("testing.iconPassed"));
+ } else if (this.maybeSuccess === false) {
+ msg = "No";
+ icon = new vscode.ThemeIcon("testing-failed-icon", new vscode.ThemeColor("testing.iconFailed"));
+ }
+
+ const item = new MutationDetailLeafTreeItemData(`Was Successful: ${msg}`);
+ item.iconPath = icon;
+ children.push(item);
+
+ return children;
+ }
+}
+
+export class MutationDetailLeafTreeItemData extends MutationsTreeItemData {
+ contextValue = "mutationDetail";
+ constructor(public name: string) {
+ super(name);
+ }
+}
+
+class Mutation {
+ mutatorId: api.MutatorId;
+ mutatorName = "";
+ mutatorDescription?: string = undefined;
+ mutationId: api.MutationId;
+ createdAt: Date;
+ experimentName?: string = undefined;
+ params: Map;
+ // N.B. currently just surfacing the overall summary, could consider
+ // the per-region summary if we expand beyond single segments
+ regionDetailsSummary?: api.MutationRegionDetails;
+
+ constructor(private mutation: api.Mutation) {
+ this.mutatorId = mutation.mutator_id;
+ this.mutationId = mutation.mutation_id;
+ this.createdAt = new Date(0);
+ this.createdAt.setUTCSeconds(mutation.created_at_utc_seconds);
+ if (mutation.linked_experiment) {
+ this.experimentName = mutation.linked_experiment;
+ }
+ if (Object.prototype.hasOwnProperty.call(mutation.mutator_attributes, "mutator.name")) {
+ this.mutatorName = mutation.mutator_attributes["mutator.name"] as string;
+ }
+ if (Object.prototype.hasOwnProperty.call(mutation.mutator_attributes, "mutator.description")) {
+ this.mutatorDescription = mutation.mutator_attributes["mutator.description"] as string;
+ }
+ this.params = new Map();
+ for (const [paramName, paramValue] of Object.entries(mutation.params)) {
+ this.params.set(paramName, paramValue);
+ }
+ if (mutation.region_details_summary) {
+ this.regionDetailsSummary = mutation.region_details_summary.overall;
+ }
+ }
+
+ wasCleared(): boolean {
+ if (this.regionDetailsSummary && this.regionDetailsSummary.clear_communicated_and_success) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/vscode/src/mutators.ts b/vscode/src/mutators.ts
new file mode 100644
index 0000000..0fb1b3f
--- /dev/null
+++ b/vscode/src/mutators.ts
@@ -0,0 +1,445 @@
+import * as vscode from "vscode";
+import * as api from "./modalityApi";
+
+class MutatorsTreeMemento {
+ constructor(private readonly memento: vscode.Memento) {}
+
+ getShowUnavailable(): boolean {
+ return this.memento.get("mutatorsTree_showUnavailable", false);
+ }
+
+ async setShowUnavailable(val: boolean): Promise {
+ return this.memento.update("mutatorsTree_showUnavailable", val);
+ }
+
+ getGroupByMutatorName(): boolean {
+ return this.memento.get("mutatorsTree_groupByMutatorName", true);
+ }
+
+ async setGroupByMutatorName(val: boolean): Promise {
+ return this.memento.update("mutatorsTree_groupByMutatorName", val);
+ }
+}
+
+export class MutatorsTreeDataProvider implements vscode.TreeDataProvider {
+ private _onDidChangeTreeData: vscode.EventEmitter =
+ new vscode.EventEmitter();
+ readonly onDidChangeTreeData: vscode.Event =
+ this._onDidChangeTreeData.event;
+
+ workspaceState?: MutatorsTreeMemento;
+ data: MutatorsTreeItemData[];
+ view: vscode.TreeView;
+
+ constructor(private readonly apiClient: api.Client) {}
+
+ register(context: vscode.ExtensionContext) {
+ this.workspaceState = new MutatorsTreeMemento(context.workspaceState);
+ this.view = vscode.window.createTreeView("auxon.mutators", {
+ treeDataProvider: this,
+ canSelectMany: false,
+ });
+
+ context.subscriptions.push(
+ this.view,
+ vscode.commands.registerCommand("auxon.mutators.refresh", () => this.refresh()),
+ vscode.commands.registerCommand("auxon.mutators.showUnavailable", () => this.showUnavailable(true)),
+ vscode.commands.registerCommand("auxon.mutators.hideUnavailable", () => this.showUnavailable(false)),
+ vscode.commands.registerCommand("auxon.mutators.setSelectedMutator", (mutatorId) => {
+ this.setSelectedMutator(mutatorId);
+ }),
+ vscode.commands.registerCommand("auxon.mutators.disableMutatorGrouping", () => {
+ this.disableMutatorGrouping();
+ }),
+ vscode.commands.registerCommand("auxon.mutators.groupMutatorsByName", () => {
+ this.groupMutatorsByName();
+ }),
+ vscode.commands.registerCommand("auxon.mutators.createMutation", (itemData) => {
+ this.createMutation(itemData);
+ })
+ );
+
+ this.refresh();
+ }
+
+ refresh(): void {
+ vscode.commands.executeCommand(
+ "setContext",
+ "auxon.mutators.unavailable",
+ this.workspaceState.getShowUnavailable() ? "SHOW" : "HIDE"
+ );
+ vscode.commands.executeCommand(
+ "setContext",
+ "auxon.mutators.groupBy",
+ this.workspaceState.getGroupByMutatorName() ? "MUTATOR_NAME" : "NONE"
+ );
+
+ this._onDidChangeTreeData.fire(undefined);
+ }
+
+ showUnavailable(show: boolean) {
+ this.workspaceState.setShowUnavailable(show);
+ this.refresh();
+ }
+
+ getTreeItem(element: MutatorsTreeItemData): vscode.TreeItem {
+ return element.treeItem();
+ }
+
+ async getChildren(element?: MutatorsTreeItemData): Promise {
+ if (!element) {
+ let mutators = await this.apiClient.mutators().list();
+ if (!this.workspaceState.getShowUnavailable()) {
+ mutators = mutators.filter((m) => m.mutator_state === "Available");
+ }
+ let items = [];
+ if (this.workspaceState.getGroupByMutatorName()) {
+ const root = new MutatorsGroupByNameTreeItemData("", []);
+ for (const m of mutators) {
+ root.insertNode(new Mutator(m));
+ }
+ root.updateDescriptions();
+ items = root.children();
+ } else {
+ items = mutators.map((m) => new NamedMutatorTreeItemData(new Mutator(m)));
+ }
+ const { compare } = Intl.Collator("en-US");
+ this.data = items.sort((a, b) => compare(a.name, b.name));
+ return this.data;
+ } else {
+ return element.children();
+ }
+ }
+
+ getParent(element: MutatorsTreeItemData): vscode.ProviderResult {
+ if (this.workspaceState.getGroupByMutatorName()) {
+ for (const group of this.data) {
+ if (!(group instanceof MutatorsGroupByNameTreeItemData)) {
+ throw new Error("Internal error: mutators tree node not of expected type");
+ }
+ if (group.childItems.includes(element)) {
+ return group;
+ }
+ }
+ }
+ return undefined;
+ }
+
+ setSelectedMutator(mutatorId: api.MutatorId) {
+ if (this.workspaceState.getGroupByMutatorName()) {
+ for (const group of this.data) {
+ if (!(group instanceof MutatorsGroupByNameTreeItemData)) {
+ throw new Error("Internal error: mutators tree node not of expected type");
+ }
+ const item = group.childItems.find((i) => i.mutatorId == mutatorId);
+ if (item) {
+ // Treeview doesn't appear to handle selecting nested items well.
+ // Instead we need to reveal the parent first then the item
+ this.view.reveal(group, { focus: true, select: true, expand: 1 }).then(() => {
+ this.view.reveal(item, { focus: true, select: true, expand: 10 });
+ });
+ return;
+ }
+ }
+ } else {
+ const item = this.data.find((i) => i.mutatorId == mutatorId);
+ if (item) {
+ this.view.reveal(item, { focus: true, select: true, expand: 10 });
+ }
+ }
+ }
+
+ disableMutatorGrouping() {
+ this.workspaceState.setGroupByMutatorName(false);
+ this.refresh();
+ }
+
+ groupMutatorsByName() {
+ this.workspaceState.setGroupByMutatorName(true);
+ this.refresh();
+ }
+
+ createMutation(item: MutatorsTreeItemData) {
+ if (!(item instanceof NamedMutatorTreeItemData)) {
+ throw new Error("Internal error: mutators tree node not of expected type");
+ }
+ // N.B. we could check for (un)available status here first, but currently we
+ // just pipe the CLI stderr message through
+ vscode.commands.executeCommand("auxon.deviant.runCreateMutationWizard", item.mutator);
+ }
+}
+
+// This is the base of all the tree item data classes
+abstract class MutatorsTreeItemData {
+ abstract contextValue: string;
+
+ id?: string = undefined;
+ mutatorId?: api.MutatorId = undefined;
+ description?: string = undefined;
+ tooltip?: vscode.MarkdownString = undefined;
+ iconPath?: vscode.ThemeIcon = undefined;
+
+ constructor(public name: string) {}
+
+ treeItem(): vscode.TreeItem {
+ let state = vscode.TreeItemCollapsibleState.Collapsed;
+ if (!this.canHaveChildren()) {
+ state = vscode.TreeItemCollapsibleState.None;
+ }
+
+ const item = new vscode.TreeItem(this.name, state);
+ item.contextValue = this.contextValue;
+ item.description = this.description;
+ item.tooltip = this.tooltip;
+ item.iconPath = this.iconPath;
+
+ // Mutator selection populates mutations view
+ if (this.contextValue == "mutator") {
+ const command = {
+ title: "Set the selected mutator in the mutations tree view",
+ command: "auxon.mutations.setSelectedMutator",
+ arguments: [this.mutatorId],
+ };
+ item.command = command;
+ }
+
+ return item;
+ }
+
+ canHaveChildren(): boolean {
+ return false;
+ }
+
+ children(): MutatorsTreeItemData[] {
+ return [];
+ }
+}
+
+export class MutatorsGroupByNameTreeItemData extends MutatorsTreeItemData {
+ contextValue = "mutatorsGroup";
+ constructor(public name: string, public childItems: MutatorsTreeItemData[]) {
+ super(name);
+ super.iconPath = new vscode.ThemeIcon("github-action");
+ const tooltip = `- **Mutator Name**: ${name}`;
+ this.tooltip = new vscode.MarkdownString(tooltip);
+ }
+
+ override canHaveChildren(): boolean {
+ return true;
+ }
+
+ override children(): MutatorsTreeItemData[] {
+ return this.childItems;
+ }
+
+ insertNode(mutator: Mutator) {
+ let nextNodeIndex = this.childItems.findIndex((item) => item.name == mutator.name);
+ if (nextNodeIndex == -1) {
+ this.childItems.push(new MutatorsGroupByNameTreeItemData(mutator.name, []));
+ nextNodeIndex = this.childItems.length - 1;
+ }
+
+ const nextNode = this.childItems[nextNodeIndex];
+ if (!(nextNode instanceof MutatorsGroupByNameTreeItemData)) {
+ throw new Error("Internal error: mutators tree node not of expected type");
+ }
+ nextNode.childItems.push(new NamedMutatorTreeItemData(mutator));
+ }
+
+ updateDescriptions() {
+ for (const group of this.childItems) {
+ if (group instanceof MutatorsGroupByNameTreeItemData && group.childItems.length > 0) {
+ const mutatorCount = group.childItems.length;
+ group.description = `${mutatorCount} mutator`;
+ if (mutatorCount > 1) {
+ group.description += "s";
+ }
+ }
+ }
+ }
+}
+
+export class NamedMutatorTreeItemData extends MutatorsTreeItemData {
+ contextValue = "mutator";
+ constructor(public mutator: Mutator) {
+ super(mutator.name);
+ this.id = `${mutator.id}`;
+ this.mutatorId = mutator.id;
+ this.description = mutator.id;
+ let tooltip = `- **Mutator Name**: ${mutator.name}`;
+ tooltip += `\n- **Mutator Id**: ${mutator.id}`;
+ this.tooltip = new vscode.MarkdownString(tooltip);
+ this.iconPath = new vscode.ThemeIcon("outline-view-icon");
+ }
+
+ override canHaveChildren(): boolean {
+ return true;
+ }
+
+ override children(): MutatorsTreeItemData[] {
+ const children = [];
+ if (this.mutator.description) {
+ children.push(new MutatorDetailLeafTreeItemData(`${this.mutator.description}`));
+ }
+ if (this.mutator.layer) {
+ children.push(new MutatorDetailLeafTreeItemData(`Layer: ${this.mutator.layer}`));
+ }
+ if (this.mutator.operation) {
+ children.push(new MutatorDetailLeafTreeItemData(`Operation: ${this.mutator.operation}`));
+ }
+ if (this.mutator.group) {
+ children.push(new MutatorDetailLeafTreeItemData(`Group: ${this.mutator.group}`));
+ }
+ children.push(new MutatorDetailLeafTreeItemData(`State: ${this.mutator.state}`));
+ if (this.mutator.orgMetadataAttrs.size != 0) {
+ children.push(new MutatorOrgMetadataTreeItemData(this.mutator.orgMetadataAttrs));
+ }
+ if (this.mutator.params.length != 0) {
+ children.push(new MutatorParametersTreeItemData(this.mutator.params));
+ }
+ return children;
+ }
+}
+
+export class MutatorOrgMetadataTreeItemData extends MutatorsTreeItemData {
+ contextValue = "mutatorOrgMetadata";
+ constructor(public orgMetadataAttrs: Map) {
+ super("Organization Metadata");
+ super.iconPath = new vscode.ThemeIcon("organization");
+ }
+
+ override canHaveChildren(): boolean {
+ return true;
+ }
+
+ override children(): MutatorsTreeItemData[] {
+ const children = [];
+ for (const [k, v] of this.orgMetadataAttrs) {
+ children.push(new MutatorDetailLeafTreeItemData(`${k}: ${v}`));
+ }
+ return children;
+ }
+}
+
+export class MutatorParametersTreeItemData extends MutatorsTreeItemData {
+ contextValue = "mutatorParameters";
+ constructor(public params: MutatorParameter[]) {
+ super("Parameters");
+ super.iconPath = new vscode.ThemeIcon("output");
+ }
+
+ override canHaveChildren(): boolean {
+ return true;
+ }
+
+ override children(): MutatorsTreeItemData[] {
+ const children = [];
+ for (const p of this.params) {
+ children.push(new MutatorParameterTreeItemData(p));
+ }
+ return children;
+ }
+}
+
+export class MutatorParameterTreeItemData extends MutatorsTreeItemData {
+ contextValue = "mutatorParameter";
+ constructor(public param: MutatorParameter) {
+ super(param.name);
+ }
+
+ override canHaveChildren(): boolean {
+ return true;
+ }
+
+ override children(): MutatorsTreeItemData[] {
+ const children = [];
+ if (this.param.description) {
+ children.push(new MutatorDetailLeafTreeItemData(`${this.param.description}`));
+ }
+ children.push(new MutatorDetailLeafTreeItemData(`value_type: ${this.param.valueType}`));
+ for (const [k, v] of this.param.attrs) {
+ children.push(new MutatorDetailLeafTreeItemData(`${k}: ${v}`));
+ }
+ return children;
+ }
+}
+
+export class MutatorDetailLeafTreeItemData extends MutatorsTreeItemData {
+ contextValue = "mutatorDetail";
+ constructor(public name: string) {
+ super(name);
+ }
+}
+
+export class Mutator {
+ id: api.MutatorId;
+ state: api.MutatorState;
+ name = "";
+ description?: string = undefined;
+ layer?: string = undefined;
+ operation?: string = undefined;
+ group?: string = undefined;
+ orgMetadataAttrs: Map;
+ params: MutatorParameter[];
+
+ constructor(private mutator: api.Mutator) {
+ this.id = mutator.mutator_id;
+ this.state = mutator.mutator_state;
+ this.orgMetadataAttrs = new Map();
+ this.params = [];
+ const paramAttrsByPrefix: Map> = new Map();
+
+ for (const key in mutator.mutator_attributes) {
+ if (key == "mutator.name") {
+ this.name = mutator.mutator_attributes[key] as string;
+ } else if (key == "mutator.description") {
+ this.description = mutator.mutator_attributes[key] as string;
+ } else if (key == "mutator.layer") {
+ this.layer = mutator.mutator_attributes[key] as string;
+ } else if (key == "mutator.operation") {
+ this.operation = mutator.mutator_attributes[key] as string;
+ } else if (key == "mutator.group") {
+ this.group = mutator.mutator_attributes[key] as string;
+ } 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());
+ }
+ const paramAttrs = paramAttrsByPrefix.get(pnamePrefix);
+ paramAttrs.set(pk.replace(`${pnamePrefix}.`, ""), mutator.mutator_attributes[key]);
+ } else {
+ // Remaining are organization_custom_metadata attributes
+ this.orgMetadataAttrs.set(key.replace("mutator.", ""), mutator.mutator_attributes[key]);
+ }
+ }
+
+ for (const [_prefix, attrs] of paramAttrsByPrefix) {
+ if (attrs.size != 0) {
+ this.params.push(new MutatorParameter(attrs));
+ }
+ }
+ }
+}
+
+export class MutatorParameter {
+ name = "";
+ description?: string = undefined;
+ valueType: string;
+ attrs: Map;
+
+ constructor(private paramAttrs: Map) {
+ this.attrs = new Map();
+ for (const [k, v] of paramAttrs) {
+ if (k == "name") {
+ this.name = v as string;
+ } else if (k == "description") {
+ this.description = v as string;
+ } else if (k == "value_type") {
+ this.valueType = v as string;
+ } else {
+ this.attrs.set(k, v);
+ }
+ }
+ }
+}
diff --git a/vscode/src/segments.ts b/vscode/src/segments.ts
index 0ee8d30..8842089 100644
--- a/vscode/src/segments.ts
+++ b/vscode/src/segments.ts
@@ -16,7 +16,10 @@ export class SegmentsTreeDataProvider implements vscode.TreeDataProvider;
+ modalityView: vscode.TreeView;
+ conformView: vscode.TreeView;
+ deviantView: vscode.TreeView;
+ activeView: vscode.TreeView;
private _onDidChangeTreeData: vscode.EventEmitter =
new vscode.EventEmitter();
@@ -29,13 +32,46 @@ export class SegmentsTreeDataProvider implements vscode.TreeDataProvider {
+ if (ev.visible) {
+ this.activeView = this.modalityView;
+ }
+ });
+
+ this.conformView = vscode.window.createTreeView("auxon.conform_segments", {
+ treeDataProvider: this,
+ canSelectMany: true,
+ });
+ const conformViewListener = this.conformView.onDidChangeVisibility(async (ev) => {
+ if (ev.visible) {
+ this.activeView = this.conformView;
+ }
+ });
+
+ this.deviantView = vscode.window.createTreeView("auxon.deviant_segments", {
+ treeDataProvider: this,
+ canSelectMany: true,
+ });
+ const deviantViewListener = this.deviantView.onDidChangeVisibility(async (ev) => {
+ if (ev.visible) {
+ this.activeView = this.deviantView;
+ }
+ });
+
+ // Default to the modality view
+ this.activeView = this.modalityView;
context.subscriptions.push(
- this.view,
+ this.modalityView,
+ modalityViewListener,
+ this.conformView,
+ conformViewListener,
+ this.deviantView,
+ deviantViewListener,
vscode.commands.registerCommand("auxon.segments.refresh", () => this.refresh()),
vscode.commands.registerCommand("auxon.segments.setActive", (itemData) => this.setActiveCommand(itemData)),
vscode.commands.registerCommand("auxon.segments.setActiveFromSelection", () =>
@@ -122,11 +158,11 @@ export class SegmentsTreeDataProvider implements vscode.TreeDataProvider this.refresh()),