Skip to content

Commit a5e8366

Browse files
Prevent overlapping pairs (#2754)
Matching pairs can be nested, but they can't be overlapping eg `( [ ) ]` In this example the parentheses are a actual pair since they came first, but the square brackets are not a pair. This corresponds to the syntax highlighting in vscode. Fixes #1318 ## Checklist - [x] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [/] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [/] I have not broken the cheatsheet --------- Co-authored-by: Phil Cohen <[email protected]>
1 parent b62d901 commit a5e8366

File tree

10 files changed

+139
-185
lines changed

10 files changed

+139
-185
lines changed

data/fixtures/recorded/surroundingPair/parseTreeParity/takeOutside13.yml

Lines changed: 0 additions & 23 deletions
This file was deleted.

data/fixtures/recorded/surroundingPair/parseTreeParity/takeOutside16.yml

Lines changed: 0 additions & 23 deletions
This file was deleted.

data/fixtures/recorded/surroundingPair/parseTreeParity/takeOutside25.yml

Lines changed: 0 additions & 23 deletions
This file was deleted.

data/fixtures/recorded/surroundingPair/textual/takeOutside13.yml

Lines changed: 0 additions & 23 deletions
This file was deleted.

data/fixtures/recorded/surroundingPair/textual/takeOutside16.yml

Lines changed: 0 additions & 23 deletions
This file was deleted.

data/fixtures/recorded/surroundingPair/textual/takeOutside25.yml

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
(a < b)
2+
(c > d)
3+
---
4+
5+
[#1 Content] =
6+
[#1 Removal] =
7+
[#1 Domain] = 0:0-0:7
8+
>-------<
9+
0| (a < b)
10+
11+
[#1 Interior] = 0:1-0:6
12+
>-----<
13+
0| (a < b)
14+
15+
[#1 Boundary L] = 0:0-0:1
16+
>-<
17+
0| (a < b)
18+
19+
[#1 Boundary R] = 0:6-0:7
20+
>-<
21+
0| (a < b)
22+
23+
[#1 Insertion delimiter] = " "
24+
25+
26+
[#2 Content] =
27+
[#2 Removal] =
28+
[#2 Domain] = 1:0-1:7
29+
>-------<
30+
1| (c > d)
31+
32+
[#2 Interior] = 1:1-1:6
33+
>-----<
34+
1| (c > d)
35+
36+
[#2 Boundary L] = 1:0-1:1
37+
>-<
38+
1| (c > d)
39+
40+
[#2 Boundary R] = 1:6-1:7
41+
>-<
42+
1| (c > d)
43+
44+
[#2 Insertion delimiter] = " "
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
( [ ) ]
2+
---
3+
4+
[Content] =
5+
[Domain] = 0:0-0:7
6+
>-------<
7+
0| ( [ ) ]
8+
9+
[Removal] = 0:0-0:9
10+
>---------<
11+
0| ( [ ) ]
12+
13+
[Trailing delimiter] = 0:7-0:9
14+
>--<
15+
0| ( [ ) ]
16+
17+
[Interior: Content] = 0:3-0:4
18+
>-<
19+
0| ( [ ) ]
20+
[Interior: Removal] = 0:1-0:6
21+
>-----<
22+
0| ( [ ) ]
23+
24+
[Boundary L: Content] = 0:0-0:1
25+
>-<
26+
0| ( [ ) ]
27+
[Boundary L: Removal] = 0:0-0:3
28+
>---<
29+
0| ( [ ) ]
30+
31+
[Boundary R: Content] = 0:6-0:7
32+
>-<
33+
0| ( [ ) ]
34+
[Boundary R: Removal] = 0:6-0:9
35+
>---<
36+
0| ( [ ) ]
37+
38+
[Insertion delimiter] = " "
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
([)]
2+
---
3+
4+
[Content] =
5+
[Removal] =
6+
[Domain] = 0:0-0:3
7+
>---<
8+
0| ([)]
9+
10+
[Interior] = 0:1-0:2
11+
>-<
12+
0| ([)]
13+
14+
[Boundary L] = 0:0-0:1
15+
>-<
16+
0| ([)]
17+
18+
[Boundary R] = 0:2-0:3
19+
>-<
20+
0| ([)]
21+
22+
[Insertion delimiter] = " "
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { SimpleSurroundingPairName } from "@cursorless/common";
2-
import { DefaultMap } from "@cursorless/common";
1+
import type { Range } from "@cursorless/common";
2+
import findLastIndex from "lodash-es/findLastIndex";
33
import type { DelimiterOccurrence, SurroundingPairOccurrence } from "./types";
44

55
/**
@@ -13,14 +13,7 @@ export function getSurroundingPairOccurrences(
1313
delimiterOccurrences: DelimiterOccurrence[],
1414
): SurroundingPairOccurrence[] {
1515
const result: SurroundingPairOccurrence[] = [];
16-
17-
/**
18-
* A map from delimiter names to occurrences of the opening delimiter
19-
*/
20-
const openingDelimiterOccurrences = new DefaultMap<
21-
SimpleSurroundingPairName,
22-
DelimiterOccurrence[]
23-
>(() => []);
16+
const openingDelimitersStack: DelimiterOccurrence[] = [];
2417

2518
for (const occurrence of delimiterOccurrences) {
2619
const {
@@ -29,48 +22,29 @@ export function getSurroundingPairOccurrences(
2922
range,
3023
} = occurrence;
3124

32-
let openingDelimiters = openingDelimiterOccurrences.get(delimiterName);
33-
34-
if (isSingleLine) {
35-
// If single line, remove all opening delimiters that are not on the same line
36-
// as occurrence
37-
openingDelimiters = openingDelimiters.filter(
38-
(openingDelimiter) =>
39-
openingDelimiter.range.start.line === range.start.line,
40-
);
41-
openingDelimiterOccurrences.set(delimiterName, openingDelimiters);
42-
}
43-
44-
/**
45-
* A list of opening delimiters that are relevant to the current occurrence.
46-
* We exclude delimiters that are not in the same text fragment range as the
47-
* current occurrence.
48-
*/
49-
const relevantOpeningDelimiters = openingDelimiters.filter(
50-
(openingDelimiter) =>
51-
(textFragmentRange == null &&
52-
openingDelimiter.textFragmentRange == null) ||
53-
(textFragmentRange != null &&
54-
openingDelimiter.textFragmentRange != null &&
55-
openingDelimiter.textFragmentRange.isRangeEqual(textFragmentRange)),
56-
);
57-
58-
if (
59-
side === "left" ||
60-
(side === "unknown" && relevantOpeningDelimiters.length % 2 === 0)
61-
) {
62-
openingDelimiters.push(occurrence);
25+
if (side === "left") {
26+
openingDelimitersStack.push(occurrence);
6327
} else {
64-
const openingDelimiter = relevantOpeningDelimiters.at(-1);
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+
);
6535

66-
if (openingDelimiter == null) {
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+
}
6741
continue;
6842
}
6943

70-
openingDelimiters.splice(
71-
openingDelimiters.lastIndexOf(openingDelimiter),
72-
1,
73-
);
44+
const openingDelimiter = openingDelimitersStack[openingDelimiterIndex];
45+
46+
// Pop stack up to and including the opening delimiter
47+
openingDelimitersStack.length = openingDelimiterIndex;
7448

7549
result.push({
7650
delimiterName: delimiterName,
@@ -82,3 +56,17 @@ export function getSurroundingPairOccurrences(
8256

8357
return result;
8458
}
59+
60+
function isSameTextFragment(
61+
a: Range | undefined,
62+
b: Range | undefined,
63+
): boolean {
64+
if (a == null || b == null) {
65+
return a === b;
66+
}
67+
return a.isRangeEqual(b);
68+
}
69+
70+
function isValidLine(isSingleLine: boolean, a: Range, b: Range): boolean {
71+
return !isSingleLine || a.start.line === b.start.line;
72+
}

0 commit comments

Comments
 (0)