From 3a51d86b9a9aa0d92d4a6dc2b75a8cdb672f0ccb Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 22 Feb 2026 11:18:06 +0100 Subject: [PATCH 1/5] Compare normalized capture index instead of names --- .../src/languages/LanguageDefinition.ts | 16 ++++----- .../TreeSitterQuery/TreeSitterQuery.ts | 34 +++++++++++------- .../TreeSitterQuery/TreeSitterQueryCache.ts | 24 +++---------- .../languages/TreeSitterQuery/captureNames.ts | 36 ++++++++++++++++--- packages/cursorless-engine/src/util/array.ts | 11 ------ .../cursorless-engine/src/util/setIsEqual.ts | 14 ++++++++ 6 files changed, 78 insertions(+), 57 deletions(-) delete mode 100644 packages/cursorless-engine/src/util/array.ts create mode 100644 packages/cursorless-engine/src/util/setIsEqual.ts diff --git a/packages/cursorless-engine/src/languages/LanguageDefinition.ts b/packages/cursorless-engine/src/languages/LanguageDefinition.ts index fd63ba8748..288154a37f 100644 --- a/packages/cursorless-engine/src/languages/LanguageDefinition.ts +++ b/packages/cursorless-engine/src/languages/LanguageDefinition.ts @@ -3,13 +3,13 @@ import type { RawTreeSitterQueryProvider, ScopeType, SimpleScopeType, - SimpleScopeTypeType, TextDocument, TreeSitter, } from "@cursorless/common"; import { matchAll, showError } from "@cursorless/common"; import { TreeSitterScopeHandler } from "../processTargets/modifiers/scopeHandlers"; import { TreeSitterQuery } from "./TreeSitterQuery"; +import type { ScopeCaptureName } from "./TreeSitterQuery/captureNames"; import type { QueryCapture } from "./TreeSitterQuery/QueryCapture"; import { validateQueryCaptures } from "./TreeSitterQuery/validateQueryCaptures"; @@ -87,23 +87,19 @@ export class LanguageDefinition { * document. We use this in our surrounding pair code. * * @param document The document to search - * @param captureNames Optional capture names to include + * @param scopeTypes Optional scope types to include * @returns A map of captures in the document */ - getCapturesMap( + getCapturesMap( document: TextDocument, - captureNames: readonly T[], + scopeTypes: readonly T[], ) { - const matches = this.query.matchesForCaptures( - document, - new Set(captureNames), - ); + const matches = this.query.matchesForScopeTypes(document, scopeTypes); const result: Partial> = {}; for (const match of matches) { for (const capture of match.captures) { - const name = capture.name as T; - (result[name] ??= []).push(capture); + (result[capture.name as T] ??= []).push(capture); } } diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts index a872ced16e..c4c5fe338e 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts @@ -1,7 +1,11 @@ import type { Position, TextDocument, TreeSitter } from "@cursorless/common"; import type * as treeSitter from "web-tree-sitter"; import { ide } from "../../singletons/ide.singleton"; -import { getNormalizedCaptureName } from "./captureNames"; +import { + getNormalizedCaptureIndex, + getNormalizedCaptureName, + type ScopeCaptureName, +} from "./captureNames"; import { checkCaptureStartEnd } from "./checkCaptureStartEnd"; import { getNodeRange } from "./getNodeRange"; import { isContainedInErrorNode } from "./isContainedInErrorNode"; @@ -67,18 +71,21 @@ export class TreeSitterQuery { return this.getMatches(document, start, end, undefined); } - matchesForCaptures( + matchesForScopeTypes( document: TextDocument, - captureNames: Set, + scopeTypes: readonly ScopeCaptureName[], ): QueryMatch[] { - return this.getMatches(document, undefined, undefined, captureNames); + const captureNameFilter = new Set( + scopeTypes.map(getNormalizedCaptureIndex), + ); + return this.getMatches(document, undefined, undefined, captureNameFilter); } private getMatches( document: TextDocument, start: Position | undefined, end: Position | undefined, - captureNameFilter: Set | undefined, + captureNameFilter: Set | undefined, ): QueryMatch[] { if ( !treeSitterQueryCache.isValid(document, start, end, captureNameFilter) @@ -104,7 +111,7 @@ export class TreeSitterQuery { document: TextDocument, start: Position | undefined, end: Position | undefined, - captureNameFilter: Set | undefined, + captureNameFilter: Set | undefined, ): QueryMatch[] { const matches = this.getTreeMatches(document, start, end); const results: QueryMatch[] = []; @@ -113,7 +120,7 @@ export class TreeSitterQuery { if ( captureNameFilter != null && !match.captures.some((capture) => - captureNameFilter.has(getNormalizedCaptureName(capture.name)), + captureNameFilter.has(getNormalizedCaptureIndex(capture.name)), ) ) { continue; @@ -164,14 +171,14 @@ export class TreeSitterQuery { private createMutableQueryMatch( document: TextDocument, match: treeSitter.QueryMatch, - captureNameFilter?: Set, + captureNameFilter: Set | undefined, ): MutableQueryMatch { const captures: MutableQueryCapture[] = []; for (const { name, node } of match.captures) { if ( captureNameFilter != null && - !captureNameFilter.has(getNormalizedCaptureName(name)) + !captureNameFilter.has(getNormalizedCaptureIndex(name)) ) { continue; } @@ -204,7 +211,7 @@ export class TreeSitterQuery { private createQueryMatch( match: MutableQueryMatch, - captureNameFilter?: Set, + captureNameFilter: Set | undefined, ): QueryMatch | undefined { const result: MutableQueryCapture[] = []; const map = new Map< @@ -218,10 +225,13 @@ export class TreeSitterQuery { // name, for which we'd return a capture with name `foo`. for (const capture of match.captures) { - const name = getNormalizedCaptureName(capture.name); - if (captureNameFilter != null && !captureNameFilter.has(name)) { + if ( + captureNameFilter != null && + !captureNameFilter.has(getNormalizedCaptureIndex(capture.name)) + ) { continue; } + const name = getNormalizedCaptureName(capture.name); const range = getStartOfEndOfRange(capture); const existing = map.get(name); diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQueryCache.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQueryCache.ts index ee229a501d..64732be00b 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQueryCache.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQueryCache.ts @@ -1,5 +1,6 @@ import type { Position, TextDocument } from "@cursorless/common"; import type { QueryMatch } from "./QueryCapture"; +import { setIsEqual } from "../../util/setIsEqual"; export class TreeSitterQueryCache { private documentVersion: number = -1; @@ -8,7 +9,7 @@ export class TreeSitterQueryCache { private startPosition: Position | undefined; private endPosition: Position | undefined; private matches: QueryMatch[] = []; - private captureNames: Set | undefined; + private captureNames: Set | undefined; clear() { this.documentUri = ""; @@ -24,7 +25,7 @@ export class TreeSitterQueryCache { document: TextDocument, startPosition: Position | undefined, endPosition: Position | undefined, - captureNames: Set | undefined, + captureNames: Set | undefined, ) { return ( this.documentVersion === document.version && @@ -32,7 +33,7 @@ export class TreeSitterQueryCache { this.documentLanguageId === document.languageId && positionsEqual(this.startPosition, startPosition) && positionsEqual(this.endPosition, endPosition) && - setEqual(this.captureNames, captureNames) + setIsEqual(this.captureNames, captureNames) ); } @@ -40,7 +41,7 @@ export class TreeSitterQueryCache { document: TextDocument, startPosition: Position | undefined, endPosition: Position | undefined, - captureNames: Set | undefined, + captureNames: Set | undefined, matches: QueryMatch[], ) { this.documentVersion = document.version; @@ -64,19 +65,4 @@ function positionsEqual(a: Position | undefined, b: Position | undefined) { return a.isEqual(b); } -function setEqual(a: Set | undefined, b: Set | undefined) { - if (a == null || b == null) { - return a === b; - } - if (a.size !== b.size) { - return false; - } - for (const item of a) { - if (!b.has(item)) { - return false; - } - } - return true; -} - export const treeSitterQueryCache = new TreeSitterQueryCache(); diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/captureNames.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/captureNames.ts index eba556276e..688a76e2dc 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/captureNames.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/captureNames.ts @@ -1,11 +1,15 @@ import { pseudoScopes, simpleScopeTypeTypes } from "@cursorless/common"; -const wildcard = "_"; -const captureNames = [ +const scopeCaptureNames = [ ...simpleScopeTypeTypes.filter((s) => !pseudoScopes.has(s)), - wildcard, + // Interior is pseudo scope` but it's implemented with an actual internal scope "interior", -]; +] as const; + +export type ScopeCaptureName = (typeof scopeCaptureNames)[number]; + +const wildcard = "_"; +const captureNames = [...scopeCaptureNames, wildcard]; const positionRelationships = ["prefix", "leading", "trailing"]; const positionSuffixes = [ @@ -68,19 +72,41 @@ for (const captureName of captureNames) { } const normalizedCaptureNamesMap = new Map(); +const normalizedCaptureIndexMap = new Map(); +const captureNameIndex: Record = Object.fromEntries( + captureNames.map((n, i) => [n, i]), +); for (const captureName of allowedCaptures) { - normalizedCaptureNamesMap.set(captureName, normalizeCaptureName(captureName)); + const normalizedCaptureName = normalizeCaptureName(captureName); + normalizedCaptureNamesMap.set(captureName, normalizedCaptureName); + const scopeName = getScopeName(captureName); + const index = captureNameIndex[scopeName]; + if (index == null) { + throw new Error(`No scope index for capture name ${captureName}`); + } + normalizedCaptureIndexMap.set(captureName, index); } function normalizeCaptureName(name: string): string { return name.replace(/(\.(start|end))?(\.(startOf|endOf))?$/, ""); } +// eg: for `statement.start.endOf`, returns `statement` +function getScopeName(name: string): string { + return /^(private\.[^.]*|[^.]*)/.exec(name)![0]; +} + export function isCaptureAllowed(captureName: string): boolean { return allowedCaptures.has(captureName); } +// Capture names missing normalized name can be things like '@_dummy' export function getNormalizedCaptureName(captureName: string): string { return normalizedCaptureNamesMap.get(captureName) ?? captureName; } + +// Capture names missing normalized index can be things like '@_dummy' +export function getNormalizedCaptureIndex(captureName: string): number { + return normalizedCaptureIndexMap.get(captureName) ?? -1; +} diff --git a/packages/cursorless-engine/src/util/array.ts b/packages/cursorless-engine/src/util/array.ts deleted file mode 100644 index a4ffc45f52..0000000000 --- a/packages/cursorless-engine/src/util/array.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { range } from "lodash-es"; - -/** - * Creates a new array repeating the given array n times - * @param array The array to repeat - * @param n The number of times to repeat the array - * @returns The new array - */ -export function repeat(array: T[], n: number) { - return range(n).flatMap(() => array); -} diff --git a/packages/cursorless-engine/src/util/setIsEqual.ts b/packages/cursorless-engine/src/util/setIsEqual.ts new file mode 100644 index 0000000000..15c21ec6b1 --- /dev/null +++ b/packages/cursorless-engine/src/util/setIsEqual.ts @@ -0,0 +1,14 @@ +export function setIsEqual(a: Set | undefined, b: Set | undefined) { + if (a == null || b == null) { + return a === b; + } + if (a.size !== b.size) { + return false; + } + for (const item of a) { + if (!b.has(item)) { + return false; + } + } + return true; +} From 5fa2918b429f3e886ea0802b75d538cb1e430162 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 22 Feb 2026 11:19:48 +0100 Subject: [PATCH 2/5] Update comment --- .../cursorless-engine/src/languages/LanguageDefinition.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cursorless-engine/src/languages/LanguageDefinition.ts b/packages/cursorless-engine/src/languages/LanguageDefinition.ts index 288154a37f..e6c84b4d23 100644 --- a/packages/cursorless-engine/src/languages/LanguageDefinition.ts +++ b/packages/cursorless-engine/src/languages/LanguageDefinition.ts @@ -83,11 +83,11 @@ export class LanguageDefinition { } /** - * This is a low-level function that just returns a map of all captures in the + * This is a low-level function that just returns a map of specified captures in the * document. We use this in our surrounding pair code. * * @param document The document to search - * @param scopeTypes Optional scope types to include + * @param scopeTypes Which scope types to include * @returns A map of captures in the document */ getCapturesMap( From 88708be028a6f50e1b678779ba5cc7e2e688219c Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 22 Feb 2026 11:35:11 +0100 Subject: [PATCH 3/5] Remove unused funk --- .../TreeSitterScopeHandler/captureUtils.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/captureUtils.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/captureUtils.ts index 998e9e761b..a387a6e55e 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/captureUtils.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/captureUtils.ts @@ -61,18 +61,6 @@ export function getRelatedRange( )?.range; } -/** - * Looks in the captures of a match for a capture with one of the given names, and - * returns the range of that capture, or undefined if no matching capture was found - * - * @param match The match to get the range from - * @param names The possible names of the capture to get the range for - * @returns A range or undefined if no matching capture was found - */ -export function findCaptureRangeByName(match: QueryMatch, ...names: string[]) { - return findCaptureByName(match, ...names)?.range; -} - /** * Looks in the captures of a match for a capture with one of the given names, and * returns that capture, or undefined if no matching capture was found From 6f65fe0335501a757d0c08b6982bb9a4d2e0977b Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 22 Feb 2026 11:54:08 +0100 Subject: [PATCH 4/5] update comment --- .../src/languages/TreeSitterQuery/captureNames.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/captureNames.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/captureNames.ts index 688a76e2dc..ffebd5b8c3 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/captureNames.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/captureNames.ts @@ -2,7 +2,7 @@ import { pseudoScopes, simpleScopeTypeTypes } from "@cursorless/common"; const scopeCaptureNames = [ ...simpleScopeTypeTypes.filter((s) => !pseudoScopes.has(s)), - // Interior is pseudo scope` but it's implemented with an actual internal scope + // Interior is a pseudo scope, but it's implemented with an actual internal scope "interior", ] as const; From a892635ab0f5ded644e024c5887627b62f3335f4 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 22 Feb 2026 11:54:37 +0100 Subject: [PATCH 5/5] remove unnecessary type --- .../src/languages/TreeSitterQuery/captureNames.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/captureNames.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/captureNames.ts index ffebd5b8c3..3de65552d7 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/captureNames.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/captureNames.ts @@ -73,9 +73,7 @@ for (const captureName of captureNames) { const normalizedCaptureNamesMap = new Map(); const normalizedCaptureIndexMap = new Map(); -const captureNameIndex: Record = Object.fromEntries( - captureNames.map((n, i) => [n, i]), -); +const captureNameIndex = Object.fromEntries(captureNames.map((n, i) => [n, i])); for (const captureName of allowedCaptures) { const normalizedCaptureName = normalizeCaptureName(captureName);