diff --git a/src/languages/python.ts b/src/languages/python.ts index a15cac22e4..f628909b1d 100644 --- a/src/languages/python.ts +++ b/src/languages/python.ts @@ -99,7 +99,15 @@ const nodeMatchers: Partial< className: "class_definition[name]", namedFunction: "decorated_definition?.function_definition", functionName: "function_definition[name]", - condition: conditionMatcher("*[condition]"), + condition: cascadingMatcher( + conditionMatcher("*[condition]"), + + // Comprehensions and match statements + leadingMatcher(["*.if_clause![0]"], ["if"]), + + // Ternaries + patternMatcher("conditional_expression[1]"), + ), type: leadingMatcher( ["function_definition[return_type]", "*[type]"], [":", "->"], diff --git a/src/languages/rust.ts b/src/languages/rust.ts index d0ec461458..196abc7cd0 100644 --- a/src/languages/rust.ts +++ b/src/languages/rust.ts @@ -14,6 +14,7 @@ import { trailingMatcher, } from "../util/nodeMatchers"; import { + childRangeSelector, makeNodePairSelection, makeRangeFromPositions, } from "../util/nodeSelectors"; @@ -146,6 +147,16 @@ const nodeMatchers: Partial< ), string: ["raw_string_literal", "string_literal"], ifStatement: ["if_expression", "if_let_expression"], + condition: cascadingMatcher( + patternMatcher("while_expression[condition]", "if_expression[condition]"), + matcher( + patternFinder("while_let_expression", "if_let_expression"), + childRangeSelector(["while", "if", "block"], [], { + includeUnnamedChildren: true, + }), + ), + leadingMatcher(["*.match_pattern![condition]"], ["if"]), + ), functionCall: ["call_expression", "macro_invocation", "struct_expression"], functionCallee: "call_expression[function]", comment: ["line_comment", "block_comment"], diff --git a/src/test/suite/fixtures/recorded/languages/python/clearCondition.yml b/src/test/suite/fixtures/recorded/languages/python/clearCondition.yml new file mode 100644 index 0000000000..9417d30da9 --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/python/clearCondition.yml @@ -0,0 +1,23 @@ +languageId: python +command: + spokenForm: clear condition + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa if bbb else ccc + selections: + - anchor: {line: 0, character: 16} + active: {line: 0, character: 16} + marks: {} +finalState: + documentContents: aaa if else ccc + selections: + - anchor: {line: 0, character: 7} + active: {line: 0, character: 7} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: condition}}]}] diff --git a/src/test/suite/fixtures/recorded/languages/python/clearCondition2.yml b/src/test/suite/fixtures/recorded/languages/python/clearCondition2.yml new file mode 100644 index 0000000000..ba35ec50ec --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/python/clearCondition2.yml @@ -0,0 +1,23 @@ +languageId: python +command: + spokenForm: clear condition + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: "[aaa for aaa in bbb if ccc]" + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + marks: {} +finalState: + documentContents: "[aaa for aaa in bbb if ]" + selections: + - anchor: {line: 0, character: 23} + active: {line: 0, character: 23} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: condition}}]}] diff --git a/src/test/suite/fixtures/recorded/languages/python/clearCondition3.yml b/src/test/suite/fixtures/recorded/languages/python/clearCondition3.yml new file mode 100644 index 0000000000..92ce49e57e --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/python/clearCondition3.yml @@ -0,0 +1,23 @@ +languageId: python +command: + spokenForm: clear condition + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: (aaa for aaa in bbb if ccc) + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + marks: {} +finalState: + documentContents: (aaa for aaa in bbb if ) + selections: + - anchor: {line: 0, character: 23} + active: {line: 0, character: 23} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: condition}}]}] diff --git a/src/test/suite/fixtures/recorded/languages/python/clearCondition4.yml b/src/test/suite/fixtures/recorded/languages/python/clearCondition4.yml new file mode 100644 index 0000000000..e9f6a3d2b1 --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/python/clearCondition4.yml @@ -0,0 +1,23 @@ +languageId: python +command: + spokenForm: clear condition + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: false + action: {name: clearAndSetSelection} +initialState: + documentContents: "{aaa for aaa in bbb if ccc}" + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + marks: {} +finalState: + documentContents: "{aaa for aaa in bbb if }" + selections: + - anchor: {line: 0, character: 23} + active: {line: 0, character: 23} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: condition}}]}] diff --git a/src/test/suite/fixtures/recorded/languages/python/clearCondition5.yml b/src/test/suite/fixtures/recorded/languages/python/clearCondition5.yml new file mode 100644 index 0000000000..40610d0fa2 --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/python/clearCondition5.yml @@ -0,0 +1,23 @@ +languageId: python +command: + spokenForm: clear condition + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: false + action: {name: clearAndSetSelection} +initialState: + documentContents: "{aaa: aaa for aaa in bbb if ccc}" + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + marks: {} +finalState: + documentContents: "{aaa: aaa for aaa in bbb if }" + selections: + - anchor: {line: 0, character: 28} + active: {line: 0, character: 28} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: condition}}]}] diff --git a/src/test/suite/fixtures/recorded/languages/python/clearCondition6.yml b/src/test/suite/fixtures/recorded/languages/python/clearCondition6.yml new file mode 100644 index 0000000000..63a174ed84 --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/python/clearCondition6.yml @@ -0,0 +1,29 @@ +languageId: python +command: + spokenForm: clear condition + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + match 0: + case a if a > 1: + pass + selections: + - anchor: {line: 2, character: 8} + active: {line: 2, character: 8} + marks: {} +finalState: + documentContents: |- + match 0: + case a if : + pass + selections: + - anchor: {line: 1, character: 14} + active: {line: 1, character: 14} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: condition}}]}] diff --git a/src/test/suite/fixtures/recorded/languages/python/ditchCondition.yml b/src/test/suite/fixtures/recorded/languages/python/ditchCondition.yml new file mode 100644 index 0000000000..5a786d6c2b --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/python/ditchCondition.yml @@ -0,0 +1,23 @@ +languageId: python +command: + spokenForm: ditch condition + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: "[aaa for aaa in bbb if ccc]" + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + marks: {} +finalState: + documentContents: "[aaa for aaa in bbb ]" + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: condition}}]}] diff --git a/src/test/suite/fixtures/recorded/languages/python/ditchCondition2.yml b/src/test/suite/fixtures/recorded/languages/python/ditchCondition2.yml new file mode 100644 index 0000000000..1be74d6e85 --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/python/ditchCondition2.yml @@ -0,0 +1,29 @@ +languageId: python +command: + spokenForm: ditch condition + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: |- + match 0: + case a if a > 1: + pass + selections: + - anchor: {line: 2, character: 8} + active: {line: 2, character: 8} + marks: {} +finalState: + documentContents: |- + match 0: + case a : + pass + selections: + - anchor: {line: 2, character: 8} + active: {line: 2, character: 8} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: condition}}]}] diff --git a/src/test/suite/fixtures/recorded/languages/rust/chuckCondition.yml b/src/test/suite/fixtures/recorded/languages/rust/chuckCondition.yml new file mode 100644 index 0000000000..c69ebf5785 --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/rust/chuckCondition.yml @@ -0,0 +1,31 @@ +languageId: rust +command: + spokenForm: chuck condition + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: | + match user { + User { first_name: "John" } => {}, + User { first_name } if first_name.starts_with("P") => {} + } + selections: + - anchor: {line: 2, character: 27} + active: {line: 2, character: 54} + marks: {} +finalState: + documentContents: | + match user { + User { first_name: "John" } => {}, + User { first_name } => {} + } + selections: + - anchor: {line: 2, character: 23} + active: {line: 2, character: 23} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: ifStatement}}]}] diff --git a/src/test/suite/fixtures/recorded/languages/rust/clearCondition.yml b/src/test/suite/fixtures/recorded/languages/rust/clearCondition.yml new file mode 100644 index 0000000000..03920d31c5 --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/rust/clearCondition.yml @@ -0,0 +1,31 @@ +languageId: rust +command: + spokenForm: clear condition + version: 3 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + match user { + User { first_name: "John" } => {}, + User { first_name } if first_name.starts_with("P") => {} + } + selections: + - anchor: {line: 2, character: 57} + active: {line: 2, character: 57} + marks: {} +finalState: + documentContents: |- + match user { + User { first_name: "John" } => {}, + User { first_name } if => {} + } + selections: + - anchor: {line: 2, character: 25} + active: {line: 2, character: 25} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: condition}}]}] diff --git a/src/test/suite/fixtures/recorded/languages/rust/clearCondition2.yml b/src/test/suite/fixtures/recorded/languages/rust/clearCondition2.yml new file mode 100644 index 0000000000..160336eaba --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/rust/clearCondition2.yml @@ -0,0 +1,33 @@ +languageId: rust +command: + version: 3 + spokenForm: clear condition + action: {name: clearAndSetSelection} + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + if n < 0 { + print!("{} is negative", n); + } else if n > 0 { + print!("{} is positive", n); + } + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: |- + if { + print!("{} is negative", n); + } else if n > 0 { + print!("{} is positive", n); + } + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: condition}}]}] diff --git a/src/test/suite/fixtures/recorded/languages/rust/clearCondition3.yml b/src/test/suite/fixtures/recorded/languages/rust/clearCondition3.yml new file mode 100644 index 0000000000..1f48ca4e1b --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/rust/clearCondition3.yml @@ -0,0 +1,33 @@ +languageId: rust +command: + version: 3 + spokenForm: clear condition + action: {name: clearAndSetSelection} + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: false +initialState: + documentContents: |- + if n < 0 { + print!("{} is negative", n); + } else if n > 0 { + print!("{} is positive", n); + } + selections: + - anchor: {line: 3, character: 2} + active: {line: 3, character: 2} + marks: {} +finalState: + documentContents: |- + if n < 0 { + print!("{} is negative", n); + } else if { + print!("{} is positive", n); + } + selections: + - anchor: {line: 2, character: 10} + active: {line: 2, character: 10} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: condition}}]}] diff --git a/src/test/suite/fixtures/recorded/languages/rust/clearCondition4.yml b/src/test/suite/fixtures/recorded/languages/rust/clearCondition4.yml new file mode 100644 index 0000000000..dda63f755b --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/rust/clearCondition4.yml @@ -0,0 +1,33 @@ +languageId: rust +command: + version: 3 + spokenForm: clear condition + action: {name: clearAndSetSelection} + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: false +initialState: + documentContents: |- + let foo = if n < 0 { + print!("{} is negative", n); + } else if n > 0 { + print!("{} is positive", n); + } + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: |- + let foo = if { + print!("{} is negative", n); + } else if n > 0 { + print!("{} is positive", n); + } + selections: + - anchor: {line: 0, character: 13} + active: {line: 0, character: 13} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: condition}}]}] diff --git a/src/test/suite/fixtures/recorded/languages/rust/clearCondition5.yml b/src/test/suite/fixtures/recorded/languages/rust/clearCondition5.yml new file mode 100644 index 0000000000..16c09c26e8 --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/rust/clearCondition5.yml @@ -0,0 +1,33 @@ +languageId: rust +command: + version: 3 + spokenForm: clear condition + action: {name: clearAndSetSelection} + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: false +initialState: + documentContents: |- + let foo = if n < 0 { + print!("{} is negative", n); + } else if n > 0 { + print!("{} is positive", n); + } + selections: + - anchor: {line: 3, character: 2} + active: {line: 3, character: 2} + marks: {} +finalState: + documentContents: |- + let foo = if n < 0 { + print!("{} is negative", n); + } else if { + print!("{} is positive", n); + } + selections: + - anchor: {line: 2, character: 10} + active: {line: 2, character: 10} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: condition}}]}] diff --git a/src/test/suite/fixtures/recorded/languages/rust/clearCondition6.yml b/src/test/suite/fixtures/recorded/languages/rust/clearCondition6.yml new file mode 100644 index 0000000000..9f95f7b348 --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/rust/clearCondition6.yml @@ -0,0 +1,29 @@ +languageId: rust +command: + version: 3 + spokenForm: clear condition + action: {name: clearAndSetSelection} + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + if let Some(i) = number { + println!("Matched {:?}!", i); + } + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: |- + if { + println!("Matched {:?}!", i); + } + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: condition}}]}] diff --git a/src/test/suite/fixtures/recorded/languages/rust/clearCondition7.yml b/src/test/suite/fixtures/recorded/languages/rust/clearCondition7.yml new file mode 100644 index 0000000000..dc3afe9426 --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/rust/clearCondition7.yml @@ -0,0 +1,29 @@ +languageId: rust +command: + version: 3 + spokenForm: clear condition + action: {name: clearAndSetSelection} + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + while let Some(i) = number { + println!("Matched {:?}!", i); + } + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: |- + while { + println!("Matched {:?}!", i); + } + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: condition}}]}] diff --git a/src/test/suite/fixtures/recorded/languages/rust/clearCondition8.yml b/src/test/suite/fixtures/recorded/languages/rust/clearCondition8.yml new file mode 100644 index 0000000000..375c2e9b41 --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/rust/clearCondition8.yml @@ -0,0 +1,29 @@ +languageId: rust +command: + version: 3 + spokenForm: clear condition + action: {name: clearAndSetSelection} + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: condition} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + while n < 101 { + n += 1; + } + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: |- + while { + n += 1; + } + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: condition}}]}] diff --git a/src/util/nodeSelectors.ts b/src/util/nodeSelectors.ts index 45ee5c9c1a..a97b40f4c1 100644 --- a/src/util/nodeSelectors.ts +++ b/src/util/nodeSelectors.ts @@ -216,6 +216,14 @@ export function selectWithLeadingDelimiter(...delimiters: string[]) { }; } +interface ChildRangeSelectorOptions { + /** + * If `true`, include unnamed children. Otherwise, only named children will + * be used to construct the child range. Defaults to `false` + */ + includeUnnamedChildren?: boolean; +} + /** * Creates an extractor that returns a contiguous range between children of a node. * When no arguments are passed, the function will return a range from the first to the last child node. Pass in either inclusions @@ -227,12 +235,13 @@ export function selectWithLeadingDelimiter(...delimiters: string[]) { export function childRangeSelector( typesToExclude: string[] = [], typesToInclude: string[] = [], + { includeUnnamedChildren = false }: ChildRangeSelectorOptions = {}, ) { return function (editor: TextEditor, node: SyntaxNode): SelectionWithContext { if (typesToExclude.length > 0 && typesToInclude.length > 0) { throw new Error("Cannot have both exclusions and inclusions."); } - let nodes = node.namedChildren; + let nodes = includeUnnamedChildren ? node.children : node.namedChildren; const exclusionSet = new Set(typesToExclude); const inclusionSet = new Set(typesToInclude); nodes = nodes.filter((child) => {