Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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,20 @@ class InsertionDelimiter extends QueryPredicateOperator<InsertionDelimiter> {
}
}

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 +238,6 @@ export const queryPredicateOperators = [
new ShrinkToMatch(),
new AllowMultiple(),
new InsertionDelimiter(),
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
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 @@ -109,7 +109,7 @@ export class ItemStage implements ModifierStage {
itemInfo: ItemInfo,
removalRange?: Range,
) {
const delimiter = getInsertionDelimiter(
const insertionDelimiter = getInsertionDelimiter(
target.editor,
itemInfo.leadingDelimiterRange,
itemInfo.trailingDelimiterRange,
Expand All @@ -120,7 +120,7 @@ export class ItemStage implements ModifierStage {
editor: target.editor,
isReversed: target.isReversed,
contentRange: itemInfo.contentRange,
delimiter,
insertionDelimiter,
leadingDelimiterRange: itemInfo.leadingDelimiterRange,
trailingDelimiterRange: itemInfo.trailingDelimiterRange,
removalRange,
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Selection,
TextEditor,
} from "@cursorless/common";
import { escapeRegExp } from "lodash";
import { EditWithRangeUpdater } from "../../typings/Types";
import {
Destination,
Expand Down Expand Up @@ -44,6 +45,10 @@ export class DestinationImpl implements Destination {
return this.target.insertionDelimiter;
}

private get insertionPrefix(): string | undefined {
return this.target.insertionPrefix;
}

get isRaw(): boolean {
return this.target.isRaw;
}
Expand All @@ -65,9 +70,10 @@ export class DestinationImpl implements Destination {

getEditNewActionType(): EditNewActionType {
if (
this.insertionDelimiter === "\n" &&
this.insertionMode === "after" &&
this.target.contentRange.isSingleLine
this.target.contentRange.isSingleLine &&
this.insertionDelimiter === "\n" &&
this.insertionPrefix == null
) {
// If the target that we're wrapping is not a single line, then we
// want to compute indentation based on the entire target. Otherwise,
Expand Down Expand Up @@ -116,12 +122,37 @@ export class DestinationImpl implements Destination {

if (this.isLineDelimiter) {
const line = this.editor.document.lineAt(contentPosition);
const nonWhitespaceCharacterIndex = this.isBefore
? line.firstNonWhitespaceCharacterIndex
: line.lastNonWhitespaceCharacterIndex;

const useFullLineRange = (() => {
if (this.isBefore) {
// With an insertion prefix the position we want to insert before is extended to the left
if (this.insertionPrefix != null) {
// The leading text on the same line before the content range
const text = line.text.slice(
line.firstNonWhitespaceCharacterIndex,
contentPosition.character,
);

// The leading text on the line is the prefix with optional whitespace
const pattern = new RegExp(
`^${escapeRegExp(this.insertionPrefix)}\\s*$`,
);
return pattern.test(text);
}

return (
contentPosition.character ===
line.firstNonWhitespaceCharacterIndex
);
}

return (
contentPosition.character === line.lastNonWhitespaceCharacterIndex
);
})();

// Use the full line to include indentation
if (contentPosition.character === nonWhitespaceCharacterIndex) {
if (useFullLineRange) {
return this.isBefore ? line.range.start : line.range.end;
}
}
Expand All @@ -133,20 +164,23 @@ export class DestinationImpl implements Destination {
}

private getEditText(text: string) {
const insertionText = this.indentationString + text;
const insertionText =
this.indentationString + (this.insertionPrefix ?? "") + text;

return this.isBefore
? insertionText + this.insertionDelimiter
: this.insertionDelimiter + insertionText;
}

private updateRange(range: Range, text: string) {
const baseStartOffset = this.editor.document.offsetAt(range.start);
const baseStartOffset =
this.editor.document.offsetAt(range.start) +
this.indentationString.length +
(this.insertionPrefix?.length ?? 0);

const startIndex = this.isBefore
? baseStartOffset + this.indentationString.length
: baseStartOffset +
this.getLengthOfInsertionDelimiter() +
this.indentationString.length;
? baseStartOffset
: baseStartOffset + this.getLengthOfInsertionDelimiter();

const endIndex = startIndex + text.length;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {

export interface ScopeTypeTargetParameters extends CommonTargetParameters {
readonly scopeTypeType: SimpleScopeTypeType;
readonly delimiter?: string;
readonly insertionDelimiter?: string;
readonly insertionPrefix?: string;
readonly removalRange?: Range;
readonly interiorRange?: Range;
readonly leadingDelimiterRange?: Range;
Expand All @@ -32,6 +33,7 @@ export class ScopeTypeTarget extends BaseTarget<ScopeTypeTargetParameters> {
private trailingDelimiterRange_?: Range;
private hasDelimiterRange_: boolean;
insertionDelimiter: string;
insertionPrefix?: string;

constructor(parameters: ScopeTypeTargetParameters) {
super(parameters);
Expand All @@ -41,7 +43,9 @@ export class ScopeTypeTarget extends BaseTarget<ScopeTypeTargetParameters> {
this.leadingDelimiterRange_ = parameters.leadingDelimiterRange;
this.trailingDelimiterRange_ = parameters.trailingDelimiterRange;
this.insertionDelimiter =
parameters.delimiter ?? getDelimiter(parameters.scopeTypeType);
parameters.insertionDelimiter ??
getInsertionDelimiter(parameters.scopeTypeType);
this.insertionPrefix = parameters.insertionPrefix;
this.hasDelimiterRange_ =
!!this.leadingDelimiterRange_ || !!this.trailingDelimiterRange_;
}
Expand Down Expand Up @@ -126,7 +130,8 @@ export class ScopeTypeTarget extends BaseTarget<ScopeTypeTargetParameters> {
protected getCloneParameters() {
return {
...this.state,
delimiter: this.insertionDelimiter,
insertionDelimiter: this.insertionDelimiter,
insertionPrefix: this.insertionPrefix,
removalRange: undefined,
interiorRange: undefined,
scopeTypeType: this.scopeTypeType_,
Expand All @@ -137,7 +142,7 @@ export class ScopeTypeTarget extends BaseTarget<ScopeTypeTargetParameters> {
}
}

function getDelimiter(scopeType: SimpleScopeTypeType): string {
function getInsertionDelimiter(scopeType: SimpleScopeTypeType): string {
switch (scopeType) {
case "class":
case "namedFunction":
Expand Down
3 changes: 3 additions & 0 deletions packages/cursorless-engine/src/typings/target.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export interface Target {
/** If this selection has a delimiter use it for inserting before or after the target. For example, new line for a line or paragraph and comma for a list or argument */
readonly insertionDelimiter: string;

/** If this selection has a prefix use it for inserting before the target. For example, dash or asterisk for a markdown item */
readonly insertionPrefix?: string;

/** If true this target should be treated as a line */
readonly isLine: boolean;

Expand Down
Loading