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

Added command history #2115

Merged
merged 50 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
b8199c5
Added command history
AndreasArvidsson Dec 10, 2023
af0f3b8
clean up
AndreasArvidsson Dec 10, 2023
957c576
More clean up
AndreasArvidsson Dec 10, 2023
10dce17
Refactor as a command runner decorator
AndreasArvidsson Dec 11, 2023
8126d6c
cleanup
AndreasArvidsson Dec 11, 2023
8d838cd
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Dec 11, 2023
0e63bd9
Added setting to package
AndreasArvidsson Dec 11, 2023
1a76fc1
Merge branch 'commandHistory' of github.com:cursorless-dev/cursorless…
AndreasArvidsson Dec 11, 2023
f2cee87
Sanitize action
AndreasArvidsson Dec 11, 2023
ef206b3
Add comments
AndreasArvidsson Dec 11, 2023
c7bcf64
update
AndreasArvidsson Dec 11, 2023
6edd8ad
cleanup
AndreasArvidsson Dec 11, 2023
caad3ef
Added types
AndreasArvidsson Dec 11, 2023
5b108dd
cleanup
AndreasArvidsson Dec 11, 2023
6d3cb21
refactor
AndreasArvidsson Dec 11, 2023
7df130e
Added docstring
AndreasArvidsson Dec 11, 2023
c99ab91
tweak
pokey Dec 11, 2023
14343e4
List all action names
AndreasArvidsson Dec 11, 2023
1f9b9c8
add comment
AndreasArvidsson Dec 11, 2023
3c7cab9
Update packages/cursorless-vscode/src/CommandHistory.ts
AndreasArvidsson Dec 11, 2023
02b1160
Added thrown error
AndreasArvidsson Dec 12, 2023
1c345d0
Added order
AndreasArvidsson Dec 12, 2023
9b76190
Experiment with immer (#2124)
pokey Dec 12, 2023
3e3a074
Added tests
AndreasArvidsson Dec 13, 2023
1532071
cleanup
AndreasArvidsson Dec 13, 2023
194c58e
more clean up
AndreasArvidsson Dec 13, 2023
92e4d7d
remove log
AndreasArvidsson Dec 13, 2023
566cd36
update tests
AndreasArvidsson Dec 16, 2023
6ddb894
Merge branch 'main' into commandHistory
AndreasArvidsson Dec 17, 2023
86ad41e
Added analyze history command
AndreasArvidsson Dec 17, 2023
dc5eb24
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Dec 17, 2023
e510ee9
meta updater
AndreasArvidsson Dec 17, 2023
24c862e
Merge branch 'commandHistory' of github.com:cursorless-dev/cursorless…
AndreasArvidsson Dec 17, 2023
2254ee3
Added number
AndreasArvidsson Dec 17, 2023
5e365f1
Added documentation
AndreasArvidsson Dec 17, 2023
43bf118
Tweak docs
pokey Dec 18, 2023
88937fd
Tweaks
pokey Dec 18, 2023
40ea48b
tweak verbiage
pokey Dec 18, 2023
e27e392
tweak tests
pokey Dec 18, 2023
2016c40
Remove analyze action
AndreasArvidsson Dec 18, 2023
827e69f
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Dec 18, 2023
54ebcd0
Refactor
AndreasArvidsson Dec 19, 2023
b19c476
Remove spoken form
AndreasArvidsson Dec 19, 2023
47b187a
Merge branch 'main' into commandHistory
AndreasArvidsson Dec 19, 2023
b7d3887
added case
AndreasArvidsson Dec 19, 2023
2ef444c
Update tests
AndreasArvidsson Dec 19, 2023
1762319
update docs
AndreasArvidsson Dec 19, 2023
4c6de81
Added command id
AndreasArvidsson Jan 2, 2024
91212cf
Remove command analyzer
pokey Jan 3, 2024
598c1cd
Add sanitization tests
pokey Jan 3, 2024
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
13 changes: 13 additions & 0 deletions docs/user/localCommandHIstory.md
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.
4 changes: 4 additions & 0 deletions packages/common/src/ide/PassthroughIDEBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ export default class PassthroughIDEBase implements IDE {
return this.original.visibleTextEditors;
}

public get cursorlessVersion(): string {
return this.original.cursorlessVersion;
}

public get assetsRoot(): string {
return this.original.assetsRoot;
}
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/ide/fake/FakeIDE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default class FakeIDE implements IDE {
capabilities: FakeCapabilities = new FakeCapabilities();

runMode: RunMode = "test";
cursorlessVersion: string = "0.0.0";
workspaceFolders: readonly WorkspaceFolder[] | undefined = undefined;
private disposables: Disposable[] = [];
private assetsRoot_: string | undefined;
Expand Down
2 changes: 2 additions & 0 deletions packages/common/src/ide/types/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type CursorlessConfiguration = {
wordSeparators: string[];
experimental: { snippetsDir: string | undefined; hatStability: HatStability };
decorationDebounceDelayMs: number;
commandHistory: boolean;
debug: boolean;
};

Expand All @@ -26,6 +27,7 @@ export const CONFIGURATION_DEFAULTS: CursorlessConfiguration = {
snippetsDir: undefined,
hatStability: HatStability.balanced,
},
commandHistory: false,
debug: false,
};

Expand Down
5 changes: 5 additions & 0 deletions packages/common/src/ide/types/FileSystem.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@ export interface FileSystem {
* The path to the Cursorless talon state JSON file.
*/
readonly cursorlessTalonStateJsonPath: string;

/**
* The path to the Cursorless command history directory.
*/
readonly cursorlessCommandHistoryDirPath: string;
}
5 changes: 5 additions & 0 deletions packages/common/src/ide/types/ide.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export interface IDE {
*/
disposeOnExit(...disposables: Disposable[]): () => void;

/**
* The version of the cursorless extension
*/
readonly cursorlessVersion: string;

/**
* The root directory of this shipped code. Can be used to access bundled
* assets.
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export * from "./types/Token";
export * from "./types/HatTokenMap";
export * from "./types/ScopeProvider";
export * from "./types/SpokenForm";
export * from "./types/commandHistory";
export * from "./util/textFormatters";
export * from "./types/snippet.types";
export * from "./testUtil/fromPlainObject";
Expand Down
24 changes: 24 additions & 0 deletions packages/common/src/types/commandHistory.ts
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;
}
2 changes: 2 additions & 0 deletions packages/cursorless-engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"lodash": "^4.17.21",
"node-html-parser": "^6.1.11",
"sbd": "^1.0.19",
"uuid": "^9.0.0",
"zod": "3.22.3"
},
"devDependencies": {
Expand All @@ -31,6 +32,7 @@
"@types/mocha": "^10.0.3",
"@types/sbd": "^1.0.3",
"@types/sinon": "^10.0.2",
"@types/uuid": "^8.3.4",
"js-yaml": "^4.1.0",
"mocha": "^10.2.0",
"sinon": "^11.1.1"
Expand Down
216 changes: 216 additions & 0 deletions packages/cursorless-engine/src/CommandHistory.ts
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");
}
2 changes: 2 additions & 0 deletions packages/cursorless-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export * from "./core/StoredTargets";
export * from "./typings/TreeSitter";
export * from "./cursorlessEngine";
export * from "./api/CursorlessEngineApi";
export * from "./CommandRunner";
export * from "./CommandHistory";
1 change: 1 addition & 0 deletions packages/cursorless-vscode-e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"dependencies": {
"@cursorless/common": "workspace:*",
"@cursorless/vscode-common": "workspace:*",
"immer": "^9.0.15",
"lodash": "^4.17.21"
},
"devDependencies": {
Expand Down
Loading