From bbab6cad1c4f22d2a7532f4abd652e2c449f63c8 Mon Sep 17 00:00:00 2001 From: Trillium Smith Date: Thu, 22 Jan 2026 13:11:07 -0800 Subject: [PATCH 1/4] feat(scope): add "that" scope type for referencing previous targets --- .../common/src/types/command/PartialTargetDescriptor.types.ts | 1 + .../cursorless-engine/src/scopeProviders/ScopeInfoProvider.ts | 1 + .../src/spokenForms/defaultSpokenFormMapCore.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/common/src/types/command/PartialTargetDescriptor.types.ts b/packages/common/src/types/command/PartialTargetDescriptor.types.ts index 3203c79690..b171955108 100644 --- a/packages/common/src/types/command/PartialTargetDescriptor.types.ts +++ b/packages/common/src/types/command/PartialTargetDescriptor.types.ts @@ -202,6 +202,7 @@ export const simpleScopeTypeTypes = [ "boundedNonWhitespaceSequence", "url", "notebookCell", + "that", // Talon "command", // Private scope types diff --git a/packages/cursorless-engine/src/scopeProviders/ScopeInfoProvider.ts b/packages/cursorless-engine/src/scopeProviders/ScopeInfoProvider.ts index 48dc157edb..21d22454e6 100644 --- a/packages/cursorless-engine/src/scopeProviders/ScopeInfoProvider.ts +++ b/packages/cursorless-engine/src/scopeProviders/ScopeInfoProvider.ts @@ -180,6 +180,7 @@ function isLanguageSpecific(scopeType: ScopeType): boolean { case "boundedNonWhitespaceSequence": case "url": case "notebookCell": + case "that": case "surroundingPair": case "surroundingPairInterior": case "customRegex": diff --git a/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts b/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts index 3bf96c1d90..07fab68e11 100644 --- a/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts +++ b/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts @@ -102,6 +102,7 @@ export const defaultSpokenFormMapCore: DefaultSpokenFormMapDefinition = { boundedNonWhitespaceSequence: "short paint", url: "link", notebookCell: "cell", + that: "that", string: isPrivate("parse tree string"), textFragment: isPrivate("text fragment"), From 0c68011db8ad33b582cda7ef606aa036cea8db68 Mon Sep 17 00:00:00 2001 From: Trillium Smith Date: Thu, 22 Jan 2026 13:11:29 -0800 Subject: [PATCH 2/4] feat(scope): implement ThatScopeHandler for "that" scope --- .../scopeHandlers/ThatScopeHandler.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ThatScopeHandler.ts diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ThatScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ThatScopeHandler.ts new file mode 100644 index 0000000000..a4f12e5333 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ThatScopeHandler.ts @@ -0,0 +1,57 @@ +import type { Direction, ScopeType } from "@cursorless/common"; +import type { StoredTargetMap } from "../../../core/StoredTargets"; +import { TokenTarget } from "../../targets"; +import { NestedScopeHandler } from "./NestedScopeHandler"; +import type { ScopeHandlerFactory } from "./ScopeHandlerFactory"; +import type { TargetScope } from "./scope.types"; + +/** + * Scope handler that returns the range(s) from the "that" mark. + * The "that" mark stores the targets from the most recent Cursorless command. + */ +export class ThatScopeHandler extends NestedScopeHandler { + public readonly scopeType = { type: "that" } as const; + public readonly iterationScopeType: ScopeType = { type: "document" }; + + constructor( + scopeHandlerFactory: ScopeHandlerFactory, + scopeType: ScopeType, + languageId: string, + private storedTargets: StoredTargetMap, + ) { + super(scopeHandlerFactory, scopeType, languageId); + } + + protected *generateScopesInSearchScope( + _direction: Direction, + { editor }: TargetScope, + ): Iterable { + // Get the targets stored in the "that" mark + const thatTargets = this.storedTargets.get("that"); + + if (thatTargets == null || thatTargets.length === 0) { + // No "that" mark available, yield nothing + return; + } + + // Filter targets to only those in the current editor + const editorTargets = thatTargets.filter( + (target) => target.editor.id === editor.id, + ); + + // Yield a TargetScope for each target + for (const target of editorTargets) { + yield { + editor: target.editor, + domain: target.contentRange, + getTargets: (isReversed) => [ + new TokenTarget({ + editor: target.editor, + contentRange: target.contentRange, + isReversed, + }), + ], + }; + } + } +} From b2076c37ff56b45ed1a48425523de6d416a25ba3 Mon Sep 17 00:00:00 2001 From: Trillium Smith Date: Thu, 22 Jan 2026 13:11:36 -0800 Subject: [PATCH 3/4] feat(scope): wire up ThatScopeHandler throughout the engine --- packages/cursorless-engine/src/cursorlessEngine.ts | 6 +++++- .../scopeHandlers/ScopeHandlerFactoryImpl.ts | 14 +++++++++++++- packages/cursorless-engine/src/runCommand.ts | 2 +- .../src/scopeProviders/ScopeRangeWatcher.ts | 8 ++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index d60dbb3057..af72c52a98 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -150,7 +150,10 @@ function createScopeProvider( storedTargets: StoredTargetMap, customSpokenFormGenerator: CustomSpokenFormGeneratorImpl, ): ScopeProvider { - const scopeHandlerFactory = new ScopeHandlerFactoryImpl(languageDefinitions); + const scopeHandlerFactory = new ScopeHandlerFactoryImpl( + languageDefinitions, + storedTargets, + ); const rangeProvider = new ScopeRangeProvider( scopeHandlerFactory, @@ -164,6 +167,7 @@ function createScopeProvider( const rangeWatcher = new ScopeRangeWatcher( languageDefinitions, rangeProvider, + storedTargets, ); const supportChecker = new ScopeSupportChecker(scopeHandlerFactory); const infoProvider = new ScopeInfoProvider(customSpokenFormGenerator); diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts index a78e17bc4d..12f1ae1952 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts @@ -32,8 +32,10 @@ import { SurroundingPairScopeHandler, } from "./SurroundingPairScopeHandler"; import { InteriorScopeHandler } from "./SurroundingPairScopeHandler/InteriorScopeHandler"; +import { ThatScopeHandler } from "./ThatScopeHandler"; import { TokenScopeHandler } from "./TokenScopeHandler"; import { WordScopeHandler } from "./WordScopeHandler/WordScopeHandler"; +import type { StoredTargetMap } from "../../../core/StoredTargets"; /** * Returns a scope handler for the given scope type and language id, or @@ -46,7 +48,10 @@ import { WordScopeHandler } from "./WordScopeHandler/WordScopeHandler"; * undefined if the given scope type / language id combination is not supported. */ export class ScopeHandlerFactoryImpl implements ScopeHandlerFactory { - constructor(private languageDefinitions: LanguageDefinitions) { + constructor( + private languageDefinitions: LanguageDefinitions, + private storedTargets: StoredTargetMap, + ) { this.maybeCreate = this.maybeCreate.bind(this); this.create = this.create.bind(this); } @@ -118,6 +123,13 @@ export class ScopeHandlerFactoryImpl implements ScopeHandlerFactory { scopeType, languageId, ); + case "that": + return new ThatScopeHandler( + this, + scopeType, + languageId, + this.storedTargets, + ); case "interior": return InteriorScopeHandler.maybeCreate( this.languageDefinitions, diff --git a/packages/cursorless-engine/src/runCommand.ts b/packages/cursorless-engine/src/runCommand.ts index cf7828bc40..9bb3be6dd9 100644 --- a/packages/cursorless-engine/src/runCommand.ts +++ b/packages/cursorless-engine/src/runCommand.ts @@ -103,7 +103,7 @@ function createCommandRunner( const modifierStageFactory = new ModifierStageFactoryImpl( languageDefinitions, storedTargets, - new ScopeHandlerFactoryImpl(languageDefinitions), + new ScopeHandlerFactoryImpl(languageDefinitions, storedTargets), ); const markStageFactory = new MarkStageFactoryImpl( diff --git a/packages/cursorless-engine/src/scopeProviders/ScopeRangeWatcher.ts b/packages/cursorless-engine/src/scopeProviders/ScopeRangeWatcher.ts index 56a4d89c80..f26c3f52ba 100644 --- a/packages/cursorless-engine/src/scopeProviders/ScopeRangeWatcher.ts +++ b/packages/cursorless-engine/src/scopeProviders/ScopeRangeWatcher.ts @@ -13,6 +13,7 @@ import type { LanguageDefinitions } from "../languages/LanguageDefinitions"; import { ide } from "../singletons/ide.singleton"; import type { ScopeRangeProvider } from "./ScopeRangeProvider"; import { DecorationDebouncer } from "../util/DecorationDebouncer"; +import type { StoredTargetMap } from "../core/StoredTargets"; /** * Watches for changes to the scope ranges of visible editors and notifies @@ -25,6 +26,7 @@ export class ScopeRangeWatcher { constructor( languageDefinitions: LanguageDefinitions, private scopeRangeProvider: ScopeRangeProvider, + storedTargets: StoredTargetMap, ) { this.onChange = this.onChange.bind(this); this.onDidChangeScopeRanges = this.onDidChangeScopeRanges.bind(this); @@ -48,6 +50,12 @@ export class ScopeRangeWatcher { ide().onDidChangeTextDocument(debouncer.run), ide().onDidChangeTextEditorVisibleRanges(debouncer.run), languageDefinitions.onDidChangeDefinition(this.onChange), + // Listen for "that" mark changes to update the "that" scope visualization + storedTargets.onStoredTargets((key, _targets) => { + if (key === "that") { + debouncer.run(); + } + }), debouncer, ); } From bb0a154ec139a2da8f45b185df2bbb90ff75ebe7 Mon Sep 17 00:00:00 2001 From: Trillium Smith Date: Thu, 22 Jan 2026 13:17:24 -0800 Subject: [PATCH 4/4] feat(talon): add "that" to scope_type for visualization --- cursorless-talon/src/spoken_forms.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cursorless-talon/src/spoken_forms.json b/cursorless-talon/src/spoken_forms.json index dd332db2ab..f12c107f23 100644 --- a/cursorless-talon/src/spoken_forms.json +++ b/cursorless-talon/src/spoken_forms.json @@ -182,7 +182,8 @@ "short paint": "boundedNonWhitespaceSequence", "short block": "boundedParagraph", "link": "url", - "cell": "notebookCell" + "cell": "notebookCell", + "that": "that" }, "surrounding_pair_scope_type": { "string": "string"