diff --git a/packages/app-web-docs/src/docs/user/languages/fixtures/markdown.json b/packages/app-web-docs/src/docs/user/languages/fixtures/markdown.json
index 9a9b111778..ee696d3643 100644
--- a/packages/app-web-docs/src/docs/user/languages/fixtures/markdown.json
+++ b/packages/app-web-docs/src/docs/user/languages/fixtures/markdown.json
@@ -23,6 +23,54 @@
}
]
},
+ {
+ "name": "scopes/markdown/collectionItem.unenclosed.iteration2",
+ "languageId": "markdown",
+ "facet": "collectionItem.unenclosed.iteration",
+ "code": "- First item\n Paragraph\n- Second item",
+ "scopes": [
+ {
+ "targets": [
+ {
+ "content": "0:0-2:13"
+ }
+ ],
+ "domain": "0:0-2:13"
+ }
+ ]
+ },
+ {
+ "name": "scopes/markdown/collectionItem.unenclosed.iteration3",
+ "languageId": "markdown",
+ "facet": "collectionItem.unenclosed.iteration",
+ "code": "- [ ] First item\n- [ ] Second item",
+ "scopes": [
+ {
+ "targets": [
+ {
+ "content": "0:0-1:17"
+ }
+ ],
+ "domain": "0:0-1:17"
+ },
+ {
+ "targets": [
+ {
+ "content": "0:3-0:4"
+ }
+ ],
+ "domain": "0:3-0:4"
+ },
+ {
+ "targets": [
+ {
+ "content": "1:3-1:4"
+ }
+ ],
+ "domain": "1:3-1:4"
+ }
+ ]
+ },
{
"name": "scopes/markdown/collectionItem.unenclosed.multiLine",
"languageId": "markdown",
@@ -51,13 +99,83 @@
"targets": [
{
"content": "2:2-2:42",
- "removal": "2:0-2:42"
+ "removal": "1:39-2:42"
}
],
"domain": "2:0-2:42"
}
]
},
+ {
+ "name": "scopes/markdown/collectionItem.unenclosed.multiLine2",
+ "languageId": "markdown",
+ "facet": "collectionItem.unenclosed.multiLine",
+ "code": "- First item\n Paragraph\n- Second item",
+ "scopes": [
+ {
+ "targets": [
+ {
+ "content": "0:2-1:11",
+ "removal": "0:0-2:0"
+ }
+ ],
+ "domain": "0:0-1:11"
+ },
+ {
+ "targets": [
+ {
+ "content": "2:2-2:13",
+ "removal": "1:11-2:13"
+ }
+ ],
+ "domain": "2:0-2:13"
+ }
+ ]
+ },
+ {
+ "name": "scopes/markdown/collectionItem.unenclosed.multiLine3",
+ "languageId": "markdown",
+ "facet": "collectionItem.unenclosed.multiLine",
+ "code": "- [ ] First item\n- [ ] Second item",
+ "scopes": [
+ {
+ "targets": [
+ {
+ "content": "0:2-0:16",
+ "removal": "0:0-1:0"
+ }
+ ],
+ "domain": "0:0-0:16"
+ },
+ {
+ "targets": [
+ {
+ "content": "0:3-0:4",
+ "removal": "0:3-0:4"
+ }
+ ],
+ "domain": "0:3-0:4"
+ },
+ {
+ "targets": [
+ {
+ "content": "1:2-1:17",
+ "removal": "0:16-1:17"
+ }
+ ],
+ "domain": "1:0-1:17"
+ },
+ {
+ "targets": [
+ {
+ "content": "1:3-1:4",
+ "removal": "1:3-1:4"
+ }
+ ],
+ "domain": "1:3-1:4"
+ }
+ ]
+ },
{
"name": "scopes/markdown/comment.block",
"languageId": "markdown",
diff --git a/packages/app-web-docs/src/docs/user/languages/markdown.mdx b/packages/app-web-docs/src/docs/user/languages/markdown.mdx
index bc9ee9a081..4f80a2d77f 100644
--- a/packages/app-web-docs/src/docs/user/languages/markdown.mdx
+++ b/packages/app-web-docs/src/docs/user/languages/markdown.mdx
@@ -19,12 +19,20 @@ Below are visualizations of all our scope tests for this language. These were cr
+
+
+
+
#### 2. Collection item: Unenclosed (iteration)
Iteration scope for items in a comma-separated list without enclosing delimiters
+
+
+
+
### Comment
#### 1. Comment: Block
diff --git a/packages/app-web-docs/src/docs/user/scopes/collectionItem.mdx b/packages/app-web-docs/src/docs/user/scopes/collectionItem.mdx
index f1e44760ac..48de661ddd 100644
--- a/packages/app-web-docs/src/docs/user/scopes/collectionItem.mdx
+++ b/packages/app-web-docs/src/docs/user/scopes/collectionItem.mdx
@@ -117,6 +117,10 @@ Default: `item`
+
+
+
+
##### Scala
@@ -255,6 +259,10 @@ Default: `item`
+
+
+
+
##### Python
diff --git a/packages/app-web-docs/src/docs/user/scopes/fixtures/collectionItem.json b/packages/app-web-docs/src/docs/user/scopes/fixtures/collectionItem.json
index b052bfb74f..e08e595c35 100644
--- a/packages/app-web-docs/src/docs/user/scopes/fixtures/collectionItem.json
+++ b/packages/app-web-docs/src/docs/user/scopes/fixtures/collectionItem.json
@@ -1499,6 +1499,54 @@
}
]
},
+ {
+ "name": "scopes/markdown/collectionItem.unenclosed.iteration2",
+ "languageId": "markdown",
+ "facet": "collectionItem.unenclosed.iteration",
+ "code": "- First item\n Paragraph\n- Second item",
+ "scopes": [
+ {
+ "targets": [
+ {
+ "content": "0:0-2:13"
+ }
+ ],
+ "domain": "0:0-2:13"
+ }
+ ]
+ },
+ {
+ "name": "scopes/markdown/collectionItem.unenclosed.iteration3",
+ "languageId": "markdown",
+ "facet": "collectionItem.unenclosed.iteration",
+ "code": "- [ ] First item\n- [ ] Second item",
+ "scopes": [
+ {
+ "targets": [
+ {
+ "content": "0:0-1:17"
+ }
+ ],
+ "domain": "0:0-1:17"
+ },
+ {
+ "targets": [
+ {
+ "content": "0:3-0:4"
+ }
+ ],
+ "domain": "0:3-0:4"
+ },
+ {
+ "targets": [
+ {
+ "content": "1:3-1:4"
+ }
+ ],
+ "domain": "1:3-1:4"
+ }
+ ]
+ },
{
"name": "scopes/markdown/collectionItem.unenclosed.multiLine",
"languageId": "markdown",
@@ -1527,13 +1575,83 @@
"targets": [
{
"content": "2:2-2:42",
- "removal": "2:0-2:42"
+ "removal": "1:39-2:42"
}
],
"domain": "2:0-2:42"
}
]
},
+ {
+ "name": "scopes/markdown/collectionItem.unenclosed.multiLine2",
+ "languageId": "markdown",
+ "facet": "collectionItem.unenclosed.multiLine",
+ "code": "- First item\n Paragraph\n- Second item",
+ "scopes": [
+ {
+ "targets": [
+ {
+ "content": "0:2-1:11",
+ "removal": "0:0-2:0"
+ }
+ ],
+ "domain": "0:0-1:11"
+ },
+ {
+ "targets": [
+ {
+ "content": "2:2-2:13",
+ "removal": "1:11-2:13"
+ }
+ ],
+ "domain": "2:0-2:13"
+ }
+ ]
+ },
+ {
+ "name": "scopes/markdown/collectionItem.unenclosed.multiLine3",
+ "languageId": "markdown",
+ "facet": "collectionItem.unenclosed.multiLine",
+ "code": "- [ ] First item\n- [ ] Second item",
+ "scopes": [
+ {
+ "targets": [
+ {
+ "content": "0:2-0:16",
+ "removal": "0:0-1:0"
+ }
+ ],
+ "domain": "0:0-0:16"
+ },
+ {
+ "targets": [
+ {
+ "content": "0:3-0:4",
+ "removal": "0:3-0:4"
+ }
+ ],
+ "domain": "0:3-0:4"
+ },
+ {
+ "targets": [
+ {
+ "content": "1:2-1:17",
+ "removal": "0:16-1:17"
+ }
+ ],
+ "domain": "1:0-1:17"
+ },
+ {
+ "targets": [
+ {
+ "content": "1:3-1:4",
+ "removal": "1:3-1:4"
+ }
+ ],
+ "domain": "1:3-1:4"
+ }
+ ]
+ },
{
"name": "scopes/plaintext/collectionItem/collectionItem.textual.iteration",
"languageId": "plaintext",
diff --git a/packages/lib-engine/src/languages/TreeSitterQuery/QueryPredicateOperator.ts b/packages/lib-engine/src/languages/TreeSitterQuery/QueryPredicateOperator.ts
index fc52f50076..bc23df4fbe 100644
--- a/packages/lib-engine/src/languages/TreeSitterQuery/QueryPredicateOperator.ts
+++ b/packages/lib-engine/src/languages/TreeSitterQuery/QueryPredicateOperator.ts
@@ -35,6 +35,16 @@ export abstract class QueryPredicateOperator {
*/
abstract readonly schema: InferSchemaType;
+ /**
+ * Whether it is ok for a node argument to be missing. If true, then the
+ * operator will just accept the pattern if the given node is missing. If
+ * false, then the operator will throw an error if the node is missing.
+ *
+ * This is useful if we want to set some flag on a node, but only if it's
+ * present.
+ */
+ protected allowMissingNode: boolean = false;
+
/**
* Given a list of operands, return whether the operator accepts the given
* operands. This is where the actual logic of the operator is implemented.
@@ -47,21 +57,6 @@ export abstract class QueryPredicateOperator {
...args: AcceptFunctionArgs>>
): boolean;
- /**
- * Whether it is ok for a node argument to be missing. If true, then the
- * operator will just accept the pattern if the given node is missing. If
- * false, then the operator will throw an error if the node is missing.
- *
- * This is useful if we want to set some flag on a node, but only if it's
- * present.
- *
- * @returns A boolean indicating whether it is ok for a node argument to be
- * missing.
- */
- protected allowMissingNode(): boolean {
- return false;
- }
-
/**
* Given a list of operands, return a predicate function that can be used to
* test whether a given match satisfies the predicate.
@@ -84,7 +79,7 @@ export abstract class QueryPredicateOperator {
} catch (error) {
if (
error instanceof CaptureNotFoundError &&
- this.allowMissingNode()
+ this.allowMissingNode
) {
return true;
}
diff --git a/packages/lib-engine/src/languages/TreeSitterQuery/queryPredicateOperators.test.ts b/packages/lib-engine/src/languages/TreeSitterQuery/queryPredicateOperators.test.ts
index 34eebea26e..07104d0a2c 100644
--- a/packages/lib-engine/src/languages/TreeSitterQuery/queryPredicateOperators.test.ts
+++ b/packages/lib-engine/src/languages/TreeSitterQuery/queryPredicateOperators.test.ts
@@ -387,6 +387,7 @@ suite("queryPredicateOperators", () => {
"shrink-to-match!": [captureOperand, "hello"],
"grow-to-named-siblings!": [captureOperand],
"trim-end!": [captureOperand, conditionOperand],
+ "trim-end-optional!": [captureOperand],
"document-range!": [captureOperand, conditionOperand],
"allow-multiple!": [captureOperand, conditionOperand],
"insertion-delimiter!": [captureOperand, ", "],
@@ -448,17 +449,15 @@ suite("queryPredicateOperators", () => {
}
test("allow-multiple! accepts missing captures while range operators require them", () => {
- for (const name of [
- "allow-multiple!",
- "trim-end!",
- "document-range!",
- ] as const) {
+ const optionalNames = ["allow-multiple!", "trim-end-optional!"];
+ const requiredNames = ["trim-end!", "document-range!"];
+ for (const name of [...optionalNames, ...requiredNames] as const) {
const operator = queryPredicateOperators.find((op) => op.name === name)!;
const result = operator.createPredicate([
{ type: "capture", name: "missing" },
]);
assert.ok(result.success);
- if (name === "allow-multiple!") {
+ if (optionalNames.includes(name)) {
assert.equal(result.predicate({ captures: [] }), true);
} else {
assert.throws(
diff --git a/packages/lib-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts b/packages/lib-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts
index 21d396057b..83a78276e2 100644
--- a/packages/lib-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts
+++ b/packages/lib-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts
@@ -318,25 +318,42 @@ class TrimEnd extends QueryPredicateOperator {
run(...captures: MutableQueryCapture[]) {
for (const capture of captures) {
- const { document, range } = capture;
- const text = getNode(capture).text;
- const whitespaceLength = text.length - text.trimEnd().length;
-
- if (whitespaceLength > 0) {
- setRange(
- capture,
- new Range(
- range.start,
- adjustPosition(document, range.end, -whitespaceLength),
- ),
- );
- }
+ trimEnd(capture);
}
+ return true;
+ }
+}
+
+/**
+ * A predicate that behaves the same as {@link TrimEnd} but allows the node to be missing.
+ */
+class TrimEndOptional extends QueryPredicateOperator {
+ name = "trim-end-optional!" as const;
+ schema = z.tuple([q.node]);
+ allowMissingNode = true;
+ run(capture: MutableQueryCapture) {
+ trimEnd(capture);
return true;
}
}
+function trimEnd(capture: MutableQueryCapture): void {
+ const { document, range } = capture;
+ const text = getNode(capture).text;
+ const whitespaceLength = text.length - text.trimEnd().length;
+
+ if (whitespaceLength > 0) {
+ setRange(
+ capture,
+ new Range(
+ range.start,
+ adjustPosition(document, range.end, -whitespaceLength),
+ ),
+ );
+ }
+}
+
/**
* A predicate operator that sets the range to the full document.
*/
@@ -367,10 +384,7 @@ class DocumentRange extends QueryPredicateOperator {
class AllowMultiple extends QueryPredicateOperator {
name = "allow-multiple!" as const;
schema = z.tuple([q.node]).rest(q.node);
-
- protected allowMissingNode(): boolean {
- return true;
- }
+ allowMissingNode = true;
run(...captures: MutableQueryCapture[]) {
for (const capture of captures) {
@@ -477,6 +491,7 @@ export const queryPredicateOperators = [
new Type(),
new NotType(),
new TrimEnd(),
+ new TrimEndOptional(),
new DocumentRange(),
new NotParentType(),
new NotChildType(),
diff --git a/resources/fixtures/recorded/languages/markdown/changeLeadingItem.yml b/resources/fixtures/recorded/languages/markdown/changeLeadingItem.yml
index 382b2f9c2b..8ccddbf0ef 100644
--- a/resources/fixtures/recorded/languages/markdown/changeLeadingItem.yml
+++ b/resources/fixtures/recorded/languages/markdown/changeLeadingItem.yml
@@ -24,9 +24,8 @@ initialState:
finalState:
documentContents: |-
- values
- - 0
- - 1
+ - 0- 1
- 2
selections:
- - anchor: {line: 2, character: 4}
- active: {line: 2, character: 4}
+ - anchor: {line: 1, character: 7}
+ active: {line: 1, character: 7}
diff --git a/resources/fixtures/recorded/languages/markdown/chuckItem4.yml b/resources/fixtures/recorded/languages/markdown/chuckItem4.yml
index 7d3fb55582..eace857961 100644
--- a/resources/fixtures/recorded/languages/markdown/chuckItem4.yml
+++ b/resources/fixtures/recorded/languages/markdown/chuckItem4.yml
@@ -23,7 +23,6 @@ finalState:
documentContents: |-
- values
- 0
-
selections:
- - anchor: {line: 2, character: 4}
- active: {line: 2, character: 4}
+ - anchor: {line: 1, character: 7}
+ active: {line: 1, character: 7}
diff --git a/resources/fixtures/scopes/markdown/collectionItem.unenclosed.iteration2.scope b/resources/fixtures/scopes/markdown/collectionItem.unenclosed.iteration2.scope
new file mode 100644
index 0000000000..cadcb8f278
--- /dev/null
+++ b/resources/fixtures/scopes/markdown/collectionItem.unenclosed.iteration2.scope
@@ -0,0 +1,12 @@
+- First item
+ Paragraph
+- Second item
+---
+
+[Content] =
+[Domain] = 0:0-2:13
+ >------------
+0| - First item
+1| Paragraph
+2| - Second item
+ -------------<
diff --git a/resources/fixtures/scopes/markdown/collectionItem.unenclosed.iteration3.scope b/resources/fixtures/scopes/markdown/collectionItem.unenclosed.iteration3.scope
new file mode 100644
index 0000000000..29b310d790
--- /dev/null
+++ b/resources/fixtures/scopes/markdown/collectionItem.unenclosed.iteration3.scope
@@ -0,0 +1,22 @@
+- [ ] First item
+- [ ] Second item
+---
+
+[#1 Content] =
+[#1 Domain] = 0:0-1:17
+ >----------------
+0| - [ ] First item
+1| - [ ] Second item
+ -----------------<
+
+
+[#2 Content] =
+[#2 Domain] = 0:3-0:4
+ >-<
+0| - [ ] First item
+
+
+[#3 Content] =
+[#3 Domain] = 1:3-1:4
+ >-<
+1| - [ ] Second item
diff --git a/resources/fixtures/scopes/markdown/collectionItem.unenclosed.multiLine.scope b/resources/fixtures/scopes/markdown/collectionItem.unenclosed.multiLine.scope
index bdceb3f2ab..ae66dd3c88 100644
--- a/resources/fixtures/scopes/markdown/collectionItem.unenclosed.multiLine.scope
+++ b/resources/fixtures/scopes/markdown/collectionItem.unenclosed.multiLine.scope
@@ -56,13 +56,20 @@
>----------------------------------------<
2| - This is the first level of a list again.
-[#3 Removal] =
-[#3 Domain] = 2:0-2:42
- >------------------------------------------<
+[#3 Removal] = 1:39-2:42
+ >
+1| - This is the second level of a list.
2| - This is the first level of a list again.
+ ------------------------------------------<
-[#3 Leading delimiter] = 2:0-2:0
- ><
+[#3 Leading delimiter] = 1:39-2:0
+ >
+1| - This is the second level of a list.
+2| - This is the first level of a list again.
+ <
+
+[#3 Domain] = 2:0-2:42
+ >------------------------------------------<
2| - This is the first level of a list again.
[#3 Insertion delimiter] = "\n"
diff --git a/resources/fixtures/scopes/markdown/collectionItem.unenclosed.multiLine2.scope b/resources/fixtures/scopes/markdown/collectionItem.unenclosed.multiLine2.scope
new file mode 100644
index 0000000000..48f0c8e06a
--- /dev/null
+++ b/resources/fixtures/scopes/markdown/collectionItem.unenclosed.multiLine2.scope
@@ -0,0 +1,54 @@
+- First item
+ Paragraph
+- Second item
+---
+
+[#1 Content] = 0:2-1:11
+ >----------
+0| - First item
+1| Paragraph
+ -----------<
+
+[#1 Removal] = 0:0-2:0
+ >------------
+0| - First item
+1| Paragraph
+2| - Second item
+ <
+
+[#1 Trailing delimiter] = 1:11-2:0
+ >
+1| Paragraph
+2| - Second item
+ <
+
+[#1 Domain] = 0:0-1:11
+ >------------
+0| - First item
+1| Paragraph
+ -----------<
+
+[#1 Insertion delimiter] = "\n"
+
+
+[#2 Content] = 2:2-2:13
+ >-----------<
+2| - Second item
+
+[#2 Removal] = 1:11-2:13
+ >
+1| Paragraph
+2| - Second item
+ -------------<
+
+[#2 Leading delimiter] = 1:11-2:0
+ >
+1| Paragraph
+2| - Second item
+ <
+
+[#2 Domain] = 2:0-2:13
+ >-------------<
+2| - Second item
+
+[#2 Insertion delimiter] = "\n"
diff --git a/resources/fixtures/scopes/markdown/collectionItem.unenclosed.multiLine3.scope b/resources/fixtures/scopes/markdown/collectionItem.unenclosed.multiLine3.scope
new file mode 100644
index 0000000000..fb8a66d772
--- /dev/null
+++ b/resources/fixtures/scopes/markdown/collectionItem.unenclosed.multiLine3.scope
@@ -0,0 +1,66 @@
+- [ ] First item
+- [ ] Second item
+---
+
+[#1 Content] = 0:2-0:16
+ >--------------<
+0| - [ ] First item
+
+[#1 Removal] = 0:0-1:0
+ >----------------
+0| - [ ] First item
+1| - [ ] Second item
+ <
+
+[#1 Trailing delimiter] = 0:16-1:0
+ >
+0| - [ ] First item
+1| - [ ] Second item
+ <
+
+[#1 Domain] = 0:0-0:16
+ >----------------<
+0| - [ ] First item
+
+[#1 Insertion delimiter] = "\n"
+
+
+[#2 Content] =
+[#2 Removal] =
+[#2 Domain] = 0:3-0:4
+ >-<
+0| - [ ] First item
+
+[#2 Insertion delimiter] = ", "
+
+
+[#3 Content] = 1:2-1:17
+ >---------------<
+1| - [ ] Second item
+
+[#3 Removal] = 0:16-1:17
+ >
+0| - [ ] First item
+1| - [ ] Second item
+ -----------------<
+
+[#3 Leading delimiter] = 0:16-1:0
+ >
+0| - [ ] First item
+1| - [ ] Second item
+ <
+
+[#3 Domain] = 1:0-1:17
+ >-----------------<
+1| - [ ] Second item
+
+[#3 Insertion delimiter] = "\n"
+
+
+[#4 Content] =
+[#4 Removal] =
+[#4 Domain] = 1:3-1:4
+ >-<
+1| - [ ] Second item
+
+[#4 Insertion delimiter] = ", "
diff --git a/resources/queries/markdown.scm b/resources/queries/markdown.scm
index daa5b46a84..cb23ecb077 100644
--- a/resources/queries/markdown.scm
+++ b/resources/queries/markdown.scm
@@ -20,18 +20,21 @@
(#shrink-to-match! @name "^\\s*(?.*)$")
) @name.domain
-;;!! - 0
-;;! ^
-;;! ---
+;;!! - Hello world
+;;! ^^^^^^^^^^^
+
(list
(list_item)? @collectionItem.leading.endOf
.
(list_item
+ .
(_) @collectionItem.prefix
- (paragraph) @collectionItem.start.startOf
+ .
+ (_) @collectionItem.start.startOf
) @collectionItem.end.endOf @collectionItem.domain
.
(list_item)? @collectionItem.trailing.startOf
+ (#trim-end-optional! @collectionItem.leading.endOf)
(#trim-end! @collectionItem.end.endOf)
(#trim-end! @collectionItem.domain)
(#insertion-delimiter! @collectionItem.start.startOf "\n")