Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple selected timelines in event view #32

Merged
merged 1 commit into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -634,10 +634,6 @@
"title": "Refresh Event List",
"icon": "$(refresh)"
},
{
"command": "auxon.events.setSelectedTimeline",
"title": "Update events summary for the selected timeline"
},
{
"command": "auxon.events.createEventTimingNotebook",
"title": "View Event Timing Plots in a Jupyter Notebook"
Expand Down
218 changes: 131 additions & 87 deletions vscode/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import * as modalityEventInspect from "./modalityEventInspect";

import * as commonNotebookCells from "./notebooks/common.json";
import * as eventTimingNotebookCells from "./notebooks/eventTiming.json";
import * as eventAttributeValuesNotebookCells from "./notebooks/eventAttributeValues.json";
import * as eventMultiAttributeValuesNotebookCells from "./notebooks/eventMultiAttributeValues.json";
type JupyterNotebookCell = (typeof commonNotebookCells.cells)[0];

export interface SelectedTimeline {
timelineId: api.TimelineId;
timelineName: string;
}

export class EventsTreeDataProvider implements vscode.TreeDataProvider<EventsTreeItemData> {
selectedTimelineId?: api.TimelineId = undefined;
selectedTimelineName?: string = undefined;
selectedTimelines: SelectedTimeline[] = [];
view: vscode.TreeView<EventsTreeItemData>;

private _onDidChangeTreeData: vscode.EventEmitter<EventsTreeItemData | EventsTreeItemData[] | undefined> =
Expand All @@ -34,9 +37,6 @@ export class EventsTreeDataProvider implements vscode.TreeDataProvider<EventsTre
this.view,
vscode.commands.registerCommand("auxon.events.refresh", () => this.refresh()),
vscode.commands.registerCommand("auxon.events.logSelected", () => this.logSelectedCommand()),
vscode.commands.registerCommand("auxon.events.setSelectedTimeline", (timelineId, timelineName) =>
this.setSelectedTimeline(timelineId, timelineName)
),
vscode.commands.registerCommand("auxon.events.createEventTimingNotebook", async (item) =>
this.createEventTimingNotebook(item)
),
Expand Down Expand Up @@ -72,76 +72,81 @@ export class EventsTreeDataProvider implements vscode.TreeDataProvider<EventsTre
}

private async getChildrenInner(element?: EventsTreeItemData): Promise<EventsTreeItemData[]> {
if (!this.selectedTimelineId) {
if (this.selectedTimelines.length === 0) {
return [];
}

if (!element) {
const children = [];
const eventsSummary = await this.apiClient.events().eventsSummaryForTimeline(this.selectedTimelineId);
for (const summary of eventsSummary.events) {
let name = summary.name;
if (name == null) {
name = "<unnamed>";
}
const attrs = [];
for (const attr of summary.attributes) {
if (attr.startsWith("event.")) {
attrs.push(attr.replace("event.", ""));
for (const selectedTimeline of this.selectedTimelines) {
const eventsSummary = await this.apiClient
.events()
.eventsSummaryForTimeline(selectedTimeline.timelineId);
for (const summary of eventsSummary.events) {
let name = summary.name;
if (name == null) {
name = "<unnamed>";
}
const attrs = [];
for (const attr of summary.attributes) {
if (attr.startsWith("event.")) {
attrs.push(attr.replace("event.", ""));
}
}
children.push(
new EventNameTreeItemData(
name,
summary.n_instances,
attrs,
selectedTimeline.timelineName,
selectedTimeline.timelineId
)
);
}
children.push(new EventNameTreeItemData(name, summary.n_instances, attrs, this.selectedTimelineId));
}

children.sort((a, b) => a.eventName.localeCompare(b.eventName));
return children;
} else {
if (element instanceof EventNameTreeItemData) {
return element.attributes.map((attrKey) => new EventAttributeTreeItemData(element.eventName, attrKey));
return element.attributes.map(
(attrKey) =>
new EventAttributeTreeItemData(
element.eventName,
element.timelineName,
element.timelineId,
attrKey
)
);
} else {
return [];
}
}
}

setSelectedTimeline(timelineId?: api.TimelineId, timelineName?: string) {
if (timelineId && timelineName) {
this.selectedTimelineId = timelineId;
this.selectedTimelineName = timelineName;
this.refresh();
}
setSelectedTimelines(timelines: SelectedTimeline[]) {
this.selectedTimelines = timelines;
this.refresh();
}

logSelectedCommand() {
if (this.selectedTimelineId == null) {
if (this.selectedTimelines.length === 0) {
vscode.window.showWarningMessage(`No timeline is selected`);
return;
}

const thingsToLog = [];
const literalTimelineId = "%" + this.selectedTimelineId.replace(/-/g, "");

for (const itemData of this.view.selection) {
thingsToLog.push(`${itemData.eventName}@*(_.timeline.id=${literalTimelineId})`);
}
vscode.commands.executeCommand(
modalityLog.MODALITY_LOG_COMMAND,
new modalityLog.ModalityLogCommandArgs({ thingToLog: thingsToLog })
);
}

async createEventTimingNotebook(item: EventNameTreeItemData) {
let selectedEventNames = this.view.selection.map((data) => data.eventName);
// Add the item the command was executed on, it may not be in the selection
selectedEventNames.push(item.eventName);
selectedEventNames = [...new Set(selectedEventNames)]; // dedupe
for (const eventName of selectedEventNames) {
const varMap = this.templateVariableMap();
if (varMap == null) {
return;
if (itemData instanceof EventNameTreeItemData && itemData.timelineId) {
const literalTimelineId = "%" + itemData.timelineId.replace(/-/g, "");
thingsToLog.push(`"${itemData.eventName}"@*(_.timeline.id=${literalTimelineId})`);
}
varMap["eventName"] = eventName;
await this.createJupyterNotebook(eventTimingNotebookCells.cells, varMap);
}
if (thingsToLog.length !== 0) {
vscode.commands.executeCommand(
modalityLog.MODALITY_LOG_COMMAND,
new modalityLog.ModalityLogCommandArgs({ thingToLog: thingsToLog })
);
}
}

Expand All @@ -152,39 +157,76 @@ export class EventsTreeDataProvider implements vscode.TreeDataProvider<EventsTre
}
}

async createEventAttrNotebook(item: EventAttributeTreeItemData) {
if (this.view.selection.length == 1) {
const varMap = this.templateVariableMap();
async createEventTimingNotebook(item: EventNameTreeItemData) {
type SelectedEvent = {
name: string;
timelineId: api.TimelineId;
timelineName: string;
};

const selection: EventNameTreeItemData[] = [];
for (const item of this.view.selection) {
if (item instanceof EventNameTreeItemData) {
selection.push(item);
}
}
// Add the item the command was executed on, it may not be in the selection
selection.push(item);
let selectedEvents: SelectedEvent[] = selection.map((data) => {
return { name: data.eventName, timelineId: data.timelineId, timelineName: data.timelineName };
});
// dedupe
selectedEvents = selectedEvents.filter(
(v, i, a) => a.findIndex((v2) => JSON.stringify(v2) === JSON.stringify(v)) === i
);
for (const event of selectedEvents) {
const varMap = this.templateVariableMap(event.timelineName, event.timelineId);
if (varMap == null) {
return;
}
varMap["eventName"] = event.name;
await this.createJupyterNotebook(eventTimingNotebookCells.cells, varMap);
}
}

varMap.eventName = item.eventName;
varMap.eventAttribute = item.attribute;
await this.createJupyterNotebook(eventAttributeValuesNotebookCells.cells, varMap);
} else {
let selectedEventAttrs = [...this.view.selection, ...[item]];
selectedEventAttrs = [...new Set(selectedEventAttrs)]; // dedupe
async createEventAttrNotebook(item: EventAttributeTreeItemData) {
type EventName = string;
type EventMeta = {
timelineName: string;
attributes: Set<string>;
};

const attributesByEvent = new Map<string, string[]>();
for (const ev of selectedEventAttrs) {
if (!(ev instanceof EventAttributeTreeItemData)) {
throw new Error("Internal error: event tree node not of expected type");
// Group attributes by event@timeline, we'll make a notebook for
// each group
const eventGroups = new Map<api.TimelineId, Map<EventName, EventMeta>>();
for (const data of [...this.view.selection, ...[item]]) {
if (data instanceof EventAttributeTreeItemData) {
let eventToMeta = null;
if (!eventGroups.has(data.timelineId)) {
eventGroups.set(data.timelineId, new Map<EventName, EventMeta>());
}
if (attributesByEvent.has(ev.eventName)) {
const attributes = attributesByEvent.get(ev.eventName);
if (attributes != null) {
attributes.push(ev.attribute);
eventToMeta = eventGroups.get(data.timelineId);

if (eventToMeta != null) {
let eventMeta = null;
if (!eventToMeta.has(data.eventName)) {
eventToMeta.set(data.eventName, {
timelineName: data.timelineName,
attributes: new Set<string>(),
});
}
eventMeta = eventToMeta.get(data.eventName);

if (eventMeta != null) {
eventMeta.attributes.add(data.attribute);
}
} else {
const attributes = [];
attributes.push(ev.attribute);
attributesByEvent.set(ev.eventName, attributes);
}
}
}

attributesByEvent.forEach(async (attributes: string[], eventName: string) => {
const varMap = this.templateVariableMap();
for (const [timelineId, eventToMeta] of eventGroups) {
for (const [eventName, eventMeta] of eventToMeta) {
const varMap = this.templateVariableMap(eventMeta.timelineName, timelineId);
if (varMap == null) {
return;
}
Expand All @@ -193,19 +235,19 @@ export class EventsTreeDataProvider implements vscode.TreeDataProvider<EventsTre
const cells = lodash.cloneDeep(eventMultiAttributeValuesNotebookCells.cells.slice(0, 2));
const srcCell = eventMultiAttributeValuesNotebookCells.cells[2];
const figShowCell = eventMultiAttributeValuesNotebookCells.cells[3];
const eventAttributesList = [];
for (let i = 0; i < attributes.length; i++) {
eventAttributesList.push("'event." + attributes[i] + "'");
varMap["eventAttribute" + i] = attributes[i];
const eventAttributesList: string[] = [];
Array.from(eventMeta.attributes).forEach((attr, i) => {
eventAttributesList.push("'event." + attr + "'");
varMap["eventAttribute" + i] = attr;
const newSrc = srcCell.source[0].replace(/eventAttribute/g, "eventAttribute" + i);
cells[1].source.push(newSrc);
}
});
cells[1].source.push(figShowCell.source[0]);

varMap.eventAttributes = eventAttributesList.join(", ");

await this.createJupyterNotebook(cells, varMap);
});
}
}
}

Expand Down Expand Up @@ -262,12 +304,12 @@ export class EventsTreeDataProvider implements vscode.TreeDataProvider<EventsTre
return cells;
}

templateVariableMap(): TemplateVariableMap | undefined {
templateVariableMap(timelineName: string, timelineId: api.TimelineId): TemplateVariableMap | undefined {
let segments: string;
switch (this.wss.activeSegments.type) {
case "WholeWorkspace":
vscode.window.showWarningMessage(`This notebook is not supported in 'whole workspace' mode.`);
return;
return undefined;
case "Explicit": {
segments = this.wss.activeSegments.segmentIds
.map((seg) => {
Expand All @@ -277,16 +319,11 @@ export class EventsTreeDataProvider implements vscode.TreeDataProvider<EventsTre
}
}

if (this.selectedTimelineId == null || this.selectedTimelineName == null) {
vscode.window.showWarningMessage(`A timeline must be selected to generate a notebook`);
return;
}

return {
workspaceVersionId: this.wss.activeWorkspaceVersionId,
segments,
timelineId: "%" + this.selectedTimelineId.replace(/-/g, ""),
timelineName: this.selectedTimelineName,
timelineId: "%" + timelineId.replace(/-/g, ""),
timelineName: timelineName,
};
}
}
Expand All @@ -311,6 +348,7 @@ export class EventNameTreeItemData {
public eventName: string,
public numInstances: number,
public attributes: string[],
public timelineName: string,
public timelineId: api.TimelineId
) {}

Expand All @@ -332,13 +370,19 @@ class EventNameTreeItem extends vscode.TreeItem {
? vscode.TreeItemCollapsibleState.Collapsed
: vscode.TreeItemCollapsibleState.None
);
const tooltip = `- **Instances**:: ${data.numInstances}`;
let tooltip = `${data.eventName} @ ${data.timelineName}`;
tooltip += `\n- **Instances**:: ${data.numInstances}`;
this.tooltip = new vscode.MarkdownString(tooltip);
}
}

export class EventAttributeTreeItemData {
constructor(public eventName: string, public attribute: string) {}
constructor(
public eventName: string,
public timelineName: string,
public timelineId: api.TimelineId,
public attribute: string
) {}
}

class EventAttributeTreeItem extends vscode.TreeItem {
Expand Down
13 changes: 11 additions & 2 deletions vscode/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,17 @@ export async function activate(context: vscode.ExtensionContext) {
// when we new them up, so we don't need to hold on to them.
new workspaces.WorkspacesTreeDataProvider(apiClient, wss, context);
new segments.SegmentsTreeDataProvider(apiClient, specCoverageProvider, wss, context);
new timelines.TimelinesTreeDataProvider(apiClient, wss, context);
new events.EventsTreeDataProvider(apiClient, wss, context);
const timelinesProvider = new timelines.TimelinesTreeDataProvider(apiClient, wss, context);
const eventsProvider = new events.EventsTreeDataProvider(apiClient, wss, context);

timelinesProvider.view.onDidChangeSelection(async (event) => {
const selection = event.selection
.filter((item) => item instanceof timelines.TimelineLeafTreeItemData && item.timelineId != undefined)
.map((item) => {
return { timelineId: item.timelineId as api.TimelineId, timelineName: item.name };
});
eventsProvider.setSelectedTimelines(selection);
});

new specs.SpecsTreeDataProvider(apiClient, specCoverageProvider, wss, context);

Expand Down
2 changes: 1 addition & 1 deletion vscode/src/modalityEventInspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function runModalityEventInspectCommand(event: string) {
const modality = config.toolPath("modality");

// We're going to send the text of the command line to the terminal. Build up the args list here.
const modalityArgs = ["LESS=R", modality, "event", "inspect", event];
const modalityArgs = ["LESS=R", modality, "event", "inspect", `"${event}"`];

for (const extra of config.extraCliArgs("modality event inspect")) {
modalityArgs.push(extra);
Expand Down
10 changes: 0 additions & 10 deletions vscode/src/timelines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,16 +298,6 @@ abstract class TimelineTreeItemData {
item.tooltip = this.tooltip;
item.iconPath = this.iconPath;

// Timeline selection updates the events summary view
if (this.contextValue == "timeline") {
const command = {
title: "Update events summary for the selected timeline",
command: "auxon.events.setSelectedTimeline",
arguments: [this.timelineId, this.name],
};
item.command = command;
}

return item;
}

Expand Down
Loading