Skip to content

Commit 33e5368

Browse files
AndreasArvidssonphillcopre-commit-ci-lite[bot]
authored
Properly handle trailing commas in python arguments (#2780)
Fixes #807 ## 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]> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent 62d49eb commit 33e5368

File tree

11 files changed

+143
-25
lines changed

11 files changed

+143
-25
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
def foo(
2+
aaa,
3+
bbb,
4+
):
5+
pass
6+
---
7+
8+
[#1 Content] =
9+
[#1 Domain] = 1:4-1:7
10+
>---<
11+
1| aaa,
12+
13+
[#1 Removal] = 1:4-2:4
14+
>----
15+
1| aaa,
16+
2| bbb,
17+
----<
18+
19+
[#1 Trailing delimiter] = 1:7-2:4
20+
>-
21+
1| aaa,
22+
2| bbb,
23+
----<
24+
25+
[#1 Insertion delimiter] = ",\n"
26+
27+
28+
[#2 Content] =
29+
[#2 Domain] = 2:4-2:7
30+
>---<
31+
2| bbb,
32+
33+
[#2 Removal] = 1:7-2:7
34+
>-
35+
1| aaa,
36+
2| bbb,
37+
-------<
38+
39+
[#2 Leading delimiter] = 1:7-2:4
40+
>-
41+
1| aaa,
42+
2| bbb,
43+
----<
44+
45+
[#2 Trailing delimiter] = 2:7-2:8
46+
>-<
47+
2| bbb,
48+
49+
[#2 Insertion delimiter] = ",\n"

packages/cursorless-engine/src/languages/TreeSitterQuery/validateQueryCaptures.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ const wildcard = "_";
55
const captureNames = [wildcard, ...simpleScopeTypeTypes];
66

77
const positionRelationships = ["prefix", "leading", "trailing"];
8-
const positionSuffixes = ["startOf", "endOf"];
8+
const positionSuffixes = [
9+
"startOf",
10+
"endOf",
11+
"start.startOf",
12+
"start.endOf",
13+
"end.startOf",
14+
"end.endOf",
15+
];
916

1017
const rangeRelationships = [
1118
"domain",

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BoundedScopeHandler.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
ScopeType,
55
TextEditor,
66
} from "@cursorless/common";
7+
import type { Target } from "../../../typings/target.types";
78
import type { InteriorTarget } from "../../targets";
89
import {
910
BoundedParagraphTarget,
@@ -18,7 +19,7 @@ import type {
1819
ScopeIteratorRequirements,
1920
} from "./scopeHandler.types";
2021
import type { ScopeHandlerFactory } from "./ScopeHandlerFactory";
21-
import type { Target } from "../../../typings/target.types";
22+
import { isEveryScopeModifier } from "./util/isHintsEveryScope";
2223

2324
abstract class BoundedBaseScopeHandler extends BaseScopeHandler {
2425
protected readonly isHierarchical = true;
@@ -91,11 +92,10 @@ abstract class BoundedBaseScopeHandler extends BaseScopeHandler {
9192
direction,
9293
{
9394
...hints,
94-
// For every (skipAncestorScopes=true) we don't want to go outside of the surrounding pair
95-
containment:
96-
hints.containment == null && hints.skipAncestorScopes
97-
? "required"
98-
: hints.containment,
95+
// For the every scope, we don't want to go outside of the surrounding pair
96+
containment: isEveryScopeModifier(hints)
97+
? "required"
98+
: hints.containment,
9999
},
100100
),
101101
);

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
ScopeIteratorRequirements,
1515
} from "../scopeHandler.types";
1616
import type { ScopeHandlerFactory } from "../ScopeHandlerFactory";
17+
import { isEveryScopeModifier } from "../util/isHintsEveryScope";
1718
import { OneWayNestedRangeFinder } from "../util/OneWayNestedRangeFinder";
1819
import { OneWayRangeFinder } from "../util/OneWayRangeFinder";
1920
import { collectionItemTextualIterationScopeHandler } from "./collectionItemTextualIterationScopeHandler";
@@ -42,7 +43,7 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler {
4243
direction: Direction,
4344
hints: ScopeIteratorRequirements,
4445
): Iterable<TargetScope> {
45-
const isEveryScope = hints.containment == null && hints.skipAncestorScopes;
46+
const isEveryScope = isEveryScopeModifier(hints);
4647
const separatorRanges = getSeparatorOccurrences(editor.document);
4748
const interiorRanges = getInteriorRanges(
4849
this.scopeHandlerFactory,

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/createTargetScope.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type TextEditor, Range } from "@cursorless/common";
2-
import { getRangeLength } from "../../../../util/rangeUtils";
32
import { ScopeTypeTarget } from "../../../targets";
43
import type { TargetScope } from "../scope.types";
4+
import { getCollectionItemRemovalRange } from "../util/getCollectionItemRemovalRange";
55

66
export function createTargetScope(
77
isEveryScope: boolean,
@@ -15,22 +15,19 @@ export function createTargetScope(
1515
previousRange != null
1616
? new Range(previousRange.end, contentRange.start)
1717
: undefined;
18+
1819
const trailingDelimiterRange =
1920
nextRange != null
2021
? new Range(contentRange.end, nextRange.start)
2122
: undefined;
2223

23-
// We have both leading and trailing delimiter ranges
24-
// If the leading one is longer/more specific, prefer to use that for removal;
25-
// otherwise use undefined to fallback to the default behavior (often trailing)
26-
const removalRange =
27-
!isEveryScope &&
28-
leadingDelimiterRange != null &&
29-
trailingDelimiterRange != null &&
30-
getRangeLength(editor, leadingDelimiterRange) >
31-
getRangeLength(editor, trailingDelimiterRange)
32-
? contentRange.union(leadingDelimiterRange)
33-
: undefined;
24+
const removalRange = getCollectionItemRemovalRange(
25+
isEveryScope,
26+
editor,
27+
contentRange,
28+
leadingDelimiterRange,
29+
trailingDelimiterRange,
30+
);
3431

3532
const insertionDelimiter = iterationRange.isSingleLine ? ", " : ",\n";
3633

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/BaseTreeSitterScopeHandler.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { BaseScopeHandler } from "../BaseScopeHandler";
88
import { compareTargetScopes } from "../compareTargetScopes";
99
import type { TargetScope } from "../scope.types";
1010
import type { ScopeIteratorRequirements } from "../scopeHandler.types";
11+
import { isEveryScopeModifier } from "../util/isHintsEveryScope";
1112
import { getQuerySearchRange } from "./getQuerySearchRange";
1213
import { mergeAdjacentBy } from "./mergeAdjacentBy";
1314

@@ -24,6 +25,7 @@ export abstract class BaseTreeSitterScopeHandler extends BaseScopeHandler {
2425
hints: ScopeIteratorRequirements,
2526
): Iterable<TargetScope> {
2627
const { document } = editor;
28+
const isEveryScope = isEveryScopeModifier(hints);
2729

2830
/** Narrow the range within which tree-sitter searches, for performance */
2931
const { start, end } = getQuerySearchRange(
@@ -35,7 +37,7 @@ export abstract class BaseTreeSitterScopeHandler extends BaseScopeHandler {
3537

3638
const scopes = this.query
3739
.matches(document, start, end)
38-
.map((match) => this.matchToScope(editor, match))
40+
.map((match) => this.matchToScope(editor, match, isEveryScope))
3941
.filter((scope): scope is ExtendedTargetScope => scope != null)
4042
.sort((a, b) => compareTargetScopes(direction, position, a, b));
4143

@@ -88,12 +90,14 @@ export abstract class BaseTreeSitterScopeHandler extends BaseScopeHandler {
8890
* relevant to this scope handler
8991
* @param editor The editor in which the match was found
9092
* @param match The match to convert to a scope
93+
* @param isEveryScope Whether the scope is being used in an "every" modifier
9194
* @returns The scope, or undefined if the match is not relevant to this scope
9295
* handler
9396
*/
9497
protected abstract matchToScope(
9598
editor: TextEditor,
9699
match: QueryMatch,
100+
isEveryScope: boolean,
97101
): ExtendedTargetScope | undefined;
98102
}
99103

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterIterationScopeHandler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export class TreeSitterIterationScopeHandler extends BaseTreeSitterScopeHandler
3434
protected matchToScope(
3535
editor: TextEditor,
3636
match: QueryMatch,
37+
_isEveryScope: boolean,
3738
): ExtendedTargetScope | undefined {
3839
const scopeTypeType = this.iterateeScopeType.type;
3940

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { TreeSitterQuery } from "../../../../languages/TreeSitterQuery";
33
import type { QueryMatch } from "../../../../languages/TreeSitterQuery/QueryCapture";
44
import { ScopeTypeTarget } from "../../../targets/ScopeTypeTarget";
55
import type { CustomScopeType } from "../scopeHandler.types";
6+
import { getCollectionItemRemovalRange } from "../util/getCollectionItemRemovalRange";
67
import type { ExtendedTargetScope } from "./BaseTreeSitterScopeHandler";
78
import { BaseTreeSitterScopeHandler } from "./BaseTreeSitterScopeHandler";
89
import { TreeSitterIterationScopeHandler } from "./TreeSitterIterationScopeHandler";
@@ -36,6 +37,7 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler {
3637
protected matchToScope(
3738
editor: TextEditor,
3839
match: QueryMatch,
40+
isEveryScope: boolean,
3941
): ExtendedTargetScope | undefined {
4042
const scopeTypeType = this.scopeType.type;
4143

@@ -51,8 +53,6 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler {
5153
const domain =
5254
getRelatedRange(match, scopeTypeType, "domain", true) ?? contentRange;
5355

54-
const removalRange = getRelatedRange(match, scopeTypeType, "removal", true);
55-
5656
const interiorRange = getRelatedRange(
5757
match,
5858
scopeTypeType,
@@ -81,6 +81,22 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler {
8181
true,
8282
)?.with(contentRange.end);
8383

84+
let removalRange = getRelatedRange(match, scopeTypeType, "removal", true);
85+
86+
if (
87+
removalRange == null &&
88+
(scopeTypeType === "collectionItem" ||
89+
scopeTypeType === "argumentOrParameter")
90+
) {
91+
removalRange = getCollectionItemRemovalRange(
92+
isEveryScope,
93+
editor,
94+
contentRange,
95+
leadingDelimiterRange,
96+
trailingDelimiterRange,
97+
);
98+
}
99+
84100
return {
85101
editor,
86102
domain,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { Range, TextEditor } from "@cursorless/common";
2+
3+
import { getRangeLength } from "../../../../util/rangeUtils";
4+
5+
/**
6+
* Picks which of the leading and trailing delimiter ranges to use for removal when both are present.
7+
*/
8+
export function getCollectionItemRemovalRange(
9+
isEveryScope: boolean,
10+
editor: TextEditor,
11+
contentRange: Range,
12+
leadingDelimiterRange: Range | undefined,
13+
trailingDelimiterRange: Range | undefined,
14+
): Range | undefined {
15+
if (isEveryScope) {
16+
// Force a fallback to the default behavior (often trailing)
17+
return undefined;
18+
}
19+
// If the leading one is longer/more specific, prefer to use that for removal
20+
if (
21+
leadingDelimiterRange != null &&
22+
trailingDelimiterRange != null &&
23+
getRangeLength(editor, leadingDelimiterRange) >
24+
getRangeLength(editor, trailingDelimiterRange)
25+
) {
26+
return contentRange.union(leadingDelimiterRange);
27+
}
28+
return undefined;
29+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { ScopeIteratorRequirements } from "../scopeHandler.types";
2+
3+
/**
4+
* Returns whether the hints belong to the every scope modifier.
5+
*/
6+
export function isEveryScopeModifier(
7+
hints: ScopeIteratorRequirements,
8+
): boolean {
9+
return hints.containment == null && hints.skipAncestorScopes;
10+
}

0 commit comments

Comments
 (0)