11import type { Range } from "@cursorless/common" ;
22import findLastIndex from "lodash-es/findLastIndex" ;
3- import type { DelimiterOccurrence , SurroundingPairOccurrence } from "./types" ;
3+ import type {
4+ DelimiterOccurrence ,
5+ IndividualDelimiter ,
6+ SurroundingPairOccurrence ,
7+ } from "./types" ;
8+
9+ interface OpeningDelimiterStackOccurrence {
10+ delimiterInfo : IndividualDelimiter ;
11+ range : Range ;
12+ textFragmentRange : Range | undefined ;
13+ }
14+
15+ interface OpeningDelimiterMatch {
16+ delimiterInfo : IndividualDelimiter ;
17+ openingDelimiterIndex : number ;
18+ }
419
520/**
621 * Given a list of occurrences of delimiters, returns a list of occurrences of
@@ -13,50 +28,91 @@ export function getSurroundingPairOccurrences(
1328 delimiterOccurrences : DelimiterOccurrence [ ] ,
1429) : SurroundingPairOccurrence [ ] {
1530 const result : SurroundingPairOccurrence [ ] = [ ] ;
16- const openingDelimitersStack : DelimiterOccurrence [ ] = [ ] ;
31+ const openingDelimitersStack : OpeningDelimiterStackOccurrence [ ] = [ ] ;
1732
1833 for ( const occurrence of delimiterOccurrences ) {
19- const {
20- delimiterInfo : { delimiterName, side, isSingleLine } ,
21- textFragmentRange,
22- range,
23- } = occurrence ;
24-
25- if ( side === "left" ) {
26- openingDelimitersStack . push ( occurrence ) ;
27- } else {
28- const openingDelimiterIndex = findLastIndex (
29- openingDelimitersStack ,
30- ( o ) =>
31- o . delimiterInfo . delimiterName === delimiterName &&
32- isSameTextFragment ( o . textFragmentRange , textFragmentRange ) &&
33- isValidLine ( isSingleLine , o . range , range ) ,
34+ // One token can represent multiple delimiters (eg ")" could close
35+ // `parentheses` or Ruby `%Q(`), so pick the best closing interpretation
36+ // based on currently open delimiters.
37+ const closestOpeningDelimiterMatch = getClosestOpeningDelimiterMatch (
38+ occurrence ,
39+ openingDelimitersStack ,
40+ ) ;
41+
42+ if ( closestOpeningDelimiterMatch == null ) {
43+ const openingDelimiterInfo = occurrence . delimiterInfos . find (
44+ ( { side } ) => side === "left" || side === "unknown" ,
3445 ) ;
3546
36- if ( openingDelimiterIndex === - 1 ) {
37- // When side is unknown and we can't find an opening delimiter, that means this *is* the opening delimiter.
38- if ( side === "unknown" ) {
39- openingDelimitersStack . push ( occurrence ) ;
40- }
47+ // Pure closing delimiters with no matching opener are ignored.
48+ if ( openingDelimiterInfo == null ) {
4149 continue ;
4250 }
4351
44- const openingDelimiter = openingDelimitersStack [ openingDelimiterIndex ] ;
45-
46- // Pop stack up to and including the opening delimiter
47- openingDelimitersStack . length = openingDelimiterIndex ;
48-
49- result . push ( {
50- delimiterName : delimiterName ,
51- openingDelimiterRange : openingDelimiter . range ,
52- closingDelimiterRange : range ,
52+ // If this token can't close anything, treat it as an opener.
53+ openingDelimitersStack . push ( {
54+ delimiterInfo : openingDelimiterInfo ,
55+ range : occurrence . range ,
56+ textFragmentRange : occurrence . textFragmentRange ,
5357 } ) ;
58+ continue ;
5459 }
60+
61+ const { delimiterInfo, openingDelimiterIndex } =
62+ closestOpeningDelimiterMatch ;
63+ const openingDelimiter = openingDelimitersStack [ openingDelimiterIndex ] ;
64+
65+ // Pop stack up to and including the opening delimiter
66+ openingDelimitersStack . length = openingDelimiterIndex ;
67+
68+ result . push ( {
69+ delimiterName : delimiterInfo . delimiterName ,
70+ openingDelimiterRange : openingDelimiter . range ,
71+ closingDelimiterRange : occurrence . range ,
72+ } ) ;
5573 }
5674
5775 return result ;
5876}
5977
78+ // When multiple interpretations are possible, choose the one whose opener is
79+ // closest on the stack, which preserves normal nesting behavior.
80+ function getClosestOpeningDelimiterMatch (
81+ occurrence : DelimiterOccurrence ,
82+ openingDelimitersStack : OpeningDelimiterStackOccurrence [ ] ,
83+ ) : OpeningDelimiterMatch | undefined {
84+ let closestMatch : OpeningDelimiterMatch | undefined ;
85+
86+ for ( const delimiterInfo of occurrence . delimiterInfos ) {
87+ if ( delimiterInfo . side === "left" ) {
88+ continue ;
89+ }
90+
91+ const openingDelimiterIndex = findLastIndex (
92+ openingDelimitersStack ,
93+ ( o ) =>
94+ o . delimiterInfo . delimiterName === delimiterInfo . delimiterName &&
95+ isSameTextFragment ( o . textFragmentRange , occurrence . textFragmentRange ) &&
96+ isValidLine ( delimiterInfo . isSingleLine , o . range , occurrence . range ) ,
97+ ) ;
98+
99+ // No opening delimiter found for this interpretation, so skip it
100+ if ( openingDelimiterIndex === - 1 ) {
101+ continue ;
102+ }
103+
104+ // If this is the closest opening delimiter so far, remember it
105+ if (
106+ closestMatch == null ||
107+ openingDelimiterIndex > closestMatch . openingDelimiterIndex
108+ ) {
109+ closestMatch = { delimiterInfo, openingDelimiterIndex } ;
110+ }
111+ }
112+
113+ return closestMatch ;
114+ }
115+
60116function isSameTextFragment (
61117 a : Range | undefined ,
62118 b : Range | undefined ,
0 commit comments