Skip to content

Commit d69c4a9

Browse files
authored
Add generalized range utility functions (#1645)
- Required by #1649 ## Checklist - [ ] 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
1 parent 0b86df9 commit d69c4a9

File tree

4 files changed

+507
-0
lines changed

4 files changed

+507
-0
lines changed

packages/common/src/types/GeneralizedRange.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,91 @@ export function toLineRange(range: Range): LineRange {
6060
export function toCharacterRange({ start, end }: Range): CharacterRange {
6161
return { type: "character", start, end };
6262
}
63+
64+
export function isGeneralizedRangeEqual(
65+
a: GeneralizedRange,
66+
b: GeneralizedRange,
67+
): boolean {
68+
if (a.type === "character" && b.type === "character") {
69+
return a.start.isEqual(b.start) && a.end.isEqual(b.end);
70+
}
71+
72+
if (a.type === "line" && b.type === "line") {
73+
return a.start === b.start && a.end === b.end;
74+
}
75+
76+
return false;
77+
}
78+
79+
/**
80+
* Determines whether {@link a} contains {@link b}. This is true if {@link a}
81+
* starts before or equal to the start of {@link b} and ends after or equal to
82+
* the end of {@link b}.
83+
*
84+
* Note that if {@link a} is a {@link CharacterRange} and {@link b} is a
85+
* {@link LineRange}, we require that the {@link LineRange} is fully contained
86+
* in the {@link CharacterRange}, because otherwise it visually looks like the
87+
* {@link LineRange} is not contained because the line range extends to the edge
88+
* of the screen.
89+
* @param a A generalized range
90+
* @param b A generalized range
91+
* @returns `true` if `a` contains `b`, `false` otherwise
92+
*/
93+
export function generalizedRangeContains(
94+
a: GeneralizedRange,
95+
b: GeneralizedRange,
96+
): boolean {
97+
if (a.type === "character") {
98+
if (b.type === "character") {
99+
// a.type === "character" && b.type === "character"
100+
return a.start.isBeforeOrEqual(b.start) && a.end.isAfterOrEqual(b.end);
101+
}
102+
103+
// a.type === "character" && b.type === "line"
104+
// Require that the line range is fully contained in the character range
105+
// because otherwise it visually looks like the line range is not contained
106+
return a.start.line < b.start && a.end.line > b.end;
107+
}
108+
109+
if (b.type === "line") {
110+
// a.type === "line" && b.type === "line"
111+
return a.start <= b.start && a.end >= b.end;
112+
}
113+
114+
// a.type === "line" && b.type === "character"
115+
return a.start <= b.start.line && a.end >= b.end.line;
116+
}
117+
118+
/**
119+
* Determines whether {@link a} touches {@link b}. This is true if {@link a}
120+
* has any intersection with {@link b}, even if the intersection is empty.
121+
*
122+
* In the case where one range is a {@link CharacterRange} and the other is a
123+
* {@link LineRange}, we return `true` if they both include at least one line
124+
* in common.
125+
* @param a A generalized range
126+
* @param b A generalized range
127+
* @returns `true` if `a` touches `b`, `false` otherwise
128+
*/
129+
export function generalizedRangeTouches(
130+
a: GeneralizedRange,
131+
b: GeneralizedRange,
132+
): boolean {
133+
if (a.type === "character") {
134+
if (b.type === "character") {
135+
// a.type === "character" && b.type === "character"
136+
return a.start.isBeforeOrEqual(b.end) && a.end.isAfterOrEqual(b.start);
137+
}
138+
139+
// a.type === "character" && b.type === "line"
140+
return a.start.line <= b.end && a.end.line >= b.start;
141+
}
142+
143+
if (b.type === "line") {
144+
// a.type === "line" && b.type === "line"
145+
return a.start <= b.end && a.end >= b.start;
146+
}
147+
148+
// a.type === "line" && b.type === "character"
149+
return a.start <= b.end.line && a.end >= b.start.line;
150+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import assert = require("assert");
2+
import { generalizedRangeContains, Position } from "..";
3+
4+
suite("generalizedRangeContains", () => {
5+
test("character", () => {
6+
assert.strictEqual(
7+
generalizedRangeContains(
8+
{
9+
type: "character",
10+
start: new Position(0, 0),
11+
end: new Position(0, 0),
12+
},
13+
{
14+
type: "character",
15+
start: new Position(0, 0),
16+
end: new Position(0, 0),
17+
},
18+
),
19+
true,
20+
);
21+
assert.strictEqual(
22+
generalizedRangeContains(
23+
{
24+
type: "character",
25+
start: new Position(0, 0),
26+
end: new Position(0, 1),
27+
},
28+
{
29+
type: "character",
30+
start: new Position(0, 0),
31+
end: new Position(0, 0),
32+
},
33+
),
34+
true,
35+
);
36+
assert.strictEqual(
37+
generalizedRangeContains(
38+
{
39+
type: "character",
40+
start: new Position(0, 0),
41+
end: new Position(0, 0),
42+
},
43+
{
44+
type: "character",
45+
start: new Position(0, 0),
46+
end: new Position(0, 1),
47+
},
48+
),
49+
false,
50+
);
51+
});
52+
53+
test("line", () => {
54+
assert.strictEqual(
55+
generalizedRangeContains(
56+
{
57+
type: "line",
58+
start: 0,
59+
end: 0,
60+
},
61+
{
62+
type: "line",
63+
start: 0,
64+
end: 0,
65+
},
66+
),
67+
true,
68+
);
69+
assert.strictEqual(
70+
generalizedRangeContains(
71+
{
72+
type: "line",
73+
start: 0,
74+
end: 1,
75+
},
76+
{
77+
type: "line",
78+
start: 0,
79+
end: 0,
80+
},
81+
),
82+
true,
83+
);
84+
assert.strictEqual(
85+
generalizedRangeContains(
86+
{
87+
type: "line",
88+
start: 0,
89+
end: 0,
90+
},
91+
{
92+
type: "line",
93+
start: 1,
94+
end: 1,
95+
},
96+
),
97+
false,
98+
);
99+
});
100+
101+
test("mixed", () => {
102+
assert.strictEqual(
103+
generalizedRangeContains(
104+
{
105+
type: "line",
106+
start: 0,
107+
end: 1,
108+
},
109+
{
110+
type: "character",
111+
start: new Position(0, 0),
112+
end: new Position(1, 1),
113+
},
114+
),
115+
true,
116+
);
117+
assert.strictEqual(
118+
generalizedRangeContains(
119+
{
120+
type: "line",
121+
start: 0,
122+
end: 0,
123+
},
124+
{
125+
type: "character",
126+
start: new Position(0, 0),
127+
end: new Position(1, 0),
128+
},
129+
),
130+
false,
131+
);
132+
assert.strictEqual(
133+
generalizedRangeContains(
134+
{
135+
type: "character",
136+
start: new Position(0, 0),
137+
end: new Position(2, 0),
138+
},
139+
{
140+
type: "line",
141+
start: 1,
142+
end: 1,
143+
},
144+
),
145+
true,
146+
);
147+
assert.strictEqual(
148+
generalizedRangeContains(
149+
{
150+
type: "character",
151+
start: new Position(0, 0),
152+
end: new Position(1, 0),
153+
},
154+
{
155+
type: "line",
156+
start: 1,
157+
end: 1,
158+
},
159+
),
160+
false,
161+
);
162+
});
163+
});
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import assert = require("assert");
2+
import { GeneralizedRange, generalizedRangeTouches, Position } from "..";
3+
4+
suite("generalizedRangeTouches", () => {
5+
test("character", () => {
6+
testRangePair(
7+
{
8+
type: "character",
9+
start: new Position(0, 0),
10+
end: new Position(0, 0),
11+
},
12+
{
13+
type: "character",
14+
start: new Position(0, 0),
15+
end: new Position(0, 0),
16+
},
17+
true,
18+
);
19+
testRangePair(
20+
{
21+
type: "character",
22+
start: new Position(0, 0),
23+
end: new Position(0, 1),
24+
},
25+
{
26+
type: "character",
27+
start: new Position(0, 0),
28+
end: new Position(0, 0),
29+
},
30+
true,
31+
);
32+
testRangePair(
33+
{
34+
type: "character",
35+
start: new Position(0, 0),
36+
end: new Position(0, 1),
37+
},
38+
{
39+
type: "character",
40+
start: new Position(0, 1),
41+
end: new Position(0, 2),
42+
},
43+
true,
44+
);
45+
testRangePair(
46+
{
47+
type: "character",
48+
start: new Position(0, 0),
49+
end: new Position(0, 0),
50+
},
51+
{
52+
type: "character",
53+
start: new Position(0, 1),
54+
end: new Position(0, 1),
55+
},
56+
false,
57+
);
58+
});
59+
60+
test("line", () => {
61+
testRangePair(
62+
{
63+
type: "line",
64+
start: 0,
65+
end: 0,
66+
},
67+
{
68+
type: "line",
69+
start: 0,
70+
end: 0,
71+
},
72+
true,
73+
);
74+
testRangePair(
75+
{
76+
type: "line",
77+
start: 0,
78+
end: 1,
79+
},
80+
{
81+
type: "line",
82+
start: 0,
83+
end: 0,
84+
},
85+
true,
86+
);
87+
testRangePair(
88+
{
89+
type: "line",
90+
start: 0,
91+
end: 0,
92+
},
93+
{
94+
type: "line",
95+
start: 1,
96+
end: 1,
97+
},
98+
false,
99+
);
100+
});
101+
102+
test("mixed", () => {
103+
testRangePair(
104+
{
105+
type: "line",
106+
start: 0,
107+
end: 0,
108+
},
109+
{
110+
type: "character",
111+
start: new Position(0, 0),
112+
end: new Position(1, 1),
113+
},
114+
true,
115+
);
116+
testRangePair(
117+
{
118+
type: "line",
119+
start: 0,
120+
end: 0,
121+
},
122+
{
123+
type: "character",
124+
start: new Position(1, 0),
125+
end: new Position(1, 1),
126+
},
127+
false,
128+
);
129+
});
130+
});
131+
132+
function testRangePair(
133+
a: GeneralizedRange,
134+
b: GeneralizedRange,
135+
expected: boolean,
136+
) {
137+
assert.strictEqual(generalizedRangeTouches(a, b), expected);
138+
assert.strictEqual(generalizedRangeTouches(b, a), expected);
139+
}

0 commit comments

Comments
 (0)