-
-
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.
setting: ```json "cursorless.commandHistory": true ``` Monthly log file: `cursorlessCommandHistory_2023-12.jsonl` ``` {"date":"2023-12-10","cursorlessVersion":"0.28.0-40a6fee4","command":{"version":6,"action":{"name":"setSelection","target":{"type":"primitive","modifiers":[{"type":"containingScope","scopeType":{"type":"line"}}]}},"usePrePhraseSnapshot":true}} ``` Voice command: `"Cursorless analyze history"` opens a new untitled document: ``` [2023-12] Total commands: 24 Actions (7): setSelection: 18 clearAndSetSelection: 1 remove: 1 wrapWithPairedDelimiter: 1 getText: 1 replace: 1 copyToClipboard: 1 Modifiers (2): containingScope: 20 relativeScope: 2 Scope types (4): line: 18 surroundingPair: 2 comment: 1 document: 1 ``` ## Checklist - [x] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [x] 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: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Pokey Rule <[email protected]>
- Loading branch information
1 parent
7341d0f
commit a4dfb8a
Showing
20 changed files
with
472 additions
and
5 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# Local command history | ||
|
||
By default, Cursorless doesn't capture anything about your usage. However, we do have a way to opt in to a local, sanitized command history. This history is never sent to our servers, and any commands that may contain text will be sanitized. | ||
|
||
The idea is that these statistics can be used in the future for doing local analyses to determine ways you can improve your Cursorless efficiency. We may also support a way for you to send your statistics to us for analysis in the future, but this will be opt-in only. | ||
|
||
To enable local, sanitized command logging, enable the `cursorless.commandHistory` VSCode setting. You should see a checkbox in the settings UI when you say `"cursorless settings"`. You can also set it manually in your `settings.json`: | ||
|
||
```json | ||
"cursorless.commandHistory": true | ||
``` | ||
|
||
The logged commands can be found in your user directory, under `.cursorless/commandHistory`. You can delete this directory at any time to clear your history. Please don't delete the parent `.cursorless` directory, as this contains other files for use by Cursorless. |
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
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,24 @@ | ||
import type { Command } from "./command/command.types"; | ||
|
||
/** | ||
* Represents a single line in a command history jsonl file. | ||
*/ | ||
export interface CommandHistoryEntry { | ||
// UUID of the log entry. | ||
id: string; | ||
|
||
// Date of the log entry. eg: "2023-09-05" | ||
date: string; | ||
|
||
// Version of the Cursorless extension. eg: "0.28.0-c7bcf64d". | ||
cursorlessVersion: string; | ||
|
||
// Name of thrown error. eg: "NoContainingScopeError". | ||
error?: string; | ||
|
||
// UUID of the phrase. | ||
phraseId: string | undefined; | ||
|
||
// The command that was executed. | ||
command: Command; | ||
} |
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,216 @@ | ||
import { | ||
ActionDescriptor, | ||
CommandComplete, | ||
CommandHistoryEntry, | ||
CommandServerApi, | ||
FileSystem, | ||
IDE, | ||
ReadOnlyHatMap, | ||
} from "@cursorless/common"; | ||
import type { | ||
CommandRunner, | ||
CommandRunnerDecorator, | ||
} from "@cursorless/cursorless-engine"; | ||
import produce from "immer"; | ||
import * as fs from "node:fs/promises"; | ||
import * as path from "node:path"; | ||
import { v4 as uuid } from "uuid"; | ||
|
||
const filePrefix = "cursorlessCommandHistory"; | ||
|
||
/** | ||
* When user opts in, this class sanitizes and appends each Cursorless command | ||
* to a local log file in `.cursorless/commandHistory` dir. | ||
*/ | ||
export class CommandHistory implements CommandRunnerDecorator { | ||
private readonly dirPath: string; | ||
private currentPhraseSignal = ""; | ||
private currentPhraseId = ""; | ||
|
||
constructor( | ||
private ide: IDE, | ||
private commandServerApi: CommandServerApi | null, | ||
fileSystem: FileSystem, | ||
) { | ||
this.dirPath = fileSystem.cursorlessCommandHistoryDirPath; | ||
} | ||
|
||
wrapCommandRunner( | ||
_readableHatMap: ReadOnlyHatMap, | ||
runner: CommandRunner, | ||
): CommandRunner { | ||
if (!this.isActive()) { | ||
return runner; | ||
} | ||
|
||
return { | ||
run: async (commandComplete: CommandComplete) => { | ||
try { | ||
const returnValue = await runner.run(commandComplete); | ||
|
||
await this.appendToLog(commandComplete); | ||
|
||
return returnValue; | ||
} catch (e) { | ||
await this.appendToLog(commandComplete, e as Error); | ||
throw e; | ||
} | ||
}, | ||
}; | ||
} | ||
|
||
private async appendToLog( | ||
command: CommandComplete, | ||
thrownError?: Error, | ||
): Promise<void> { | ||
const date = new Date(); | ||
const fileName = `${filePrefix}_${getMonthDate(date)}.jsonl`; | ||
const file = path.join(this.dirPath, fileName); | ||
|
||
const historyItem: CommandHistoryEntry = { | ||
id: uuid(), | ||
date: getDayDate(date), | ||
cursorlessVersion: this.ide.cursorlessVersion, | ||
error: thrownError?.name, | ||
phraseId: await this.getPhraseId(), | ||
command: produce(command, sanitizeCommandInPlace), | ||
}; | ||
const data = JSON.stringify(historyItem) + "\n"; | ||
|
||
await fs.mkdir(this.dirPath, { recursive: true }); | ||
await fs.appendFile(file, data, "utf8"); | ||
} | ||
|
||
private async getPhraseId(): Promise<string | undefined> { | ||
const phraseStartSignal = this.commandServerApi?.signals?.prePhrase; | ||
|
||
if (phraseStartSignal == null) { | ||
return undefined; | ||
} | ||
|
||
const newSignal = await phraseStartSignal.getVersion(); | ||
|
||
if (newSignal == null) { | ||
return undefined; | ||
} | ||
|
||
if (newSignal !== this.currentPhraseSignal) { | ||
this.currentPhraseSignal = newSignal; | ||
this.currentPhraseId = uuid(); | ||
} | ||
|
||
return this.currentPhraseId; | ||
} | ||
|
||
private isActive(): boolean { | ||
return this.ide.configuration.getOwnConfiguration("commandHistory"); | ||
} | ||
} | ||
|
||
// Remove spoken form and sanitize action | ||
function sanitizeCommandInPlace(command: CommandComplete): void { | ||
delete command.spokenForm; | ||
sanitizeActionInPlace(command.action); | ||
} | ||
|
||
function sanitizeActionInPlace(action: ActionDescriptor): void { | ||
switch (action.name) { | ||
// Remove replace with text | ||
case "replace": | ||
if (Array.isArray(action.replaceWith)) { | ||
action.replaceWith = []; | ||
} | ||
break; | ||
|
||
// Remove substitutions and custom body | ||
case "insertSnippet": | ||
delete action.snippetDescription.substitutions; | ||
if (action.snippetDescription.type === "custom") { | ||
action.snippetDescription.body = ""; | ||
} | ||
break; | ||
|
||
case "wrapWithSnippet": | ||
if (action.snippetDescription.type === "custom") { | ||
action.snippetDescription.body = ""; | ||
} | ||
break; | ||
|
||
case "executeCommand": | ||
delete action.options?.commandArgs; | ||
break; | ||
|
||
case "breakLine": | ||
case "clearAndSetSelection": | ||
case "copyToClipboard": | ||
case "cutToClipboard": | ||
case "deselect": | ||
case "editNewLineAfter": | ||
case "editNewLineBefore": | ||
case "experimental.setInstanceReference": | ||
case "extractVariable": | ||
case "findInWorkspace": | ||
case "foldRegion": | ||
case "followLink": | ||
case "indentLine": | ||
case "insertCopyAfter": | ||
case "insertCopyBefore": | ||
case "insertEmptyLineAfter": | ||
case "insertEmptyLineBefore": | ||
case "insertEmptyLinesAround": | ||
case "joinLines": | ||
case "outdentLine": | ||
case "randomizeTargets": | ||
case "remove": | ||
case "rename": | ||
case "revealDefinition": | ||
case "revealTypeDefinition": | ||
case "reverseTargets": | ||
case "scrollToBottom": | ||
case "scrollToCenter": | ||
case "scrollToTop": | ||
case "setSelection": | ||
case "setSelectionAfter": | ||
case "setSelectionBefore": | ||
case "showDebugHover": | ||
case "showHover": | ||
case "showQuickFix": | ||
case "showReferences": | ||
case "sortTargets": | ||
case "toggleLineBreakpoint": | ||
case "toggleLineComment": | ||
case "unfoldRegion": | ||
case "private.showParseTree": | ||
case "private.getTargets": | ||
case "callAsFunction": | ||
case "editNew": | ||
case "generateSnippet": | ||
case "getText": | ||
case "highlight": | ||
case "moveToTarget": | ||
case "pasteFromClipboard": | ||
case "replaceWithTarget": | ||
case "rewrapWithPairedDelimiter": | ||
case "swapTargets": | ||
case "wrapWithPairedDelimiter": | ||
case "findInDocument": | ||
break; | ||
|
||
default: { | ||
// Ensure we don't miss any new actions | ||
const _exhaustiveCheck: never = action; | ||
} | ||
} | ||
} | ||
|
||
function getMonthDate(date: Date): string { | ||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}`; | ||
} | ||
|
||
function getDayDate(date: Date): string { | ||
return `${getMonthDate(date)}-${pad(date.getDate())}`; | ||
} | ||
|
||
function pad(num: number): string { | ||
return num.toString().padStart(2, "0"); | ||
} |
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
Oops, something went wrong.