Added support for child indexes in the pattern finder (#395)

* Added support for child indexes in the pattern finder

* Updated pattern finder to correctly do descending search with optional parents

* Clear up any type and not type in pattern finder

* Updated go to use new index feature in pattern finder
This commit is contained in:
Andreas Arvidsson 2022-01-17 15:17:04 +01:00 committed by GitHub
parent 6f472846fe
commit d0d41d9a99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 222 additions and 88 deletions

View File

@ -1,20 +1,11 @@
import {
createPatternMatchers,
argumentMatcher,
leadingMatcher,
conditionMatcher,
trailingMatcher,
childAtIndexMatcher,
cascadingMatcher,
patternMatcher,
} from "../util/nodeMatchers";
import {
NodeMatcherAlternative,
ScopeType,
SelectionWithEditor,
} from "../typings/Types";
import { getNodeRange } from "../util/nodeSelectors";
import { SyntaxNode } from "web-tree-sitter";
import { NodeMatcherAlternative, ScopeType } from "../typings/Types";
// Generated by the following command:
// `curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-go/master/src/node-types.json | jq '[.[] | select(.type == "_statement" or .type == "_simple_statement") | .subtypes[].type]'`
@ -42,7 +33,7 @@ const STATEMENT_TYPES = [
"select_statement",
"type_declaration",
"type_switch_statement",
"var_declaration"
"var_declaration",
];
const nodeMatchers: Partial<Record<ScopeType, NodeMatcherAlternative>> = {
@ -70,11 +61,11 @@ const nodeMatchers: Partial<Record<ScopeType, NodeMatcherAlternative>> = {
patternMatcher("argument_declaration")
),
collectionItem: ["keyed_element", "element"],
collectionKey: childAtIndexMatcher(["keyed_element"], 0),
collectionKey: "keyed_element[0]",
value: cascadingMatcher(
childAtIndexMatcher(["keyed_element"], 1),
patternMatcher("keyed_element[1]"),
patternMatcher("return_statement.expression_list!")
)
),
};
export default createPatternMatchers(nodeMatchers);
export default createPatternMatchers(nodeMatchers);

View File

@ -0,0 +1,31 @@
languageId: typescript
command:
version: 1
spokenForm: take funk cap
action: setSelection
targets:
- type: primitive
modifier: {type: containingScope, scopeType: namedFunction, includeSiblings: false}
mark: {type: decoratedSymbol, symbolColor: default, character: c}
initialState:
documentContents: |
const myFunk = () => {
};
selections:
- anchor: {line: 2, character: 0}
active: {line: 2, character: 0}
marks:
default.c:
start: {line: 0, character: 0}
end: {line: 0, character: 5}
finalState:
documentContents: |
const myFunk = () => {
};
selections:
- anchor: {line: 0, character: 0}
active: {line: 1, character: 2}
thatMark:
- anchor: {line: 0, character: 0}
active: {line: 1, character: 2}
fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: c}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: containingScope, scopeType: namedFunction, includeSiblings: false}, isImplicit: false}]

View File

@ -0,0 +1,31 @@
languageId: typescript
command:
version: 1
spokenForm: take funk made
action: setSelection
targets:
- type: primitive
modifier: {type: containingScope, scopeType: namedFunction, includeSiblings: false}
mark: {type: decoratedSymbol, symbolColor: default, character: m}
initialState:
documentContents: |
const myFunk = () => {
};
selections:
- anchor: {line: 2, character: 0}
active: {line: 2, character: 0}
marks:
default.m:
start: {line: 0, character: 6}
end: {line: 0, character: 12}
finalState:
documentContents: |
const myFunk = () => {
};
selections:
- anchor: {line: 0, character: 0}
active: {line: 1, character: 2}
thatMark:
- anchor: {line: 0, character: 0}
active: {line: 1, character: 2}
fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: m}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: containingScope, scopeType: namedFunction, includeSiblings: false}, isImplicit: false}]

View File

@ -0,0 +1,31 @@
languageId: typescript
command:
version: 1
spokenForm: take funk made
action: setSelection
targets:
- type: primitive
modifier: {type: containingScope, scopeType: namedFunction, includeSiblings: false}
mark: {type: decoratedSymbol, symbolColor: default, character: m}
initialState:
documentContents: |
const myFunk = function() {
}
selections:
- anchor: {line: 2, character: 0}
active: {line: 2, character: 0}
marks:
default.m:
start: {line: 0, character: 6}
end: {line: 0, character: 12}
finalState:
documentContents: |
const myFunk = function() {
}
selections:
- anchor: {line: 0, character: 0}
active: {line: 1, character: 1}
thatMark:
- anchor: {line: 0, character: 0}
active: {line: 1, character: 1}
fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: m}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: containingScope, scopeType: namedFunction, includeSiblings: false}, isImplicit: false}]

View File

@ -0,0 +1,31 @@
languageId: typescript
command:
version: 1
spokenForm: take funk soon
action: setSelection
targets:
- type: primitive
modifier: {type: containingScope, scopeType: namedFunction, includeSiblings: false}
mark: {type: decoratedSymbol, symbolColor: default, character: s}
initialState:
documentContents: |
const myFunk = function() {
}
selections:
- anchor: {line: 2, character: 0}
active: {line: 2, character: 0}
marks:
default.s:
start: {line: 0, character: 0}
end: {line: 0, character: 5}
finalState:
documentContents: |
const myFunk = function() {
}
selections:
- anchor: {line: 0, character: 0}
active: {line: 1, character: 1}
thatMark:
- anchor: {line: 0, character: 0}
active: {line: 1, character: 1}
fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: s}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: containingScope, scopeType: namedFunction, includeSiblings: false}, isImplicit: false}]

View File

@ -147,32 +147,19 @@ function tryPatternMatch(
node: SyntaxNode,
patterns: Pattern[]
): SyntaxNode | null {
const firstPattern = patterns[0];
const lastPattern = patterns[patterns.length - 1];
let result = searchNodeAscending(node, patterns);
if (!result && patterns.length > 1) {
result = searchNodeDescending(node, patterns);
}
let resultNode: SyntaxNode | null = null;
let resultPattern;
// Only one type try to match current node.
if (patterns.length === 1) {
if (firstPattern.typeEquals(node)) {
resultNode = node;
resultPattern = firstPattern;
}
} else {
// Matched last. Ascending search.
if (lastPattern.typeEquals(node)) {
const result = searchNodeAscending(node, lastPattern, patterns);
if (result != null) {
[resultNode, resultPattern] = result;
}
}
// Matched first. Descending search.
if (resultNode == null && firstPattern.typeEquals(node)) {
const result = searchNodeDescending(node, firstPattern, patterns);
if (result != null) {
[resultNode, resultPattern] = result;
}
}
if (result != null) {
[resultNode, resultPattern] = result;
}
// Use field name child if field name is given
if (
resultNode != null &&
@ -180,9 +167,13 @@ function tryPatternMatch(
resultPattern.fields != null
) {
resultPattern.fields.forEach((field) => {
resultNode = resultNode?.childForFieldName(field) ?? null;
resultNode =
(field.isIndex
? resultNode?.namedChild(field.value)
: resultNode?.childForFieldName(field.value)) ?? null;
});
}
return resultNode;
}
@ -190,76 +181,118 @@ type NodePattern = [SyntaxNode, Pattern] | null;
function searchNodeAscending(
node: SyntaxNode,
lastPattern: Pattern,
patterns: Pattern[]
): NodePattern {
let resultNode = node;
let resultPattern = lastPattern;
let important: NodePattern = lastPattern.isImportant
? [node, lastPattern]
: null;
for (let i = patterns.length - 2; i > -1; --i) {
let result: NodePattern = null;
let currentNode: SyntaxNode | null = node;
for (let i = patterns.length - 1; i > -1; --i) {
const pattern = patterns[i];
if (resultNode.parent == null || !pattern.typeEquals(resultNode.parent)) {
if (currentNode == null || !pattern.typeEquals(currentNode)) {
if (pattern.isOptional) {
continue;
}
return null;
}
resultNode = resultNode.parent;
resultPattern = pattern;
if (pattern.isImportant) {
important = [resultNode, pattern];
// Return top node if not important found
if (!result || !result[1].isImportant) {
result = [currentNode, pattern];
}
currentNode = currentNode.parent;
}
return important != null ? important : [resultNode, resultPattern];
return result;
}
function searchNodeDescending(
node: SyntaxNode,
firstPattern: Pattern,
patterns: Pattern[]
): NodePattern {
let tmpNode = node;
// Even if descending search we return the "top" node by default.
let important: NodePattern = [node, firstPattern];
for (let i = 1; i < patterns.length; ++i) {
let result: NodePattern = null;
let currentNode: SyntaxNode | null = node;
for (let i = 0; i < patterns.length; ++i) {
const pattern = patterns[i];
const children = tmpNode.namedChildren.filter((node) =>
pattern.typeEquals(node)
);
if (children.length !== 1) {
if (currentNode == null || !pattern.typeEquals(currentNode)) {
if (pattern.isOptional) {
continue;
}
return null;
}
tmpNode = children[0];
if (pattern.isImportant) {
important = [tmpNode, pattern];
// Return top node if not important found
if (!result || pattern.isImportant) {
result = [currentNode, pattern];
}
if (i + 1 < patterns.length) {
const children: SyntaxNode[] = currentNode.namedChildren.filter((node) =>
patterns[i + 1].typeEquals(node)
);
currentNode = children.length === 1 ? children[0] : null;
}
}
return important;
return result;
}
interface PatternFieldIndex {
isIndex: true;
value: number;
}
interface PatternFieldName {
isIndex: false;
value: string;
}
type PatternField = PatternFieldName | PatternFieldIndex;
class Pattern {
type: string;
fields: string[] | null = null;
isImportant: boolean = false;
isOptional: boolean = false;
fields: PatternField[];
isImportant: boolean;
isOptional: boolean;
anyType: boolean = false;
notType: boolean = false;
constructor(pattern: string) {
this.type = pattern.match(/^[\w*~]+/)![0];
this.fields = [...pattern.matchAll(/(?<=\[).+?(?=\])/g)].map((m) => m[0]);
if (this.type === "*") {
this.anyType = true;
} else if (this.type.startsWith("~")) {
this.type = this.type.slice(1);
this.notType = true;
}
this.isImportant = pattern.indexOf("!") > -1;
this.isOptional = pattern.indexOf("?") > -1;
this.fields = [...pattern.matchAll(/(?<=\[).+?(?=\])/g)]
.map((m) => m[0])
.map((field) => {
if (/\d+/.test(field)) {
return {
isIndex: true,
value: Number(field),
};
}
return {
isIndex: false,
value: field,
};
});
}
typeEquals(node: SyntaxNode) {
return (
this.type === node.type ||
this.type === "*" ||
(this.type.startsWith("~") && this.type.slice(1) !== node.type)
);
if (this.anyType) {
return true;
}
if (this.notType) {
return this.type !== node.type;
}
return this.type === node.type;
}
}

View File

@ -86,20 +86,6 @@ export function argumentMatcher(...parentTypes: string[]): NodeMatcher {
export function conditionMatcher(...patterns: string[]): NodeMatcher {
return matcher(patternFinder(...patterns), conditionSelectionExtractor);
}
/**
* Given `patterns`, creates a node matcher that selects the named child
* at the specified index of the pattern matched node.
*
* @param patterns Patterns for pattern finder
* @param childIdx Index of child
* @returns A node matcher
*/
export function childAtIndexMatcher(patterns: string[], childIdx: number): NodeMatcher {
const finder = patternFinder(...patterns);
return matcher(
(node: SyntaxNode) => finder(node)?.namedChild(childIdx) ?? null
);
}
/**
* Given `patterns`, creates a node matcher that will add leading delimiter to