From 7ad0871b21e1488bfa372cbea7a3c6cebfda8b48 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 18 Nov 2024 08:57:12 +0100 Subject: [PATCH 01/21] Started working on collection item scope provider --- .../ModifierStageFactoryImpl.ts | 3 - .../modifiers/ItemStage/ItemStage.ts | 214 ------------------ .../modifiers/ItemStage/getIterationScope.ts | 194 ---------------- .../modifiers/ItemStage/index.ts | 1 - .../modifiers/ItemStage/tokenizeRange.ts | 186 --------------- .../CollectionItemScopeHandler.ts | 38 ++++ .../scopeHandlers/ScopeHandlerFactoryImpl.ts | 6 + 7 files changed, 44 insertions(+), 598 deletions(-) delete mode 100644 packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts delete mode 100644 packages/cursorless-engine/src/processTargets/modifiers/ItemStage/getIterationScope.ts delete mode 100644 packages/cursorless-engine/src/processTargets/modifiers/ItemStage/index.ts delete mode 100644 packages/cursorless-engine/src/processTargets/modifiers/ItemStage/tokenizeRange.ts create mode 100644 packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts diff --git a/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts b/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts index 5f7b6ec009..420cec1225 100644 --- a/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts +++ b/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts @@ -21,7 +21,6 @@ import { ExcludeInteriorStage, InteriorOnlyStage, } from "./modifiers/InteriorStage"; -import { ItemStage } from "./modifiers/ItemStage"; import { LeadingStage, TrailingStage } from "./modifiers/LeadingTrailingStages"; import { OrdinalScopeStage } from "./modifiers/OrdinalScopeStage"; import { EndOfStage, StartOfStage } from "./modifiers/PositionStage"; @@ -131,8 +130,6 @@ export class ModifierStageFactoryImpl implements ModifierStageFactory { switch (modifier.scopeType.type) { case "notebookCell": return new NotebookCellStage(modifier); - case "collectionItem": - return new ItemStage(this.languageDefinitions, this, modifier); default: // Default to containing syntax scope using tree sitter return new LegacyContainingSyntaxScopeStage( diff --git a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts deleted file mode 100644 index 0efea9f7c2..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts +++ /dev/null @@ -1,214 +0,0 @@ -import type { - ContainingScopeModifier, - EveryScopeModifier, - SimpleScopeTypeType, - TextEditor, -} from "@cursorless/common"; -import { NoContainingScopeError, Range } from "@cursorless/common"; -import type { LanguageDefinitions } from "../../../languages/LanguageDefinitions"; -import type { Target } from "../../../typings/target.types"; -import { getRangeLength } from "../../../util/rangeUtils"; -import type { ModifierStageFactory } from "../../ModifierStageFactory"; -import type { ModifierStage } from "../../PipelineStages.types"; -import { ScopeTypeTarget } from "../../targets"; -import type { SimpleContainingScopeModifier } from "../scopeTypeStages/LegacyContainingSyntaxScopeStage"; -import { LegacyContainingSyntaxScopeStage } from "../scopeTypeStages/LegacyContainingSyntaxScopeStage"; -import { getIterationScope } from "./getIterationScope"; -import { tokenizeRange } from "./tokenizeRange"; - -export class ItemStage implements ModifierStage { - constructor( - private languageDefinitions: LanguageDefinitions, - private modifierStageFactory: ModifierStageFactory, - private modifier: ContainingScopeModifier | EveryScopeModifier, - ) {} - - run(target: Target): Target[] { - // First try the language specific implementation of item - try { - return new LegacyContainingSyntaxScopeStage( - this.languageDefinitions, - this.modifier as SimpleContainingScopeModifier, - ).run(target); - } catch (_error) { - // do nothing - } - - // Then try the textual implementation - if (this.modifier.type === "everyScope") { - return this.getEveryTarget(this.modifierStageFactory, target); - } - return [this.getSingleTarget(this.modifierStageFactory, target)]; - } - - private getEveryTarget( - modifierStageFactory: ModifierStageFactory, - target: Target, - ) { - const itemInfos = getItemInfosForIterationScope( - modifierStageFactory, - target, - ); - - // If target has explicit range filter to items in that range. Otherwise expand to all items in iteration scope. - const filteredItemInfos = target.hasExplicitRange - ? filterItemInfos(target, itemInfos) - : itemInfos; - - if (filteredItemInfos.length === 0) { - throw new NoContainingScopeError(this.modifier.scopeType.type); - } - - return filteredItemInfos.map((itemInfo) => - this.itemInfoToTarget(target, itemInfo), - ); - } - - private getSingleTarget( - modifierStageFactory: ModifierStageFactory, - target: Target, - ) { - const itemInfos = getItemInfosForIterationScope( - modifierStageFactory, - target, - ); - - const filteredItemInfos = filterItemInfos(target, itemInfos); - - if (filteredItemInfos.length === 0) { - throw new NoContainingScopeError(this.modifier.scopeType.type); - } - - const first = filteredItemInfos[0]; - const last = filteredItemInfos[filteredItemInfos.length - 1]; - - const itemInfo: ItemInfo = { - contentRange: first.contentRange.union(last.contentRange), - domain: first.domain.union(last.domain), - leadingDelimiterRange: first.leadingDelimiterRange, - trailingDelimiterRange: last.trailingDelimiterRange, - }; - - // We have both leading and trailing delimiter ranges - // The leading one is longer/more specific so prefer to use that for removal. - const removalRange = - itemInfo.leadingDelimiterRange != null && - itemInfo.trailingDelimiterRange != null && - getRangeLength(target.editor, itemInfo.leadingDelimiterRange) > - getRangeLength(target.editor, itemInfo.trailingDelimiterRange) - ? itemInfo.contentRange.union(itemInfo.leadingDelimiterRange) - : undefined; - - return this.itemInfoToTarget(target, itemInfo, removalRange); - } - - private itemInfoToTarget( - target: Target, - itemInfo: ItemInfo, - removalRange?: Range, - ) { - const insertionDelimiter = getInsertionDelimiter( - itemInfo.leadingDelimiterRange, - itemInfo.trailingDelimiterRange, - ); - return new ScopeTypeTarget({ - scopeTypeType: this.modifier.scopeType.type as SimpleScopeTypeType, - editor: target.editor, - isReversed: target.isReversed, - contentRange: itemInfo.contentRange, - insertionDelimiter, - leadingDelimiterRange: itemInfo.leadingDelimiterRange, - trailingDelimiterRange: itemInfo.trailingDelimiterRange, - removalRange, - }); - } -} - -function getInsertionDelimiter( - leadingDelimiterRange?: Range, - trailingDelimiterRange?: Range, -): string { - return (leadingDelimiterRange != null && - !leadingDelimiterRange.isSingleLine) || - (trailingDelimiterRange != null && !trailingDelimiterRange.isSingleLine) - ? ",\n" - : ", "; -} - -/** Filter item infos by content range and domain intersection */ -function filterItemInfos(target: Target, itemInfos: ItemInfo[]): ItemInfo[] { - return itemInfos.filter( - (itemInfo) => itemInfo.domain.intersection(target.contentRange) != null, - ); -} - -function getItemInfosForIterationScope( - modifierStageFactory: ModifierStageFactory, - target: Target, -) { - const { range, boundary } = getIterationScope(modifierStageFactory, target); - return getItemsInRange(target.editor, range, boundary); -} - -function getItemsInRange( - editor: TextEditor, - interior: Range, - boundary?: [Range, Range], -): ItemInfo[] { - const tokens = tokenizeRange(editor, interior, boundary); - const itemInfos: ItemInfo[] = []; - - tokens.forEach((token, i) => { - if (token.type === "separator" || token.type === "boundary") { - return; - } - - const leadingDelimiterRange = (() => { - if (tokens[i - 2]?.type === "item") { - return new Range(tokens[i - 2].range.end, token.range.start); - } - if (tokens[i - 1]?.type === "separator") { - return new Range(tokens[i - 1].range.start, token.range.start); - } - return undefined; - })(); - - const trailingDelimiterRange = (() => { - if (tokens[i + 2]?.type === "item") { - return new Range(token.range.end, tokens[i + 2].range.start); - } - if (tokens[i + 1]?.type === "separator") { - return new Range(token.range.end, tokens[i + 1].range.end); - } - return undefined; - })(); - - // Leading boundary and separator are excluded - const domainStart = - tokens[i - 1]?.type === "boundary" || tokens[i - 1]?.type === "separator" - ? tokens[i - 1].range.end - : token.range.start; - - // Trailing boundary and separator are excluded - const domainEnd = - tokens[i + 1]?.type === "boundary" || tokens[i + 1]?.type === "separator" - ? tokens[i + 1].range.start - : token.range.end; - - itemInfos.push({ - contentRange: token.range, - leadingDelimiterRange, - trailingDelimiterRange, - domain: new Range(domainStart, domainEnd), - }); - }); - - return itemInfos; -} - -interface ItemInfo { - contentRange: Range; - leadingDelimiterRange?: Range; - trailingDelimiterRange?: Range; - domain: Range; -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/getIterationScope.ts b/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/getIterationScope.ts deleted file mode 100644 index 61fb0b3166..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/getIterationScope.ts +++ /dev/null @@ -1,194 +0,0 @@ -import type { TextEditor, TextLine } from "@cursorless/common"; -import { Range, type SurroundingPairScopeType } from "@cursorless/common"; -import type { Target } from "../../../typings/target.types"; -import type { ModifierStageFactory } from "../../ModifierStageFactory"; -import { PlainTarget } from "../../targets"; -import { fitRangeToLineContent } from "../scopeHandlers"; - -/** - * Get the iteration scope range for item scope. - * Try to find non-string surrounding scope with a fallback to line content. - * @param context The stage process context - * @param target The stage target - * @returns The stage iteration scope and optional surrounding pair boundaries - */ -export function getIterationScope( - modifierStageFactory: ModifierStageFactory, - target: Target, -): { range: Range; boundary?: [Range, Range] } { - let surroundingTarget = getBoundarySurroundingPair( - modifierStageFactory, - target, - ); - - // Iteration is necessary in case of in valid surrounding targets (nested strings, content range adjacent to delimiter) - while (surroundingTarget != null) { - if ( - useInteriorOfSurroundingTarget( - modifierStageFactory, - target, - surroundingTarget, - ) - ) { - return { - range: surroundingTarget.getInterior()![0].contentRange, - boundary: getBoundary(surroundingTarget), - }; - } - - surroundingTarget = getParentSurroundingPair( - modifierStageFactory, - target.editor, - surroundingTarget, - ); - } - - // We have not found a surrounding pair. Use the line. - return { - range: fitRangeToLineContent(target.editor, target.contentRange), - }; -} - -function useInteriorOfSurroundingTarget( - modifierStageFactory: ModifierStageFactory, - target: Target, - surroundingTarget: Target, -): boolean { - const { contentRange } = target; - - if (contentRange.isEmpty) { - const [left, right] = getBoundary(surroundingTarget); - const pos = contentRange.start; - // Content range is outside adjacent to pair - if (pos.isEqual(left.start) || pos.isEqual(right.end)) { - return false; - } - const line = target.editor.document.lineAt(pos); - // Content range is just inside of opening/left delimiter - if ( - pos.isEqual(left.end) && - characterIsWhitespaceOrMissing(line, pos.character) - ) { - return false; - } - // Content range is just inside of closing/right delimiter - if ( - pos.isEqual(right.start) && - characterIsWhitespaceOrMissing(line, pos.character - 1) - ) { - return false; - } - } else { - // Content range is equal to surrounding range - if (contentRange.isRangeEqual(surroundingTarget.contentRange)) { - return false; - } - - // Content range is equal to one of the boundaries of the surrounding range - const [left, right] = getBoundary(surroundingTarget); - if (contentRange.isRangeEqual(left) || contentRange.isRangeEqual(right)) { - return false; - } - } - - // We don't look for items inside strings. - // A non-string surrounding pair that is inside a surrounding string is fine. - const surroundingStringTarget = getStringSurroundingPair( - modifierStageFactory, - surroundingTarget, - ); - if ( - surroundingStringTarget != null && - surroundingTarget.contentRange.start.isBeforeOrEqual( - surroundingStringTarget.contentRange.start, - ) - ) { - return false; - } - - return true; -} - -function getBoundary(surroundingTarget: Target): [Range, Range] { - return surroundingTarget.getBoundary()!.map((t) => t.contentRange) as [ - Range, - Range, - ]; -} - -function characterIsWhitespaceOrMissing( - line: TextLine, - index: number, -): boolean { - return ( - index < line.range.start.character || - index >= line.range.end.character || - line.text[index].trim() === "" - ); -} - -function getParentSurroundingPair( - modifierStageFactory: ModifierStageFactory, - editor: TextEditor, - target: Target, -) { - const startOffset = editor.document.offsetAt(target.contentRange.start); - // Can't have a parent; already at start of document - if (startOffset === 0) { - return undefined; - } - // Step out of this pair and see if we have a parent - const position = editor.document.positionAt(startOffset - 1); - return getBoundarySurroundingPair( - modifierStageFactory, - new PlainTarget({ - editor, - contentRange: new Range(position, position), - isReversed: false, - }), - ); -} - -function getBoundarySurroundingPair( - modifierStageFactory: ModifierStageFactory, - target: Target, -): Target | undefined { - return getSurroundingPair(modifierStageFactory, target, { - type: "surroundingPair", - delimiter: "collectionBoundary", - requireStrongContainment: true, - }); -} - -function getStringSurroundingPair( - modifierStageFactory: ModifierStageFactory, - target: Target, -): Target | undefined { - return getSurroundingPair(modifierStageFactory, target, { - type: "surroundingPair", - delimiter: "string", - requireStrongContainment: true, - }); -} - -function getSurroundingPair( - modifierStageFactory: ModifierStageFactory, - target: Target, - scopeType: SurroundingPairScopeType, -): Target | undefined { - const pairStage = modifierStageFactory.create({ - type: "containingScope", - scopeType, - }); - const targets = (() => { - try { - return pairStage.run(target); - } catch (_error) { - return []; - } - })(); - if (targets.length > 1) { - throw Error("Expected only one surrounding pair target"); - } - return targets[0]; -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/index.ts b/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/index.ts deleted file mode 100644 index 492aa91ffb..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./ItemStage"; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/tokenizeRange.ts b/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/tokenizeRange.ts deleted file mode 100644 index 43269862b5..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/ItemStage/tokenizeRange.ts +++ /dev/null @@ -1,186 +0,0 @@ -import type { TextEditor } from "@cursorless/common"; -import { Range } from "@cursorless/common"; - -/** - * Given the iteration scope, returns a list of "tokens" within that collection - * In this context, we define a "token" to be either an item in the collection, - * a delimiter or a separator. For example, if {@link interior} is a range - * containing `foo(hello), bar, whatever`, and {@link boundary} consists of - * two ranges containing `(` and `)`, then we'd return the following: - * - * ```json - * [ - * { range: "(", type: "boundary" }, - * { range: "foo(hello)", type: "item" }, - * { range: ",", type: "separator" }, - * { range: "bar", type: "item" }, - * { range: ",", type: "separator" }, - * { range: "whatever", type: "item" }, - * { range: ")", type: "boundary" }, - * ] - * ``` - * - * Where each `range` isn't actually a string, but a range whose text is the - * given string. - * @param editor The editor containing the range - * @param interior The range to look for tokens within - * @param boundary Optional boundaries for collections. [], {} - * @returns List of tokens - */ -export function tokenizeRange( - editor: TextEditor, - interior: Range, - boundary?: [Range, Range], -): Token[] { - const { document } = editor; - const text = document.getText(interior); - /** - * The interior range tokenized into delimited regions, including the delimiters themselves. For example: - * `"foo(hello), bar, whatever"` => - * `["foo", "(", "hello", ")", ",", " bar", ",", " whatever"]` - */ - const lexemes = text - // NB: Both the delimiters and the text between them are included because we - // use a capture group in this split regex - .split(/([,(){}<>[\]"'`])|(? lexeme != null && lexeme.length > 0); - const joinedLexemes = joinLexemesBySkippingMatchingPairs(lexemes); - const tokens: Token[] = []; - let offset = document.offsetAt(interior.start); - - joinedLexemes.forEach((lexeme) => { - // Whitespace found. Just skip - if (lexeme.trim().length === 0) { - offset += lexeme.length; - return; - } - - // Separator delimiter found. - if (lexeme === separator) { - tokens.push({ - type: "separator", - range: new Range( - document.positionAt(offset), - document.positionAt(offset + lexeme.length), - ), - }); - } - - // Text/item content found - else { - const offsetStart = offset + (lexeme.length - lexeme.trimStart().length); - tokens.push({ - type: "item", - range: new Range( - document.positionAt(offsetStart), - document.positionAt(offsetStart + lexeme.trim().length), - ), - }); - } - - offset += lexeme.length; - }); - - if (boundary != null) { - return [ - { type: "boundary", range: boundary[0] }, - ...tokens, - { type: "boundary", range: boundary[1] }, - ]; - } - - return tokens; -} - -/** - * Takes a list of lexemes and joins them into a list of alternating items and separators, skipping matching pairs (), {}, etc - * @param lexemes List of lexemes to operate on - * @returns List of merged lexemes. Note that its length will be less than or equal to {@link lexemes} - */ -export function joinLexemesBySkippingMatchingPairs(lexemes: string[]) { - const result: string[] = []; - /** - * The number of left delimiters minus right delimiters we've seen. If the - * balance is 0, we're at the top level of the collection, so separators are - * relevant. Otherwise we ignore separators because they're nested - */ - let delimiterBalance = 0; - /** The most recent opening delimiter we've seen */ - let openingDelimiter: string | null = null; - /** The closing delimiter we're currently looking for */ - let closingDelimiter: string | null = null; - /** - * The index in {@link lexemes} of the first lexeme in the current token we're - * merging. - */ - let startIndex: number = -1; - - lexemes.forEach((lexeme, index) => { - if (delimiterBalance > 0) { - // We are waiting for a closing delimiter - if (lexeme === closingDelimiter) { - // Closing delimiter found - --delimiterBalance; - } - // Additional opening delimiter found - else if (lexeme === openingDelimiter) { - ++delimiterBalance; - } - } - - // Starting delimiter found - // Make sure that there is a matching closing delimiter - else if ( - leftToRightMap[lexeme] != null && - lexemes.indexOf(leftToRightMap[lexeme], index + 1) > -1 - ) { - openingDelimiter = lexeme; - closingDelimiter = leftToRightMap[lexeme]; - delimiterBalance = 1; - if (startIndex < 0) { - // This is the first lexeme to be joined - startIndex = index; - } - } - - // This is the first lexeme to be joined - else if (startIndex < 0) { - startIndex = index; - } - - const isSeparator = lexeme === separator && delimiterBalance === 0; - - if (isSeparator || index === lexemes.length - 1) { - // This is the last lexeme to be joined - const endIndex = isSeparator ? index : index + 1; - result.push(lexemes.slice(startIndex, endIndex).join("")); - startIndex = -1; - if (isSeparator) { - // Add the separator itself - result.push(lexeme); - } - } - }); - - return result; -} - -const separator = ","; - -// Mapping between opening and closing delimiters -/* eslint-disable @typescript-eslint/naming-convention */ -const leftToRightMap: { [key: string]: string } = { - "(": ")", - "{": "}", - "<": ">", - "[": "]", - '"': '"', - "'": "'", - "`": "`", -}; -/* eslint-enable @typescript-eslint/naming-convention */ - -interface Token { - range: Range; - type: "item" | "separator" | "boundary"; -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts new file mode 100644 index 0000000000..70eff1def7 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts @@ -0,0 +1,38 @@ +import type { Direction, Position, TextEditor } from "@cursorless/common"; +import { type ScopeType } from "@cursorless/common"; +import type { LanguageDefinitions } from "../../../../languages/LanguageDefinitions"; +import { BaseScopeHandler } from "../BaseScopeHandler"; +import type { TargetScope } from "../scope.types"; +import type { ScopeIteratorRequirements } from "../scopeHandler.types"; + +export class CollectionItemScopeHandler extends BaseScopeHandler { + public scopeType: ScopeType = { type: "collectionItem" }; + protected isHierarchical = true; + + public readonly iterationScopeType: ScopeType = { + type: "oneOf", + scopeTypes: [ + { type: "line" }, + { + type: "surroundingPairInterior", + delimiter: "collectionBoundary", + }, + ], + }; + + constructor( + private languageDefinitions: LanguageDefinitions, + private languageId: string, + ) { + super(); + } + + *generateScopeCandidates( + editor: TextEditor, + position: Position, + direction: Direction, + _hints: ScopeIteratorRequirements, + ): Iterable { + // return null; + } +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts index 0d4b4056d7..5dce2e953a 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts @@ -5,6 +5,7 @@ import { BoundedParagraphScopeHandler, } from "./BoundedScopeHandler"; import { CharacterScopeHandler } from "./CharacterScopeHandler"; +import { CollectionItemScopeHandler } from "./CollectionItemScopeHandler/CollectionItemScopeHandler"; import { DocumentScopeHandler } from "./DocumentScopeHandler"; import { IdentifierScopeHandler } from "./IdentifierScopeHandler"; import { LineScopeHandler } from "./LineScopeHandler"; @@ -91,6 +92,11 @@ export class ScopeHandlerFactoryImpl implements ScopeHandlerFactory { return new CustomRegexScopeHandler(this, scopeType, languageId); case "glyph": return new GlyphScopeHandler(this, scopeType, languageId); + case "collectionItem": + return new CollectionItemScopeHandler( + this.languageDefinitions, + languageId, + ); case "surroundingPair": return new SurroundingPairScopeHandler( this.languageDefinitions, From 670afa85ef4f7f1c34151ac1da0eeb8c3f2068c1 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 18 Nov 2024 10:24:01 +0100 Subject: [PATCH 02/21] More work --- .../CollectionItemScopeHandler.ts | 122 +++++++++++++++++- .../createTargetScope.ts | 36 ++++++ .../getDelimiterOccurrences.ts | 15 +++ .../scopeHandlers/ScopeHandlerFactoryImpl.ts | 1 + 4 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/createTargetScope.ts create mode 100644 packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/getDelimiterOccurrences.ts diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts index 70eff1def7..56340575de 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts @@ -1,14 +1,28 @@ -import type { Direction, Position, TextEditor } from "@cursorless/common"; -import { type ScopeType } from "@cursorless/common"; +import { + type Direction, + type Position, + Range, + type ScopeType, + type TextEditor, +} from "@cursorless/common"; import type { LanguageDefinitions } from "../../../../languages/LanguageDefinitions"; +import { shrinkRangeToFitContent } from "../../../../util/selectionUtils"; import { BaseScopeHandler } from "../BaseScopeHandler"; import type { TargetScope } from "../scope.types"; -import type { ScopeIteratorRequirements } from "../scopeHandler.types"; +import type { + ScopeHandler, + ScopeIteratorRequirements, +} from "../scopeHandler.types"; +import type { ScopeHandlerFactory } from "../ScopeHandlerFactory"; +import { createTargetScope } from "./createTargetScope"; +import { getDelimiterOccurrences } from "./getDelimiterOccurrences"; export class CollectionItemScopeHandler extends BaseScopeHandler { public scopeType: ScopeType = { type: "collectionItem" }; protected isHierarchical = true; + private readonly surroundingPairInteriorScopeHandler: ScopeHandler; + public readonly iterationScopeType: ScopeType = { type: "oneOf", scopeTypes: [ @@ -21,18 +35,116 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { }; constructor( + private scopeHandlerFactory: ScopeHandlerFactory, private languageDefinitions: LanguageDefinitions, private languageId: string, ) { super(); + + this.surroundingPairInteriorScopeHandler = this.scopeHandlerFactory.create( + { + type: "surroundingPairInterior", + delimiter: "collectionBoundary", + }, + this.languageId, + )!; + } + + private getInteriorRanges( + editor: TextEditor, + position: Position, + direction: Direction, + hints: ScopeIteratorRequirements, + ): Range[] { + return Array.from( + this.surroundingPairInteriorScopeHandler.generateScopes( + editor, + position, + direction, + { + ...hints, + // For every (skipAncestorScopes=true) we don't want to go outside of the surrounding pair + containment: + hints.containment == null && hints.skipAncestorScopes + ? "required" + : hints.containment, + }, + ), + (scope) => scope.domain, + ); } *generateScopeCandidates( editor: TextEditor, position: Position, direction: Direction, - _hints: ScopeIteratorRequirements, + hints: ScopeIteratorRequirements, ): Iterable { - // return null; + const { document } = editor; + const delimiterRanges = getDelimiterOccurrences(document); + + const interiorRanges = this.getInteriorRanges( + editor, + position, + direction, + hints, + ); + + let delimitersInIteration: Range[] = []; + let previousIterationRange: Range | undefined; + + function createItemRanges() { + if (delimitersInIteration.length === 0) { + return []; + } + const itemRanges: Range[] = []; + for (let i = 0; i < delimitersInIteration.length; ++i) { + const current = delimitersInIteration[i]; + const previous = + delimitersInIteration[i - 1]?.end ?? previousIterationRange!.start; + const itemRange = new Range(previous, current.start); + itemRanges.push(itemRange); + } + const lastDelimiter = + delimitersInIteration[delimitersInIteration.length - 1]; + const itemRange = new Range( + lastDelimiter.end, + previousIterationRange!.end, + ); + itemRanges.push(itemRange); + const trimmedRanges = itemRanges.map((range) => + shrinkRangeToFitContent(editor, range), + ); + + return trimmedRanges.map((range, i) => + createTargetScope( + editor, + range, + trimmedRanges[i - 1], + trimmedRanges[i + 1], + ), + ); + } + + for (const delimiter of delimiterRanges) { + if ( + previousIterationRange != null && + previousIterationRange.contains(delimiter) + ) { + delimitersInIteration.push(delimiter); + continue; + } + + yield* createItemRanges(); + + const interiorRange = interiorRanges.find((range) => + range.contains(delimiter), + ); + previousIterationRange = + interiorRange ?? document.lineAt(delimiter.start.line).range; + delimitersInIteration = [delimiter]; + } + + yield* createItemRanges(); } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/createTargetScope.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/createTargetScope.ts new file mode 100644 index 0000000000..04056701a9 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/createTargetScope.ts @@ -0,0 +1,36 @@ +import { type TextEditor, Range } from "@cursorless/common"; +import { ScopeTypeTarget } from "../../../targets"; +import type { TargetScope } from "../scope.types"; + +export function createTargetScope( + editor: TextEditor, + contentRange: Range, + previousRange?: Range, + nextRange?: Range, +): TargetScope { + const leadingDelimiterRange = + previousRange != null + ? new Range(previousRange.end, contentRange.start) + : undefined; + const trailingDelimiterRange = + nextRange != null + ? new Range(contentRange.end, nextRange.start) + : undefined; + return { + editor, + domain: contentRange, + getTargets(isReversed) { + return [ + new ScopeTypeTarget({ + scopeTypeType: "collectionItem", + editor, + isReversed, + contentRange, + insertionDelimiter: ", ", + leadingDelimiterRange, + trailingDelimiterRange, + }), + ]; + }, + }; +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/getDelimiterOccurrences.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/getDelimiterOccurrences.ts new file mode 100644 index 0000000000..c319bb8ed5 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/getDelimiterOccurrences.ts @@ -0,0 +1,15 @@ +import { matchAll, Range, type TextDocument } from "@cursorless/common"; + +export function getDelimiterOccurrences(document: TextDocument): Range[] { + const delimiter = ","; + const delimiterRegex = new RegExp(delimiter, "g"); + + const text = document.getText(); + + return matchAll(text, delimiterRegex, (match): Range => { + return new Range( + document.positionAt(match.index!), + document.positionAt(match.index! + match[0].length), + ); + }); +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts index 5dce2e953a..6d32281899 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts @@ -94,6 +94,7 @@ export class ScopeHandlerFactoryImpl implements ScopeHandlerFactory { return new GlyphScopeHandler(this, scopeType, languageId); case "collectionItem": return new CollectionItemScopeHandler( + this, this.languageDefinitions, languageId, ); From 3acc3a075deec8eed8ae8d4a69cd8f974b541ca2 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 18 Nov 2024 14:59:52 +0100 Subject: [PATCH 03/21] Added iteration handler --- .../CollectionItemIterationScopeHandler.ts | 78 ++++++++++ .../CollectionItemScopeHandler.ts | 137 +++++++++++------- .../createTargetScope.ts | 18 ++- .../scopeHandlers/ScopeHandlerFactoryImpl.ts | 6 +- 4 files changed, 183 insertions(+), 56 deletions(-) create mode 100644 packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemIterationScopeHandler.ts diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemIterationScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemIterationScopeHandler.ts new file mode 100644 index 0000000000..d4da65056f --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemIterationScopeHandler.ts @@ -0,0 +1,78 @@ +import { + type Direction, + type Position, + type ScopeType, + type TextEditor, +} from "@cursorless/common"; +import { LineTarget } from "../../../targets"; +import { BaseScopeHandler } from "../BaseScopeHandler"; +import type { TargetScope } from "../scope.types"; +import type { + CustomScopeType, + ScopeHandler, + ScopeIteratorRequirements, +} from "../scopeHandler.types"; +import type { ScopeHandlerFactory } from "../ScopeHandlerFactory"; + +export class CollectionItemIterationScopeHandler extends BaseScopeHandler { + public scopeType: ScopeType = { type: "collectionItem" }; + protected isHierarchical = true; + + private readonly surroundingPairInteriorScopeHandler: ScopeHandler; + + get iterationScopeType(): CustomScopeType { + throw Error( + "Iteration scope doesn't exist for CollectionItemIterationScopeHandler", + ); + } + + constructor( + private scopeHandlerFactory: ScopeHandlerFactory, + private languageId: string, + ) { + super(); + + this.surroundingPairInteriorScopeHandler = this.scopeHandlerFactory.create( + { + type: "surroundingPairInterior", + delimiter: "collectionBoundary", + }, + this.languageId, + )!; + } + + *generateScopeCandidates( + editor: TextEditor, + position: Position, + direction: Direction, + hints: ScopeIteratorRequirements, + ): Iterable { + const scopes = this.surroundingPairInteriorScopeHandler.generateScopes( + editor, + position, + direction, + hints, + ); + + yield* scopes; + + yield makeLineScope(editor, position); + } +} + +function makeLineScope(editor: TextEditor, position: Position): TargetScope { + const contentRange = editor.document.lineAt(position.line).range; + return { + editor, + domain: contentRange, + getTargets(isReversed) { + return [ + new LineTarget({ + editor, + contentRange, + isReversed, + }), + ]; + }, + }; +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts index 56340575de..dfdc7f4ff7 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts @@ -5,15 +5,17 @@ import { type ScopeType, type TextEditor, } from "@cursorless/common"; -import type { LanguageDefinitions } from "../../../../languages/LanguageDefinitions"; import { shrinkRangeToFitContent } from "../../../../util/selectionUtils"; import { BaseScopeHandler } from "../BaseScopeHandler"; +import { compareTargetScopes } from "../compareTargetScopes"; import type { TargetScope } from "../scope.types"; import type { + CustomScopeType, ScopeHandler, ScopeIteratorRequirements, } from "../scopeHandler.types"; import type { ScopeHandlerFactory } from "../ScopeHandlerFactory"; +import { CollectionItemIterationScopeHandler } from "./CollectionItemIterationScopeHandler"; import { createTargetScope } from "./createTargetScope"; import { getDelimiterOccurrences } from "./getDelimiterOccurrences"; @@ -23,20 +25,18 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { private readonly surroundingPairInteriorScopeHandler: ScopeHandler; - public readonly iterationScopeType: ScopeType = { - type: "oneOf", - scopeTypes: [ - { type: "line" }, - { - type: "surroundingPairInterior", - delimiter: "collectionBoundary", - }, - ], - }; + get iterationScopeType(): CustomScopeType { + return { + type: "custom", + scopeHandler: new CollectionItemIterationScopeHandler( + this.scopeHandlerFactory, + this.languageId, + ), + }; + } constructor( private scopeHandlerFactory: ScopeHandlerFactory, - private languageDefinitions: LanguageDefinitions, private languageId: string, ) { super(); @@ -63,11 +63,7 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { direction, { ...hints, - // For every (skipAncestorScopes=true) we don't want to go outside of the surrounding pair - containment: - hints.containment == null && hints.skipAncestorScopes - ? "required" - : hints.containment, + containment: undefined, }, ), (scope) => scope.domain, @@ -90,61 +86,102 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { hints, ); + const scopes: TargetScope[] = []; + const usedInteriors = new Set(); let delimitersInIteration: Range[] = []; - let previousIterationRange: Range | undefined; + let previousInteriorRange: Range | undefined; + let previousLineRange: Range | undefined; - function createItemRanges() { + function addScopes() { if (delimitersInIteration.length === 0) { - return []; + return; } + + const previousIterationRange = (previousInteriorRange ?? + previousLineRange)!; const itemRanges: Range[] = []; + for (let i = 0; i < delimitersInIteration.length; ++i) { const current = delimitersInIteration[i]; + const previous = - delimitersInIteration[i - 1]?.end ?? previousIterationRange!.start; - const itemRange = new Range(previous, current.start); - itemRanges.push(itemRange); + delimitersInIteration[i - 1]?.end ?? previousIterationRange.start; + itemRanges.push(new Range(previous, current.start)); } + const lastDelimiter = delimitersInIteration[delimitersInIteration.length - 1]; - const itemRange = new Range( - lastDelimiter.end, - previousIterationRange!.end, - ); - itemRanges.push(itemRange); - const trimmedRanges = itemRanges.map((range) => - shrinkRangeToFitContent(editor, range), - ); + itemRanges.push(new Range(lastDelimiter.end, previousIterationRange.end)); - return trimmedRanges.map((range, i) => - createTargetScope( - editor, - range, - trimmedRanges[i - 1], - trimmedRanges[i + 1], - ), - ); + const trimmedRanges = itemRanges + .map((range) => shrinkRangeToFitContent(editor, range)) + .filter((range) => document.getText(range).trim() !== ""); + + for (let i = 0; i < trimmedRanges.length; ++i) { + scopes.push( + createTargetScope( + editor, + previousIterationRange, + trimmedRanges[i], + trimmedRanges[i - 1], + trimmedRanges[i + 1], + ), + ); + } } for (const delimiter of delimiterRanges) { - if ( - previousIterationRange != null && - previousIterationRange.contains(delimiter) - ) { - delimitersInIteration.push(delimiter); - continue; + if (previousInteriorRange != null) { + if (previousInteriorRange.contains(delimiter)) { + delimitersInIteration.push(delimiter); + continue; + } + } else { + const interiorRange = interiorRanges.find((range) => + range.contains(delimiter), + ); + + if (interiorRange == null) { + if ( + previousLineRange != null && + previousLineRange.contains(delimiter) + ) { + delimitersInIteration.push(delimiter); + continue; + } + } } - yield* createItemRanges(); + addScopes(); - const interiorRange = interiorRanges.find((range) => + previousInteriorRange = interiorRanges.find((range) => range.contains(delimiter), ); - previousIterationRange = - interiorRange ?? document.lineAt(delimiter.start.line).range; + + if (previousInteriorRange != null) { + usedInteriors.add(previousInteriorRange); + previousLineRange = undefined; + } else { + previousLineRange = document.lineAt(delimiter.start.line).range; + } + delimitersInIteration = [delimiter]; } - yield* createItemRanges(); + addScopes(); + + // Add interior ranges without a delimiter in them. eg: `[foo]` + for (const interior of interiorRanges) { + if (!usedInteriors.has(interior)) { + const range = shrinkRangeToFitContent(editor, interior); + if (!range.isEmpty) { + scopes.push(createTargetScope(editor, interior, range)); + } + } + } + + yield* scopes.sort((a, b) => + compareTargetScopes(direction, position, a, b), + ); } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/createTargetScope.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/createTargetScope.ts index 04056701a9..a3ed83ad20 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/createTargetScope.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/createTargetScope.ts @@ -1,9 +1,11 @@ import { type TextEditor, Range } from "@cursorless/common"; +import { getRangeLength } from "../../../../util/rangeUtils"; import { ScopeTypeTarget } from "../../../targets"; import type { TargetScope } from "../scope.types"; export function createTargetScope( editor: TextEditor, + iterationRange: Range, contentRange: Range, previousRange?: Range, nextRange?: Range, @@ -16,6 +18,19 @@ export function createTargetScope( nextRange != null ? new Range(contentRange.end, nextRange.start) : undefined; + + // We have both leading and trailing delimiter ranges + // The leading one is longer/more specific so prefer to use that for removal. + const removalRange = + leadingDelimiterRange != null && + trailingDelimiterRange != null && + getRangeLength(editor, leadingDelimiterRange) > + getRangeLength(editor, trailingDelimiterRange) + ? contentRange.union(leadingDelimiterRange) + : undefined; + + const insertionDelimiter = iterationRange.isSingleLine ? ", " : ",\n"; + return { editor, domain: contentRange, @@ -26,9 +41,10 @@ export function createTargetScope( editor, isReversed, contentRange, - insertionDelimiter: ", ", + insertionDelimiter, leadingDelimiterRange, trailingDelimiterRange, + removalRange, }), ]; }, diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts index 6d32281899..bfee5fc595 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts @@ -93,11 +93,7 @@ export class ScopeHandlerFactoryImpl implements ScopeHandlerFactory { case "glyph": return new GlyphScopeHandler(this, scopeType, languageId); case "collectionItem": - return new CollectionItemScopeHandler( - this, - this.languageDefinitions, - languageId, - ); + return new CollectionItemScopeHandler(this, languageId); case "surroundingPair": return new SurroundingPairScopeHandler( this.languageDefinitions, From d88f3702abc34d8e9e17105de244d08b7207fd8c Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 18 Nov 2024 15:31:01 +0100 Subject: [PATCH 04/21] fix --- .../CollectionItemScopeHandler.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts index dfdc7f4ff7..49953f842e 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts @@ -113,11 +113,18 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { delimitersInIteration[delimitersInIteration.length - 1]; itemRanges.push(new Range(lastDelimiter.end, previousIterationRange.end)); - const trimmedRanges = itemRanges - .map((range) => shrinkRangeToFitContent(editor, range)) - .filter((range) => document.getText(range).trim() !== ""); + const trimmedRanges = itemRanges.map((range) => + shrinkRangeToFitContent(editor, range), + ); for (let i = 0; i < trimmedRanges.length; ++i) { + // Handle trailing delimiter + if ( + i === trimmedRanges.length - 1 && + document.getText(trimmedRanges[i]).trim() === "" + ) { + continue; + } scopes.push( createTargetScope( editor, From 9e83dadccabef027f5c86cea7c922018a1705818 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 18 Nov 2024 15:46:05 +0100 Subject: [PATCH 05/21] Update removal range --- .../CollectionItemScopeHandler/CollectionItemScopeHandler.ts | 4 +++- .../CollectionItemScopeHandler/createTargetScope.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts index 49953f842e..918925cd14 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts @@ -77,6 +77,7 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { hints: ScopeIteratorRequirements, ): Iterable { const { document } = editor; + const isEveryScope = hints.containment == null && hints.skipAncestorScopes; const delimiterRanges = getDelimiterOccurrences(document); const interiorRanges = this.getInteriorRanges( @@ -127,6 +128,7 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { } scopes.push( createTargetScope( + isEveryScope, editor, previousIterationRange, trimmedRanges[i], @@ -182,7 +184,7 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { if (!usedInteriors.has(interior)) { const range = shrinkRangeToFitContent(editor, interior); if (!range.isEmpty) { - scopes.push(createTargetScope(editor, interior, range)); + scopes.push(createTargetScope(isEveryScope, editor, interior, range)); } } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/createTargetScope.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/createTargetScope.ts index a3ed83ad20..d9edd0b636 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/createTargetScope.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/createTargetScope.ts @@ -4,6 +4,7 @@ import { ScopeTypeTarget } from "../../../targets"; import type { TargetScope } from "../scope.types"; export function createTargetScope( + isEveryScope: boolean, editor: TextEditor, iterationRange: Range, contentRange: Range, @@ -22,6 +23,7 @@ export function createTargetScope( // We have both leading and trailing delimiter ranges // The leading one is longer/more specific so prefer to use that for removal. const removalRange = + !isEveryScope && leadingDelimiterRange != null && trailingDelimiterRange != null && getRangeLength(editor, leadingDelimiterRange) > From a1c3909cb47b0b253ede2e334ac5f986983170d2 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 18 Nov 2024 16:12:39 +0100 Subject: [PATCH 06/21] ignore delimiters in string --- .../CollectionItemScopeHandler.ts | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts index 918925cd14..98b678a30a 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts @@ -24,6 +24,7 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { protected isHierarchical = true; private readonly surroundingPairInteriorScopeHandler: ScopeHandler; + private readonly surroundingPairStringScopeHandler: ScopeHandler; get iterationScopeType(): CustomScopeType { return { @@ -48,6 +49,13 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { }, this.languageId, )!; + this.surroundingPairStringScopeHandler = this.scopeHandlerFactory.create( + { + type: "surroundingPairInterior", + delimiter: "string", + }, + this.languageId, + )!; } private getInteriorRanges( @@ -70,6 +78,26 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { ); } + private getStringRanges( + editor: TextEditor, + position: Position, + direction: Direction, + hints: ScopeIteratorRequirements, + ): Range[] { + return Array.from( + this.surroundingPairStringScopeHandler.generateScopes( + editor, + position, + direction, + { + ...hints, + containment: undefined, + }, + ), + (scope) => scope.domain, + ); + } + *generateScopeCandidates( editor: TextEditor, position: Position, @@ -86,20 +114,26 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { direction, hints, ); + const stringRanges = this.getStringRanges( + editor, + position, + direction, + hints, + ); const scopes: TargetScope[] = []; const usedInteriors = new Set(); let delimitersInIteration: Range[] = []; let previousInteriorRange: Range | undefined; let previousLineRange: Range | undefined; + const previousIterationRanges: Range[] = []; function addScopes() { if (delimitersInIteration.length === 0) { return; } - const previousIterationRange = (previousInteriorRange ?? - previousLineRange)!; + const previousIterationRange = previousIterationRanges.pop()!; const itemRanges: Range[] = []; for (let i = 0; i < delimitersInIteration.length; ++i) { @@ -140,6 +174,10 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { } for (const delimiter of delimiterRanges) { + if (stringRanges.some((range) => range.contains(delimiter))) { + continue; + } + if (previousInteriorRange != null) { if (previousInteriorRange.contains(delimiter)) { delimitersInIteration.push(delimiter); @@ -174,6 +212,8 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { previousLineRange = document.lineAt(delimiter.start.line).range; } + previousIterationRanges.push(previousInteriorRange ?? previousLineRange!); + delimitersInIteration = [delimiter]; } From 3ff084329f4006891c8b6cb30897cf02cbed6e11 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 19 Nov 2024 00:07:01 +0100 Subject: [PATCH 07/21] Updates --- .../CollectionItemScopeHandler.ts | 184 +++++++----------- 1 file changed, 74 insertions(+), 110 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts index 98b678a30a..84afc47c1d 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts @@ -1,8 +1,9 @@ import { type Direction, - type Position, + Position, Range, type ScopeType, + type SurroundingPairName, type TextEditor, } from "@cursorless/common"; import { shrinkRangeToFitContent } from "../../../../util/selectionUtils"; @@ -11,7 +12,6 @@ import { compareTargetScopes } from "../compareTargetScopes"; import type { TargetScope } from "../scope.types"; import type { CustomScopeType, - ScopeHandler, ScopeIteratorRequirements, } from "../scopeHandler.types"; import type { ScopeHandlerFactory } from "../ScopeHandlerFactory"; @@ -23,9 +23,6 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { public scopeType: ScopeType = { type: "collectionItem" }; protected isHierarchical = true; - private readonly surroundingPairInteriorScopeHandler: ScopeHandler; - private readonly surroundingPairStringScopeHandler: ScopeHandler; - get iterationScopeType(): CustomScopeType { return { type: "custom", @@ -41,59 +38,25 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { private languageId: string, ) { super(); + } - this.surroundingPairInteriorScopeHandler = this.scopeHandlerFactory.create( - { - type: "surroundingPairInterior", - delimiter: "collectionBoundary", - }, - this.languageId, - )!; - this.surroundingPairStringScopeHandler = this.scopeHandlerFactory.create( + private getInteriorRanges( + editor: TextEditor, + delimiter: SurroundingPairName, + ): Range[] { + const scopeHandler = this.scopeHandlerFactory.create( { type: "surroundingPairInterior", - delimiter: "string", + delimiter, }, this.languageId, )!; - } - - private getInteriorRanges( - editor: TextEditor, - position: Position, - direction: Direction, - hints: ScopeIteratorRequirements, - ): Range[] { return Array.from( - this.surroundingPairInteriorScopeHandler.generateScopes( - editor, - position, - direction, - { - ...hints, - containment: undefined, - }, - ), - (scope) => scope.domain, - ); - } - - private getStringRanges( - editor: TextEditor, - position: Position, - direction: Direction, - hints: ScopeIteratorRequirements, - ): Range[] { - return Array.from( - this.surroundingPairStringScopeHandler.generateScopes( - editor, - position, - direction, - { - ...hints, - containment: undefined, - }, - ), + scopeHandler.generateScopes(editor, new Position(0, 0), "forward", { + containment: undefined, + skipAncestorScopes: false, + includeDescendantScopes: true, + }), (scope) => scope.domain, ); } @@ -108,45 +71,31 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { const isEveryScope = hints.containment == null && hints.skipAncestorScopes; const delimiterRanges = getDelimiterOccurrences(document); - const interiorRanges = this.getInteriorRanges( - editor, - position, - direction, - hints, - ); - const stringRanges = this.getStringRanges( - editor, - position, - direction, - hints, - ); + const interiorRanges = this.getInteriorRanges(editor, "collectionBoundary"); + const stringRanges = this.getInteriorRanges(editor, "string"); const scopes: TargetScope[] = []; const usedInteriors = new Set(); - let delimitersInIteration: Range[] = []; - let previousInteriorRange: Range | undefined; - let previousLineRange: Range | undefined; - const previousIterationRanges: Range[] = []; + const previousIterationRanges: IterationState[] = []; + + function addScopes(state: IterationState) { + const { delimiters, range: iterationRange } = state; - function addScopes() { - if (delimitersInIteration.length === 0) { + if (delimiters.length === 0) { return; } - const previousIterationRange = previousIterationRanges.pop()!; const itemRanges: Range[] = []; - for (let i = 0; i < delimitersInIteration.length; ++i) { - const current = delimitersInIteration[i]; + for (let i = 0; i < delimiters.length; ++i) { + const current = delimiters[i]; - const previous = - delimitersInIteration[i - 1]?.end ?? previousIterationRange.start; + const previous = delimiters[i - 1]?.end ?? iterationRange.start; itemRanges.push(new Range(previous, current.start)); } - const lastDelimiter = - delimitersInIteration[delimitersInIteration.length - 1]; - itemRanges.push(new Range(lastDelimiter.end, previousIterationRange.end)); + const lastDelimiter = delimiters[delimiters.length - 1]; + itemRanges.push(new Range(lastDelimiter.end, iterationRange.end)); const trimmedRanges = itemRanges.map((range) => shrinkRangeToFitContent(editor, range), @@ -164,7 +113,7 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { createTargetScope( isEveryScope, editor, - previousIterationRange, + iterationRange, trimmedRanges[i], trimmedRanges[i - 1], trimmedRanges[i + 1], @@ -174,50 +123,60 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { } for (const delimiter of delimiterRanges) { + // Delimiters in a string are not considered if (stringRanges.some((range) => range.contains(delimiter))) { continue; } - if (previousInteriorRange != null) { - if (previousInteriorRange.contains(delimiter)) { - delimitersInIteration.push(delimiter); + const currentState = + previousIterationRanges[previousIterationRanges.length - 1]; + + // Get range for smallest containing interior + const containingInteriorRange: Range | undefined = interiorRanges + .filter((range) => range.contains(delimiter)) + .sort((a, b) => (a.contains(b) ? 1 : b.contains(a) ? -1 : 0))[0]; + + // The contain range is either the interior or the line containing the delimiter + const containingRange = + containingInteriorRange ?? document.lineAt(delimiter.start.line).range; + + if (currentState != null) { + // The current containing range is the same as the previous one. Just append delimiter. + if (currentState.range.isRangeEqual(containingRange)) { + currentState.delimiters.push(delimiter); continue; } - } else { - const interiorRange = interiorRanges.find((range) => - range.contains(delimiter), - ); - if (interiorRange == null) { - if ( - previousLineRange != null && - previousLineRange.contains(delimiter) - ) { - delimitersInIteration.push(delimiter); - continue; - } + // The current containing range does not intersect previous one. Add scopes and remove state. + if (!currentState.range.contains(delimiter)) { + addScopes(currentState); + // Remove already added state + previousIterationRanges.pop(); } } - addScopes(); - - previousInteriorRange = interiorRanges.find((range) => - range.contains(delimiter), - ); - - if (previousInteriorRange != null) { - usedInteriors.add(previousInteriorRange); - previousLineRange = undefined; - } else { - previousLineRange = document.lineAt(delimiter.start.line).range; + // The current containing range is the same as the previous one. Just append delimiter. + if (previousIterationRanges.length > 0) { + const lastState = + previousIterationRanges[previousIterationRanges.length - 1]; + if (lastState.range.isRangeEqual(containingRange)) { + lastState.delimiters.push(delimiter); + continue; + } } - previousIterationRanges.push(previousInteriorRange ?? previousLineRange!); + // New containing range. Add it to the list. + usedInteriors.add(containingInteriorRange); - delimitersInIteration = [delimiter]; + previousIterationRanges.push({ + range: containingRange, + delimiters: [delimiter], + }); } - addScopes(); + for (const state of previousIterationRanges) { + addScopes(state); + } // Add interior ranges without a delimiter in them. eg: `[foo]` for (const interior of interiorRanges) { @@ -229,8 +188,13 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { } } - yield* scopes.sort((a, b) => - compareTargetScopes(direction, position, a, b), - ); + scopes.sort((a, b) => compareTargetScopes(direction, position, a, b)); + + yield* scopes; } } + +interface IterationState { + range: Range; + delimiters: Range[]; +} From 499f922bebb6ea820bdbef03c305faa100b43109 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 19 Nov 2024 00:08:37 +0100 Subject: [PATCH 08/21] Remove old the tests --- .../recorded/itemTextual/chuckItem4.yml | 31 ------------------- .../itemTextual/clearEveryItemToken.yml | 25 --------------- .../recorded/itemTextual/clearItem10.yml | 23 -------------- .../recorded/itemTextual/clearItem11.yml | 23 -------------- .../recorded/itemTextual/clearItem13.yml | 23 -------------- .../recorded/itemTextual/clearItem14.yml | 23 -------------- .../recorded/itemTextual/clearItem6.yml | 23 -------------- .../recorded/itemTextual/clearItemDrip.yml | 27 ---------------- 8 files changed, 198 deletions(-) delete mode 100644 data/fixtures/recorded/itemTextual/chuckItem4.yml delete mode 100644 data/fixtures/recorded/itemTextual/clearEveryItemToken.yml delete mode 100644 data/fixtures/recorded/itemTextual/clearItem10.yml delete mode 100644 data/fixtures/recorded/itemTextual/clearItem11.yml delete mode 100644 data/fixtures/recorded/itemTextual/clearItem13.yml delete mode 100644 data/fixtures/recorded/itemTextual/clearItem14.yml delete mode 100644 data/fixtures/recorded/itemTextual/clearItem6.yml delete mode 100644 data/fixtures/recorded/itemTextual/clearItemDrip.yml diff --git a/data/fixtures/recorded/itemTextual/chuckItem4.yml b/data/fixtures/recorded/itemTextual/chuckItem4.yml deleted file mode 100644 index fbbe2a17cf..0000000000 --- a/data/fixtures/recorded/itemTextual/chuckItem4.yml +++ /dev/null @@ -1,31 +0,0 @@ -languageId: plaintext -command: - version: 6 - spokenForm: chuck item - action: - name: remove - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - usePrePhraseSnapshot: true -initialState: - documentContents: |- - [ - foo, - bar, - baz, - ] - selections: - - anchor: {line: 2, character: 8} - active: {line: 2, character: 7} - marks: {} -finalState: - documentContents: |- - [ - foo, - ] - selections: - - anchor: {line: 1, character: 7} - active: {line: 1, character: 7} diff --git a/data/fixtures/recorded/itemTextual/clearEveryItemToken.yml b/data/fixtures/recorded/itemTextual/clearEveryItemToken.yml deleted file mode 100644 index c706e90534..0000000000 --- a/data/fixtures/recorded/itemTextual/clearEveryItemToken.yml +++ /dev/null @@ -1,25 +0,0 @@ -languageId: plaintext -command: - version: 6 - spokenForm: change every item token - action: - name: clearAndSetSelection - target: - type: primitive - modifiers: - - type: everyScope - scopeType: {type: collectionItem} - - type: containingScope - scopeType: {type: token} - usePrePhraseSnapshot: true -initialState: - documentContents: aaa bbb, ccc - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} - marks: {} -finalState: - documentContents: ", ccc" - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/itemTextual/clearItem10.yml b/data/fixtures/recorded/itemTextual/clearItem10.yml deleted file mode 100644 index 7bd7c2b453..0000000000 --- a/data/fixtures/recorded/itemTextual/clearItem10.yml +++ /dev/null @@ -1,23 +0,0 @@ -languageId: typescript -command: - version: 6 - spokenForm: change item - action: - name: clearAndSetSelection - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - usePrePhraseSnapshot: false -initialState: - documentContents: foo(hello, world) - selections: - - anchor: {line: 0, character: 10} - active: {line: 0, character: 10} - marks: {} -finalState: - documentContents: foo(hello, ) - selections: - - anchor: {line: 0, character: 11} - active: {line: 0, character: 11} diff --git a/data/fixtures/recorded/itemTextual/clearItem11.yml b/data/fixtures/recorded/itemTextual/clearItem11.yml deleted file mode 100644 index 52ad8ebe0d..0000000000 --- a/data/fixtures/recorded/itemTextual/clearItem11.yml +++ /dev/null @@ -1,23 +0,0 @@ -languageId: typescript -command: - version: 6 - spokenForm: change item - action: - name: clearAndSetSelection - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - usePrePhraseSnapshot: false -initialState: - documentContents: foo(hello, world) - selections: - - anchor: {line: 0, character: 7} - active: {line: 0, character: 13} - marks: {} -finalState: - documentContents: foo() - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/itemTextual/clearItem13.yml b/data/fixtures/recorded/itemTextual/clearItem13.yml deleted file mode 100644 index 692134407a..0000000000 --- a/data/fixtures/recorded/itemTextual/clearItem13.yml +++ /dev/null @@ -1,23 +0,0 @@ -languageId: plaintext -command: - version: 6 - spokenForm: change item - action: - name: clearAndSetSelection - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - usePrePhraseSnapshot: true -initialState: - documentContents: "[1, [2, 3]];" - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 5} - marks: {} -finalState: - documentContents: "[1, ];" - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/itemTextual/clearItem14.yml b/data/fixtures/recorded/itemTextual/clearItem14.yml deleted file mode 100644 index fc438d0268..0000000000 --- a/data/fixtures/recorded/itemTextual/clearItem14.yml +++ /dev/null @@ -1,23 +0,0 @@ -languageId: plaintext -command: - version: 6 - spokenForm: change item - action: - name: clearAndSetSelection - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - usePrePhraseSnapshot: true -initialState: - documentContents: "[1, [2, 3]];" - selections: - - anchor: {line: 0, character: 10} - active: {line: 0, character: 9} - marks: {} -finalState: - documentContents: "[1, ];" - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/itemTextual/clearItem6.yml b/data/fixtures/recorded/itemTextual/clearItem6.yml deleted file mode 100644 index fd0471d915..0000000000 --- a/data/fixtures/recorded/itemTextual/clearItem6.yml +++ /dev/null @@ -1,23 +0,0 @@ -languageId: plaintext -command: - version: 6 - spokenForm: change item - action: - name: clearAndSetSelection - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - usePrePhraseSnapshot: true -initialState: - documentContents: aaa aaa - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} - marks: {} -finalState: - documentContents: "" - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/itemTextual/clearItemDrip.yml b/data/fixtures/recorded/itemTextual/clearItemDrip.yml deleted file mode 100644 index 545936cc75..0000000000 --- a/data/fixtures/recorded/itemTextual/clearItemDrip.yml +++ /dev/null @@ -1,27 +0,0 @@ -languageId: typescript -command: - version: 6 - spokenForm: change item comma - action: - name: clearAndSetSelection - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - mark: {type: decoratedSymbol, symbolColor: default, character: ','} - usePrePhraseSnapshot: true -initialState: - documentContents: foo(hello, world) - selections: - - anchor: {line: 0, character: 13} - active: {line: 0, character: 13} - marks: - default.,: - start: {line: 0, character: 9} - end: {line: 0, character: 10} -finalState: - documentContents: foo() - selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} From 2896e139e70ba29b5996f98bbcfc6ad3bf7c9598 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 19 Nov 2024 00:51:04 +0100 Subject: [PATCH 09/21] Added one off --- .../modifiers/ContainingScopeStage.ts | 23 -- .../modifiers/EveryScopeStage.ts | 7 - .../CollectionItemIterationScopeHandler.ts | 5 +- .../CollectionItemScopeHandler.ts | 215 ++++-------------- .../CollectionItemTextualScopeHandler.ts | 200 ++++++++++++++++ .../scopeHandlers/OneOfScopeHandler.ts | 14 ++ .../scopeHandlers/ScopeHandlerFactoryImpl.ts | 6 +- 7 files changed, 266 insertions(+), 204 deletions(-) create mode 100644 packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts diff --git a/packages/cursorless-engine/src/processTargets/modifiers/ContainingScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/ContainingScopeStage.ts index 6b22f1c783..dcfa4af961 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/ContainingScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/ContainingScopeStage.ts @@ -50,29 +50,6 @@ export class ContainingScopeStage implements ModifierStage { scopeHandler, ancestorIndex, ); - if (scopeType.type === "collectionItem") { - // For `collectionItem`, combine with generic implementation - try { - const legacyScopes = this.modifierStageFactory - .getLegacyScopeStage(this.modifier) - .run(target); - if (containingScopes == null) { - return legacyScopes; - } - if (containingScopes.length === 1 && legacyScopes.length === 1) { - const containingRange = containingScopes[0].contentRange; - const legacyRange = legacyScopes[0].contentRange; - if ( - containingRange.contains(legacyRange) && - !containingRange.isRangeEqual(legacyRange) - ) { - return legacyScopes; - } - } - } catch (_ex) { - // Do nothing - } - } if (containingScopes == null) { throw new NoContainingScopeError(this.modifier.scopeType.type); diff --git a/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts index 8d8f00df85..592eaa8c78 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts @@ -90,13 +90,6 @@ export class EveryScopeStage implements ModifierStage { } if (scopes.length === 0) { - if (scopeType.type === "collectionItem") { - // For `collectionItem`, fall back to generic implementation - return this.modifierStageFactory - .getLegacyScopeStage(this.modifier) - .run(target); - } - throw new NoContainingScopeError(scopeType.type); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemIterationScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemIterationScopeHandler.ts index d4da65056f..1d1ad00e99 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemIterationScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemIterationScopeHandler.ts @@ -1,4 +1,5 @@ import { + NoContainingScopeError, type Direction, type Position, type ScopeType, @@ -21,8 +22,8 @@ export class CollectionItemIterationScopeHandler extends BaseScopeHandler { private readonly surroundingPairInteriorScopeHandler: ScopeHandler; get iterationScopeType(): CustomScopeType { - throw Error( - "Iteration scope doesn't exist for CollectionItemIterationScopeHandler", + throw new NoContainingScopeError( + "Iteration scope for CollectionItemIterationScopeHandler", ); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts index 84afc47c1d..e6a4c0090a 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts @@ -1,200 +1,73 @@ -import { - type Direction, +import type { + Direction, Position, - Range, - type ScopeType, - type SurroundingPairName, - type TextEditor, + ScopeType, + TextEditor, } from "@cursorless/common"; -import { shrinkRangeToFitContent } from "../../../../util/selectionUtils"; +import type { LanguageDefinitions } from "../../../../languages/LanguageDefinitions"; import { BaseScopeHandler } from "../BaseScopeHandler"; -import { compareTargetScopes } from "../compareTargetScopes"; +import { OneOfScopeHandler } from "../OneOfScopeHandler"; import type { TargetScope } from "../scope.types"; import type { CustomScopeType, + ScopeHandler, ScopeIteratorRequirements, } from "../scopeHandler.types"; import type { ScopeHandlerFactory } from "../ScopeHandlerFactory"; -import { CollectionItemIterationScopeHandler } from "./CollectionItemIterationScopeHandler"; -import { createTargetScope } from "./createTargetScope"; -import { getDelimiterOccurrences } from "./getDelimiterOccurrences"; +import { CollectionItemTextualScopeHandler } from "./CollectionItemTextualScopeHandler"; export class CollectionItemScopeHandler extends BaseScopeHandler { public scopeType: ScopeType = { type: "collectionItem" }; protected isHierarchical = true; - get iterationScopeType(): CustomScopeType { - return { - type: "custom", - scopeHandler: new CollectionItemIterationScopeHandler( - this.scopeHandlerFactory, - this.languageId, - ), - }; + private scopeHandler: ScopeHandler; + + get iterationScopeType(): ScopeType | CustomScopeType { + return this.scopeHandler.iterationScopeType; } constructor( - private scopeHandlerFactory: ScopeHandlerFactory, - private languageId: string, + scopeHandlerFactory: ScopeHandlerFactory, + languageDefinitions: LanguageDefinitions, + languageId: string, ) { super(); - } - private getInteriorRanges( - editor: TextEditor, - delimiter: SurroundingPairName, - ): Range[] { - const scopeHandler = this.scopeHandlerFactory.create( - { - type: "surroundingPairInterior", - delimiter, - }, - this.languageId, - )!; - return Array.from( - scopeHandler.generateScopes(editor, new Position(0, 0), "forward", { - containment: undefined, - skipAncestorScopes: false, - includeDescendantScopes: true, - }), - (scope) => scope.domain, - ); + this.scopeHandler = (() => { + const textualScopeHandler = new CollectionItemTextualScopeHandler( + scopeHandlerFactory, + languageId, + ); + + const languageScopeHandler = languageDefinitions + .get(languageId) + ?.getScopeHandler(this.scopeType); + + if (languageScopeHandler == null) { + return textualScopeHandler; + } + + return OneOfScopeHandler.createFromScopeHandlers( + scopeHandlerFactory, + { + type: "oneOf", + scopeTypes: [ + textualScopeHandler.scopeType, + languageScopeHandler.scopeType, + ], + }, + [textualScopeHandler, languageScopeHandler], + languageId, + ); + })(); } - *generateScopeCandidates( + generateScopeCandidates( editor: TextEditor, position: Position, direction: Direction, hints: ScopeIteratorRequirements, ): Iterable { - const { document } = editor; - const isEveryScope = hints.containment == null && hints.skipAncestorScopes; - const delimiterRanges = getDelimiterOccurrences(document); - - const interiorRanges = this.getInteriorRanges(editor, "collectionBoundary"); - const stringRanges = this.getInteriorRanges(editor, "string"); - - const scopes: TargetScope[] = []; - const usedInteriors = new Set(); - const previousIterationRanges: IterationState[] = []; - - function addScopes(state: IterationState) { - const { delimiters, range: iterationRange } = state; - - if (delimiters.length === 0) { - return; - } - - const itemRanges: Range[] = []; - - for (let i = 0; i < delimiters.length; ++i) { - const current = delimiters[i]; - - const previous = delimiters[i - 1]?.end ?? iterationRange.start; - itemRanges.push(new Range(previous, current.start)); - } - - const lastDelimiter = delimiters[delimiters.length - 1]; - itemRanges.push(new Range(lastDelimiter.end, iterationRange.end)); - - const trimmedRanges = itemRanges.map((range) => - shrinkRangeToFitContent(editor, range), - ); - - for (let i = 0; i < trimmedRanges.length; ++i) { - // Handle trailing delimiter - if ( - i === trimmedRanges.length - 1 && - document.getText(trimmedRanges[i]).trim() === "" - ) { - continue; - } - scopes.push( - createTargetScope( - isEveryScope, - editor, - iterationRange, - trimmedRanges[i], - trimmedRanges[i - 1], - trimmedRanges[i + 1], - ), - ); - } - } - - for (const delimiter of delimiterRanges) { - // Delimiters in a string are not considered - if (stringRanges.some((range) => range.contains(delimiter))) { - continue; - } - - const currentState = - previousIterationRanges[previousIterationRanges.length - 1]; - - // Get range for smallest containing interior - const containingInteriorRange: Range | undefined = interiorRanges - .filter((range) => range.contains(delimiter)) - .sort((a, b) => (a.contains(b) ? 1 : b.contains(a) ? -1 : 0))[0]; - - // The contain range is either the interior or the line containing the delimiter - const containingRange = - containingInteriorRange ?? document.lineAt(delimiter.start.line).range; - - if (currentState != null) { - // The current containing range is the same as the previous one. Just append delimiter. - if (currentState.range.isRangeEqual(containingRange)) { - currentState.delimiters.push(delimiter); - continue; - } - - // The current containing range does not intersect previous one. Add scopes and remove state. - if (!currentState.range.contains(delimiter)) { - addScopes(currentState); - // Remove already added state - previousIterationRanges.pop(); - } - } - - // The current containing range is the same as the previous one. Just append delimiter. - if (previousIterationRanges.length > 0) { - const lastState = - previousIterationRanges[previousIterationRanges.length - 1]; - if (lastState.range.isRangeEqual(containingRange)) { - lastState.delimiters.push(delimiter); - continue; - } - } - - // New containing range. Add it to the list. - usedInteriors.add(containingInteriorRange); - - previousIterationRanges.push({ - range: containingRange, - delimiters: [delimiter], - }); - } - - for (const state of previousIterationRanges) { - addScopes(state); - } - - // Add interior ranges without a delimiter in them. eg: `[foo]` - for (const interior of interiorRanges) { - if (!usedInteriors.has(interior)) { - const range = shrinkRangeToFitContent(editor, interior); - if (!range.isEmpty) { - scopes.push(createTargetScope(isEveryScope, editor, interior, range)); - } - } - } - - scopes.sort((a, b) => compareTargetScopes(direction, position, a, b)); - - yield* scopes; + return this.scopeHandler.generateScopes(editor, position, direction, hints); } } - -interface IterationState { - range: Range; - delimiters: Range[]; -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts new file mode 100644 index 0000000000..c7fd3704dd --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts @@ -0,0 +1,200 @@ +import { + type Direction, + Position, + Range, + type ScopeType, + type SurroundingPairName, + type TextEditor, +} from "@cursorless/common"; +import { shrinkRangeToFitContent } from "../../../../util/selectionUtils"; +import { BaseScopeHandler } from "../BaseScopeHandler"; +import { compareTargetScopes } from "../compareTargetScopes"; +import type { TargetScope } from "../scope.types"; +import type { + CustomScopeType, + ScopeIteratorRequirements, +} from "../scopeHandler.types"; +import type { ScopeHandlerFactory } from "../ScopeHandlerFactory"; +import { CollectionItemIterationScopeHandler } from "./CollectionItemIterationScopeHandler"; +import { createTargetScope } from "./createTargetScope"; +import { getDelimiterOccurrences } from "./getDelimiterOccurrences"; + +export class CollectionItemTextualScopeHandler extends BaseScopeHandler { + public scopeType: ScopeType = { type: "collectionItem" }; + protected isHierarchical = true; + + get iterationScopeType(): CustomScopeType { + return { + type: "custom", + scopeHandler: new CollectionItemIterationScopeHandler( + this.scopeHandlerFactory, + this.languageId, + ), + }; + } + + constructor( + private scopeHandlerFactory: ScopeHandlerFactory, + private languageId: string, + ) { + super(); + } + + private getInteriorRanges( + editor: TextEditor, + delimiter: SurroundingPairName, + ): Range[] { + const scopeHandler = this.scopeHandlerFactory.create( + { + type: "surroundingPairInterior", + delimiter, + }, + this.languageId, + )!; + return Array.from( + scopeHandler.generateScopes(editor, new Position(0, 0), "forward", { + containment: undefined, + skipAncestorScopes: false, + includeDescendantScopes: true, + }), + (scope) => scope.domain, + ); + } + + *generateScopeCandidates( + editor: TextEditor, + position: Position, + direction: Direction, + hints: ScopeIteratorRequirements, + ): Iterable { + const { document } = editor; + const isEveryScope = hints.containment == null && hints.skipAncestorScopes; + const delimiterRanges = getDelimiterOccurrences(document); + + const interiorRanges = this.getInteriorRanges(editor, "collectionBoundary"); + const stringRanges = this.getInteriorRanges(editor, "string"); + + const scopes: TargetScope[] = []; + const usedInteriors = new Set(); + const previousIterationRanges: IterationState[] = []; + + function addScopes(state: IterationState) { + const { delimiters, range: iterationRange } = state; + + if (delimiters.length === 0) { + return; + } + + const itemRanges: Range[] = []; + + for (let i = 0; i < delimiters.length; ++i) { + const current = delimiters[i]; + + const previous = delimiters[i - 1]?.end ?? iterationRange.start; + itemRanges.push(new Range(previous, current.start)); + } + + const lastDelimiter = delimiters[delimiters.length - 1]; + itemRanges.push(new Range(lastDelimiter.end, iterationRange.end)); + + const trimmedRanges = itemRanges.map((range) => + shrinkRangeToFitContent(editor, range), + ); + + for (let i = 0; i < trimmedRanges.length; ++i) { + // Handle trailing delimiter + if ( + i === trimmedRanges.length - 1 && + document.getText(trimmedRanges[i]).trim() === "" + ) { + continue; + } + scopes.push( + createTargetScope( + isEveryScope, + editor, + iterationRange, + trimmedRanges[i], + trimmedRanges[i - 1], + trimmedRanges[i + 1], + ), + ); + } + } + + for (const delimiter of delimiterRanges) { + // Delimiters in a string are not considered + if (stringRanges.some((range) => range.contains(delimiter))) { + continue; + } + + const currentState = + previousIterationRanges[previousIterationRanges.length - 1]; + + // Get range for smallest containing interior + const containingInteriorRange: Range | undefined = interiorRanges + .filter((range) => range.contains(delimiter)) + .sort((a, b) => (a.contains(b) ? 1 : b.contains(a) ? -1 : 0))[0]; + + // The contain range is either the interior or the line containing the delimiter + const containingRange = + containingInteriorRange ?? document.lineAt(delimiter.start.line).range; + + if (currentState != null) { + // The current containing range is the same as the previous one. Just append delimiter. + if (currentState.range.isRangeEqual(containingRange)) { + currentState.delimiters.push(delimiter); + continue; + } + + // The current containing range does not intersect previous one. Add scopes and remove state. + if (!currentState.range.contains(delimiter)) { + addScopes(currentState); + // Remove already added state + previousIterationRanges.pop(); + } + } + + // The current containing range is the same as the previous one. Just append delimiter. + if (previousIterationRanges.length > 0) { + const lastState = + previousIterationRanges[previousIterationRanges.length - 1]; + if (lastState.range.isRangeEqual(containingRange)) { + lastState.delimiters.push(delimiter); + continue; + } + } + + // New containing range. Add it to the list. + usedInteriors.add(containingInteriorRange); + + previousIterationRanges.push({ + range: containingRange, + delimiters: [delimiter], + }); + } + + for (const state of previousIterationRanges) { + addScopes(state); + } + + // Add interior ranges without a delimiter in them. eg: `[foo]` + for (const interior of interiorRanges) { + if (!usedInteriors.has(interior)) { + const range = shrinkRangeToFitContent(editor, interior); + if (!range.isEmpty) { + scopes.push(createTargetScope(isEveryScope, editor, interior, range)); + } + } + } + + scopes.sort((a, b) => compareTargetScopes(direction, position, a, b)); + + yield* scopes; + } +} + +interface IterationState { + range: Range; + delimiters: Range[]; +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts index d8ad2cdb52..8d3a8b989f 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts @@ -33,6 +33,20 @@ export class OneOfScopeHandler extends BaseScopeHandler { }, ); + return this.createFromScopeHandlers( + scopeHandlerFactory, + scopeType, + scopeHandlers, + languageId, + ); + } + + static createFromScopeHandlers( + scopeHandlerFactory: ScopeHandlerFactory, + scopeType: OneOfScopeType, + scopeHandlers: ScopeHandler[], + languageId: string, + ): ScopeHandler { const iterationScopeType = (): CustomScopeType => ({ type: "custom", scopeHandler: new OneOfScopeHandler( diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts index bfee5fc595..6d32281899 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts @@ -93,7 +93,11 @@ export class ScopeHandlerFactoryImpl implements ScopeHandlerFactory { case "glyph": return new GlyphScopeHandler(this, scopeType, languageId); case "collectionItem": - return new CollectionItemScopeHandler(this, languageId); + return new CollectionItemScopeHandler( + this, + this.languageDefinitions, + languageId, + ); case "surroundingPair": return new SurroundingPairScopeHandler( this.languageDefinitions, From 2ba4434da425bd4d0c175429bbbb146f28c328b9 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 19 Nov 2024 01:33:13 +0100 Subject: [PATCH 10/21] Don't yield iteration scope missing delimiters --- .../languages/typescript/takeItem4.yml | 27 ---------------- .../languages/typescript/takeItemComma.yml | 31 ------------------- .../CollectionItemIterationScopeHandler.ts | 19 ++++++++++-- .../getDelimiterOccurrences.ts | 7 +++-- 4 files changed, 21 insertions(+), 63 deletions(-) delete mode 100644 data/fixtures/recorded/languages/typescript/takeItem4.yml delete mode 100644 data/fixtures/recorded/languages/typescript/takeItemComma.yml diff --git a/data/fixtures/recorded/languages/typescript/takeItem4.yml b/data/fixtures/recorded/languages/typescript/takeItem4.yml deleted file mode 100644 index 675da100a1..0000000000 --- a/data/fixtures/recorded/languages/typescript/takeItem4.yml +++ /dev/null @@ -1,27 +0,0 @@ -languageId: typescript -command: - version: 6 - spokenForm: take item - action: - name: setSelection - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - usePrePhraseSnapshot: false -initialState: - documentContents: | - - const value = { a: 1, b: 2, c: 3 }; - selections: - - anchor: {line: 1, character: 21} - active: {line: 1, character: 21} - marks: {} -finalState: - documentContents: | - - const value = { a: 1, b: 2, c: 3 }; - selections: - - anchor: {line: 1, character: 22} - active: {line: 1, character: 26} diff --git a/data/fixtures/recorded/languages/typescript/takeItemComma.yml b/data/fixtures/recorded/languages/typescript/takeItemComma.yml deleted file mode 100644 index 8cadeffc69..0000000000 --- a/data/fixtures/recorded/languages/typescript/takeItemComma.yml +++ /dev/null @@ -1,31 +0,0 @@ -languageId: typescript -command: - version: 6 - spokenForm: take item comma - action: - name: setSelection - target: - type: primitive - modifiers: - - type: containingScope - scopeType: {type: collectionItem} - mark: {type: decoratedSymbol, symbolColor: default, character: ','} - usePrePhraseSnapshot: false -initialState: - documentContents: | - - const value = { a: 1, b: 2, c: 3 }; - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} - marks: - default.,: - start: {line: 1, character: 20} - end: {line: 1, character: 21} -finalState: - documentContents: | - - const value = { a: 1, b: 2, c: 3 }; - selections: - - anchor: {line: 1, character: 16} - active: {line: 1, character: 26} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemIterationScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemIterationScopeHandler.ts index 1d1ad00e99..5686fcedeb 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemIterationScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemIterationScopeHandler.ts @@ -1,5 +1,6 @@ import { NoContainingScopeError, + testRegex, type Direction, type Position, type ScopeType, @@ -14,6 +15,7 @@ import type { ScopeIteratorRequirements, } from "../scopeHandler.types"; import type { ScopeHandlerFactory } from "../ScopeHandlerFactory"; +import { delimiterRegex } from "./getDelimiterOccurrences"; export class CollectionItemIterationScopeHandler extends BaseScopeHandler { public scopeType: ScopeType = { type: "collectionItem" }; @@ -57,12 +59,25 @@ export class CollectionItemIterationScopeHandler extends BaseScopeHandler { yield* scopes; - yield makeLineScope(editor, position); + const lineScope = makeLineScope(editor, position); + + if (lineScope != null) { + yield lineScope; + } } } -function makeLineScope(editor: TextEditor, position: Position): TargetScope { +function makeLineScope( + editor: TextEditor, + position: Position, +): TargetScope | null { const contentRange = editor.document.lineAt(position.line).range; + const text = editor.document.getText(contentRange); + + if (!testRegex(delimiterRegex, text)) { + return null; + } + return { editor, domain: contentRange, diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/getDelimiterOccurrences.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/getDelimiterOccurrences.ts index c319bb8ed5..efdc81a319 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/getDelimiterOccurrences.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/getDelimiterOccurrences.ts @@ -1,9 +1,10 @@ import { matchAll, Range, type TextDocument } from "@cursorless/common"; -export function getDelimiterOccurrences(document: TextDocument): Range[] { - const delimiter = ","; - const delimiterRegex = new RegExp(delimiter, "g"); +const delimiter = ","; + +export const delimiterRegex = new RegExp(delimiter, "g"); +export function getDelimiterOccurrences(document: TextDocument): Range[] { const text = document.getText(); return matchAll(text, delimiterRegex, (match): Range => { From 45c045bbacf291b0e5b05a38fc8953556d6c8ffb Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 19 Nov 2024 10:23:37 +0100 Subject: [PATCH 11/21] Update rust --- data/fixtures/recorded/languages/rust/changeItemOne.yml | 6 +++--- packages/cursorless-engine/src/languages/rust.ts | 5 ----- .../CollectionItemScopeHandler.ts | 1 - queries/rust.scm | 3 +++ 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/data/fixtures/recorded/languages/rust/changeItemOne.yml b/data/fixtures/recorded/languages/rust/changeItemOne.yml index 34b4d26b62..556b73e8b8 100644 --- a/data/fixtures/recorded/languages/rust/changeItemOne.yml +++ b/data/fixtures/recorded/languages/rust/changeItemOne.yml @@ -23,7 +23,7 @@ initialState: end: {line: 0, character: 21} finalState: documentContents: | - let x = [None, ]; + let x = [None, Some()]; selections: - - anchor: {line: 0, character: 15} - active: {line: 0, character: 15} + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} diff --git a/packages/cursorless-engine/src/languages/rust.ts b/packages/cursorless-engine/src/languages/rust.ts index 21f1d5186a..f7bf5eda7f 100644 --- a/packages/cursorless-engine/src/languages/rust.ts +++ b/packages/cursorless-engine/src/languages/rust.ts @@ -159,11 +159,6 @@ const nodeMatchers: Partial< ), leadingMatcher(["*.match_pattern![condition]"], ["if"]), ), - collectionItem: argumentMatcher( - "array_expression", - "tuple_expression", - "tuple_type", - ), type: cascadingMatcher( leadingMatcher( [ diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts index e6a4c0090a..33c45af2db 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts @@ -19,7 +19,6 @@ import { CollectionItemTextualScopeHandler } from "./CollectionItemTextualScopeH export class CollectionItemScopeHandler extends BaseScopeHandler { public scopeType: ScopeType = { type: "collectionItem" }; protected isHierarchical = true; - private scopeHandler: ScopeHandler; get iterationScopeType(): ScopeType | CustomScopeType { diff --git a/queries/rust.scm b/queries/rust.scm index 22dfafefce..1031506c46 100644 --- a/queries/rust.scm +++ b/queries/rust.scm @@ -82,3 +82,6 @@ operator: [ (macro_rule "=>" @disqualifyDelimiter ) +(lifetime + "'" @disqualifyDelimiter +) From 3b959ae621c3865984cf802c4116e146c4e756d7 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 19 Nov 2024 10:34:53 +0100 Subject: [PATCH 12/21] Update ruby --- queries/ruby.scm | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/queries/ruby.scm b/queries/ruby.scm index 99a5a5e905..473a701a53 100644 --- a/queries/ruby.scm +++ b/queries/ruby.scm @@ -69,3 +69,29 @@ operator: [ (match_pattern "=>" @disqualifyDelimiter ) + +;;!! %w(foo bar) +;;! ^^^ ^^^ +( + (string_array + (bare_string)? @_.leading.endOf + . + (bare_string) @collectionItem + . + (bare_string)? @_.trailing.startOf + ) + (#insertion-delimiter! @collectionItem " ") +) + +;;!! %i(foo bar) +;;! ^^^ ^^^ +( + (symbol_array + (bare_symbol)? @_.leading.endOf + . + (bare_symbol) @collectionItem + . + (bare_symbol)? @_.trailing.startOf + ) + (#insertion-delimiter! @collectionItem " ") +) From ded5c798d83ea293c86e7c3b1496778046446494 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 19 Nov 2024 10:35:03 +0100 Subject: [PATCH 13/21] Update ruby --- packages/cursorless-engine/src/languages/ruby.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/cursorless-engine/src/languages/ruby.ts b/packages/cursorless-engine/src/languages/ruby.ts index ba186c3188..62ed5a1a92 100644 --- a/packages/cursorless-engine/src/languages/ruby.ts +++ b/packages/cursorless-engine/src/languages/ruby.ts @@ -113,9 +113,6 @@ const EXPRESSION_STATEMENT_PARENT_TYPES = [ "then", ]; -const mapTypes = ["hash"]; -const listTypes = ["array", "string_array", "symbol_array"]; - const assignmentOperators = [ "=", "+=", @@ -187,6 +184,5 @@ const nodeMatchers: Partial< ], assignmentOperators.concat(mapKeyValueSeparators), ), - collectionItem: argumentMatcher(...mapTypes, ...listTypes), }; export const patternMatchers = createPatternMatchers(nodeMatchers); From 7fb6dc62c936ebfacbc56fa11e7677c8d2edd58d Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 19 Nov 2024 10:37:28 +0100 Subject: [PATCH 14/21] Updates support --- .../src/scopeProviders/ScopeSupportChecker.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/cursorless-engine/src/scopeProviders/ScopeSupportChecker.ts b/packages/cursorless-engine/src/scopeProviders/ScopeSupportChecker.ts index a3844346eb..26cbe408da 100644 --- a/packages/cursorless-engine/src/scopeProviders/ScopeSupportChecker.ts +++ b/packages/cursorless-engine/src/scopeProviders/ScopeSupportChecker.ts @@ -88,9 +88,6 @@ function getLegacyScopeSupport( scopeType: ScopeType, ): ScopeSupport { switch (scopeType.type) { - case "boundedNonWhitespaceSequence": - case "surroundingPair": - return ScopeSupport.supportedLegacy; case "notebookCell": // FIXME: What to do here return ScopeSupport.unsupported; From 78ebe5645de49e00fd0d0e2e6c359bd6a7342d96 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 19 Nov 2024 10:58:01 +0100 Subject: [PATCH 15/21] Update latex --- queries/latex.scm | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/queries/latex.scm b/queries/latex.scm index c0ce621d5c..a67decdc20 100644 --- a/queries/latex.scm +++ b/queries/latex.scm @@ -35,3 +35,18 @@ ">" ] @disqualifyDelimiter ) + +;;!! \item one \LaTeX +;;! ^^^^^^^^^^ +( + (_ + (enum_item + (text) @collectionItem.start.startOf + ) @collectionItem.leading.startOf @collectionItem.end.endOf + ) +) + +(generic_environment + (begin) @collectionItem.iteration.start.endOf + (end) @collectionItem.iteration.end.startOf +) @collectionItem.iteration.domain From 99aa3d01e6ad6ac18644b8a897dbf7e36516e80f Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 19 Nov 2024 10:58:48 +0100 Subject: [PATCH 16/21] Cleanup latex --- .../cursorless-engine/src/languages/latex.ts | 34 +------------------ 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/packages/cursorless-engine/src/languages/latex.ts b/packages/cursorless-engine/src/languages/latex.ts index 46bb4e0c57..22f5001cca 100644 --- a/packages/cursorless-engine/src/languages/latex.ts +++ b/packages/cursorless-engine/src/languages/latex.ts @@ -1,5 +1,5 @@ import type { SimpleScopeTypeType, TextEditor } from "@cursorless/common"; -import { Range, Selection } from "@cursorless/common"; +import { Selection } from "@cursorless/common"; import type { SyntaxNode } from "web-tree-sitter"; import type { NodeMatcherAlternative, @@ -120,36 +120,6 @@ function extendToNamedSiblingIfExists( }; } -function extractItemContent( - editor: TextEditor, - node: SyntaxNode, -): SelectionWithContext { - let contentStartIndex = node.startIndex; - - const label = node.childForFieldName("label"); - if (label == null) { - const command = node.childForFieldName("command"); - if (command != null) { - contentStartIndex = command.endIndex + 1; - } - } else { - contentStartIndex = label.endIndex + 1; - } - - return { - selection: new Selection( - editor.document.positionAt(contentStartIndex), - editor.document.positionAt(node.endIndex), - ), - context: { - leadingDelimiterRange: new Range( - editor.document.positionAt(node.startIndex), - editor.document.positionAt(contentStartIndex - 1), - ), - }, - }; -} - const nodeMatchers: Partial< Record > = { @@ -174,8 +144,6 @@ const nodeMatchers: Partial< matcher(patternFinder(...sectioningText), unwrapGroupParens), patternMatcher("begin[name][text]", "end[name][text]"), ), - - collectionItem: matcher(patternFinder("enum_item"), extractItemContent), }; export default createPatternMatchers(nodeMatchers); From 0004e983070d83e81ef8c38dee105e66129d4c11 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 19 Nov 2024 14:00:11 +0100 Subject: [PATCH 17/21] Update closuje --- .../languages/clojure/chuckItemZip.yml | 6 ++-- .../languages/clojure/clearEveryItem.yml | 5 ++- .../recorded/languages/clojure/clearItem.yml | 9 ++++-- .../languages/clojure/clearItemFine.yml | 2 +- .../src/languages/clojure.ts | 32 +++---------------- queries/clojure.scm | 30 +++++++++++++++++ 6 files changed, 45 insertions(+), 39 deletions(-) diff --git a/data/fixtures/recorded/languages/clojure/chuckItemZip.yml b/data/fixtures/recorded/languages/clojure/chuckItemZip.yml index 3972d6f0ea..6e5c8a77d5 100644 --- a/data/fixtures/recorded/languages/clojure/chuckItemZip.yml +++ b/data/fixtures/recorded/languages/clojure/chuckItemZip.yml @@ -29,9 +29,7 @@ finalState: documentContents: |- { :foo "bar", - ;; hello - , } selections: - - anchor: {line: 4, character: 1} - active: {line: 4, character: 1} + - anchor: {line: 2, character: 1} + active: {line: 2, character: 1} diff --git a/data/fixtures/recorded/languages/clojure/clearEveryItem.yml b/data/fixtures/recorded/languages/clojure/clearEveryItem.yml index b4202900fd..99f7622d33 100644 --- a/data/fixtures/recorded/languages/clojure/clearEveryItem.yml +++ b/data/fixtures/recorded/languages/clojure/clearEveryItem.yml @@ -27,11 +27,10 @@ finalState: { , - ;; hello , } selections: - anchor: {line: 2, character: 4} active: {line: 2, character: 4} - - anchor: {line: 4, character: 4} - active: {line: 4, character: 4} + - anchor: {line: 3, character: 4} + active: {line: 3, character: 4} diff --git a/data/fixtures/recorded/languages/clojure/clearItem.yml b/data/fixtures/recorded/languages/clojure/clearItem.yml index c03ec79ef5..b956e1fa9e 100644 --- a/data/fixtures/recorded/languages/clojure/clearItem.yml +++ b/data/fixtures/recorded/languages/clojure/clearItem.yml @@ -26,8 +26,11 @@ initialState: finalState: documentContents: |- { - + :bongo { + :foo "bar", + , + } } selections: - - anchor: {line: 1, character: 4} - active: {line: 1, character: 4} + - anchor: {line: 3, character: 8} + active: {line: 3, character: 8} diff --git a/data/fixtures/recorded/languages/clojure/clearItemFine.yml b/data/fixtures/recorded/languages/clojure/clearItemFine.yml index df2f19e0e8..56bfdc6554 100644 --- a/data/fixtures/recorded/languages/clojure/clearItemFine.yml +++ b/data/fixtures/recorded/languages/clojure/clearItemFine.yml @@ -21,7 +21,7 @@ initialState: start: {line: 0, character: 2} end: {line: 0, character: 5} finalState: - documentContents: "{ :baz \"whatever\"}" + documentContents: "{}" selections: - anchor: {line: 0, character: 1} active: {line: 0, character: 1} diff --git a/packages/cursorless-engine/src/languages/clojure.ts b/packages/cursorless-engine/src/languages/clojure.ts index ed87759de9..4ecb9ef947 100644 --- a/packages/cursorless-engine/src/languages/clojure.ts +++ b/packages/cursorless-engine/src/languages/clojure.ts @@ -1,3 +1,7 @@ +import type { SimpleScopeTypeType } from "@cursorless/common"; +import type { SyntaxNode } from "web-tree-sitter"; +import type { NodeFinder, NodeMatcherAlternative } from "../typings/Types"; +import { patternFinder } from "../util/nodeFinders"; import { cascadingMatcher, chainedMatcher, @@ -5,13 +9,7 @@ import { matcher, patternMatcher, } from "../util/nodeMatchers"; -import type { NodeMatcherAlternative, NodeFinder } from "../typings/Types"; -import type { SimpleScopeTypeType } from "@cursorless/common"; -import type { SyntaxNode } from "web-tree-sitter"; -import { delimitedSelector } from "../util/nodeSelectors"; -import { identity } from "lodash-es"; import { getChildNodesForFieldName } from "../util/treeSitterUtils"; -import { patternFinder } from "../util/nodeFinders"; /** * Picks a node by rounding down and using the given parity. This function is @@ -73,13 +71,6 @@ function indexNodeFinder( }; } -function itemFinder() { - return indexNodeFinder( - (node) => node, - (nodeIndex: number) => nodeIndex, - ); -} - /** * Return the "value" node children of a given node. These are the items in a list * @param node The node whose children to get @@ -134,21 +125,6 @@ const nodeMatchers: Partial< Record > = { collectionKey: matcher(mapParityNodeFinder(0)), - collectionItem: cascadingMatcher( - // Treat each key value pair as a single item if we're in a map - matcher( - mapParityNodeFinder(0), - delimitedSelector( - (node) => node.type === "{" || node.type === "}", - ", ", - identity, - mapParityNodeFinder(1) as (node: SyntaxNode) => SyntaxNode, - ), - ), - - // Otherwise just treat every item within a list as an item - matcher(itemFinder()), - ), value: matcher(mapParityNodeFinder(1)), // FIXME: Handle formal parameters diff --git a/queries/clojure.scm b/queries/clojure.scm index 753d2f488e..31f196e9c3 100644 --- a/queries/clojure.scm +++ b/queries/clojure.scm @@ -11,3 +11,33 @@ (quoting_lit (list_lit) ) @list + +;;!! '(foo bar) +;;! ^^^ ^^^ +(list_lit + (_)? @_.leading.endOf + . + (_) @collectionItem.start + . + (_)? @_.trailing.startOf +) + +(list_lit + open: "(" @collectionItem.iteration.start.startOf + close: ")" @collectionItem.iteration.end.endOf +) @collectionItem.iteration.domain + +;;!! [foo bar] +;;! ^^^ ^^^ +(vec_lit + (_)? @_.leading.endOf + . + (_) @collectionItem.start + . + (_)? @_.trailing.startOf +) + +(vec_lit + open: "[" @collectionItem.iteration.start.startOf + close: "]" @collectionItem.iteration.end.endOf +) @collectionItem.iteration.domain From a25d3295ca58be0c4ae393dc1b9a15ac50612794 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 19 Nov 2024 16:21:20 +0100 Subject: [PATCH 18/21] Reorder handlers --- .../CollectionItemScopeHandler/CollectionItemScopeHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts index 33c45af2db..c2780c6fea 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts @@ -51,11 +51,11 @@ export class CollectionItemScopeHandler extends BaseScopeHandler { { type: "oneOf", scopeTypes: [ - textualScopeHandler.scopeType, languageScopeHandler.scopeType, + textualScopeHandler.scopeType, ], }, - [textualScopeHandler, languageScopeHandler], + [languageScopeHandler, textualScopeHandler], languageId, ); })(); From 4298d597dd1cf66f6d5d3500b378695b27761068 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 19 Nov 2024 16:24:08 +0100 Subject: [PATCH 19/21] Update tests --- .../collectionItem.unenclosed.scope | 48 +++++++++++++++---- ...collectionItem.unenclosed.iteration3.scope | 10 +++- ...collectionItem.unenclosed.iteration4.scope | 10 +++- .../python/collectionItem.unenclosed2.scope | 32 +++++++++---- .../python/collectionItem.unenclosed5.scope | 36 ++++++++++---- .../python/collectionItem.unenclosed6.scope | 32 +++++++++---- .../python/collectionItem.unenclosed7.scope | 48 +++++++++++++++---- 7 files changed, 170 insertions(+), 46 deletions(-) diff --git a/data/fixtures/scopes/javascript.core/collectionItem.unenclosed.scope b/data/fixtures/scopes/javascript.core/collectionItem.unenclosed.scope index 703a4b32bb..12cf5428b2 100644 --- a/data/fixtures/scopes/javascript.core/collectionItem.unenclosed.scope +++ b/data/fixtures/scopes/javascript.core/collectionItem.unenclosed.scope @@ -2,12 +2,12 @@ let foo, bar; --- [#1 Content] = -[#1 Domain] = 0:4-0:7 - >---< +[#1 Domain] = 0:0-0:7 + >-------< 0| let foo, bar; -[#1 Removal] = 0:4-0:9 - >-----< +[#1 Removal] = 0:0-0:9 + >---------< 0| let foo, bar; [#1 Trailing delimiter] = 0:7-0:9 @@ -18,16 +18,48 @@ let foo, bar; [#2 Content] = -[#2 Domain] = 0:9-0:12 +[#2 Domain] = 0:4-0:7 + >---< +0| let foo, bar; + +[#2 Removal] = 0:4-0:9 + >-----< +0| let foo, bar; + +[#2 Trailing delimiter] = 0:7-0:9 + >--< +0| let foo, bar; + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:9-0:12 >---< 0| let foo, bar; -[#2 Removal] = 0:7-0:12 +[#3 Removal] = 0:7-0:12 >-----< 0| let foo, bar; -[#2 Leading delimiter] = 0:7-0:9 +[#3 Leading delimiter] = 0:7-0:9 >--< 0| let foo, bar; -[#2 Insertion delimiter] = ", " +[#3 Insertion delimiter] = ", " + + +[#4 Content] = +[#4 Domain] = 0:9-0:13 + >----< +0| let foo, bar; + +[#4 Removal] = 0:7-0:13 + >------< +0| let foo, bar; + +[#4 Leading delimiter] = 0:7-0:9 + >--< +0| let foo, bar; + +[#4 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/python/collectionItem.unenclosed.iteration3.scope b/data/fixtures/scopes/python/collectionItem.unenclosed.iteration3.scope index d24c8b8d1a..ba629ed73c 100644 --- a/data/fixtures/scopes/python/collectionItem.unenclosed.iteration3.scope +++ b/data/fixtures/scopes/python/collectionItem.unenclosed.iteration3.scope @@ -2,10 +2,16 @@ def foo(): global bar, baz --- -[Range] = 1:11-1:19 +[#1 Range] = +[#1 Domain] = 0:8-0:8 + >< +0| def foo(): + + +[#2 Range] = 1:11-1:19 >--------< 1| global bar, baz -[Domain] = 1:4-1:19 +[#2 Domain] = 1:4-1:19 >---------------< 1| global bar, baz diff --git a/data/fixtures/scopes/python/collectionItem.unenclosed.iteration4.scope b/data/fixtures/scopes/python/collectionItem.unenclosed.iteration4.scope index 79ad18b890..c5748186a1 100644 --- a/data/fixtures/scopes/python/collectionItem.unenclosed.iteration4.scope +++ b/data/fixtures/scopes/python/collectionItem.unenclosed.iteration4.scope @@ -2,7 +2,13 @@ for key, value in map.items(): pass --- -[Range] = -[Domain] = 0:4-0:14 +[#1 Range] = +[#1 Domain] = 0:4-0:14 >----------< 0| for key, value in map.items(): + + +[#2 Range] = +[#2 Domain] = 0:28-0:28 + >< +0| for key, value in map.items(): diff --git a/data/fixtures/scopes/python/collectionItem.unenclosed2.scope b/data/fixtures/scopes/python/collectionItem.unenclosed2.scope index fa8b5b2e11..ee8ba44d8e 100644 --- a/data/fixtures/scopes/python/collectionItem.unenclosed2.scope +++ b/data/fixtures/scopes/python/collectionItem.unenclosed2.scope @@ -2,12 +2,12 @@ import foo, bar --- [#1 Content] = -[#1 Domain] = 0:7-0:10 - >---< +[#1 Domain] = 0:0-0:10 + >----------< 0| import foo, bar -[#1 Removal] = 0:7-0:12 - >-----< +[#1 Removal] = 0:0-0:12 + >------------< 0| import foo, bar [#1 Trailing delimiter] = 0:10-0:12 @@ -18,16 +18,32 @@ import foo, bar [#2 Content] = -[#2 Domain] = 0:12-0:15 +[#2 Domain] = 0:7-0:10 + >---< +0| import foo, bar + +[#2 Removal] = 0:7-0:12 + >-----< +0| import foo, bar + +[#2 Trailing delimiter] = 0:10-0:12 + >--< +0| import foo, bar + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:12-0:15 >---< 0| import foo, bar -[#2 Removal] = 0:10-0:15 +[#3 Removal] = 0:10-0:15 >-----< 0| import foo, bar -[#2 Leading delimiter] = 0:10-0:12 +[#3 Leading delimiter] = 0:10-0:12 >--< 0| import foo, bar -[#2 Insertion delimiter] = ", " +[#3 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/python/collectionItem.unenclosed5.scope b/data/fixtures/scopes/python/collectionItem.unenclosed5.scope index 0ebe039ba3..f595441373 100644 --- a/data/fixtures/scopes/python/collectionItem.unenclosed5.scope +++ b/data/fixtures/scopes/python/collectionItem.unenclosed5.scope @@ -2,36 +2,52 @@ from foo import bar, baz --- [#1 Content] = -[#1 Domain] = 0:16-0:19 +[#1 Domain] = 0:0-0:19 + >-------------------< +0| from foo import bar, baz + +[#1 Removal] = 0:0-0:21 + >---------------------< +0| from foo import bar, baz + +[#1 Trailing delimiter] = 0:19-0:21 + >--< +0| from foo import bar, baz + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:16-0:19 >---< 0| from foo import bar, baz -[#1 Removal] = 0:16-0:21 +[#2 Removal] = 0:16-0:21 >-----< 0| from foo import bar, baz -[#1 Leading delimiter] = 0:15-0:16 +[#2 Leading delimiter] = 0:15-0:16 >-< 0| from foo import bar, baz -[#1 Trailing delimiter] = 0:19-0:21 +[#2 Trailing delimiter] = 0:19-0:21 >--< 0| from foo import bar, baz -[#1 Insertion delimiter] = ", " +[#2 Insertion delimiter] = ", " -[#2 Content] = -[#2 Domain] = 0:21-0:24 +[#3 Content] = +[#3 Domain] = 0:21-0:24 >---< 0| from foo import bar, baz -[#2 Removal] = 0:19-0:24 +[#3 Removal] = 0:19-0:24 >-----< 0| from foo import bar, baz -[#2 Leading delimiter] = 0:19-0:21 +[#3 Leading delimiter] = 0:19-0:21 >--< 0| from foo import bar, baz -[#2 Insertion delimiter] = ", " +[#3 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/python/collectionItem.unenclosed6.scope b/data/fixtures/scopes/python/collectionItem.unenclosed6.scope index 4b5eda3fd4..b4aa4158f1 100644 --- a/data/fixtures/scopes/python/collectionItem.unenclosed6.scope +++ b/data/fixtures/scopes/python/collectionItem.unenclosed6.scope @@ -3,12 +3,12 @@ def foo(): --- [#1 Content] = -[#1 Domain] = 1:11-1:14 - >---< +[#1 Domain] = 1:4-1:14 + >----------< 1| global bar, baz -[#1 Removal] = 1:11-1:16 - >-----< +[#1 Removal] = 1:4-1:16 + >------------< 1| global bar, baz [#1 Trailing delimiter] = 1:14-1:16 @@ -19,16 +19,32 @@ def foo(): [#2 Content] = -[#2 Domain] = 1:16-1:19 +[#2 Domain] = 1:11-1:14 + >---< +1| global bar, baz + +[#2 Removal] = 1:11-1:16 + >-----< +1| global bar, baz + +[#2 Trailing delimiter] = 1:14-1:16 + >--< +1| global bar, baz + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 1:16-1:19 >---< 1| global bar, baz -[#2 Removal] = 1:14-1:19 +[#3 Removal] = 1:14-1:19 >-----< 1| global bar, baz -[#2 Leading delimiter] = 1:14-1:16 +[#3 Leading delimiter] = 1:14-1:16 >--< 1| global bar, baz -[#2 Insertion delimiter] = ", " +[#3 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/python/collectionItem.unenclosed7.scope b/data/fixtures/scopes/python/collectionItem.unenclosed7.scope index 9f30c7a870..30844ef51c 100644 --- a/data/fixtures/scopes/python/collectionItem.unenclosed7.scope +++ b/data/fixtures/scopes/python/collectionItem.unenclosed7.scope @@ -3,12 +3,12 @@ for key, value in map.items(): --- [#1 Content] = -[#1 Domain] = 0:4-0:7 - >---< +[#1 Domain] = 0:0-0:7 + >-------< 0| for key, value in map.items(): -[#1 Removal] = 0:4-0:9 - >-----< +[#1 Removal] = 0:0-0:9 + >---------< 0| for key, value in map.items(): [#1 Trailing delimiter] = 0:7-0:9 @@ -19,16 +19,48 @@ for key, value in map.items(): [#2 Content] = -[#2 Domain] = 0:9-0:14 +[#2 Domain] = 0:4-0:7 + >---< +0| for key, value in map.items(): + +[#2 Removal] = 0:4-0:9 + >-----< +0| for key, value in map.items(): + +[#2 Trailing delimiter] = 0:7-0:9 + >--< +0| for key, value in map.items(): + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:9-0:14 >-----< 0| for key, value in map.items(): -[#2 Removal] = 0:7-0:14 +[#3 Removal] = 0:7-0:14 >-------< 0| for key, value in map.items(): -[#2 Leading delimiter] = 0:7-0:9 +[#3 Leading delimiter] = 0:7-0:9 >--< 0| for key, value in map.items(): -[#2 Insertion delimiter] = ", " +[#3 Insertion delimiter] = ", " + + +[#4 Content] = +[#4 Domain] = 0:9-0:30 + >---------------------< +0| for key, value in map.items(): + +[#4 Removal] = 0:7-0:30 + >-----------------------< +0| for key, value in map.items(): + +[#4 Leading delimiter] = 0:7-0:9 + >--< +0| for key, value in map.items(): + +[#4 Insertion delimiter] = ", " From 7630ea1483719df9525e5de80a8c2b81628bcf80 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 20 Nov 2024 04:12:49 +0100 Subject: [PATCH 20/21] Added last yielded index --- .../modifiers/scopeHandlers/IteratorInfo.ts | 10 +++-- .../scopeHandlers/OneOfScopeHandler.ts | 38 ++++++++++++++----- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/IteratorInfo.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/IteratorInfo.ts index 582c45375f..deafe0ad4a 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/IteratorInfo.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/IteratorInfo.ts @@ -5,6 +5,7 @@ interface IteratorInfo { iterator: Iterator; value: T; + index: number; } /** @@ -19,7 +20,7 @@ interface IteratorInfo { export function getInitialIteratorInfos( iterators: Iterator[], ): IteratorInfo[] { - return iterators.flatMap((iterator) => { + return iterators.flatMap((iterator, i) => { const { value, done } = iterator.next(); return done ? [] @@ -27,6 +28,7 @@ export function getInitialIteratorInfos( { iterator, value, + index: i, }, ]; }); @@ -47,10 +49,10 @@ export function advanceIteratorsUntil( criterion: (arg: T) => boolean, ): IteratorInfo[] { return iteratorInfos.flatMap((iteratorInfo) => { - const { iterator } = iteratorInfo; + const { iterator, index } = iteratorInfo; let { value } = iteratorInfo; - let done: boolean | undefined = false; + while (!done && !criterion(value)) { ({ value, done } = iterator.next()); } @@ -59,6 +61,6 @@ export function advanceIteratorsUntil( return []; } - return [{ iterator, value }]; + return [{ iterator, value, index }]; }); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts index 8d3a8b989f..2bad8b9690 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts @@ -17,6 +17,8 @@ import type { export class OneOfScopeHandler extends BaseScopeHandler { protected isHierarchical = true; + private iterationScopeHandler: OneOfScopeHandler | undefined; + private lastYieldedIndex: number | undefined; static create( scopeHandlerFactory: ScopeHandlerFactory, @@ -47,9 +49,8 @@ export class OneOfScopeHandler extends BaseScopeHandler { scopeHandlers: ScopeHandler[], languageId: string, ): ScopeHandler { - const iterationScopeType = (): CustomScopeType => ({ - type: "custom", - scopeHandler: new OneOfScopeHandler( + const getIterationScopeHandler = () => + new OneOfScopeHandler( undefined, scopeHandlers.map( (scopeHandler) => @@ -61,20 +62,29 @@ export class OneOfScopeHandler extends BaseScopeHandler { () => { throw new Error("Not implemented"); }, - ), - }); + ); - return new OneOfScopeHandler(scopeType, scopeHandlers, iterationScopeType); + return new OneOfScopeHandler( + scopeType, + scopeHandlers, + getIterationScopeHandler, + ); } get iterationScopeType(): CustomScopeType { - return this.getIterationScopeType(); + if (this.iterationScopeHandler == null) { + this.iterationScopeHandler = this.getIterationScopeHandler(); + } + return { + type: "custom", + scopeHandler: this.iterationScopeHandler, + }; } private constructor( public readonly scopeType: OneOfScopeType | undefined, private scopeHandlers: ScopeHandler[], - private getIterationScopeType: () => CustomScopeType, + private getIterationScopeHandler: () => OneOfScopeHandler, ) { super(); } @@ -85,6 +95,14 @@ export class OneOfScopeHandler extends BaseScopeHandler { direction: Direction, hints: ScopeIteratorRequirements, ): Iterable { + // If we have used the iteration scope handler we only want to yield from it's handler + if (this.iterationScopeHandler?.lastYieldedIndex != null) { + const handlerIndex = this.iterationScopeHandler.lastYieldedIndex; + const handler = this.scopeHandlers[handlerIndex]; + yield* handler.generateScopes(editor, position, direction, hints); + return; + } + const iterators = this.scopeHandlers.map((scopeHandler) => scopeHandler .generateScopes(editor, position, direction, hints) @@ -99,7 +117,9 @@ export class OneOfScopeHandler extends BaseScopeHandler { ); // Pick minimum scope according to canonical scope ordering - const currentScope = iteratorInfos[0].value; + const iteratorInfo = iteratorInfos[0]; + const currentScope = iteratorInfo.value; + this.lastYieldedIndex = iteratorInfo.index; yield currentScope; From 1ae5c322ffa71887bbeb7cd7404336fe98c821ea Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 20 Nov 2024 04:39:06 +0100 Subject: [PATCH 21/21] Added scope tests --- .../collectionItem.textual.iteration.scope | 7 ++ .../collectionItem.textual.iteration2.scope | 7 ++ .../collectionItem.textual.iteration3.scope | 7 ++ .../collectionItem.textual.iteration4.scope | 7 ++ .../collectionItem.textual.iteration5.scope | 10 +++ .../collectionItem.textual.iteration6.scope | 22 ++++++ .../collectionItem.textual.iteration7.scope | 22 ++++++ .../textual/collectionItem.textual.scope | 53 +++++++++++++ .../textual/collectionItem.textual10.scope | 65 ++++++++++++++++ .../textual/collectionItem.textual11.scope | 53 +++++++++++++ .../textual/collectionItem.textual12.scope | 21 ++++++ .../textual/collectionItem.textual13.scope | 10 +++ .../textual/collectionItem.textual14.scope | 21 ++++++ .../textual/collectionItem.textual2.scope | 53 +++++++++++++ .../textual/collectionItem.textual3.scope | 53 +++++++++++++ .../textual/collectionItem.textual4.scope | 53 +++++++++++++ .../textual/collectionItem.textual5.scope | 53 +++++++++++++ .../textual/collectionItem.textual6.scope | 75 +++++++++++++++++++ .../textual/collectionItem.textual7.scope | 75 +++++++++++++++++++ .../textual/collectionItem.textual8.scope | 33 ++++++++ .../textual/collectionItem.textual9.scope | 33 ++++++++ .../scopeSupportFacets.types.ts | 4 +- .../textualScopeSupportFacetInfos.ts | 13 ++++ 23 files changed, 749 insertions(+), 1 deletion(-) create mode 100644 data/fixtures/scopes/textual/collectionItem.textual.iteration.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual.iteration2.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual.iteration3.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual.iteration4.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual.iteration5.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual.iteration6.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual.iteration7.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual10.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual11.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual12.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual13.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual14.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual2.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual3.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual4.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual5.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual6.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual7.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual8.scope create mode 100644 data/fixtures/scopes/textual/collectionItem.textual9.scope diff --git a/data/fixtures/scopes/textual/collectionItem.textual.iteration.scope b/data/fixtures/scopes/textual/collectionItem.textual.iteration.scope new file mode 100644 index 0000000000..9cced361b5 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual.iteration.scope @@ -0,0 +1,7 @@ +(1, 2, 3) +--- + +[Range] = +[Domain] = 0:1-0:8 + >-------< +0| (1, 2, 3) diff --git a/data/fixtures/scopes/textual/collectionItem.textual.iteration2.scope b/data/fixtures/scopes/textual/collectionItem.textual.iteration2.scope new file mode 100644 index 0000000000..09af92177c --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual.iteration2.scope @@ -0,0 +1,7 @@ +[1, 2, 3] +--- + +[Range] = +[Domain] = 0:1-0:8 + >-------< +0| [1, 2, 3] diff --git a/data/fixtures/scopes/textual/collectionItem.textual.iteration3.scope b/data/fixtures/scopes/textual/collectionItem.textual.iteration3.scope new file mode 100644 index 0000000000..06f24d63a9 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual.iteration3.scope @@ -0,0 +1,7 @@ +{1, 2, 3} +--- + +[Range] = +[Domain] = 0:1-0:8 + >-------< +0| {1, 2, 3} diff --git a/data/fixtures/scopes/textual/collectionItem.textual.iteration4.scope b/data/fixtures/scopes/textual/collectionItem.textual.iteration4.scope new file mode 100644 index 0000000000..81c9d30a35 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual.iteration4.scope @@ -0,0 +1,7 @@ +<1, 2, 3> +--- + +[Range] = +[Domain] = 0:1-0:8 + >-------< +0| <1, 2, 3> diff --git a/data/fixtures/scopes/textual/collectionItem.textual.iteration5.scope b/data/fixtures/scopes/textual/collectionItem.textual.iteration5.scope new file mode 100644 index 0000000000..166cce77cd --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual.iteration5.scope @@ -0,0 +1,10 @@ +( 1, 2, 3 ) +--- + +[Range] = 0:2-0:9 + >-------< +0| ( 1, 2, 3 ) + +[Domain] = 0:1-0:10 + >---------< +0| ( 1, 2, 3 ) diff --git a/data/fixtures/scopes/textual/collectionItem.textual.iteration6.scope b/data/fixtures/scopes/textual/collectionItem.textual.iteration6.scope new file mode 100644 index 0000000000..ef6a9bcbc0 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual.iteration6.scope @@ -0,0 +1,22 @@ +[ + 1, + 2, + 3, +] +--- + +[Range] = 1:4-3:6 + >-- +1| 1, +2| 2, +3| 3, + ------< + +[Domain] = 0:1-4:0 + > +0| [ +1| 1, +2| 2, +3| 3, +4| ] + < diff --git a/data/fixtures/scopes/textual/collectionItem.textual.iteration7.scope b/data/fixtures/scopes/textual/collectionItem.textual.iteration7.scope new file mode 100644 index 0000000000..ad2a3ac43e --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual.iteration7.scope @@ -0,0 +1,22 @@ +{ + a: 1, + b: 2, + c: 3, +} +--- + +[Range] = 1:4-3:9 + >----- +1| a: 1, +2| b: 2, +3| c: 3, + ---------< + +[Domain] = 0:1-4:0 + > +0| { +1| a: 1, +2| b: 2, +3| c: 3, +4| } + < diff --git a/data/fixtures/scopes/textual/collectionItem.textual.scope b/data/fixtures/scopes/textual/collectionItem.textual.scope new file mode 100644 index 0000000000..b6fb913f1e --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual.scope @@ -0,0 +1,53 @@ +(1, 2, 3) +--- + +[#1 Content] = +[#1 Domain] = 0:1-0:2 + >-< +0| (1, 2, 3) + +[#1 Removal] = 0:1-0:4 + >---< +0| (1, 2, 3) + +[#1 Trailing delimiter] = 0:2-0:4 + >--< +0| (1, 2, 3) + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:4-0:5 + >-< +0| (1, 2, 3) + +[#2 Removal] = 0:4-0:7 + >---< +0| (1, 2, 3) + +[#2 Leading delimiter] = 0:2-0:4 + >--< +0| (1, 2, 3) + +[#2 Trailing delimiter] = 0:5-0:7 + >--< +0| (1, 2, 3) + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:7-0:8 + >-< +0| (1, 2, 3) + +[#3 Removal] = 0:5-0:8 + >---< +0| (1, 2, 3) + +[#3 Leading delimiter] = 0:5-0:7 + >--< +0| (1, 2, 3) + +[#3 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual10.scope b/data/fixtures/scopes/textual/collectionItem.textual10.scope new file mode 100644 index 0000000000..ecdbbf9473 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual10.scope @@ -0,0 +1,65 @@ +aaa, ( bbb, ccc ) +--- + +[#1 Content] = +[#1 Domain] = 0:0-0:3 + >---< +0| aaa, ( bbb, ccc ) + +[#1 Removal] = 0:0-0:5 + >-----< +0| aaa, ( bbb, ccc ) + +[#1 Trailing delimiter] = 0:3-0:5 + >--< +0| aaa, ( bbb, ccc ) + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:5-0:17 + >------------< +0| aaa, ( bbb, ccc ) + +[#2 Removal] = 0:3-0:17 + >--------------< +0| aaa, ( bbb, ccc ) + +[#2 Leading delimiter] = 0:3-0:5 + >--< +0| aaa, ( bbb, ccc ) + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:7-0:10 + >---< +0| aaa, ( bbb, ccc ) + +[#3 Removal] = 0:7-0:12 + >-----< +0| aaa, ( bbb, ccc ) + +[#3 Trailing delimiter] = 0:10-0:12 + >--< +0| aaa, ( bbb, ccc ) + +[#3 Insertion delimiter] = ", " + + +[#4 Content] = +[#4 Domain] = 0:12-0:15 + >---< +0| aaa, ( bbb, ccc ) + +[#4 Removal] = 0:10-0:15 + >-----< +0| aaa, ( bbb, ccc ) + +[#4 Leading delimiter] = 0:10-0:12 + >--< +0| aaa, ( bbb, ccc ) + +[#4 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual11.scope b/data/fixtures/scopes/textual/collectionItem.textual11.scope new file mode 100644 index 0000000000..cc98d05466 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual11.scope @@ -0,0 +1,53 @@ +[ + 1, + + 2, +] +--- + +[#1 Content] = +[#1 Domain] = 1:4-1:5 + >-< +1| 1, + +[#1 Removal] = 1:4-3:4 + >-- +1| 1, +2| +3| 2, + ----< + +[#1 Trailing delimiter] = 1:5-3:4 + >- +1| 1, +2| +3| 2, + ----< + +[#1 Insertion delimiter] = ",\n" + + +[#2 Content] = +[#2 Domain] = 3:4-3:5 + >-< +3| 2, + +[#2 Removal] = 1:5-3:5 + >- +1| 1, +2| +3| 2, + -----< + +[#2 Leading delimiter] = 1:5-3:4 + >- +1| 1, +2| +3| 2, + ----< + +[#2 Trailing delimiter] = 3:5-3:6 + >-< +3| 2, + +[#2 Insertion delimiter] = ",\n" diff --git a/data/fixtures/scopes/textual/collectionItem.textual12.scope b/data/fixtures/scopes/textual/collectionItem.textual12.scope new file mode 100644 index 0000000000..441a9eecad --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual12.scope @@ -0,0 +1,21 @@ +[ + + 1 + +] +--- + +[Content] = +[Domain] = 2:4-2:5 + >-< +2| 1 + +[Removal] = 2:0-2:5 + >-----< +2| 1 + +[Leading delimiter] = 2:0-2:4 + >----< +2| 1 + +[Insertion delimiter] = ",\n" diff --git a/data/fixtures/scopes/textual/collectionItem.textual13.scope b/data/fixtures/scopes/textual/collectionItem.textual13.scope new file mode 100644 index 0000000000..075adf3633 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual13.scope @@ -0,0 +1,10 @@ +(aaa) +--- + +[Content] = +[Removal] = +[Domain] = 0:1-0:4 + >---< +0| (aaa) + +[Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual14.scope b/data/fixtures/scopes/textual/collectionItem.textual14.scope new file mode 100644 index 0000000000..2e3a109339 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual14.scope @@ -0,0 +1,21 @@ +( aaa ) +--- + +[Content] = +[Domain] = 0:2-0:5 + >---< +0| ( aaa ) + +[Removal] = 0:2-0:6 + >----< +0| ( aaa ) + +[Leading delimiter] = 0:1-0:2 + >-< +0| ( aaa ) + +[Trailing delimiter] = 0:5-0:6 + >-< +0| ( aaa ) + +[Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual2.scope b/data/fixtures/scopes/textual/collectionItem.textual2.scope new file mode 100644 index 0000000000..885304a300 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual2.scope @@ -0,0 +1,53 @@ +[1, 2, 3] +--- + +[#1 Content] = +[#1 Domain] = 0:1-0:2 + >-< +0| [1, 2, 3] + +[#1 Removal] = 0:1-0:4 + >---< +0| [1, 2, 3] + +[#1 Trailing delimiter] = 0:2-0:4 + >--< +0| [1, 2, 3] + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:4-0:5 + >-< +0| [1, 2, 3] + +[#2 Removal] = 0:4-0:7 + >---< +0| [1, 2, 3] + +[#2 Leading delimiter] = 0:2-0:4 + >--< +0| [1, 2, 3] + +[#2 Trailing delimiter] = 0:5-0:7 + >--< +0| [1, 2, 3] + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:7-0:8 + >-< +0| [1, 2, 3] + +[#3 Removal] = 0:5-0:8 + >---< +0| [1, 2, 3] + +[#3 Leading delimiter] = 0:5-0:7 + >--< +0| [1, 2, 3] + +[#3 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual3.scope b/data/fixtures/scopes/textual/collectionItem.textual3.scope new file mode 100644 index 0000000000..5a5fb483bd --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual3.scope @@ -0,0 +1,53 @@ +{1, 2, 3} +--- + +[#1 Content] = +[#1 Domain] = 0:1-0:2 + >-< +0| {1, 2, 3} + +[#1 Removal] = 0:1-0:4 + >---< +0| {1, 2, 3} + +[#1 Trailing delimiter] = 0:2-0:4 + >--< +0| {1, 2, 3} + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:4-0:5 + >-< +0| {1, 2, 3} + +[#2 Removal] = 0:4-0:7 + >---< +0| {1, 2, 3} + +[#2 Leading delimiter] = 0:2-0:4 + >--< +0| {1, 2, 3} + +[#2 Trailing delimiter] = 0:5-0:7 + >--< +0| {1, 2, 3} + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:7-0:8 + >-< +0| {1, 2, 3} + +[#3 Removal] = 0:5-0:8 + >---< +0| {1, 2, 3} + +[#3 Leading delimiter] = 0:5-0:7 + >--< +0| {1, 2, 3} + +[#3 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual4.scope b/data/fixtures/scopes/textual/collectionItem.textual4.scope new file mode 100644 index 0000000000..6387887fbc --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual4.scope @@ -0,0 +1,53 @@ +<1, 2, 3> +--- + +[#1 Content] = +[#1 Domain] = 0:1-0:2 + >-< +0| <1, 2, 3> + +[#1 Removal] = 0:1-0:4 + >---< +0| <1, 2, 3> + +[#1 Trailing delimiter] = 0:2-0:4 + >--< +0| <1, 2, 3> + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:4-0:5 + >-< +0| <1, 2, 3> + +[#2 Removal] = 0:4-0:7 + >---< +0| <1, 2, 3> + +[#2 Leading delimiter] = 0:2-0:4 + >--< +0| <1, 2, 3> + +[#2 Trailing delimiter] = 0:5-0:7 + >--< +0| <1, 2, 3> + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:7-0:8 + >-< +0| <1, 2, 3> + +[#3 Removal] = 0:5-0:8 + >---< +0| <1, 2, 3> + +[#3 Leading delimiter] = 0:5-0:7 + >--< +0| <1, 2, 3> + +[#3 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual5.scope b/data/fixtures/scopes/textual/collectionItem.textual5.scope new file mode 100644 index 0000000000..9730f65458 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual5.scope @@ -0,0 +1,53 @@ +( 1, 2, 3 ) +--- + +[#1 Content] = +[#1 Domain] = 0:2-0:3 + >-< +0| ( 1, 2, 3 ) + +[#1 Removal] = 0:2-0:5 + >---< +0| ( 1, 2, 3 ) + +[#1 Trailing delimiter] = 0:3-0:5 + >--< +0| ( 1, 2, 3 ) + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:5-0:6 + >-< +0| ( 1, 2, 3 ) + +[#2 Removal] = 0:5-0:8 + >---< +0| ( 1, 2, 3 ) + +[#2 Leading delimiter] = 0:3-0:5 + >--< +0| ( 1, 2, 3 ) + +[#2 Trailing delimiter] = 0:6-0:8 + >--< +0| ( 1, 2, 3 ) + +[#2 Insertion delimiter] = ", " + + +[#3 Content] = +[#3 Domain] = 0:8-0:9 + >-< +0| ( 1, 2, 3 ) + +[#3 Removal] = 0:6-0:9 + >---< +0| ( 1, 2, 3 ) + +[#3 Leading delimiter] = 0:6-0:8 + >--< +0| ( 1, 2, 3 ) + +[#3 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual6.scope b/data/fixtures/scopes/textual/collectionItem.textual6.scope new file mode 100644 index 0000000000..b1ffcc88ca --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual6.scope @@ -0,0 +1,75 @@ +[ + 1, + 2, + 3, +] +--- + +[#1 Content] = +[#1 Domain] = 1:4-1:5 + >-< +1| 1, + +[#1 Removal] = 1:4-2:4 + >-- +1| 1, +2| 2, + ----< + +[#1 Trailing delimiter] = 1:5-2:4 + >- +1| 1, +2| 2, + ----< + +[#1 Insertion delimiter] = ",\n" + + +[#2 Content] = +[#2 Domain] = 2:4-2:5 + >-< +2| 2, + +[#2 Removal] = 2:4-3:4 + >-- +2| 2, +3| 3, + ----< + +[#2 Leading delimiter] = 1:5-2:4 + >- +1| 1, +2| 2, + ----< + +[#2 Trailing delimiter] = 2:5-3:4 + >- +2| 2, +3| 3, + ----< + +[#2 Insertion delimiter] = ",\n" + + +[#3 Content] = +[#3 Domain] = 3:4-3:5 + >-< +3| 3, + +[#3 Removal] = 2:5-3:5 + >- +2| 2, +3| 3, + -----< + +[#3 Leading delimiter] = 2:5-3:4 + >- +2| 2, +3| 3, + ----< + +[#3 Trailing delimiter] = 3:5-3:6 + >-< +3| 3, + +[#3 Insertion delimiter] = ",\n" diff --git a/data/fixtures/scopes/textual/collectionItem.textual7.scope b/data/fixtures/scopes/textual/collectionItem.textual7.scope new file mode 100644 index 0000000000..453ca9a258 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual7.scope @@ -0,0 +1,75 @@ +{ + a: 1, + b: 2, + c: 3, +} +--- + +[#1 Content] = +[#1 Domain] = 1:4-1:8 + >----< +1| a: 1, + +[#1 Removal] = 1:4-2:4 + >----- +1| a: 1, +2| b: 2, + ----< + +[#1 Trailing delimiter] = 1:8-2:4 + >- +1| a: 1, +2| b: 2, + ----< + +[#1 Insertion delimiter] = ",\n" + + +[#2 Content] = +[#2 Domain] = 2:4-2:8 + >----< +2| b: 2, + +[#2 Removal] = 2:4-3:4 + >----- +2| b: 2, +3| c: 3, + ----< + +[#2 Leading delimiter] = 1:8-2:4 + >- +1| a: 1, +2| b: 2, + ----< + +[#2 Trailing delimiter] = 2:8-3:4 + >- +2| b: 2, +3| c: 3, + ----< + +[#2 Insertion delimiter] = ",\n" + + +[#3 Content] = +[#3 Domain] = 3:4-3:8 + >----< +3| c: 3, + +[#3 Removal] = 2:8-3:8 + >- +2| b: 2, +3| c: 3, + --------< + +[#3 Leading delimiter] = 2:8-3:4 + >- +2| b: 2, +3| c: 3, + ----< + +[#3 Trailing delimiter] = 3:8-3:9 + >-< +3| c: 3, + +[#3 Insertion delimiter] = ",\n" diff --git a/data/fixtures/scopes/textual/collectionItem.textual8.scope b/data/fixtures/scopes/textual/collectionItem.textual8.scope new file mode 100644 index 0000000000..2b2cb01fd3 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual8.scope @@ -0,0 +1,33 @@ +[1, "2, 3"] +--- + +[#1 Content] = +[#1 Domain] = 0:1-0:2 + >-< +0| [1, "2, 3"] + +[#1 Removal] = 0:1-0:4 + >---< +0| [1, "2, 3"] + +[#1 Trailing delimiter] = 0:2-0:4 + >--< +0| [1, "2, 3"] + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:4-0:10 + >------< +0| [1, "2, 3"] + +[#2 Removal] = 0:2-0:10 + >--------< +0| [1, "2, 3"] + +[#2 Leading delimiter] = 0:2-0:4 + >--< +0| [1, "2, 3"] + +[#2 Insertion delimiter] = ", " diff --git a/data/fixtures/scopes/textual/collectionItem.textual9.scope b/data/fixtures/scopes/textual/collectionItem.textual9.scope new file mode 100644 index 0000000000..cfef536609 --- /dev/null +++ b/data/fixtures/scopes/textual/collectionItem.textual9.scope @@ -0,0 +1,33 @@ +aaa, bbb +--- + +[#1 Content] = +[#1 Domain] = 0:0-0:3 + >---< +0| aaa, bbb + +[#1 Removal] = 0:0-0:5 + >-----< +0| aaa, bbb + +[#1 Trailing delimiter] = 0:3-0:5 + >--< +0| aaa, bbb + +[#1 Insertion delimiter] = ", " + + +[#2 Content] = +[#2 Domain] = 0:5-0:8 + >---< +0| aaa, bbb + +[#2 Removal] = 0:3-0:8 + >-----< +0| aaa, bbb + +[#2 Leading delimiter] = 0:3-0:5 + >--< +0| aaa, bbb + +[#2 Insertion delimiter] = ", " diff --git a/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts b/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts index 638a1bea20..195aa88c6a 100644 --- a/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts +++ b/packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts @@ -204,7 +204,9 @@ export type TextualScopeSupportFacet = | "boundedNonWhitespaceSequence.iteration" | "url" | "surroundingPair" - | "surroundingPair.iteration"; + | "surroundingPair.iteration" + | "collectionItem.textual" + | "collectionItem.textual.iteration"; export type LanguageScopeSupportFacetMap = Partial< Record diff --git a/packages/common/src/scopeSupportFacets/textualScopeSupportFacetInfos.ts b/packages/common/src/scopeSupportFacets/textualScopeSupportFacetInfos.ts index eb933a8876..86c34b5abf 100644 --- a/packages/common/src/scopeSupportFacets/textualScopeSupportFacetInfos.ts +++ b/packages/common/src/scopeSupportFacets/textualScopeSupportFacetInfos.ts @@ -83,4 +83,17 @@ export const textualScopeSupportFacetInfos: Record< }, isIteration: true, }, + "collectionItem.textual": { + description: "A text based collection item", + scopeType: { + type: "collectionItem", + }, + }, + "collectionItem.textual.iteration": { + description: "Iteration scope for text based collection items", + scopeType: { + type: "collectionItem", + }, + isIteration: true, + }, };