Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export interface QueryCapture {

/** The insertion delimiter to use if any */
readonly insertionDelimiter: string | undefined;

/** The insertion prefix to use if any */
readonly insertionPrefix: string | undefined;
}

/**
Expand All @@ -64,6 +67,7 @@ export interface MutableQueryCapture extends QueryCapture {
range: Range;
allowMultiple: boolean;
insertionDelimiter: string | undefined;
insertionPrefix: string | undefined;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export class TreeSitterQuery {
document,
range: getNodeRange(node),
insertionDelimiter: undefined,
insertionPrefix: undefined,
allowMultiple: false,
})),
}),
Expand Down Expand Up @@ -117,6 +118,9 @@ export class TreeSitterQuery {
insertionDelimiter: captures.find(
(capture) => capture.insertionDelimiter != null,
)?.insertionDelimiter,
insertionPrefix: captures.find(
(capture) => capture.insertionPrefix != null,
)?.insertionPrefix,
};
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import assert from "assert";

interface TestCase {
name: string;
captures: Omit<QueryCapture, "allowMultiple" | "insertionDelimiter">[];
captures: Omit<
QueryCapture,
"allowMultiple" | "insertionDelimiter" | "insertionPrefix"
>[];
isValid: boolean;
expectedErrorMessageIds: string[];
}
Expand Down Expand Up @@ -193,6 +196,7 @@ suite("checkCaptureStartEnd", () => {
...capture,
allowMultiple: false,
insertionDelimiter: undefined,
insertionPrefix: undefined,
})),
messages,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,48 @@ class InsertionDelimiter extends QueryPredicateOperator<InsertionDelimiter> {
}
}

/**
* A predicate operator that sets the insertion delimiter of the match based on a condition. For
* example, `(#conditional-insertion-delimiter! @foo @bar ", " ",\n")` will set the insertion delimiter
* of the `@foo` capture to `", "` if the `@bar` capture is a single line and `",\n"` otherwise.
*/
class ConditionalInsertionDelimiter extends QueryPredicateOperator<ConditionalInsertionDelimiter> {
name = "conditional-insertion-delimiter!" as const;
schema = z.tuple([q.node, q.node, q.string, q.string]);

run(
nodeInfo: MutableQueryCapture,
nodeCondition: MutableQueryCapture,
insertionDelimiterConsequence: string,
insertionDelimiterAlternative: string,
) {
nodeInfo.insertionDelimiter = nodeCondition.range.isSingleLine
? insertionDelimiterConsequence
: insertionDelimiterAlternative;

return true;
}
}

/**
* A predicate operator that sets the insertion prefix of the match. For
* example, `(#insertion-prefix! @foo "* ")` will set the insertion prefix
* of the `@foo` capture to `"* "`.
*/
class InsertionPrefix extends QueryPredicateOperator<InsertionPrefix> {
name = "insertion-prefix!" as const;
schema = z.union([z.tuple([q.node, q.string]), z.tuple([q.node, q.node])]);

run(nodeInfo: MutableQueryCapture, prefix: string | MutableQueryCapture) {
nodeInfo.insertionPrefix =
typeof prefix === "string"
? prefix
: nodeInfo.document.getText(prefix.range);

return true;
}
}

export const queryPredicateOperators = [
new Log(),
new NotType(),
Expand All @@ -224,5 +266,7 @@ export const queryPredicateOperators = [
new ShrinkToMatch(),
new AllowMultiple(),
new InsertionDelimiter(),
new ConditionalInsertionDelimiter(),
new InsertionPrefix(),
new HasMultipleChildrenOfType(),
];
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ function fillOutCapture(capture: NameRange): MutableQueryCapture {
...capture,
allowMultiple: false,
insertionDelimiter: undefined,
insertionPrefix: undefined,
document: null as unknown as TextDocument,
node: null as unknown as SyntaxNode,
};
Expand Down
53 changes: 3 additions & 50 deletions packages/cursorless-engine/src/languages/markdown.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import { Range, SimpleScopeTypeType, TextEditor } from "@cursorless/common";
import { SimpleScopeTypeType, TextEditor } from "@cursorless/common";
import type { SyntaxNode } from "web-tree-sitter";
import {
NodeFinder,
NodeMatcherAlternative,
SelectionWithContext,
} from "../typings/Types";
import { getMatchesInRange } from "../util/getMatchesInRange";
import { NodeFinder, NodeMatcherAlternative } from "../typings/Types";
import { leadingSiblingNodeFinder, patternFinder } from "../util/nodeFinders";
import { createPatternMatchers, matcher } from "../util/nodeMatchers";
import {
extendUntilNextMatchingSiblingOrLast,
selectWithLeadingDelimiter,
} from "../util/nodeSelectors";
import { extendUntilNextMatchingSiblingOrLast } from "../util/nodeSelectors";
import { shrinkRangeToFitContent } from "../util/selectionUtils";

const HEADING_MARKER_TYPES = [
Expand Down Expand Up @@ -69,48 +61,9 @@ function sectionMatcher(...patterns: string[]) {
return matcher(leadingSiblingNodeFinder(finder), sectionExtractor);
}

const itemLeadingDelimiterExtractor = selectWithLeadingDelimiter(
"list_marker_parenthesis",
"list_marker_dot",
"list_marker_star",
"list_marker_minus",
"list_marker_plus",
);

function excludeTrailingNewline(editor: TextEditor, range: Range) {
const matches = getMatchesInRange(/\r?\n\s*$/g, editor, range);

if (matches.length > 0) {
return new Range(range.start, matches[0].start);
}

return range;
}

function itemExtractor(
editor: TextEditor,
node: SyntaxNode,
): SelectionWithContext {
const { selection } = itemLeadingDelimiterExtractor(editor, node);
const line = editor.document.lineAt(selection.start);
const leadingRange = new Range(line.range.start, selection.start);
const indent = editor.document.getText(leadingRange);

return {
context: {
containingListDelimiter: `\n${indent}`,
leadingDelimiterRange: leadingRange,
},
selection: excludeTrailingNewline(editor, selection).toSelection(
selection.isReversed,
),
};
}

const nodeMatchers: Partial<
Record<SimpleScopeTypeType, NodeMatcherAlternative>
> = {
collectionItem: matcher(patternFinder("list_item.paragraph!"), itemExtractor),
section: sectionMatcher("atx_heading"),
sectionLevelOne: sectionMatcher("atx_heading.atx_h1_marker"),
sectionLevelTwo: sectionMatcher("atx_heading.atx_h2_marker"),
Expand Down
3 changes: 1 addition & 2 deletions packages/cursorless-engine/src/languages/typescript.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { SimpleScopeTypeType } from "@cursorless/common";
import { NodeMatcherAlternative } from "../typings/Types";
import { argumentMatcher, createPatternMatchers } from "../util/nodeMatchers";
import { createPatternMatchers } from "../util/nodeMatchers";

const nodeMatchers: Partial<
Record<SimpleScopeTypeType, NodeMatcherAlternative>
> = {
collectionItem: "jsx_attribute",
argumentOrParameter: argumentMatcher("formal_parameters", "arguments"),
};

export const patternMatchers = createPatternMatchers(nodeMatchers);
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,17 @@ export class EveryScopeStage implements ModifierStage {
if (scopes == null) {
// If target had no explicit range, or was contained by a single target
// instance, expand to iteration scope before overlapping
scopes = this.getDefaultIterationRange(
scopeHandler,
this.scopeHandlerFactory,
target,
).flatMap((iterationRange) =>
getScopesOverlappingRange(scopeHandler, editor, iterationRange),
);
try {
Copy link
Member Author

@AndreasArvidsson AndreasArvidsson Dec 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getDefaultIterationRange throws an error so we never got to the legacy implementation below

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a NoContainingScopeError? If so I'd argue we should look for that error specifically

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. Fixed now

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just commenting from the future to say that we removed this :D #2740

scopes = this.getDefaultIterationRange(
scopeHandler,
this.scopeHandlerFactory,
target,
).flatMap((iterationRange) =>
getScopesOverlappingRange(scopeHandler, editor, iterationRange),
);
} catch (error) {
scopes = [];
}
}

if (scopes.length === 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
} from "@cursorless/common";
import { LanguageDefinitions } from "../../../languages/LanguageDefinitions";
import { Target } from "../../../typings/target.types";
import { getInsertionDelimiter } from "../../../util/nodeSelectors";
import { getRangeLength } from "../../../util/rangeUtils";
import { ModifierStage } from "../../PipelineStages.types";
import { ScopeTypeTarget } from "../../targets";
Expand Down Expand Up @@ -109,25 +108,34 @@ export class ItemStage implements ModifierStage {
itemInfo: ItemInfo,
removalRange?: Range,
) {
const delimiter = getInsertionDelimiter(
target.editor,
const insertionDelimiter = getInsertionDelimiter(
itemInfo.leadingDelimiterRange,
itemInfo.trailingDelimiterRange,
", ",
);
return new ScopeTypeTarget({
scopeTypeType: this.modifier.scopeType.type as SimpleScopeTypeType,
editor: target.editor,
isReversed: target.isReversed,
contentRange: itemInfo.contentRange,
delimiter,
insertionDelimiter,
leadingDelimiterRange: itemInfo.leadingDelimiterRange,
trailingDelimiterRange: itemInfo.trailingDelimiterRange,
removalRange,
});
}
}

function getInsertionDelimiter(
leadingDelimiterRange?: Range,
trailingDelimiterRange?: Range,
): string {
return (leadingDelimiterRange != null &&
!leadingDelimiterRange.isSingleLine) ||
(trailingDelimiterRange != null && !trailingDelimiterRange.isSingleLine)
? ",\n"
: ", ";
}

/** Filter item infos by content range and domain intersection */
function filterItemInfos(target: Target, itemInfos: ItemInfo[]): ItemInfo[] {
return itemInfos.filter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler {
return undefined;
}

const { range: contentRange, allowMultiple, insertionDelimiter } = capture;
const {
range: contentRange,
allowMultiple,
insertionDelimiter,
insertionPrefix,
} = capture;

const domain =
getRelatedRange(match, scopeTypeType, "domain", true) ?? contentRange;
Expand Down Expand Up @@ -84,7 +89,8 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler {
leadingDelimiterRange,
trailingDelimiterRange,
interiorRange,
delimiter: insertionDelimiter,
insertionDelimiter,
insertionPrefix,
}),
],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export class LegacyContainingSyntaxScopeStage implements ModifierStage {
contentRange: contentSelection,
removalRange: removalRange,
interiorRange: interiorRange,
delimiter: containingListDelimiter,
insertionDelimiter: containingListDelimiter,
leadingDelimiterRange,
trailingDelimiterRange,
});
Expand Down
Loading