From 65546c5299edbf39b058894ae4ad5e1ed07197d7 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Fri, 14 Nov 2025 03:50:42 +0100 Subject: [PATCH 01/12] Testing performance with multiple cursors --- .../src/suite/performance.vscode.test.ts | 55 +++++++++++++++++-- packages/vscode-common/src/runCommand.ts | 16 +++++- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts index 838ae86a8e..15025d6030 100644 --- a/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts @@ -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"; @@ -37,6 +37,11 @@ suite("Performance", async function () { asyncSafety(() => removeToken(smallThresholdMs)), ); + test( + "Select with multiple cursors", + asyncSafety(() => selectWithMultipleCursors(largeThresholdMs)), + ); + const fixtures: ( | [SimpleScopeTypeType | ScopeType, number] | [SimpleScopeTypeType | ScopeType, number, ModifierType] @@ -112,6 +117,28 @@ async function selectScopeType( }); } +async function selectWithMultipleCursors(thresholdMs: number) { + const callback = async () => { + await runCursorlessAction({ + name: "setSelectionBefore", + target: { + type: "primitive", + modifiers: [getModifier({ type: "collectionItem" }, "every")], + }, + }); + + await runCursorlessAction({ + name: "setSelection", + target: { + type: "primitive", + modifiers: [getModifier({ type: "surroundingPair", delimiter: "any" })], + }, + }); + }; + + await testPerformanceCallback(thresholdMs, callback); +} + function getModifier( scopeType: ScopeType, modifierType: ModifierType = "containing", @@ -142,11 +169,27 @@ async function testPerformance(thresholdMs: number, action: ActionDescriptor) { const start = performance.now(); - await runCursorlessCommand({ - version: 7, - usePrePhraseSnapshot: false, - action, - }); + const callback = async () => { + await runCursorlessAction(action); + }; + + testPerformanceCallback(thresholdMs, callback); +} + +async function testPerformanceCallback( + thresholdMs: number, + callback: () => Promise, +) { + 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); + + const start = performance.now(); + + await callback(); const duration = Math.round(performance.now() - start); diff --git a/packages/vscode-common/src/runCommand.ts b/packages/vscode-common/src/runCommand.ts index fd2c986e1b..8f1e464558 100644 --- a/packages/vscode-common/src/runCommand.ts +++ b/packages/vscode-common/src/runCommand.ts @@ -1,5 +1,9 @@ -import type { Command, CommandResponse } from "@cursorless/common"; -import { CURSORLESS_COMMAND_ID } from "@cursorless/common"; +import type { + ActionDescriptor, + Command, + CommandResponse, +} from "@cursorless/common"; +import { CURSORLESS_COMMAND_ID, LATEST_VERSION } from "@cursorless/common"; import * as vscode from "vscode"; export async function runCursorlessCommand( @@ -7,3 +11,11 @@ export async function runCursorlessCommand( ): Promise { return await vscode.commands.executeCommand(CURSORLESS_COMMAND_ID, command); } + +export async function runCursorlessAction(action: ActionDescriptor) { + return runCursorlessCommand({ + version: LATEST_VERSION, + usePrePhraseSnapshot: false, + action, + }); +} From ec9b4848af6bdaaa8f6b3e1de3cac0f603311ff5 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Fri, 14 Nov 2025 04:05:08 +0100 Subject: [PATCH 02/12] Clean up --- .../src/suite/performance.vscode.test.ts | 95 ++++++++----------- 1 file changed, 41 insertions(+), 54 deletions(-) diff --git a/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts index 15025d6030..686345c790 100644 --- a/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts @@ -93,8 +93,8 @@ suite("Performance", async function () { } }); -async function removeToken(thresholdMs: number) { - await testPerformance(thresholdMs, { +function removeToken(thresholdMs: number) { + return testPerformance(thresholdMs, { name: "remove", target: { type: "primitive", @@ -103,22 +103,8 @@ async function removeToken(thresholdMs: number) { }); } -async function selectScopeType( - scopeType: ScopeType, - thresholdMs: number, - modifierType?: ModifierType, -) { - await testPerformance(thresholdMs, { - name: "setSelection", - target: { - type: "primitive", - modifiers: [getModifier(scopeType, modifierType)], - }, - }); -} - -async function selectWithMultipleCursors(thresholdMs: number) { - const callback = async () => { +function selectWithMultipleCursors(thresholdMs: number) { + return testPerformanceCallback(thresholdMs, async () => { await runCursorlessAction({ name: "setSelectionBefore", target: { @@ -134,51 +120,32 @@ async function selectWithMultipleCursors(thresholdMs: number) { modifiers: [getModifier({ type: "surroundingPair", delimiter: "any" })], }, }); - }; - - await testPerformanceCallback(thresholdMs, callback); + }); } -function getModifier( +function selectScopeType( 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, - }; - } + thresholdMs: number, + modifierType?: ModifierType, +) { + return testPerformance(thresholdMs, { + name: "setSelection", + target: { + type: "primitive", + modifiers: [getModifier(scopeType, modifierType)], + }, + }); } -async function testPerformance(thresholdMs: number, action: ActionDescriptor) { - 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); - - const start = performance.now(); - - const callback = async () => { - await runCursorlessAction(action); - }; - - testPerformanceCallback(thresholdMs, callback); +function testPerformance(thresholdMs: number, action: ActionDescriptor) { + return testPerformanceCallback(thresholdMs, () => { + return runCursorlessAction(action); + }); } async function testPerformanceCallback( thresholdMs: number, - callback: () => Promise, + callback: () => Promise, ) { const editor = await openNewEditor(testData, { languageId: "json" }); // This is the position of the last json key in the document @@ -201,6 +168,26 @@ async function testPerformanceCallback( ); } +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] { From 3c07a5d86677c83cc56f951fc637797ab1173a63 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Fri, 14 Nov 2025 04:16:28 +0100 Subject: [PATCH 03/12] Updating --- .../cursorless-engine/src/languages/LanguageDefinitions.ts | 2 +- .../src/languages/TreeSitterQuery/TreeSitterQuery.ts | 2 +- .../{treeSitterQueryCache.ts => aTreeSitterQueryCache.ts} | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename packages/cursorless-engine/src/languages/TreeSitterQuery/{treeSitterQueryCache.ts => aTreeSitterQueryCache.ts} (94%) diff --git a/packages/cursorless-engine/src/languages/LanguageDefinitions.ts b/packages/cursorless-engine/src/languages/LanguageDefinitions.ts index 34c3b38920..ac5b2248bc 100644 --- a/packages/cursorless-engine/src/languages/LanguageDefinitions.ts +++ b/packages/cursorless-engine/src/languages/LanguageDefinitions.ts @@ -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/aTreeSitterQueryCache"; /** * Sentinel value to indicate that a language doesn't have diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts index 145724b729..d33d1f2f0c 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts @@ -16,7 +16,7 @@ import { getStartOfEndOfRange, rewriteStartOfEndOf, } from "./rewriteStartOfEndOf"; -import { treeSitterQueryCache } from "./treeSitterQueryCache"; +import { treeSitterQueryCache } from "./aTreeSitterQueryCache"; /** * Wrapper around a tree-sitter query that provides a more convenient API, and diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/treeSitterQueryCache.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/aTreeSitterQueryCache.ts similarity index 94% rename from packages/cursorless-engine/src/languages/TreeSitterQuery/treeSitterQueryCache.ts rename to packages/cursorless-engine/src/languages/TreeSitterQuery/aTreeSitterQueryCache.ts index 07124e5e38..a2b251912e 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/treeSitterQueryCache.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/aTreeSitterQueryCache.ts @@ -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 = ""; @@ -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(); From fb295e7c2df9307712b01d8a3127bc99bf8c0900 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Fri, 14 Nov 2025 04:16:56 +0100 Subject: [PATCH 04/12] rename --- packages/cursorless-engine/src/languages/LanguageDefinitions.ts | 2 +- .../src/languages/TreeSitterQuery/TreeSitterQuery.ts | 2 +- .../{aTreeSitterQueryCache.ts => TreeSitterQueryCache.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/cursorless-engine/src/languages/TreeSitterQuery/{aTreeSitterQueryCache.ts => TreeSitterQueryCache.ts} (100%) diff --git a/packages/cursorless-engine/src/languages/LanguageDefinitions.ts b/packages/cursorless-engine/src/languages/LanguageDefinitions.ts index ac5b2248bc..9129c7909d 100644 --- a/packages/cursorless-engine/src/languages/LanguageDefinitions.ts +++ b/packages/cursorless-engine/src/languages/LanguageDefinitions.ts @@ -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/aTreeSitterQueryCache"; +import { treeSitterQueryCache } from "./TreeSitterQuery/TreeSitterQueryCache"; /** * Sentinel value to indicate that a language doesn't have diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts index d33d1f2f0c..18f4974bb9 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts @@ -16,7 +16,7 @@ import { getStartOfEndOfRange, rewriteStartOfEndOf, } from "./rewriteStartOfEndOf"; -import { treeSitterQueryCache } from "./aTreeSitterQueryCache"; +import { treeSitterQueryCache } from "./TreeSitterQueryCache"; /** * Wrapper around a tree-sitter query that provides a more convenient API, and diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/aTreeSitterQueryCache.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQueryCache.ts similarity index 100% rename from packages/cursorless-engine/src/languages/TreeSitterQuery/aTreeSitterQueryCache.ts rename to packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQueryCache.ts From f56d6307cd003395427f14891d5bc05732e23d48 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Fri, 14 Nov 2025 05:18:33 +0100 Subject: [PATCH 05/12] Added scope handler cache --- packages/cursorless-engine/src/index.ts | 2 + .../CollectionItemTextualScopeHandler.ts | 25 ++++++++++-- .../scopeHandlers/ScopeHandlerCache.ts | 40 +++++++++++++++++++ .../SurroundingPairScopeHandler.ts | 26 ++++++++---- .../src/suite/performance.vscode.test.ts | 37 +++++++++++++---- .../src/constructTestHelpers.ts | 15 +++++-- packages/vscode-common/src/TestHelpers.ts | 3 +- 7 files changed, 125 insertions(+), 23 deletions(-) create mode 100644 packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerCache.ts diff --git a/packages/cursorless-engine/src/index.ts b/packages/cursorless-engine/src/index.ts index e00aaf5fce..d8bde764d8 100644 --- a/packages/cursorless-engine/src/index.ts +++ b/packages/cursorless-engine/src/index.ts @@ -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"; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts index 57afc37d04..25e65aa2d8 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts @@ -21,6 +21,9 @@ import { collectionItemTextualIterationScopeHandler } from "./collectionItemText import { createTargetScope } from "./createTargetScope"; import { getInteriorRanges } from "./getInteriorRanges"; import { getSeparatorOccurrences } from "./getSeparatorOccurrences"; +import { scopeHandlerCache } from "../ScopeHandlerCache"; + +const cacheKey = "CollectionItemTextualScopeHandler"; export class CollectionItemTextualScopeHandler extends BaseScopeHandler { public scopeType: ScopeType = { type: "collectionItem" }; @@ -43,6 +46,24 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler { direction: Direction, hints: ScopeIteratorRequirements, ): Iterable { + if (!scopeHandlerCache.isValid(cacheKey, editor.document)) { + const scopes = this.getsScopes(editor, direction, hints); + + scopeHandlerCache.update(cacheKey, editor.document, scopes); + } + + const scopes = scopeHandlerCache.get(); + + scopes.sort((a, b) => compareTargetScopes(direction, position, a, b)); + + yield* scopes; + } + + private getsScopes( + editor: TextEditor, + direction: Direction, + hints: ScopeIteratorRequirements, + ) { const isEveryScope = isEveryScopeModifier(hints); const separatorRanges = getSeparatorOccurrences(editor.document); const interiorRanges = getInteriorRanges( @@ -134,9 +155,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) { diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerCache.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerCache.ts new file mode 100644 index 0000000000..c6a1f98b43 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerCache.ts @@ -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[] { + return this.matches; + } +} + +export const scopeHandlerCache = new ScopeHandlerCache(); diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/SurroundingPairScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/SurroundingPairScopeHandler.ts index eeafbdead3..073085a28d 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/SurroundingPairScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/SurroundingPairScopeHandler.ts @@ -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 = { @@ -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(); - surroundingPairs = maybeApplyEmptyTargetHack( + const updatedSurroundingPairs = maybeApplyEmptyTargetHack( direction, hints, position, surroundingPairs, ); - yield* surroundingPairs + yield* updatedSurroundingPairs .map((pair) => createTargetScope( editor, diff --git a/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts index 686345c790..9398da9237 100644 --- a/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts @@ -5,7 +5,11 @@ import { type ScopeType, type SimpleScopeTypeType, } from "@cursorless/common"; -import { openNewEditor, runCursorlessAction } from "@cursorless/vscode-common"; +import { + getCursorlessApi, + openNewEditor, + runCursorlessAction, +} from "@cursorless/vscode-common"; import assert from "assert"; import * as vscode from "vscode"; import { endToEndTestSetup } from "../endToEndTestSetup"; @@ -14,6 +18,7 @@ const testData = generateTestData(100); const smallThresholdMs = 100; const largeThresholdMs = 600; +const xlThresholdMs = 1500; type ModifierType = "containing" | "previous" | "every"; @@ -37,11 +42,6 @@ suite("Performance", async function () { asyncSafety(() => removeToken(smallThresholdMs)), ); - test( - "Select with multiple cursors", - asyncSafety(() => selectWithMultipleCursors(largeThresholdMs)), - ); - const fixtures: ( | [SimpleScopeTypeType | ScopeType, number] | [SimpleScopeTypeType | ScopeType, number, ModifierType] @@ -91,6 +91,25 @@ suite("Performance", async function () { asyncSafety(() => selectScopeType(scopeType, threshold, modifierType)), ); } + + test( + "Select surroundingPair with multiple cursors", + asyncSafety(() => + selectWithMultipleCursors(xlThresholdMs, { + type: "surroundingPair", + delimiter: "any", + }), + ), + ); + + test( + "Select collectionItem with multiple cursors", + asyncSafety(() => + selectWithMultipleCursors(xlThresholdMs, { + type: "collectionItem", + }), + ), + ); }); function removeToken(thresholdMs: number) { @@ -103,7 +122,7 @@ function removeToken(thresholdMs: number) { }); } -function selectWithMultipleCursors(thresholdMs: number) { +function selectWithMultipleCursors(thresholdMs: number, scopeType: ScopeType) { return testPerformanceCallback(thresholdMs, async () => { await runCursorlessAction({ name: "setSelectionBefore", @@ -117,7 +136,7 @@ function selectWithMultipleCursors(thresholdMs: number) { name: "setSelection", target: { type: "primitive", - modifiers: [getModifier({ type: "surroundingPair", delimiter: "any" })], + modifiers: [getModifier(scopeType)], }, }); }); @@ -154,6 +173,8 @@ async function testPerformanceCallback( editor.selections = [selection]; editor.revealRange(selection); + (await getCursorlessApi()).testHelpers!.clearCache(); + const start = performance.now(); await callback(); diff --git a/packages/cursorless-vscode/src/constructTestHelpers.ts b/packages/cursorless-vscode/src/constructTestHelpers.ts index d224027ff4..dd3cdd7631 100644 --- a/packages/cursorless-vscode/src/constructTestHelpers.ts +++ b/packages/cursorless-vscode/src/constructTestHelpers.ts @@ -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"; @@ -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( diff --git a/packages/vscode-common/src/TestHelpers.ts b/packages/vscode-common/src/TestHelpers.ts index dbbf3224c5..04e04de498 100644 --- a/packages/vscode-common/src/TestHelpers.ts +++ b/packages/vscode-common/src/TestHelpers.ts @@ -11,7 +11,8 @@ import type { SpyWebViewEvent } from "./SpyWebViewEvent"; export interface VscodeTestHelpers extends TestHelpers { ide: NormalizedIDE; - injectIde: (ide: IDE) => void; + injectIde(ide: IDE): void; + clearCache(): void; scopeProvider: ScopeProvider; From b6857b4e4b7ffb6cbf257dfcc1f48a656286b921 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Fri, 14 Nov 2025 05:25:23 +0100 Subject: [PATCH 06/12] Added hints to key --- .../CollectionItemTextualScopeHandler.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts index 25e65aa2d8..07cb0f1a76 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts @@ -23,8 +23,6 @@ import { getInteriorRanges } from "./getInteriorRanges"; import { getSeparatorOccurrences } from "./getSeparatorOccurrences"; import { scopeHandlerCache } from "../ScopeHandlerCache"; -const cacheKey = "CollectionItemTextualScopeHandler"; - export class CollectionItemTextualScopeHandler extends BaseScopeHandler { public scopeType: ScopeType = { type: "collectionItem" }; protected isHierarchical = true; @@ -46,8 +44,11 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler { direction: Direction, hints: ScopeIteratorRequirements, ): Iterable { + const isEveryScope = isEveryScopeModifier(hints); + const cacheKey = "CollectionItemTextualScopeHandler_" + isEveryScope; + if (!scopeHandlerCache.isValid(cacheKey, editor.document)) { - const scopes = this.getsScopes(editor, direction, hints); + const scopes = this.getsScopes(editor, direction, isEveryScope); scopeHandlerCache.update(cacheKey, editor.document, scopes); } @@ -62,9 +63,8 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler { private getsScopes( editor: TextEditor, direction: Direction, - hints: ScopeIteratorRequirements, + isEveryScope: boolean, ) { - const isEveryScope = isEveryScopeModifier(hints); const separatorRanges = getSeparatorOccurrences(editor.document); const interiorRanges = getInteriorRanges( this.scopeHandlerFactory, From b39a24fd65b8e67e4d6a556d31f76664979ca238 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Fri, 14 Nov 2025 09:54:44 +0100 Subject: [PATCH 07/12] Clear cache when opening new editor --- .../src/suite/performance.vscode.test.ts | 2 -- packages/vscode-common/src/testUtil/openNewEditor.ts | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts index 9398da9237..b0cb5b1c31 100644 --- a/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts @@ -173,8 +173,6 @@ async function testPerformanceCallback( editor.selections = [selection]; editor.revealRange(selection); - (await getCursorlessApi()).testHelpers!.clearCache(); - const start = performance.now(); await callback(); diff --git a/packages/vscode-common/src/testUtil/openNewEditor.ts b/packages/vscode-common/src/testUtil/openNewEditor.ts index ff7e82008b..812ae8c8de 100644 --- a/packages/vscode-common/src/testUtil/openNewEditor.ts +++ b/packages/vscode-common/src/testUtil/openNewEditor.ts @@ -1,5 +1,5 @@ import * as vscode from "vscode"; -import { getParseTreeApi } from "../getExtensionApi"; +import { getCursorlessApi, getParseTreeApi } from "../getExtensionApi"; interface NewEditorOptions { languageId?: string; @@ -36,6 +36,8 @@ export async function openNewEditor( // Many times running these tests opens the sidebar, which slows performance. Close it. vscode.commands.executeCommand("workbench.action.closeSidebar"); + (await getCursorlessApi()).testHelpers!.clearCache(); + return editor; } From cfb37f33a58e775c7f7071cde190ff6aafcc9f77 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Fri, 14 Nov 2025 10:01:04 +0100 Subject: [PATCH 08/12] Fix imports --- .../src/suite/performance.vscode.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts index b0cb5b1c31..a1fdbdba19 100644 --- a/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts @@ -5,11 +5,7 @@ import { type ScopeType, type SimpleScopeTypeType, } from "@cursorless/common"; -import { - getCursorlessApi, - openNewEditor, - runCursorlessAction, -} from "@cursorless/vscode-common"; +import { openNewEditor, runCursorlessAction } from "@cursorless/vscode-common"; import assert from "assert"; import * as vscode from "vscode"; import { endToEndTestSetup } from "../endToEndTestSetup"; From b01b5b2a7291972980b030f37ba51656d460ac3c Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Fri, 14 Nov 2025 10:25:19 +0100 Subject: [PATCH 09/12] Empty commit From bc47e98a2a1072fc49aae6006fdf69238c0e313c Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Fri, 14 Nov 2025 10:27:25 +0100 Subject: [PATCH 10/12] Clear cache for notebook editors --- packages/vscode-common/src/testUtil/openNewEditor.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/vscode-common/src/testUtil/openNewEditor.ts b/packages/vscode-common/src/testUtil/openNewEditor.ts index 812ae8c8de..5df456dad8 100644 --- a/packages/vscode-common/src/testUtil/openNewEditor.ts +++ b/packages/vscode-common/src/testUtil/openNewEditor.ts @@ -21,6 +21,8 @@ export async function openNewEditor( await (await getParseTreeApi()).loadLanguage(languageId); + (await getCursorlessApi()).testHelpers!.clearCache(); + const editor = await vscode.window.showTextDocument( document, openBeside ? vscode.ViewColumn.Beside : undefined, @@ -36,8 +38,6 @@ export async function openNewEditor( // Many times running these tests opens the sidebar, which slows performance. Close it. vscode.commands.executeCommand("workbench.action.closeSidebar"); - (await getCursorlessApi()).testHelpers!.clearCache(); - return editor; } @@ -98,6 +98,8 @@ export async function openNewNotebookEditor( await (await getParseTreeApi()).loadLanguage(language); + (await getCursorlessApi()).testHelpers!.clearCache(); + // FIXME: There seems to be some timing issue when you create a notebook // editor await waitForEditorToOpen(); From 8228fedc07f21f55cfe2beb591b7cce9cd7f250e Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 15 Nov 2025 11:21:45 +0100 Subject: [PATCH 11/12] update tests --- .../src/suite/performance.vscode.test.ts | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts index a1fdbdba19..d2c03de1cf 100644 --- a/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/performance.vscode.test.ts @@ -14,7 +14,6 @@ const testData = generateTestData(100); const smallThresholdMs = 100; const largeThresholdMs = 600; -const xlThresholdMs = 1500; type ModifierType = "containing" | "previous" | "every"; @@ -52,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], @@ -66,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" }, @@ -91,7 +92,7 @@ suite("Performance", async function () { test( "Select surroundingPair with multiple cursors", asyncSafety(() => - selectWithMultipleCursors(xlThresholdMs, { + selectWithMultipleCursors(largeThresholdMs, { type: "surroundingPair", delimiter: "any", }), @@ -101,7 +102,7 @@ suite("Performance", async function () { test( "Select collectionItem with multiple cursors", asyncSafety(() => - selectWithMultipleCursors(xlThresholdMs, { + selectWithMultipleCursors(largeThresholdMs, { type: "collectionItem", }), ), @@ -119,23 +120,27 @@ function removeToken(thresholdMs: number) { } function selectWithMultipleCursors(thresholdMs: number, scopeType: ScopeType) { - return testPerformanceCallback(thresholdMs, async () => { - await runCursorlessAction({ - name: "setSelectionBefore", - target: { - type: "primitive", - modifiers: [getModifier({ type: "collectionItem" }, "every")], - }, - }); - - await runCursorlessAction({ - name: "setSelection", - target: { - type: "primitive", - modifiers: [getModifier(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( @@ -161,6 +166,7 @@ function testPerformance(thresholdMs: number, action: ActionDescriptor) { async function testPerformanceCallback( thresholdMs: number, callback: () => Promise, + beforeCallback?: () => Promise, ) { const editor = await openNewEditor(testData, { languageId: "json" }); // This is the position of the last json key in the document @@ -169,6 +175,10 @@ async function testPerformanceCallback( editor.selections = [selection]; editor.revealRange(selection); + if (beforeCallback != null) { + await beforeCallback(); + } + const start = performance.now(); await callback(); From 4a6aaf1eae8e6b26fe306579174a83e11a35951a Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 15 Nov 2025 11:48:21 +0100 Subject: [PATCH 12/12] Sorting ports --- .../CollectionItemTextualScopeHandler.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts index 07cb0f1a76..e356957b2d 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts @@ -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"; @@ -21,7 +22,6 @@ import { collectionItemTextualIterationScopeHandler } from "./collectionItemText import { createTargetScope } from "./createTargetScope"; import { getInteriorRanges } from "./getInteriorRanges"; import { getSeparatorOccurrences } from "./getSeparatorOccurrences"; -import { scopeHandlerCache } from "../ScopeHandlerCache"; export class CollectionItemTextualScopeHandler extends BaseScopeHandler { public scopeType: ScopeType = { type: "collectionItem" }; @@ -49,7 +49,6 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler { if (!scopeHandlerCache.isValid(cacheKey, editor.document)) { const scopes = this.getsScopes(editor, direction, isEveryScope); - scopeHandlerCache.update(cacheKey, editor.document, scopes); }