diff --git a/changelog/2023-12.addedGrandScopeModifier.md b/changelog/2023-12.addedGrandScopeModifier.md new file mode 100644 index 0000000000..394d516509 --- /dev/null +++ b/changelog/2023-12.addedGrandScopeModifier.md @@ -0,0 +1,6 @@ +--- +tags: [enhancement] +pullRequest: 2130 +--- + +- Added `grand` modifier. This modifier will take the containing grandparent scope. eg `"take grand statement air"` diff --git a/cursorless-talon/src/cheatsheet/sections/modifiers.py b/cursorless-talon/src/cheatsheet/sections/modifiers.py index 576edc4721..55d3f396a3 100644 --- a/cursorless-talon/src/cheatsheet/sections/modifiers.py +++ b/cursorless-talon/src/cheatsheet/sections/modifiers.py @@ -22,6 +22,7 @@ def get_modifiers(): "extendThroughStartOf", "extendThroughEndOf", "every", + "ancestor", "first", "last", "previous", @@ -101,6 +102,16 @@ def get_modifiers(): }, ], }, + { + "id": "ancestor", + "type": "modifier", + "variations": [ + { + "spokenForm": f"{complex_modifiers['ancestor']} ", + "description": "Grandparent containing instance of ", + }, + ], + }, { "id": "relativeScope", "type": "modifier", diff --git a/cursorless-talon/src/modifiers/simple_scope_modifier.py b/cursorless-talon/src/modifiers/simple_scope_modifier.py index 7725623304..5d51ba8a4e 100644 --- a/cursorless-talon/src/modifiers/simple_scope_modifier.py +++ b/cursorless-talon/src/modifiers/simple_scope_modifier.py @@ -15,7 +15,23 @@ ) def cursorless_simple_scope_modifier(m) -> dict[str, Any]: """Containing scope, every scope, etc""" + if hasattr(m, "cursorless_simple_scope_modifier"): + modifier = m.cursorless_simple_scope_modifier + + if modifier == "every": + return { + "type": "everyScope", + "scopeType": m.cursorless_scope_type, + } + + if modifier == "ancestor": + return { + "type": "containingScope", + "scopeType": m.cursorless_scope_type, + "ancestorIndex": 1, + } + return { - "type": "everyScope" if m[0] == "every" else "containingScope", + "type": "containingScope", "scopeType": m.cursorless_scope_type, } diff --git a/cursorless-talon/src/spoken_forms.json b/cursorless-talon/src/spoken_forms.json index eec6aac6e2..20fe7b1260 100644 --- a/cursorless-talon/src/spoken_forms.json +++ b/cursorless-talon/src/spoken_forms.json @@ -81,7 +81,7 @@ "its": "inferPreviousMark", "visible": "visible" }, - "simple_scope_modifier": { "every": "every" }, + "simple_scope_modifier": { "every": "every", "grand": "ancestor" }, "interior_modifier": { "inside": "interiorOnly" }, diff --git a/docs/user/README.md b/docs/user/README.md index 1b6f1674c3..897534a1d9 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -215,6 +215,15 @@ The command `"every"` can be used to select a syntactic element and all of its m For example, the command `take every key [blue] air` will select every key in the map/object/dict including the token with a blue hat over the letter 'a'. +##### `"grand"` + +The command `"grand"` can be used to select the grand parent of the containing syntactic element. + +- `"take grand statement air"` +- `"take grand funk air"` + +For example, the command `take grand statement [blue] air` will select that parent statement of the statement containing the token with a blue hat over the letter 'a'. + ##### Sub-token modifiers ###### `"sub"` diff --git a/packages/cursorless-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts b/packages/cursorless-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts index 6dba6a8f65..28c92afb5f 100644 --- a/packages/cursorless-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts +++ b/packages/cursorless-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts @@ -49,7 +49,18 @@ export class PrimitiveTargetSpokenFormGenerator { throw new NoSpokenFormError(`Modifier '${modifier.type}'`); case "containingScope": - return [this.handleScopeType(modifier.scopeType)]; + if (modifier.ancestorIndex == null || modifier.ancestorIndex === 0) { + return this.handleScopeType(modifier.scopeType); + } + if (modifier.ancestorIndex === 1) { + return [ + this.spokenFormMap.modifierExtra.ancestor, + this.handleScopeType(modifier.scopeType), + ]; + } + throw new NoSpokenFormError( + `Modifier '${modifier.type}' with ancestor index ${modifier.ancestorIndex}`, + ); case "everyScope": return [ diff --git a/packages/cursorless-engine/src/processTargets/modifiers/getContainingScopeTarget.ts b/packages/cursorless-engine/src/processTargets/modifiers/getContainingScopeTarget.ts index 24aa3f6c6a..c1b38b610d 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/getContainingScopeTarget.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/getContainingScopeTarget.ts @@ -38,7 +38,8 @@ export function getContainingScopeTarget( editor, scope.domain.end, "forward", - ancestorIndex - 1, + ancestorIndex, + true, ); } @@ -86,10 +87,12 @@ function expandFromPosition( position: Position, direction: Direction, ancestorIndex: number, + allowAdjacentScopes: boolean = false, ): TargetScope | undefined { let nextAncestorIndex = 0; for (const scope of scopeHandler.generateScopes(editor, position, direction, { containment: "required", + allowAdjacentScopes, })) { if (nextAncestorIndex === ancestorIndex) { return scope; diff --git a/packages/cursorless-engine/src/spokenForms/SpokenFormType.ts b/packages/cursorless-engine/src/spokenForms/SpokenFormType.ts index e1d604fce3..3665eb5cd9 100644 --- a/packages/cursorless-engine/src/spokenForms/SpokenFormType.ts +++ b/packages/cursorless-engine/src/spokenForms/SpokenFormType.ts @@ -60,6 +60,7 @@ type ModifierExtra = | "previous" | "next" | "forward" - | "backward"; + | "backward" + | "ancestor"; export type SpokenFormType = keyof SpokenFormMapKeyTypes; diff --git a/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts b/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts index 6b94ec67ba..300c2cacab 100644 --- a/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts +++ b/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts @@ -133,6 +133,7 @@ export const defaultSpokenFormMapCore: DefaultSpokenFormMapDefinition = { next: "next", forward: "forward", backward: "backward", + ancestor: "grand", }, customRegex: {}, diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/containingScope/changeGrandState.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/containingScope/changeGrandState.yml new file mode 100644 index 0000000000..dd890307c3 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/containingScope/changeGrandState.yml @@ -0,0 +1,27 @@ +languageId: javascript +command: + version: 6 + spokenForm: change grand state + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: statement} + ancestorIndex: 1 + usePrePhraseSnapshot: true +initialState: + documentContents: |- + function myFunk() { + const value = 2 + } + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/containingScope/changeGrandState2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/containingScope/changeGrandState2.yml new file mode 100644 index 0000000000..4a13a62f9f --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/containingScope/changeGrandState2.yml @@ -0,0 +1,26 @@ +languageId: python +command: + version: 6 + spokenForm: change grand state + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: statement} + ancestorIndex: 1 + usePrePhraseSnapshot: true +initialState: + documentContents: |- + def my_funk(): + value = 2 + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/containingScope/changeGrandState3.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/containingScope/changeGrandState3.yml new file mode 100644 index 0000000000..59240fd15e --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/containingScope/changeGrandState3.yml @@ -0,0 +1,29 @@ +languageId: javascript +command: + version: 6 + spokenForm: change grand state + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: statement} + ancestorIndex: 1 + usePrePhraseSnapshot: true +initialState: + documentContents: |- + class MyClass { + myFunk() { + + } + } + selections: + - anchor: {line: 2, character: 8} + active: {line: 2, character: 8} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0}