diff --git a/cursorless-talon/src/modifiers/head_tail.py b/cursorless-talon/src/modifiers/head_tail.py new file mode 100644 index 0000000000..85dcff2731 --- /dev/null +++ b/cursorless-talon/src/modifiers/head_tail.py @@ -0,0 +1,26 @@ +from talon import Module + +head_tail_modifiers = { + "head": "extendThroughStartOf", + "tail": "extendThroughEndOf", +} + +mod = Module() + +mod.list( + "cursorless_head_tail_modifier", + desc="Cursorless head and tail modifiers", +) + + +@mod.capture(rule="{user.cursorless_head_tail_modifier} *") +def cursorless_head_tail_modifier(m) -> dict[str, str]: + """Cursorless head and tail modifier""" + result = { + "type": m.cursorless_head_tail_modifier, + } + try: + result["modifiers"] = m.cursorless_modifier_list + except AttributeError: + pass + return result diff --git a/cursorless-talon/src/modifiers/modifiers.py b/cursorless-talon/src/modifiers/modifiers.py index 217c6033f8..b4481808fd 100644 --- a/cursorless-talon/src/modifiers/modifiers.py +++ b/cursorless-talon/src/modifiers/modifiers.py @@ -1,6 +1,7 @@ from talon import Module, app from ..csv_overrides import init_csv_and_watch_changes +from .head_tail import head_tail_modifiers from .range_type import range_types mod = Module() @@ -11,8 +12,6 @@ "inside": "interiorOnly", "bounds": "excludeInterior", "just": "toRawSelection", - "head": "extendThroughStartOf", - "tail": "extendThroughEndOf", "leading": "leading", "trailing": "trailing", } @@ -31,11 +30,28 @@ def cursorless_simple_modifier(m) -> dict[str, str]: } +modifiers = [ + "", # before, end of + "", # inside, bounds, just, leading, trailing + "", # head, tail + "", # funk, state, class + "", # first past second word + "", # matching/pair [curly, round] +] + + +@mod.capture(rule="|".join(modifiers)) +def cursorless_modifier(m) -> str: + """Cursorless modifier""" + return m[0] + + def on_ready(): init_csv_and_watch_changes( "modifiers", { "simple_modifier": simple_modifiers, + "head_tail_modifier": head_tail_modifiers, "range_type": range_types, }, ) diff --git a/cursorless-talon/src/primitive_target.py b/cursorless-talon/src/primitive_target.py index d7a769a46a..f3a55c81c7 100644 --- a/cursorless-talon/src/primitive_target.py +++ b/cursorless-talon/src/primitive_target.py @@ -8,21 +8,6 @@ IMPLICIT_TARGET = {"type": "primitive", "isImplicit": True} -modifiers = [ - "", # before, end of - "", # eg inside, bounds, just, head, tail, leading, trailing - "", # funk, state, class - "", # first past second word - "", # matching/pair [curly, round] -] - - -@mod.capture(rule="|".join(modifiers)) -def cursorless_modifier(m) -> str: - """Cursorless modifier""" - return m[0] - - @mod.capture( rule="+ [] | " ) diff --git a/src/processTargets/marks/LineNumberStage.ts b/src/processTargets/marks/LineNumberStage.ts index 955264936f..68a180b02c 100644 --- a/src/processTargets/marks/LineNumberStage.ts +++ b/src/processTargets/marks/LineNumberStage.ts @@ -22,7 +22,7 @@ export default class implements MarkStage { const activeRange = editor.document.lineAt(activeLine).range; const contentRange = anchorRange.union(activeRange); const isReversed = this.modifier.anchor < this.modifier.active; - return [createLineTarget(editor, contentRange, isReversed)]; + return [createLineTarget(editor, isReversed, contentRange)]; } } diff --git a/src/processTargets/modifiers/HeadTailStage.ts b/src/processTargets/modifiers/HeadTailStage.ts index 175eaceb64..1b246a436b 100644 --- a/src/processTargets/modifiers/HeadTailStage.ts +++ b/src/processTargets/modifiers/HeadTailStage.ts @@ -1,46 +1,69 @@ -import { Position, Range, TextEditor } from "vscode"; +import { Range, TextEditor } from "vscode"; import { Target } from "../../typings/target.types"; import { - HeadModifier, - TailModifier, + HeadTailModifier, + Modifier, } from "../../typings/targetDescriptor.types"; import { ProcessedTargetsContext } from "../../typings/Types"; import { ModifierStage } from "../PipelineStages.types"; -import TokenTarget from "../targets/TokenTarget"; +import { + getModifierStagesFromTargetModifiers, + processModifierStages, +} from "../processTargets"; +import { TokenTarget } from "../targets"; abstract class HeadTailStage implements ModifierStage { - abstract update(editor: TextEditor, range: Range): Range; - - constructor(private isReversed: boolean) {} + constructor(private isReversed: boolean, private modifiers?: Modifier[]) {} run(context: ProcessedTargetsContext, target: Target): Target[] { - const contentRange = this.update(target.editor, target.contentRange); - return [ - new TokenTarget({ + const modifiers = this.modifiers ?? [ + { + type: "containingScope", + scopeType: { type: "line" }, + }, + ]; + + const modifierStages = getModifierStagesFromTargetModifiers(modifiers); + const modifiedTargets = processModifierStages(context, modifierStages, [ + target, + ]); + + return modifiedTargets.map((modifiedTarget) => { + const contentRange = this.constructContentRange( + target.contentRange, + modifiedTarget.contentRange + ); + + return new TokenTarget({ editor: target.editor, isReversed: this.isReversed, contentRange, - }), - ]; + }); + }); } + + protected abstract constructContentRange( + originalRange: Range, + modifiedRange: Range + ): Range; } export class HeadStage extends HeadTailStage { - constructor(private modifier: HeadModifier) { - super(true); + constructor(modifier: HeadTailModifier) { + super(true, modifier.modifiers); } - update(editor: TextEditor, range: Range) { - return new Range(new Position(range.start.line, 0), range.end); + protected constructContentRange(originalRange: Range, modifiedRange: Range) { + return new Range(modifiedRange.start, originalRange.end); } } export class TailStage extends HeadTailStage { - constructor(private modifier: TailModifier) { - super(false); + constructor(modifier: HeadTailModifier) { + super(false, modifier.modifiers); } - update(editor: TextEditor, range: Range) { - return new Range(range.start, editor.document.lineAt(range.end).range.end); + protected constructContentRange(originalRange: Range, modifiedRange: Range) { + return new Range(originalRange.start, modifiedRange.end); } } diff --git a/src/processTargets/modifiers/scopeTypeStages/LineStage.ts b/src/processTargets/modifiers/scopeTypeStages/LineStage.ts index 2e8afefcfa..66f8389f68 100644 --- a/src/processTargets/modifiers/scopeTypeStages/LineStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/LineStage.ts @@ -18,7 +18,7 @@ export default class implements ModifierStage { return [toLineTarget(target)]; } - getEveryTarget(target: Target): LineTarget[] { + private getEveryTarget(target: Target): LineTarget[] { const { contentRange, editor } = target; const { isEmpty } = contentRange; const startLine = isEmpty ? 0 : contentRange.start.line; @@ -31,7 +31,7 @@ export default class implements ModifierStage { const line = editor.document.lineAt(i); if (!line.isEmptyOrWhitespace) { targets.push( - createLineTarget(target.editor, line.range, target.isReversed) + createLineTarget(target.editor, target.isReversed, line.range) ); } } @@ -46,18 +46,18 @@ export default class implements ModifierStage { } } -export function toLineTarget(target: Target): LineTarget { +function toLineTarget(target: Target): LineTarget { return createLineTarget( target.editor, - target.contentRange, - target.isReversed + target.isReversed, + target.contentRange ); } export function createLineTarget( editor: TextEditor, - range: Range, - isReversed: boolean + isReversed: boolean, + range: Range ) { return new LineTarget({ editor, diff --git a/src/processTargets/modifiers/scopeTypeStages/ParagraphStage.ts b/src/processTargets/modifiers/scopeTypeStages/ParagraphStage.ts index cac90e429a..2b6e2ede0b 100644 --- a/src/processTargets/modifiers/scopeTypeStages/ParagraphStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/ParagraphStage.ts @@ -19,7 +19,7 @@ export default class implements ModifierStage { return [this.getSingleTarget(target)]; } - getEveryTarget(target: Target): ParagraphTarget[] { + private getEveryTarget(target: Target): ParagraphTarget[] { const { contentRange, editor } = target; const { isEmpty } = contentRange; const { lineCount } = editor.document; @@ -71,15 +71,11 @@ export default class implements ModifierStage { return targets; } - getSingleTarget(target: Target): ParagraphTarget { - return this.getTargetFromRange(target); + private getSingleTarget(target: Target): ParagraphTarget { + return this.getTargetFromRange(target, calculateRange(target)); } - getTargetFromRange(target: Target, range?: Range): ParagraphTarget { - if (range == null) { - range = calculateRange(target); - } - + private getTargetFromRange(target: Target, range: Range): ParagraphTarget { return new ParagraphTarget({ editor: target.editor, isReversed: target.isReversed, diff --git a/src/processTargets/modifiers/scopeTypeStages/RegexStage.ts b/src/processTargets/modifiers/scopeTypeStages/RegexStage.ts index a0b7bf86cd..301c45c927 100644 --- a/src/processTargets/modifiers/scopeTypeStages/RegexStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/RegexStage.ts @@ -24,7 +24,7 @@ class RegexStage implements ModifierStage { return [this.getSingleTarget(target)]; } - getEveryTarget(target: Target): ScopeTypeTarget[] { + private getEveryTarget(target: Target): ScopeTypeTarget[] { const { contentRange, editor } = target; const { isEmpty } = contentRange; const start = isEmpty @@ -55,7 +55,7 @@ class RegexStage implements ModifierStage { return targets; } - getSingleTarget(target: Target): ScopeTypeTarget { + private getSingleTarget(target: Target): ScopeTypeTarget { const { editor } = target; const start = this.getMatchForPos(editor, target.contentRange.start).start; const end = this.getMatchForPos(editor, target.contentRange.end).end; @@ -63,16 +63,19 @@ class RegexStage implements ModifierStage { return this.getTargetFromRange(target, contentRange); } - getTargetFromRange(target: Target, range: Range): ScopeTypeTarget { + private getTargetFromRange( + target: Target, + contentRange: Range + ): ScopeTypeTarget { return new ScopeTypeTarget({ scopeTypeType: this.modifier.scopeType.type, editor: target.editor, isReversed: target.isReversed, - contentRange: range, + contentRange, }); } - getMatchForPos(editor: TextEditor, position: Position) { + private getMatchForPos(editor: TextEditor, position: Position) { const match = this.getMatchesForLine(editor, position.line).find((range) => range.contains(position) ); @@ -86,7 +89,7 @@ class RegexStage implements ModifierStage { return match; } - getMatchesForLine(editor: TextEditor, lineNum: number) { + private getMatchesForLine(editor: TextEditor, lineNum: number) { const line = editor.document.lineAt(lineNum); const result = [...line.text.matchAll(this.regex)].map( (match) => diff --git a/src/processTargets/modifiers/scopeTypeStages/TokenStage.ts b/src/processTargets/modifiers/scopeTypeStages/TokenStage.ts index be7f943683..bcd15861f2 100644 --- a/src/processTargets/modifiers/scopeTypeStages/TokenStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/TokenStage.ts @@ -19,7 +19,7 @@ export default class implements ModifierStage { return [this.getSingleTarget(target)]; } - getEveryTarget( + private getEveryTarget( context: ProcessedTargetsContext, target: Target ): TokenTarget[] { @@ -46,11 +46,11 @@ export default class implements ModifierStage { return targets; } - getSingleTarget(target: Target): TokenTarget { + private getSingleTarget(target: Target): TokenTarget { return this.getTargetFromRange(target, target.contentRange); } - getTargetFromRange(target: Target, range: Range): TokenTarget { + private getTargetFromRange(target: Target, range: Range): TokenTarget { const contentRange = getTokenRangeForSelection(target.editor, range); return new TokenTarget({ editor: target.editor, diff --git a/src/processTargets/processTargets.ts b/src/processTargets/processTargets.ts index 922bd7371d..24bdeb0264 100644 --- a/src/processTargets/processTargets.ts +++ b/src/processTargets/processTargets.ts @@ -2,6 +2,7 @@ import { uniqWith, zip } from "lodash"; import { Range } from "vscode"; import { Target } from "../typings/target.types"; import { + Modifier, PrimitiveTargetDescriptor, RangeTargetDescriptor, TargetDescriptor, @@ -10,6 +11,7 @@ import { ProcessedTargetsContext } from "../typings/Types"; import { ensureSingleEditor } from "../util/targetUtils"; import getMarkStage from "./getMarkStage"; import getModifierStage from "./getModifierStage"; +import { ModifierStage } from "./PipelineStages.types"; import PlainTarget from "./targets/PlainTarget"; import PositionTarget from "./targets/PositionTarget"; @@ -193,33 +195,40 @@ function processPrimitiveTarget( const markStage = getMarkStage(targetDescriptor.mark); const markOutputTargets = markStage.run(context); - /** - * The modifier pipeline that will be applied to construct our final targets - */ + /** The modifier pipeline that will be applied to construct our final targets */ const modifierStages = [ - // Reverse target modifiers because they are returned in reverse order from - // the api, to match the order in which they are spoken. - ...targetDescriptor.modifiers.map(getModifierStage).reverse(), + ...getModifierStagesFromTargetModifiers(targetDescriptor.modifiers), ...context.finalStages, ]; - /** - * Intermediate variable to store the output of the current pipeline stage. - * We initialise it to start with the outputs from the mark. - */ - let currentTargets = markOutputTargets; + // Run all targets through the modifier stages + return processModifierStages(context, modifierStages, markOutputTargets); +} + +/** Convert a list of target modifiers to modifier stages */ +export function getModifierStagesFromTargetModifiers( + targetModifiers: Modifier[] +) { + // Reverse target modifiers because they are returned in reverse order from + // the api, to match the order in which they are spoken. + return targetModifiers.map(getModifierStage).reverse(); +} - // Then we apply each stage in sequence, letting each stage see the targets +/** Run all targets through the modifier stages */ +export function processModifierStages( + context: ProcessedTargetsContext, + modifierStages: ModifierStage[], + targets: Target[] +) { + // First we apply each stage in sequence, letting each stage see the targets // one-by-one and concatenating the results before passing them on to the // next stage. modifierStages.forEach((stage) => { - currentTargets = currentTargets.flatMap((target) => - stage.run(context, target) - ); + targets = targets.flatMap((target) => stage.run(context, target)); }); // Then return the output from the final stage - return currentTargets; + return targets; } function calcIsReversed(anchor: Target, active: Target) { diff --git a/src/test/suite/fixtures/recorded/headTail/chuckHeadAir.yml b/src/test/suite/fixtures/recorded/headTail/chuckHeadAir.yml new file mode 100644 index 0000000000..65db463cdc --- /dev/null +++ b/src/test/suite/fixtures/recorded/headTail/chuckHeadAir.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: chuck head air + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: a} + modifiers: + - {type: extendThroughStartOf} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: " hello world whatever" + selections: + - anchor: {line: 0, character: 24} + active: {line: 0, character: 24} + marks: + default.a: + start: {line: 0, character: 16} + end: {line: 0, character: 24} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: a}, modifiers: [{type: extendThroughStartOf}]}] diff --git a/src/test/suite/fixtures/recorded/headTail/chuckHeadWhale.yml b/src/test/suite/fixtures/recorded/headTail/chuckHeadWhale.yml new file mode 100644 index 0000000000..eebc2a927e --- /dev/null +++ b/src/test/suite/fixtures/recorded/headTail/chuckHeadWhale.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: chuck head whale + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: extendThroughStartOf} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: " hello world whatever" + selections: + - anchor: {line: 0, character: 24} + active: {line: 0, character: 24} + marks: + default.w: + start: {line: 0, character: 10} + end: {line: 0, character: 15} +finalState: + documentContents: " whatever" + selections: + - anchor: {line: 0, character: 12} + active: {line: 0, character: 12} + thatMark: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: extendThroughStartOf}]}] diff --git a/src/test/suite/fixtures/recorded/headTail/chuckTailHarp.yml b/src/test/suite/fixtures/recorded/headTail/chuckTailHarp.yml new file mode 100644 index 0000000000..abadc44913 --- /dev/null +++ b/src/test/suite/fixtures/recorded/headTail/chuckTailHarp.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: chuck tail harp + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: h} + modifiers: + - {type: extendThroughEndOf} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: " hello world whatever" + selections: + - anchor: {line: 0, character: 24} + active: {line: 0, character: 24} + marks: + default.h: + start: {line: 0, character: 4} + end: {line: 0, character: 9} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: h}, modifiers: [{type: extendThroughEndOf}]}] diff --git a/src/test/suite/fixtures/recorded/headTail/chuckTailWhale.yml b/src/test/suite/fixtures/recorded/headTail/chuckTailWhale.yml new file mode 100644 index 0000000000..8810148298 --- /dev/null +++ b/src/test/suite/fixtures/recorded/headTail/chuckTailWhale.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: chuck tail whale + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + modifiers: + - {type: extendThroughEndOf} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: " hello world whatever" + selections: + - anchor: {line: 0, character: 24} + active: {line: 0, character: 24} + marks: + default.w: + start: {line: 0, character: 10} + end: {line: 0, character: 15} +finalState: + documentContents: " hello" + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} + thatMark: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: extendThroughEndOf}]}] diff --git a/src/test/suite/fixtures/recorded/headTail/clearHeadAir.yml b/src/test/suite/fixtures/recorded/headTail/clearHeadAir.yml new file mode 100644 index 0000000000..f5983e014f --- /dev/null +++ b/src/test/suite/fixtures/recorded/headTail/clearHeadAir.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: clear head air + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: a} + modifiers: + - {type: extendThroughStartOf} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: " hello world whatever" + selections: + - anchor: {line: 0, character: 24} + active: {line: 0, character: 24} + marks: + default.a: + start: {line: 0, character: 16} + end: {line: 0, character: 24} +finalState: + documentContents: " " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + thatMark: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: a}, modifiers: [{type: extendThroughStartOf}]}] diff --git a/src/test/suite/fixtures/recorded/headTail/clearTailHarp.yml b/src/test/suite/fixtures/recorded/headTail/clearTailHarp.yml new file mode 100644 index 0000000000..f4b9b0c5eb --- /dev/null +++ b/src/test/suite/fixtures/recorded/headTail/clearTailHarp.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + spokenForm: clear tail harp + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: h} + modifiers: + - {type: extendThroughEndOf} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: " hello world whatever" + selections: + - anchor: {line: 0, character: 24} + active: {line: 0, character: 24} + marks: + default.h: + start: {line: 0, character: 4} + end: {line: 0, character: 9} +finalState: + documentContents: " " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + thatMark: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: h}, modifiers: [{type: extendThroughEndOf}]}] diff --git a/src/test/suite/fixtures/recorded/languages/php/changeClass.yml b/src/test/suite/fixtures/recorded/languages/php/changeClass.yml index f52f64ba74..d9b8d3f42b 100644 --- a/src/test/suite/fixtures/recorded/languages/php/changeClass.yml +++ b/src/test/suite/fixtures/recorded/languages/php/changeClass.yml @@ -19,7 +19,9 @@ initialState: active: {line: 4, character: 0} marks: {} finalState: - documentContents: "