|
| 1 | +import { |
| 2 | + CommandHistoryEntry, |
| 3 | + Modifier, |
| 4 | + PartialPrimitiveTargetDescriptor, |
| 5 | + ScopeType, |
| 6 | + showWarning, |
| 7 | +} from "@cursorless/common"; |
| 8 | +import { groupBy, map, sum } from "lodash"; |
| 9 | +import { asyncIteratorToList } from "./asyncIteratorToList"; |
| 10 | +import { canonicalizeAndValidateCommand } from "./core/commandVersionUpgrades/canonicalizeAndValidateCommand"; |
| 11 | +import { generateCommandHistoryEntries } from "./generateCommandHistoryEntries"; |
| 12 | +import { ide } from "./singletons/ide.singleton"; |
| 13 | +import { getPartialTargetDescriptors } from "./util/getPartialTargetDescriptors"; |
| 14 | +import { getPartialPrimitiveTargets } from "./util/getPrimitiveTargets"; |
| 15 | +import { getScopeType } from "./util/getScopeType"; |
| 16 | + |
| 17 | +/** |
| 18 | + * Analyzes the command history for a given time period, and outputs a report |
| 19 | + */ |
| 20 | +class Period { |
| 21 | + private readonly period: string; |
| 22 | + private readonly actions: Record<string, number> = {}; |
| 23 | + private readonly modifiers: Record<string, number> = {}; |
| 24 | + private readonly scopeTypes: Record<string, number> = {}; |
| 25 | + private count: number = 0; |
| 26 | + |
| 27 | + constructor(period: string, entries: CommandHistoryEntry[]) { |
| 28 | + this.period = period; |
| 29 | + for (const entry of entries) { |
| 30 | + this.append(entry); |
| 31 | + } |
| 32 | + } |
| 33 | + |
| 34 | + toString(): string { |
| 35 | + return [ |
| 36 | + `# ${this.period}`, |
| 37 | + `Total command count: ${this.count}`, |
| 38 | + this.serializeMap("Actions", this.actions), |
| 39 | + this.serializeMap("Modifiers", this.modifiers), |
| 40 | + this.serializeMap("Scope types", this.scopeTypes), |
| 41 | + ].join("\n\n"); |
| 42 | + } |
| 43 | + |
| 44 | + private serializeMap(name: string, map: Record<string, number>) { |
| 45 | + const total = sum(Object.values(map)); |
| 46 | + const entries = Object.entries(map); |
| 47 | + entries.sort((a, b) => b[1] - a[1]); |
| 48 | + const entriesSerialized = entries |
| 49 | + .map(([key, value]) => ` ${key}: ${value} (${toPercent(value / total)})`) |
| 50 | + .join("\n"); |
| 51 | + return `${name}:\n${entriesSerialized}`; |
| 52 | + } |
| 53 | + |
| 54 | + private append(entry: CommandHistoryEntry) { |
| 55 | + this.count++; |
| 56 | + const command = canonicalizeAndValidateCommand(entry.command); |
| 57 | + this.incrementAction(command.action.name); |
| 58 | + |
| 59 | + this.parsePrimitiveTargets( |
| 60 | + getPartialPrimitiveTargets(getPartialTargetDescriptors(command.action)), |
| 61 | + ); |
| 62 | + } |
| 63 | + |
| 64 | + private parsePrimitiveTargets( |
| 65 | + partialPrimitiveTargets: PartialPrimitiveTargetDescriptor[], |
| 66 | + ) { |
| 67 | + for (const target of partialPrimitiveTargets) { |
| 68 | + if (target.modifiers == null) { |
| 69 | + continue; |
| 70 | + } |
| 71 | + for (const modifier of target.modifiers) { |
| 72 | + this.incrementModifier(modifier); |
| 73 | + |
| 74 | + const scopeType = getScopeType(modifier); |
| 75 | + if (scopeType != null) { |
| 76 | + this.incrementScope(scopeType); |
| 77 | + } |
| 78 | + } |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + private incrementAction(actionName: string) { |
| 83 | + this.actions[actionName] = (this.actions[actionName] ?? 0) + 1; |
| 84 | + } |
| 85 | + |
| 86 | + private incrementModifier(modifier: Modifier) { |
| 87 | + this.modifiers[modifier.type] = (this.modifiers[modifier.type] ?? 0) + 1; |
| 88 | + } |
| 89 | + |
| 90 | + private incrementScope(scopeType: ScopeType) { |
| 91 | + this.scopeTypes[scopeType.type] = |
| 92 | + (this.scopeTypes[scopeType.type] ?? 0) + 1; |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +function getMonth(entry: CommandHistoryEntry): string { |
| 97 | + return entry.date.slice(0, 7); |
| 98 | +} |
| 99 | + |
| 100 | +export async function analyzeCommandHistory(dir: string) { |
| 101 | + const entries = await asyncIteratorToList(generateCommandHistoryEntries(dir)); |
| 102 | + |
| 103 | + if (entries.length === 0) { |
| 104 | + const TAKE_ME_THERE = "Show me"; |
| 105 | + const result = await showWarning( |
| 106 | + ide().messages, |
| 107 | + "noHistory", |
| 108 | + "No command history entries found. Please enable the command history in the settings.", |
| 109 | + TAKE_ME_THERE, |
| 110 | + ); |
| 111 | + |
| 112 | + if (result === TAKE_ME_THERE) { |
| 113 | + // FIXME: This is VSCode-specific |
| 114 | + await ide().executeCommand( |
| 115 | + "workbench.action.openSettings", |
| 116 | + "cursorless.commandHistory", |
| 117 | + ); |
| 118 | + } |
| 119 | + |
| 120 | + return; |
| 121 | + } |
| 122 | + |
| 123 | + const content = [ |
| 124 | + new Period("Totals", entries).toString(), |
| 125 | + |
| 126 | + ...map(Object.entries(groupBy(entries, getMonth)), ([key, entries]) => |
| 127 | + new Period(key, entries).toString(), |
| 128 | + ), |
| 129 | + ].join("\n\n\n"); |
| 130 | + |
| 131 | + await ide().openUntitledTextDocument({ content }); |
| 132 | +} |
| 133 | + |
| 134 | +function toPercent(value: number) { |
| 135 | + return Intl.NumberFormat(undefined, { |
| 136 | + style: "percent", |
| 137 | + minimumFractionDigits: 0, |
| 138 | + maximumFractionDigits: 1, |
| 139 | + }).format(value); |
| 140 | +} |
0 commit comments