Skip to content

Add "branch" scope type #1149

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jan 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cursorless-talon/src/modifiers/scopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"selector": "selector",
"state": "statement",
"string": "string",
"branch": "branch",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: any reason this is listed in this order in particular?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really. I should prob file a follow-up to alphabetise; didn't want to mess with the diff

"type": "type",
"value": "value",
"condition": "condition",
Expand Down
1 change: 1 addition & 0 deletions docs/user/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ For programming languages where Cursorless has rich parse tree support, we suppo
| -------------- | --------------------------------------------------- |
| `"arg"` | function parameter or function call argument |
| `"attribute"` | attribute, eg on html element |
| `"branch"` | branch of a `switch` or `if` statement |
| `"call"` | function call, eg `foo(1, 2)` |
| `"callee"` | the function being called in a function call |
| `"class name"` | the name in a class declaration |
Expand Down
1 change: 1 addition & 0 deletions schemas/cursorless-snippets.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"argumentOrParameter",
"anonymousFunction",
"attribute",
"branch",
"class",
"className",
"collectionItem",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export type SimpleScopeTypeType =
| "argumentOrParameter"
| "anonymousFunction"
| "attribute"
| "branch"
| "class"
| "className"
| "collectionItem"
Expand Down
33 changes: 33 additions & 0 deletions src/languages/branchMatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { NodeMatcher } from "../typings/Types";
import { patternFinder } from "../util/nodeFinders";
import {
cascadingMatcher,
matcher,
patternMatcher,
} from "../util/nodeMatchers";
import { childRangeSelector } from "../util/nodeSelectors";

/**
* Constructs a branch matcher for constructs that have a primary branch
* followed by zero or more optional branches, such as `if` statements or `try`
* statements
* @param statementType The top-level statement type for this construct, eg
* "if_statement" or "try_statement"
* @param optionalBranchTypes The optional branch type names that can be
* children of the top-level statement, eg "else_clause" or "except_clause"
* @returns A node matcher capabale of matching this type of branch
*/
export function branchMatcher(
statementType: string,
optionalBranchTypes: string[],
): NodeMatcher {
return cascadingMatcher(
patternMatcher(...optionalBranchTypes),
matcher(
patternFinder(statementType),
childRangeSelector(optionalBranchTypes, [], {
includeUnnamedChildren: true,
}),
),
);
}
93 changes: 93 additions & 0 deletions src/languages/elseIfExtractor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Selection, TextEditor } from "@cursorless/common";
import { SyntaxNode } from "web-tree-sitter";
import { SelectionExtractor, SelectionWithContext } from "../typings/Types";
import {
childRangeSelector,
positionFromPoint,
simpleSelectionExtractor,
} from "../util/nodeSelectors";

/**
* Returns an extractor that can be used to extract `else if` branches in languages
* with C-like structure, where the `if` portion of an `else if` structure is
* structurally just an arbitrary statement that happens to be an `if`
* statement.
* @returns An extractor that will exctract `else if` branches
*/
export function elseIfExtractor(): SelectionExtractor {
/**
* This extractor pulls out `if (foo) {}` from `if (foo) {} else {}`, ie it
* excludes any child `else` statement if it exists. It will be used as the
* content range, but the removal range will want to include a leading or
* trailing `else` keyword if one exists.
*/
const contentRangeExtractor = childRangeSelector(["else_clause"], [], {
includeUnnamedChildren: true,
});

return function (editor: TextEditor, node: SyntaxNode): SelectionWithContext {
const contentRange = contentRangeExtractor(editor, node);

const parent = node.parent;
if (parent?.type !== "else_clause") {
// We have no leading `else` clause; ie we are a standalone `if`
// statement. We may still have our own `else` child, but we are not
// ourselves a branch of a bigger `if` statement.
const alternative = node.childForFieldName("alternative");

if (alternative == null) {
// If we have no nested else clause, and are not part of an else clause
// ourself, then we don't need to remove any leading / trailing `else`
// keyword
return contentRange;
}

// Otherwise, we have no leading `else`, but we do have our own nested
// `else` clause, so we want to remove its `else` keyword
const { selection } = contentRange;
return {
selection,
context: {
removalRange: new Selection(
selection.start,
positionFromPoint(alternative.namedChild(0)!.startPosition),
),
},
};
}

// If we get here, we are part of a bigger `if` statement; extend our
// removal range past our leading `else` keyword.
const { selection } = contentRange;
return {
selection,
context: {
removalRange: new Selection(
positionFromPoint(parent.child(0)!.startPosition),
selection.end,
),
},
};
};
}

/**
* Returns an extractor that can be used to extract `else` branches in languages
* with C-like structure, where the `if` portion of an `else if` structure is
* structurally just an arbitrary statement that happens to be an `if`
* statement.
* @param ifNodeType The node type for `if` statements
* @returns An extractor that will exctract `else` branches
*/
export function elseExtractor(ifNodeType: string): SelectionExtractor {
const nestedElseIfExtractor = elseIfExtractor();

return function (editor: TextEditor, node: SyntaxNode): SelectionWithContext {
// If we are an `else if` statement, then we just run `elseIfExtractor` on
// our nested `if` node. Otherwise we are a simple `else` branch and don't
// need to do anything fancy.
return node.namedChild(0)!.type === ifNodeType
? nestedElseIfExtractor(editor, node.namedChild(0)!)
: simpleSelectionExtractor(editor, node);
};
}
1 change: 1 addition & 0 deletions src/languages/java.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ const nodeMatchers: Partial<
),
condition: conditionMatcher("*[condition]"),
argumentOrParameter: argumentMatcher("formal_parameters", "argument_list"),
branch: ["switch_block_statement_group", "switch_rule"],
switchStatementSubject: "switch_expression[condition][0]",
};

Expand Down
15 changes: 15 additions & 0 deletions src/languages/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
argumentSelectionExtractor,
childRangeSelector,
} from "../util/nodeSelectors";
import { branchMatcher } from "./branchMatcher";
import { ternaryBranchMatcher } from "./ternaryBranchMatcher";

// Generated by the following command:
// > curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-python/d6210ceab11e8d812d4ab59c07c81458ec6e5184/src/node-types.json \
Expand Down Expand Up @@ -145,6 +147,19 @@ const nodeMatchers: Partial<
argumentMatcher("parameters", "argument_list"),
matcher(patternFinder("call.generator_expression!"), childRangeSelector()),
),
branch: cascadingMatcher(
patternMatcher("case_clause"),
branchMatcher("if_statement", ["else_clause", "elif_clause"]),
branchMatcher("while_statement", ["else_clause"]),
branchMatcher("for_statement", ["else_clause"]),
branchMatcher("try_statement", [
"except_clause",
"finally_clause",
"else_clause",
"except_group_clause",
]),
ternaryBranchMatcher("conditional_expression", [0, 2]),
),
switchStatementSubject: "match_statement[subject]",
};

Expand Down
6 changes: 6 additions & 0 deletions src/languages/rust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
makeNodePairSelection,
makeRangeFromPositions,
} from "../util/nodeSelectors";
import { elseExtractor, elseIfExtractor } from "./elseIfExtractor";

// Generated by the following command:
// `curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-rust/36ae187ed6dd3803a8a89dbb54f3124c8ee74662/src/node-types.STATEMENT_TYPES | jq '[.[] | select(.type == "_declaration_statement") | .subtypes[].type, "expression_statement"]'`
Expand Down Expand Up @@ -235,6 +236,11 @@ const nodeMatchers: Partial<
matcher(returnValueFinder),
),
attribute: trailingMatcher(["mutable_specifier", "attribute_item"]),
branch: cascadingMatcher(
patternMatcher("match_arm"),
matcher(patternFinder("else_clause"), elseExtractor("if_expression")),
matcher(patternFinder("if_expression"), elseIfExtractor()),
),
switchStatementSubject: "match_expression[value]",
};

Expand Down
17 changes: 13 additions & 4 deletions src/languages/scala.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { SimpleScopeTypeType } from "../core/commandRunner/typings/PartialTargetDescriptor.types";
import { NodeMatcherAlternative } from "../typings/Types";
import { patternFinder } from "../util/nodeFinders";
import {
createPatternMatchers,
argumentMatcher,
leadingMatcher,
conditionMatcher,
createPatternMatchers,
leadingMatcher,
matcher,
} from "../util/nodeMatchers";
import { NodeMatcherAlternative } from "../typings/Types";
import { SimpleScopeTypeType } from "../core/commandRunner/typings/PartialTargetDescriptor.types";
import { childRangeSelector } from "../util/nodeSelectors";

const nodeMatchers: Partial<
Record<SimpleScopeTypeType, NodeMatcherAlternative>
Expand Down Expand Up @@ -34,6 +37,12 @@ const nodeMatchers: Partial<
"class_parameters",
"bindings",
),
branch: matcher(
patternFinder("case_clause"),
childRangeSelector([], [], {
includeUnnamedChildren: true,
}),
),

switchStatementSubject: "match_expression[value]",
name: ["*[name]", "*[pattern]"],
Expand Down
40 changes: 40 additions & 0 deletions src/languages/ternaryBranchMatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { SyntaxNode } from "web-tree-sitter";
import { NodeMatcher } from "../typings/Types";
import { matcher } from "../util/nodeMatchers";

/**
* Constructs a matcher for matching ternary branches. Branches are expected to
* be named children at particular indices of a ternary node.
*
* NB: We can't just use the "foo[0]" syntax of our pattern language because
* that just uses `foo` for the finder; the `[0]` is just part of the extractor,
* so if we had `foo[0]` and `foo[1]`, they would both match for either branch.
* @param ternaryTypename The type name for ternary expressions
* @param acceptableNamedChildIndices Which named children, by index, of the
* ternary node correspond to branches
* @returns A matcher that can match ternary branches
*/
export function ternaryBranchMatcher(
ternaryTypename: string,
acceptableNamedChildIndices: number[],
): NodeMatcher {
function finder(node: SyntaxNode) {
const parent = node.parent;
if (parent == null) {
return null;
}

if (
parent.type === ternaryTypename &&
acceptableNamedChildIndices.some((index) =>
parent.namedChild(index)!.equals(node),
)
) {
return node;
}

return null;
}

return matcher(finder);
}
10 changes: 10 additions & 0 deletions src/languages/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import {
unwrapSelectionExtractor,
xmlElementExtractor,
} from "../util/nodeSelectors";
import { branchMatcher } from "./branchMatcher";
import { elseExtractor, elseIfExtractor } from "./elseIfExtractor";
import { ternaryBranchMatcher } from "./ternaryBranchMatcher";

// Generated by the following command:
// > curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-typescript/4c20b54771e4b390ee058af2930feb2cd55f2bf8/typescript/src/node-types.json \
Expand Down Expand Up @@ -228,6 +231,13 @@ const nodeMatchers: Partial<
patternFinder("switch_statement[value]"),
unwrapSelectionExtractor,
),
branch: cascadingMatcher(
patternMatcher("switch_case"),
matcher(patternFinder("else_clause"), elseExtractor("if_statement")),
matcher(patternFinder("if_statement"), elseIfExtractor()),
branchMatcher("try_statement", ["catch_clause", "finally_clause"]),
ternaryBranchMatcher("ternary_expression", [1, 2]),
),
class: [
"export_statement?.class_declaration", // export class | class
"export_statement?.abstract_class_declaration", // export abstract class | abstract class
Expand Down
1 change: 1 addition & 0 deletions src/processTargets/targets/ScopeTypeTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ function getDelimiter(scopeType: SimpleScopeTypeType): string {
case "comment":
case "xmlElement":
case "collectionItem":
case "branch":
return "\n";

default:
Expand Down
42 changes: 42 additions & 0 deletions src/test/suite/fixtures/recorded/languages/java/clearBranch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
languageId: java
command:
spokenForm: clear branch
version: 3
targets:
- type: primitive
modifiers:
- type: containingScope
scopeType: {type: branch}
usePrePhraseSnapshot: true
action: {name: clearAndSetSelection}
initialState:
documentContents: |-
class Aaa {
static void bbb() {
switch ("0") {
case ("0"):
break;
case ("1"):
break;
}
}
}
selections:
- anchor: {line: 4, character: 12}
active: {line: 4, character: 12}
marks: {}
finalState:
documentContents: |-
class Aaa {
static void bbb() {
switch ("0") {

case ("1"):
break;
}
}
}
selections:
- anchor: {line: 3, character: 6}
active: {line: 3, character: 6}
fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: branch}}]}]
Loading