Skip to content

Preliminary clojure support #375

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 12 commits into from
Dec 16, 2021
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