From d0d41d9a994e68e6e68818e9eb9e1173037b2b56 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 17 Jan 2022 15:17:04 +0100 Subject: [PATCH] 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 --- src/languages/go.ts | 21 +-- .../languages/typescript/takeFunkCap.yml | 31 ++++ .../languages/typescript/takeFunkMade.yml | 31 ++++ .../languages/typescript/takeFunkMade2.yml | 31 ++++ .../languages/typescript/takeFunkSoon.yml | 31 ++++ src/util/nodeFinders.ts | 151 +++++++++++------- src/util/nodeMatchers.ts | 14 -- 7 files changed, 222 insertions(+), 88 deletions(-) create mode 100644 src/test/suite/fixtures/recorded/languages/typescript/takeFunkCap.yml create mode 100644 src/test/suite/fixtures/recorded/languages/typescript/takeFunkMade.yml create mode 100644 src/test/suite/fixtures/recorded/languages/typescript/takeFunkMade2.yml create mode 100644 src/test/suite/fixtures/recorded/languages/typescript/takeFunkSoon.yml diff --git a/src/languages/go.ts b/src/languages/go.ts index e4b1d8f13..8de97bd28 100644 --- a/src/languages/go.ts +++ b/src/languages/go.ts @@ -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> = { @@ -70,11 +61,11 @@ const nodeMatchers: Partial> = { 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); \ No newline at end of file +export default createPatternMatchers(nodeMatchers); diff --git a/src/test/suite/fixtures/recorded/languages/typescript/takeFunkCap.yml b/src/test/suite/fixtures/recorded/languages/typescript/takeFunkCap.yml new file mode 100644 index 000000000..6a1339a05 --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/typescript/takeFunkCap.yml @@ -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}] diff --git a/src/test/suite/fixtures/recorded/languages/typescript/takeFunkMade.yml b/src/test/suite/fixtures/recorded/languages/typescript/takeFunkMade.yml new file mode 100644 index 000000000..75cb96a1f --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/typescript/takeFunkMade.yml @@ -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}] diff --git a/src/test/suite/fixtures/recorded/languages/typescript/takeFunkMade2.yml b/src/test/suite/fixtures/recorded/languages/typescript/takeFunkMade2.yml new file mode 100644 index 000000000..103a4a14b --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/typescript/takeFunkMade2.yml @@ -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}] diff --git a/src/test/suite/fixtures/recorded/languages/typescript/takeFunkSoon.yml b/src/test/suite/fixtures/recorded/languages/typescript/takeFunkSoon.yml new file mode 100644 index 000000000..bcd8a69a3 --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/typescript/takeFunkSoon.yml @@ -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}] diff --git a/src/util/nodeFinders.ts b/src/util/nodeFinders.ts index dc0aabd9a..191fbe2eb 100644 --- a/src/util/nodeFinders.ts +++ b/src/util/nodeFinders.ts @@ -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; } } diff --git a/src/util/nodeMatchers.ts b/src/util/nodeMatchers.ts index 7ba5efdb0..f58363ac7 100644 --- a/src/util/nodeMatchers.ts +++ b/src/util/nodeMatchers.ts @@ -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