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()),