-
-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add simple command history analyzer (#2163)
<img width="525" alt="image" src="https://github.com/cursorless-dev/cursorless/assets/755842/fae1bdd3-470a-4c28-b2ef-e47818943b59"> ## Checklist - [ ] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [ ] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [ ] I have not broken the cheatsheet --------- Co-authored-by: Pokey Rule <[email protected]> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
- Loading branch information
1 parent
09c28c8
commit be6ab53
Showing
14 changed files
with
246 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
packages/cursorless-engine/src/CommandHistoryAnalyzer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import { | ||
CommandHistoryEntry, | ||
Modifier, | ||
PartialPrimitiveTargetDescriptor, | ||
ScopeType, | ||
showWarning, | ||
} from "@cursorless/common"; | ||
import { groupBy, map, sum } from "lodash"; | ||
import { asyncIteratorToList } from "./asyncIteratorToList"; | ||
import { canonicalizeAndValidateCommand } from "./core/commandVersionUpgrades/canonicalizeAndValidateCommand"; | ||
import { generateCommandHistoryEntries } from "./generateCommandHistoryEntries"; | ||
import { ide } from "./singletons/ide.singleton"; | ||
import { getPartialTargetDescriptors } from "./util/getPartialTargetDescriptors"; | ||
import { getPartialPrimitiveTargets } from "./util/getPrimitiveTargets"; | ||
import { getScopeType } from "./util/getScopeType"; | ||
|
||
/** | ||
* Analyzes the command history for a given time period, and outputs a report | ||
*/ | ||
class Period { | ||
private readonly period: string; | ||
private readonly actions: Record<string, number> = {}; | ||
private readonly modifiers: Record<string, number> = {}; | ||
private readonly scopeTypes: Record<string, number> = {}; | ||
private count: number = 0; | ||
|
||
constructor(period: string, entries: CommandHistoryEntry[]) { | ||
this.period = period; | ||
for (const entry of entries) { | ||
this.append(entry); | ||
} | ||
} | ||
|
||
toString(): string { | ||
return [ | ||
`# ${this.period}`, | ||
`Total command count: ${this.count}`, | ||
this.serializeMap("Actions", this.actions), | ||
this.serializeMap("Modifiers", this.modifiers), | ||
this.serializeMap("Scope types", this.scopeTypes), | ||
].join("\n\n"); | ||
} | ||
|
||
private serializeMap(name: string, map: Record<string, number>) { | ||
const total = sum(Object.values(map)); | ||
const entries = Object.entries(map); | ||
entries.sort((a, b) => b[1] - a[1]); | ||
const entriesSerialized = entries | ||
.map(([key, value]) => ` ${key}: ${value} (${toPercent(value / total)})`) | ||
.join("\n"); | ||
return `${name}:\n${entriesSerialized}`; | ||
} | ||
|
||
private append(entry: CommandHistoryEntry) { | ||
this.count++; | ||
const command = canonicalizeAndValidateCommand(entry.command); | ||
this.incrementAction(command.action.name); | ||
|
||
this.parsePrimitiveTargets( | ||
getPartialPrimitiveTargets(getPartialTargetDescriptors(command.action)), | ||
); | ||
} | ||
|
||
private parsePrimitiveTargets( | ||
partialPrimitiveTargets: PartialPrimitiveTargetDescriptor[], | ||
) { | ||
for (const target of partialPrimitiveTargets) { | ||
if (target.modifiers == null) { | ||
continue; | ||
} | ||
for (const modifier of target.modifiers) { | ||
this.incrementModifier(modifier); | ||
|
||
const scopeType = getScopeType(modifier); | ||
if (scopeType != null) { | ||
this.incrementScope(scopeType); | ||
} | ||
} | ||
} | ||
} | ||
|
||
private incrementAction(actionName: string) { | ||
this.actions[actionName] = (this.actions[actionName] ?? 0) + 1; | ||
} | ||
|
||
private incrementModifier(modifier: Modifier) { | ||
this.modifiers[modifier.type] = (this.modifiers[modifier.type] ?? 0) + 1; | ||
} | ||
|
||
private incrementScope(scopeType: ScopeType) { | ||
this.scopeTypes[scopeType.type] = | ||
(this.scopeTypes[scopeType.type] ?? 0) + 1; | ||
} | ||
} | ||
|
||
function getMonth(entry: CommandHistoryEntry): string { | ||
return entry.date.slice(0, 7); | ||
} | ||
|
||
export async function analyzeCommandHistory(dir: string) { | ||
const entries = await asyncIteratorToList(generateCommandHistoryEntries(dir)); | ||
|
||
if (entries.length === 0) { | ||
const TAKE_ME_THERE = "Show me"; | ||
const result = await showWarning( | ||
ide().messages, | ||
"noHistory", | ||
"No command history entries found. Please enable the command history in the settings.", | ||
TAKE_ME_THERE, | ||
); | ||
|
||
if (result === TAKE_ME_THERE) { | ||
// FIXME: This is VSCode-specific | ||
await ide().executeCommand( | ||
"workbench.action.openSettings", | ||
"cursorless.commandHistory", | ||
); | ||
} | ||
|
||
return; | ||
} | ||
|
||
const content = [ | ||
new Period("Totals", entries).toString(), | ||
|
||
...map(Object.entries(groupBy(entries, getMonth)), ([key, entries]) => | ||
new Period(key, entries).toString(), | ||
), | ||
].join("\n\n\n"); | ||
|
||
await ide().openUntitledTextDocument({ content }); | ||
} | ||
|
||
function toPercent(value: number) { | ||
return Intl.NumberFormat(undefined, { | ||
style: "percent", | ||
minimumFractionDigits: 0, | ||
maximumFractionDigits: 1, | ||
}).format(value); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export async function asyncIteratorToList<T>( | ||
iterator: AsyncIterable<T>, | ||
): Promise<T[]> { | ||
const list: T[] = []; | ||
for await (const item of iterator) { | ||
list.push(item); | ||
} | ||
return list; | ||
} |
25 changes: 25 additions & 0 deletions
25
packages/cursorless-engine/src/generateCommandHistoryEntries.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { CommandHistoryEntry } from "@cursorless/common"; | ||
import globRaw from "glob"; | ||
import { readFile } from "node:fs/promises"; | ||
import path from "node:path"; | ||
import { promisify } from "node:util"; | ||
|
||
export async function* generateCommandHistoryEntries(dir: string) { | ||
const files = await glob("*.jsonl", { cwd: dir }); | ||
|
||
for (const file of files) { | ||
const filePath = path.join(dir, file); | ||
const content = await readFile(filePath, "utf8"); | ||
const lines = content.split("\n"); | ||
|
||
for (const line of lines) { | ||
if (line.length === 0) { | ||
continue; | ||
} | ||
|
||
yield JSON.parse(line) as CommandHistoryEntry; | ||
} | ||
} | ||
} | ||
|
||
const glob = promisify(globRaw); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import type { Modifier, ScopeType } from "@cursorless/common"; | ||
|
||
export function getScopeType(modifier: Modifier): ScopeType | undefined { | ||
switch (modifier.type) { | ||
case "containingScope": | ||
case "everyScope": | ||
case "ordinalScope": | ||
case "relativeScope": | ||
return modifier.scopeType; | ||
|
||
case "interiorOnly": | ||
case "excludeInterior": | ||
case "visible": | ||
case "toRawSelection": | ||
case "inferPreviousMark": | ||
case "keepContentFilter": | ||
case "keepEmptyFilter": | ||
case "leading": | ||
case "trailing": | ||
case "startOf": | ||
case "endOf": | ||
case "extendThroughStartOf": | ||
case "extendThroughEndOf": | ||
case "cascading": | ||
case "range": | ||
case "modifyIfUntyped": | ||
return undefined; | ||
|
||
default: { | ||
const _exhaustiveCheck: never = modifier; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.