Skip to content
2 changes: 2 additions & 0 deletions packages/cursorless-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export * from "./cursorlessEngine";
export * from "./customCommandGrammar/parseCommand";
export * from "./generateSpokenForm/defaultSpokenForms/surroundingPairsDelimiters";
export * from "./generateSpokenForm/generateSpokenForm";
export * from "./languages/TreeSitterQuery/TreeSitterQueryCache";
export * from "./processTargets/modifiers/scopeHandlers/ScopeHandlerCache";
export * from "./singletons/ide.singleton";
export * from "./spokenForms/defaultSpokenFormMap";
export * from "./testUtil/extractTargetKeys";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
import { Notifier, showError } from "@cursorless/common";
import { toString } from "lodash-es";
import { LanguageDefinition } from "./LanguageDefinition";
import { treeSitterQueryCache } from "./TreeSitterQuery/treeSitterQueryCache";
import { treeSitterQueryCache } from "./TreeSitterQuery/TreeSitterQueryCache";

/**
* Sentinel value to indicate that a language doesn't have
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
getStartOfEndOfRange,
rewriteStartOfEndOf,
} from "./rewriteStartOfEndOf";
import { treeSitterQueryCache } from "./treeSitterQueryCache";
import { treeSitterQueryCache } from "./TreeSitterQueryCache";

/**
* Wrapper around a tree-sitter query that provides a more convenient API, and
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Position, TextDocument } from "@cursorless/common";
import type { QueryMatch } from "./QueryCapture";

export class Cache {
export class TreeSitterQueryCache {
private documentVersion: number = -1;
private documentUri: string = "";
private documentLanguageId: string = "";
Expand Down Expand Up @@ -58,4 +58,4 @@ function positionsEqual(a: Position | undefined, b: Position | undefined) {
return a.isEqual(b);
}

export const treeSitterQueryCache = new Cache();
export const treeSitterQueryCache = new TreeSitterQueryCache();
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
ComplexScopeType,
ScopeIteratorRequirements,
} from "../scopeHandler.types";
import { scopeHandlerCache } from "../ScopeHandlerCache";
import type { ScopeHandlerFactory } from "../ScopeHandlerFactory";
import { isEveryScopeModifier } from "../util/isHintsEveryScope";
import { OneWayNestedRangeFinder } from "../util/OneWayNestedRangeFinder";
Expand Down Expand Up @@ -44,6 +45,25 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler {
hints: ScopeIteratorRequirements,
): Iterable<TargetScope> {
const isEveryScope = isEveryScopeModifier(hints);
const cacheKey = "CollectionItemTextualScopeHandler_" + isEveryScope;

if (!scopeHandlerCache.isValid(cacheKey, editor.document)) {
const scopes = this.getsScopes(editor, direction, isEveryScope);
scopeHandlerCache.update(cacheKey, editor.document, scopes);
}

const scopes = scopeHandlerCache.get<TargetScope>();

scopes.sort((a, b) => compareTargetScopes(direction, position, a, b));

yield* scopes;
}

private getsScopes(
editor: TextEditor,
direction: Direction,
isEveryScope: boolean,
) {
const separatorRanges = getSeparatorOccurrences(editor.document);
const interiorRanges = getInteriorRanges(
this.scopeHandlerFactory,
Expand Down Expand Up @@ -134,9 +154,7 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler {
}
}

scopes.sort((a, b) => compareTargetScopes(direction, position, a, b));

yield* scopes;
return scopes;
}

private addScopes(scopes: TargetScope[], state: IterationState) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { TextDocument } from "@cursorless/common";

export class ScopeHandlerCache {
private key: string = "";
private documentVersion: number = -1;
private documentUri: string = "";
private documentLanguageId: string = "";
private matches: any[] = [];

clear() {
this.key = "";
this.documentUri = "";
this.documentVersion = -1;
this.documentLanguageId = "";
this.matches = [];
}

isValid(key: string, document: TextDocument) {
return (
this.key === key &&
this.documentVersion === document.version &&
this.documentUri === document.uri.toString() &&
this.documentLanguageId === document.languageId
);
}

update(key: string, document: TextDocument, matches: any[]) {
this.key = key;
this.documentVersion = document.version;
this.documentUri = document.uri.toString();
this.documentLanguageId = document.languageId;
this.matches = matches;
}

get<T>(): T[] {
return this.matches;
}
}

export const scopeHandlerCache = new ScopeHandlerCache();
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { getDelimiterOccurrences } from "./getDelimiterOccurrences";
import { getIndividualDelimiters } from "./getIndividualDelimiters";
import { getSurroundingPairOccurrences } from "./getSurroundingPairOccurrences";
import type { SurroundingPairOccurrence } from "./types";
import { scopeHandlerCache } from "../ScopeHandlerCache";

export class SurroundingPairScopeHandler extends BaseScopeHandler {
public readonly iterationScopeType: ConditionalScopeType = {
Expand Down Expand Up @@ -52,22 +53,31 @@ export class SurroundingPairScopeHandler extends BaseScopeHandler {
return;
}

const delimiterOccurrences = getDelimiterOccurrences(
this.languageDefinitions.get(this.languageId),
editor.document,
getIndividualDelimiters(this.scopeType.delimiter, this.languageId),
);
const cacheKey = "SurroundingPairScopeHandler_" + this.scopeType.delimiter;

if (!scopeHandlerCache.isValid(cacheKey, editor.document)) {
const delimiterOccurrences = getDelimiterOccurrences(
this.languageDefinitions.get(this.languageId),
editor.document,
getIndividualDelimiters(this.scopeType.delimiter, this.languageId),
);

const surroundingPairs =
getSurroundingPairOccurrences(delimiterOccurrences);

scopeHandlerCache.update(cacheKey, editor.document, surroundingPairs);
}

let surroundingPairs = getSurroundingPairOccurrences(delimiterOccurrences);
const surroundingPairs = scopeHandlerCache.get<SurroundingPairOccurrence>();

surroundingPairs = maybeApplyEmptyTargetHack(
const updatedSurroundingPairs = maybeApplyEmptyTargetHack(
direction,
hints,
position,
surroundingPairs,
);

yield* surroundingPairs
yield* updatedSurroundingPairs
.map((pair) =>
createTargetScope(
editor,
Expand Down
117 changes: 86 additions & 31 deletions packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
type ScopeType,
type SimpleScopeTypeType,
} from "@cursorless/common";
import { openNewEditor, runCursorlessCommand } from "@cursorless/vscode-common";
import { openNewEditor, runCursorlessAction } from "@cursorless/vscode-common";
import assert from "assert";
import * as vscode from "vscode";
import { endToEndTestSetup } from "../endToEndTestSetup";
Expand Down Expand Up @@ -51,7 +51,7 @@ suite("Performance", async function () {
["paragraph", smallThresholdMs],
["document", smallThresholdMs],
["nonWhitespaceSequence", smallThresholdMs],
// Parse tree based, containing/every scope
// Parse tree based, containing / every scope
["string", smallThresholdMs],
["map", smallThresholdMs],
["collectionKey", smallThresholdMs],
Expand All @@ -65,9 +65,11 @@ suite("Performance", async function () {
["boundedParagraph", largeThresholdMs],
["boundedNonWhitespaceSequence", largeThresholdMs],
["collectionItem", largeThresholdMs],
["collectionItem", largeThresholdMs, "every"],
["collectionItem", largeThresholdMs, "previous"],
// Surrounding pair
[{ type: "surroundingPair", delimiter: "any" }, largeThresholdMs],
[{ type: "surroundingPair", delimiter: "curlyBrackets" }, largeThresholdMs],
[{ type: "surroundingPair", delimiter: "any" }, largeThresholdMs],
[{ type: "surroundingPair", delimiter: "any" }, largeThresholdMs, "every"],
[
{ type: "surroundingPair", delimiter: "any" },
Expand All @@ -86,10 +88,29 @@ suite("Performance", async function () {
asyncSafety(() => selectScopeType(scopeType, threshold, modifierType)),
);
}

test(
"Select surroundingPair with multiple cursors",
asyncSafety(() =>
selectWithMultipleCursors(largeThresholdMs, {
type: "surroundingPair",
delimiter: "any",
}),
),
);

test(
"Select collectionItem with multiple cursors",
asyncSafety(() =>
selectWithMultipleCursors(largeThresholdMs, {
type: "collectionItem",
}),
),
);
});

async function removeToken(thresholdMs: number) {
await testPerformance(thresholdMs, {
function removeToken(thresholdMs: number) {
return testPerformance(thresholdMs, {
name: "remove",
target: {
type: "primitive",
Expand All @@ -98,12 +119,36 @@ async function removeToken(thresholdMs: number) {
});
}

async function selectScopeType(
function selectWithMultipleCursors(thresholdMs: number, scopeType: ScopeType) {
return testPerformanceCallback(
thresholdMs,
() => {
return runCursorlessAction({
name: "setSelectionBefore",
target: {
type: "primitive",
modifiers: [getModifier({ type: "collectionItem" }, "every")],
},
});
},
() => {
return runCursorlessAction({
name: "setSelection",
target: {
type: "primitive",
modifiers: [getModifier(scopeType)],
},
});
},
);
}

function selectScopeType(
scopeType: ScopeType,
thresholdMs: number,
modifierType?: ModifierType,
) {
await testPerformance(thresholdMs, {
return testPerformance(thresholdMs, {
name: "setSelection",
target: {
type: "primitive",
Expand All @@ -112,41 +157,31 @@ async function selectScopeType(
});
}

function getModifier(
scopeType: ScopeType,
modifierType: ModifierType = "containing",
): Modifier {
switch (modifierType) {
case "containing":
return { type: "containingScope", scopeType };
case "every":
return { type: "everyScope", scopeType };
case "previous":
return {
type: "relativeScope",
direction: "backward",
offset: 1,
length: 1,
scopeType,
};
}
function testPerformance(thresholdMs: number, action: ActionDescriptor) {
return testPerformanceCallback(thresholdMs, () => {
return runCursorlessAction(action);
});
}

async function testPerformance(thresholdMs: number, action: ActionDescriptor) {
async function testPerformanceCallback(
thresholdMs: number,
callback: () => Promise<unknown>,
beforeCallback?: () => Promise<unknown>,
) {
const editor = await openNewEditor(testData, { languageId: "json" });
// This is the position of the last json key in the document
const position = new vscode.Position(editor.document.lineCount - 3, 5);
const selection = new vscode.Selection(position, position);
editor.selections = [selection];
editor.revealRange(selection);

if (beforeCallback != null) {
await beforeCallback();
}

const start = performance.now();

await runCursorlessCommand({
version: 7,
usePrePhraseSnapshot: false,
action,
});
await callback();

const duration = Math.round(performance.now() - start);

Expand All @@ -158,6 +193,26 @@ async function testPerformance(thresholdMs: number, action: ActionDescriptor) {
);
}

function getModifier(
scopeType: ScopeType,
modifierType: ModifierType = "containing",
): Modifier {
switch (modifierType) {
case "containing":
return { type: "containingScope", scopeType };
case "every":
return { type: "everyScope", scopeType };
case "previous":
return {
type: "relativeScope",
direction: "backward",
offset: 1,
length: 1,
scopeType,
};
}
}

function getScopeTypeAndTitle(
scope: SimpleScopeTypeType | ScopeType,
): [ScopeType, string] {
Expand Down
15 changes: 12 additions & 3 deletions packages/cursorless-vscode/src/constructTestHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@ import type {
TextEditor,
} from "@cursorless/common";
import type { StoredTargetMap } from "@cursorless/cursorless-engine";
import { plainObjectToTarget } from "@cursorless/cursorless-engine";
import {
plainObjectToTarget,
scopeHandlerCache,
treeSitterQueryCache,
} from "@cursorless/cursorless-engine";
import { takeSnapshot } from "@cursorless/test-case-recorder";
import type { VscodeTestHelpers } from "@cursorless/vscode-common";
import type * as vscode from "vscode";
import { takeSnapshot } from "@cursorless/test-case-recorder";
import { toVscodeEditor } from "./ide/vscode/toVscodeEditor";
import type { VscodeFileSystem } from "./ide/vscode/VscodeFileSystem";
import type { VscodeIDE } from "./ide/vscode/VscodeIDE";
import { toVscodeEditor } from "./ide/vscode/toVscodeEditor";
import { vscodeApi } from "./vscodeApi";
import type { VscodeTutorial } from "./VscodeTutorial";

Expand All @@ -45,6 +49,11 @@ export function constructTestHelpers(
return vscodeIDE.fromVscodeEditor(editor);
},

clearCache() {
scopeHandlerCache.clear();
treeSitterQueryCache.clear();
},

// FIXME: Remove this once we have a better way to get this function
// accessible from our tests
takeSnapshot(
Expand Down
Loading
Loading