From 1046522664fed008b88ff6bef78dd9e70512fa9d Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 2 Dec 2023 14:25:02 +0100 Subject: [PATCH 01/21] Migrate markdown and yaml collection item scope --- .../languages/TreeSitterQuery/QueryCapture.ts | 4 ++ .../TreeSitterQuery/TreeSitterQuery.ts | 4 ++ .../checkCaptureStartEnd.test.ts | 6 +- .../queryPredicateOperators.ts | 15 +++++ .../rewriteStartOfEndOf.test.ts | 1 + .../src/languages/markdown.ts | 53 +---------------- .../modifiers/EveryScopeStage.ts | 18 +++--- .../modifiers/ItemStage/ItemStage.ts | 4 +- .../TreeSitterScopeHandler.ts | 10 +++- .../LegacyContainingSyntaxScopeStage.ts | 2 +- .../processTargets/targets/DestinationImpl.ts | 58 +++++++++++++++---- .../processTargets/targets/ScopeTypeTarget.ts | 13 +++-- .../src/typings/target.types.ts | 3 + .../recorded/languages/markdown/chuckItem.yml | 1 - .../languages/markdown/chuckItem2.yml | 5 +- .../languages/yaml/changeEveryItem3.yml | 12 ++-- .../recorded/languages/yaml/changeItem2.yml | 6 +- .../recorded/languages/yaml/changeItem4.yml | 8 +-- queries/markdown.scm | 15 +++++ queries/yaml.scm | 13 ++++- 20 files changed, 152 insertions(+), 99 deletions(-) diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts index f92fde3098..7bb771cdf1 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts @@ -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; } /** @@ -64,6 +67,7 @@ export interface MutableQueryCapture extends QueryCapture { range: Range; allowMultiple: boolean; insertionDelimiter: string | undefined; + insertionPrefix: string | undefined; } /** diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts index 3ecd56b1a5..44a919184d 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts @@ -81,6 +81,7 @@ export class TreeSitterQuery { document, range: getNodeRange(node), insertionDelimiter: undefined, + insertionPrefix: undefined, allowMultiple: false, })), }), @@ -117,6 +118,9 @@ export class TreeSitterQuery { insertionDelimiter: captures.find( (capture) => capture.insertionDelimiter != null, )?.insertionDelimiter, + insertionPrefix: captures.find( + (capture) => capture.insertionPrefix != null, + )?.insertionPrefix, }; }); diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts index 3d4afa5729..02bd5d1207 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts @@ -5,7 +5,10 @@ import assert from "assert"; interface TestCase { name: string; - captures: Omit[]; + captures: Omit< + QueryCapture, + "allowMultiple" | "insertionDelimiter" | "insertionPrefix" + >[]; isValid: boolean; expectedErrorMessageIds: string[]; } @@ -193,6 +196,7 @@ suite("checkCaptureStartEnd", () => { ...capture, allowMultiple: false, insertionDelimiter: undefined, + insertionPrefix: undefined, })), messages, ); diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts index a5077ffbde..4080da9a7f 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts @@ -214,6 +214,20 @@ class InsertionDelimiter extends QueryPredicateOperator { } } +class InsertionPrefix extends QueryPredicateOperator { + 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(), @@ -224,5 +238,6 @@ export const queryPredicateOperators = [ new ShrinkToMatch(), new AllowMultiple(), new InsertionDelimiter(), + new InsertionPrefix(), new HasMultipleChildrenOfType(), ]; diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/rewriteStartOfEndOf.test.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/rewriteStartOfEndOf.test.ts index cc8e99c789..b919e07c5e 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/rewriteStartOfEndOf.test.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/rewriteStartOfEndOf.test.ts @@ -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, }; diff --git a/packages/cursorless-engine/src/languages/markdown.ts b/packages/cursorless-engine/src/languages/markdown.ts index e08c481482..9ccd16ddc1 100644 --- a/packages/cursorless-engine/src/languages/markdown.ts +++ b/packages/cursorless-engine/src/languages/markdown.ts @@ -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 = [ @@ -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 > = { - 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"), diff --git a/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts index 74785bb372..98fd69f41b 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts @@ -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 { + scopes = this.getDefaultIterationRange( + scopeHandler, + this.scopeHandlerFactory, + target, + ).flatMap((iterationRange) => + getScopesOverlappingRange(scopeHandler, editor, iterationRange), + ); + } catch (error) { + scopes = []; + } } if (scopes.length === 0) { diff --git a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts index f2ef697e35..5fad07f2ed 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts @@ -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, @@ -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, diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts index d05940133f..46ca402d3e 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts @@ -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; @@ -84,7 +89,8 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler { leadingDelimiterRange, trailingDelimiterRange, interiorRange, - delimiter: insertionDelimiter, + insertionDelimiter, + insertionPrefix, }), ], }; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeTypeStages/LegacyContainingSyntaxScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeTypeStages/LegacyContainingSyntaxScopeStage.ts index baed206ed3..b099e10dca 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeTypeStages/LegacyContainingSyntaxScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeTypeStages/LegacyContainingSyntaxScopeStage.ts @@ -82,7 +82,7 @@ export class LegacyContainingSyntaxScopeStage implements ModifierStage { contentRange: contentSelection, removalRange: removalRange, interiorRange: interiorRange, - delimiter: containingListDelimiter, + insertionDelimiter: containingListDelimiter, leadingDelimiterRange, trailingDelimiterRange, }); diff --git a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts index aabb637ae4..9f94856752 100644 --- a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts +++ b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts @@ -4,6 +4,7 @@ import { Selection, TextEditor, } from "@cursorless/common"; +import { escapeRegExp } from "lodash"; import { EditWithRangeUpdater } from "../../typings/Types"; import { Destination, @@ -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; } @@ -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, @@ -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; } } @@ -133,7 +164,8 @@ 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 @@ -141,12 +173,14 @@ export class DestinationImpl implements Destination { } 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; diff --git a/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts b/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts index 02bf7814f4..864ddcd3d4 100644 --- a/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts @@ -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; @@ -32,6 +33,7 @@ export class ScopeTypeTarget extends BaseTarget { private trailingDelimiterRange_?: Range; private hasDelimiterRange_: boolean; insertionDelimiter: string; + insertionPrefix?: string; constructor(parameters: ScopeTypeTargetParameters) { super(parameters); @@ -41,7 +43,9 @@ export class ScopeTypeTarget extends BaseTarget { 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_; } @@ -126,7 +130,8 @@ export class ScopeTypeTarget extends BaseTarget { protected getCloneParameters() { return { ...this.state, - delimiter: this.insertionDelimiter, + insertionDelimiter: this.insertionDelimiter, + insertionPrefix: this.insertionPrefix, removalRange: undefined, interiorRange: undefined, scopeTypeType: this.scopeTypeType_, @@ -137,7 +142,7 @@ export class ScopeTypeTarget extends BaseTarget { } } -function getDelimiter(scopeType: SimpleScopeTypeType): string { +function getInsertionDelimiter(scopeType: SimpleScopeTypeType): string { switch (scopeType) { case "class": case "namedFunction": diff --git a/packages/cursorless-engine/src/typings/target.types.ts b/packages/cursorless-engine/src/typings/target.types.ts index 7f4042cb36..6e590e8f5b 100644 --- a/packages/cursorless-engine/src/typings/target.types.ts +++ b/packages/cursorless-engine/src/typings/target.types.ts @@ -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; diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem.yml index 2c886b2f20..fde3f9ecf9 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem.yml @@ -18,7 +18,6 @@ initialState: finalState: documentContents: |- - - whatever now selections: - anchor: {line: 1, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem2.yml index 69a6c36df6..f1e0cdb73c 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem2.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem2.yml @@ -21,8 +21,7 @@ initialState: finalState: documentContents: |- - aaa - - ddd selections: - - anchor: {line: 1, character: 0} - active: {line: 1, character: 0} + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeEveryItem3.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeEveryItem3.yml index 82bcfcb031..ea006f3821 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeEveryItem3.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeEveryItem3.yml @@ -22,10 +22,10 @@ initialState: finalState: documentContents: |- foo: - - + - + - selections: - - anchor: {line: 1, character: 2} - active: {line: 1, character: 2} - - anchor: {line: 2, character: 2} - active: {line: 2, character: 2} + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeItem2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeItem2.yml index 42ae1cc305..bdd3b9f678 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeItem2.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeItem2.yml @@ -22,8 +22,8 @@ initialState: finalState: documentContents: |- foo: - + - - 1 selections: - - anchor: {line: 1, character: 2} - active: {line: 1, character: 2} + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeItem4.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeItem4.yml index e0df6c2ea8..c44d3c1792 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeItem4.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeItem4.yml @@ -18,8 +18,8 @@ initialState: active: {line: 0, character: 0} marks: {} finalState: - documentContents: |+ - + documentContents: | + - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} diff --git a/queries/markdown.scm b/queries/markdown.scm index 42ae6b1651..e5c132de1a 100644 --- a/queries/markdown.scm +++ b/queries/markdown.scm @@ -17,3 +17,18 @@ ) @_.removal (#shrink-to-match! @name "^\\s*(?.*)$") ) @_.domain + +;;!! - 0 +;;! ^ +;;! --- +( + (list_item + (_) @prefix + (paragraph + (inline) @collectionItem + ) + ) @collectionItem.domain @collectionItem.removal + (#trim-end! @collectionItem.domain) + (#insertion-delimiter! @collectionItem "\n") + (#insertion-prefix! @collectionItem @prefix) +) diff --git a/queries/yaml.scm b/queries/yaml.scm index 10319b7137..ef93f85a8d 100644 --- a/queries/yaml.scm +++ b/queries/yaml.scm @@ -16,15 +16,22 @@ ) @map ;;!! - 0 -;;! ^^^ +;;! ^ +;;! --- ( (block_sequence (block_sequence_item)? @collectionItem.leading.start.endOf . - (block_sequence_item) @collectionItem @collectionItem.leading.end.startOf @collectionItem.trailing.end.endOf + (block_sequence_item + (_) @collectionItem @collectionItem.leading.end.startOf @collectionItem.trailing.end.endOf + ) @collectionItem.domain . - (block_sequence_item)? @collectionItem.trailing.end.startOf + (block_sequence_item + (_) @collectionItem.trailing.end.startOf + )? (#trim-end! @collectionItem) + (#insertion-delimiter! @collectionItem "\n") + (#insertion-prefix! @collectionItem "- ") ) @list (#trim-end! @list) ) From 4daa2928eb63ec051406854af3f5d9679bc8f699 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 3 Dec 2023 04:42:46 +0100 Subject: [PATCH 02/21] cleanup --- .../processTargets/targets/DestinationImpl.ts | 69 ++++++++++--------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts index 9f94856752..9843b02df3 100644 --- a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts +++ b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts @@ -1,5 +1,6 @@ import { InsertionMode, + Position, Range, Selection, TextEditor, @@ -116,43 +117,16 @@ export class DestinationImpl implements Destination { private getEditRange() { const position = (() => { - const contentPosition = this.isBefore - ? this.contentRange.start - : this.contentRange.end; + const contentPosition = this.getContentPosition(); if (this.isLineDelimiter) { const line = this.editor.document.lineAt(contentPosition); - 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 (useFullLineRange) { + // Use the full line with included indentation and trailing whitespaces + if ( + contentPosition.character === line.firstNonWhitespaceCharacterIndex || + contentPosition.character === line.lastNonWhitespaceCharacterIndex + ) { return this.isBefore ? line.range.start : line.range.end; } } @@ -163,6 +137,35 @@ export class DestinationImpl implements Destination { return new Range(position, position); } + private getContentPosition(): Position { + if (this.isBefore) { + const { start } = this.contentRange; + + // With an insertion prefix the position we want to edit/insert before is extended to the left + if (this.insertionPrefix != null) { + // The leading text on the same line before the content range + const leadingText = this.editor.document + .lineAt(start) + .text.slice(0, start.character); + + // Try to find the prefix (with optional trailing whitespace) just before the content range + const prefixIndex = leadingText.search( + new RegExp(`${escapeRegExp(this.insertionPrefix)}\\s*$`), + ); + if (prefixIndex !== -1) { + return start.with( + undefined, + start.character - (leadingText.length - prefixIndex), + ); + } + } + + return start; + } + + return this.contentRange.end; + } + private getEditText(text: string) { const insertionText = this.indentationString + (this.insertionPrefix ?? "") + text; From f90eeddfb519bf9ad61abf1e210858262cc63378 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 3 Dec 2023 04:46:39 +0100 Subject: [PATCH 03/21] cleanup --- .../src/processTargets/targets/DestinationImpl.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts index 9843b02df3..192646ad8a 100644 --- a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts +++ b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts @@ -121,12 +121,12 @@ export class DestinationImpl implements Destination { if (this.isLineDelimiter) { const line = this.editor.document.lineAt(contentPosition); + const nonWhitespaceCharacterIndex = this.isBefore + ? line.firstNonWhitespaceCharacterIndex + : line.lastNonWhitespaceCharacterIndex; // Use the full line with included indentation and trailing whitespaces - if ( - contentPosition.character === line.firstNonWhitespaceCharacterIndex || - contentPosition.character === line.lastNonWhitespaceCharacterIndex - ) { + if (contentPosition.character === nonWhitespaceCharacterIndex) { return this.isBefore ? line.range.start : line.range.end; } } From 6ddfe3fb3d4431afc26e719d3bfec958dcfca96b Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 3 Dec 2023 04:48:31 +0100 Subject: [PATCH 04/21] cleanup --- .../src/processTargets/targets/DestinationImpl.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts index 192646ad8a..219bc656b1 100644 --- a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts +++ b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts @@ -153,10 +153,8 @@ export class DestinationImpl implements Destination { new RegExp(`${escapeRegExp(this.insertionPrefix)}\\s*$`), ); if (prefixIndex !== -1) { - return start.with( - undefined, - start.character - (leadingText.length - prefixIndex), - ); + const delta = leadingText.length - prefixIndex; + return start.with(undefined, start.character - delta); } } From 32e227722eb2fd5f98b6dd07629edac66b5fe89e Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 3 Dec 2023 05:57:16 +0100 Subject: [PATCH 05/21] migrate typescript argument --- .../queryPredicateOperators.ts | 19 +++++++++++++ .../src/languages/typescript.ts | 3 +-- .../modifiers/ItemStage/ItemStage.ts | 14 +++++++--- .../processTargets/targets/DestinationImpl.ts | 5 ++-- queries/javascript.core.scm | 27 +++++++++++++++++++ queries/markdown.scm | 8 +++--- 6 files changed, 64 insertions(+), 12 deletions(-) diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts index 4080da9a7f..d344389b13 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts @@ -214,6 +214,24 @@ class InsertionDelimiter extends QueryPredicateOperator { } } +class ConditionalInsertionDelimiter extends QueryPredicateOperator { + 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; + } +} + class InsertionPrefix extends QueryPredicateOperator { name = "insertion-prefix!" as const; schema = z.union([z.tuple([q.node, q.string]), z.tuple([q.node, q.node])]); @@ -238,6 +256,7 @@ export const queryPredicateOperators = [ new ShrinkToMatch(), new AllowMultiple(), new InsertionDelimiter(), + new ConditionalInsertionDelimiter(), new InsertionPrefix(), new HasMultipleChildrenOfType(), ]; diff --git a/packages/cursorless-engine/src/languages/typescript.ts b/packages/cursorless-engine/src/languages/typescript.ts index 23470a4cc4..cd53c0bcff 100644 --- a/packages/cursorless-engine/src/languages/typescript.ts +++ b/packages/cursorless-engine/src/languages/typescript.ts @@ -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 > = { collectionItem: "jsx_attribute", - argumentOrParameter: argumentMatcher("formal_parameters", "arguments"), }; export const patternMatchers = createPatternMatchers(nodeMatchers); diff --git a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts index 5fad07f2ed..7f595356e1 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts @@ -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"; @@ -110,10 +109,8 @@ export class ItemStage implements ModifierStage { removalRange?: Range, ) { const insertionDelimiter = getInsertionDelimiter( - target.editor, itemInfo.leadingDelimiterRange, itemInfo.trailingDelimiterRange, - ", ", ); return new ScopeTypeTarget({ scopeTypeType: this.modifier.scopeType.type as SimpleScopeTypeType, @@ -128,6 +125,17 @@ export class ItemStage implements ModifierStage { } } +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( diff --git a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts index 219bc656b1..5077b0b3f6 100644 --- a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts +++ b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts @@ -26,8 +26,7 @@ export class DestinationImpl implements Destination { ) { this.contentRange = getContentRange(target.contentRange, insertionMode); this.isBefore = insertionMode === "before"; - // It's only considered a line if the delimiter is only new line symbols - this.isLineDelimiter = /^(\n)+$/.test(target.insertionDelimiter); + this.isLineDelimiter = target.insertionDelimiter.includes("\n"); this.indentationString = indentationString ?? this.isLineDelimiter ? getIndentationString(target.editor, target.contentRange) @@ -197,7 +196,7 @@ export class DestinationImpl implements Destination { if (this.editor.document.eol === "CRLF") { // This function is only called when inserting after a range. Therefore we // only care about leading new lines in the insertion delimiter. - const match = this.insertionDelimiter.match(/^\n+/); + const match = this.insertionDelimiter.match(/\n+/); const nlCount = match?.[0].length ?? 0; return this.insertionDelimiter.length + nlCount; } diff --git a/queries/javascript.core.scm b/queries/javascript.core.scm index b9745d7f38..dac7cdf72c 100644 --- a/queries/javascript.core.scm +++ b/queries/javascript.core.scm @@ -664,3 +664,30 @@ ] @statement (#not-parent-type? @statement export_statement) ) + +( + (formal_parameters + (_)? @_.leading.start.endOf + . + (_) @argumentOrParameter @_.leading.end.startOf @_.trailing.start.endOf + . + (_)? @_.trailing.end.startOf + ) @dummy + (#conditional-insertion-delimiter! @argumentOrParameter @dummy ", " ",\n") +) + +( + (arguments + (_)? @_.leading.start.endOf + . + (_) @argumentOrParameter @_.leading.end.startOf @_.trailing.start.endOf + . + (_)? @_.trailing.end.startOf + ) @dummy + (#conditional-insertion-delimiter! @argumentOrParameter @dummy ", " ",\n") +) + +[ + (formal_parameters) + (arguments) +] @argumentOrParameter.iteration diff --git a/queries/markdown.scm b/queries/markdown.scm index e5c132de1a..1e08865f87 100644 --- a/queries/markdown.scm +++ b/queries/markdown.scm @@ -23,12 +23,12 @@ ;;! --- ( (list_item - (_) @prefix + (_) @dummy (paragraph (inline) @collectionItem ) - ) @collectionItem.domain @collectionItem.removal - (#trim-end! @collectionItem.domain) + ) @_.domain @_.removal + (#trim-end! @_.domain) (#insertion-delimiter! @collectionItem "\n") - (#insertion-prefix! @collectionItem @prefix) + (#insertion-prefix! @collectionItem @dummy) ) From aca116e77877dc9c2e06cef693e480ae8e6b4a67 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 3 Dec 2023 06:00:59 +0100 Subject: [PATCH 06/21] Added the comment --- queries/javascript.core.scm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/queries/javascript.core.scm b/queries/javascript.core.scm index dac7cdf72c..39679e1622 100644 --- a/queries/javascript.core.scm +++ b/queries/javascript.core.scm @@ -665,6 +665,8 @@ (#not-parent-type? @statement export_statement) ) +;;!! foo(name: string) {} +;;! ^^^^^^^^^^^^ ( (formal_parameters (_)? @_.leading.start.endOf @@ -676,6 +678,8 @@ (#conditional-insertion-delimiter! @argumentOrParameter @dummy ", " ",\n") ) +;;!! foo("bar") +;;! ^^^^^ ( (arguments (_)? @_.leading.start.endOf From 7420dece30867b920ca66cdfec365aad9fd260c6 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 3 Dec 2023 06:08:59 +0100 Subject: [PATCH 07/21] Added comments --- .../TreeSitterQuery/queryPredicateOperators.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts index d344389b13..d93ad63222 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts @@ -214,6 +214,11 @@ class InsertionDelimiter extends QueryPredicateOperator { } } +/** + * 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 { name = "conditional-insertion-delimiter!" as const; schema = z.tuple([q.node, q.node, q.string, q.string]); @@ -232,6 +237,11 @@ class ConditionalInsertionDelimiter extends QueryPredicateOperator { name = "insertion-prefix!" as const; schema = z.union([z.tuple([q.node, q.string]), z.tuple([q.node, q.node])]); From 4dbd9f5474c864d8f27e15e7dfde50d2f9e0bba1 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 3 Dec 2023 06:33:36 +0100 Subject: [PATCH 08/21] cleanup --- .../src/processTargets/targets/DestinationImpl.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts index 5077b0b3f6..959790ae4a 100644 --- a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts +++ b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts @@ -194,10 +194,8 @@ export class DestinationImpl implements Destination { // Went inserting a new line with eol `CRLF` each `\n` will be converted to // `\r\n` and therefore the length is doubled. if (this.editor.document.eol === "CRLF") { - // This function is only called when inserting after a range. Therefore we - // only care about leading new lines in the insertion delimiter. - const match = this.insertionDelimiter.match(/\n+/); - const nlCount = match?.[0].length ?? 0; + const matches = this.insertionDelimiter.match(/\n/g); + const nlCount = matches?.length ?? 0; return this.insertionDelimiter.length + nlCount; } return this.insertionDelimiter.length; From bb0e6a6e0e9f42a0bdf50c9ed2a43d6ea049751f Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 3 Dec 2023 06:52:27 +0100 Subject: [PATCH 09/21] cleanup --- .../src/processTargets/targets/DestinationImpl.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts index 959790ae4a..4804ed919f 100644 --- a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts +++ b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts @@ -195,8 +195,9 @@ export class DestinationImpl implements Destination { // `\r\n` and therefore the length is doubled. if (this.editor.document.eol === "CRLF") { const matches = this.insertionDelimiter.match(/\n/g); - const nlCount = matches?.length ?? 0; - return this.insertionDelimiter.length + nlCount; + if (matches != null) { + return this.insertionDelimiter.length + matches.length; + } } return this.insertionDelimiter.length; } From b0e3331a673a7ebae821754c04cb97e1016c46f4 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 3 Dec 2023 08:24:54 +0100 Subject: [PATCH 10/21] Exclude comments --- .../languages/typescript/changeEveryArg.yml | 35 +++++++++++++++++++ .../languages/typescript/changeEveryArg2.yml | 35 +++++++++++++++++++ queries/javascript.core.scm | 2 ++ 3 files changed, 72 insertions(+) create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/typescript/changeEveryArg.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/typescript/changeEveryArg2.yml diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/typescript/changeEveryArg.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/typescript/changeEveryArg.yml new file mode 100644 index 0000000000..9303457e26 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/typescript/changeEveryArg.yml @@ -0,0 +1,35 @@ +languageId: typescript +command: + version: 6 + spokenForm: change every arg + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: everyScope + scopeType: {type: argumentOrParameter} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + function funk( + foo: number, // Comment1 + // Comment2 + bar?: string + ) {} + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: |- + function funk( + , // Comment1 + // Comment2 + + ) {} + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + - anchor: {line: 3, character: 2} + active: {line: 3, character: 2} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/typescript/changeEveryArg2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/typescript/changeEveryArg2.yml new file mode 100644 index 0000000000..ff58404877 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/typescript/changeEveryArg2.yml @@ -0,0 +1,35 @@ +languageId: typescript +command: + version: 6 + spokenForm: change every arg + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: everyScope + scopeType: {type: argumentOrParameter} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + funk( + "foo", // Comment1 + // Comment2 + 2 + ); + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: |- + funk( + , // Comment1 + // Comment2 + + ); + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + - anchor: {line: 3, character: 2} + active: {line: 3, character: 2} diff --git a/queries/javascript.core.scm b/queries/javascript.core.scm index 39679e1622..78f55222b9 100644 --- a/queries/javascript.core.scm +++ b/queries/javascript.core.scm @@ -675,6 +675,7 @@ . (_)? @_.trailing.end.startOf ) @dummy + (#not-type? @argumentOrParameter "comment") (#conditional-insertion-delimiter! @argumentOrParameter @dummy ", " ",\n") ) @@ -688,6 +689,7 @@ . (_)? @_.trailing.end.startOf ) @dummy + (#not-type? @argumentOrParameter "comment") (#conditional-insertion-delimiter! @argumentOrParameter @dummy ", " ",\n") ) From 6fd01d1a86a2b04e3d96db328e2747b6dd6b7ba5 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 3 Dec 2023 20:47:55 +0100 Subject: [PATCH 11/21] Added tests --- .../recorded/languages/markdown/cloneItem.yml | 30 +++++++++++++++++++ .../languages/markdown/cloneUpItem.yml | 30 +++++++++++++++++++ .../languages/markdown/drinkItem2.yml | 30 +++++++++++++++++++ .../recorded/languages/markdown/pourItem3.yml | 30 +++++++++++++++++++ 4 files changed, 120 insertions(+) create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/cloneItem.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/cloneUpItem.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/drinkItem2.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/pourItem3.yml diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/cloneItem.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/cloneItem.yml new file mode 100644 index 0000000000..2d9a5e3b46 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/cloneItem.yml @@ -0,0 +1,30 @@ +languageId: markdown +command: + version: 6 + spokenForm: clone item + action: + name: insertCopyAfter + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + - aaa + - bbb + - ccc + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: |- + - aaa + - bbb + - bbb + - ccc + selections: + - anchor: {line: 2, character: 2} + active: {line: 2, character: 2} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/cloneUpItem.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/cloneUpItem.yml new file mode 100644 index 0000000000..ea8bb53e5b --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/cloneUpItem.yml @@ -0,0 +1,30 @@ +languageId: markdown +command: + version: 6 + spokenForm: clone up item + action: + name: insertCopyBefore + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + - aaa + - bbb + - ccc + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: |- + - aaa + - bbb + - bbb + - ccc + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/drinkItem2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/drinkItem2.yml new file mode 100644 index 0000000000..6eeab87bc7 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/drinkItem2.yml @@ -0,0 +1,30 @@ +languageId: markdown +command: + version: 6 + spokenForm: drink item + action: + name: editNewLineBefore + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + - aaa + - bbb + - ccc + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: |- + - aaa + - + - bbb + - ccc + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/pourItem3.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/pourItem3.yml new file mode 100644 index 0000000000..2cb79a3c7b --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/pourItem3.yml @@ -0,0 +1,30 @@ +languageId: markdown +command: + version: 6 + spokenForm: pour item + action: + name: editNewLineAfter + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + - aaa + - bbb + - ccc + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: |- + - aaa + - bbb + - + - ccc + selections: + - anchor: {line: 2, character: 2} + active: {line: 2, character: 2} From 45f96b19ed8656c3575cc9800d5620b7fded9164 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 4 Dec 2023 07:46:30 +0100 Subject: [PATCH 12/21] Introduced insertion range --- .../TreeSitterScopeHandler.ts | 8 ++++ .../processTargets/targets/DestinationImpl.ts | 42 ++++--------------- .../processTargets/targets/ScopeTypeTarget.ts | 5 ++- .../src/typings/target.types.ts | 3 ++ queries/markdown.scm | 4 +- queries/yaml.scm | 2 +- 6 files changed, 27 insertions(+), 37 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts index 46ca402d3e..a68a1d1097 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts @@ -60,6 +60,13 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler { const removalRange = getRelatedRange(match, scopeTypeType, "removal", true); + const insertionRange = getRelatedRange( + match, + scopeTypeType, + "insertion", + true, + ); + const leadingDelimiterRange = dropEmptyRange( getRelatedRange(match, scopeTypeType, "leading", true), ); @@ -86,6 +93,7 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler { isReversed, contentRange, removalRange, + insertionRange, leadingDelimiterRange, trailingDelimiterRange, interiorRange, diff --git a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts index 4804ed919f..0919a58d31 100644 --- a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts +++ b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts @@ -1,11 +1,9 @@ import { InsertionMode, - Position, Range, Selection, TextEditor, } from "@cursorless/common"; -import { escapeRegExp } from "lodash"; import { EditWithRangeUpdater } from "../../typings/Types"; import { Destination, @@ -116,53 +114,31 @@ export class DestinationImpl implements Destination { private getEditRange() { const position = (() => { - const contentPosition = this.getContentPosition(); + const insertionRange = + this.target.insertionRange ?? this.target.contentRange; + + const insertionPosition = this.isBefore + ? insertionRange.start + : insertionRange.end; if (this.isLineDelimiter) { - const line = this.editor.document.lineAt(contentPosition); + const line = this.editor.document.lineAt(insertionPosition); const nonWhitespaceCharacterIndex = this.isBefore ? line.firstNonWhitespaceCharacterIndex : line.lastNonWhitespaceCharacterIndex; // Use the full line with included indentation and trailing whitespaces - if (contentPosition.character === nonWhitespaceCharacterIndex) { + if (insertionPosition.character === nonWhitespaceCharacterIndex) { return this.isBefore ? line.range.start : line.range.end; } } - return contentPosition; + return insertionPosition; })(); return new Range(position, position); } - private getContentPosition(): Position { - if (this.isBefore) { - const { start } = this.contentRange; - - // With an insertion prefix the position we want to edit/insert before is extended to the left - if (this.insertionPrefix != null) { - // The leading text on the same line before the content range - const leadingText = this.editor.document - .lineAt(start) - .text.slice(0, start.character); - - // Try to find the prefix (with optional trailing whitespace) just before the content range - const prefixIndex = leadingText.search( - new RegExp(`${escapeRegExp(this.insertionPrefix)}\\s*$`), - ); - if (prefixIndex !== -1) { - const delta = leadingText.length - prefixIndex; - return start.with(undefined, start.character - delta); - } - } - - return start; - } - - return this.contentRange.end; - } - private getEditText(text: string) { const insertionText = this.indentationString + (this.insertionPrefix ?? "") + text; diff --git a/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts b/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts index 864ddcd3d4..032ed713c5 100644 --- a/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts @@ -18,6 +18,7 @@ export interface ScopeTypeTargetParameters extends CommonTargetParameters { readonly scopeTypeType: SimpleScopeTypeType; readonly insertionDelimiter?: string; readonly insertionPrefix?: string; + readonly insertionRange?: Range; readonly removalRange?: Range; readonly interiorRange?: Range; readonly leadingDelimiterRange?: Range; @@ -32,12 +33,14 @@ export class ScopeTypeTarget extends BaseTarget { private leadingDelimiterRange_?: Range; private trailingDelimiterRange_?: Range; private hasDelimiterRange_: boolean; + insertionRange?: Range; insertionDelimiter: string; insertionPrefix?: string; constructor(parameters: ScopeTypeTargetParameters) { super(parameters); this.scopeTypeType_ = parameters.scopeTypeType; + this.insertionRange = parameters.insertionRange; this.removalRange_ = parameters.removalRange; this.interiorRange_ = parameters.interiorRange; this.leadingDelimiterRange_ = parameters.leadingDelimiterRange; @@ -132,10 +135,10 @@ export class ScopeTypeTarget extends BaseTarget { ...this.state, insertionDelimiter: this.insertionDelimiter, insertionPrefix: this.insertionPrefix, + insertionRange: this.insertionRange, removalRange: undefined, interiorRange: undefined, scopeTypeType: this.scopeTypeType_, - contentRemovalRange: this.removalRange_, leadingDelimiterRange: this.leadingDelimiterRange_, trailingDelimiterRange: this.trailingDelimiterRange_, }; diff --git a/packages/cursorless-engine/src/typings/target.types.ts b/packages/cursorless-engine/src/typings/target.types.ts index 6e590e8f5b..14bb1dc3e2 100644 --- a/packages/cursorless-engine/src/typings/target.types.ts +++ b/packages/cursorless-engine/src/typings/target.types.ts @@ -39,6 +39,9 @@ export interface Target { /** The range of the content */ readonly contentRange: Range; + /* The range to insert before or after. If not defined fallback to content range */ + readonly insertionRange?: Range; + /** 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; diff --git a/queries/markdown.scm b/queries/markdown.scm index 1e08865f87..2db105eaac 100644 --- a/queries/markdown.scm +++ b/queries/markdown.scm @@ -23,9 +23,9 @@ ;;! --- ( (list_item - (_) @dummy + (_) @dummy @_.insertion.start (paragraph - (inline) @collectionItem + (inline) @collectionItem @_.insertion.end ) ) @_.domain @_.removal (#trim-end! @_.domain) diff --git a/queries/yaml.scm b/queries/yaml.scm index ef93f85a8d..b3b93cd0c5 100644 --- a/queries/yaml.scm +++ b/queries/yaml.scm @@ -24,7 +24,7 @@ . (block_sequence_item (_) @collectionItem @collectionItem.leading.end.startOf @collectionItem.trailing.end.endOf - ) @collectionItem.domain + ) @collectionItem.domain @collectionItem.insertion . (block_sequence_item (_) @collectionItem.trailing.end.startOf From 11269187048fc1c471013c321e9d0c3fffe013af Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 4 Dec 2023 15:14:13 +0100 Subject: [PATCH 13/21] Fix mark down and yaml it them removal range --- ...limitedSequenceInsertionRemovalBehavior.ts | 2 +- .../TokenInsertionRemovalBehavior.ts | 7 +++-- .../languages/markdown/changeLeadingItem.yml | 31 +++++++++++++++++++ .../languages/markdown/changeTrailingItem.yml | 31 +++++++++++++++++++ .../languages/markdown/chuckItem3.yml | 30 ++++++++++++++++++ .../languages/markdown/chuckItem4.yml | 28 +++++++++++++++++ .../languages/markdown/chuckItem5.yml | 26 ++++++++++++++++ .../languages/yaml/changeLeadingItem.yml | 31 +++++++++++++++++++ .../languages/yaml/changeTrailingItem.yml | 31 +++++++++++++++++++ .../recorded/languages/yaml/chuckItem10.yml | 28 +++++++++++++++++ .../recorded/languages/yaml/chuckItem11.yml | 26 ++++++++++++++++ .../recorded/languages/yaml/chuckItem9.yml | 30 ++++++++++++++++++ queries/markdown.scm | 14 +++++++-- queries/yaml.scm | 7 ++--- 14 files changed, 311 insertions(+), 11 deletions(-) create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/changeLeadingItem.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/changeTrailingItem.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem3.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem4.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem5.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeLeadingItem.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeTrailingItem.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/chuckItem10.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/chuckItem11.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/chuckItem9.yml diff --git a/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior.ts b/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior.ts index 6132a78512..8795aa292c 100644 --- a/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior.ts +++ b/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior.ts @@ -8,7 +8,7 @@ import { Target } from "../../../../typings/target.types"; * @returns The removal range for the given target */ export function getDelimitedSequenceRemovalRange(target: Target): Range { - const { contentRange } = target; + const contentRange = target.insertionRange ?? target.contentRange; const delimiterTarget = target.getTrailingDelimiterTarget() ?? target.getLeadingDelimiterTarget(); diff --git a/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts b/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts index 160cab6351..9e942d5f83 100644 --- a/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts +++ b/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts @@ -10,7 +10,7 @@ export function getTokenLeadingDelimiterTarget( target: Target, ): Target | undefined { const { editor } = target; - const { start } = target.contentRange; + const { start } = target.insertionRange ?? target.contentRange; const startLine = editor.document.lineAt(start); const leadingText = startLine.text.slice(0, start.character); @@ -34,7 +34,7 @@ export function getTokenTrailingDelimiterTarget( target: Target, ): Target | undefined { const { editor } = target; - const { end } = target.contentRange; + const { end } = target.insertionRange ?? target.contentRange; const endLine = editor.document.lineAt(end); const trailingText = endLine.text.slice(end.character); @@ -80,7 +80,8 @@ export function getTokenTrailingDelimiterTarget( * @returns The removal range for the given target */ export function getTokenRemovalRange(target: Target): Range { - const { editor, contentRange } = target; + const { editor } = target; + const contentRange = target.insertionRange ?? target.contentRange; const { start, end } = contentRange; const leadingWhitespaceRange = diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/changeLeadingItem.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/changeLeadingItem.yml new file mode 100644 index 0000000000..fd0d38aece --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/changeLeadingItem.yml @@ -0,0 +1,31 @@ +languageId: markdown +command: + version: 6 + spokenForm: change leading item + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - {type: leading} + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + - values + - 0 + - 1 + - 2 + selections: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} + marks: {} +finalState: + documentContents: |- + - values + - 0- 1 + - 2 + selections: + - anchor: {line: 1, character: 7} + active: {line: 1, character: 7} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/changeTrailingItem.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/changeTrailingItem.yml new file mode 100644 index 0000000000..46dadadaf7 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/changeTrailingItem.yml @@ -0,0 +1,31 @@ +languageId: markdown +command: + version: 6 + spokenForm: change trailing item + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - {type: trailing} + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + - values + - 0 + - 1 + - 2 + selections: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} + marks: {} +finalState: + documentContents: |- + - values + - 0 + - 1- 2 + selections: + - anchor: {line: 2, character: 7} + active: {line: 2, character: 7} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem3.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem3.yml new file mode 100644 index 0000000000..f6e10d8a8d --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem3.yml @@ -0,0 +1,30 @@ +languageId: markdown +command: + version: 6 + spokenForm: chuck item + action: + name: remove + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + - values + - 0 + - 1 + - 2 + selections: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} + marks: {} +finalState: + documentContents: |- + - values + - 0 + - 2 + selections: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem4.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem4.yml new file mode 100644 index 0000000000..9178465ace --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem4.yml @@ -0,0 +1,28 @@ +languageId: markdown +command: + version: 6 + spokenForm: chuck item + action: + name: remove + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: false +initialState: + documentContents: |- + - values + - 0 + - 2 + selections: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} + marks: {} +finalState: + documentContents: |- + - values + - 0 + selections: + - anchor: {line: 1, character: 7} + active: {line: 1, character: 7} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem5.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem5.yml new file mode 100644 index 0000000000..28ea83d69d --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/markdown/chuckItem5.yml @@ -0,0 +1,26 @@ +languageId: markdown +command: + version: 6 + spokenForm: chuck item + action: + name: remove + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: false +initialState: + documentContents: |- + - values + - 0 + selections: + - anchor: {line: 1, character: 7} + active: {line: 1, character: 7} + marks: {} +finalState: + documentContents: | + - values + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeLeadingItem.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeLeadingItem.yml new file mode 100644 index 0000000000..7e7ac03f06 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeLeadingItem.yml @@ -0,0 +1,31 @@ +languageId: yaml +command: + version: 6 + spokenForm: change leading item + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - {type: leading} + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + foo: + - 0 + - 1 + - 2 + selections: + - anchor: {line: 2, character: 2} + active: {line: 2, character: 2} + marks: {} +finalState: + documentContents: |- + foo: + - 0- 1 + - 2 + selections: + - anchor: {line: 1, character: 5} + active: {line: 1, character: 5} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeTrailingItem.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeTrailingItem.yml new file mode 100644 index 0000000000..65e6593d24 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/changeTrailingItem.yml @@ -0,0 +1,31 @@ +languageId: yaml +command: + version: 6 + spokenForm: change trailing item + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - {type: trailing} + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + foo: + - 0 + - 1 + - 2 + selections: + - anchor: {line: 2, character: 2} + active: {line: 2, character: 2} + marks: {} +finalState: + documentContents: |- + foo: + - 0 + - 1- 2 + selections: + - anchor: {line: 2, character: 5} + active: {line: 2, character: 5} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/chuckItem10.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/chuckItem10.yml new file mode 100644 index 0000000000..2320833dd4 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/chuckItem10.yml @@ -0,0 +1,28 @@ +languageId: yaml +command: + version: 6 + spokenForm: chuck item + action: + name: remove + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: false +initialState: + documentContents: |- + foo: + - 0 + - 2 + selections: + - anchor: {line: 2, character: 2} + active: {line: 2, character: 2} + marks: {} +finalState: + documentContents: |- + foo: + - 0 + selections: + - anchor: {line: 1, character: 5} + active: {line: 1, character: 5} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/chuckItem11.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/chuckItem11.yml new file mode 100644 index 0000000000..213243ad42 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/chuckItem11.yml @@ -0,0 +1,26 @@ +languageId: yaml +command: + version: 6 + spokenForm: chuck item + action: + name: remove + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + foo: + - 0 + selections: + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + marks: {} +finalState: + documentContents: | + foo: + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/chuckItem9.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/chuckItem9.yml new file mode 100644 index 0000000000..f54ade268d --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/yaml/chuckItem9.yml @@ -0,0 +1,30 @@ +languageId: yaml +command: + version: 6 + spokenForm: chuck item + action: + name: remove + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true +initialState: + documentContents: |- + foo: + - 0 + - 1 + - 2 + selections: + - anchor: {line: 2, character: 3} + active: {line: 2, character: 3} + marks: {} +finalState: + documentContents: |- + foo: + - 0 + - 2 + selections: + - anchor: {line: 2, character: 2} + active: {line: 2, character: 2} diff --git a/queries/markdown.scm b/queries/markdown.scm index 2db105eaac..8a30e1d4a9 100644 --- a/queries/markdown.scm +++ b/queries/markdown.scm @@ -21,13 +21,21 @@ ;;!! - 0 ;;! ^ ;;! --- -( +(list + (list_item + (paragraph + (inline) @_.leading.start.endOf + ) + )? + . (list_item (_) @dummy @_.insertion.start (paragraph - (inline) @collectionItem @_.insertion.end + (inline) @collectionItem @_.insertion.end @_.trailing.start.endOf ) - ) @_.domain @_.removal + ) @_.domain @_.leading.end.startOf + . + (list_item)? @_.trailing.end.startOf (#trim-end! @_.domain) (#insertion-delimiter! @collectionItem "\n") (#insertion-prefix! @collectionItem @dummy) diff --git a/queries/yaml.scm b/queries/yaml.scm index b3b93cd0c5..f76449701a 100644 --- a/queries/yaml.scm +++ b/queries/yaml.scm @@ -23,12 +23,11 @@ (block_sequence_item)? @collectionItem.leading.start.endOf . (block_sequence_item - (_) @collectionItem @collectionItem.leading.end.startOf @collectionItem.trailing.end.endOf + "-" @collectionItem.leading.end.startOf + (_) @collectionItem @collectionItem.trailing.end.endOf ) @collectionItem.domain @collectionItem.insertion . - (block_sequence_item - (_) @collectionItem.trailing.end.startOf - )? + (block_sequence_item)? @collectionItem.trailing.end.startOf (#trim-end! @collectionItem) (#insertion-delimiter! @collectionItem "\n") (#insertion-prefix! @collectionItem "- ") From 2878f44433e3c294dbb8694f1f8f5216549a37a6 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 4 Dec 2023 18:36:01 +0100 Subject: [PATCH 14/21] Change insertion range to extended content range --- .../TreeSitterScopeHandler/TreeSitterScopeHandler.ts | 6 +++--- .../src/processTargets/targets/BaseTarget.ts | 4 ++++ .../src/processTargets/targets/DestinationImpl.ts | 7 ++----- .../src/processTargets/targets/ScopeTypeTarget.ts | 12 ++++++++---- .../DelimitedSequenceInsertionRemovalBehavior.ts | 2 +- .../TokenInsertionRemovalBehavior.ts | 7 +++---- .../cursorless-engine/src/typings/target.types.ts | 4 ++-- queries/markdown.scm | 4 ++-- queries/yaml.scm | 2 +- 9 files changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts index a68a1d1097..0567ce7b6e 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts @@ -60,10 +60,10 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler { const removalRange = getRelatedRange(match, scopeTypeType, "removal", true); - const insertionRange = getRelatedRange( + const extendedContentRange = getRelatedRange( match, scopeTypeType, - "insertion", + "extended", true, ); @@ -92,8 +92,8 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler { editor, isReversed, contentRange, + extendedContentRange, removalRange, - insertionRange, leadingDelimiterRange, trailingDelimiterRange, interiorRange, diff --git a/packages/cursorless-engine/src/processTargets/targets/BaseTarget.ts b/packages/cursorless-engine/src/processTargets/targets/BaseTarget.ts index 53cb2c8cd6..8100587d7a 100644 --- a/packages/cursorless-engine/src/processTargets/targets/BaseTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/BaseTarget.ts @@ -87,6 +87,10 @@ export abstract class BaseTarget< return this.state.contentRange; } + get extendedContentRange(): Range { + return this.state.contentRange; + } + constructRemovalEdit(): EditWithRangeUpdater { return { range: this.getRemovalRange(), diff --git a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts index 0919a58d31..102770c0fb 100644 --- a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts +++ b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts @@ -114,12 +114,9 @@ export class DestinationImpl implements Destination { private getEditRange() { const position = (() => { - const insertionRange = - this.target.insertionRange ?? this.target.contentRange; - const insertionPosition = this.isBefore - ? insertionRange.start - : insertionRange.end; + ? this.target.extendedContentRange.start + : this.target.extendedContentRange.end; if (this.isLineDelimiter) { const line = this.editor.document.lineAt(insertionPosition); diff --git a/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts b/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts index 032ed713c5..74169255ee 100644 --- a/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts @@ -18,7 +18,7 @@ export interface ScopeTypeTargetParameters extends CommonTargetParameters { readonly scopeTypeType: SimpleScopeTypeType; readonly insertionDelimiter?: string; readonly insertionPrefix?: string; - readonly insertionRange?: Range; + readonly extendedContentRange?: Range; readonly removalRange?: Range; readonly interiorRange?: Range; readonly leadingDelimiterRange?: Range; @@ -28,19 +28,19 @@ export interface ScopeTypeTargetParameters extends CommonTargetParameters { export class ScopeTypeTarget extends BaseTarget { type = "ScopeTypeTarget"; private scopeTypeType_: SimpleScopeTypeType; + private extendedContentRange_?: Range; private removalRange_?: Range; private interiorRange_?: Range; private leadingDelimiterRange_?: Range; private trailingDelimiterRange_?: Range; private hasDelimiterRange_: boolean; - insertionRange?: Range; insertionDelimiter: string; insertionPrefix?: string; constructor(parameters: ScopeTypeTargetParameters) { super(parameters); this.scopeTypeType_ = parameters.scopeTypeType; - this.insertionRange = parameters.insertionRange; + this.extendedContentRange_ = parameters.extendedContentRange; this.removalRange_ = parameters.removalRange; this.interiorRange_ = parameters.interiorRange; this.leadingDelimiterRange_ = parameters.leadingDelimiterRange; @@ -53,6 +53,10 @@ export class ScopeTypeTarget extends BaseTarget { !!this.leadingDelimiterRange_ || !!this.trailingDelimiterRange_; } + get extendedContentRange(): Range { + return this.extendedContentRange_ ?? this.state.contentRange; + } + getLeadingDelimiterTarget(): Target | undefined { if (this.leadingDelimiterRange_ != null) { return new PlainTarget({ @@ -135,7 +139,7 @@ export class ScopeTypeTarget extends BaseTarget { ...this.state, insertionDelimiter: this.insertionDelimiter, insertionPrefix: this.insertionPrefix, - insertionRange: this.insertionRange, + extendedContentRange: this.extendedContentRange_, removalRange: undefined, interiorRange: undefined, scopeTypeType: this.scopeTypeType_, diff --git a/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior.ts b/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior.ts index 8795aa292c..74abb149e7 100644 --- a/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior.ts +++ b/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior.ts @@ -8,7 +8,7 @@ import { Target } from "../../../../typings/target.types"; * @returns The removal range for the given target */ export function getDelimitedSequenceRemovalRange(target: Target): Range { - const contentRange = target.insertionRange ?? target.contentRange; + const contentRange = target.extendedContentRange; const delimiterTarget = target.getTrailingDelimiterTarget() ?? target.getLeadingDelimiterTarget(); diff --git a/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts b/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts index 9e942d5f83..b6ccb0318e 100644 --- a/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts +++ b/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts @@ -10,7 +10,7 @@ export function getTokenLeadingDelimiterTarget( target: Target, ): Target | undefined { const { editor } = target; - const { start } = target.insertionRange ?? target.contentRange; + const { start } = target.extendedContentRange; const startLine = editor.document.lineAt(start); const leadingText = startLine.text.slice(0, start.character); @@ -34,7 +34,7 @@ export function getTokenTrailingDelimiterTarget( target: Target, ): Target | undefined { const { editor } = target; - const { end } = target.insertionRange ?? target.contentRange; + const { end } = target.extendedContentRange; const endLine = editor.document.lineAt(end); const trailingText = endLine.text.slice(end.character); @@ -80,8 +80,7 @@ export function getTokenTrailingDelimiterTarget( * @returns The removal range for the given target */ export function getTokenRemovalRange(target: Target): Range { - const { editor } = target; - const contentRange = target.insertionRange ?? target.contentRange; + const { editor, extendedContentRange: contentRange } = target; const { start, end } = contentRange; const leadingWhitespaceRange = diff --git a/packages/cursorless-engine/src/typings/target.types.ts b/packages/cursorless-engine/src/typings/target.types.ts index 14bb1dc3e2..ac8c331189 100644 --- a/packages/cursorless-engine/src/typings/target.types.ts +++ b/packages/cursorless-engine/src/typings/target.types.ts @@ -39,8 +39,8 @@ export interface Target { /** The range of the content */ readonly contentRange: Range; - /* The range to insert before or after. If not defined fallback to content range */ - readonly insertionRange?: Range; + /* The content range including prefixes like `-` and `*` in markdown collection items */ + readonly extendedContentRange: Range; /** 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; diff --git a/queries/markdown.scm b/queries/markdown.scm index 8a30e1d4a9..7b6e6e493c 100644 --- a/queries/markdown.scm +++ b/queries/markdown.scm @@ -29,9 +29,9 @@ )? . (list_item - (_) @dummy @_.insertion.start + (_) @dummy @_.extended.start (paragraph - (inline) @collectionItem @_.insertion.end @_.trailing.start.endOf + (inline) @collectionItem @_.extended.end @_.trailing.start.endOf ) ) @_.domain @_.leading.end.startOf . diff --git a/queries/yaml.scm b/queries/yaml.scm index f76449701a..2b8cb4e6c8 100644 --- a/queries/yaml.scm +++ b/queries/yaml.scm @@ -25,7 +25,7 @@ (block_sequence_item "-" @collectionItem.leading.end.startOf (_) @collectionItem @collectionItem.trailing.end.endOf - ) @collectionItem.domain @collectionItem.insertion + ) @collectionItem.domain @collectionItem.extended . (block_sequence_item)? @collectionItem.trailing.end.startOf (#trim-end! @collectionItem) From 1956c86d12e222112f8f4aa3f88b033307c28a7e Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 7 Dec 2023 14:28:11 +0100 Subject: [PATCH 15/21] remove unused typescript file --- .../cursorless-engine/src/languages/getNodeMatcher.ts | 5 ----- packages/cursorless-engine/src/languages/typescript.ts | 9 --------- 2 files changed, 14 deletions(-) delete mode 100644 packages/cursorless-engine/src/languages/typescript.ts diff --git a/packages/cursorless-engine/src/languages/getNodeMatcher.ts b/packages/cursorless-engine/src/languages/getNodeMatcher.ts index c74cfd3099..39aee32b25 100644 --- a/packages/cursorless-engine/src/languages/getNodeMatcher.ts +++ b/packages/cursorless-engine/src/languages/getNodeMatcher.ts @@ -22,7 +22,6 @@ import { patternMatchers as ruby } from "./ruby"; import rust from "./rust"; import scala from "./scala"; import { patternMatchers as scss } from "./scss"; -import { patternMatchers as typescript } from "./typescript"; export function getNodeMatcher( languageId: string, @@ -59,8 +58,6 @@ export const languageMatchers: Record< clojure, go, java, - javascript: typescript, - javascriptreact: typescript, latex, markdown, php, @@ -69,8 +66,6 @@ export const languageMatchers: Record< scala, scss, rust, - typescript, - typescriptreact: typescript, }; function matcherIncludeSiblings(matcher: NodeMatcher): NodeMatcher { diff --git a/packages/cursorless-engine/src/languages/typescript.ts b/packages/cursorless-engine/src/languages/typescript.ts deleted file mode 100644 index eac27b1c95..0000000000 --- a/packages/cursorless-engine/src/languages/typescript.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { SimpleScopeTypeType } from "@cursorless/common"; -import { NodeMatcherAlternative } from "../typings/Types"; -import { createPatternMatchers } from "../util/nodeMatchers"; - -const nodeMatchers: Partial< - Record -> = {}; - -export const patternMatchers = createPatternMatchers(nodeMatchers); From c85623ee82646039746e902ec20c77b97b93c0fb Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 7 Dec 2023 15:07:42 +0100 Subject: [PATCH 16/21] use prefix range --- .../languages/TreeSitterQuery/QueryCapture.ts | 4 --- .../TreeSitterQuery/TreeSitterQuery.ts | 4 --- .../checkCaptureStartEnd.test.ts | 5 +--- .../queryPredicateOperators.ts | 28 +++---------------- .../rewriteStartOfEndOf.test.ts | 1 - .../TreeSitterScopeHandler.ts | 28 +++++++++---------- .../src/processTargets/targets/BaseTarget.ts | 4 --- .../processTargets/targets/DestinationImpl.ts | 13 +++++---- .../processTargets/targets/ScopeTypeTarget.ts | 16 +++-------- ...limitedSequenceInsertionRemovalBehavior.ts | 3 +- .../TokenInsertionRemovalBehavior.ts | 9 +++--- .../src/typings/target.types.ts | 7 ++--- .../cursorless-engine/src/util/rangeUtils.ts | 12 ++++++++ queries/javascript.core.scm | 4 +-- queries/markdown.scm | 5 ++-- queries/yaml.scm | 5 ++-- 16 files changed, 56 insertions(+), 92 deletions(-) diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts index 7bb771cdf1..f92fde3098 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts @@ -38,9 +38,6 @@ 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; } /** @@ -67,7 +64,6 @@ export interface MutableQueryCapture extends QueryCapture { range: Range; allowMultiple: boolean; insertionDelimiter: string | undefined; - insertionPrefix: string | undefined; } /** diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts index 44a919184d..3ecd56b1a5 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts @@ -81,7 +81,6 @@ export class TreeSitterQuery { document, range: getNodeRange(node), insertionDelimiter: undefined, - insertionPrefix: undefined, allowMultiple: false, })), }), @@ -118,9 +117,6 @@ export class TreeSitterQuery { insertionDelimiter: captures.find( (capture) => capture.insertionDelimiter != null, )?.insertionDelimiter, - insertionPrefix: captures.find( - (capture) => capture.insertionPrefix != null, - )?.insertionPrefix, }; }); diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts index 02bd5d1207..217c0a2d37 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts @@ -5,10 +5,7 @@ import assert from "assert"; interface TestCase { name: string; - captures: Omit< - QueryCapture, - "allowMultiple" | "insertionDelimiter" | "insertionPrefix" - >[]; + captures: Omit[]; isValid: boolean; expectedErrorMessageIds: string[]; } diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts index d93ad63222..ecab675e71 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts @@ -216,11 +216,11 @@ class InsertionDelimiter extends QueryPredicateOperator { /** * 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 + * example, `(#single-or-multi-line-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 { - name = "conditional-insertion-delimiter!" as const; +class SingleOrMultilineDelimiter extends QueryPredicateOperator { + name = "single-or-multi-line-delimiter!" as const; schema = z.tuple([q.node, q.node, q.string, q.string]); run( @@ -237,25 +237,6 @@ class ConditionalInsertionDelimiter extends QueryPredicateOperator { - 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(), @@ -266,7 +247,6 @@ export const queryPredicateOperators = [ new ShrinkToMatch(), new AllowMultiple(), new InsertionDelimiter(), - new ConditionalInsertionDelimiter(), - new InsertionPrefix(), + new SingleOrMultilineDelimiter(), new HasMultipleChildrenOfType(), ]; diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/rewriteStartOfEndOf.test.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/rewriteStartOfEndOf.test.ts index b919e07c5e..cc8e99c789 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/rewriteStartOfEndOf.test.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/rewriteStartOfEndOf.test.ts @@ -55,7 +55,6 @@ function fillOutCapture(capture: NameRange): MutableQueryCapture { ...capture, allowMultiple: false, insertionDelimiter: undefined, - insertionPrefix: undefined, document: null as unknown as TextDocument, node: null as unknown as SyntaxNode, }; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts index 0567ce7b6e..3aa95dac22 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts @@ -48,25 +48,13 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler { return undefined; } - const { - range: contentRange, - allowMultiple, - insertionDelimiter, - insertionPrefix, - } = capture; + const { range: contentRange, allowMultiple, insertionDelimiter } = capture; const domain = getRelatedRange(match, scopeTypeType, "domain", true) ?? contentRange; const removalRange = getRelatedRange(match, scopeTypeType, "removal", true); - const extendedContentRange = getRelatedRange( - match, - scopeTypeType, - "extended", - true, - ); - const leadingDelimiterRange = dropEmptyRange( getRelatedRange(match, scopeTypeType, "leading", true), ); @@ -82,6 +70,17 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler { true, ); + const rawPrefixRange = getRelatedRange( + match, + scopeTypeType, + "prefix", + true, + ); + const prefixRange = + rawPrefixRange != null + ? new Range(rawPrefixRange.start, contentRange.start) + : undefined; + return { editor, domain, @@ -92,13 +91,12 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler { editor, isReversed, contentRange, - extendedContentRange, + prefixRange, removalRange, leadingDelimiterRange, trailingDelimiterRange, interiorRange, insertionDelimiter, - insertionPrefix, }), ], }; diff --git a/packages/cursorless-engine/src/processTargets/targets/BaseTarget.ts b/packages/cursorless-engine/src/processTargets/targets/BaseTarget.ts index 8100587d7a..53cb2c8cd6 100644 --- a/packages/cursorless-engine/src/processTargets/targets/BaseTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/BaseTarget.ts @@ -87,10 +87,6 @@ export abstract class BaseTarget< return this.state.contentRange; } - get extendedContentRange(): Range { - return this.state.contentRange; - } - constructRemovalEdit(): EditWithRangeUpdater { return { range: this.getRemovalRange(), diff --git a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts index 102770c0fb..cfe35ac16c 100644 --- a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts +++ b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts @@ -16,6 +16,7 @@ export class DestinationImpl implements Destination { private readonly isLineDelimiter: boolean; private readonly isBefore: boolean; private readonly indentationString: string; + private readonly insertionPrefix?: string; constructor( public readonly target: Target, @@ -29,6 +30,10 @@ export class DestinationImpl implements Destination { indentationString ?? this.isLineDelimiter ? getIndentationString(target.editor, target.contentRange) : ""; + this.insertionPrefix = + target.prefixRange != null + ? target.editor.document.getText(target.prefixRange) + : undefined; } get contentSelection(): Selection { @@ -43,10 +48,6 @@ 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; } @@ -115,8 +116,8 @@ export class DestinationImpl implements Destination { private getEditRange() { const position = (() => { const insertionPosition = this.isBefore - ? this.target.extendedContentRange.start - : this.target.extendedContentRange.end; + ? this.target.prefixRange?.start ?? this.target.contentRange.start + : this.target.contentRange.end; if (this.isLineDelimiter) { const line = this.editor.document.lineAt(insertionPosition); diff --git a/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts b/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts index 74169255ee..6a4737336a 100644 --- a/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts @@ -17,8 +17,7 @@ import { export interface ScopeTypeTargetParameters extends CommonTargetParameters { readonly scopeTypeType: SimpleScopeTypeType; readonly insertionDelimiter?: string; - readonly insertionPrefix?: string; - readonly extendedContentRange?: Range; + readonly prefixRange?: Range; readonly removalRange?: Range; readonly interiorRange?: Range; readonly leadingDelimiterRange?: Range; @@ -28,19 +27,18 @@ export interface ScopeTypeTargetParameters extends CommonTargetParameters { export class ScopeTypeTarget extends BaseTarget { type = "ScopeTypeTarget"; private scopeTypeType_: SimpleScopeTypeType; - private extendedContentRange_?: Range; private removalRange_?: Range; private interiorRange_?: Range; private leadingDelimiterRange_?: Range; private trailingDelimiterRange_?: Range; private hasDelimiterRange_: boolean; + public prefixRange?: Range; insertionDelimiter: string; - insertionPrefix?: string; constructor(parameters: ScopeTypeTargetParameters) { super(parameters); this.scopeTypeType_ = parameters.scopeTypeType; - this.extendedContentRange_ = parameters.extendedContentRange; + this.prefixRange = parameters.prefixRange; this.removalRange_ = parameters.removalRange; this.interiorRange_ = parameters.interiorRange; this.leadingDelimiterRange_ = parameters.leadingDelimiterRange; @@ -48,15 +46,10 @@ export class ScopeTypeTarget extends BaseTarget { this.insertionDelimiter = parameters.insertionDelimiter ?? getInsertionDelimiter(parameters.scopeTypeType); - this.insertionPrefix = parameters.insertionPrefix; this.hasDelimiterRange_ = !!this.leadingDelimiterRange_ || !!this.trailingDelimiterRange_; } - get extendedContentRange(): Range { - return this.extendedContentRange_ ?? this.state.contentRange; - } - getLeadingDelimiterTarget(): Target | undefined { if (this.leadingDelimiterRange_ != null) { return new PlainTarget({ @@ -138,8 +131,7 @@ export class ScopeTypeTarget extends BaseTarget { return { ...this.state, insertionDelimiter: this.insertionDelimiter, - insertionPrefix: this.insertionPrefix, - extendedContentRange: this.extendedContentRange_, + prefixRange: this.prefixRange, removalRange: undefined, interiorRange: undefined, scopeTypeType: this.scopeTypeType_, diff --git a/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior.ts b/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior.ts index 74abb149e7..94626a50da 100644 --- a/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior.ts +++ b/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior.ts @@ -1,5 +1,6 @@ import { Range } from "@cursorless/common"; import { Target } from "../../../../typings/target.types"; +import { union } from "../../../../util/rangeUtils"; /** * Constructs a removal range for the given target that includes either the @@ -8,7 +9,7 @@ import { Target } from "../../../../typings/target.types"; * @returns The removal range for the given target */ export function getDelimitedSequenceRemovalRange(target: Target): Range { - const contentRange = target.extendedContentRange; + const contentRange = union(target.contentRange, target.prefixRange); const delimiterTarget = target.getTrailingDelimiterTarget() ?? target.getLeadingDelimiterTarget(); diff --git a/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts b/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts index b6ccb0318e..7d0303b9f5 100644 --- a/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts +++ b/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts @@ -1,6 +1,6 @@ import { Range, TextEditor } from "@cursorless/common"; import type { Target } from "../../../../typings/target.types"; -import { expandToFullLine } from "../../../../util/rangeUtils"; +import { expandToFullLine, union } from "../../../../util/rangeUtils"; import { PlainTarget } from "../../PlainTarget"; const leadingDelimiters = ['"', "'", "(", "[", "{", "<"]; @@ -10,7 +10,7 @@ export function getTokenLeadingDelimiterTarget( target: Target, ): Target | undefined { const { editor } = target; - const { start } = target.extendedContentRange; + const start = target.prefixRange?.start ?? target.contentRange.start; const startLine = editor.document.lineAt(start); const leadingText = startLine.text.slice(0, start.character); @@ -34,7 +34,7 @@ export function getTokenTrailingDelimiterTarget( target: Target, ): Target | undefined { const { editor } = target; - const { end } = target.extendedContentRange; + const { end } = target.contentRange; const endLine = editor.document.lineAt(end); const trailingText = endLine.text.slice(end.character); @@ -80,7 +80,8 @@ export function getTokenTrailingDelimiterTarget( * @returns The removal range for the given target */ export function getTokenRemovalRange(target: Target): Range { - const { editor, extendedContentRange: contentRange } = target; + const { editor } = target; + const contentRange = union(target.contentRange, target.prefixRange); const { start, end } = contentRange; const leadingWhitespaceRange = diff --git a/packages/cursorless-engine/src/typings/target.types.ts b/packages/cursorless-engine/src/typings/target.types.ts index ac8c331189..e817ea45db 100644 --- a/packages/cursorless-engine/src/typings/target.types.ts +++ b/packages/cursorless-engine/src/typings/target.types.ts @@ -39,14 +39,11 @@ export interface Target { /** The range of the content */ readonly contentRange: Range; - /* The content range including prefixes like `-` and `*` in markdown collection items */ - readonly extendedContentRange: Range; - /** 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; + /** Optional prefix. For example, dash or asterisk for a markdown item */ + readonly prefixRange?: Range; /** If true this target should be treated as a line */ readonly isLine: boolean; diff --git a/packages/cursorless-engine/src/util/rangeUtils.ts b/packages/cursorless-engine/src/util/rangeUtils.ts index 89bf2aebd3..79dddd435d 100644 --- a/packages/cursorless-engine/src/util/rangeUtils.ts +++ b/packages/cursorless-engine/src/util/rangeUtils.ts @@ -53,3 +53,15 @@ export function strictlyContains( : [rangeOrPosition.start, rangeOrPosition.end]; return range1.start.isBefore(start) && range1.end.isAfter(end); } + +/** + * Make union between range and additional optional ranges + */ +export function union(range: Range, ...unionWith: (Range | undefined)[]) { + for (const r of unionWith) { + if (r != null) { + range = range.union(r); + } + } + return range; +} diff --git a/queries/javascript.core.scm b/queries/javascript.core.scm index 6a45334fa0..1e81e248ad 100644 --- a/queries/javascript.core.scm +++ b/queries/javascript.core.scm @@ -692,7 +692,7 @@ (_)? @_.trailing.end.startOf ) @dummy (#not-type? @argumentOrParameter "comment") - (#conditional-insertion-delimiter! @argumentOrParameter @dummy ", " ",\n") + (#single-or-multi-line-delimiter! @argumentOrParameter @dummy ", " ",\n") ) ;;!! foo("bar") @@ -706,7 +706,7 @@ (_)? @_.trailing.end.startOf ) @dummy (#not-type? @argumentOrParameter "comment") - (#conditional-insertion-delimiter! @argumentOrParameter @dummy ", " ",\n") + (#single-or-multi-line-delimiter! @argumentOrParameter @dummy ", " ",\n") ) [ diff --git a/queries/markdown.scm b/queries/markdown.scm index 7b6e6e493c..e5d5a50310 100644 --- a/queries/markdown.scm +++ b/queries/markdown.scm @@ -29,14 +29,13 @@ )? . (list_item - (_) @dummy @_.extended.start + (_) @dummy @_.prefix (paragraph - (inline) @collectionItem @_.extended.end @_.trailing.start.endOf + (inline) @collectionItem @_.trailing.start.endOf ) ) @_.domain @_.leading.end.startOf . (list_item)? @_.trailing.end.startOf (#trim-end! @_.domain) (#insertion-delimiter! @collectionItem "\n") - (#insertion-prefix! @collectionItem @dummy) ) diff --git a/queries/yaml.scm b/queries/yaml.scm index 2b8cb4e6c8..899f208507 100644 --- a/queries/yaml.scm +++ b/queries/yaml.scm @@ -23,14 +23,13 @@ (block_sequence_item)? @collectionItem.leading.start.endOf . (block_sequence_item - "-" @collectionItem.leading.end.startOf + "-" @collectionItem.leading.end.startOf @collectionItem.prefix (_) @collectionItem @collectionItem.trailing.end.endOf - ) @collectionItem.domain @collectionItem.extended + ) @collectionItem.domain . (block_sequence_item)? @collectionItem.trailing.end.startOf (#trim-end! @collectionItem) (#insertion-delimiter! @collectionItem "\n") - (#insertion-prefix! @collectionItem "- ") ) @list (#trim-end! @list) ) From 17657f3bc16cd5a1cc82e704a4c2b6559d394a6d Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 7 Dec 2023 15:13:45 +0100 Subject: [PATCH 17/21] cleanup --- .../TreeSitterQuery/checkCaptureStartEnd.test.ts | 1 - .../languages/TreeSitterQuery/queryPredicateOperators.ts | 8 +++++--- .../src/processTargets/targets/DestinationImpl.ts | 3 ++- .../src/processTargets/targets/ScopeTypeTarget.ts | 2 +- .../TokenInsertionRemovalBehavior.ts | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts index 217c0a2d37..3d4afa5729 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.test.ts @@ -193,7 +193,6 @@ suite("checkCaptureStartEnd", () => { ...capture, allowMultiple: false, insertionDelimiter: undefined, - insertionPrefix: undefined, })), messages, ); diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts index ecab675e71..5f0353292e 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts @@ -215,9 +215,11 @@ class InsertionDelimiter extends QueryPredicateOperator { } /** - * A predicate operator that sets the insertion delimiter of the match based on a condition. For - * example, `(#single-or-multi-line-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. + * A predicate operator that sets the insertion delimiter based upon if the + * capture note is single or multiline. For + * example,`(#single-or-multi-line-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 SingleOrMultilineDelimiter extends QueryPredicateOperator { name = "single-or-multi-line-delimiter!" as const; diff --git a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts index cfe35ac16c..a7087d4e46 100644 --- a/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts +++ b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts @@ -10,6 +10,7 @@ import { EditNewActionType, Target, } from "../../typings/target.types"; +import { union } from "../../util/rangeUtils"; export class DestinationImpl implements Destination { public readonly contentRange: Range; @@ -116,7 +117,7 @@ export class DestinationImpl implements Destination { private getEditRange() { const position = (() => { const insertionPosition = this.isBefore - ? this.target.prefixRange?.start ?? this.target.contentRange.start + ? union(this.target.contentRange, this.target.prefixRange).start : this.target.contentRange.end; if (this.isLineDelimiter) { diff --git a/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts b/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts index 6a4737336a..35ec428663 100644 --- a/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts @@ -38,11 +38,11 @@ export class ScopeTypeTarget extends BaseTarget { constructor(parameters: ScopeTypeTargetParameters) { super(parameters); this.scopeTypeType_ = parameters.scopeTypeType; - this.prefixRange = parameters.prefixRange; this.removalRange_ = parameters.removalRange; this.interiorRange_ = parameters.interiorRange; this.leadingDelimiterRange_ = parameters.leadingDelimiterRange; this.trailingDelimiterRange_ = parameters.trailingDelimiterRange; + this.prefixRange = parameters.prefixRange; this.insertionDelimiter = parameters.insertionDelimiter ?? getInsertionDelimiter(parameters.scopeTypeType); diff --git a/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts b/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts index 7d0303b9f5..ee75898054 100644 --- a/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts +++ b/packages/cursorless-engine/src/processTargets/targets/util/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts @@ -10,7 +10,7 @@ export function getTokenLeadingDelimiterTarget( target: Target, ): Target | undefined { const { editor } = target; - const start = target.prefixRange?.start ?? target.contentRange.start; + const { start } = union(target.contentRange, target.prefixRange); const startLine = editor.document.lineAt(start); const leadingText = startLine.text.slice(0, start.character); From d484c2bc6f20ffd4a74563cb0c3362fd0beccee8 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 7 Dec 2023 15:18:38 +0100 Subject: [PATCH 18/21] cleanup --- .../src/processTargets/targets/ScopeTypeTarget.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts b/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts index 35ec428663..4e31218c04 100644 --- a/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts @@ -32,8 +32,8 @@ export class ScopeTypeTarget extends BaseTarget { private leadingDelimiterRange_?: Range; private trailingDelimiterRange_?: Range; private hasDelimiterRange_: boolean; - public prefixRange?: Range; - insertionDelimiter: string; + public readonly prefixRange?: Range; + readonly insertionDelimiter: string; constructor(parameters: ScopeTypeTargetParameters) { super(parameters); From 265aee81660374740ab585a951ed9df8c7110783 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 7 Dec 2023 16:51:58 +0000 Subject: [PATCH 19/21] Docs --- .../queryPredicateOperators.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts index 5f0353292e..92177ace8f 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts @@ -215,11 +215,17 @@ class InsertionDelimiter extends QueryPredicateOperator { } /** - * A predicate operator that sets the insertion delimiter based upon if the - * capture note is single or multiline. For - * example,`(#single-or-multi-line-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. + * A predicate operator that sets the insertion delimiter of {@link nodeInfo} to + * either {@link insertionDelimiterConsequence} or + * {@link insertionDelimiterAlternative} depending on whether + * {@link conditionNodeInfo} is single or multiline, respectively. For example, + * + * ```scm + * (#single-or-multi-line-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 SingleOrMultilineDelimiter extends QueryPredicateOperator { name = "single-or-multi-line-delimiter!" as const; @@ -227,11 +233,11 @@ class SingleOrMultilineDelimiter extends QueryPredicateOperator Date: Thu, 7 Dec 2023 17:17:22 +0000 Subject: [PATCH 20/21] tweak --- queries/markdown.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/queries/markdown.scm b/queries/markdown.scm index e5d5a50310..a1974f11f6 100644 --- a/queries/markdown.scm +++ b/queries/markdown.scm @@ -29,7 +29,7 @@ )? . (list_item - (_) @dummy @_.prefix + (_) @_.prefix (paragraph (inline) @collectionItem @_.trailing.start.endOf ) From 33fdd5a5d33e8ec037664ff6d56981cf2fc83409 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 7 Dec 2023 18:37:32 +0100 Subject: [PATCH 21/21] Check error type --- .../src/processTargets/modifiers/EveryScopeStage.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts index 98fd69f41b..b94bf21e50 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts @@ -82,6 +82,9 @@ export class EveryScopeStage implements ModifierStage { getScopesOverlappingRange(scopeHandler, editor, iterationRange), ); } catch (error) { + if (!(error instanceof NoContainingScopeError)) { + throw error; + } scopes = []; } }