1- import {
2- type Direction ,
3- type Position ,
4- Range ,
5- type ScopeType ,
6- type TextEditor ,
1+ import type {
2+ Direction ,
3+ Position ,
4+ ScopeType ,
5+ TextEditor ,
76} from "@cursorless/common" ;
7+ import { Range } from "@cursorless/common" ;
88import { shrinkRangeToFitContent } from "../../../../util/selectionUtils" ;
99import { BaseScopeHandler } from "../BaseScopeHandler" ;
1010import { compareTargetScopes } from "../compareTargetScopes" ;
@@ -48,7 +48,7 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler {
4848 const cacheKey = "CollectionItemTextualScopeHandler_" + isEveryScope ;
4949
5050 if ( ! scopeHandlerCache . isValid ( cacheKey , editor . document ) ) {
51- const scopes = this . getsScopes ( editor , direction , isEveryScope ) ;
51+ const scopes = this . getScopes ( editor , isEveryScope ) ;
5252 scopeHandlerCache . update ( cacheKey , editor . document , scopes ) ;
5353 }
5454
@@ -59,29 +59,72 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler {
5959 yield * scopes ;
6060 }
6161
62- private getsScopes (
63- editor : TextEditor ,
64- direction : Direction ,
65- isEveryScope : boolean ,
66- ) {
67- const separatorRanges = getSeparatorOccurrences ( editor . document ) ;
62+ private getScopes ( editor : TextEditor , isEveryScope : boolean ) {
6863 const interiorRanges = getInteriorRanges (
6964 this . scopeHandlerFactory ,
7065 this . languageId ,
7166 editor ,
7267 "collectionBoundary" ,
7368 ) ;
74- const interiorRangeFinder = new OneWayNestedRangeFinder ( interiorRanges ) ;
75- const stringRanges = getInteriorRanges (
76- this . scopeHandlerFactory ,
77- this . languageId ,
69+ const scopes : TargetScope [ ] = [ ] ;
70+
71+ const usedInteriors = this . addSeparatorRanges (
7872 editor ,
79- "string" ,
73+ isEveryScope ,
74+ interiorRanges ,
75+ scopes ,
8076 ) ;
81- const stringRangeFinder = new OneWayRangeFinder ( stringRanges ) ;
82- const scopes : TargetScope [ ] = [ ] ;
77+
78+ // Add interior ranges without a delimiter in them. eg: `[foo]`
79+ for ( const interior of interiorRanges ) {
80+ if ( ! usedInteriors . has ( interior . range ) && ! interior . range . isEmpty ) {
81+ const range = shrinkRangeToFitContent ( editor , interior . range ) ;
82+ if ( ! range . isEmpty ) {
83+ scopes . push (
84+ createTargetScope ( isEveryScope , editor , interior . range , range ) ,
85+ ) ;
86+ }
87+ }
88+ }
89+
90+ return scopes ;
91+ }
92+
93+ private addSeparatorRanges (
94+ editor : TextEditor ,
95+ isEveryScope : boolean ,
96+ interiorRanges : { range : Range } [ ] ,
97+ scopes : TargetScope [ ] ,
98+ ) : Set < Range > {
99+ const separatorRanges = getSeparatorOccurrences ( editor . document ) ;
83100 const usedInteriors = new Set < Range > ( ) ;
101+
102+ if ( separatorRanges . length === 0 ) {
103+ return usedInteriors ;
104+ }
105+
84106 const iterationStatesStack : IterationState [ ] = [ ] ;
107+ const interiorRangeFinder = new OneWayNestedRangeFinder ( interiorRanges ) ;
108+ const stringRangeFinder = new OneWayRangeFinder (
109+ getInteriorRanges (
110+ this . scopeHandlerFactory ,
111+ this . languageId ,
112+ editor ,
113+ "string" ,
114+ ) ,
115+ ) ;
116+ let previousLine = - 1 ;
117+ let previousLineRange : Range | undefined ;
118+
119+ const getLineRange = ( line : number ) => {
120+ // Separators are processed in document order, so line numbers are
121+ // monotonically increasing.
122+ if ( line !== previousLine || previousLineRange == null ) {
123+ previousLine = line ;
124+ previousLineRange = editor . document . lineAt ( line ) . range ;
125+ }
126+ return previousLineRange ;
127+ } ;
85128
86129 for ( const separator of separatorRanges ) {
87130 // Separators in a string are not considered
@@ -109,8 +152,7 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler {
109152
110153 // The containing iteration range is either the interior or the line containing the separator
111154 const containingIterationRange =
112- containingInteriorRange ??
113- editor . document . lineAt ( separator . start . line ) . range ;
155+ containingInteriorRange ?? getLineRange ( separator . start . line ) ;
114156
115157 // The current containing iteration range is the same as the previous one. Just append delimiter.
116158 if (
@@ -142,19 +184,7 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler {
142184 this . addScopes ( scopes , state ) ;
143185 }
144186
145- // Add interior ranges without a delimiter in them. eg: `[foo]`
146- for ( const interior of interiorRanges ) {
147- if ( ! usedInteriors . has ( interior . range ) && ! interior . range . isEmpty ) {
148- const range = shrinkRangeToFitContent ( editor , interior . range ) ;
149- if ( ! range . isEmpty ) {
150- scopes . push (
151- createTargetScope ( isEveryScope , editor , interior . range , range ) ,
152- ) ;
153- }
154- }
155- }
156-
157- return scopes ;
187+ return usedInteriors ;
158188 }
159189
160190 private addScopes ( scopes : TargetScope [ ] , state : IterationState ) {
@@ -164,41 +194,68 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler {
164194 return ;
165195 }
166196
167- const itemRanges : Range [ ] = [ ] ;
168-
169- for ( let i = 0 ; i < delimiters . length ; ++ i ) {
170- const current = delimiters [ i ] ;
171-
172- const previous = delimiters [ i - 1 ] ?. end ?? iterationRange . start ;
173- itemRanges . push ( new Range ( previous , current . start ) ) ;
174- }
175-
176- const lastDelimiter = delimiters [ delimiters . length - 1 ] ;
177- itemRanges . push ( new Range ( lastDelimiter . end , iterationRange . end ) ) ;
178-
179- const trimmedRanges = itemRanges . map ( ( range ) =>
180- shrinkRangeToFitContent ( editor , range ) ,
197+ const firstDelimiter = delimiters [ 0 ] ;
198+ let previousRange : Range | undefined ;
199+ let currentRange = shrinkRangeToFitContent (
200+ editor ,
201+ new Range ( iterationRange . start , firstDelimiter . start ) ,
181202 ) ;
203+ let previousDelimiterEnd = firstDelimiter . end ;
204+
205+ for ( let i = 1 ; i < delimiters . length ; ++ i ) {
206+ const nextRange = shrinkRangeToFitContent (
207+ editor ,
208+ new Range ( previousDelimiterEnd , delimiters [ i ] . start ) ,
209+ ) ;
182210
183- for ( let i = 0 ; i < trimmedRanges . length ; ++ i ) {
184- // Handle trailing delimiter
185- if (
186- i === trimmedRanges . length - 1 &&
187- editor . document . getText ( trimmedRanges [ i ] ) . trim ( ) === ""
188- ) {
189- continue ;
190- }
191211 scopes . push (
192212 createTargetScope (
193213 isEveryScope ,
194214 editor ,
195215 iterationRange ,
196- trimmedRanges [ i ] ,
197- trimmedRanges [ i - 1 ] ,
198- trimmedRanges [ i + 1 ] ,
216+ currentRange ,
217+ previousRange ,
218+ nextRange ,
199219 ) ,
200220 ) ;
221+
222+ previousRange = currentRange ;
223+ currentRange = nextRange ;
224+ previousDelimiterEnd = delimiters [ i ] . end ;
225+ }
226+
227+ const trailingRange = shrinkRangeToFitContent (
228+ editor ,
229+ new Range ( previousDelimiterEnd , iterationRange . end ) ,
230+ ) ;
231+
232+ // Emit the item before the final delimiter, using the trailing range as
233+ // nextRange so delimiter metadata remains correct.
234+ scopes . push (
235+ createTargetScope (
236+ isEveryScope ,
237+ editor ,
238+ iterationRange ,
239+ currentRange ,
240+ previousRange ,
241+ trailingRange ,
242+ ) ,
243+ ) ;
244+
245+ // Handle trailing delimiter.
246+ if ( editor . document . getText ( trailingRange ) . trim ( ) === "" ) {
247+ return ;
201248 }
249+
250+ scopes . push (
251+ createTargetScope (
252+ isEveryScope ,
253+ editor ,
254+ iterationRange ,
255+ trailingRange ,
256+ currentRange ,
257+ ) ,
258+ ) ;
202259 }
203260}
204261
0 commit comments