Skip to content
192 changes: 192 additions & 0 deletions src/languages/clojure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import {
cascadingMatcher,
chainedMatcher,
createPatternMatchers,
matcher,
patternMatcher,
} from "../util/nodeMatchers";
import {
ScopeType,
NodeMatcherAlternative,
NodeFinder,
} from "../typings/Types";
import { SyntaxNode } from "web-tree-sitter";
import { delimitedSelector } from "../util/nodeSelectors";
import { flow, identity } from "lodash";
import { getChildNodesForFieldName } from "../util/treeSitterUtils";
import { patternFinder } from "../util/nodeFinders";

/**
* Picks a node by rounding down and using the given parity. This function is
* useful for picking the picking eg the key in a sequence of key-value pairs
* @param parentFinder The finder to use to determine whether the parent is a
* match
* @param parity The parity that we're looking for
* @returns A node finder
*/
function parityNodeFinder(parentFinder: NodeFinder, parity: 0 | 1) {
return indexNodeFinder(
parentFinder,
(nodeIndex: number) => Math.floor(nodeIndex / 2) * 2 + parity
);
}

function mapParityNodeFinder(parity: 0 | 1) {
return parityNodeFinder(patternFinder("map_lit"), parity);
}

/**
* Creates a node finder which will apply a transformation to the index of a
* value node and return the node at the given index of the nodes parent
* @param parentFinder A finder which will be applied to the parent to determine
* whether it is a match
* @param indexTransform A function that will be applied to the index of the
* value node. The node at the given index will be used instead of the node
* itself
* @returns A node finder based on the given description
*/
function indexNodeFinder(
parentFinder: NodeFinder,
indexTransform: (index: number) => number
) {
return (node: SyntaxNode) => {
const parent = node.parent;

if (parent == null || parentFinder(parent) == null) {
return null;
}

const valueNodes = getValueNodes(parent);

const nodeIndex = valueNodes.findIndex(({ id }) => id === node.id);

if (nodeIndex === -1) {
// TODO: In the future we might conceivably try to handle saying "take
// item" when the selection is inside a comment between the key and value
return null;
}

const desiredIndex = indexTransform(nodeIndex);

if (desiredIndex === -1) {
return null;
}

return valueNodes[desiredIndex];
};
}

function itemFinder() {
return indexNodeFinder(
(node) => node,
(nodeIndex: number) => nodeIndex
);
}

/**
* Return the "value" node children of a given node. These are the items in a list
* @param node The node whose children to get
* @returns A list of the value node children of the given node
*/
const getValueNodes = (node: SyntaxNode) =>
getChildNodesForFieldName(node, "value");

// A function call is a list literal which is not quoted
const functionCallPattern = "~quoting_lit.list_lit!";
const functionCallFinder = patternFinder(functionCallPattern);

/**
* Matches a function call if the name of the function is one of the given names
* @param names The acceptable function names
* @returns The function call node if the name matches otherwise null
*/
function functionNameBasedFinder(...names: string[]) {
return (node: SyntaxNode) => {
const functionCallNode = functionCallFinder(node);
if (functionCallNode == null) {
return null;
}

const functionNode = getValueNodes(functionCallNode)[0];

return names.includes(functionNode?.text) ? functionCallNode : null;
};
}

function functionNameBasedMatcher(...names: string[]) {
return matcher(functionNameBasedFinder(...names));
}

const functionFinder = functionNameBasedFinder("defn", "defmacro");

const functionNameMatcher = chainedMatcher([
functionFinder,
(functionNode) => getValueNodes(functionNode)[1],
]);

const ifStatementFinder = functionNameBasedFinder(
"if",
"if-let",
"when",
"when-let"
);

const ifStatementMatcher = matcher(ifStatementFinder);

const nodeMatchers: Partial<Record<ScopeType, NodeMatcherAlternative>> = {
comment: "comment",
map: "map_lit",

collectionKey: matcher(mapParityNodeFinder(0)),
collectionItem: cascadingMatcher(
// Treat each key value pair as a single item if we're in a map
matcher(
mapParityNodeFinder(0),
delimitedSelector(
(node) => node.type === "{" || node.type === "}",
", ",
identity,
mapParityNodeFinder(1) as (node: SyntaxNode) => SyntaxNode
)
),

// Otherwise just treat every item within a list as an item
matcher(itemFinder())
),
value: matcher(mapParityNodeFinder(1)),

// TODO: Handle formal parameters
argumentOrParameter: matcher(
indexNodeFinder(patternFinder(functionCallPattern), (nodeIndex: number) =>
nodeIndex !== 0 ? nodeIndex : -1
)
),

// A list is either a vector literal or a quoted list literal
list: ["vec_lit", "quoting_lit.list_lit"],

string: "str_lit",

functionCall: functionCallPattern,

namedFunction: matcher(functionFinder),

functionName: functionNameMatcher,

// TODO: Handle `let` declarations, defs, etc
name: functionNameMatcher,

anonymousFunction: cascadingMatcher(
functionNameBasedMatcher("fn"),
patternMatcher("anon_fn_lit")
),

ifStatement: ifStatementMatcher,

condition: chainedMatcher([
ifStatementFinder,
(node) => getValueNodes(node)[1],
]),
};

export default createPatternMatchers(nodeMatchers);
1 change: 1 addition & 0 deletions src/languages/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const supportedLanguageIds = [
"c",
"clojure",
"cpp",
"csharp",
"java",
Expand Down
12 changes: 6 additions & 6 deletions src/languages/csharp.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SyntaxNode } from "web-tree-sitter";
import {
cascadingMatcher,
composedMatcher,
chainedMatcher,
createPatternMatchers,
matcher,
trailingMatcher,
Expand Down Expand Up @@ -162,26 +162,26 @@ const makeDelimitedSelector = (leftType: string, rightType: string) =>

const getMapMatchers = {
map: cascadingMatcher(
composedMatcher([
chainedMatcher([
typedNodeFinder(...OBJECT_TYPES_WITH_INITIALIZERS_AS_CHILDREN),
getChildInitializerNode,
]),
composedMatcher([
chainedMatcher([
typedNodeFinder("object_creation_expression"),
getInitializerNode,
])
),
collectionKey: composedMatcher([
collectionKey: chainedMatcher([
typedNodeFinder("assignment_expression"),
(node: SyntaxNode) => node.childForFieldName("left"),
]),
value: matcher((node: SyntaxNode) => node.childForFieldName("right")),
list: cascadingMatcher(
composedMatcher([
chainedMatcher([
typedNodeFinder(...LIST_TYPES_WITH_INITIALIZERS_AS_CHILDREN),
getChildInitializerNode,
]),
composedMatcher([
chainedMatcher([
typedNodeFinder("object_creation_expression"),
(node: SyntaxNode) => node.childForFieldName("initializer"),
])
Expand Down
6 changes: 4 additions & 2 deletions src/languages/getNodeMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import {
SelectionWithEditor,
} from "../typings/Types";
import cpp from "./cpp";
import clojure from "./clojure";
import csharp from "./csharp";
import { patternMatchers as json } from "./json";
import { patternMatchers as typescript } from "./typescript";
import { patternMatchers as java } from "./java";
import java from "./java";
import python from "./python";
import { UnsupportedLanguageError } from "../errors";
import { SupportedLanguageId } from "./constants";
Expand Down Expand Up @@ -45,7 +46,8 @@ const languageMatchers: Record<
Record<ScopeType, NodeMatcher>
> = {
c: cpp,
cpp: cpp,
clojure,
cpp,
csharp: csharp,
java,
javascript: typescript,
Expand Down
34 changes: 32 additions & 2 deletions src/languages/getTextFragmentExtractor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { SyntaxNode } from "web-tree-sitter";
import { SelectionWithEditor } from "../typings/Types";
import { stringTextFragmentExtractor as jsonStringTextFragmentExtractor } from "./json";
import { stringTextFragmentExtractor as javaStringTextFragmentExtractor } from "./java";
import { stringTextFragmentExtractor as typescriptStringTextFragmentExtractor } from "./typescript";
import { UnsupportedLanguageError } from "../errors";
import { Range } from "vscode";
Expand Down Expand Up @@ -67,6 +66,33 @@ function constructDefaultStringTextFragmentExtractor(
};
}

/**
* Extracts string text fragments in languages that don't have quotation mark
* tokens as children of string tokens, but instead include them in the text of
* the string.
*
* This is a hack. Rather than letting the parse tree handle the quotation marks
* in java, we instead just let the textual surround handle them by letting it
* see the quotation marks. In other languages we prefer to let the parser
* handle the quotation marks in case they are more than one character long.
* @param node The node which might be a string node
* @param selection The selection from which to expand
* @returns The range of the string text or null if the node is not a string
*/
function constructHackedStringTextFragmentExtractor(
languageId: SupportedLanguageId
) {
const stringNodeMatcher = getNodeMatcher(languageId, "string", false);

return (node: SyntaxNode, selection: SelectionWithEditor) => {
if (stringNodeMatcher(selection, node) != null) {
return getNodeRange(node);
}

return null;
};
}

/**
* Returns a function which can be used to extract the range of a text fragment
* from within a parsed language. This function should only return a nominal
Expand Down Expand Up @@ -94,11 +120,15 @@ const textFragmentExtractors: Record<
TextFragmentExtractor
> = {
c: constructDefaultTextFragmentExtractor("c"),
clojure: constructDefaultTextFragmentExtractor(
"clojure",
constructHackedStringTextFragmentExtractor("clojure")
),
cpp: constructDefaultTextFragmentExtractor("cpp"),
csharp: constructDefaultTextFragmentExtractor("csharp"),
java: constructDefaultTextFragmentExtractor(
"java",
javaStringTextFragmentExtractor
constructHackedStringTextFragmentExtractor("java")
),
javascript: constructDefaultTextFragmentExtractor(
"javascript",
Expand Down
34 changes: 2 additions & 32 deletions src/languages/java.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,7 @@ import {
conditionMatcher,
trailingMatcher,
} 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-java/master/src/node-types.json | jq '[.[] | select(.type == "statement" or .type == "declaration") | .subtypes[].type]'
Expand Down Expand Up @@ -78,28 +72,4 @@ const nodeMatchers: Partial<Record<ScopeType, NodeMatcherAlternative>> = {
argumentOrParameter: argumentMatcher("formal_parameters", "argument_list"),
};

export const patternMatchers = createPatternMatchers(nodeMatchers);

/**
* Extracts string text fragments in java.
*
* This is a hack to deal with the fact that java doesn't have
* quotation mark tokens as children of the string. Rather than letting
* the parse tree handle the quotation marks in java, we instead just
* let the textual surround handle them by letting it see the quotation
* marks. In other languages we prefer to let the parser handle the
* quotation marks in case they are more than one character long.
* @param node The node which might be a string node
* @param selection The selection from which to expand
* @returns The range of the string text or null if the node is not a string
*/
export function stringTextFragmentExtractor(
node: SyntaxNode,
selection: SelectionWithEditor
) {
if (node.type === "string_literal") {
return getNodeRange(node);
}

return null;
}
export default createPatternMatchers(nodeMatchers);
Loading