Skip to content

Commit 8d5e372

Browse files
authored
Preliminary clojure support (#375)
* Preliminary clojure support * Cleanup * Support clojure key, value, and item * Add tests for multiple cursors * Handle case where selection inside comment * Add some basic argument support * More work on clojure * A bit of cleanup * Support if statements
1 parent 57aeb8f commit 8d5e372

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+1901
-56
lines changed

src/languages/clojure.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import {
2+
cascadingMatcher,
3+
chainedMatcher,
4+
createPatternMatchers,
5+
matcher,
6+
patternMatcher,
7+
} from "../util/nodeMatchers";
8+
import {
9+
ScopeType,
10+
NodeMatcherAlternative,
11+
NodeFinder,
12+
} from "../typings/Types";
13+
import { SyntaxNode } from "web-tree-sitter";
14+
import { delimitedSelector } from "../util/nodeSelectors";
15+
import { flow, identity } from "lodash";
16+
import { getChildNodesForFieldName } from "../util/treeSitterUtils";
17+
import { patternFinder } from "../util/nodeFinders";
18+
19+
/**
20+
* Picks a node by rounding down and using the given parity. This function is
21+
* useful for picking the picking eg the key in a sequence of key-value pairs
22+
* @param parentFinder The finder to use to determine whether the parent is a
23+
* match
24+
* @param parity The parity that we're looking for
25+
* @returns A node finder
26+
*/
27+
function parityNodeFinder(parentFinder: NodeFinder, parity: 0 | 1) {
28+
return indexNodeFinder(
29+
parentFinder,
30+
(nodeIndex: number) => Math.floor(nodeIndex / 2) * 2 + parity
31+
);
32+
}
33+
34+
function mapParityNodeFinder(parity: 0 | 1) {
35+
return parityNodeFinder(patternFinder("map_lit"), parity);
36+
}
37+
38+
/**
39+
* Creates a node finder which will apply a transformation to the index of a
40+
* value node and return the node at the given index of the nodes parent
41+
* @param parentFinder A finder which will be applied to the parent to determine
42+
* whether it is a match
43+
* @param indexTransform A function that will be applied to the index of the
44+
* value node. The node at the given index will be used instead of the node
45+
* itself
46+
* @returns A node finder based on the given description
47+
*/
48+
function indexNodeFinder(
49+
parentFinder: NodeFinder,
50+
indexTransform: (index: number) => number
51+
) {
52+
return (node: SyntaxNode) => {
53+
const parent = node.parent;
54+
55+
if (parent == null || parentFinder(parent) == null) {
56+
return null;
57+
}
58+
59+
const valueNodes = getValueNodes(parent);
60+
61+
const nodeIndex = valueNodes.findIndex(({ id }) => id === node.id);
62+
63+
if (nodeIndex === -1) {
64+
// TODO: In the future we might conceivably try to handle saying "take
65+
// item" when the selection is inside a comment between the key and value
66+
return null;
67+
}
68+
69+
const desiredIndex = indexTransform(nodeIndex);
70+
71+
if (desiredIndex === -1) {
72+
return null;
73+
}
74+
75+
return valueNodes[desiredIndex];
76+
};
77+
}
78+
79+
function itemFinder() {
80+
return indexNodeFinder(
81+
(node) => node,
82+
(nodeIndex: number) => nodeIndex
83+
);
84+
}
85+
86+
/**
87+
* Return the "value" node children of a given node. These are the items in a list
88+
* @param node The node whose children to get
89+
* @returns A list of the value node children of the given node
90+
*/
91+
const getValueNodes = (node: SyntaxNode) =>
92+
getChildNodesForFieldName(node, "value");
93+
94+
// A function call is a list literal which is not quoted
95+
const functionCallPattern = "~quoting_lit.list_lit!";
96+
const functionCallFinder = patternFinder(functionCallPattern);
97+
98+
/**
99+
* Matches a function call if the name of the function is one of the given names
100+
* @param names The acceptable function names
101+
* @returns The function call node if the name matches otherwise null
102+
*/
103+
function functionNameBasedFinder(...names: string[]) {
104+
return (node: SyntaxNode) => {
105+
const functionCallNode = functionCallFinder(node);
106+
if (functionCallNode == null) {
107+
return null;
108+
}
109+
110+
const functionNode = getValueNodes(functionCallNode)[0];
111+
112+
return names.includes(functionNode?.text) ? functionCallNode : null;
113+
};
114+
}
115+
116+
function functionNameBasedMatcher(...names: string[]) {
117+
return matcher(functionNameBasedFinder(...names));
118+
}
119+
120+
const functionFinder = functionNameBasedFinder("defn", "defmacro");
121+
122+
const functionNameMatcher = chainedMatcher([
123+
functionFinder,
124+
(functionNode) => getValueNodes(functionNode)[1],
125+
]);
126+
127+
const ifStatementFinder = functionNameBasedFinder(
128+
"if",
129+
"if-let",
130+
"when",
131+
"when-let"
132+
);
133+
134+
const ifStatementMatcher = matcher(ifStatementFinder);
135+
136+
const nodeMatchers: Partial<Record<ScopeType, NodeMatcherAlternative>> = {
137+
comment: "comment",
138+
map: "map_lit",
139+
140+
collectionKey: matcher(mapParityNodeFinder(0)),
141+
collectionItem: cascadingMatcher(
142+
// Treat each key value pair as a single item if we're in a map
143+
matcher(
144+
mapParityNodeFinder(0),
145+
delimitedSelector(
146+
(node) => node.type === "{" || node.type === "}",
147+
", ",
148+
identity,
149+
mapParityNodeFinder(1) as (node: SyntaxNode) => SyntaxNode
150+
)
151+
),
152+
153+
// Otherwise just treat every item within a list as an item
154+
matcher(itemFinder())
155+
),
156+
value: matcher(mapParityNodeFinder(1)),
157+
158+
// TODO: Handle formal parameters
159+
argumentOrParameter: matcher(
160+
indexNodeFinder(patternFinder(functionCallPattern), (nodeIndex: number) =>
161+
nodeIndex !== 0 ? nodeIndex : -1
162+
)
163+
),
164+
165+
// A list is either a vector literal or a quoted list literal
166+
list: ["vec_lit", "quoting_lit.list_lit"],
167+
168+
string: "str_lit",
169+
170+
functionCall: functionCallPattern,
171+
172+
namedFunction: matcher(functionFinder),
173+
174+
functionName: functionNameMatcher,
175+
176+
// TODO: Handle `let` declarations, defs, etc
177+
name: functionNameMatcher,
178+
179+
anonymousFunction: cascadingMatcher(
180+
functionNameBasedMatcher("fn"),
181+
patternMatcher("anon_fn_lit")
182+
),
183+
184+
ifStatement: ifStatementMatcher,
185+
186+
condition: chainedMatcher([
187+
ifStatementFinder,
188+
(node) => getValueNodes(node)[1],
189+
]),
190+
};
191+
192+
export default createPatternMatchers(nodeMatchers);

src/languages/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export const supportedLanguageIds = [
22
"c",
3+
"clojure",
34
"cpp",
45
"csharp",
56
"java",

src/languages/csharp.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { SyntaxNode } from "web-tree-sitter";
22
import {
33
cascadingMatcher,
4-
composedMatcher,
4+
chainedMatcher,
55
createPatternMatchers,
66
matcher,
77
trailingMatcher,
@@ -162,26 +162,26 @@ const makeDelimitedSelector = (leftType: string, rightType: string) =>
162162

163163
const getMapMatchers = {
164164
map: cascadingMatcher(
165-
composedMatcher([
165+
chainedMatcher([
166166
typedNodeFinder(...OBJECT_TYPES_WITH_INITIALIZERS_AS_CHILDREN),
167167
getChildInitializerNode,
168168
]),
169-
composedMatcher([
169+
chainedMatcher([
170170
typedNodeFinder("object_creation_expression"),
171171
getInitializerNode,
172172
])
173173
),
174-
collectionKey: composedMatcher([
174+
collectionKey: chainedMatcher([
175175
typedNodeFinder("assignment_expression"),
176176
(node: SyntaxNode) => node.childForFieldName("left"),
177177
]),
178178
value: matcher((node: SyntaxNode) => node.childForFieldName("right")),
179179
list: cascadingMatcher(
180-
composedMatcher([
180+
chainedMatcher([
181181
typedNodeFinder(...LIST_TYPES_WITH_INITIALIZERS_AS_CHILDREN),
182182
getChildInitializerNode,
183183
]),
184-
composedMatcher([
184+
chainedMatcher([
185185
typedNodeFinder("object_creation_expression"),
186186
(node: SyntaxNode) => node.childForFieldName("initializer"),
187187
])

src/languages/getNodeMatcher.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import {
88
SelectionWithEditor,
99
} from "../typings/Types";
1010
import cpp from "./cpp";
11+
import clojure from "./clojure";
1112
import csharp from "./csharp";
1213
import { patternMatchers as json } from "./json";
1314
import { patternMatchers as typescript } from "./typescript";
14-
import { patternMatchers as java } from "./java";
15+
import java from "./java";
1516
import python from "./python";
1617
import { UnsupportedLanguageError } from "../errors";
1718
import { SupportedLanguageId } from "./constants";
@@ -45,7 +46,8 @@ const languageMatchers: Record<
4546
Record<ScopeType, NodeMatcher>
4647
> = {
4748
c: cpp,
48-
cpp: cpp,
49+
clojure,
50+
cpp,
4951
csharp: csharp,
5052
java,
5153
javascript: typescript,

src/languages/getTextFragmentExtractor.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { SyntaxNode } from "web-tree-sitter";
22
import { SelectionWithEditor } from "../typings/Types";
33
import { stringTextFragmentExtractor as jsonStringTextFragmentExtractor } from "./json";
4-
import { stringTextFragmentExtractor as javaStringTextFragmentExtractor } from "./java";
54
import { stringTextFragmentExtractor as typescriptStringTextFragmentExtractor } from "./typescript";
65
import { UnsupportedLanguageError } from "../errors";
76
import { Range } from "vscode";
@@ -67,6 +66,33 @@ function constructDefaultStringTextFragmentExtractor(
6766
};
6867
}
6968

69+
/**
70+
* Extracts string text fragments in languages that don't have quotation mark
71+
* tokens as children of string tokens, but instead include them in the text of
72+
* the string.
73+
*
74+
* This is a hack. Rather than letting the parse tree handle the quotation marks
75+
* in java, we instead just let the textual surround handle them by letting it
76+
* see the quotation marks. In other languages we prefer to let the parser
77+
* handle the quotation marks in case they are more than one character long.
78+
* @param node The node which might be a string node
79+
* @param selection The selection from which to expand
80+
* @returns The range of the string text or null if the node is not a string
81+
*/
82+
function constructHackedStringTextFragmentExtractor(
83+
languageId: SupportedLanguageId
84+
) {
85+
const stringNodeMatcher = getNodeMatcher(languageId, "string", false);
86+
87+
return (node: SyntaxNode, selection: SelectionWithEditor) => {
88+
if (stringNodeMatcher(selection, node) != null) {
89+
return getNodeRange(node);
90+
}
91+
92+
return null;
93+
};
94+
}
95+
7096
/**
7197
* Returns a function which can be used to extract the range of a text fragment
7298
* from within a parsed language. This function should only return a nominal
@@ -94,11 +120,15 @@ const textFragmentExtractors: Record<
94120
TextFragmentExtractor
95121
> = {
96122
c: constructDefaultTextFragmentExtractor("c"),
123+
clojure: constructDefaultTextFragmentExtractor(
124+
"clojure",
125+
constructHackedStringTextFragmentExtractor("clojure")
126+
),
97127
cpp: constructDefaultTextFragmentExtractor("cpp"),
98128
csharp: constructDefaultTextFragmentExtractor("csharp"),
99129
java: constructDefaultTextFragmentExtractor(
100130
"java",
101-
javaStringTextFragmentExtractor
131+
constructHackedStringTextFragmentExtractor("java")
102132
),
103133
javascript: constructDefaultTextFragmentExtractor(
104134
"javascript",

src/languages/java.ts

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,7 @@ import {
55
conditionMatcher,
66
trailingMatcher,
77
} from "../util/nodeMatchers";
8-
import {
9-
NodeMatcherAlternative,
10-
ScopeType,
11-
SelectionWithEditor,
12-
} from "../typings/Types";
13-
import { getNodeRange } from "../util/nodeSelectors";
14-
import { SyntaxNode } from "web-tree-sitter";
8+
import { NodeMatcherAlternative, ScopeType } from "../typings/Types";
159

1610
// Generated by the following command:
1711
// > curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-java/master/src/node-types.json | jq '[.[] | select(.type == "statement" or .type == "declaration") | .subtypes[].type]'
@@ -78,28 +72,4 @@ const nodeMatchers: Partial<Record<ScopeType, NodeMatcherAlternative>> = {
7872
argumentOrParameter: argumentMatcher("formal_parameters", "argument_list"),
7973
};
8074

81-
export const patternMatchers = createPatternMatchers(nodeMatchers);
82-
83-
/**
84-
* Extracts string text fragments in java.
85-
*
86-
* This is a hack to deal with the fact that java doesn't have
87-
* quotation mark tokens as children of the string. Rather than letting
88-
* the parse tree handle the quotation marks in java, we instead just
89-
* let the textual surround handle them by letting it see the quotation
90-
* marks. In other languages we prefer to let the parser handle the
91-
* quotation marks in case they are more than one character long.
92-
* @param node The node which might be a string node
93-
* @param selection The selection from which to expand
94-
* @returns The range of the string text or null if the node is not a string
95-
*/
96-
export function stringTextFragmentExtractor(
97-
node: SyntaxNode,
98-
selection: SelectionWithEditor
99-
) {
100-
if (node.type === "string_literal") {
101-
return getNodeRange(node);
102-
}
103-
104-
return null;
105-
}
75+
export default createPatternMatchers(nodeMatchers);

0 commit comments

Comments
 (0)