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")