diff --git a/changelog/2025-01-addedCollectionItemScopeProvider.md b/changelog/2025-01-addedCollectionItemScopeProvider.md new file mode 100644 index 0000000000..37d26d2390 --- /dev/null +++ b/changelog/2025-01-addedCollectionItemScopeProvider.md @@ -0,0 +1,6 @@ +--- +tags: [enhancement] +pullRequest: 2683 +--- + +At long last, collection items have been migrated to our next generation scope framework! This means, within a list of items, you can now use relative navigation (`previous item`), absolute navigation via ordinals (`fifth item`), multiple selection (`two items`, optionally preceded with `previous` or `next`), and lastly, requesting multiple items to be individually selected via `every` (`every two items`)! diff --git a/data/fixtures/recorded/itemTextual/chuckItem4.yml b/data/fixtures/recorded/itemTextual/chuckItem4.yml deleted file mode 100644 index fbbe2a17cf..0000000000 --- a/data/fixtures/recorded/itemTextual/chuckItem4.yml +++ /dev/null @@ -1,31 +0,0 @@ -languageId: plaintext -command: - version: 6 - spokenForm: chuck item - action: - name: remove - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - usePrePhraseSnapshot: true -initialState: - documentContents: |- - [ - foo, - bar, - baz, - ] - selections: - - anchor: {line: 2, character: 8} - active: {line: 2, character: 7} - marks: {} -finalState: - documentContents: |- - [ - foo, - ] - selections: - - anchor: {line: 1, character: 7} - active: {line: 1, character: 7} diff --git a/data/fixtures/recorded/itemTextual/clearEveryItemToken.yml b/data/fixtures/recorded/itemTextual/clearEveryItemToken.yml deleted file mode 100644 index c706e90534..0000000000 --- a/data/fixtures/recorded/itemTextual/clearEveryItemToken.yml +++ /dev/null @@ -1,25 +0,0 @@ -languageId: plaintext -command: - version: 6 - spokenForm: change every item token - action: - name: clearAndSetSelection - target: - type: primitive - modifiers: - - type: everyScope - scopeType: {type: collectionItem} - - type: containingScope - scopeType: {type: token} - usePrePhraseSnapshot: true -initialState: - documentContents: aaa bbb, ccc - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} - marks: {} -finalState: - documentContents: ", ccc" - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/itemTextual/clearItem10.yml b/data/fixtures/recorded/itemTextual/clearItem10.yml deleted file mode 100644 index 7bd7c2b453..0000000000 --- a/data/fixtures/recorded/itemTextual/clearItem10.yml +++ /dev/null @@ -1,23 +0,0 @@ -languageId: typescript -command: - version: 6 - spokenForm: change item - action: - name: clearAndSetSelection - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - usePrePhraseSnapshot: false -initialState: - documentContents: foo(hello, world) - selections: - - anchor: {line: 0, character: 10} - active: {line: 0, character: 10} - marks: {} -finalState: - documentContents: foo(hello, ) - selections: - - anchor: {line: 0, character: 11} - active: {line: 0, character: 11} diff --git a/data/fixtures/recorded/itemTextual/clearItem11.yml b/data/fixtures/recorded/itemTextual/clearItem11.yml deleted file mode 100644 index 52ad8ebe0d..0000000000 --- a/data/fixtures/recorded/itemTextual/clearItem11.yml +++ /dev/null @@ -1,23 +0,0 @@ -languageId: typescript -command: - version: 6 - spokenForm: change item - action: - name: clearAndSetSelection - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - usePrePhraseSnapshot: false -initialState: - documentContents: foo(hello, world) - selections: - - anchor: {line: 0, character: 7} - active: {line: 0, character: 13} - marks: {} -finalState: - documentContents: foo() - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/itemTextual/clearItem13.yml b/data/fixtures/recorded/itemTextual/clearItem13.yml deleted file mode 100644 index 692134407a..0000000000 --- a/data/fixtures/recorded/itemTextual/clearItem13.yml +++ /dev/null @@ -1,23 +0,0 @@ -languageId: plaintext -command: - version: 6 - spokenForm: change item - action: - name: clearAndSetSelection - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - usePrePhraseSnapshot: true -initialState: - documentContents: "[1, [2, 3]];" - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 5} - marks: {} -finalState: - documentContents: "[1, ];" - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/itemTextual/clearItem14.yml b/data/fixtures/recorded/itemTextual/clearItem14.yml deleted file mode 100644 index fc438d0268..0000000000 --- a/data/fixtures/recorded/itemTextual/clearItem14.yml +++ /dev/null @@ -1,23 +0,0 @@ -languageId: plaintext -command: - version: 6 - spokenForm: change item - action: - name: clearAndSetSelection - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - usePrePhraseSnapshot: true -initialState: - documentContents: "[1, [2, 3]];" - selections: - - anchor: {line: 0, character: 10} - active: {line: 0, character: 9} - marks: {} -finalState: - documentContents: "[1, ];" - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/itemTextual/clearItem6.yml b/data/fixtures/recorded/itemTextual/clearItem6.yml deleted file mode 100644 index fd0471d915..0000000000 --- a/data/fixtures/recorded/itemTextual/clearItem6.yml +++ /dev/null @@ -1,23 +0,0 @@ -languageId: plaintext -command: - version: 6 - spokenForm: change item - action: - name: clearAndSetSelection - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - usePrePhraseSnapshot: true -initialState: - documentContents: aaa aaa - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} - marks: {} -finalState: - documentContents: "" - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/itemTextual/clearItemDrip.yml b/data/fixtures/recorded/itemTextual/clearItemDrip.yml deleted file mode 100644 index 545936cc75..0000000000 --- a/data/fixtures/recorded/itemTextual/clearItemDrip.yml +++ /dev/null @@ -1,27 +0,0 @@ -languageId: typescript -command: - version: 6 - spokenForm: change item comma - action: - name: clearAndSetSelection - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - mark: {type: decoratedSymbol, symbolColor: default, character: ','} - usePrePhraseSnapshot: true -initialState: - documentContents: foo(hello, world) - selections: - - anchor: {line: 0, character: 13} - active: {line: 0, character: 13} - marks: - default.,: - start: {line: 0, character: 9} - end: {line: 0, character: 10} -finalState: - documentContents: foo() - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/languages/clojure/chuckItemZip.yml b/data/fixtures/recorded/languages/clojure/chuckItemZip.yml index 3972d6f0ea..6e5c8a77d5 100644 --- a/data/fixtures/recorded/languages/clojure/chuckItemZip.yml +++ b/data/fixtures/recorded/languages/clojure/chuckItemZip.yml @@ -29,9 +29,7 @@ finalState: documentContents: |- { :foo "bar", - ;; hello - , } selections: - - anchor: {line: 4, character: 1} - active: {line: 4, character: 1} + - anchor: {line: 2, character: 1} + active: {line: 2, character: 1} diff --git a/data/fixtures/recorded/languages/clojure/clearEveryItem.yml b/data/fixtures/recorded/languages/clojure/clearEveryItem.yml index b4202900fd..99f7622d33 100644 --- a/data/fixtures/recorded/languages/clojure/clearEveryItem.yml +++ b/data/fixtures/recorded/languages/clojure/clearEveryItem.yml @@ -27,11 +27,10 @@ finalState: { , - ;; hello , } selections: - anchor: {line: 2, character: 4} active: {line: 2, character: 4} - - anchor: {line: 4, character: 4} - active: {line: 4, character: 4} + - anchor: {line: 3, character: 4} + active: {line: 3, character: 4} diff --git a/data/fixtures/recorded/languages/clojure/clearItem.yml b/data/fixtures/recorded/languages/clojure/clearItem.yml index c03ec79ef5..b956e1fa9e 100644 --- a/data/fixtures/recorded/languages/clojure/clearItem.yml +++ b/data/fixtures/recorded/languages/clojure/clearItem.yml @@ -26,8 +26,11 @@ initialState: finalState: documentContents: |- { - + :bongo { + :foo "bar", + , + } } selections: - - anchor: {line: 1, character: 4} - active: {line: 1, character: 4} + - anchor: {line: 3, character: 8} + active: {line: 3, character: 8} diff --git a/data/fixtures/recorded/languages/clojure/clearItemFine.yml b/data/fixtures/recorded/languages/clojure/clearItemFine.yml index df2f19e0e8..56bfdc6554 100644 --- a/data/fixtures/recorded/languages/clojure/clearItemFine.yml +++ b/data/fixtures/recorded/languages/clojure/clearItemFine.yml @@ -21,7 +21,7 @@ initialState: start: {line: 0, character: 2} end: {line: 0, character: 5} finalState: - documentContents: "{ :baz \"whatever\"}" + documentContents: "{}" selections: - anchor: {line: 0, character: 1} active: {line: 0, character: 1} diff --git a/data/fixtures/recorded/languages/rust/changeItemOne.yml b/data/fixtures/recorded/languages/rust/changeItemOne.yml index 34b4d26b62..556b73e8b8 100644 --- a/data/fixtures/recorded/languages/rust/changeItemOne.yml +++ b/data/fixtures/recorded/languages/rust/changeItemOne.yml @@ -23,7 +23,7 @@ initialState: end: {line: 0, character: 21} finalState: documentContents: | - let x = [None, ]; + let x = [None, Some()]; selections: - - anchor: {line: 0, character: 15} - active: {line: 0, character: 15} + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} diff --git a/data/fixtures/recorded/languages/typescript/takeItem4.yml b/data/fixtures/recorded/languages/typescript/takeItem4.yml deleted file mode 100644 index 675da100a1..0000000000 --- a/data/fixtures/recorded/languages/typescript/takeItem4.yml +++ /dev/null @@ -1,27 +0,0 @@ -languageId: typescript -command: - version: 6 - spokenForm: take item - action: - name: setSelection - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - usePrePhraseSnapshot: false -initialState: - documentContents: | - - const value = { a: 1, b: 2, c: 3 }; - selections: - - anchor: {line: 1, character: 21} - active: {line: 1, character: 21} - marks: {} -finalState: - documentContents: | - - const value = { a: 1, b: 2, c: 3 }; - selections: - - anchor: {line: 1, character: 22} - active: {line: 1, character: 26} diff --git a/data/fixtures/recorded/languages/typescript/takeItemComma.yml b/data/fixtures/recorded/languages/typescript/takeItemComma.yml deleted file mode 100644 index 8cadeffc69..0000000000 --- a/data/fixtures/recorded/languages/typescript/takeItemComma.yml +++ /dev/null @@ -1,31 +0,0 @@ -languageId: typescript -command: - version: 6 - spokenForm: take item comma - action: - name: setSelection - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - mark: {type: decoratedSymbol, symbolColor: default, character: ','} - usePrePhraseSnapshot: false -initialState: - documentContents: | - - const value = { a: 1, b: 2, c: 3 }; - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} - marks: - default.,: - start: {line: 1, character: 20} - end: {line: 1, character: 21} -finalState: - documentContents: | - - const value = { a: 1, b: 2, c: 3 }; - selections: - - anchor: {line: 1, character: 16} - active: {line: 1, character: 26} diff --git a/data/fixtures/scopes/javascript.core/collectionItem.unenclosed.scope b/data/fixtures/scopes/javascript.core/collectionItem.unenclosed.scope index 703a4b32bb..12cf5428b2 100644 --- a/data/fixtures/scopes/javascript.core/collectionItem.unenclosed.scope +++ b/data/fixtures/scopes/javascript.core/collectionItem.unenclosed.scope @@ -2,12 +2,12 @@ let foo, bar; --- [#1 Content] = -[#1 Domain] = 0:4-0:7 - >---< +[#1 Domain] = 0:0-0:7 + >-------< 0| let foo, bar; -[#1 Removal] = 0:4-0:9 - >-----< +[#1 Removal] = 0:0-0:9 + >---------< 0| let foo, bar; [#1 Trailing delimiter] = 0:7-0:9 @@ -18,16 +18,48 @@ let foo, bar; [#2 Content] = -[#2 Domain] = 0:9-0:12 +[#2 Domain] = 0:4-0:7 + >---< +0| let foo, bar; + +[#2 Removal] = 0:4-0:9 + >-----< +0| let foo, bar; + +[#2 Trailing delimiter] = 0:7-0:9 + >--< +0| let foo, bar; + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:9-0:12 >---< 0| let foo, bar; -[#2 Removal] = 0:7-0:12 +[#3 Removal] = 0:7-0:12 >-----< 0| let foo, bar; -[#2 Leading delimiter] = 0:7-0:9 +[#3 Leading delimiter] = 0:7-0:9 >--< 0| let foo, bar; -[#2 Insertion delimiter] = ", " +[#3 Insertion delimiter] = ", " + + +[#4 Content] = +[#4 Domain] = 0:9-0:13 + >----< +0| let foo, bar; + +[#4 Removal] = 0:7-0:13 + >------< +0| let foo, bar; + +[#4 Leading delimiter] = 0:7-0:9 + >--< +0| let foo, bar; + +[#4 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/python/collectionItem.unenclosed.iteration3.scope b/data/fixtures/scopes/python/collectionItem.unenclosed.iteration3.scope index d24c8b8d1a..badf04bc7e 100644 --- a/data/fixtures/scopes/python/collectionItem.unenclosed.iteration3.scope +++ b/data/fixtures/scopes/python/collectionItem.unenclosed.iteration3.scope @@ -2,10 +2,25 @@ def foo(): global bar, baz --- -[Range] = 1:11-1:19 +[#1 Range] = +[#1 Domain] = 0:8-0:8 + >< +0| def foo(): + + +[#2 Range] = 1:4-1:19 + >---------------< +1| global bar, baz + +[#2 Domain] = 1:0-1:19 + >-------------------< +1| global bar, baz + + +[#3 Range] = 1:11-1:19 >--------< 1| global bar, baz -[Domain] = 1:4-1:19 +[#3 Domain] = 1:4-1:19 >---------------< 1| global bar, baz diff --git a/data/fixtures/scopes/python/collectionItem.unenclosed.iteration4.scope b/data/fixtures/scopes/python/collectionItem.unenclosed.iteration4.scope index 79ad18b890..c5748186a1 100644 --- a/data/fixtures/scopes/python/collectionItem.unenclosed.iteration4.scope +++ b/data/fixtures/scopes/python/collectionItem.unenclosed.iteration4.scope @@ -2,7 +2,13 @@ for key, value in map.items(): pass --- -[Range] = -[Domain] = 0:4-0:14 +[#1 Range] = +[#1 Domain] = 0:4-0:14 >----------< 0| for key, value in map.items(): + + +[#2 Range] = +[#2 Domain] = 0:28-0:28 + >< +0| for key, value in map.items(): diff --git a/data/fixtures/scopes/python/collectionItem.unenclosed2.scope b/data/fixtures/scopes/python/collectionItem.unenclosed2.scope index fa8b5b2e11..ee8ba44d8e 100644 --- a/data/fixtures/scopes/python/collectionItem.unenclosed2.scope +++ b/data/fixtures/scopes/python/collectionItem.unenclosed2.scope @@ -2,12 +2,12 @@ import foo, bar --- [#1 Content] = -[#1 Domain] = 0:7-0:10 - >---< +[#1 Domain] = 0:0-0:10 + >----------< 0| import foo, bar -[#1 Removal] = 0:7-0:12 - >-----< +[#1 Removal] = 0:0-0:12 + >------------< 0| import foo, bar [#1 Trailing delimiter] = 0:10-0:12 @@ -18,16 +18,32 @@ import foo, bar [#2 Content] = -[#2 Domain] = 0:12-0:15 +[#2 Domain] = 0:7-0:10 + >---< +0| import foo, bar + +[#2 Removal] = 0:7-0:12 + >-----< +0| import foo, bar + +[#2 Trailing delimiter] = 0:10-0:12 + >--< +0| import foo, bar + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:12-0:15 >---< 0| import foo, bar -[#2 Removal] = 0:10-0:15 +[#3 Removal] = 0:10-0:15 >-----< 0| import foo, bar -[#2 Leading delimiter] = 0:10-0:12 +[#3 Leading delimiter] = 0:10-0:12 >--< 0| import foo, bar -[#2 Insertion delimiter] = ", " +[#3 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/python/collectionItem.unenclosed5.scope b/data/fixtures/scopes/python/collectionItem.unenclosed5.scope index 0ebe039ba3..f595441373 100644 --- a/data/fixtures/scopes/python/collectionItem.unenclosed5.scope +++ b/data/fixtures/scopes/python/collectionItem.unenclosed5.scope @@ -2,36 +2,52 @@ from foo import bar, baz --- [#1 Content] = -[#1 Domain] = 0:16-0:19 +[#1 Domain] = 0:0-0:19 + >-------------------< +0| from foo import bar, baz + +[#1 Removal] = 0:0-0:21 + >---------------------< +0| from foo import bar, baz + +[#1 Trailing delimiter] = 0:19-0:21 + >--< +0| from foo import bar, baz + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:16-0:19 >---< 0| from foo import bar, baz -[#1 Removal] = 0:16-0:21 +[#2 Removal] = 0:16-0:21 >-----< 0| from foo import bar, baz -[#1 Leading delimiter] = 0:15-0:16 +[#2 Leading delimiter] = 0:15-0:16 >-< 0| from foo import bar, baz -[#1 Trailing delimiter] = 0:19-0:21 +[#2 Trailing delimiter] = 0:19-0:21 >--< 0| from foo import bar, baz -[#1 Insertion delimiter] = ", " +[#2 Insertion delimiter] = ", " -[#2 Content] = -[#2 Domain] = 0:21-0:24 +[#3 Content] = +[#3 Domain] = 0:21-0:24 >---< 0| from foo import bar, baz -[#2 Removal] = 0:19-0:24 +[#3 Removal] = 0:19-0:24 >-----< 0| from foo import bar, baz -[#2 Leading delimiter] = 0:19-0:21 +[#3 Leading delimiter] = 0:19-0:21 >--< 0| from foo import bar, baz -[#2 Insertion delimiter] = ", " +[#3 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/python/collectionItem.unenclosed6.scope b/data/fixtures/scopes/python/collectionItem.unenclosed6.scope index 4b5eda3fd4..b4aa4158f1 100644 --- a/data/fixtures/scopes/python/collectionItem.unenclosed6.scope +++ b/data/fixtures/scopes/python/collectionItem.unenclosed6.scope @@ -3,12 +3,12 @@ def foo(): --- [#1 Content] = -[#1 Domain] = 1:11-1:14 - >---< +[#1 Domain] = 1:4-1:14 + >----------< 1| global bar, baz -[#1 Removal] = 1:11-1:16 - >-----< +[#1 Removal] = 1:4-1:16 + >------------< 1| global bar, baz [#1 Trailing delimiter] = 1:14-1:16 @@ -19,16 +19,32 @@ def foo(): [#2 Content] = -[#2 Domain] = 1:16-1:19 +[#2 Domain] = 1:11-1:14 + >---< +1| global bar, baz + +[#2 Removal] = 1:11-1:16 + >-----< +1| global bar, baz + +[#2 Trailing delimiter] = 1:14-1:16 + >--< +1| global bar, baz + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 1:16-1:19 >---< 1| global bar, baz -[#2 Removal] = 1:14-1:19 +[#3 Removal] = 1:14-1:19 >-----< 1| global bar, baz -[#2 Leading delimiter] = 1:14-1:16 +[#3 Leading delimiter] = 1:14-1:16 >--< 1| global bar, baz -[#2 Insertion delimiter] = ", " +[#3 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/python/collectionItem.unenclosed7.scope b/data/fixtures/scopes/python/collectionItem.unenclosed7.scope index 9f30c7a870..30844ef51c 100644 --- a/data/fixtures/scopes/python/collectionItem.unenclosed7.scope +++ b/data/fixtures/scopes/python/collectionItem.unenclosed7.scope @@ -3,12 +3,12 @@ for key, value in map.items(): --- [#1 Content] = -[#1 Domain] = 0:4-0:7 - >---< +[#1 Domain] = 0:0-0:7 + >-------< 0| for key, value in map.items(): -[#1 Removal] = 0:4-0:9 - >-----< +[#1 Removal] = 0:0-0:9 + >---------< 0| for key, value in map.items(): [#1 Trailing delimiter] = 0:7-0:9 @@ -19,16 +19,48 @@ for key, value in map.items(): [#2 Content] = -[#2 Domain] = 0:9-0:14 +[#2 Domain] = 0:4-0:7 + >---< +0| for key, value in map.items(): + +[#2 Removal] = 0:4-0:9 + >-----< +0| for key, value in map.items(): + +[#2 Trailing delimiter] = 0:7-0:9 + >--< +0| for key, value in map.items(): + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:9-0:14 >-----< 0| for key, value in map.items(): -[#2 Removal] = 0:7-0:14 +[#3 Removal] = 0:7-0:14 >-------< 0| for key, value in map.items(): -[#2 Leading delimiter] = 0:7-0:9 +[#3 Leading delimiter] = 0:7-0:9 >--< 0| for key, value in map.items(): -[#2 Insertion delimiter] = ", " +[#3 Insertion delimiter] = ", " + + +[#4 Content] = +[#4 Domain] = 0:9-0:30 + >---------------------< +0| for key, value in map.items(): + +[#4 Removal] = 0:7-0:30 + >-----------------------< +0| for key, value in map.items(): + +[#4 Leading delimiter] = 0:7-0:9 + >--< +0| for key, value in map.items(): + +[#4 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual.iteration.scope b/data/fixtures/scopes/textual/collectionItem.textual.iteration.scope new file mode 100644 index 0000000000..9cced361b5 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual.iteration.scope @@ -0,0 +1,7 @@ +(1, 2, 3) +--- + +[Range] = +[Domain] = 0:1-0:8 + >-------< +0| (1, 2, 3) diff --git a/data/fixtures/scopes/textual/collectionItem.textual.iteration2.scope b/data/fixtures/scopes/textual/collectionItem.textual.iteration2.scope new file mode 100644 index 0000000000..09af92177c --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual.iteration2.scope @@ -0,0 +1,7 @@ +[1, 2, 3] +--- + +[Range] = +[Domain] = 0:1-0:8 + >-------< +0| [1, 2, 3] diff --git a/data/fixtures/scopes/textual/collectionItem.textual.iteration3.scope b/data/fixtures/scopes/textual/collectionItem.textual.iteration3.scope new file mode 100644 index 0000000000..06f24d63a9 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual.iteration3.scope @@ -0,0 +1,7 @@ +{1, 2, 3} +--- + +[Range] = +[Domain] = 0:1-0:8 + >-------< +0| {1, 2, 3} diff --git a/data/fixtures/scopes/textual/collectionItem.textual.iteration4.scope b/data/fixtures/scopes/textual/collectionItem.textual.iteration4.scope new file mode 100644 index 0000000000..81c9d30a35 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual.iteration4.scope @@ -0,0 +1,7 @@ +<1, 2, 3> +--- + +[Range] = +[Domain] = 0:1-0:8 + >-------< +0| <1, 2, 3> diff --git a/data/fixtures/scopes/textual/collectionItem.textual.iteration5.scope b/data/fixtures/scopes/textual/collectionItem.textual.iteration5.scope new file mode 100644 index 0000000000..166cce77cd --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual.iteration5.scope @@ -0,0 +1,10 @@ +( 1, 2, 3 ) +--- + +[Range] = 0:2-0:9 + >-------< +0| ( 1, 2, 3 ) + +[Domain] = 0:1-0:10 + >---------< +0| ( 1, 2, 3 ) diff --git a/data/fixtures/scopes/textual/collectionItem.textual.iteration6.scope b/data/fixtures/scopes/textual/collectionItem.textual.iteration6.scope new file mode 100644 index 0000000000..996188a6e7 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual.iteration6.scope @@ -0,0 +1,49 @@ +[ + 1, + 2, + 3, +] +--- + +[#1 Range] = 1:4-3:6 + >-- +1| 1, +2| 2, +3| 3, + ------< + +[#1 Domain] = 0:1-4:0 + > +0| [ +1| 1, +2| 2, +3| 3, +4| ] + < + + +[#2 Range] = 1:4-1:6 + >--< +1| 1, + +[#2 Domain] = 1:0-1:6 + >------< +1| 1, + + +[#3 Range] = 2:4-2:6 + >--< +2| 2, + +[#3 Domain] = 2:0-2:6 + >------< +2| 2, + + +[#4 Range] = 3:4-3:6 + >--< +3| 3, + +[#4 Domain] = 3:0-3:6 + >------< +3| 3, diff --git a/data/fixtures/scopes/textual/collectionItem.textual.iteration7.scope b/data/fixtures/scopes/textual/collectionItem.textual.iteration7.scope new file mode 100644 index 0000000000..5f081d0268 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual.iteration7.scope @@ -0,0 +1,49 @@ +{ + a: 1, + b: 2, + c: 3, +} +--- + +[#1 Range] = 1:4-3:9 + >----- +1| a: 1, +2| b: 2, +3| c: 3, + ---------< + +[#1 Domain] = 0:1-4:0 + > +0| { +1| a: 1, +2| b: 2, +3| c: 3, +4| } + < + + +[#2 Range] = 1:4-1:9 + >-----< +1| a: 1, + +[#2 Domain] = 1:0-1:9 + >---------< +1| a: 1, + + +[#3 Range] = 2:4-2:9 + >-----< +2| b: 2, + +[#3 Domain] = 2:0-2:9 + >---------< +2| b: 2, + + +[#4 Range] = 3:4-3:9 + >-----< +3| c: 3, + +[#4 Domain] = 3:0-3:9 + >---------< +3| c: 3, diff --git a/data/fixtures/scopes/textual/collectionItem.textual.scope b/data/fixtures/scopes/textual/collectionItem.textual.scope new file mode 100644 index 0000000000..b6fb913f1e --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual.scope @@ -0,0 +1,53 @@ +(1, 2, 3) +--- + +[#1 Content] = +[#1 Domain] = 0:1-0:2 + >-< +0| (1, 2, 3) + +[#1 Removal] = 0:1-0:4 + >---< +0| (1, 2, 3) + +[#1 Trailing delimiter] = 0:2-0:4 + >--< +0| (1, 2, 3) + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:4-0:5 + >-< +0| (1, 2, 3) + +[#2 Removal] = 0:4-0:7 + >---< +0| (1, 2, 3) + +[#2 Leading delimiter] = 0:2-0:4 + >--< +0| (1, 2, 3) + +[#2 Trailing delimiter] = 0:5-0:7 + >--< +0| (1, 2, 3) + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:7-0:8 + >-< +0| (1, 2, 3) + +[#3 Removal] = 0:5-0:8 + >---< +0| (1, 2, 3) + +[#3 Leading delimiter] = 0:5-0:7 + >--< +0| (1, 2, 3) + +[#3 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual10.scope b/data/fixtures/scopes/textual/collectionItem.textual10.scope new file mode 100644 index 0000000000..ecdbbf9473 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual10.scope @@ -0,0 +1,65 @@ +aaa, ( bbb, ccc ) +--- + +[#1 Content] = +[#1 Domain] = 0:0-0:3 + >---< +0| aaa, ( bbb, ccc ) + +[#1 Removal] = 0:0-0:5 + >-----< +0| aaa, ( bbb, ccc ) + +[#1 Trailing delimiter] = 0:3-0:5 + >--< +0| aaa, ( bbb, ccc ) + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:5-0:17 + >------------< +0| aaa, ( bbb, ccc ) + +[#2 Removal] = 0:3-0:17 + >--------------< +0| aaa, ( bbb, ccc ) + +[#2 Leading delimiter] = 0:3-0:5 + >--< +0| aaa, ( bbb, ccc ) + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:7-0:10 + >---< +0| aaa, ( bbb, ccc ) + +[#3 Removal] = 0:7-0:12 + >-----< +0| aaa, ( bbb, ccc ) + +[#3 Trailing delimiter] = 0:10-0:12 + >--< +0| aaa, ( bbb, ccc ) + +[#3 Insertion delimiter] = ", " + + +[#4 Content] = +[#4 Domain] = 0:12-0:15 + >---< +0| aaa, ( bbb, ccc ) + +[#4 Removal] = 0:10-0:15 + >-----< +0| aaa, ( bbb, ccc ) + +[#4 Leading delimiter] = 0:10-0:12 + >--< +0| aaa, ( bbb, ccc ) + +[#4 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual11.scope b/data/fixtures/scopes/textual/collectionItem.textual11.scope new file mode 100644 index 0000000000..cc98d05466 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual11.scope @@ -0,0 +1,53 @@ +[ + 1, + + 2, +] +--- + +[#1 Content] = +[#1 Domain] = 1:4-1:5 + >-< +1| 1, + +[#1 Removal] = 1:4-3:4 + >-- +1| 1, +2| +3| 2, + ----< + +[#1 Trailing delimiter] = 1:5-3:4 + >- +1| 1, +2| +3| 2, + ----< + +[#1 Insertion delimiter] = ",\n" + + +[#2 Content] = +[#2 Domain] = 3:4-3:5 + >-< +3| 2, + +[#2 Removal] = 1:5-3:5 + >- +1| 1, +2| +3| 2, + -----< + +[#2 Leading delimiter] = 1:5-3:4 + >- +1| 1, +2| +3| 2, + ----< + +[#2 Trailing delimiter] = 3:5-3:6 + >-< +3| 2, + +[#2 Insertion delimiter] = ",\n" diff --git a/data/fixtures/scopes/textual/collectionItem.textual12.scope b/data/fixtures/scopes/textual/collectionItem.textual12.scope new file mode 100644 index 0000000000..441a9eecad --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual12.scope @@ -0,0 +1,21 @@ +[ + + 1 + +] +--- + +[Content] = +[Domain] = 2:4-2:5 + >-< +2| 1 + +[Removal] = 2:0-2:5 + >-----< +2| 1 + +[Leading delimiter] = 2:0-2:4 + >----< +2| 1 + +[Insertion delimiter] = ",\n" diff --git a/data/fixtures/scopes/textual/collectionItem.textual13.scope b/data/fixtures/scopes/textual/collectionItem.textual13.scope new file mode 100644 index 0000000000..075adf3633 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual13.scope @@ -0,0 +1,10 @@ +(aaa) +--- + +[Content] = +[Removal] = +[Domain] = 0:1-0:4 + >---< +0| (aaa) + +[Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual14.scope b/data/fixtures/scopes/textual/collectionItem.textual14.scope new file mode 100644 index 0000000000..2e3a109339 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual14.scope @@ -0,0 +1,21 @@ +( aaa ) +--- + +[Content] = +[Domain] = 0:2-0:5 + >---< +0| ( aaa ) + +[Removal] = 0:2-0:6 + >----< +0| ( aaa ) + +[Leading delimiter] = 0:1-0:2 + >-< +0| ( aaa ) + +[Trailing delimiter] = 0:5-0:6 + >-< +0| ( aaa ) + +[Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual2.scope b/data/fixtures/scopes/textual/collectionItem.textual2.scope new file mode 100644 index 0000000000..885304a300 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual2.scope @@ -0,0 +1,53 @@ +[1, 2, 3] +--- + +[#1 Content] = +[#1 Domain] = 0:1-0:2 + >-< +0| [1, 2, 3] + +[#1 Removal] = 0:1-0:4 + >---< +0| [1, 2, 3] + +[#1 Trailing delimiter] = 0:2-0:4 + >--< +0| [1, 2, 3] + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:4-0:5 + >-< +0| [1, 2, 3] + +[#2 Removal] = 0:4-0:7 + >---< +0| [1, 2, 3] + +[#2 Leading delimiter] = 0:2-0:4 + >--< +0| [1, 2, 3] + +[#2 Trailing delimiter] = 0:5-0:7 + >--< +0| [1, 2, 3] + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:7-0:8 + >-< +0| [1, 2, 3] + +[#3 Removal] = 0:5-0:8 + >---< +0| [1, 2, 3] + +[#3 Leading delimiter] = 0:5-0:7 + >--< +0| [1, 2, 3] + +[#3 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual3.scope b/data/fixtures/scopes/textual/collectionItem.textual3.scope new file mode 100644 index 0000000000..5a5fb483bd --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual3.scope @@ -0,0 +1,53 @@ +{1, 2, 3} +--- + +[#1 Content] = +[#1 Domain] = 0:1-0:2 + >-< +0| {1, 2, 3} + +[#1 Removal] = 0:1-0:4 + >---< +0| {1, 2, 3} + +[#1 Trailing delimiter] = 0:2-0:4 + >--< +0| {1, 2, 3} + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:4-0:5 + >-< +0| {1, 2, 3} + +[#2 Removal] = 0:4-0:7 + >---< +0| {1, 2, 3} + +[#2 Leading delimiter] = 0:2-0:4 + >--< +0| {1, 2, 3} + +[#2 Trailing delimiter] = 0:5-0:7 + >--< +0| {1, 2, 3} + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:7-0:8 + >-< +0| {1, 2, 3} + +[#3 Removal] = 0:5-0:8 + >---< +0| {1, 2, 3} + +[#3 Leading delimiter] = 0:5-0:7 + >--< +0| {1, 2, 3} + +[#3 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual4.scope b/data/fixtures/scopes/textual/collectionItem.textual4.scope new file mode 100644 index 0000000000..6387887fbc --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual4.scope @@ -0,0 +1,53 @@ +<1, 2, 3> +--- + +[#1 Content] = +[#1 Domain] = 0:1-0:2 + >-< +0| <1, 2, 3> + +[#1 Removal] = 0:1-0:4 + >---< +0| <1, 2, 3> + +[#1 Trailing delimiter] = 0:2-0:4 + >--< +0| <1, 2, 3> + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:4-0:5 + >-< +0| <1, 2, 3> + +[#2 Removal] = 0:4-0:7 + >---< +0| <1, 2, 3> + +[#2 Leading delimiter] = 0:2-0:4 + >--< +0| <1, 2, 3> + +[#2 Trailing delimiter] = 0:5-0:7 + >--< +0| <1, 2, 3> + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:7-0:8 + >-< +0| <1, 2, 3> + +[#3 Removal] = 0:5-0:8 + >---< +0| <1, 2, 3> + +[#3 Leading delimiter] = 0:5-0:7 + >--< +0| <1, 2, 3> + +[#3 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual5.scope b/data/fixtures/scopes/textual/collectionItem.textual5.scope new file mode 100644 index 0000000000..9730f65458 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual5.scope @@ -0,0 +1,53 @@ +( 1, 2, 3 ) +--- + +[#1 Content] = +[#1 Domain] = 0:2-0:3 + >-< +0| ( 1, 2, 3 ) + +[#1 Removal] = 0:2-0:5 + >---< +0| ( 1, 2, 3 ) + +[#1 Trailing delimiter] = 0:3-0:5 + >--< +0| ( 1, 2, 3 ) + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:5-0:6 + >-< +0| ( 1, 2, 3 ) + +[#2 Removal] = 0:5-0:8 + >---< +0| ( 1, 2, 3 ) + +[#2 Leading delimiter] = 0:3-0:5 + >--< +0| ( 1, 2, 3 ) + +[#2 Trailing delimiter] = 0:6-0:8 + >--< +0| ( 1, 2, 3 ) + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:8-0:9 + >-< +0| ( 1, 2, 3 ) + +[#3 Removal] = 0:6-0:9 + >---< +0| ( 1, 2, 3 ) + +[#3 Leading delimiter] = 0:6-0:8 + >--< +0| ( 1, 2, 3 ) + +[#3 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual6.scope b/data/fixtures/scopes/textual/collectionItem.textual6.scope new file mode 100644 index 0000000000..b1ffcc88ca --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual6.scope @@ -0,0 +1,75 @@ +[ + 1, + 2, + 3, +] +--- + +[#1 Content] = +[#1 Domain] = 1:4-1:5 + >-< +1| 1, + +[#1 Removal] = 1:4-2:4 + >-- +1| 1, +2| 2, + ----< + +[#1 Trailing delimiter] = 1:5-2:4 + >- +1| 1, +2| 2, + ----< + +[#1 Insertion delimiter] = ",\n" + + +[#2 Content] = +[#2 Domain] = 2:4-2:5 + >-< +2| 2, + +[#2 Removal] = 2:4-3:4 + >-- +2| 2, +3| 3, + ----< + +[#2 Leading delimiter] = 1:5-2:4 + >- +1| 1, +2| 2, + ----< + +[#2 Trailing delimiter] = 2:5-3:4 + >- +2| 2, +3| 3, + ----< + +[#2 Insertion delimiter] = ",\n" + + +[#3 Content] = +[#3 Domain] = 3:4-3:5 + >-< +3| 3, + +[#3 Removal] = 2:5-3:5 + >- +2| 2, +3| 3, + -----< + +[#3 Leading delimiter] = 2:5-3:4 + >- +2| 2, +3| 3, + ----< + +[#3 Trailing delimiter] = 3:5-3:6 + >-< +3| 3, + +[#3 Insertion delimiter] = ",\n" diff --git a/data/fixtures/scopes/textual/collectionItem.textual7.scope b/data/fixtures/scopes/textual/collectionItem.textual7.scope new file mode 100644 index 0000000000..453ca9a258 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual7.scope @@ -0,0 +1,75 @@ +{ + a: 1, + b: 2, + c: 3, +} +--- + +[#1 Content] = +[#1 Domain] = 1:4-1:8 + >----< +1| a: 1, + +[#1 Removal] = 1:4-2:4 + >----- +1| a: 1, +2| b: 2, + ----< + +[#1 Trailing delimiter] = 1:8-2:4 + >- +1| a: 1, +2| b: 2, + ----< + +[#1 Insertion delimiter] = ",\n" + + +[#2 Content] = +[#2 Domain] = 2:4-2:8 + >----< +2| b: 2, + +[#2 Removal] = 2:4-3:4 + >----- +2| b: 2, +3| c: 3, + ----< + +[#2 Leading delimiter] = 1:8-2:4 + >- +1| a: 1, +2| b: 2, + ----< + +[#2 Trailing delimiter] = 2:8-3:4 + >- +2| b: 2, +3| c: 3, + ----< + +[#2 Insertion delimiter] = ",\n" + + +[#3 Content] = +[#3 Domain] = 3:4-3:8 + >----< +3| c: 3, + +[#3 Removal] = 2:8-3:8 + >- +2| b: 2, +3| c: 3, + --------< + +[#3 Leading delimiter] = 2:8-3:4 + >- +2| b: 2, +3| c: 3, + ----< + +[#3 Trailing delimiter] = 3:8-3:9 + >-< +3| c: 3, + +[#3 Insertion delimiter] = ",\n" diff --git a/data/fixtures/scopes/textual/collectionItem.textual8.scope b/data/fixtures/scopes/textual/collectionItem.textual8.scope new file mode 100644 index 0000000000..2b2cb01fd3 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual8.scope @@ -0,0 +1,33 @@ +[1, "2, 3"] +--- + +[#1 Content] = +[#1 Domain] = 0:1-0:2 + >-< +0| [1, "2, 3"] + +[#1 Removal] = 0:1-0:4 + >---< +0| [1, "2, 3"] + +[#1 Trailing delimiter] = 0:2-0:4 + >--< +0| [1, "2, 3"] + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:4-0:10 + >------< +0| [1, "2, 3"] + +[#2 Removal] = 0:2-0:10 + >--------< +0| [1, "2, 3"] + +[#2 Leading delimiter] = 0:2-0:4 + >--< +0| [1, "2, 3"] + +[#2 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual9.scope b/data/fixtures/scopes/textual/collectionItem.textual9.scope new file mode 100644 index 0000000000..cfef536609 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual9.scope @@ -0,0 +1,33 @@ +aaa, bbb +--- + +[#1 Content] = +[#1 Domain] = 0:0-0:3 + >---< +0| aaa, bbb + +[#1 Removal] = 0:0-0:5 + >-----< +0| aaa, bbb + +[#1 Trailing delimiter] = 0:3-0:5 + >--< +0| aaa, bbb + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:5-0:8 + >---< +0| aaa, bbb + +[#2 Removal] = 0:3-0:8 + >-----< +0| aaa, bbb + +[#2 Leading delimiter] = 0:3-0:5 + >--< +0| aaa, bbb + +[#2 Insertion delimiter] = ", " diff --git a/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts b/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts index 40728f4a9a..14fbdc19cf 100644 --- a/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts +++ b/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts @@ -205,7 +205,9 @@ export type TextualScopeSupportFacet = | "boundedNonWhitespaceSequence.iteration" | "url" | "surroundingPair" - | "surroundingPair.iteration"; + | "surroundingPair.iteration" + | "collectionItem.textual" + | "collectionItem.textual.iteration"; export type LanguageScopeSupportFacetMap = Partial< Record diff --git a/packages/common/src/scopeSupportFacets/textualScopeSupportFacetInfos.ts b/packages/common/src/scopeSupportFacets/textualScopeSupportFacetInfos.ts index eb933a8876..86c34b5abf 100644 --- a/packages/common/src/scopeSupportFacets/textualScopeSupportFacetInfos.ts +++ b/packages/common/src/scopeSupportFacets/textualScopeSupportFacetInfos.ts @@ -83,4 +83,17 @@ export const textualScopeSupportFacetInfos: Record< }, isIteration: true, }, + "collectionItem.textual": { + description: "A text based collection item", + scopeType: { + type: "collectionItem", + }, + }, + "collectionItem.textual.iteration": { + description: "Iteration scope for text based collection items", + scopeType: { + type: "collectionItem", + }, + isIteration: true, + }, }; diff --git a/packages/cursorless-engine/src/languages/clojure.ts b/packages/cursorless-engine/src/languages/clojure.ts index ed87759de9..4ecb9ef947 100644 --- a/packages/cursorless-engine/src/languages/clojure.ts +++ b/packages/cursorless-engine/src/languages/clojure.ts @@ -1,3 +1,7 @@ +import type { SimpleScopeTypeType } from "@cursorless/common"; +import type { SyntaxNode } from "web-tree-sitter"; +import type { NodeFinder, NodeMatcherAlternative } from "../typings/Types"; +import { patternFinder } from "../util/nodeFinders"; import { cascadingMatcher, chainedMatcher, @@ -5,13 +9,7 @@ import { matcher, patternMatcher, } from "../util/nodeMatchers"; -import type { NodeMatcherAlternative, NodeFinder } from "../typings/Types"; -import type { SimpleScopeTypeType } from "@cursorless/common"; -import type { SyntaxNode } from "web-tree-sitter"; -import { delimitedSelector } from "../util/nodeSelectors"; -import { identity } from "lodash-es"; import { getChildNodesForFieldName } from "../util/treeSitterUtils"; -import { patternFinder } from "../util/nodeFinders"; /** * Picks a node by rounding down and using the given parity. This function is @@ -73,13 +71,6 @@ function indexNodeFinder( }; } -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 @@ -134,21 +125,6 @@ const nodeMatchers: Partial< Record > = { 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)), // FIXME: Handle formal parameters diff --git a/packages/cursorless-engine/src/languages/latex.ts b/packages/cursorless-engine/src/languages/latex.ts index 46bb4e0c57..22f5001cca 100644 --- a/packages/cursorless-engine/src/languages/latex.ts +++ b/packages/cursorless-engine/src/languages/latex.ts @@ -1,5 +1,5 @@ import type { SimpleScopeTypeType, TextEditor } from "@cursorless/common"; -import { Range, Selection } from "@cursorless/common"; +import { Selection } from "@cursorless/common"; import type { SyntaxNode } from "web-tree-sitter"; import type { NodeMatcherAlternative, @@ -120,36 +120,6 @@ function extendToNamedSiblingIfExists( }; } -function extractItemContent( - editor: TextEditor, - node: SyntaxNode, -): SelectionWithContext { - let contentStartIndex = node.startIndex; - - const label = node.childForFieldName("label"); - if (label == null) { - const command = node.childForFieldName("command"); - if (command != null) { - contentStartIndex = command.endIndex + 1; - } - } else { - contentStartIndex = label.endIndex + 1; - } - - return { - selection: new Selection( - editor.document.positionAt(contentStartIndex), - editor.document.positionAt(node.endIndex), - ), - context: { - leadingDelimiterRange: new Range( - editor.document.positionAt(node.startIndex), - editor.document.positionAt(contentStartIndex - 1), - ), - }, - }; -} - const nodeMatchers: Partial< Record > = { @@ -174,8 +144,6 @@ const nodeMatchers: Partial< matcher(patternFinder(...sectioningText), unwrapGroupParens), patternMatcher("begin[name][text]", "end[name][text]"), ), - - collectionItem: matcher(patternFinder("enum_item"), extractItemContent), }; export default createPatternMatchers(nodeMatchers); diff --git a/packages/cursorless-engine/src/languages/ruby.ts b/packages/cursorless-engine/src/languages/ruby.ts index ba186c3188..62ed5a1a92 100644 --- a/packages/cursorless-engine/src/languages/ruby.ts +++ b/packages/cursorless-engine/src/languages/ruby.ts @@ -113,9 +113,6 @@ const EXPRESSION_STATEMENT_PARENT_TYPES = [ "then", ]; -const mapTypes = ["hash"]; -const listTypes = ["array", "string_array", "symbol_array"]; - const assignmentOperators = [ "=", "+=", @@ -187,6 +184,5 @@ const nodeMatchers: Partial< ], assignmentOperators.concat(mapKeyValueSeparators), ), - collectionItem: argumentMatcher(...mapTypes, ...listTypes), }; export const patternMatchers = createPatternMatchers(nodeMatchers); diff --git a/packages/cursorless-engine/src/languages/rust.ts b/packages/cursorless-engine/src/languages/rust.ts index 21f1d5186a..f7bf5eda7f 100644 --- a/packages/cursorless-engine/src/languages/rust.ts +++ b/packages/cursorless-engine/src/languages/rust.ts @@ -159,11 +159,6 @@ const nodeMatchers: Partial< ), leadingMatcher(["*.match_pattern![condition]"], ["if"]), ), - collectionItem: argumentMatcher( - "array_expression", - "tuple_expression", - "tuple_type", - ), type: cascadingMatcher( leadingMatcher( [ diff --git a/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts b/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts index e414e99d1c..17e040c88f 100644 --- a/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts +++ b/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts @@ -21,7 +21,6 @@ import { ExcludeInteriorStage, InteriorOnlyStage, } from "./modifiers/InteriorStage"; -import { ItemStage } from "./modifiers/ItemStage"; import { LeadingStage, TrailingStage } from "./modifiers/LeadingTrailingStages"; import { OrdinalScopeStage } from "./modifiers/OrdinalScopeStage"; import { EndOfStage, StartOfStage } from "./modifiers/PositionStage"; @@ -138,8 +137,6 @@ export class ModifierStageFactoryImpl implements ModifierStageFactory { switch (modifier.scopeType.type) { case "notebookCell": return new NotebookCellStage(modifier); - case "collectionItem": - return new ItemStage(this.languageDefinitions, this, modifier); default: // Default to containing syntax scope using tree sitter return new LegacyContainingSyntaxScopeStage( diff --git a/packages/cursorless-engine/src/processTargets/modifiers/ContainingScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/ContainingScopeStage.ts index 2c3f34108b..bf74445f54 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/ContainingScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/ContainingScopeStage.ts @@ -50,29 +50,6 @@ export class ContainingScopeStage implements ModifierStage { scopeHandler, ancestorIndex, ); - if (scopeType.type === "collectionItem") { - // For `collectionItem`, combine with generic implementation - try { - const legacyScopes = this.modifierStageFactory - .getLegacyScopeStage(this.modifier) - .run(target); - if (containingScopes == null) { - return legacyScopes; - } - if (containingScopes.length === 1 && legacyScopes.length === 1) { - const containingRange = containingScopes[0].contentRange; - const legacyRange = legacyScopes[0].contentRange; - if ( - containingRange.contains(legacyRange) && - !containingRange.isRangeEqual(legacyRange) - ) { - return legacyScopes; - } - } - } catch (_ex) { - // Do nothing - } - } if (containingScopes == null) { throw new NoContainingScopeError(this.modifier.scopeType.type); diff --git a/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts index cde16de7dd..1c294e3867 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts @@ -90,13 +90,6 @@ export class EveryScopeStage implements ModifierStage { } if (scopes.length === 0) { - if (scopeType.type === "collectionItem") { - // For `collectionItem`, fall back to generic implementation - return this.modifierStageFactory - .getLegacyScopeStage(this.modifier) - .run(target); - } - throw new NoContainingScopeError(scopeType.type); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts deleted file mode 100644 index 0efea9f7c2..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts +++ /dev/null @@ -1,214 +0,0 @@ -import type { - ContainingScopeModifier, - EveryScopeModifier, - SimpleScopeTypeType, - TextEditor, -} from "@cursorless/common"; -import { NoContainingScopeError, Range } from "@cursorless/common"; -import type { LanguageDefinitions } from "../../../languages/LanguageDefinitions"; -import type { Target } from "../../../typings/target.types"; -import { getRangeLength } from "../../../util/rangeUtils"; -import type { ModifierStageFactory } from "../../ModifierStageFactory"; -import type { ModifierStage } from "../../PipelineStages.types"; -import { ScopeTypeTarget } from "../../targets"; -import type { SimpleContainingScopeModifier } from "../scopeTypeStages/LegacyContainingSyntaxScopeStage"; -import { LegacyContainingSyntaxScopeStage } from "../scopeTypeStages/LegacyContainingSyntaxScopeStage"; -import { getIterationScope } from "./getIterationScope"; -import { tokenizeRange } from "./tokenizeRange"; - -export class ItemStage implements ModifierStage { - constructor( - private languageDefinitions: LanguageDefinitions, - private modifierStageFactory: ModifierStageFactory, - private modifier: ContainingScopeModifier | EveryScopeModifier, - ) {} - - run(target: Target): Target[] { - // First try the language specific implementation of item - try { - return new LegacyContainingSyntaxScopeStage( - this.languageDefinitions, - this.modifier as SimpleContainingScopeModifier, - ).run(target); - } catch (_error) { - // do nothing - } - - // Then try the textual implementation - if (this.modifier.type === "everyScope") { - return this.getEveryTarget(this.modifierStageFactory, target); - } - return [this.getSingleTarget(this.modifierStageFactory, target)]; - } - - private getEveryTarget( - modifierStageFactory: ModifierStageFactory, - target: Target, - ) { - const itemInfos = getItemInfosForIterationScope( - modifierStageFactory, - target, - ); - - // If target has explicit range filter to items in that range. Otherwise expand to all items in iteration scope. - const filteredItemInfos = target.hasExplicitRange - ? filterItemInfos(target, itemInfos) - : itemInfos; - - if (filteredItemInfos.length === 0) { - throw new NoContainingScopeError(this.modifier.scopeType.type); - } - - return filteredItemInfos.map((itemInfo) => - this.itemInfoToTarget(target, itemInfo), - ); - } - - private getSingleTarget( - modifierStageFactory: ModifierStageFactory, - target: Target, - ) { - const itemInfos = getItemInfosForIterationScope( - modifierStageFactory, - target, - ); - - const filteredItemInfos = filterItemInfos(target, itemInfos); - - if (filteredItemInfos.length === 0) { - throw new NoContainingScopeError(this.modifier.scopeType.type); - } - - const first = filteredItemInfos[0]; - const last = filteredItemInfos[filteredItemInfos.length - 1]; - - const itemInfo: ItemInfo = { - contentRange: first.contentRange.union(last.contentRange), - domain: first.domain.union(last.domain), - leadingDelimiterRange: first.leadingDelimiterRange, - trailingDelimiterRange: last.trailingDelimiterRange, - }; - - // We have both leading and trailing delimiter ranges - // The leading one is longer/more specific so prefer to use that for removal. - const removalRange = - itemInfo.leadingDelimiterRange != null && - itemInfo.trailingDelimiterRange != null && - getRangeLength(target.editor, itemInfo.leadingDelimiterRange) > - getRangeLength(target.editor, itemInfo.trailingDelimiterRange) - ? itemInfo.contentRange.union(itemInfo.leadingDelimiterRange) - : undefined; - - return this.itemInfoToTarget(target, itemInfo, removalRange); - } - - private itemInfoToTarget( - target: Target, - itemInfo: ItemInfo, - removalRange?: Range, - ) { - const insertionDelimiter = getInsertionDelimiter( - itemInfo.leadingDelimiterRange, - itemInfo.trailingDelimiterRange, - ); - return new ScopeTypeTarget({ - scopeTypeType: this.modifier.scopeType.type as SimpleScopeTypeType, - editor: target.editor, - isReversed: target.isReversed, - contentRange: itemInfo.contentRange, - insertionDelimiter, - leadingDelimiterRange: itemInfo.leadingDelimiterRange, - trailingDelimiterRange: itemInfo.trailingDelimiterRange, - removalRange, - }); - } -} - -function getInsertionDelimiter( - leadingDelimiterRange?: Range, - trailingDelimiterRange?: Range, -): string { - return (leadingDelimiterRange != null && - !leadingDelimiterRange.isSingleLine) || - (trailingDelimiterRange != null && !trailingDelimiterRange.isSingleLine) - ? ",\n" - : ", "; -} - -/** Filter item infos by content range and domain intersection */ -function filterItemInfos(target: Target, itemInfos: ItemInfo[]): ItemInfo[] { - return itemInfos.filter( - (itemInfo) => itemInfo.domain.intersection(target.contentRange) != null, - ); -} - -function getItemInfosForIterationScope( - modifierStageFactory: ModifierStageFactory, - target: Target, -) { - const { range, boundary } = getIterationScope(modifierStageFactory, target); - return getItemsInRange(target.editor, range, boundary); -} - -function getItemsInRange( - editor: TextEditor, - interior: Range, - boundary?: [Range, Range], -): ItemInfo[] { - const tokens = tokenizeRange(editor, interior, boundary); - const itemInfos: ItemInfo[] = []; - - tokens.forEach((token, i) => { - if (token.type === "separator" || token.type === "boundary") { - return; - } - - const leadingDelimiterRange = (() => { - if (tokens[i - 2]?.type === "item") { - return new Range(tokens[i - 2].range.end, token.range.start); - } - if (tokens[i - 1]?.type === "separator") { - return new Range(tokens[i - 1].range.start, token.range.start); - } - return undefined; - })(); - - const trailingDelimiterRange = (() => { - if (tokens[i + 2]?.type === "item") { - return new Range(token.range.end, tokens[i + 2].range.start); - } - if (tokens[i + 1]?.type === "separator") { - return new Range(token.range.end, tokens[i + 1].range.end); - } - return undefined; - })(); - - // Leading boundary and separator are excluded - const domainStart = - tokens[i - 1]?.type === "boundary" || tokens[i - 1]?.type === "separator" - ? tokens[i - 1].range.end - : token.range.start; - - // Trailing boundary and separator are excluded - const domainEnd = - tokens[i + 1]?.type === "boundary" || tokens[i + 1]?.type === "separator" - ? tokens[i + 1].range.start - : token.range.end; - - itemInfos.push({ - contentRange: token.range, - leadingDelimiterRange, - trailingDelimiterRange, - domain: new Range(domainStart, domainEnd), - }); - }); - - return itemInfos; -} - -interface ItemInfo { - contentRange: Range; - leadingDelimiterRange?: Range; - trailingDelimiterRange?: Range; - domain: Range; -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/getIterationScope.ts b/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/getIterationScope.ts deleted file mode 100644 index 61fb0b3166..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/getIterationScope.ts +++ /dev/null @@ -1,194 +0,0 @@ -import type { TextEditor, TextLine } from "@cursorless/common"; -import { Range, type SurroundingPairScopeType } from "@cursorless/common"; -import type { Target } from "../../../typings/target.types"; -import type { ModifierStageFactory } from "../../ModifierStageFactory"; -import { PlainTarget } from "../../targets"; -import { fitRangeToLineContent } from "../scopeHandlers"; - -/** - * Get the iteration scope range for item scope. - * Try to find non-string surrounding scope with a fallback to line content. - * @param context The stage process context - * @param target The stage target - * @returns The stage iteration scope and optional surrounding pair boundaries - */ -export function getIterationScope( - modifierStageFactory: ModifierStageFactory, - target: Target, -): { range: Range; boundary?: [Range, Range] } { - let surroundingTarget = getBoundarySurroundingPair( - modifierStageFactory, - target, - ); - - // Iteration is necessary in case of in valid surrounding targets (nested strings, content range adjacent to delimiter) - while (surroundingTarget != null) { - if ( - useInteriorOfSurroundingTarget( - modifierStageFactory, - target, - surroundingTarget, - ) - ) { - return { - range: surroundingTarget.getInterior()![0].contentRange, - boundary: getBoundary(surroundingTarget), - }; - } - - surroundingTarget = getParentSurroundingPair( - modifierStageFactory, - target.editor, - surroundingTarget, - ); - } - - // We have not found a surrounding pair. Use the line. - return { - range: fitRangeToLineContent(target.editor, target.contentRange), - }; -} - -function useInteriorOfSurroundingTarget( - modifierStageFactory: ModifierStageFactory, - target: Target, - surroundingTarget: Target, -): boolean { - const { contentRange } = target; - - if (contentRange.isEmpty) { - const [left, right] = getBoundary(surroundingTarget); - const pos = contentRange.start; - // Content range is outside adjacent to pair - if (pos.isEqual(left.start) || pos.isEqual(right.end)) { - return false; - } - const line = target.editor.document.lineAt(pos); - // Content range is just inside of opening/left delimiter - if ( - pos.isEqual(left.end) && - characterIsWhitespaceOrMissing(line, pos.character) - ) { - return false; - } - // Content range is just inside of closing/right delimiter - if ( - pos.isEqual(right.start) && - characterIsWhitespaceOrMissing(line, pos.character - 1) - ) { - return false; - } - } else { - // Content range is equal to surrounding range - if (contentRange.isRangeEqual(surroundingTarget.contentRange)) { - return false; - } - - // Content range is equal to one of the boundaries of the surrounding range - const [left, right] = getBoundary(surroundingTarget); - if (contentRange.isRangeEqual(left) || contentRange.isRangeEqual(right)) { - return false; - } - } - - // We don't look for items inside strings. - // A non-string surrounding pair that is inside a surrounding string is fine. - const surroundingStringTarget = getStringSurroundingPair( - modifierStageFactory, - surroundingTarget, - ); - if ( - surroundingStringTarget != null && - surroundingTarget.contentRange.start.isBeforeOrEqual( - surroundingStringTarget.contentRange.start, - ) - ) { - return false; - } - - return true; -} - -function getBoundary(surroundingTarget: Target): [Range, Range] { - return surroundingTarget.getBoundary()!.map((t) => t.contentRange) as [ - Range, - Range, - ]; -} - -function characterIsWhitespaceOrMissing( - line: TextLine, - index: number, -): boolean { - return ( - index < line.range.start.character || - index >= line.range.end.character || - line.text[index].trim() === "" - ); -} - -function getParentSurroundingPair( - modifierStageFactory: ModifierStageFactory, - editor: TextEditor, - target: Target, -) { - const startOffset = editor.document.offsetAt(target.contentRange.start); - // Can't have a parent; already at start of document - if (startOffset === 0) { - return undefined; - } - // Step out of this pair and see if we have a parent - const position = editor.document.positionAt(startOffset - 1); - return getBoundarySurroundingPair( - modifierStageFactory, - new PlainTarget({ - editor, - contentRange: new Range(position, position), - isReversed: false, - }), - ); -} - -function getBoundarySurroundingPair( - modifierStageFactory: ModifierStageFactory, - target: Target, -): Target | undefined { - return getSurroundingPair(modifierStageFactory, target, { - type: "surroundingPair", - delimiter: "collectionBoundary", - requireStrongContainment: true, - }); -} - -function getStringSurroundingPair( - modifierStageFactory: ModifierStageFactory, - target: Target, -): Target | undefined { - return getSurroundingPair(modifierStageFactory, target, { - type: "surroundingPair", - delimiter: "string", - requireStrongContainment: true, - }); -} - -function getSurroundingPair( - modifierStageFactory: ModifierStageFactory, - target: Target, - scopeType: SurroundingPairScopeType, -): Target | undefined { - const pairStage = modifierStageFactory.create({ - type: "containingScope", - scopeType, - }); - const targets = (() => { - try { - return pairStage.run(target); - } catch (_error) { - return []; - } - })(); - if (targets.length > 1) { - throw Error("Expected only one surrounding pair target"); - } - return targets[0]; -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/index.ts b/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/index.ts deleted file mode 100644 index 492aa91ffb..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./ItemStage"; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/tokenizeRange.ts b/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/tokenizeRange.ts deleted file mode 100644 index 43269862b5..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/tokenizeRange.ts +++ /dev/null @@ -1,186 +0,0 @@ -import type { TextEditor } from "@cursorless/common"; -import { Range } from "@cursorless/common"; - -/** - * Given the iteration scope, returns a list of "tokens" within that collection - * In this context, we define a "token" to be either an item in the collection, - * a delimiter or a separator. For example, if {@link interior} is a range - * containing `foo(hello), bar, whatever`, and {@link boundary} consists of - * two ranges containing `(` and `)`, then we'd return the following: - * - * ```json - * [ - * { range: "(", type: "boundary" }, - * { range: "foo(hello)", type: "item" }, - * { range: ",", type: "separator" }, - * { range: "bar", type: "item" }, - * { range: ",", type: "separator" }, - * { range: "whatever", type: "item" }, - * { range: ")", type: "boundary" }, - * ] - * ``` - * - * Where each `range` isn't actually a string, but a range whose text is the - * given string. - * @param editor The editor containing the range - * @param interior The range to look for tokens within - * @param boundary Optional boundaries for collections. [], {} - * @returns List of tokens - */ -export function tokenizeRange( - editor: TextEditor, - interior: Range, - boundary?: [Range, Range], -): Token[] { - const { document } = editor; - const text = document.getText(interior); - /** - * The interior range tokenized into delimited regions, including the delimiters themselves. For example: - * `"foo(hello), bar, whatever"` => - * `["foo", "(", "hello", ")", ",", " bar", ",", " whatever"]` - */ - const lexemes = text - // NB: Both the delimiters and the text between them are included because we - // use a capture group in this split regex - .split(/([,(){}<>[\]"'`])|(? lexeme != null && lexeme.length > 0); - const joinedLexemes = joinLexemesBySkippingMatchingPairs(lexemes); - const tokens: Token[] = []; - let offset = document.offsetAt(interior.start); - - joinedLexemes.forEach((lexeme) => { - // Whitespace found. Just skip - if (lexeme.trim().length === 0) { - offset += lexeme.length; - return; - } - - // Separator delimiter found. - if (lexeme === separator) { - tokens.push({ - type: "separator", - range: new Range( - document.positionAt(offset), - document.positionAt(offset + lexeme.length), - ), - }); - } - - // Text/item content found - else { - const offsetStart = offset + (lexeme.length - lexeme.trimStart().length); - tokens.push({ - type: "item", - range: new Range( - document.positionAt(offsetStart), - document.positionAt(offsetStart + lexeme.trim().length), - ), - }); - } - - offset += lexeme.length; - }); - - if (boundary != null) { - return [ - { type: "boundary", range: boundary[0] }, - ...tokens, - { type: "boundary", range: boundary[1] }, - ]; - } - - return tokens; -} - -/** - * Takes a list of lexemes and joins them into a list of alternating items and separators, skipping matching pairs (), {}, etc - * @param lexemes List of lexemes to operate on - * @returns List of merged lexemes. Note that its length will be less than or equal to {@link lexemes} - */ -export function joinLexemesBySkippingMatchingPairs(lexemes: string[]) { - const result: string[] = []; - /** - * The number of left delimiters minus right delimiters we've seen. If the - * balance is 0, we're at the top level of the collection, so separators are - * relevant. Otherwise we ignore separators because they're nested - */ - let delimiterBalance = 0; - /** The most recent opening delimiter we've seen */ - let openingDelimiter: string | null = null; - /** The closing delimiter we're currently looking for */ - let closingDelimiter: string | null = null; - /** - * The index in {@link lexemes} of the first lexeme in the current token we're - * merging. - */ - let startIndex: number = -1; - - lexemes.forEach((lexeme, index) => { - if (delimiterBalance > 0) { - // We are waiting for a closing delimiter - if (lexeme === closingDelimiter) { - // Closing delimiter found - --delimiterBalance; - } - // Additional opening delimiter found - else if (lexeme === openingDelimiter) { - ++delimiterBalance; - } - } - - // Starting delimiter found - // Make sure that there is a matching closing delimiter - else if ( - leftToRightMap[lexeme] != null && - lexemes.indexOf(leftToRightMap[lexeme], index + 1) > -1 - ) { - openingDelimiter = lexeme; - closingDelimiter = leftToRightMap[lexeme]; - delimiterBalance = 1; - if (startIndex < 0) { - // This is the first lexeme to be joined - startIndex = index; - } - } - - // This is the first lexeme to be joined - else if (startIndex < 0) { - startIndex = index; - } - - const isSeparator = lexeme === separator && delimiterBalance === 0; - - if (isSeparator || index === lexemes.length - 1) { - // This is the last lexeme to be joined - const endIndex = isSeparator ? index : index + 1; - result.push(lexemes.slice(startIndex, endIndex).join("")); - startIndex = -1; - if (isSeparator) { - // Add the separator itself - result.push(lexeme); - } - } - }); - - return result; -} - -const separator = ","; - -// Mapping between opening and closing delimiters -/* eslint-disable @typescript-eslint/naming-convention */ -const leftToRightMap: { [key: string]: string } = { - "(": ")", - "{": "}", - "<": ">", - "[": "]", - '"': '"', - "'": "'", - "`": "`", -}; -/* eslint-enable @typescript-eslint/naming-convention */ - -interface Token { - range: Range; - type: "item" | "separator" | "boundary"; -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts index 33c9614e9a..32daa91003 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.ts @@ -1,9 +1,13 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports -import type { Direction, ScopeType } from "@cursorless/common"; -import type { Position, TextEditor } from "@cursorless/common"; +import type { + Direction, + Position, + ScopeType, + TextEditor, +} from "@cursorless/common"; import type { TargetScope } from "./scope.types"; import type { - CustomScopeType, + ComplexScopeType, ScopeHandler, ScopeIteratorRequirements, } from "./scopeHandler.types"; @@ -22,7 +26,7 @@ const DEFAULT_REQUIREMENTS: Omit = */ export abstract class BaseScopeHandler implements ScopeHandler { public abstract readonly scopeType: ScopeType | undefined; - public abstract readonly iterationScopeType: ScopeType | CustomScopeType; + public abstract readonly iterationScopeType: ScopeType | ComplexScopeType; public readonly includeAdjacentInEvery: boolean = false; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BoundedScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BoundedScopeHandler.ts index 56d0f6bbf2..19d11caa7e 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BoundedScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BoundedScopeHandler.ts @@ -46,10 +46,13 @@ abstract class BoundedBaseScopeHandler extends BaseScopeHandler { } get iterationScopeType(): ScopeType { - if (this.targetScopeHandler.iterationScopeType.type === "custom") { - throw Error( - "Iteration scope type can't be custom for BoundedBaseScopeHandler", - ); + switch (this.targetScopeHandler.iterationScopeType.type) { + case "custom": + case "fallback": + case "conditional": + throw Error( + `Iteration scope type can't be '${this.targetScopeHandler.iterationScopeType.type}' for BoundedBaseScopeHandler`, + ); } return { type: "oneOf", diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts new file mode 100644 index 0000000000..8c67ba3690 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts @@ -0,0 +1,72 @@ +import type { + Direction, + Position, + ScopeType, + TextEditor, +} from "@cursorless/common"; +import type { LanguageDefinitions } from "../../../../languages/LanguageDefinitions"; +import { BaseScopeHandler } from "../BaseScopeHandler"; +import { OneOfScopeHandler } from "../OneOfScopeHandler"; +import type { TargetScope } from "../scope.types"; +import type { + ComplexScopeType, + ScopeHandler, + ScopeIteratorRequirements, +} from "../scopeHandler.types"; +import type { ScopeHandlerFactory } from "../ScopeHandlerFactory"; +import { CollectionItemTextualScopeHandler } from "./CollectionItemTextualScopeHandler"; + +export class CollectionItemScopeHandler extends BaseScopeHandler { + public scopeType: ScopeType = { type: "collectionItem" }; + protected isHierarchical = true; + private scopeHandler: ScopeHandler; + + get iterationScopeType(): ScopeType | ComplexScopeType { + return this.scopeHandler.iterationScopeType; + } + + constructor( + scopeHandlerFactory: ScopeHandlerFactory, + languageDefinitions: LanguageDefinitions, + languageId: string, + ) { + super(); + + this.scopeHandler = (() => { + const textualScopeHandler = new CollectionItemTextualScopeHandler( + scopeHandlerFactory, + languageId, + ); + + const languageScopeHandler = languageDefinitions + .get(languageId) + ?.getScopeHandler(this.scopeType); + + if (languageScopeHandler == null) { + return textualScopeHandler; + } + + return OneOfScopeHandler.createFromScopeHandlers( + scopeHandlerFactory, + { + type: "oneOf", + scopeTypes: [ + languageScopeHandler.scopeType, + textualScopeHandler.scopeType, + ], + }, + [languageScopeHandler, textualScopeHandler], + languageId, + ); + })(); + } + + generateScopeCandidates( + editor: TextEditor, + position: Position, + direction: Direction, + hints: ScopeIteratorRequirements, + ): Iterable { + return this.scopeHandler.generateScopes(editor, position, direction, hints); + } +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts new file mode 100644 index 0000000000..f7928bcf74 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts @@ -0,0 +1,195 @@ +import { + type Direction, + type Position, + Range, + type ScopeType, + type TextEditor, +} from "@cursorless/common"; +import { shrinkRangeToFitContent } from "../../../../util/selectionUtils"; +import { BaseScopeHandler } from "../BaseScopeHandler"; +import { compareTargetScopes } from "../compareTargetScopes"; +import type { TargetScope } from "../scope.types"; +import type { + ComplexScopeType, + ScopeIteratorRequirements, +} from "../scopeHandler.types"; +import type { ScopeHandlerFactory } from "../ScopeHandlerFactory"; +import { OneWayNestedRangeFinder } from "../util/OneWayNestedRangeFinder"; +import { OneWayRangeFinder } from "../util/OneWayRangeFinder"; +import { collectionItemIterationScopeHandler } from "./collectionItemIterationScopeHandler"; +import { createTargetScope } from "./createTargetScope"; +import { getInteriorRanges } from "./getInteriorRanges"; +import { getSeparatorOccurrences } from "./getSeparatorOccurrences"; + +export class CollectionItemTextualScopeHandler extends BaseScopeHandler { + public scopeType: ScopeType = { type: "collectionItem" }; + protected isHierarchical = true; + + get iterationScopeType(): ComplexScopeType { + return collectionItemIterationScopeHandler; + } + + constructor( + private scopeHandlerFactory: ScopeHandlerFactory, + private languageId: string, + ) { + super(); + } + + *generateScopeCandidates( + editor: TextEditor, + position: Position, + direction: Direction, + hints: ScopeIteratorRequirements, + ): Iterable { + const isEveryScope = hints.containment == null && hints.skipAncestorScopes; + const separatorRanges = getSeparatorOccurrences(editor.document); + const interiorRanges = getInteriorRanges( + this.scopeHandlerFactory, + this.languageId, + editor, + "collectionBoundary", + ); + const interiorRangeFinder = new OneWayNestedRangeFinder(interiorRanges); + const stringRanges = getInteriorRanges( + this.scopeHandlerFactory, + this.languageId, + editor, + "string", + ); + const stringRangeFinder = new OneWayRangeFinder(stringRanges); + const scopes: TargetScope[] = []; + const usedInteriors = new Set(); + const iterationStatesStack: IterationState[] = []; + + for (const separator of separatorRanges) { + // Separators in a string are not considered + if (stringRangeFinder.contains(separator)) { + continue; + } + + const currentIterationState = + iterationStatesStack[iterationStatesStack.length - 1]; + + // Get range for smallest containing interior + const containingInteriorRange = + interiorRangeFinder.getSmallestContaining(separator)?.range; + + // The contain range is either the interior or the line containing the separator + const containingIterationRange = + containingInteriorRange ?? + editor.document.lineAt(separator.start.line).range; + + if (currentIterationState != null) { + // The current containing iteration range is the same as the previous one. Just append delimiter. + if ( + currentIterationState.iterationRange.isRangeEqual( + containingIterationRange, + ) + ) { + currentIterationState.delimiters.push(separator); + continue; + } + + // The current containing range does not intersect previous one. Add scopes and remove state. + if (!currentIterationState.iterationRange.contains(separator)) { + this.addScopes(scopes, currentIterationState); + // Remove already added state + iterationStatesStack.pop(); + } + } + + // The current containing range is the same as the previous one. Just append delimiter. + if (iterationStatesStack.length > 0) { + const lastState = iterationStatesStack[iterationStatesStack.length - 1]; + if (lastState.iterationRange.isRangeEqual(containingIterationRange)) { + lastState.delimiters.push(separator); + continue; + } + } + + // New containing range. Add it to the list. + if (containingInteriorRange != null) { + usedInteriors.add(containingInteriorRange); + } + + iterationStatesStack.push({ + editor, + isEveryScope, + iterationRange: containingIterationRange, + delimiters: [separator], + }); + } + + for (const state of iterationStatesStack) { + this.addScopes(scopes, state); + } + + // Add interior ranges without a delimiter in them. eg: `[foo]` + for (const interior of interiorRanges) { + if (!usedInteriors.has(interior.range)) { + const range = shrinkRangeToFitContent(editor, interior.range); + if (!range.isEmpty) { + scopes.push( + createTargetScope(isEveryScope, editor, interior.range, range), + ); + } + } + } + + scopes.sort((a, b) => compareTargetScopes(direction, position, a, b)); + + yield* scopes; + } + + private addScopes(scopes: TargetScope[], state: IterationState) { + const { editor, iterationRange, isEveryScope, delimiters } = state; + + if (delimiters.length === 0) { + return; + } + + const itemRanges: Range[] = []; + + for (let i = 0; i < delimiters.length; ++i) { + const current = delimiters[i]; + + const previous = delimiters[i - 1]?.end ?? iterationRange.start; + itemRanges.push(new Range(previous, current.start)); + } + + const lastDelimiter = delimiters[delimiters.length - 1]; + itemRanges.push(new Range(lastDelimiter.end, iterationRange.end)); + + const trimmedRanges = itemRanges.map((range) => + shrinkRangeToFitContent(editor, range), + ); + + for (let i = 0; i < trimmedRanges.length; ++i) { + // Handle trailing delimiter + if ( + i === trimmedRanges.length - 1 && + editor.document.getText(trimmedRanges[i]).trim() === "" + ) { + continue; + } + scopes.push( + createTargetScope( + isEveryScope, + editor, + iterationRange, + trimmedRanges[i], + trimmedRanges[i - 1], + trimmedRanges[i + 1], + ), + ); + } + } +} + +interface IterationState { + editor: TextEditor; + iterationRange: Range; + isEveryScope: boolean; + delimiters: Range[]; +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/collectionItemIterationScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/collectionItemIterationScopeHandler.ts new file mode 100644 index 0000000000..6c643935b9 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/collectionItemIterationScopeHandler.ts @@ -0,0 +1,24 @@ +import { testRegex } from "@cursorless/common"; +import type { TargetScope } from "../scope.types"; +import type { ComplexScopeType } from "../scopeHandler.types"; +import { separatorRegex } from "./getSeparatorOccurrences"; + +export const collectionItemIterationScopeHandler: ComplexScopeType = { + type: "fallback", + scopeTypes: [ + { + type: "surroundingPairInterior", + delimiter: "collectionBoundary", + }, + { + type: "conditional", + scopeType: { + type: "line", + }, + predicate: (scope: TargetScope) => { + const text = scope.editor.document.getText(scope.domain); + return testRegex(separatorRegex, text); + }, + }, + ], +}; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/createTargetScope.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/createTargetScope.ts new file mode 100644 index 0000000000..770440a833 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/createTargetScope.ts @@ -0,0 +1,55 @@ +import { type TextEditor, Range } from "@cursorless/common"; +import { getRangeLength } from "../../../../util/rangeUtils"; +import { ScopeTypeTarget } from "../../../targets"; +import type { TargetScope } from "../scope.types"; + +export function createTargetScope( + isEveryScope: boolean, + editor: TextEditor, + iterationRange: Range, + contentRange: Range, + previousRange?: Range, + nextRange?: Range, +): TargetScope { + const leadingDelimiterRange = + previousRange != null + ? new Range(previousRange.end, contentRange.start) + : undefined; + const trailingDelimiterRange = + nextRange != null + ? new Range(contentRange.end, nextRange.start) + : undefined; + + // We have both leading and trailing delimiter ranges + // If the leading one is longer/more specific, prefer to use that for removal; + // otherwise use undefined to fallback to the default behavior (often trailing) + const removalRange = + !isEveryScope && + leadingDelimiterRange != null && + trailingDelimiterRange != null && + getRangeLength(editor, leadingDelimiterRange) > + getRangeLength(editor, trailingDelimiterRange) + ? contentRange.union(leadingDelimiterRange) + : undefined; + + const insertionDelimiter = iterationRange.isSingleLine ? ", " : ",\n"; + + return { + editor, + domain: contentRange, + getTargets(isReversed) { + return [ + new ScopeTypeTarget({ + scopeTypeType: "collectionItem", + editor, + isReversed, + contentRange, + insertionDelimiter, + leadingDelimiterRange, + trailingDelimiterRange, + removalRange, + }), + ]; + }, + }; +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/getInteriorRanges.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/getInteriorRanges.ts new file mode 100644 index 0000000000..fbd7a76324 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/getInteriorRanges.ts @@ -0,0 +1,31 @@ +import { + type Range, + type SurroundingPairName, + type TextEditor, + Position, +} from "@cursorless/common"; +import type { ScopeHandlerFactory } from "../ScopeHandlerFactory"; + +export function getInteriorRanges( + scopeHandlerFactory: ScopeHandlerFactory, + languageId: string, + editor: TextEditor, + delimiter: SurroundingPairName, +): { range: Range }[] { + const scopeHandler = scopeHandlerFactory.create( + { + type: "surroundingPairInterior", + delimiter, + }, + languageId, + ); + + return Array.from( + scopeHandler.generateScopes(editor, new Position(0, 0), "forward", { + containment: undefined, + skipAncestorScopes: false, + includeDescendantScopes: true, + }), + (scope) => ({ range: scope.domain }), + ); +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/getSeparatorOccurrences.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/getSeparatorOccurrences.ts new file mode 100644 index 0000000000..5d78f70d59 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/getSeparatorOccurrences.ts @@ -0,0 +1,16 @@ +import { matchAll, Range, type TextDocument } from "@cursorless/common"; + +const separator = ","; + +export const separatorRegex = new RegExp(separator, "g"); + +export function getSeparatorOccurrences(document: TextDocument): Range[] { + const text = document.getText(); + + return matchAll(text, separatorRegex, (match): Range => { + return new Range( + document.positionAt(match.index!), + document.positionAt(match.index! + match[0].length), + ); + }); +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ConditionalScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ConditionalScopeHandler.ts new file mode 100644 index 0000000000..139f749626 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ConditionalScopeHandler.ts @@ -0,0 +1,51 @@ +import { + NoContainingScopeError, + type Direction, + type Position, + type ScopeType, + type TextEditor, +} from "@cursorless/common"; +import { ifilter } from "itertools"; +import { BaseScopeHandler } from "./BaseScopeHandler"; +import type { TargetScope } from "./scope.types"; +import type { + ConditionalScopeType, + ScopeIteratorRequirements, +} from "./scopeHandler.types"; +import type { ScopeHandlerFactory } from "./ScopeHandlerFactory"; + +export class ConditionalScopeHandler extends BaseScopeHandler { + public scopeType = undefined; + protected isHierarchical = true; + + constructor( + public scopeHandlerFactory: ScopeHandlerFactory, + private conditionalScopeType: ConditionalScopeType, + private languageId: string, + ) { + super(); + } + + get iterationScopeType(): ScopeType { + throw new NoContainingScopeError( + "Iteration scope for ConditionalScopeHandler", + ); + } + + generateScopeCandidates( + editor: TextEditor, + position: Position, + direction: Direction, + hints: ScopeIteratorRequirements, + ): Iterable { + const scopeHandler = this.scopeHandlerFactory.create( + this.conditionalScopeType.scopeType, + this.languageId, + ); + + return ifilter( + scopeHandler.generateScopes(editor, position, direction, hints), + this.conditionalScopeType.predicate, + ); + } +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/FallbackScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/FallbackScopeHandler.ts new file mode 100644 index 0000000000..869d4993b3 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/FallbackScopeHandler.ts @@ -0,0 +1,50 @@ +import { + NoContainingScopeError, + type Direction, + type Position, + type ScopeType, + type TextEditor, +} from "@cursorless/common"; +import { BaseScopeHandler } from "./BaseScopeHandler"; +import type { ScopeHandlerFactory } from "./ScopeHandlerFactory"; +import type { TargetScope } from "./scope.types"; +import type { + FallbackScopeType, + ScopeHandler, + ScopeIteratorRequirements, +} from "./scopeHandler.types"; + +export class FallbackScopeHandler extends BaseScopeHandler { + public scopeType = undefined; + protected isHierarchical = true; + + get iterationScopeType(): ScopeType { + throw new NoContainingScopeError( + "Iteration scope for FallbackScopeHandler", + ); + } + + constructor( + public scopeHandlerFactory: ScopeHandlerFactory, + private fallbackScopeType: FallbackScopeType, + private languageId: string, + ) { + super(); + } + + *generateScopeCandidates( + editor: TextEditor, + position: Position, + direction: Direction, + hints: ScopeIteratorRequirements, + ): Iterable { + const scopeHandlers: ScopeHandler[] = this.fallbackScopeType.scopeTypes.map( + (scopeType) => + this.scopeHandlerFactory.create(scopeType, this.languageId), + ); + + for (const scopeHandler of scopeHandlers) { + yield* scopeHandler.generateScopes(editor, position, direction, hints); + } + } +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/IteratorInfo.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/IteratorInfo.ts index 582c45375f..deafe0ad4a 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/IteratorInfo.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/IteratorInfo.ts @@ -5,6 +5,7 @@ interface IteratorInfo { iterator: Iterator; value: T; + index: number; } /** @@ -19,7 +20,7 @@ interface IteratorInfo { export function getInitialIteratorInfos( iterators: Iterator[], ): IteratorInfo[] { - return iterators.flatMap((iterator) => { + return iterators.flatMap((iterator, i) => { const { value, done } = iterator.next(); return done ? [] @@ -27,6 +28,7 @@ export function getInitialIteratorInfos( { iterator, value, + index: i, }, ]; }); @@ -47,10 +49,10 @@ export function advanceIteratorsUntil( criterion: (arg: T) => boolean, ): IteratorInfo[] { return iteratorInfos.flatMap((iteratorInfo) => { - const { iterator } = iteratorInfo; + const { iterator, index } = iteratorInfo; let { value } = iteratorInfo; - let done: boolean | undefined = false; + while (!done && !criterion(value)) { ({ value, done } = iterator.next()); } @@ -59,6 +61,6 @@ export function advanceIteratorsUntil( return []; } - return [{ iterator, value }]; + return [{ iterator, value, index }]; }); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts index c484ef76a9..c4febc6165 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts @@ -17,6 +17,8 @@ import type { export class OneOfScopeHandler extends BaseScopeHandler { protected isHierarchical = true; + private iterationScopeHandler: OneOfScopeHandler | undefined; + private lastYieldedIndex: number | undefined; static create( scopeHandlerFactory: ScopeHandlerFactory, @@ -27,9 +29,22 @@ export class OneOfScopeHandler extends BaseScopeHandler { (scopeType) => scopeHandlerFactory.create(scopeType, languageId), ); - const iterationScopeType = (): CustomScopeType => ({ - type: "custom", - scopeHandler: new OneOfScopeHandler( + return this.createFromScopeHandlers( + scopeHandlerFactory, + scopeType, + scopeHandlers, + languageId, + ); + } + + static createFromScopeHandlers( + scopeHandlerFactory: ScopeHandlerFactory, + scopeType: OneOfScopeType, + scopeHandlers: ScopeHandler[], + languageId: string, + ): ScopeHandler { + const getIterationScopeHandler = () => + new OneOfScopeHandler( undefined, scopeHandlers.map((scopeHandler) => scopeHandlerFactory.create( @@ -40,20 +55,29 @@ export class OneOfScopeHandler extends BaseScopeHandler { () => { throw new Error("Not implemented"); }, - ), - }); + ); - return new OneOfScopeHandler(scopeType, scopeHandlers, iterationScopeType); + return new OneOfScopeHandler( + scopeType, + scopeHandlers, + getIterationScopeHandler, + ); } get iterationScopeType(): CustomScopeType { - return this.getIterationScopeType(); + if (this.iterationScopeHandler == null) { + this.iterationScopeHandler = this.getIterationScopeHandler(); + } + return { + type: "custom", + scopeHandler: this.iterationScopeHandler, + }; } private constructor( public readonly scopeType: OneOfScopeType | undefined, private scopeHandlers: ScopeHandler[], - private getIterationScopeType: () => CustomScopeType, + private getIterationScopeHandler: () => OneOfScopeHandler, ) { super(); } @@ -64,6 +88,14 @@ export class OneOfScopeHandler extends BaseScopeHandler { direction: Direction, hints: ScopeIteratorRequirements, ): Iterable { + // If we have used the iteration scope handler, we only want to yield from its handler. + if (this.iterationScopeHandler?.lastYieldedIndex != null) { + const handlerIndex = this.iterationScopeHandler.lastYieldedIndex; + const handler = this.scopeHandlers[handlerIndex]; + yield* handler.generateScopes(editor, position, direction, hints); + return; + } + const iterators = this.scopeHandlers.map((scopeHandler) => scopeHandler .generateScopes(editor, position, direction, hints) @@ -78,7 +110,9 @@ export class OneOfScopeHandler extends BaseScopeHandler { ); // Pick minimum scope according to canonical scope ordering - const currentScope = iteratorInfos[0].value; + const iteratorInfo = iteratorInfos[0]; + const currentScope = iteratorInfo.value; + this.lastYieldedIndex = iteratorInfo.index; yield currentScope; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactory.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactory.ts index b50cfac4f3..0ae0823266 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactory.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactory.ts @@ -1,14 +1,14 @@ import type { ScopeType } from "@cursorless/common"; -import type { CustomScopeType, ScopeHandler } from "./scopeHandler.types"; +import type { ComplexScopeType, ScopeHandler } from "./scopeHandler.types"; export interface ScopeHandlerFactory { maybeCreate( - scopeType: ScopeType | CustomScopeType, + scopeType: ScopeType | ComplexScopeType, languageId: string, ): ScopeHandler | undefined; create( - scopeType: ScopeType | CustomScopeType, + scopeType: ScopeType | ComplexScopeType, languageId: string, ): ScopeHandler; } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts index 17b7f9c129..c8501df9ca 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts @@ -5,7 +5,10 @@ import { BoundedParagraphScopeHandler, } from "./BoundedScopeHandler"; import { CharacterScopeHandler } from "./CharacterScopeHandler"; +import { CollectionItemScopeHandler } from "./CollectionItemScopeHandler/CollectionItemScopeHandler"; +import { ConditionalScopeHandler } from "./ConditionalScopeHandler"; import { DocumentScopeHandler } from "./DocumentScopeHandler"; +import { FallbackScopeHandler } from "./FallbackScopeHandler"; import { IdentifierScopeHandler } from "./IdentifierScopeHandler"; import { LineScopeHandler } from "./LineScopeHandler"; import { OneOfScopeHandler } from "./OneOfScopeHandler"; @@ -24,7 +27,7 @@ import { } from "./SurroundingPairScopeHandler"; import { TokenScopeHandler } from "./TokenScopeHandler"; import { WordScopeHandler } from "./WordScopeHandler/WordScopeHandler"; -import type { CustomScopeType, ScopeHandler } from "./scopeHandler.types"; +import type { ComplexScopeType, ScopeHandler } from "./scopeHandler.types"; /** * Returns a scope handler for the given scope type and language id, or @@ -50,7 +53,7 @@ export class ScopeHandlerFactoryImpl implements ScopeHandlerFactory { } maybeCreate( - scopeType: ScopeType | CustomScopeType, + scopeType: ScopeType | ComplexScopeType, languageId: string, ): ScopeHandler | undefined { switch (scopeType.type) { @@ -72,8 +75,6 @@ export class ScopeHandlerFactoryImpl implements ScopeHandlerFactory { return new BoundedParagraphScopeHandler(this, scopeType, languageId); case "document": return new DocumentScopeHandler(scopeType, languageId); - case "oneOf": - return OneOfScopeHandler.create(this, scopeType, languageId); case "nonWhitespaceSequence": return new NonWhitespaceSequenceScopeHandler( this, @@ -92,6 +93,12 @@ export class ScopeHandlerFactoryImpl implements ScopeHandlerFactory { return new CustomRegexScopeHandler(this, scopeType, languageId); case "glyph": return new GlyphScopeHandler(this, scopeType, languageId); + case "collectionItem": + return new CollectionItemScopeHandler( + this, + this.languageDefinitions, + languageId, + ); case "surroundingPair": return new SurroundingPairScopeHandler( this.languageDefinitions, @@ -106,6 +113,12 @@ export class ScopeHandlerFactoryImpl implements ScopeHandlerFactory { ); case "custom": return scopeType.scopeHandler; + case "oneOf": + return OneOfScopeHandler.create(this, scopeType, languageId); + case "fallback": + return new FallbackScopeHandler(this, scopeType, languageId); + case "conditional": + return new ConditionalScopeHandler(this, scopeType, languageId); case "instance": // Handle instance pseudoscope with its own special modifier throw Error("Unexpected scope type 'instance'"); @@ -117,7 +130,7 @@ export class ScopeHandlerFactoryImpl implements ScopeHandlerFactory { } create( - scopeType: ScopeType | CustomScopeType, + scopeType: ScopeType | ComplexScopeType, languageId: string, ): ScopeHandler { const handler = this.maybeCreate(scopeType, languageId); diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts index a7ad5b51cb..80e8a4a1af 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts @@ -1,9 +1,9 @@ import { matchAllIterator, Range, type TextDocument } from "@cursorless/common"; import type { LanguageDefinition } from "../../../../languages/LanguageDefinition"; import type { QueryCapture } from "../../../../languages/TreeSitterQuery/QueryCapture"; +import { OneWayNestedRangeFinder } from "../util/OneWayNestedRangeFinder"; +import { OneWayRangeFinder } from "../util/OneWayRangeFinder"; import { getDelimiterRegex } from "./getDelimiterRegex"; -import { OneWayRangeFinder } from "./OneWayRangeFinder"; -import { OneWayNestedRangeFinder } from "./OneWayNestedRangeFinder"; import type { DelimiterOccurrence, IndividualDelimiter } from "./types"; /** diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts index 730adf60e4..0242b87780 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scopeHandler.types.ts @@ -1,5 +1,9 @@ -import type { Position, TextEditor } from "@cursorless/common"; -import type { Direction, ScopeType } from "@cursorless/common"; +import type { + Direction, + Position, + ScopeType, + TextEditor, +} from "@cursorless/common"; import type { TargetScope } from "./scope.types"; /** @@ -12,6 +16,30 @@ export interface CustomScopeType { scopeHandler: ScopeHandler; } +/** + * Used to handle fallback scope types. The scope types are yielded in specified + * order. + */ +export interface FallbackScopeType { + type: "fallback"; + scopeTypes: (ScopeType | ComplexScopeType)[]; +} + +/** + * Used to handle conditional scope types. The predicate determines if the + * scope should be yielded or not. + */ +export interface ConditionalScopeType { + type: "conditional"; + scopeType: ScopeType; + predicate: (scope: TargetScope) => boolean; +} + +export type ComplexScopeType = + | CustomScopeType + | FallbackScopeType + | ConditionalScopeType; + /** * Represents a scope type. The functions in this interface allow us to find * specific instances of the given scope type in a document. These functions are @@ -44,7 +72,7 @@ export interface ScopeHandler { * scope type will be used when the input target has no explicit range (ie * {@link Target.hasExplicitRange} is `false`). */ - readonly iterationScopeType: ScopeType | CustomScopeType; + readonly iterationScopeType: ScopeType | ComplexScopeType; /** * Returns an iterable of scopes meeting the requirements in diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/OneWayNestedRangeFinder.test.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/util/OneWayNestedRangeFinder.test.ts similarity index 100% rename from packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/OneWayNestedRangeFinder.test.ts rename to packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/util/OneWayNestedRangeFinder.test.ts diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/OneWayNestedRangeFinder.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/util/OneWayNestedRangeFinder.ts similarity index 100% rename from packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/OneWayNestedRangeFinder.ts rename to packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/util/OneWayNestedRangeFinder.ts diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/OneWayRangeFinder.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/util/OneWayRangeFinder.ts similarity index 100% rename from packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/OneWayRangeFinder.ts rename to packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/util/OneWayRangeFinder.ts diff --git a/packages/cursorless-engine/src/scopeProviders/ScopeSupportChecker.ts b/packages/cursorless-engine/src/scopeProviders/ScopeSupportChecker.ts index d2b9e8345f..73918bd104 100644 --- a/packages/cursorless-engine/src/scopeProviders/ScopeSupportChecker.ts +++ b/packages/cursorless-engine/src/scopeProviders/ScopeSupportChecker.ts @@ -94,9 +94,6 @@ function getLegacyScopeSupport( scopeType: ScopeType, ): ScopeSupport { switch (scopeType.type) { - case "boundedNonWhitespaceSequence": - case "surroundingPair": - return ScopeSupport.supportedLegacy; case "notebookCell": // FIXME: What to do here return ScopeSupport.unsupported; diff --git a/queries/clojure.scm b/queries/clojure.scm index 753d2f488e..31f196e9c3 100644 --- a/queries/clojure.scm +++ b/queries/clojure.scm @@ -11,3 +11,33 @@ (quoting_lit (list_lit) ) @list + +;;!! '(foo bar) +;;! ^^^ ^^^ +(list_lit + (_)? @_.leading.endOf + . + (_) @collectionItem.start + . + (_)? @_.trailing.startOf +) + +(list_lit + open: "(" @collectionItem.iteration.start.startOf + close: ")" @collectionItem.iteration.end.endOf +) @collectionItem.iteration.domain + +;;!! [foo bar] +;;! ^^^ ^^^ +(vec_lit + (_)? @_.leading.endOf + . + (_) @collectionItem.start + . + (_)? @_.trailing.startOf +) + +(vec_lit + open: "[" @collectionItem.iteration.start.startOf + close: "]" @collectionItem.iteration.end.endOf +) @collectionItem.iteration.domain diff --git a/queries/latex.scm b/queries/latex.scm index c0ce621d5c..a67decdc20 100644 --- a/queries/latex.scm +++ b/queries/latex.scm @@ -35,3 +35,18 @@ ">" ] @disqualifyDelimiter ) + +;;!! \item one \LaTeX +;;! ^^^^^^^^^^ +( + (_ + (enum_item + (text) @collectionItem.start.startOf + ) @collectionItem.leading.startOf @collectionItem.end.endOf + ) +) + +(generic_environment + (begin) @collectionItem.iteration.start.endOf + (end) @collectionItem.iteration.end.startOf +) @collectionItem.iteration.domain diff --git a/queries/ruby.scm b/queries/ruby.scm index 99a5a5e905..473a701a53 100644 --- a/queries/ruby.scm +++ b/queries/ruby.scm @@ -69,3 +69,29 @@ operator: [ (match_pattern "=>" @disqualifyDelimiter ) + +;;!! %w(foo bar) +;;! ^^^ ^^^ +( + (string_array + (bare_string)? @_.leading.endOf + . + (bare_string) @collectionItem + . + (bare_string)? @_.trailing.startOf + ) + (#insertion-delimiter! @collectionItem " ") +) + +;;!! %i(foo bar) +;;! ^^^ ^^^ +( + (symbol_array + (bare_symbol)? @_.leading.endOf + . + (bare_symbol) @collectionItem + . + (bare_symbol)? @_.trailing.startOf + ) + (#insertion-delimiter! @collectionItem " ") +) diff --git a/queries/rust.scm b/queries/rust.scm index 22dfafefce..1031506c46 100644 --- a/queries/rust.scm +++ b/queries/rust.scm @@ -82,3 +82,6 @@ operator: [ (macro_rule "=>" @disqualifyDelimiter ) +(lifetime + "'" @disqualifyDelimiter +)