From 2be775a9338ef587ec3eff2541f83d8b0071854d Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 7 Jun 2022 19:44:10 +0100 Subject: [PATCH 01/44] Added text based item scope --- src/processTargets/getModifierStage.ts | 3 + .../modifiers/scopeTypeStages/ItemStage.ts | 239 ++++++++++++++++++ .../recorded/selectionTypes/takeEveryItem.yml | 86 +++++++ .../selectionTypes/takeEveryItemAir.yml | 90 +++++++ .../recorded/selectionTypes/takeItemAir.yml | 46 ++++ .../selectionTypes/takeItemBlueRisk.yml | 46 ++++ 6 files changed, 510 insertions(+) create mode 100644 src/processTargets/modifiers/scopeTypeStages/ItemStage.ts create mode 100644 src/test/suite/fixtures/recorded/selectionTypes/takeEveryItem.yml create mode 100644 src/test/suite/fixtures/recorded/selectionTypes/takeEveryItemAir.yml create mode 100644 src/test/suite/fixtures/recorded/selectionTypes/takeItemAir.yml create mode 100644 src/test/suite/fixtures/recorded/selectionTypes/takeItemBlueRisk.yml diff --git a/src/processTargets/getModifierStage.ts b/src/processTargets/getModifierStage.ts index 5d40653d5b..a573375b3e 100644 --- a/src/processTargets/getModifierStage.ts +++ b/src/processTargets/getModifierStage.ts @@ -19,6 +19,7 @@ import ContainingSyntaxScopeStage, { SimpleContainingScopeModifier, } from "./modifiers/scopeTypeStages/ContainingSyntaxScopeStage"; import DocumentStage from "./modifiers/scopeTypeStages/DocumentStage"; +import ItemStage from "./modifiers/scopeTypeStages/ItemStage"; import LineStage from "./modifiers/scopeTypeStages/LineStage"; import NotebookCellStage from "./modifiers/scopeTypeStages/NotebookCellStage"; import ParagraphStage from "./modifiers/scopeTypeStages/ParagraphStage"; @@ -85,6 +86,8 @@ const getContainingScopeStage = ( ); case "url": return new UrlStage(modifier as UrlModifier); + case "collectionItem": + return new ItemStage(modifier); case "surroundingPair": return new SurroundingPairStage( modifier as ContainingSurroundingPairModifier diff --git a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts new file mode 100644 index 0000000000..a4056849c8 --- /dev/null +++ b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts @@ -0,0 +1,239 @@ +import { Range, TextEditor } from "vscode"; +import { + ContainingScopeModifier, + EveryScopeModifier, + SimpleScopeTypeType, + Target, +} from "../../../typings/target.types"; +import { ProcessedTargetsContext } from "../../../typings/Types"; +import { ModifierStage } from "../../PipelineStages.types"; +import ScopeTypeTarget from "../../targets/ScopeTypeTarget"; +import { processSurroundingPair } from "../surroundingPair"; +import { fitRangeToLineContent } from "./LineStage"; + +export default class ItemStage implements ModifierStage { + constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} + + run(context: ProcessedTargetsContext, target: Target): Target[] { + if (this.modifier.type === "everyScope") { + return this.getEveryTarget(context, target); + } + return [this.getSingleTarget(context, target)]; + } + + private getEveryTarget(context: ProcessedTargetsContext, target: Target) { + const itemInfos = getItemInfos(context, target); + + if (itemInfos.length < 1) { + throw Error(`Couldn't find containing ${this.modifier.scopeType.type}`); + } + + return itemInfos.map((itemInfo) => this.itemInfoToTarget(target, itemInfo)); + } + + private getSingleTarget(context: ProcessedTargetsContext, target: Target) { + const itemInfos = getItemInfos(context, target); + + const itemInfo = + itemInfos.find((itemInfo) => + itemInfo.range.intersection(target.contentRange) + ) ?? + itemInfos.find((itemInfo) => + itemInfo.delimiterRange?.intersection(target.contentRange) + ); + + if (itemInfo == null) { + throw Error(`Couldn't find containing ${this.modifier.scopeType.type}`); + } + + return this.itemInfoToTarget(target, itemInfo); + } + + private itemInfoToTarget(target: Target, itemInfo: ItemInfo) { + return new ScopeTypeTarget({ + scopeTypeType: this.modifier.scopeType.type, + editor: target.editor, + isReversed: target.isReversed, + contentRange: itemInfo.range, + delimiter, + leadingDelimiterRange: itemInfo.leadingDelimiterRange, + trailingDelimiterRange: itemInfo.trailingDelimiterRange, + }); + } +} + +function getItemInfos(context: ProcessedTargetsContext, target: Target) { + const collectionRange = getCollectionRange(context, target); + return tokensToItemInfos(target.editor, collectionRange); +} + +function getCollectionRange(context: ProcessedTargetsContext, target: Target) { + let pairInfo = getSurroundingPair( + context, + target.editor, + target.contentRange + ); + + while (pairInfo != null) { + // The selection from the beginning was this pair and we should not go into the interior but instead look in the parent. + const isNotInterior = + target.contentRange.isEqual(pairInfo.contentRange) || + target.contentRange.start.isBeforeOrEqual(pairInfo.boundary[0].start) || + target.contentRange.end.isAfterOrEqual(pairInfo.boundary[1].end); + if (!isNotInterior) { + return pairInfo.interiorRange; + } + // Step out of this pair and see if we have a parent + const position = target.editor.document.positionAt( + target.editor.document.offsetAt(pairInfo.contentRange.start) - 1 + ); + pairInfo = getSurroundingPair( + context, + target.editor, + new Range(position, position) + ); + } + + // We have not found a pair containing the delimiter. Look at the full line. + return fitRangeToLineContent(target.editor, target.contentRange); +} + +function tokensToItemInfos( + editor: TextEditor, + collectionRange: Range +): ItemInfo[] { + const tokens = tokenizeRange(editor, collectionRange); + const itemInfos: ItemInfo[] = []; + + tokens.forEach((token, i) => { + if (token.type === "delimiter") { + 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 === "delimiter") { + 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 === "delimiter") { + return new Range(token.range.end, tokens[i + 1].range.end); + } + return undefined; + })(); + const delimiterRange = + tokens[i + 1]?.type === "delimiter" ? tokens[i + 1].range : undefined; + itemInfos.push({ + range: token.range, + leadingDelimiterRange, + trailingDelimiterRange, + delimiterRange, + }); + }); + + return itemInfos; +} + +function tokenizeRange(editor: TextEditor, collectionRange: Range) { + const { document } = editor; + const text = document.getText(collectionRange); + const parts = text.split(/([,(){}<>[\]"'])/g).filter(Boolean); + const tokens: Token[] = []; + let offset = document.offsetAt(collectionRange.start); + let waitingForDelimiter: string | null = null; + let offsetStart = 0; + + parts.forEach((text) => { + // Whitespace found. Just skip + if (text.trim().length === 0) { + offset += text.length; + return; + } + + // We are waiting for a closing delimiter + if (waitingForDelimiter != null) { + // Closing delimiter found + if (waitingForDelimiter === text) { + waitingForDelimiter = null; + tokens.push({ + type: "item", + range: new Range( + document.positionAt(offsetStart), + document.positionAt(offset + text.length) + ), + }); + } + } + // Separator delimiter found. + else if (text === delimiter) { + tokens.push({ + type: "delimiter", + range: new Range( + document.positionAt(offset), + document.positionAt(offset + text.length) + ), + }); + } + // Starting delimiter found + else if (delimiters[text] != null) { + waitingForDelimiter = delimiters[text]; + offsetStart = offset; + } + // Text/item content found + else { + const offsetStart = offset + (text.length - text.trimStart().length); + tokens.push({ + type: "item", + range: new Range( + document.positionAt(offsetStart), + document.positionAt(offsetStart + text.trim().length) + ), + }); + } + + offset += text.length; + }); + + return tokens; +} + +function getSurroundingPair( + context: ProcessedTargetsContext, + editor: TextEditor, + contentRange: Range +) { + return processSurroundingPair(context, editor, contentRange, { + type: "surroundingPair", + delimiter: "any", + }); +} + +interface ItemInfo { + range: Range; + leadingDelimiterRange?: Range; + trailingDelimiterRange?: Range; + delimiterRange?: Range; +} + +interface Token { + range: Range; + type: string; +} + +const delimiter = ","; + +// Mapping between opening and closing delimiters +const delimiters: { [key: string]: string } = { + "(": ")", + "{": "}", + "<": ">", + "[": "]", + '"': '"', + "'": "'", +}; diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItem.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItem.yml new file mode 100644 index 0000000000..695938cd59 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItem.yml @@ -0,0 +1,86 @@ +languageId: plaintext +command: + spokenForm: take every item + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: item} + usePrePhraseSnapshot: true + action: {name: setSelection} +initialState: + documentContents: |- + [ + a b, (c, d), + e f, {g, h}, + i j, [k, l], + m n, , + q r, "s, t", + u v, "foo(bar)baz", + ] + selections: + - anchor: {line: 1, character: 10} + active: {line: 1, character: 10} + - anchor: {line: 2, character: 10} + active: {line: 2, character: 10} + - anchor: {line: 3, character: 10} + active: {line: 3, character: 10} + - anchor: {line: 4, character: 10} + active: {line: 4, character: 10} + - anchor: {line: 5, character: 10} + active: {line: 5, character: 10} + marks: {} +finalState: + documentContents: |- + [ + a b, (c, d), + e f, {g, h}, + i j, [k, l], + m n, , + q r, "s, t", + u v, "foo(bar)baz", + ] + selections: + - anchor: {line: 1, character: 10} + active: {line: 1, character: 11} + - anchor: {line: 1, character: 13} + active: {line: 1, character: 14} + - anchor: {line: 2, character: 10} + active: {line: 2, character: 11} + - anchor: {line: 2, character: 13} + active: {line: 2, character: 14} + - anchor: {line: 3, character: 10} + active: {line: 3, character: 11} + - anchor: {line: 3, character: 13} + active: {line: 3, character: 14} + - anchor: {line: 4, character: 10} + active: {line: 4, character: 11} + - anchor: {line: 4, character: 13} + active: {line: 4, character: 14} + - anchor: {line: 5, character: 10} + active: {line: 5, character: 11} + - anchor: {line: 5, character: 13} + active: {line: 5, character: 14} + thatMark: + - anchor: {line: 1, character: 10} + active: {line: 1, character: 11} + - anchor: {line: 1, character: 13} + active: {line: 1, character: 14} + - anchor: {line: 2, character: 10} + active: {line: 2, character: 11} + - anchor: {line: 2, character: 13} + active: {line: 2, character: 14} + - anchor: {line: 3, character: 10} + active: {line: 3, character: 11} + - anchor: {line: 3, character: 13} + active: {line: 3, character: 14} + - anchor: {line: 4, character: 10} + active: {line: 4, character: 11} + - anchor: {line: 4, character: 13} + active: {line: 4, character: 14} + - anchor: {line: 5, character: 10} + active: {line: 5, character: 11} + - anchor: {line: 5, character: 13} + active: {line: 5, character: 14} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: item}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItemAir.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItemAir.yml new file mode 100644 index 0000000000..222f04c2d8 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItemAir.yml @@ -0,0 +1,90 @@ +languageId: plaintext +command: + spokenForm: take every item air + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: a} + modifiers: + - type: everyScope + scopeType: {type: item} + usePrePhraseSnapshot: true + action: {name: setSelection} +initialState: + documentContents: |- + [ + a b, (c, d), + e f, {g, h}, + i j, [k, l], + m n, , + q r, "s, t", + u v, "foo(bar)baz", + ] + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.a: + start: {line: 1, character: 4} + end: {line: 1, character: 5} +finalState: + documentContents: |- + [ + a b, (c, d), + e f, {g, h}, + i j, [k, l], + m n, , + q r, "s, t", + u v, "foo(bar)baz", + ] + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 7} + - anchor: {line: 1, character: 9} + active: {line: 1, character: 15} + - anchor: {line: 2, character: 4} + active: {line: 2, character: 7} + - anchor: {line: 2, character: 9} + active: {line: 2, character: 15} + - anchor: {line: 3, character: 4} + active: {line: 3, character: 7} + - anchor: {line: 3, character: 9} + active: {line: 3, character: 15} + - anchor: {line: 4, character: 4} + active: {line: 4, character: 7} + - anchor: {line: 4, character: 9} + active: {line: 4, character: 15} + - anchor: {line: 5, character: 4} + active: {line: 5, character: 7} + - anchor: {line: 5, character: 9} + active: {line: 5, character: 15} + - anchor: {line: 6, character: 4} + active: {line: 6, character: 7} + - anchor: {line: 6, character: 9} + active: {line: 6, character: 22} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 7} + - anchor: {line: 1, character: 9} + active: {line: 1, character: 15} + - anchor: {line: 2, character: 4} + active: {line: 2, character: 7} + - anchor: {line: 2, character: 9} + active: {line: 2, character: 15} + - anchor: {line: 3, character: 4} + active: {line: 3, character: 7} + - anchor: {line: 3, character: 9} + active: {line: 3, character: 15} + - anchor: {line: 4, character: 4} + active: {line: 4, character: 7} + - anchor: {line: 4, character: 9} + active: {line: 4, character: 15} + - anchor: {line: 5, character: 4} + active: {line: 5, character: 7} + - anchor: {line: 5, character: 9} + active: {line: 5, character: 15} + - anchor: {line: 6, character: 4} + active: {line: 6, character: 7} + - anchor: {line: 6, character: 9} + active: {line: 6, character: 22} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: a}, modifiers: [{type: everyScope, scopeType: {type: item}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeItemAir.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeItemAir.yml new file mode 100644 index 0000000000..2e6f16af0f --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeItemAir.yml @@ -0,0 +1,46 @@ +languageId: plaintext +command: + spokenForm: take item air + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: a} + modifiers: + - type: containingScope + scopeType: {type: item} + usePrePhraseSnapshot: true + action: {name: setSelection} +initialState: + documentContents: |- + [ + a b, (c, d), + e f, {g, h}, + i j, [k, l], + m n, , + q r, "s, t", + u v, "foo(bar)baz", + ] + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.a: + start: {line: 1, character: 4} + end: {line: 1, character: 5} +finalState: + documentContents: |- + [ + a b, (c, d), + e f, {g, h}, + i j, [k, l], + m n, , + q r, "s, t", + u v, "foo(bar)baz", + ] + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 7} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 7} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: a}, modifiers: [{type: containingScope, scopeType: {type: item}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeItemBlueRisk.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeItemBlueRisk.yml new file mode 100644 index 0000000000..bd66b61eb7 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeItemBlueRisk.yml @@ -0,0 +1,46 @@ +languageId: plaintext +command: + spokenForm: take item blue risk + version: 2 + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: blue, character: r} + modifiers: + - type: containingScope + scopeType: {type: item} + usePrePhraseSnapshot: true + action: {name: setSelection} +initialState: + documentContents: |- + [ + a b, (c, d), + e f, {g, h}, + i j, [k, l], + m n, , + q r, "s, t", + u v, "foo(bar)baz", + ] + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + blue.r: + start: {line: 6, character: 14} + end: {line: 6, character: 17} +finalState: + documentContents: |- + [ + a b, (c, d), + e f, {g, h}, + i j, [k, l], + m n, , + q r, "s, t", + u v, "foo(bar)baz", + ] + selections: + - anchor: {line: 6, character: 9} + active: {line: 6, character: 22} + thatMark: + - anchor: {line: 6, character: 9} + active: {line: 6, character: 22} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: blue, character: r}, modifiers: [{type: containingScope, scopeType: {type: item}}]}] From 94048ec879f4e158f2cb679e297db05dd8a34432 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 13 Jun 2022 11:38:52 +0200 Subject: [PATCH 02/44] Cleaned up imports --- src/processTargets/modifiers/scopeTypeStages/ItemStage.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts index a4056849c8..c57b37a3f3 100644 --- a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts @@ -1,10 +1,10 @@ import { Range, TextEditor } from "vscode"; +import { Target } from "../../../typings/target.types"; import { ContainingScopeModifier, EveryScopeModifier, SimpleScopeTypeType, - Target, -} from "../../../typings/target.types"; +} from "../../../typings/targetDescriptor.types"; import { ProcessedTargetsContext } from "../../../typings/Types"; import { ModifierStage } from "../../PipelineStages.types"; import ScopeTypeTarget from "../../targets/ScopeTypeTarget"; @@ -229,6 +229,7 @@ interface Token { const delimiter = ","; // Mapping between opening and closing delimiters +/* eslint-disable @typescript-eslint/naming-convention */ const delimiters: { [key: string]: string } = { "(": ")", "{": "}", @@ -237,3 +238,4 @@ const delimiters: { [key: string]: string } = { '"': '"', "'": "'", }; +/* eslint-enable @typescript-eslint/naming-convention */ From ee74800359fd7861da13f19efcc2ed379172a4df Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 15 Jun 2022 10:17:00 +0200 Subject: [PATCH 03/44] Use text based item as a fallback --- .../modifiers/scopeTypeStages/ItemStage.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts index c57b37a3f3..fae5770875 100644 --- a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts @@ -1,4 +1,5 @@ import { Range, TextEditor } from "vscode"; +import { NoContainingScopeError } from "../../../errors"; import { Target } from "../../../typings/target.types"; import { ContainingScopeModifier, @@ -9,12 +10,25 @@ import { ProcessedTargetsContext } from "../../../typings/Types"; import { ModifierStage } from "../../PipelineStages.types"; import ScopeTypeTarget from "../../targets/ScopeTypeTarget"; import { processSurroundingPair } from "../surroundingPair"; +import ContainingSyntaxScopeStage, { + SimpleContainingScopeModifier, +} from "./ContainingSyntaxScopeStage"; import { fitRangeToLineContent } from "./LineStage"; export default class ItemStage implements ModifierStage { constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} run(context: ProcessedTargetsContext, target: Target): Target[] { + try { + return new ContainingSyntaxScopeStage( + this.modifier + ).run(context, target); + } catch (error) { + if (!(error instanceof NoContainingScopeError)) { + throw error; + } + } + if (this.modifier.type === "everyScope") { return this.getEveryTarget(context, target); } @@ -55,7 +69,7 @@ export default class ItemStage implements ModifierStage { editor: target.editor, isReversed: target.isReversed, contentRange: itemInfo.range, - delimiter, + delimiter: delimiterInsertion, leadingDelimiterRange: itemInfo.leadingDelimiterRange, trailingDelimiterRange: itemInfo.trailingDelimiterRange, }); @@ -227,6 +241,7 @@ interface Token { } const delimiter = ","; +const delimiterInsertion = ", "; // Mapping between opening and closing delimiters /* eslint-disable @typescript-eslint/naming-convention */ From 3f0a79ac1b069e7f0e7501a2166a177ecd9e9bc0 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 15 Jun 2022 10:36:48 +0200 Subject: [PATCH 04/44] Updated tests --- .../modifiers/scopeTypeStages/ItemStage.ts | 14 ++++++++++---- .../recorded/selectionTypes/takeEveryItem.yml | 4 ++-- .../recorded/selectionTypes/takeEveryItemAir.yml | 4 ++-- .../recorded/selectionTypes/takeItemAir.yml | 4 ++-- .../recorded/selectionTypes/takeItemBlueRisk.yml | 4 ++-- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts index fae5770875..cc04cb513d 100644 --- a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts @@ -1,5 +1,8 @@ import { Range, TextEditor } from "vscode"; -import { NoContainingScopeError } from "../../../errors"; +import { + NoContainingScopeError, + UnsupportedLanguageError, +} from "../../../errors"; import { Target } from "../../../typings/target.types"; import { ContainingScopeModifier, @@ -24,7 +27,10 @@ export default class ItemStage implements ModifierStage { this.modifier ).run(context, target); } catch (error) { - if (!(error instanceof NoContainingScopeError)) { + if ( + !(error instanceof NoContainingScopeError) && + !(error instanceof UnsupportedLanguageError) + ) { throw error; } } @@ -39,7 +45,7 @@ export default class ItemStage implements ModifierStage { const itemInfos = getItemInfos(context, target); if (itemInfos.length < 1) { - throw Error(`Couldn't find containing ${this.modifier.scopeType.type}`); + throw new NoContainingScopeError(this.modifier.scopeType.type); } return itemInfos.map((itemInfo) => this.itemInfoToTarget(target, itemInfo)); @@ -57,7 +63,7 @@ export default class ItemStage implements ModifierStage { ); if (itemInfo == null) { - throw Error(`Couldn't find containing ${this.modifier.scopeType.type}`); + throw new NoContainingScopeError(this.modifier.scopeType.type); } return this.itemInfoToTarget(target, itemInfo); diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItem.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItem.yml index 695938cd59..381a0a0211 100644 --- a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItem.yml +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItem.yml @@ -6,7 +6,7 @@ command: - type: primitive modifiers: - type: everyScope - scopeType: {type: item} + scopeType: {type: collectionItem} usePrePhraseSnapshot: true action: {name: setSelection} initialState: @@ -83,4 +83,4 @@ finalState: active: {line: 5, character: 11} - anchor: {line: 5, character: 13} active: {line: 5, character: 14} -fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: item}}]}] +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItemAir.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItemAir.yml index 222f04c2d8..c55cb78c70 100644 --- a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItemAir.yml +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItemAir.yml @@ -7,7 +7,7 @@ command: mark: {type: decoratedSymbol, symbolColor: default, character: a} modifiers: - type: everyScope - scopeType: {type: item} + scopeType: {type: collectionItem} usePrePhraseSnapshot: true action: {name: setSelection} initialState: @@ -87,4 +87,4 @@ finalState: active: {line: 6, character: 7} - anchor: {line: 6, character: 9} active: {line: 6, character: 22} -fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: a}, modifiers: [{type: everyScope, scopeType: {type: item}}]}] +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: a}, modifiers: [{type: everyScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeItemAir.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeItemAir.yml index 2e6f16af0f..0af92e3b73 100644 --- a/src/test/suite/fixtures/recorded/selectionTypes/takeItemAir.yml +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeItemAir.yml @@ -7,7 +7,7 @@ command: mark: {type: decoratedSymbol, symbolColor: default, character: a} modifiers: - type: containingScope - scopeType: {type: item} + scopeType: {type: collectionItem} usePrePhraseSnapshot: true action: {name: setSelection} initialState: @@ -43,4 +43,4 @@ finalState: thatMark: - anchor: {line: 1, character: 4} active: {line: 1, character: 7} -fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: a}, modifiers: [{type: containingScope, scopeType: {type: item}}]}] +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: a}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeItemBlueRisk.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeItemBlueRisk.yml index bd66b61eb7..cad2bd0982 100644 --- a/src/test/suite/fixtures/recorded/selectionTypes/takeItemBlueRisk.yml +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeItemBlueRisk.yml @@ -7,7 +7,7 @@ command: mark: {type: decoratedSymbol, symbolColor: blue, character: r} modifiers: - type: containingScope - scopeType: {type: item} + scopeType: {type: collectionItem} usePrePhraseSnapshot: true action: {name: setSelection} initialState: @@ -43,4 +43,4 @@ finalState: thatMark: - anchor: {line: 6, character: 9} active: {line: 6, character: 22} -fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: blue, character: r}, modifiers: [{type: containingScope, scopeType: {type: item}}]}] +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: blue, character: r}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] From 0f0241dc51190cafb2c67bb234cc763d1822d865 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 18 Jun 2022 13:57:33 +0200 Subject: [PATCH 05/44] Don't look for items inside strings --- .../modifiers/scopeTypeStages/ItemStage.ts | 43 ++++++++++++++----- .../recorded/selectionTypes/takeEveryItem.yml | 14 ------ 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts index cc04cb513d..63883bf532 100644 --- a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts @@ -13,6 +13,7 @@ import { ProcessedTargetsContext } from "../../../typings/Types"; import { ModifierStage } from "../../PipelineStages.types"; import ScopeTypeTarget from "../../targets/ScopeTypeTarget"; import { processSurroundingPair } from "../surroundingPair"; +import { SurroundingPairInfo } from "../surroundingPair/extractSelectionFromSurroundingPairOffsets"; import ContainingSyntaxScopeStage, { SimpleContainingScopeModifier, } from "./ContainingSyntaxScopeStage"; @@ -88,12 +89,19 @@ function getItemInfos(context: ProcessedTargetsContext, target: Target) { } function getCollectionRange(context: ProcessedTargetsContext, target: Target) { - let pairInfo = getSurroundingPair( + // First check if we are in a string + let pairInfo = getStringSurroundingPair( context, target.editor, target.contentRange ); + // We don't look for items inside strings. If we are in a string go to parent + pairInfo = + pairInfo != null + ? getParentSurroundingPair(context, target.editor, pairInfo) + : getSurroundingPair(context, target.editor, target.contentRange); + while (pairInfo != null) { // The selection from the beginning was this pair and we should not go into the interior but instead look in the parent. const isNotInterior = @@ -103,15 +111,7 @@ function getCollectionRange(context: ProcessedTargetsContext, target: Target) { if (!isNotInterior) { return pairInfo.interiorRange; } - // Step out of this pair and see if we have a parent - const position = target.editor.document.positionAt( - target.editor.document.offsetAt(pairInfo.contentRange.start) - 1 - ); - pairInfo = getSurroundingPair( - context, - target.editor, - new Range(position, position) - ); + pairInfo = getParentSurroundingPair(context, target.editor, pairInfo); } // We have not found a pair containing the delimiter. Look at the full line. @@ -234,6 +234,29 @@ function getSurroundingPair( }); } +function getParentSurroundingPair( + context: ProcessedTargetsContext, + editor: TextEditor, + pairInfo: SurroundingPairInfo +) { + // Step out of this pair and see if we have a parent + const position = editor.document.positionAt( + editor.document.offsetAt(pairInfo.contentRange.start) - 1 + ); + return getSurroundingPair(context, editor, new Range(position, position)); +} + +function getStringSurroundingPair( + context: ProcessedTargetsContext, + editor: TextEditor, + contentRange: Range +) { + return processSurroundingPair(context, editor, contentRange, { + type: "surroundingPair", + delimiter: "string", + }); +} + interface ItemInfo { range: Range; leadingDelimiterRange?: Range; diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItem.yml b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItem.yml index 381a0a0211..43559479e1 100644 --- a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItem.yml +++ b/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItem.yml @@ -16,8 +16,6 @@ initialState: e f, {g, h}, i j, [k, l], m n, , - q r, "s, t", - u v, "foo(bar)baz", ] selections: - anchor: {line: 1, character: 10} @@ -28,8 +26,6 @@ initialState: active: {line: 3, character: 10} - anchor: {line: 4, character: 10} active: {line: 4, character: 10} - - anchor: {line: 5, character: 10} - active: {line: 5, character: 10} marks: {} finalState: documentContents: |- @@ -38,8 +34,6 @@ finalState: e f, {g, h}, i j, [k, l], m n, , - q r, "s, t", - u v, "foo(bar)baz", ] selections: - anchor: {line: 1, character: 10} @@ -58,10 +52,6 @@ finalState: active: {line: 4, character: 11} - anchor: {line: 4, character: 13} active: {line: 4, character: 14} - - anchor: {line: 5, character: 10} - active: {line: 5, character: 11} - - anchor: {line: 5, character: 13} - active: {line: 5, character: 14} thatMark: - anchor: {line: 1, character: 10} active: {line: 1, character: 11} @@ -79,8 +69,4 @@ finalState: active: {line: 4, character: 11} - anchor: {line: 4, character: 13} active: {line: 4, character: 14} - - anchor: {line: 5, character: 10} - active: {line: 5, character: 11} - - anchor: {line: 5, character: 13} - active: {line: 5, character: 14} fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: collectionItem}}]}] From 05c253b4474d9f441ae41c0def07bb92e3b1cb33 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 28 Jun 2022 22:26:36 +0200 Subject: [PATCH 06/44] More work on item stage --- .../modifiers/scopeTypeStages/ItemStage.ts | 93 ++++++++++++------- src/util/rangeUtils.ts | 6 ++ 2 files changed, 66 insertions(+), 33 deletions(-) diff --git a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts index 63883bf532..b44d3cadae 100644 --- a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts @@ -1,8 +1,5 @@ import { Range, TextEditor } from "vscode"; -import { - NoContainingScopeError, - UnsupportedLanguageError, -} from "../../../errors"; +import { NoContainingScopeError } from "../../../errors"; import { Target } from "../../../typings/target.types"; import { ContainingScopeModifier, @@ -10,6 +7,7 @@ import { SimpleScopeTypeType, } from "../../../typings/targetDescriptor.types"; import { ProcessedTargetsContext } from "../../../typings/Types"; +import { rangeLength } from "../../../util/rangeUtils"; import { ModifierStage } from "../../PipelineStages.types"; import ScopeTypeTarget from "../../targets/ScopeTypeTarget"; import { processSurroundingPair } from "../surroundingPair"; @@ -27,14 +25,7 @@ export default class ItemStage implements ModifierStage { return new ContainingSyntaxScopeStage( this.modifier ).run(context, target); - } catch (error) { - if ( - !(error instanceof NoContainingScopeError) && - !(error instanceof UnsupportedLanguageError) - ) { - throw error; - } - } + } catch (_error) {} if (this.modifier.type === "everyScope") { return this.getEveryTarget(context, target); @@ -55,19 +46,24 @@ export default class ItemStage implements ModifierStage { private getSingleTarget(context: ProcessedTargetsContext, target: Target) { const itemInfos = getItemInfos(context, target); - const itemInfo = - itemInfos.find((itemInfo) => - itemInfo.range.intersection(target.contentRange) - ) ?? - itemInfos.find((itemInfo) => - itemInfo.delimiterRange?.intersection(target.contentRange) - ); + const itemInfoWithIntersections = itemInfos + .map((itemInfo) => ({ + itemInfo, + intersection: itemInfo.matchRange.intersection(target.contentRange), + })) + .filter((e) => e.intersection != null); - if (itemInfo == null) { + if (itemInfoWithIntersections.length === 0) { throw new NoContainingScopeError(this.modifier.scopeType.type); } - return this.itemInfoToTarget(target, itemInfo); + itemInfoWithIntersections.sort( + (a, b) => + rangeLength(target.editor, b.intersection!) - + rangeLength(target.editor, a.intersection!) + ); + + return this.itemInfoToTarget(target, itemInfoWithIntersections[0].itemInfo); } private itemInfoToTarget(target: Target, itemInfo: ItemInfo) { @@ -84,8 +80,8 @@ export default class ItemStage implements ModifierStage { } function getItemInfos(context: ProcessedTargetsContext, target: Target) { - const collectionRange = getCollectionRange(context, target); - return tokensToItemInfos(target.editor, collectionRange); + const { range, boundary } = getCollectionRange(context, target); + return tokensToItemInfos(target.editor, range, boundary); } function getCollectionRange(context: ProcessedTargetsContext, target: Target) { @@ -109,24 +105,30 @@ function getCollectionRange(context: ProcessedTargetsContext, target: Target) { target.contentRange.start.isBeforeOrEqual(pairInfo.boundary[0].start) || target.contentRange.end.isAfterOrEqual(pairInfo.boundary[1].end); if (!isNotInterior) { - return pairInfo.interiorRange; + return { + range: pairInfo.interiorRange, + boundary: pairInfo.boundary, + }; } pairInfo = getParentSurroundingPair(context, target.editor, pairInfo); } // We have not found a pair containing the delimiter. Look at the full line. - return fitRangeToLineContent(target.editor, target.contentRange); + return { + range: fitRangeToLineContent(target.editor, target.contentRange), + }; } function tokensToItemInfos( editor: TextEditor, - collectionRange: Range + collectionRange: Range, + collectionBoundary?: [Range, Range] ): ItemInfo[] { - const tokens = tokenizeRange(editor, collectionRange); + const tokens = tokenizeRange(editor, collectionRange, collectionBoundary); const itemInfos: ItemInfo[] = []; tokens.forEach((token, i) => { - if (token.type === "delimiter") { + if (token.type === "delimiter" || token.type === "boundary") { return; } const leadingDelimiterRange = (() => { @@ -147,20 +149,33 @@ function tokensToItemInfos( } return undefined; })(); - const delimiterRange = - tokens[i + 1]?.type === "delimiter" ? tokens[i + 1].range : undefined; + const leadingMatchStart = + tokens[i - 1]?.type === "delimiter" || tokens[i - 1]?.type === "boundary" + ? tokens[i - 1].range.start + : token.range.start; + const trailingMatchEnd = + tokens[i + 1]?.type === "boundary" + ? tokens[i + 1].range.end + : tokens[i + 1]?.type === "delimiter" + ? tokens[i + 1].range.start + : token.range.start; + const matchRange = new Range(leadingMatchStart, trailingMatchEnd); itemInfos.push({ range: token.range, leadingDelimiterRange, trailingDelimiterRange, - delimiterRange, + matchRange, }); }); return itemInfos; } -function tokenizeRange(editor: TextEditor, collectionRange: Range) { +function tokenizeRange( + editor: TextEditor, + collectionRange: Range, + collectionBoundary?: [Range, Range] +) { const { document } = editor; const text = document.getText(collectionRange); const parts = text.split(/([,(){}<>[\]"'])/g).filter(Boolean); @@ -220,6 +235,18 @@ function tokenizeRange(editor: TextEditor, collectionRange: Range) { offset += text.length; }); + if (tokens.length > 1 && !tokens.find((t) => t.type === "delimiter")) { + return []; + } + + if (collectionBoundary != null) { + return [ + { type: "boundary", range: collectionBoundary[0] }, + ...tokens, + { type: "boundary", range: collectionBoundary[1] }, + ]; + } + return tokens; } @@ -261,7 +288,7 @@ interface ItemInfo { range: Range; leadingDelimiterRange?: Range; trailingDelimiterRange?: Range; - delimiterRange?: Range; + matchRange: Range; } interface Token { diff --git a/src/util/rangeUtils.ts b/src/util/rangeUtils.ts index a812180221..a2629ff1c2 100644 --- a/src/util/rangeUtils.ts +++ b/src/util/rangeUtils.ts @@ -28,3 +28,9 @@ export function expandToFullLine(editor: TextEditor, range: Range) { export function makeEmptyRange(position: Position) { return new Range(position, position); } + +export function rangeLength(editor: TextEditor, range: Range) { + return ( + editor.document.offsetAt(range.end) - editor.document.offsetAt(range.start) + ); +} From 4c51ff51d4dccf1f4d9c44a3bf19760e51d9dcfe Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 29 Jun 2022 10:55:08 +0200 Subject: [PATCH 07/44] updated test --- .../languages/typescript/takeItemBrace.yml | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/test/suite/fixtures/recorded/languages/typescript/takeItemBrace.yml b/src/test/suite/fixtures/recorded/languages/typescript/takeItemBrace.yml index a562da0bfb..ac1d34e3f3 100644 --- a/src/test/suite/fixtures/recorded/languages/typescript/takeItemBrace.yml +++ b/src/test/suite/fixtures/recorded/languages/typescript/takeItemBrace.yml @@ -1,31 +1,40 @@ languageId: typescript command: - version: 1 spokenForm: take item brace - action: setSelection + version: 2 targets: - type: primitive - modifier: {type: containingScope, scopeType: collectionItem} mark: {type: decoratedSymbol, symbolColor: default, character: '{'} + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: setSelection} initialState: - documentContents: | - - const value = { a: 1, b: 2, c: 3 }; + documentContents: |- + [ + { + foo: "bar", + }, + ] selections: - anchor: {line: 0, character: 0} active: {line: 0, character: 0} marks: default.{: - start: {line: 1, character: 14} - end: {line: 1, character: 15} + start: {line: 1, character: 4} + end: {line: 1, character: 5} finalState: - documentContents: | - - const value = { a: 1, b: 2, c: 3 }; + documentContents: |- + [ + { + foo: "bar", + }, + ] selections: - - anchor: {line: 1, character: 16} - active: {line: 1, character: 20} + - anchor: {line: 1, character: 4} + active: {line: 3, character: 5} thatMark: - - anchor: {line: 1, character: 16} - active: {line: 1, character: 20} -fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: '{'}, selectionType: token, position: contents, modifier: {type: containingScope, scopeType: collectionItem}, insideOutsideType: inside}] + - anchor: {line: 1, character: 4} + active: {line: 3, character: 5} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: '{'}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] From 3f9afa5c806b963222dc41d3d980e10fd2f9f7d7 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 29 Jun 2022 11:02:39 +0200 Subject: [PATCH 08/44] Replaced take test with clear test --- .../{takeItemBrace.yml => clearItemBrace.yml} | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) rename src/test/suite/fixtures/recorded/languages/typescript/{takeItemBrace.yml => clearItemBrace.yml} (64%) diff --git a/src/test/suite/fixtures/recorded/languages/typescript/takeItemBrace.yml b/src/test/suite/fixtures/recorded/languages/typescript/clearItemBrace.yml similarity index 64% rename from src/test/suite/fixtures/recorded/languages/typescript/takeItemBrace.yml rename to src/test/suite/fixtures/recorded/languages/typescript/clearItemBrace.yml index ac1d34e3f3..a13d0fc93c 100644 --- a/src/test/suite/fixtures/recorded/languages/typescript/takeItemBrace.yml +++ b/src/test/suite/fixtures/recorded/languages/typescript/clearItemBrace.yml @@ -1,6 +1,6 @@ languageId: typescript command: - spokenForm: take item brace + spokenForm: clear item brace version: 2 targets: - type: primitive @@ -9,32 +9,30 @@ command: - type: containingScope scopeType: {type: collectionItem} usePrePhraseSnapshot: true - action: {name: setSelection} + action: {name: clearAndSetSelection} initialState: documentContents: |- [ - { - foo: "bar", - }, + { + foo: "bar", + }, ] selections: - anchor: {line: 0, character: 0} active: {line: 0, character: 0} marks: default.{: - start: {line: 1, character: 4} - end: {line: 1, character: 5} + start: {line: 1, character: 2} + end: {line: 1, character: 3} finalState: documentContents: |- [ - { - foo: "bar", - }, + , ] selections: - - anchor: {line: 1, character: 4} - active: {line: 3, character: 5} + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} thatMark: - - anchor: {line: 1, character: 4} - active: {line: 3, character: 5} + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: '{'}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] From 3499f53d095c385c263a0c576ab0e5d280a9c984 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 29 Jun 2022 13:02:48 +0200 Subject: [PATCH 09/44] updated item stage --- .../modifiers/scopeTypeStages/ItemStage.ts | 104 ++++++++++++------ 1 file changed, 68 insertions(+), 36 deletions(-) diff --git a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts index b44d3cadae..5c14de32b6 100644 --- a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts @@ -36,7 +36,7 @@ export default class ItemStage implements ModifierStage { private getEveryTarget(context: ProcessedTargetsContext, target: Target) { const itemInfos = getItemInfos(context, target); - if (itemInfos.length < 1) { + if (itemInfos.length === 0) { throw new NoContainingScopeError(this.modifier.scopeType.type); } @@ -81,7 +81,7 @@ export default class ItemStage implements ModifierStage { function getItemInfos(context: ProcessedTargetsContext, target: Target) { const { range, boundary } = getCollectionRange(context, target); - return tokensToItemInfos(target.editor, range, boundary); + return rangeToItemInfos(target.editor, range, boundary); } function getCollectionRange(context: ProcessedTargetsContext, target: Target) { @@ -119,7 +119,7 @@ function getCollectionRange(context: ProcessedTargetsContext, target: Target) { }; } -function tokensToItemInfos( +function rangeToItemInfos( editor: TextEditor, collectionRange: Range, collectionBoundary?: [Range, Range] @@ -178,66 +178,46 @@ function tokenizeRange( ) { const { document } = editor; const text = document.getText(collectionRange); - const parts = text.split(/([,(){}<>[\]"'])/g).filter(Boolean); + const lexemes = text.split(/([,(){}<>[\]"'])/g).filter(Boolean); + const joinedLexemes = joinLexemesBySkippingMatchingPairs(lexemes); const tokens: Token[] = []; let offset = document.offsetAt(collectionRange.start); - let waitingForDelimiter: string | null = null; - let offsetStart = 0; - parts.forEach((text) => { + joinedLexemes.forEach((lexeme) => { // Whitespace found. Just skip - if (text.trim().length === 0) { - offset += text.length; + if (lexeme.trim().length === 0) { + offset += lexeme.length; return; } - // We are waiting for a closing delimiter - if (waitingForDelimiter != null) { - // Closing delimiter found - if (waitingForDelimiter === text) { - waitingForDelimiter = null; - tokens.push({ - type: "item", - range: new Range( - document.positionAt(offsetStart), - document.positionAt(offset + text.length) - ), - }); - } - } // Separator delimiter found. - else if (text === delimiter) { + if (lexeme === delimiter) { tokens.push({ type: "delimiter", range: new Range( document.positionAt(offset), - document.positionAt(offset + text.length) + document.positionAt(offset + lexeme.length) ), }); } - // Starting delimiter found - else if (delimiters[text] != null) { - waitingForDelimiter = delimiters[text]; - offsetStart = offset; - } // Text/item content found else { - const offsetStart = offset + (text.length - text.trimStart().length); + const offsetStart = offset + (lexeme.length - lexeme.trimStart().length); tokens.push({ type: "item", range: new Range( document.positionAt(offsetStart), - document.positionAt(offsetStart + text.trim().length) + document.positionAt(offsetStart + lexeme.trim().length) ), }); } - offset += text.length; + offset += lexeme.length; }); - if (tokens.length > 1 && !tokens.find((t) => t.type === "delimiter")) { - return []; - } + // if (tokens.length > 1 && !tokens.find((t) => t.type === "delimiter")) { + // return []; + // } if (collectionBoundary != null) { return [ @@ -250,6 +230,58 @@ function tokenizeRange( return tokens; } +function joinLexemesBySkippingMatchingPairs(lexemes: string[]) { + const result: string[] = []; + let delimiterCount = 0; + let openingDelimiter: string | null = null; + let closingDelimiter: string | null = null; + let startIndex: number = -1; + + lexemes.forEach((lexeme, index) => { + // We are waiting for a closing delimiter + if (delimiterCount > 0) { + // Closing delimiter found + if (closingDelimiter === lexeme) { + --delimiterCount; + } + // Additional opening delimiter found + else if (openingDelimiter === lexeme) { + ++delimiterCount; + } + } + + // Starting delimiter found + else if (delimiters[lexeme] != null) { + openingDelimiter = lexeme; + closingDelimiter = delimiters[lexeme]; + delimiterCount = 1; + // This is the first lexeme to be joined + if (startIndex < 0) { + startIndex = index; + } + } + + // This is the first lexeme to be joined + else if (startIndex < 0) { + startIndex = index; + } + + const isDelimiter = lexeme === delimiter && delimiterCount === 0; + + // This is the last lexeme to be joined + if (isDelimiter || index === lexemes.length - 1) { + const endIndex = isDelimiter ? index : index + 1; + result.push(lexemes.slice(startIndex, endIndex).join("")); + startIndex = -1; + if (isDelimiter) { + result.push(lexeme); + } + } + }); + + return result; +} + function getSurroundingPair( context: ProcessedTargetsContext, editor: TextEditor, From 2c13872eee1693c677376f3acaa0eb3f8425b19f Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 29 Jun 2022 16:20:42 +0200 Subject: [PATCH 10/44] Typescript now uses text based item --- src/languages/typescript.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/languages/typescript.ts b/src/languages/typescript.ts index c0de5e2125..feac541e83 100644 --- a/src/languages/typescript.ts +++ b/src/languages/typescript.ts @@ -173,7 +173,6 @@ const nodeMatchers: Partial< ], [":"] ), - collectionItem: argumentMatcher(...mapTypes, ...listTypes), value: cascadingMatcher( valueMatcher(), patternMatcher("return_statement.~return!"), From 974aeea7e0b9b53c2ff4032086b23657228f406e Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 29 Jun 2022 17:17:59 +0200 Subject: [PATCH 11/44] Use text based item for more languages --- src/languages/cpp.ts | 1 - src/languages/go.ts | 1 - src/languages/java.ts | 1 - src/languages/json.ts | 1 - src/languages/php.ts | 1 - src/languages/python.ts | 1 - .../modifiers/scopeTypeStages/ItemStage.ts | 20 ++++++++++++------- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/languages/cpp.ts b/src/languages/cpp.ts index b8b4879e96..c4689de387 100644 --- a/src/languages/cpp.ts +++ b/src/languages/cpp.ts @@ -113,7 +113,6 @@ const nodeMatchers: Partial< ], [":", "=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>="] ), - collectionItem: argumentMatcher("initializer_list"), argumentOrParameter: argumentMatcher("parameter_list", "argument_list"), attribute: "attribute", }; diff --git a/src/languages/go.ts b/src/languages/go.ts index 477c652133..e26aae2320 100644 --- a/src/languages/go.ts +++ b/src/languages/go.ts @@ -64,7 +64,6 @@ const nodeMatchers: Partial< patternMatcher("parameter_declaration"), patternMatcher("argument_declaration") ), - collectionItem: ["keyed_element", "element"], collectionKey: "keyed_element[0]", value: cascadingMatcher( patternMatcher("keyed_element[1]"), diff --git a/src/languages/java.ts b/src/languages/java.ts index 4df9d82a3b..9abe0bf303 100644 --- a/src/languages/java.ts +++ b/src/languages/java.ts @@ -110,7 +110,6 @@ const nodeMatchers: Partial< ["=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>="] ), condition: conditionMatcher("*[condition]"), - collectionItem: argumentMatcher("array_initializer"), argumentOrParameter: argumentMatcher("formal_parameters", "argument_list"), }; diff --git a/src/languages/json.ts b/src/languages/json.ts index 23c69c0718..e86e1ce7b0 100644 --- a/src/languages/json.ts +++ b/src/languages/json.ts @@ -17,7 +17,6 @@ const nodeMatchers: Partial< string: "string", collectionKey: trailingMatcher(["pair[key]"], [":"]), value: leadingMatcher(["*[value]"], [":"]), - collectionItem: argumentMatcher("object", "array"), }; export const patternMatchers = createPatternMatchers(nodeMatchers); diff --git a/src/languages/php.ts b/src/languages/php.ts index f9e5b629fc..4c6e633521 100644 --- a/src/languages/php.ts +++ b/src/languages/php.ts @@ -145,7 +145,6 @@ const nodeMatchers: Partial< ), collectionKey: trailingMatcher(["array_element_initializer[0]"], ["=>"]), - collectionItem: argumentMatcher("array_creation_expression"), argumentOrParameter: argumentMatcher("arguments", "formal_parameters"), }; diff --git a/src/languages/python.ts b/src/languages/python.ts index 264f40d99d..d6f31d88f3 100644 --- a/src/languages/python.ts +++ b/src/languages/python.ts @@ -78,7 +78,6 @@ const nodeMatchers: Partial< "parameters.identifier!", "*[name]", ], - collectionItem: argumentMatcher(...dictionaryTypes, ...listTypes), value: cascadingMatcher( leadingMatcher( ["assignment[right]", "augmented_assignment[right]", "~subscript[value]"], diff --git a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts index 5c14de32b6..5ad38d5069 100644 --- a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts @@ -67,12 +67,22 @@ export default class ItemStage implements ModifierStage { } private itemInfoToTarget(target: Target, itemInfo: ItemInfo) { + const editor = target.editor; + const delimiters = [ + itemInfo.trailingDelimiterRange != null + ? editor.document.getText(itemInfo.trailingDelimiterRange) + : defaultDelimiterInsertion, + itemInfo.leadingDelimiterRange != null + ? editor.document.getText(itemInfo.leadingDelimiterRange) + : defaultDelimiterInsertion, + ]; + delimiters.sort((a, b) => b.length - a.length); return new ScopeTypeTarget({ scopeTypeType: this.modifier.scopeType.type, - editor: target.editor, + editor, isReversed: target.isReversed, contentRange: itemInfo.range, - delimiter: delimiterInsertion, + delimiter: delimiters[0], leadingDelimiterRange: itemInfo.leadingDelimiterRange, trailingDelimiterRange: itemInfo.trailingDelimiterRange, }); @@ -215,10 +225,6 @@ function tokenizeRange( offset += lexeme.length; }); - // if (tokens.length > 1 && !tokens.find((t) => t.type === "delimiter")) { - // return []; - // } - if (collectionBoundary != null) { return [ { type: "boundary", range: collectionBoundary[0] }, @@ -329,7 +335,7 @@ interface Token { } const delimiter = ","; -const delimiterInsertion = ", "; +const defaultDelimiterInsertion = ", "; // Mapping between opening and closing delimiters /* eslint-disable @typescript-eslint/naming-convention */ From 1e3941cecedc908510a0f52a7cda99ce199d7e9e Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 29 Jun 2022 17:31:25 +0200 Subject: [PATCH 12/44] Use text based item for more languages --- src/languages/csharp.ts | 17 ----------- .../modifiers/scopeTypeStages/ItemStage.ts | 29 +++++++++++-------- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/languages/csharp.ts b/src/languages/csharp.ts index 42bcbb08e9..ee5115ace2 100644 --- a/src/languages/csharp.ts +++ b/src/languages/csharp.ts @@ -197,23 +197,6 @@ const getMapMatchers = { (node: SyntaxNode) => node.childForFieldName("initializer"), ]) ), - collectionItem: matcher( - nodeFinder( - (node) => - (node.parent?.type === "initializer_expression" && - isExpression(node)) || - node.type === "assignment_expression" - ), - delimitedSelector( - (node) => - node.type === "," || - node.type === "[" || - node.type === "]" || - node.type === "}" || - node.type === "{", - ", " - ) - ), string: typeMatcher("string_literal"), }; diff --git a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts index 5ad38d5069..6616a81c0d 100644 --- a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts @@ -67,28 +67,33 @@ export default class ItemStage implements ModifierStage { } private itemInfoToTarget(target: Target, itemInfo: ItemInfo) { - const editor = target.editor; - const delimiters = [ - itemInfo.trailingDelimiterRange != null - ? editor.document.getText(itemInfo.trailingDelimiterRange) - : defaultDelimiterInsertion, - itemInfo.leadingDelimiterRange != null - ? editor.document.getText(itemInfo.leadingDelimiterRange) - : defaultDelimiterInsertion, - ]; - delimiters.sort((a, b) => b.length - a.length); return new ScopeTypeTarget({ scopeTypeType: this.modifier.scopeType.type, - editor, + editor: target.editor, isReversed: target.isReversed, contentRange: itemInfo.range, - delimiter: delimiters[0], + delimiter: getInsertionDelimiter(target, itemInfo), leadingDelimiterRange: itemInfo.leadingDelimiterRange, trailingDelimiterRange: itemInfo.trailingDelimiterRange, }); } } +function getInsertionDelimiter(target: Target, itemInfo: ItemInfo) { + const { getText } = target.editor.document; + const delimiters = [ + itemInfo.trailingDelimiterRange != null + ? getText(itemInfo.trailingDelimiterRange) + : defaultDelimiterInsertion, + itemInfo.leadingDelimiterRange != null + ? getText(itemInfo.leadingDelimiterRange) + : defaultDelimiterInsertion, + ]; + // Use longest delimiter text for insertion + delimiters.sort((a, b) => b.length - a.length); + return delimiters[0]; +} + function getItemInfos(context: ProcessedTargetsContext, target: Target) { const { range, boundary } = getCollectionRange(context, target); return rangeToItemInfos(target.editor, range, boundary); From 768eef28db563ddc13d672614e1eecc3fed1a09a Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 29 Jun 2022 17:52:04 +0200 Subject: [PATCH 13/44] Cleanup --- src/languages/csharp.ts | 63 ----------------------------------------- src/languages/json.ts | 7 ++--- 2 files changed, 3 insertions(+), 67 deletions(-) diff --git a/src/languages/csharp.ts b/src/languages/csharp.ts index ee5115ace2..8817ac2eb3 100644 --- a/src/languages/csharp.ts +++ b/src/languages/csharp.ts @@ -16,69 +16,6 @@ import { nodeFinder, typedNodeFinder } from "../util/nodeFinders"; import { delimitedSelector, childRangeSelector } from "../util/nodeSelectors"; import { patternFinder } from "../util/nodeFinders"; -// Generated by the following command: -// > curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-c-sharp/master/src/node-types.json \ -// | jq '.[] | select(.type == "_expression") | [.subtypes[].type]' -const EXPRESSION_TYPES = [ - "anonymous_method_expression", - "anonymous_object_creation_expression", - "array_creation_expression", - "as_expression", - "assignment_expression", - "await_expression", - "base_expression", - "binary_expression", - "boolean_literal", - "cast_expression", - "character_literal", - "checked_expression", - "conditional_access_expression", - "conditional_expression", - "default_expression", - "element_access_expression", - "element_binding_expression", - "generic_name", - "global", - "identifier", - "implicit_array_creation_expression", - "implicit_object_creation_expression", - "implicit_stack_alloc_array_creation_expression", - "initializer_expression", - "integer_literal", - "interpolated_string_expression", - "invocation_expression", - "is_expression", - "is_pattern_expression", - "lambda_expression", - "make_ref_expression", - "member_access_expression", - "null_literal", - "object_creation_expression", - "parenthesized_expression", - "postfix_unary_expression", - "prefix_unary_expression", - "query_expression", - "range_expression", - "real_literal", - "ref_expression", - "ref_type_expression", - "ref_value_expression", - "size_of_expression", - "stack_alloc_array_creation_expression", - "string_literal", - "switch_expression", - "this_expression", - "throw_expression", - "tuple_expression", - "type_of_expression", - "verbatim_string_literal", - "with_expression", -]; - -function isExpression(node: SyntaxNode) { - return EXPRESSION_TYPES.includes(node.type); -} - // Generated by the following command: // > curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-c-sharp/master/src/node-types.json \ // | jq '.[] | select(.type == "_statement" or .type == "_declaration") | [.subtypes[].type]' diff --git a/src/languages/json.ts b/src/languages/json.ts index e86e1ce7b0..b32645a88a 100644 --- a/src/languages/json.ts +++ b/src/languages/json.ts @@ -1,12 +1,11 @@ +import { SyntaxNode } from "web-tree-sitter"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; +import { NodeMatcherAlternative, SelectionWithEditor } from "../typings/Types"; import { createPatternMatchers, - argumentMatcher, leadingMatcher, trailingMatcher, } from "../util/nodeMatchers"; -import { NodeMatcherAlternative, SelectionWithEditor } from "../typings/Types"; -import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; -import { SyntaxNode } from "web-tree-sitter"; import { getNodeRange } from "../util/nodeSelectors"; const nodeMatchers: Partial< From 9ab438807973a09c5a37e018075a92c37c5be456 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 29 Jun 2022 18:33:50 +0200 Subject: [PATCH 14/44] Added more tests --- .../modifiers/scopeTypeStages/ItemStage.ts | 12 ++--- .../recorded/itemTextual/chuckItem.yml | 32 ++++++++++++++ .../recorded/itemTextual/chuckItem2.yml | 32 ++++++++++++++ .../recorded/itemTextual/clearEveryItem.yml | 44 +++++++++++++++++++ .../recorded/itemTextual/clearEveryItem2.yml | 34 ++++++++++++++ .../recorded/itemTextual/clearItem.yml | 36 +++++++++++++++ .../recorded/itemTextual/clearItem2.yml | 36 +++++++++++++++ .../recorded/itemTextual/clearItem3.yml | 36 +++++++++++++++ .../recorded/itemTextual/clearItem4.yml | 26 +++++++++++ .../recorded/itemTextual/clearItem5.yml | 26 +++++++++++ .../recorded/itemTextual/clearItem6.yml | 26 +++++++++++ .../recorded/itemTextual/drinkItem.yml | 37 ++++++++++++++++ .../recorded/itemTextual/drinkItem2.yml | 37 ++++++++++++++++ .../takeEveryItem.yml | 0 .../takeEveryItemAir.yml | 0 .../takeItemAir.yml | 0 .../takeItemBlueRisk.yml | 0 17 files changed, 409 insertions(+), 5 deletions(-) create mode 100644 src/test/suite/fixtures/recorded/itemTextual/chuckItem.yml create mode 100644 src/test/suite/fixtures/recorded/itemTextual/chuckItem2.yml create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearEveryItem.yml create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearEveryItem2.yml create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearItem.yml create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearItem2.yml create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearItem3.yml create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearItem4.yml create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearItem5.yml create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearItem6.yml create mode 100644 src/test/suite/fixtures/recorded/itemTextual/drinkItem.yml create mode 100644 src/test/suite/fixtures/recorded/itemTextual/drinkItem2.yml rename src/test/suite/fixtures/recorded/{selectionTypes => itemTextual}/takeEveryItem.yml (100%) rename src/test/suite/fixtures/recorded/{selectionTypes => itemTextual}/takeEveryItemAir.yml (100%) rename src/test/suite/fixtures/recorded/{selectionTypes => itemTextual}/takeItemAir.yml (100%) rename src/test/suite/fixtures/recorded/{selectionTypes => itemTextual}/takeItemBlueRisk.yml (100%) diff --git a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts index 6616a81c0d..2bc0a11083 100644 --- a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts @@ -164,16 +164,18 @@ function rangeToItemInfos( } return undefined; })(); + // Leading boundary is excluded and leading delimiter is included const leadingMatchStart = - tokens[i - 1]?.type === "delimiter" || tokens[i - 1]?.type === "boundary" + tokens[i - 1]?.type === "boundary" + ? tokens[i - 1].range.end + : tokens[i - 1]?.type === "delimiter" ? tokens[i - 1].range.start : token.range.start; + // Trailing boundary and delimiter is excluded const trailingMatchEnd = - tokens[i + 1]?.type === "boundary" - ? tokens[i + 1].range.end - : tokens[i + 1]?.type === "delimiter" + tokens[i + 1]?.type === "boundary" || tokens[i + 1]?.type === "delimiter" ? tokens[i + 1].range.start - : token.range.start; + : token.range.end; const matchRange = new Range(leadingMatchStart, trailingMatchEnd); itemInfos.push({ range: token.range, diff --git a/src/test/suite/fixtures/recorded/itemTextual/chuckItem.yml b/src/test/suite/fixtures/recorded/itemTextual/chuckItem.yml new file mode 100644 index 0000000000..68e3fadbed --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/chuckItem.yml @@ -0,0 +1,32 @@ +languageId: plaintext +command: + spokenForm: chuck item + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: |- + [ + aaa, + ] + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + marks: {} +finalState: + documentContents: |- + [ + + ] + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/chuckItem2.yml b/src/test/suite/fixtures/recorded/itemTextual/chuckItem2.yml new file mode 100644 index 0000000000..102aeed0a8 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/chuckItem2.yml @@ -0,0 +1,32 @@ +languageId: plaintext +command: + spokenForm: chuck item + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: |- + [ + aaa + ] + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + marks: {} +finalState: + documentContents: |- + [ + + ] + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + thatMark: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem.yml b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem.yml new file mode 100644 index 0000000000..7463059ead --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem.yml @@ -0,0 +1,44 @@ +languageId: plaintext +command: + spokenForm: clear every item + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + [ + 111, + "a\"b\"c", + 222, + ] + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + marks: {} +finalState: + documentContents: |- + [ + , + , + , + ] + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} + - anchor: {line: 3, character: 4} + active: {line: 3, character: 4} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} + - anchor: {line: 3, character: 4} + active: {line: 3, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem2.yml b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem2.yml new file mode 100644 index 0000000000..1016795f85 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem2.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + spokenForm: clear every item + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa, bbb, ccc + 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} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearItem.yml b/src/test/suite/fixtures/recorded/itemTextual/clearItem.yml new file mode 100644 index 0000000000..b15f7288e1 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearItem.yml @@ -0,0 +1,36 @@ +languageId: plaintext +command: + spokenForm: clear item + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + [ + 111, + "a\"b\"c", + 222, + ] + selections: + - anchor: {line: 2, character: 5} + active: {line: 2, character: 5} + marks: {} +finalState: + documentContents: |- + [ + 111, + , + 222, + ] + selections: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} + thatMark: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearItem2.yml b/src/test/suite/fixtures/recorded/itemTextual/clearItem2.yml new file mode 100644 index 0000000000..9b3800bfec --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearItem2.yml @@ -0,0 +1,36 @@ +languageId: plaintext +command: + spokenForm: clear item + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + [ + 111, + "a\"b\"c", + 222, + ] + selections: + - anchor: {line: 2, character: 9} + active: {line: 2, character: 9} + marks: {} +finalState: + documentContents: |- + [ + 111, + , + 222, + ] + selections: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} + thatMark: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearItem3.yml b/src/test/suite/fixtures/recorded/itemTextual/clearItem3.yml new file mode 100644 index 0000000000..0114305869 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearItem3.yml @@ -0,0 +1,36 @@ +languageId: plaintext +command: + spokenForm: clear item + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + [ + 111, + "a\"b\"c", + 222, + ] + selections: + - anchor: {line: 2, character: 12} + active: {line: 2, character: 12} + marks: {} +finalState: + documentContents: |- + [ + 111, + , + 222, + ] + selections: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} + thatMark: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearItem4.yml b/src/test/suite/fixtures/recorded/itemTextual/clearItem4.yml new file mode 100644 index 0000000000..f76256abf8 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearItem4.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear item + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: (aaa) + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + marks: {} +finalState: + documentContents: () + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + thatMark: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearItem5.yml b/src/test/suite/fixtures/recorded/itemTextual/clearItem5.yml new file mode 100644 index 0000000000..73d5d86cb3 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearItem5.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear item + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa aaa, bbb bbb + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: ", bbb bbb" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearItem6.yml b/src/test/suite/fixtures/recorded/itemTextual/clearItem6.yml new file mode 100644 index 0000000000..cb400b2436 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearItem6.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear item + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +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} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/drinkItem.yml b/src/test/suite/fixtures/recorded/itemTextual/drinkItem.yml new file mode 100644 index 0000000000..d1066592b4 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/drinkItem.yml @@ -0,0 +1,37 @@ +languageId: plaintext +command: + spokenForm: drink item + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: editNewLineBefore} +initialState: + documentContents: |- + [ + aaa, + bbb, + ccc + ] + selections: + - anchor: {line: 3, character: 7} + active: {line: 3, character: 7} + marks: {} +finalState: + documentContents: |- + [ + aaa, + bbb, + , + ccc + ] + selections: + - anchor: {line: 3, character: 4} + active: {line: 3, character: 4} + thatMark: + - anchor: {line: 4, character: 4} + active: {line: 4, character: 7} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/drinkItem2.yml b/src/test/suite/fixtures/recorded/itemTextual/drinkItem2.yml new file mode 100644 index 0000000000..7377d91886 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/drinkItem2.yml @@ -0,0 +1,37 @@ +languageId: plaintext +command: + spokenForm: drink item + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: editNewLineBefore} +initialState: + documentContents: |- + [ + aaa, + bbb, + ccc, + ] + selections: + - anchor: {line: 3, character: 7} + active: {line: 3, character: 7} + marks: {} +finalState: + documentContents: |- + [ + aaa, + bbb, + , + ccc, + ] + selections: + - anchor: {line: 3, character: 4} + active: {line: 3, character: 4} + thatMark: + - anchor: {line: 4, character: 4} + active: {line: 4, character: 7} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItem.yml b/src/test/suite/fixtures/recorded/itemTextual/takeEveryItem.yml similarity index 100% rename from src/test/suite/fixtures/recorded/selectionTypes/takeEveryItem.yml rename to src/test/suite/fixtures/recorded/itemTextual/takeEveryItem.yml diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeEveryItemAir.yml b/src/test/suite/fixtures/recorded/itemTextual/takeEveryItemAir.yml similarity index 100% rename from src/test/suite/fixtures/recorded/selectionTypes/takeEveryItemAir.yml rename to src/test/suite/fixtures/recorded/itemTextual/takeEveryItemAir.yml diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeItemAir.yml b/src/test/suite/fixtures/recorded/itemTextual/takeItemAir.yml similarity index 100% rename from src/test/suite/fixtures/recorded/selectionTypes/takeItemAir.yml rename to src/test/suite/fixtures/recorded/itemTextual/takeItemAir.yml diff --git a/src/test/suite/fixtures/recorded/selectionTypes/takeItemBlueRisk.yml b/src/test/suite/fixtures/recorded/itemTextual/takeItemBlueRisk.yml similarity index 100% rename from src/test/suite/fixtures/recorded/selectionTypes/takeItemBlueRisk.yml rename to src/test/suite/fixtures/recorded/itemTextual/takeItemBlueRisk.yml From cb0972f05ee9ca5c8aee4e9ab42b3bd6aa1a5601 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 29 Jun 2022 19:22:05 +0200 Subject: [PATCH 15/44] Added test --- .../recorded/itemTextual/chuckEveryItem.yml | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/test/suite/fixtures/recorded/itemTextual/chuckEveryItem.yml diff --git a/src/test/suite/fixtures/recorded/itemTextual/chuckEveryItem.yml b/src/test/suite/fixtures/recorded/itemTextual/chuckEveryItem.yml new file mode 100644 index 0000000000..0a5d3c0fb0 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/chuckEveryItem.yml @@ -0,0 +1,33 @@ +languageId: plaintext +command: + spokenForm: chuck every item + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: |- + { + "hello": "there", + "testing": "whatever", + } + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + marks: {} +finalState: + documentContents: |- + { + + } + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: collectionItem}}]}] From 0a5b8a3e809c2709328359d2baec87bd145dbfee Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 30 Jun 2022 08:11:01 +0200 Subject: [PATCH 16/44] Added support for escaped strings --- .../modifiers/scopeTypeStages/ItemStage.ts | 6 ++- .../recorded/itemTextual/clearEveryItem3.yml | 30 +++++++++++++++ .../recorded/itemTextual/clearEveryItem4.yml | 38 +++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearEveryItem3.yml create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearEveryItem4.yml diff --git a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts index 2bc0a11083..5ee25153e8 100644 --- a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts @@ -195,7 +195,7 @@ function tokenizeRange( ) { const { document } = editor; const text = document.getText(collectionRange); - const lexemes = text.split(/([,(){}<>[\]"'])/g).filter(Boolean); + const lexemes = text.split(/([,(){}<>[\]"'`]|\\"|\\'|\\`)/g).filter(Boolean); const joinedLexemes = joinLexemesBySkippingMatchingPairs(lexemes); const tokens: Token[] = []; let offset = document.offsetAt(collectionRange.start); @@ -315,6 +315,9 @@ function getParentSurroundingPair( const position = editor.document.positionAt( editor.document.offsetAt(pairInfo.contentRange.start) - 1 ); + if (position.isEqual(pairInfo.contentRange.start)) { + return null; + } return getSurroundingPair(context, editor, new Range(position, position)); } @@ -353,5 +356,6 @@ const delimiters: { [key: string]: string } = { "[": "]", '"': '"', "'": "'", + "`": "`", }; /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem3.yml b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem3.yml new file mode 100644 index 0000000000..719b08bea2 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem3.yml @@ -0,0 +1,30 @@ +languageId: plaintext +command: + spokenForm: clear every item + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: aaa, """bbb""" + 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} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem4.yml b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem4.yml new file mode 100644 index 0000000000..a469793ecd --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem4.yml @@ -0,0 +1,38 @@ +languageId: plaintext +command: + spokenForm: clear every item + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: a, "b\",b\"b", 'c\',c\'c', `d\`,d\`d` + 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} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: collectionItem}}]}] From 0e346a72b3cbea208167655d5aaff9bc23e43823 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 30 Jun 2022 08:22:46 +0200 Subject: [PATCH 17/44] Added additional test --- .../recorded/itemTextual/clearEveryItem5.yml | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearEveryItem5.yml diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem5.yml b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem5.yml new file mode 100644 index 0000000000..551b9822b1 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem5.yml @@ -0,0 +1,46 @@ +languageId: plaintext +command: + spokenForm: clear every item + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + [ + a, + [ + b + ], + c + ] + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + marks: {} +finalState: + documentContents: |- + [ + , + , + + ] + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} + - anchor: {line: 3, character: 4} + active: {line: 3, character: 4} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} + - anchor: {line: 3, character: 4} + active: {line: 3, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: collectionItem}}]}] From e5efc63f6a3432d7dbcc3264c7634f9d97c3f473 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 30 Jun 2022 10:33:31 +0200 Subject: [PATCH 18/44] Added more tests --- .../recorded/itemTextual/clearEveryItem6.yml | 34 ++++++++++++++ .../itemTextual/clearEveryItemBlock.yml | 44 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearEveryItem6.yml create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearEveryItemBlock.yml diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem6.yml b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem6.yml new file mode 100644 index 0000000000..9dd42158c4 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem6.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + spokenForm: clear every item + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + a, b, + c, d + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |- + , , + c, d + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearEveryItemBlock.yml b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItemBlock.yml new file mode 100644 index 0000000000..7751b69200 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItemBlock.yml @@ -0,0 +1,44 @@ +languageId: plaintext +command: + spokenForm: clear every item block + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: collectionItem} + - type: containingScope + scopeType: {type: paragraph} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: |- + a, b, + c, d + 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} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + - anchor: {line: 1, character: 2} + active: {line: 1, character: 2} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: collectionItem}}, {type: containingScope, scopeType: {type: paragraph}}]}] From eccb1c98d487d0a113bb2e07b10cb2199d2f99d0 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 30 Jun 2022 12:01:32 +0100 Subject: [PATCH 19/44] Light clean up and add test --- .../modifiers/scopeTypeStages/ItemStage.ts | 25 ++++++------ .../recorded/itemTextual/clearEveryItem7.yml | 38 +++++++++++++++++++ 2 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearEveryItem7.yml diff --git a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts index 5ee25153e8..0818405f2a 100644 --- a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts +++ b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts @@ -1,3 +1,4 @@ +import { maxBy } from "lodash"; import { Range, TextEditor } from "vscode"; import { NoContainingScopeError } from "../../../errors"; import { Target } from "../../../typings/target.types"; @@ -49,7 +50,7 @@ export default class ItemStage implements ModifierStage { const itemInfoWithIntersections = itemInfos .map((itemInfo) => ({ itemInfo, - intersection: itemInfo.matchRange.intersection(target.contentRange), + intersection: itemInfo.domain.intersection(target.contentRange), })) .filter((e) => e.intersection != null); @@ -71,7 +72,7 @@ export default class ItemStage implements ModifierStage { scopeTypeType: this.modifier.scopeType.type, editor: target.editor, isReversed: target.isReversed, - contentRange: itemInfo.range, + contentRange: itemInfo.contentRange, delimiter: getInsertionDelimiter(target, itemInfo), leadingDelimiterRange: itemInfo.leadingDelimiterRange, trailingDelimiterRange: itemInfo.trailingDelimiterRange, @@ -89,17 +90,19 @@ function getInsertionDelimiter(target: Target, itemInfo: ItemInfo) { ? getText(itemInfo.leadingDelimiterRange) : defaultDelimiterInsertion, ]; - // Use longest delimiter text for insertion - delimiters.sort((a, b) => b.length - a.length); - return delimiters[0]; + + return maxBy(delimiters, "length"); } function getItemInfos(context: ProcessedTargetsContext, target: Target) { - const { range, boundary } = getCollectionRange(context, target); + const { range, boundary } = getIterationScopeRange(context, target); return rangeToItemInfos(target.editor, range, boundary); } -function getCollectionRange(context: ProcessedTargetsContext, target: Target) { +function getIterationScopeRange( + context: ProcessedTargetsContext, + target: Target +) { // First check if we are in a string let pairInfo = getStringSurroundingPair( context, @@ -178,10 +181,10 @@ function rangeToItemInfos( : token.range.end; const matchRange = new Range(leadingMatchStart, trailingMatchEnd); itemInfos.push({ - range: token.range, + contentRange: token.range, leadingDelimiterRange, trailingDelimiterRange, - matchRange, + domain: matchRange, }); }); @@ -333,10 +336,10 @@ function getStringSurroundingPair( } interface ItemInfo { - range: Range; + contentRange: Range; leadingDelimiterRange?: Range; trailingDelimiterRange?: Range; - matchRange: Range; + domain: Range; } interface Token { diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem7.yml b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem7.yml new file mode 100644 index 0000000000..c9e2e9c255 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItem7.yml @@ -0,0 +1,38 @@ +languageId: typescript +command: + spokenForm: clear every item + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: seq('(', ",", $._import_list, ')') + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + marks: {} +finalState: + documentContents: seq(, , , ) + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + - anchor: {line: 0, character: 8} + active: {line: 0, character: 8} + - anchor: {line: 0, character: 10} + active: {line: 0, character: 10} + thatMark: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + - anchor: {line: 0, character: 8} + active: {line: 0, character: 8} + - anchor: {line: 0, character: 10} + active: {line: 0, character: 10} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: collectionItem}}]}] From 05218be6063e2be9c9908f097fc4333344d6770a Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 30 Jun 2022 13:49:07 +0200 Subject: [PATCH 20/44] cleanup --- src/processTargets/getModifierStage.ts | 2 +- .../modifiers/ItemStage/ItemStage.ts | 157 ++++++++ .../modifiers/ItemStage/getIterationScope.ts | 81 ++++ .../modifiers/ItemStage/index.ts | 2 + .../modifiers/ItemStage/tokenizeRange.ts | 129 +++++++ .../modifiers/scopeTypeStages/ItemStage.ts | 364 ------------------ src/util/nodeSelectors.ts | 45 ++- 7 files changed, 399 insertions(+), 381 deletions(-) create mode 100644 src/processTargets/modifiers/ItemStage/ItemStage.ts create mode 100644 src/processTargets/modifiers/ItemStage/getIterationScope.ts create mode 100644 src/processTargets/modifiers/ItemStage/index.ts create mode 100644 src/processTargets/modifiers/ItemStage/tokenizeRange.ts delete mode 100644 src/processTargets/modifiers/scopeTypeStages/ItemStage.ts diff --git a/src/processTargets/getModifierStage.ts b/src/processTargets/getModifierStage.ts index a573375b3e..a47e1f191e 100644 --- a/src/processTargets/getModifierStage.ts +++ b/src/processTargets/getModifierStage.ts @@ -9,6 +9,7 @@ import { ExcludeInteriorStage, InteriorOnlyStage, } from "./modifiers/InteriorStage"; +import ItemStage from "./modifiers/ItemStage"; import { LeadingStage, TrailingStage } from "./modifiers/LeadingTrailingStages"; import OrdinalRangeSubTokenStage, { OrdinalRangeSubTokenModifier, @@ -19,7 +20,6 @@ import ContainingSyntaxScopeStage, { SimpleContainingScopeModifier, } from "./modifiers/scopeTypeStages/ContainingSyntaxScopeStage"; import DocumentStage from "./modifiers/scopeTypeStages/DocumentStage"; -import ItemStage from "./modifiers/scopeTypeStages/ItemStage"; import LineStage from "./modifiers/scopeTypeStages/LineStage"; import NotebookCellStage from "./modifiers/scopeTypeStages/NotebookCellStage"; import ParagraphStage from "./modifiers/scopeTypeStages/ParagraphStage"; diff --git a/src/processTargets/modifiers/ItemStage/ItemStage.ts b/src/processTargets/modifiers/ItemStage/ItemStage.ts new file mode 100644 index 0000000000..848fed888b --- /dev/null +++ b/src/processTargets/modifiers/ItemStage/ItemStage.ts @@ -0,0 +1,157 @@ +import { Range, TextEditor } from "vscode"; +import { NoContainingScopeError } from "../../../errors"; +import { Target } from "../../../typings/target.types"; +import { + ContainingScopeModifier, + EveryScopeModifier, + SimpleScopeTypeType, +} from "../../../typings/targetDescriptor.types"; +import { ProcessedTargetsContext } from "../../../typings/Types"; +import { getInsertionDelimiter } from "../../../util/nodeSelectors"; +import { rangeLength } from "../../../util/rangeUtils"; +import { ModifierStage } from "../../PipelineStages.types"; +import ScopeTypeTarget from "../../targets/ScopeTypeTarget"; +import ContainingSyntaxScopeStage, { + SimpleContainingScopeModifier, +} from "../scopeTypeStages/ContainingSyntaxScopeStage"; +import { getIterationScope } from "./getIterationScope"; +import { tokenizeRange } from "./tokenizeRange"; + +export default class ItemStage implements ModifierStage { + constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} + + run(context: ProcessedTargetsContext, target: Target): Target[] { + // First try the language specific implementation of item + try { + return new ContainingSyntaxScopeStage( + this.modifier + ).run(context, target); + } catch (_error) {} + + // Then try the textual implementation + if (this.modifier.type === "everyScope") { + return this.getEveryTarget(context, target); + } + return [this.getSingleTarget(context, target)]; + } + + private getEveryTarget(context: ProcessedTargetsContext, target: Target) { + const itemInfos = getItemInfosForIterationScope(context, target); + + if (itemInfos.length === 0) { + throw new NoContainingScopeError(this.modifier.scopeType.type); + } + + return itemInfos.map((itemInfo) => this.itemInfoToTarget(target, itemInfo)); + } + + private getSingleTarget(context: ProcessedTargetsContext, target: Target) { + const itemInfos = getItemInfosForIterationScope(context, target); + + const itemInfoWithIntersections = itemInfos + .map((itemInfo) => ({ + itemInfo, + intersection: itemInfo.domain.intersection(target.contentRange), + })) + .filter((e) => e.intersection != null); + + if (itemInfoWithIntersections.length === 0) { + throw new NoContainingScopeError(this.modifier.scopeType.type); + } + + itemInfoWithIntersections.sort( + (a, b) => + rangeLength(target.editor, b.intersection!) - + rangeLength(target.editor, a.intersection!) + ); + + return this.itemInfoToTarget(target, itemInfoWithIntersections[0].itemInfo); + } + + private itemInfoToTarget(target: Target, itemInfo: ItemInfo) { + const delimiter = getInsertionDelimiter( + target.editor, + itemInfo.leadingDelimiterRange, + itemInfo.trailingDelimiterRange, + ", " + ); + return new ScopeTypeTarget({ + scopeTypeType: this.modifier.scopeType.type, + editor: target.editor, + isReversed: target.isReversed, + contentRange: itemInfo.contentRange, + delimiter, + leadingDelimiterRange: itemInfo.leadingDelimiterRange, + trailingDelimiterRange: itemInfo.trailingDelimiterRange, + }); + } +} + +function getItemInfosForIterationScope( + context: ProcessedTargetsContext, + target: Target +) { + const { range, boundary } = getIterationScope(context, target); + return rangeToItemInfos(target.editor, range, boundary); +} + +function rangeToItemInfos( + editor: TextEditor, + collectionRange: Range, + collectionBoundary?: [Range, Range] +): ItemInfo[] { + const tokens = tokenizeRange(editor, collectionRange, collectionBoundary); + const itemInfos: ItemInfo[] = []; + + tokens.forEach((token, i) => { + if (token.type === "delimiter" || 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 === "delimiter") { + 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 === "delimiter") { + return new Range(token.range.end, tokens[i + 1].range.end); + } + return undefined; + })(); + // Leading boundary is excluded and leading delimiter is included + const leadingMatchStart = + tokens[i - 1]?.type === "boundary" + ? tokens[i - 1].range.end + : tokens[i - 1]?.type === "delimiter" + ? tokens[i - 1].range.start + : token.range.start; + // Trailing boundary and delimiter is excluded + const trailingMatchEnd = + tokens[i + 1]?.type === "boundary" || tokens[i + 1]?.type === "delimiter" + ? tokens[i + 1].range.start + : token.range.end; + const matchRange = new Range(leadingMatchStart, trailingMatchEnd); + itemInfos.push({ + contentRange: token.range, + leadingDelimiterRange, + trailingDelimiterRange, + domain: matchRange, + }); + }); + + return itemInfos; +} + +interface ItemInfo { + contentRange: Range; + leadingDelimiterRange?: Range; + trailingDelimiterRange?: Range; + domain: Range; +} diff --git a/src/processTargets/modifiers/ItemStage/getIterationScope.ts b/src/processTargets/modifiers/ItemStage/getIterationScope.ts new file mode 100644 index 0000000000..2c1dd67754 --- /dev/null +++ b/src/processTargets/modifiers/ItemStage/getIterationScope.ts @@ -0,0 +1,81 @@ +import { Range, TextEditor } from "vscode"; +import { Target } from "../../../typings/target.types"; +import { ProcessedTargetsContext } from "../../../typings/Types"; +import { fitRangeToLineContent } from "../scopeTypeStages/LineStage"; +import { processSurroundingPair } from "../surroundingPair"; +import { SurroundingPairInfo } from "../surroundingPair/extractSelectionFromSurroundingPairOffsets"; + +export function getIterationScope( + context: ProcessedTargetsContext, + target: Target +) { + // First check if we are in a string + let pairInfo = getStringSurroundingPair( + context, + target.editor, + target.contentRange + ); + + // We don't look for items inside strings. If we are in a string go to parent + pairInfo = + pairInfo != null + ? getParentSurroundingPair(context, target.editor, pairInfo) + : getSurroundingPair(context, target.editor, target.contentRange); + + while (pairInfo != null) { + // The selection from the beginning was this pair and we should not go into the interior but instead look in the parent. + const isNotInterior = + target.contentRange.isEqual(pairInfo.contentRange) || + target.contentRange.start.isBeforeOrEqual(pairInfo.boundary[0].start) || + target.contentRange.end.isAfterOrEqual(pairInfo.boundary[1].end); + if (!isNotInterior) { + return { + range: pairInfo.interiorRange, + boundary: pairInfo.boundary, + }; + } + pairInfo = getParentSurroundingPair(context, target.editor, pairInfo); + } + + // We have not found a pair containing the delimiter. Look at the full line. + return { + range: fitRangeToLineContent(target.editor, target.contentRange), + }; +} + +function getSurroundingPair( + context: ProcessedTargetsContext, + editor: TextEditor, + contentRange: Range +) { + return processSurroundingPair(context, editor, contentRange, { + type: "surroundingPair", + delimiter: "any", + }); +} + +function getParentSurroundingPair( + context: ProcessedTargetsContext, + editor: TextEditor, + pairInfo: SurroundingPairInfo +) { + // Step out of this pair and see if we have a parent + const position = editor.document.positionAt( + editor.document.offsetAt(pairInfo.contentRange.start) - 1 + ); + if (position.isEqual(pairInfo.contentRange.start)) { + return null; + } + return getSurroundingPair(context, editor, new Range(position, position)); +} + +function getStringSurroundingPair( + context: ProcessedTargetsContext, + editor: TextEditor, + contentRange: Range +) { + return processSurroundingPair(context, editor, contentRange, { + type: "surroundingPair", + delimiter: "string", + }); +} diff --git a/src/processTargets/modifiers/ItemStage/index.ts b/src/processTargets/modifiers/ItemStage/index.ts new file mode 100644 index 0000000000..246be4d0d7 --- /dev/null +++ b/src/processTargets/modifiers/ItemStage/index.ts @@ -0,0 +1,2 @@ +import ItemStage from "./ItemStage"; +export default ItemStage; diff --git a/src/processTargets/modifiers/ItemStage/tokenizeRange.ts b/src/processTargets/modifiers/ItemStage/tokenizeRange.ts new file mode 100644 index 0000000000..04a94f6f57 --- /dev/null +++ b/src/processTargets/modifiers/ItemStage/tokenizeRange.ts @@ -0,0 +1,129 @@ +import { Range, TextEditor } from "vscode"; + +export function tokenizeRange( + editor: TextEditor, + collectionRange: Range, + collectionBoundary?: [Range, Range] +) { + const { document } = editor; + const text = document.getText(collectionRange); + const lexemes = text.split(/([,(){}<>[\]"'`]|\\"|\\'|\\`)/g).filter(Boolean); + const joinedLexemes = joinLexemesBySkippingMatchingPairs(lexemes); + const tokens: Token[] = []; + let offset = document.offsetAt(collectionRange.start); + + joinedLexemes.forEach((lexeme) => { + // Whitespace found. Just skip + if (lexeme.trim().length === 0) { + offset += lexeme.length; + return; + } + + // Separator delimiter found. + if (lexeme === delimiter) { + tokens.push({ + type: "delimiter", + 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 (collectionBoundary != null) { + return [ + { type: "boundary", range: collectionBoundary[0] }, + ...tokens, + { type: "boundary", range: collectionBoundary[1] }, + ]; + } + + return tokens; +} + +export function joinLexemesBySkippingMatchingPairs(lexemes: string[]) { + const result: string[] = []; + let delimiterCount = 0; + let openingDelimiter: string | null = null; + let closingDelimiter: string | null = null; + let startIndex: number = -1; + + lexemes.forEach((lexeme, index) => { + // We are waiting for a closing delimiter + if (delimiterCount > 0) { + // Closing delimiter found + if (closingDelimiter === lexeme) { + --delimiterCount; + } + // Additional opening delimiter found + else if (openingDelimiter === lexeme) { + ++delimiterCount; + } + } + + // Starting delimiter found + else if (delimiters[lexeme] != null) { + openingDelimiter = lexeme; + closingDelimiter = delimiters[lexeme]; + delimiterCount = 1; + // This is the first lexeme to be joined + if (startIndex < 0) { + startIndex = index; + } + } + + // This is the first lexeme to be joined + else if (startIndex < 0) { + startIndex = index; + } + + const isDelimiter = lexeme === delimiter && delimiterCount === 0; + + // This is the last lexeme to be joined + if (isDelimiter || index === lexemes.length - 1) { + const endIndex = isDelimiter ? index : index + 1; + result.push(lexemes.slice(startIndex, endIndex).join("")); + startIndex = -1; + if (isDelimiter) { + result.push(lexeme); + } + } + }); + + return result; +} + +const delimiter = ","; + +// Mapping between opening and closing delimiters +/* eslint-disable @typescript-eslint/naming-convention */ +const delimiters: { [key: string]: string } = { + "(": ")", + "{": "}", + "<": ">", + "[": "]", + '"': '"', + "'": "'", + "`": "`", +}; +/* eslint-enable @typescript-eslint/naming-convention */ + +interface Token { + range: Range; + type: string; +} diff --git a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts b/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts deleted file mode 100644 index 0818405f2a..0000000000 --- a/src/processTargets/modifiers/scopeTypeStages/ItemStage.ts +++ /dev/null @@ -1,364 +0,0 @@ -import { maxBy } from "lodash"; -import { Range, TextEditor } from "vscode"; -import { NoContainingScopeError } from "../../../errors"; -import { Target } from "../../../typings/target.types"; -import { - ContainingScopeModifier, - EveryScopeModifier, - SimpleScopeTypeType, -} from "../../../typings/targetDescriptor.types"; -import { ProcessedTargetsContext } from "../../../typings/Types"; -import { rangeLength } from "../../../util/rangeUtils"; -import { ModifierStage } from "../../PipelineStages.types"; -import ScopeTypeTarget from "../../targets/ScopeTypeTarget"; -import { processSurroundingPair } from "../surroundingPair"; -import { SurroundingPairInfo } from "../surroundingPair/extractSelectionFromSurroundingPairOffsets"; -import ContainingSyntaxScopeStage, { - SimpleContainingScopeModifier, -} from "./ContainingSyntaxScopeStage"; -import { fitRangeToLineContent } from "./LineStage"; - -export default class ItemStage implements ModifierStage { - constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} - - run(context: ProcessedTargetsContext, target: Target): Target[] { - try { - return new ContainingSyntaxScopeStage( - this.modifier - ).run(context, target); - } catch (_error) {} - - if (this.modifier.type === "everyScope") { - return this.getEveryTarget(context, target); - } - return [this.getSingleTarget(context, target)]; - } - - private getEveryTarget(context: ProcessedTargetsContext, target: Target) { - const itemInfos = getItemInfos(context, target); - - if (itemInfos.length === 0) { - throw new NoContainingScopeError(this.modifier.scopeType.type); - } - - return itemInfos.map((itemInfo) => this.itemInfoToTarget(target, itemInfo)); - } - - private getSingleTarget(context: ProcessedTargetsContext, target: Target) { - const itemInfos = getItemInfos(context, target); - - const itemInfoWithIntersections = itemInfos - .map((itemInfo) => ({ - itemInfo, - intersection: itemInfo.domain.intersection(target.contentRange), - })) - .filter((e) => e.intersection != null); - - if (itemInfoWithIntersections.length === 0) { - throw new NoContainingScopeError(this.modifier.scopeType.type); - } - - itemInfoWithIntersections.sort( - (a, b) => - rangeLength(target.editor, b.intersection!) - - rangeLength(target.editor, a.intersection!) - ); - - return this.itemInfoToTarget(target, itemInfoWithIntersections[0].itemInfo); - } - - private itemInfoToTarget(target: Target, itemInfo: ItemInfo) { - return new ScopeTypeTarget({ - scopeTypeType: this.modifier.scopeType.type, - editor: target.editor, - isReversed: target.isReversed, - contentRange: itemInfo.contentRange, - delimiter: getInsertionDelimiter(target, itemInfo), - leadingDelimiterRange: itemInfo.leadingDelimiterRange, - trailingDelimiterRange: itemInfo.trailingDelimiterRange, - }); - } -} - -function getInsertionDelimiter(target: Target, itemInfo: ItemInfo) { - const { getText } = target.editor.document; - const delimiters = [ - itemInfo.trailingDelimiterRange != null - ? getText(itemInfo.trailingDelimiterRange) - : defaultDelimiterInsertion, - itemInfo.leadingDelimiterRange != null - ? getText(itemInfo.leadingDelimiterRange) - : defaultDelimiterInsertion, - ]; - - return maxBy(delimiters, "length"); -} - -function getItemInfos(context: ProcessedTargetsContext, target: Target) { - const { range, boundary } = getIterationScopeRange(context, target); - return rangeToItemInfos(target.editor, range, boundary); -} - -function getIterationScopeRange( - context: ProcessedTargetsContext, - target: Target -) { - // First check if we are in a string - let pairInfo = getStringSurroundingPair( - context, - target.editor, - target.contentRange - ); - - // We don't look for items inside strings. If we are in a string go to parent - pairInfo = - pairInfo != null - ? getParentSurroundingPair(context, target.editor, pairInfo) - : getSurroundingPair(context, target.editor, target.contentRange); - - while (pairInfo != null) { - // The selection from the beginning was this pair and we should not go into the interior but instead look in the parent. - const isNotInterior = - target.contentRange.isEqual(pairInfo.contentRange) || - target.contentRange.start.isBeforeOrEqual(pairInfo.boundary[0].start) || - target.contentRange.end.isAfterOrEqual(pairInfo.boundary[1].end); - if (!isNotInterior) { - return { - range: pairInfo.interiorRange, - boundary: pairInfo.boundary, - }; - } - pairInfo = getParentSurroundingPair(context, target.editor, pairInfo); - } - - // We have not found a pair containing the delimiter. Look at the full line. - return { - range: fitRangeToLineContent(target.editor, target.contentRange), - }; -} - -function rangeToItemInfos( - editor: TextEditor, - collectionRange: Range, - collectionBoundary?: [Range, Range] -): ItemInfo[] { - const tokens = tokenizeRange(editor, collectionRange, collectionBoundary); - const itemInfos: ItemInfo[] = []; - - tokens.forEach((token, i) => { - if (token.type === "delimiter" || 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 === "delimiter") { - 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 === "delimiter") { - return new Range(token.range.end, tokens[i + 1].range.end); - } - return undefined; - })(); - // Leading boundary is excluded and leading delimiter is included - const leadingMatchStart = - tokens[i - 1]?.type === "boundary" - ? tokens[i - 1].range.end - : tokens[i - 1]?.type === "delimiter" - ? tokens[i - 1].range.start - : token.range.start; - // Trailing boundary and delimiter is excluded - const trailingMatchEnd = - tokens[i + 1]?.type === "boundary" || tokens[i + 1]?.type === "delimiter" - ? tokens[i + 1].range.start - : token.range.end; - const matchRange = new Range(leadingMatchStart, trailingMatchEnd); - itemInfos.push({ - contentRange: token.range, - leadingDelimiterRange, - trailingDelimiterRange, - domain: matchRange, - }); - }); - - return itemInfos; -} - -function tokenizeRange( - editor: TextEditor, - collectionRange: Range, - collectionBoundary?: [Range, Range] -) { - const { document } = editor; - const text = document.getText(collectionRange); - const lexemes = text.split(/([,(){}<>[\]"'`]|\\"|\\'|\\`)/g).filter(Boolean); - const joinedLexemes = joinLexemesBySkippingMatchingPairs(lexemes); - const tokens: Token[] = []; - let offset = document.offsetAt(collectionRange.start); - - joinedLexemes.forEach((lexeme) => { - // Whitespace found. Just skip - if (lexeme.trim().length === 0) { - offset += lexeme.length; - return; - } - - // Separator delimiter found. - if (lexeme === delimiter) { - tokens.push({ - type: "delimiter", - 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 (collectionBoundary != null) { - return [ - { type: "boundary", range: collectionBoundary[0] }, - ...tokens, - { type: "boundary", range: collectionBoundary[1] }, - ]; - } - - return tokens; -} - -function joinLexemesBySkippingMatchingPairs(lexemes: string[]) { - const result: string[] = []; - let delimiterCount = 0; - let openingDelimiter: string | null = null; - let closingDelimiter: string | null = null; - let startIndex: number = -1; - - lexemes.forEach((lexeme, index) => { - // We are waiting for a closing delimiter - if (delimiterCount > 0) { - // Closing delimiter found - if (closingDelimiter === lexeme) { - --delimiterCount; - } - // Additional opening delimiter found - else if (openingDelimiter === lexeme) { - ++delimiterCount; - } - } - - // Starting delimiter found - else if (delimiters[lexeme] != null) { - openingDelimiter = lexeme; - closingDelimiter = delimiters[lexeme]; - delimiterCount = 1; - // This is the first lexeme to be joined - if (startIndex < 0) { - startIndex = index; - } - } - - // This is the first lexeme to be joined - else if (startIndex < 0) { - startIndex = index; - } - - const isDelimiter = lexeme === delimiter && delimiterCount === 0; - - // This is the last lexeme to be joined - if (isDelimiter || index === lexemes.length - 1) { - const endIndex = isDelimiter ? index : index + 1; - result.push(lexemes.slice(startIndex, endIndex).join("")); - startIndex = -1; - if (isDelimiter) { - result.push(lexeme); - } - } - }); - - return result; -} - -function getSurroundingPair( - context: ProcessedTargetsContext, - editor: TextEditor, - contentRange: Range -) { - return processSurroundingPair(context, editor, contentRange, { - type: "surroundingPair", - delimiter: "any", - }); -} - -function getParentSurroundingPair( - context: ProcessedTargetsContext, - editor: TextEditor, - pairInfo: SurroundingPairInfo -) { - // Step out of this pair and see if we have a parent - const position = editor.document.positionAt( - editor.document.offsetAt(pairInfo.contentRange.start) - 1 - ); - if (position.isEqual(pairInfo.contentRange.start)) { - return null; - } - return getSurroundingPair(context, editor, new Range(position, position)); -} - -function getStringSurroundingPair( - context: ProcessedTargetsContext, - editor: TextEditor, - contentRange: Range -) { - return processSurroundingPair(context, editor, contentRange, { - type: "surroundingPair", - delimiter: "string", - }); -} - -interface ItemInfo { - contentRange: Range; - leadingDelimiterRange?: Range; - trailingDelimiterRange?: Range; - domain: Range; -} - -interface Token { - range: Range; - type: string; -} - -const delimiter = ","; -const defaultDelimiterInsertion = ", "; - -// Mapping between opening and closing delimiters -/* eslint-disable @typescript-eslint/naming-convention */ -const delimiters: { [key: string]: string } = { - "(": ")", - "{": "}", - "<": ">", - "[": "]", - '"': '"', - "'": "'", - "`": "`", -}; -/* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/util/nodeSelectors.ts b/src/util/nodeSelectors.ts index 5423d96a34..21831b4041 100644 --- a/src/util/nodeSelectors.ts +++ b/src/util/nodeSelectors.ts @@ -1,11 +1,11 @@ -import { SyntaxNode, Point } from "web-tree-sitter"; +import { identity, maxBy } from "lodash"; import { Position, Range, Selection, TextEditor } from "vscode"; +import { Point, SyntaxNode } from "web-tree-sitter"; import { - SelectionWithContext, - SelectionExtractor, NodeFinder, + SelectionExtractor, + SelectionWithContext, } from "../typings/Types"; -import { identity } from "lodash"; export function makeRangeFromPositions( startPosition: Point, @@ -317,7 +317,6 @@ export function delimitedSelector( getEndNode: (node: SyntaxNode) => SyntaxNode = identity ): SelectionExtractor { return (editor: TextEditor, node: SyntaxNode) => { - let containingListDelimiter: string | undefined; let leadingDelimiterRange: Range | undefined; let trailingDelimiterRange: Range | undefined; const startNode = getStartNode(node); @@ -337,8 +336,6 @@ export function delimitedSelector( endNode.endPosition, nextNonDelimiterNode.startPosition ); - - containingListDelimiter = editor.document.getText(trailingDelimiterRange); } if (previousNonDelimiterNode != null) { @@ -346,17 +343,14 @@ export function delimitedSelector( previousNonDelimiterNode.endPosition, startNode.startPosition ); - - if (containingListDelimiter == null) { - containingListDelimiter = editor.document.getText( - leadingDelimiterRange - ); - } } - if (containingListDelimiter == null) { - containingListDelimiter = defaultDelimiter; - } + const containingListDelimiter = getInsertionDelimiter( + editor, + leadingDelimiterRange, + trailingDelimiterRange, + defaultDelimiter + ); return { selection: new Selection( @@ -374,3 +368,22 @@ export function delimitedSelector( }; }; } + +export function getInsertionDelimiter( + editor: TextEditor, + leadingDelimiterRange: Range | undefined, + trailingDelimiterRange: Range | undefined, + defaultDelimiterInsertion: string +) { + const { getText } = editor.document; + const delimiters = [ + trailingDelimiterRange != null + ? getText(trailingDelimiterRange) + : defaultDelimiterInsertion, + leadingDelimiterRange != null + ? getText(leadingDelimiterRange) + : defaultDelimiterInsertion, + ]; + + return maxBy(delimiters, "length"); +} From 571269db9ad213f6aa69de4de8448fb2a95be4c0 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 30 Jun 2022 14:05:38 +0200 Subject: [PATCH 21/44] Include trailing delimiter in arguments --- .../languages/python/chuckEveryArg.yml | 35 +++++++ src/util/nodeSelectors.ts | 99 +++++++++++++++---- 2 files changed, 113 insertions(+), 21 deletions(-) create mode 100644 src/test/suite/fixtures/recorded/languages/python/chuckEveryArg.yml diff --git a/src/test/suite/fixtures/recorded/languages/python/chuckEveryArg.yml b/src/test/suite/fixtures/recorded/languages/python/chuckEveryArg.yml new file mode 100644 index 0000000000..bf401ce114 --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/python/chuckEveryArg.yml @@ -0,0 +1,35 @@ +languageId: python +command: + spokenForm: chuck every arg + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: argumentOrParameter} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: |- + def foo( + aaa, + bbb, + ): + + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + marks: {} +finalState: + documentContents: |- + def foo( + + ): + + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: argumentOrParameter}}]}] diff --git a/src/util/nodeSelectors.ts b/src/util/nodeSelectors.ts index 21831b4041..ce02ca4ccb 100644 --- a/src/util/nodeSelectors.ts +++ b/src/util/nodeSelectors.ts @@ -293,6 +293,40 @@ function getPreviousNonDelimiterNode( return node; } +function getNextDelimiterNode( + startNode: SyntaxNode, + isDelimiterNode: (node: SyntaxNode) => boolean +): SyntaxNode | null { + let node = startNode.nextSibling; + + while (node != null) { + if (isDelimiterNode(node)) { + return node; + } + + node = node.nextSibling; + } + + return node; +} + +function getPreviousDelimiterNode( + startNode: SyntaxNode, + isDelimiterNode: (node: SyntaxNode) => boolean +): SyntaxNode | null { + let node = startNode.previousSibling; + + while (node != null) { + if (isDelimiterNode(node)) { + return node; + } + + node = node.previousSibling; + } + + return node; +} + export function delimitersSelector(...delimiters: string[]) { return delimitedSelector((node) => delimiters.includes(node.type), ", "); } @@ -317,33 +351,56 @@ export function delimitedSelector( getEndNode: (node: SyntaxNode) => SyntaxNode = identity ): SelectionExtractor { return (editor: TextEditor, node: SyntaxNode) => { - let leadingDelimiterRange: Range | undefined; - let trailingDelimiterRange: Range | undefined; const startNode = getStartNode(node); const endNode = getEndNode(node); - const nextNonDelimiterNode = getNextNonDelimiterNode( - endNode, - isDelimiterNode - ); - const previousNonDelimiterNode = getPreviousNonDelimiterNode( - startNode, - isDelimiterNode - ); - - if (nextNonDelimiterNode != null) { - trailingDelimiterRange = makeRangeFromPositions( - endNode.endPosition, - nextNonDelimiterNode.startPosition + const leadingDelimiterRange = (() => { + const previousNonDelimiterNode = getPreviousNonDelimiterNode( + startNode, + isDelimiterNode ); - } + if (previousNonDelimiterNode != null) { + return makeRangeFromPositions( + previousNonDelimiterNode.endPosition, + startNode.startPosition + ); + } + const previousDelimiterNode = getPreviousDelimiterNode( + startNode, + isDelimiterNode + ); + if (previousDelimiterNode != null) { + return makeRangeFromPositions( + previousDelimiterNode.startPosition, + startNode.startPosition + ); + } + return undefined; + })(); - if (previousNonDelimiterNode != null) { - leadingDelimiterRange = makeRangeFromPositions( - previousNonDelimiterNode.endPosition, - startNode.startPosition + const trailingDelimiterRange = (() => { + const nextNonDelimiterNode = getNextNonDelimiterNode( + startNode, + isDelimiterNode ); - } + if (nextNonDelimiterNode != null) { + return makeRangeFromPositions( + nextNonDelimiterNode.startPosition, + startNode.startPosition + ); + } + const nextDelimiterNode = getNextDelimiterNode( + startNode, + isDelimiterNode + ); + if (nextDelimiterNode != null) { + return makeRangeFromPositions( + nextDelimiterNode.endPosition, + startNode.startPosition + ); + } + return undefined; + })(); const containingListDelimiter = getInsertionDelimiter( editor, From c67f1e2aa9ce2caf49df1c5470e9f0e99e7a8ad9 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 30 Jun 2022 14:07:23 +0200 Subject: [PATCH 22/44] Cleanup --- src/util/nodeSelectors.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/util/nodeSelectors.ts b/src/util/nodeSelectors.ts index ce02ca4ccb..dbe0e32133 100644 --- a/src/util/nodeSelectors.ts +++ b/src/util/nodeSelectors.ts @@ -385,8 +385,8 @@ export function delimitedSelector( ); if (nextNonDelimiterNode != null) { return makeRangeFromPositions( - nextNonDelimiterNode.startPosition, - startNode.startPosition + endNode.endPosition, + nextNonDelimiterNode.startPosition ); } const nextDelimiterNode = getNextDelimiterNode( @@ -395,8 +395,8 @@ export function delimitedSelector( ); if (nextDelimiterNode != null) { return makeRangeFromPositions( - nextDelimiterNode.endPosition, - startNode.startPosition + endNode.endPosition, + nextDelimiterNode.endPosition ); } return undefined; From 2b318ffc41fb928d6fa8faf1dfebd958cc8ce6dd Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 30 Jun 2022 14:15:12 +0200 Subject: [PATCH 23/44] Cleanup --- .../languages/python/chuckEveryArg.yml | 35 ------- src/util/nodeSelectors.ts | 99 ++++--------------- 2 files changed, 21 insertions(+), 113 deletions(-) delete mode 100644 src/test/suite/fixtures/recorded/languages/python/chuckEveryArg.yml diff --git a/src/test/suite/fixtures/recorded/languages/python/chuckEveryArg.yml b/src/test/suite/fixtures/recorded/languages/python/chuckEveryArg.yml deleted file mode 100644 index bf401ce114..0000000000 --- a/src/test/suite/fixtures/recorded/languages/python/chuckEveryArg.yml +++ /dev/null @@ -1,35 +0,0 @@ -languageId: python -command: - spokenForm: chuck every arg - version: 2 - targets: - - type: primitive - modifiers: - - type: everyScope - scopeType: {type: argumentOrParameter} - usePrePhraseSnapshot: true - action: {name: remove} -initialState: - documentContents: |- - def foo( - aaa, - bbb, - ): - - selections: - - anchor: {line: 1, character: 4} - active: {line: 1, character: 4} - marks: {} -finalState: - documentContents: |- - def foo( - - ): - - selections: - - anchor: {line: 1, character: 4} - active: {line: 1, character: 4} - thatMark: - - anchor: {line: 1, character: 4} - active: {line: 1, character: 4} -fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: argumentOrParameter}}]}] diff --git a/src/util/nodeSelectors.ts b/src/util/nodeSelectors.ts index dbe0e32133..2b8bae3c5a 100644 --- a/src/util/nodeSelectors.ts +++ b/src/util/nodeSelectors.ts @@ -293,40 +293,6 @@ function getPreviousNonDelimiterNode( return node; } -function getNextDelimiterNode( - startNode: SyntaxNode, - isDelimiterNode: (node: SyntaxNode) => boolean -): SyntaxNode | null { - let node = startNode.nextSibling; - - while (node != null) { - if (isDelimiterNode(node)) { - return node; - } - - node = node.nextSibling; - } - - return node; -} - -function getPreviousDelimiterNode( - startNode: SyntaxNode, - isDelimiterNode: (node: SyntaxNode) => boolean -): SyntaxNode | null { - let node = startNode.previousSibling; - - while (node != null) { - if (isDelimiterNode(node)) { - return node; - } - - node = node.previousSibling; - } - - return node; -} - export function delimitersSelector(...delimiters: string[]) { return delimitedSelector((node) => delimiters.includes(node.type), ", "); } @@ -351,56 +317,33 @@ export function delimitedSelector( getEndNode: (node: SyntaxNode) => SyntaxNode = identity ): SelectionExtractor { return (editor: TextEditor, node: SyntaxNode) => { + let leadingDelimiterRange: Range | undefined; + let trailingDelimiterRange: Range | undefined; const startNode = getStartNode(node); const endNode = getEndNode(node); - const leadingDelimiterRange = (() => { - const previousNonDelimiterNode = getPreviousNonDelimiterNode( - startNode, - isDelimiterNode - ); - if (previousNonDelimiterNode != null) { - return makeRangeFromPositions( - previousNonDelimiterNode.endPosition, - startNode.startPosition - ); - } - const previousDelimiterNode = getPreviousDelimiterNode( - startNode, - isDelimiterNode - ); - if (previousDelimiterNode != null) { - return makeRangeFromPositions( - previousDelimiterNode.startPosition, - startNode.startPosition - ); - } - return undefined; - })(); + const previousNonDelimiterNode = getPreviousNonDelimiterNode( + startNode, + isDelimiterNode + ); + const nextNonDelimiterNode = getNextNonDelimiterNode( + endNode, + isDelimiterNode + ); - const trailingDelimiterRange = (() => { - const nextNonDelimiterNode = getNextNonDelimiterNode( - startNode, - isDelimiterNode + if (previousNonDelimiterNode != null) { + leadingDelimiterRange = makeRangeFromPositions( + previousNonDelimiterNode.endPosition, + startNode.startPosition ); - if (nextNonDelimiterNode != null) { - return makeRangeFromPositions( - endNode.endPosition, - nextNonDelimiterNode.startPosition - ); - } - const nextDelimiterNode = getNextDelimiterNode( - startNode, - isDelimiterNode + } + + if (nextNonDelimiterNode != null) { + trailingDelimiterRange = makeRangeFromPositions( + endNode.endPosition, + nextNonDelimiterNode.startPosition ); - if (nextDelimiterNode != null) { - return makeRangeFromPositions( - endNode.endPosition, - nextDelimiterNode.endPosition - ); - } - return undefined; - })(); + } const containingListDelimiter = getInsertionDelimiter( editor, From 16a720c4b0bbbf5371e1797a9bf26fa8821f605d Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 30 Jun 2022 14:31:16 +0200 Subject: [PATCH 24/44] Don't expand for weak item target --- src/processTargets/modifiers/ItemStage/ItemStage.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/processTargets/modifiers/ItemStage/ItemStage.ts b/src/processTargets/modifiers/ItemStage/ItemStage.ts index 848fed888b..ddd2eac776 100644 --- a/src/processTargets/modifiers/ItemStage/ItemStage.ts +++ b/src/processTargets/modifiers/ItemStage/ItemStage.ts @@ -91,7 +91,10 @@ function getItemInfosForIterationScope( context: ProcessedTargetsContext, target: Target ) { - const { range, boundary } = getIterationScope(context, target); + // It's only for week targets we expand to iteration scope + const { range, boundary } = target.isWeak + ? getIterationScope(context, target) + : { range: target.contentRange, boundary: undefined }; return rangeToItemInfos(target.editor, range, boundary); } From b2ae923ade6d6735f8f75bcd37512917a25dd847 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 30 Jun 2022 14:45:26 +0200 Subject: [PATCH 25/44] Include pairs inside strings --- .../modifiers/ItemStage/getIterationScope.ts | 30 +++++++++++-------- .../recorded/itemTextual/clearItem7.yml | 26 ++++++++++++++++ .../recorded/itemTextual/clearItem8.yml | 26 ++++++++++++++++ 3 files changed, 70 insertions(+), 12 deletions(-) create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearItem7.yml create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearItem8.yml diff --git a/src/processTargets/modifiers/ItemStage/getIterationScope.ts b/src/processTargets/modifiers/ItemStage/getIterationScope.ts index 2c1dd67754..9b31104239 100644 --- a/src/processTargets/modifiers/ItemStage/getIterationScope.ts +++ b/src/processTargets/modifiers/ItemStage/getIterationScope.ts @@ -9,30 +9,36 @@ export function getIterationScope( context: ProcessedTargetsContext, target: Target ) { - // First check if we are in a string - let pairInfo = getStringSurroundingPair( + let pairInfo = getSurroundingPair( context, target.editor, target.contentRange ); - // We don't look for items inside strings. If we are in a string go to parent - pairInfo = - pairInfo != null - ? getParentSurroundingPair(context, target.editor, pairInfo) - : getSurroundingPair(context, target.editor, target.contentRange); - while (pairInfo != null) { // The selection from the beginning was this pair and we should not go into the interior but instead look in the parent. const isNotInterior = target.contentRange.isEqual(pairInfo.contentRange) || target.contentRange.start.isBeforeOrEqual(pairInfo.boundary[0].start) || target.contentRange.end.isAfterOrEqual(pairInfo.boundary[1].end); + if (!isNotInterior) { - return { - range: pairInfo.interiorRange, - boundary: pairInfo.boundary, - }; + const stringPairInfo = getStringSurroundingPair( + context, + target.editor, + pairInfo.contentRange + ); + + // We don't look for items inside strings. + if ( + stringPairInfo == null || + !stringPairInfo.contentRange.isEqual(pairInfo.contentRange) + ) { + return { + range: pairInfo.interiorRange, + boundary: pairInfo.boundary, + }; + } } pairInfo = getParentSurroundingPair(context, target.editor, pairInfo); } diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearItem7.yml b/src/test/suite/fixtures/recorded/itemTextual/clearItem7.yml new file mode 100644 index 0000000000..8ada6d822b --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearItem7.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear item + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: a, "foo(bar)baz", b + selections: + - anchor: {line: 0, character: 8} + active: {line: 0, character: 8} + marks: {} +finalState: + documentContents: a, "foo()baz", b + selections: + - anchor: {line: 0, character: 8} + active: {line: 0, character: 8} + thatMark: + - anchor: {line: 0, character: 8} + active: {line: 0, character: 8} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearItem8.yml b/src/test/suite/fixtures/recorded/itemTextual/clearItem8.yml new file mode 100644 index 0000000000..7371d7c2d8 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearItem8.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + spokenForm: clear item + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: a, "foo(bar)baz", b + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: a, , b + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} + thatMark: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] From 96e2dcb29f8e0dc2f4ce6ec177f11705afcdd438 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 30 Jun 2022 15:33:48 +0200 Subject: [PATCH 26/44] Cleanup --- .../modifiers/ItemStage/ItemStage.ts | 33 ++++++++++++------- .../itemTextual/clearEveryItemToken.yml | 28 ++++++++++++++++ 2 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearEveryItemToken.yml diff --git a/src/processTargets/modifiers/ItemStage/ItemStage.ts b/src/processTargets/modifiers/ItemStage/ItemStage.ts index ddd2eac776..7c295ddc1d 100644 --- a/src/processTargets/modifiers/ItemStage/ItemStage.ts +++ b/src/processTargets/modifiers/ItemStage/ItemStage.ts @@ -38,22 +38,24 @@ export default class ItemStage implements ModifierStage { private getEveryTarget(context: ProcessedTargetsContext, target: Target) { const itemInfos = getItemInfosForIterationScope(context, target); - if (itemInfos.length === 0) { + // If weak expand to all items in iteration scope + const filteredItemInfos = target.isWeak + ? itemInfos + : filterItemInfos(target, itemInfos).map((e) => e.itemInfo); + + if (filteredItemInfos.length === 0) { throw new NoContainingScopeError(this.modifier.scopeType.type); } - return itemInfos.map((itemInfo) => this.itemInfoToTarget(target, itemInfo)); + return filteredItemInfos.map((itemInfo) => + this.itemInfoToTarget(target, itemInfo) + ); } private getSingleTarget(context: ProcessedTargetsContext, target: Target) { const itemInfos = getItemInfosForIterationScope(context, target); - const itemInfoWithIntersections = itemInfos - .map((itemInfo) => ({ - itemInfo, - intersection: itemInfo.domain.intersection(target.contentRange), - })) - .filter((e) => e.intersection != null); + const itemInfoWithIntersections = filterItemInfos(target, itemInfos); if (itemInfoWithIntersections.length === 0) { throw new NoContainingScopeError(this.modifier.scopeType.type); @@ -87,14 +89,21 @@ export default class ItemStage implements ModifierStage { } } +/** Filter item infos by content range and domain intersection */ +function filterItemInfos(target: Target, itemInfos: ItemInfo[]) { + return itemInfos + .map((itemInfo) => ({ + itemInfo, + intersection: itemInfo.domain.intersection(target.contentRange), + })) + .filter((e) => e.intersection != null); +} + function getItemInfosForIterationScope( context: ProcessedTargetsContext, target: Target ) { - // It's only for week targets we expand to iteration scope - const { range, boundary } = target.isWeak - ? getIterationScope(context, target) - : { range: target.contentRange, boundary: undefined }; + const { range, boundary } = getIterationScope(context, target); return rangeToItemInfos(target.editor, range, boundary); } diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearEveryItemToken.yml b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItemToken.yml new file mode 100644 index 0000000000..bd23dfa8cf --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearEveryItemToken.yml @@ -0,0 +1,28 @@ +languageId: plaintext +command: + spokenForm: clear every item token + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: collectionItem} + - type: containingScope + scopeType: {type: token} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +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} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: collectionItem}}, {type: containingScope, scopeType: {type: token}}]}] From f72b201bb1d61f30f4b2d8d15bc981429ffa9863 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 30 Jun 2022 16:26:53 +0200 Subject: [PATCH 27/44] p --- .../modifiers/ItemStage/getIterationScope.ts | 17 +++++--- .../modifiers/surroundingPair/index.ts | 40 +++++++++++++++++-- .../recorded/itemTextual/takeItemBlueRisk.yml | 8 ++-- 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/processTargets/modifiers/ItemStage/getIterationScope.ts b/src/processTargets/modifiers/ItemStage/getIterationScope.ts index 9b31104239..49b23a05b1 100644 --- a/src/processTargets/modifiers/ItemStage/getIterationScope.ts +++ b/src/processTargets/modifiers/ItemStage/getIterationScope.ts @@ -2,7 +2,10 @@ import { Range, TextEditor } from "vscode"; import { Target } from "../../../typings/target.types"; import { ProcessedTargetsContext } from "../../../typings/Types"; import { fitRangeToLineContent } from "../scopeTypeStages/LineStage"; -import { processSurroundingPair } from "../surroundingPair"; +import { + processSurroundingPair, + processSurroundingPairForDelimiters, +} from "../surroundingPair"; import { SurroundingPairInfo } from "../surroundingPair/extractSelectionFromSurroundingPairOffsets"; export function getIterationScope( @@ -32,7 +35,7 @@ export function getIterationScope( // We don't look for items inside strings. if ( stringPairInfo == null || - !stringPairInfo.contentRange.isEqual(pairInfo.contentRange) + stringPairInfo.contentRange.start.isBefore(pairInfo.contentRange.start) ) { return { range: pairInfo.interiorRange, @@ -54,10 +57,12 @@ function getSurroundingPair( editor: TextEditor, contentRange: Range ) { - return processSurroundingPair(context, editor, contentRange, { - type: "surroundingPair", - delimiter: "any", - }); + return processSurroundingPairForDelimiters(context, editor, contentRange, [ + "parentheses", + "squareBrackets", + "curlyBrackets", + "angleBrackets", + ]); } function getParentSurroundingPair( diff --git a/src/processTargets/modifiers/surroundingPair/index.ts b/src/processTargets/modifiers/surroundingPair/index.ts index b86b154785..1c3819ff23 100644 --- a/src/processTargets/modifiers/surroundingPair/index.ts +++ b/src/processTargets/modifiers/surroundingPair/index.ts @@ -5,6 +5,8 @@ import getTextFragmentExtractor, { } from "../../../languages/getTextFragmentExtractor"; import { ComplexSurroundingPairName, + SimpleSurroundingPairName, + SurroundingPairDirection, SurroundingPairScopeType, } from "../../../typings/targetDescriptor.types"; import { ProcessedTargetsContext } from "../../../typings/Types"; @@ -32,10 +34,40 @@ export function processSurroundingPair( range: Range, modifier: SurroundingPairScopeType ): SurroundingPairInfo | null { - const document = editor.document; const delimiters = complexDelimiterMap[ modifier.delimiter as ComplexSurroundingPairName ] ?? [modifier.delimiter]; + return processSurroundingPairForDelimiters( + context, + editor, + range, + delimiters, + modifier.forceDirection + ); +} + +/** + * Applies the surrounding pair modifier to the given selection. First looks to + * see if the target is itself adjacent to or contained by a modifier token. If + * so it will expand the selection to the opposite delimiter token. Otherwise, + * or if the opposite token wasn't found, it will proceed by finding the + * smallest pair of delimiters which contains the selection. + * + * @param context Context to be leveraged by modifier + * @param selection The selection to process + * @param modifier The surrounding pair modifier information + * @param delimiters List of delimiter per names to use + * @returns The new selection expanded to the containing surrounding pair or + * `null` if none was found + */ +export function processSurroundingPairForDelimiters( + context: ProcessedTargetsContext, + editor: TextEditor, + range: Range, + delimiters: SimpleSurroundingPairName[], + forceDirection?: SurroundingPairDirection +): SurroundingPairInfo | null { + const document = editor.document; let node: SyntaxNode | null; let textFragmentExtractor: TextFragmentExtractor; @@ -53,7 +85,7 @@ export function processSurroundingPair( range, null, delimiters, - modifier.forceDirection + forceDirection ); } else { throw err; @@ -73,7 +105,7 @@ export function processSurroundingPair( range, textFragmentRange, delimiters, - modifier.forceDirection + forceDirection ); if (surroundingRange != null) { @@ -89,6 +121,6 @@ export function processSurroundingPair( range, node, delimiters, - modifier.forceDirection + forceDirection ); } diff --git a/src/test/suite/fixtures/recorded/itemTextual/takeItemBlueRisk.yml b/src/test/suite/fixtures/recorded/itemTextual/takeItemBlueRisk.yml index cad2bd0982..bfb208408f 100644 --- a/src/test/suite/fixtures/recorded/itemTextual/takeItemBlueRisk.yml +++ b/src/test/suite/fixtures/recorded/itemTextual/takeItemBlueRisk.yml @@ -38,9 +38,9 @@ finalState: u v, "foo(bar)baz", ] selections: - - anchor: {line: 6, character: 9} - active: {line: 6, character: 22} + - anchor: {line: 6, character: 14} + active: {line: 6, character: 17} thatMark: - - anchor: {line: 6, character: 9} - active: {line: 6, character: 22} + - anchor: {line: 6, character: 14} + active: {line: 6, character: 17} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: blue, character: r}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] From 8b441ada4a01a5390b5be03434e5ce47ab9084af Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Fri, 1 Jul 2022 09:29:58 +0200 Subject: [PATCH 28/44] Added item support to python argument list --- src/languages/python.ts | 35 ++++++++++++++----- .../languages/python/clearEveryItem.yml | 34 ++++++++++++++++++ 2 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 src/test/suite/fixtures/recorded/languages/python/clearEveryItem.yml diff --git a/src/languages/python.ts b/src/languages/python.ts index d6f31d88f3..08951b92b7 100644 --- a/src/languages/python.ts +++ b/src/languages/python.ts @@ -1,18 +1,22 @@ +import { Selection } from "vscode"; import { SyntaxNode } from "web-tree-sitter"; +import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; +import { NodeFinder, NodeMatcherAlternative } from "../typings/Types"; +import { argumentNodeFinder, patternFinder } from "../util/nodeFinders"; import { - createPatternMatchers, argumentMatcher, - leadingMatcher, - trailingMatcher, cascadingMatcher, - patternMatcher, conditionMatcher, + createPatternMatchers, + leadingMatcher, matcher, + patternMatcher, + trailingMatcher, } from "../util/nodeMatchers"; -import { patternFinder } from "../util/nodeFinders"; -import { NodeMatcherAlternative } from "../typings/Types"; -import { SimpleScopeTypeType } from "../typings/targetDescriptor.types"; -import { childRangeSelector } from "../util/nodeSelectors"; +import { + argumentSelectionExtractor, + childRangeSelector, +} from "../util/nodeSelectors"; // Generated by the following command: // > curl https://raw.githubusercontent.com/tree-sitter/tree-sitter-python/d6210ceab11e8d812d4ab59c07c81458ec6e5184/src/node-types.json \ @@ -49,6 +53,20 @@ export const getTypeNode = (node: SyntaxNode) => const dictionaryTypes = ["dictionary", "dictionary_comprehension"]; const listTypes = ["list", "list_comprehension", "set"]; +function importNodeFinder(): NodeFinder { + const finder = argumentNodeFinder("import_from_statement"); + return (node: SyntaxNode, selection?: Selection) => { + const childNode = finder(node, selection); + if ( + childNode?.type !== "dotted_name" || + childNode?.id === childNode?.parent?.namedChildren[0].id + ) { + return null; + } + return childNode; + }; +} + const nodeMatchers: Partial< Record > = { @@ -56,6 +74,7 @@ const nodeMatchers: Partial< list: listTypes, statement: STATEMENT_TYPES, string: "string", + collectionItem: matcher(importNodeFinder(), argumentSelectionExtractor()), collectionKey: trailingMatcher(["pair[key]"], [":"]), ifStatement: "if_statement", anonymousFunction: "lambda?.lambda", diff --git a/src/test/suite/fixtures/recorded/languages/python/clearEveryItem.yml b/src/test/suite/fixtures/recorded/languages/python/clearEveryItem.yml new file mode 100644 index 0000000000..15fa41ed76 --- /dev/null +++ b/src/test/suite/fixtures/recorded/languages/python/clearEveryItem.yml @@ -0,0 +1,34 @@ +languageId: python +command: + spokenForm: clear every item + version: 2 + targets: + - type: primitive + modifiers: + - type: everyScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: from talon import Context, Module, actions + selections: + - anchor: {line: 0, character: 25} + active: {line: 0, character: 25} + marks: {} +finalState: + documentContents: "from talon import , , " + selections: + - anchor: {line: 0, character: 18} + active: {line: 0, character: 18} + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + - anchor: {line: 0, character: 22} + active: {line: 0, character: 22} + thatMark: + - anchor: {line: 0, character: 18} + active: {line: 0, character: 18} + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} + - anchor: {line: 0, character: 22} + active: {line: 0, character: 22} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: collectionItem}}]}] From 2ab20f687daccd2495da8f03a888005e39492d41 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 4 Jul 2022 14:24:05 +0200 Subject: [PATCH 29/44] Use new requireStrongContainment --- .../modifiers/ItemStage/getIterationScope.ts | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/processTargets/modifiers/ItemStage/getIterationScope.ts b/src/processTargets/modifiers/ItemStage/getIterationScope.ts index eb3f9199e4..7d813ea086 100644 --- a/src/processTargets/modifiers/ItemStage/getIterationScope.ts +++ b/src/processTargets/modifiers/ItemStage/getIterationScope.ts @@ -19,30 +19,23 @@ export function getIterationScope( ); while (pairInfo != null) { - // The selection from the beginning was this pair and we should not go into the interior but instead look in the parent. - const isNotInterior = - target.contentRange.isEqual(pairInfo.contentRange) || - target.contentRange.start.isBeforeOrEqual(pairInfo.boundary[0].start) || - target.contentRange.end.isAfterOrEqual(pairInfo.boundary[1].end); + const stringPairInfo = getStringSurroundingPair( + context, + target.editor, + pairInfo.contentRange + ); - if (!isNotInterior) { - const stringPairInfo = getStringSurroundingPair( - context, - target.editor, - pairInfo.contentRange - ); - - // We don't look for items inside strings. - if ( - stringPairInfo == null || - stringPairInfo.contentRange.start.isBefore(pairInfo.contentRange.start) - ) { - return { - range: pairInfo.interiorRange, - boundary: pairInfo.boundary, - }; - } + // We don't look for items inside strings. + if ( + stringPairInfo == null || + stringPairInfo.contentRange.start.isBefore(pairInfo.contentRange.start) + ) { + return { + range: pairInfo.interiorRange, + boundary: pairInfo.boundary, + }; } + pairInfo = getParentSurroundingPair(context, target.editor, pairInfo); } @@ -61,7 +54,11 @@ function getSurroundingPair( context, editor, contentRange, - { type: "surroundingPair", delimiter: "any" }, + { + type: "surroundingPair", + delimiter: "any", + requireStrongContainment: true, + }, ["parentheses", "squareBrackets", "curlyBrackets", "angleBrackets"] ); } @@ -89,5 +86,6 @@ function getStringSurroundingPair( return processSurroundingPair(context, editor, contentRange, { type: "surroundingPair", delimiter: "string", + requireStrongContainment: true, }); } From 022efb15a6b94ecbef32f7ecbcc6f2b188ac03ef Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 4 Jul 2022 14:29:48 +0200 Subject: [PATCH 30/44] Cleanup --- .../modifiers/ItemStage/getIterationScope.ts | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/processTargets/modifiers/ItemStage/getIterationScope.ts b/src/processTargets/modifiers/ItemStage/getIterationScope.ts index 7d813ea086..540beec6ae 100644 --- a/src/processTargets/modifiers/ItemStage/getIterationScope.ts +++ b/src/processTargets/modifiers/ItemStage/getIterationScope.ts @@ -27,7 +27,9 @@ export function getIterationScope( // We don't look for items inside strings. if ( + // Not in a string stringPairInfo == null || + // In a non-string surrounding pair that is inside a surrounding string. This is fine. stringPairInfo.contentRange.start.isBefore(pairInfo.contentRange.start) ) { return { @@ -39,12 +41,27 @@ export function getIterationScope( pairInfo = getParentSurroundingPair(context, target.editor, pairInfo); } - // We have not found a pair containing the delimiter. Look at the full line. + // We have not found a surrounding pair. Use the line. return { range: fitRangeToLineContent(target.editor, target.contentRange), }; } +function getParentSurroundingPair( + context: ProcessedTargetsContext, + editor: TextEditor, + pairInfo: SurroundingPairInfo +) { + // Step out of this pair and see if we have a parent + const position = editor.document.positionAt( + editor.document.offsetAt(pairInfo.contentRange.start) - 1 + ); + if (position.isEqual(pairInfo.contentRange.start)) { + return null; + } + return getSurroundingPair(context, editor, new Range(position, position)); +} + function getSurroundingPair( context: ProcessedTargetsContext, editor: TextEditor, @@ -63,21 +80,6 @@ function getSurroundingPair( ); } -function getParentSurroundingPair( - context: ProcessedTargetsContext, - editor: TextEditor, - pairInfo: SurroundingPairInfo -) { - // Step out of this pair and see if we have a parent - const position = editor.document.positionAt( - editor.document.offsetAt(pairInfo.contentRange.start) - 1 - ); - if (position.isEqual(pairInfo.contentRange.start)) { - return null; - } - return getSurroundingPair(context, editor, new Range(position, position)); -} - function getStringSurroundingPair( context: ProcessedTargetsContext, editor: TextEditor, From 9db3da3c21f93a5414542d9cf8c4a779b9d6e25e Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 4 Jul 2022 17:45:41 +0200 Subject: [PATCH 31/44] Cleanup --- .../modifiers/ItemStage/ItemStage.ts | 14 ++++++------- .../modifiers/ItemStage/getIterationScope.ts | 20 +++++++++++++------ .../modifiers/ItemStage/tokenizeRange.ts | 18 ++++++++++++++--- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/processTargets/modifiers/ItemStage/ItemStage.ts b/src/processTargets/modifiers/ItemStage/ItemStage.ts index 7c295ddc1d..9f3d488e0e 100644 --- a/src/processTargets/modifiers/ItemStage/ItemStage.ts +++ b/src/processTargets/modifiers/ItemStage/ItemStage.ts @@ -116,14 +116,14 @@ function rangeToItemInfos( const itemInfos: ItemInfo[] = []; tokens.forEach((token, i) => { - if (token.type === "delimiter" || token.type === "boundary") { + 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 === "delimiter") { + if (tokens[i - 1]?.type === "separator") { return new Range(tokens[i - 1].range.start, token.range.start); } return undefined; @@ -132,21 +132,21 @@ function rangeToItemInfos( if (tokens[i + 2]?.type === "item") { return new Range(token.range.end, tokens[i + 2].range.start); } - if (tokens[i + 1]?.type === "delimiter") { + if (tokens[i + 1]?.type === "separator") { return new Range(token.range.end, tokens[i + 1].range.end); } return undefined; })(); - // Leading boundary is excluded and leading delimiter is included + // Leading boundary is excluded and leading separator is included const leadingMatchStart = tokens[i - 1]?.type === "boundary" ? tokens[i - 1].range.end - : tokens[i - 1]?.type === "delimiter" + : tokens[i - 1]?.type === "separator" ? tokens[i - 1].range.start : token.range.start; - // Trailing boundary and delimiter is excluded + // Trailing boundary and separator is excluded const trailingMatchEnd = - tokens[i + 1]?.type === "boundary" || tokens[i + 1]?.type === "delimiter" + tokens[i + 1]?.type === "boundary" || tokens[i + 1]?.type === "separator" ? tokens[i + 1].range.start : token.range.end; const matchRange = new Range(leadingMatchStart, trailingMatchEnd); diff --git a/src/processTargets/modifiers/ItemStage/getIterationScope.ts b/src/processTargets/modifiers/ItemStage/getIterationScope.ts index 540beec6ae..c78a8246c6 100644 --- a/src/processTargets/modifiers/ItemStage/getIterationScope.ts +++ b/src/processTargets/modifiers/ItemStage/getIterationScope.ts @@ -8,16 +8,24 @@ import { } from "../surroundingPair"; import { SurroundingPairInfo } from "../surroundingPair/extractSelectionFromSurroundingPairOffsets"; +/** + * 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( context: ProcessedTargetsContext, target: Target -) { +): { range: Range; boundary?: [Range, Range] } { let pairInfo = getSurroundingPair( context, target.editor, target.contentRange ); + // Iteration is necessary in case of nested strings while (pairInfo != null) { const stringPairInfo = getStringSurroundingPair( context, @@ -52,13 +60,13 @@ function getParentSurroundingPair( editor: TextEditor, pairInfo: SurroundingPairInfo ) { - // Step out of this pair and see if we have a parent - const position = editor.document.positionAt( - editor.document.offsetAt(pairInfo.contentRange.start) - 1 - ); - if (position.isEqual(pairInfo.contentRange.start)) { + const startOffset = editor.document.offsetAt(pairInfo.contentRange.start); + // Can't have a parent; already at start of document + if (startOffset === 0) { return null; } + // Step out of this pair and see if we have a parent + const position = editor.document.positionAt(startOffset - 1); return getSurroundingPair(context, editor, new Range(position, position)); } diff --git a/src/processTargets/modifiers/ItemStage/tokenizeRange.ts b/src/processTargets/modifiers/ItemStage/tokenizeRange.ts index 04a94f6f57..4046ba9921 100644 --- a/src/processTargets/modifiers/ItemStage/tokenizeRange.ts +++ b/src/processTargets/modifiers/ItemStage/tokenizeRange.ts @@ -1,10 +1,17 @@ import { Range, TextEditor } from "vscode"; +/** + * Takes the range for a collection and returns a list of tokens within that collection + * @param editor The editor containing the range + * @param collectionRange The range to look for tokens within + * @param collectionBoundary Optional boundaries for collections. [], {} + * @returns List of tokens + */ export function tokenizeRange( editor: TextEditor, collectionRange: Range, collectionBoundary?: [Range, Range] -) { +): Token[] { const { document } = editor; const text = document.getText(collectionRange); const lexemes = text.split(/([,(){}<>[\]"'`]|\\"|\\'|\\`)/g).filter(Boolean); @@ -22,7 +29,7 @@ export function tokenizeRange( // Separator delimiter found. if (lexeme === delimiter) { tokens.push({ - type: "delimiter", + type: "separator", range: new Range( document.positionAt(offset), document.positionAt(offset + lexeme.length) @@ -56,6 +63,11 @@ export function tokenizeRange( return tokens; } +/** + * Takes a list of lexemes and joins them by skipping matching pairs (), {}, etc + * @param lexemes List of lexemes to operate on + * @returns List of lexemes with equal or less length then {@link lexemes} + */ export function joinLexemesBySkippingMatchingPairs(lexemes: string[]) { const result: string[] = []; let delimiterCount = 0; @@ -125,5 +137,5 @@ const delimiters: { [key: string]: string } = { interface Token { range: Range; - type: string; + type: "item" | "separator" | "boundary"; } From 95acb520abd9134c683b4e52a4b055ff703a6832 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 4 Jul 2022 18:09:57 +0200 Subject: [PATCH 32/44] Added complex surrounding pair scope type --- .../modifiers/ItemStage/getIterationScope.ts | 21 +++++----- .../findDelimiterPairAdjacentToSelection.ts | 4 +- .../findSurroundingPairCore.ts | 13 ++----- .../findSurroundingPairParseTreeBased.ts | 20 ++-------- .../findSurroundingPairTextBased.ts | 25 +++--------- .../modifiers/surroundingPair/index.ts | 38 ++++++------------- src/typings/targetDescriptor.types.ts | 9 +++++ 7 files changed, 46 insertions(+), 84 deletions(-) diff --git a/src/processTargets/modifiers/ItemStage/getIterationScope.ts b/src/processTargets/modifiers/ItemStage/getIterationScope.ts index c78a8246c6..068370fca4 100644 --- a/src/processTargets/modifiers/ItemStage/getIterationScope.ts +++ b/src/processTargets/modifiers/ItemStage/getIterationScope.ts @@ -75,17 +75,16 @@ function getSurroundingPair( editor: TextEditor, contentRange: Range ) { - return processSurroundingPairForDelimiters( - context, - editor, - contentRange, - { - type: "surroundingPair", - delimiter: "any", - requireStrongContainment: true, - }, - ["parentheses", "squareBrackets", "curlyBrackets", "angleBrackets"] - ); + return processSurroundingPairForDelimiters(context, editor, contentRange, { + type: "surroundingPair", + requireStrongContainment: true, + delimiters: [ + "parentheses", + "squareBrackets", + "curlyBrackets", + "angleBrackets", + ], + }); } function getStringSurroundingPair( diff --git a/src/processTargets/modifiers/surroundingPair/findDelimiterPairAdjacentToSelection.ts b/src/processTargets/modifiers/surroundingPair/findDelimiterPairAdjacentToSelection.ts index 64f8e3f246..4c1d344767 100644 --- a/src/processTargets/modifiers/surroundingPair/findDelimiterPairAdjacentToSelection.ts +++ b/src/processTargets/modifiers/surroundingPair/findDelimiterPairAdjacentToSelection.ts @@ -1,4 +1,4 @@ -import { SurroundingPairScopeType } from "../../../typings/targetDescriptor.types"; +import { SurroundingPairComplexScopeType } from "../../../typings/targetDescriptor.types"; import { findOppositeDelimiter } from "./findOppositeDelimiter"; import { getSurroundingPairOffsets } from "./getSurroundingPairOffsets"; import { @@ -30,7 +30,7 @@ export function findDelimiterPairAdjacentToSelection( initialIndex: number, delimiterOccurrences: PossibleDelimiterOccurrence[], selectionOffsets: Offsets, - scopeType: SurroundingPairScopeType, + scopeType: SurroundingPairComplexScopeType, bailOnUnmatchedAdjacent: boolean = false ): SurroundingPairOffsets | null { const indicesToTry = [initialIndex + 1, initialIndex]; diff --git a/src/processTargets/modifiers/surroundingPair/findSurroundingPairCore.ts b/src/processTargets/modifiers/surroundingPair/findSurroundingPairCore.ts index 6a0708aa4b..064fe05401 100644 --- a/src/processTargets/modifiers/surroundingPair/findSurroundingPairCore.ts +++ b/src/processTargets/modifiers/surroundingPair/findSurroundingPairCore.ts @@ -1,14 +1,11 @@ import { sortedIndexBy } from "lodash"; -import { - SimpleSurroundingPairName, - SurroundingPairScopeType, -} from "../../../typings/targetDescriptor.types"; +import { SurroundingPairComplexScopeType } from "../../../typings/targetDescriptor.types"; import { findDelimiterPairAdjacentToSelection } from "./findDelimiterPairAdjacentToSelection"; import { findDelimiterPairContainingSelection } from "./findDelimiterPairContainingSelection"; import { - SurroundingPairOffsets, Offsets, PossibleDelimiterOccurrence, + SurroundingPairOffsets, } from "./types"; /** @@ -23,7 +20,6 @@ import { * delimiter pair that contains the selection. * * @param delimiterOccurrences A list of delimiter occurrences. Expected to be sorted by offsets - * @param acceptableDelimiters A list of names of acceptable delimiters to look for * @param selectionOffsets The offsets of the selection * @param bailOnUnmatchedAdjacent If `true`, immediately return null if we find * an adjacent delimiter that we can't find a match for. This variable will @@ -33,9 +29,8 @@ import { * @returns */ export function findSurroundingPairCore( - scopeType: SurroundingPairScopeType, + scopeType: SurroundingPairComplexScopeType, delimiterOccurrences: PossibleDelimiterOccurrence[], - acceptableDelimiters: SimpleSurroundingPairName[], selectionOffsets: Offsets, bailOnUnmatchedAdjacent: boolean = false ): SurroundingPairOffsets | null { @@ -72,7 +67,7 @@ export function findSurroundingPairCore( return findDelimiterPairContainingSelection( initialIndex, delimiterOccurrences, - acceptableDelimiters, + scopeType.delimiters, selectionOffsets ); } diff --git a/src/processTargets/modifiers/surroundingPair/findSurroundingPairParseTreeBased.ts b/src/processTargets/modifiers/surroundingPair/findSurroundingPairParseTreeBased.ts index 8968e5dc41..30b2063e79 100644 --- a/src/processTargets/modifiers/surroundingPair/findSurroundingPairParseTreeBased.ts +++ b/src/processTargets/modifiers/surroundingPair/findSurroundingPairParseTreeBased.ts @@ -1,9 +1,6 @@ import { Range, TextDocument, TextEditor } from "vscode"; import { SyntaxNode } from "web-tree-sitter"; -import { - SimpleSurroundingPairName, - SurroundingPairScopeType, -} from "../../../typings/targetDescriptor.types"; +import { SurroundingPairComplexScopeType } from "../../../typings/targetDescriptor.types"; import { getNodeRange } from "../../../util/nodeSelectors"; import { isContainedInErrorNode } from "../../../util/treeSitterUtils"; import { extractSelectionFromSurroundingPairOffsets } from "./extractSelectionFromSurroundingPairOffsets"; @@ -61,12 +58,11 @@ export function findSurroundingPairParseTreeBased( editor: TextEditor, selection: Range, node: SyntaxNode, - delimiters: SimpleSurroundingPairName[], - scopeType: SurroundingPairScopeType + scopeType: SurroundingPairComplexScopeType ) { const document: TextDocument = editor.document; - const individualDelimiters = getIndividualDelimiters(delimiters); + const individualDelimiters = getIndividualDelimiters(scopeType.delimiters); const delimiterTextToDelimiterInfoMap = Object.fromEntries( individualDelimiters.map((individualDelimiter) => [ @@ -86,7 +82,6 @@ export function findSurroundingPairParseTreeBased( const context: Context = { delimiterTextToDelimiterInfoMap, individualDelimiters, - delimiters, selectionOffsets, scopeType, }; @@ -139,17 +134,12 @@ interface Context { */ individualDelimiters: IndividualDelimiter[]; - /** - * The names of the delimiters that we're considering - */ - delimiters: SimpleSurroundingPairName[]; - /** * The offsets of the selection */ selectionOffsets: Offsets; - scopeType: SurroundingPairScopeType; + scopeType: SurroundingPairComplexScopeType; } /** @@ -168,7 +158,6 @@ function findSurroundingPairContainedInNode( const { delimiterTextToDelimiterInfoMap, individualDelimiters, - delimiters, selectionOffsets, scopeType, } = context; @@ -229,7 +218,6 @@ function findSurroundingPairContainedInNode( return findSurroundingPairCore( scopeType, delimiterOccurrences, - delimiters, selectionOffsets, // If we're not the root node of the parse tree (ie `node.parent != diff --git a/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts b/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts index f395354898..343d3342bf 100644 --- a/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts +++ b/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts @@ -1,9 +1,8 @@ import { escapeRegExp, findLast, uniq } from "lodash"; import { Range, TextDocument, TextEditor } from "vscode"; import { - SimpleSurroundingPairName, + SurroundingPairComplexScopeType, SurroundingPairName, - SurroundingPairScopeType, } from "../../../typings/targetDescriptor.types"; import { getDocumentRange } from "../../../util/range"; import { matchAll } from "../../../util/regex"; @@ -69,13 +68,12 @@ export function findSurroundingPairTextBased( editor: TextEditor, range: Range, allowableRange: Range | null, - delimiters: SimpleSurroundingPairName[], - scopeType: SurroundingPairScopeType + scopeType: SurroundingPairComplexScopeType ) { const document: TextDocument = editor.document; const fullRange = allowableRange ?? getDocumentRange(document); - const individualDelimiters = getIndividualDelimiters(delimiters); + const individualDelimiters = getIndividualDelimiters(scopeType.delimiters); const delimiterTextToDelimiterInfoMap = Object.fromEntries( individualDelimiters.map((individualDelimiter) => [ @@ -108,7 +106,6 @@ export function findSurroundingPairTextBased( const context: Context = { scopeType, delimiterRegex, - delimiters, delimiterTextToDelimiterInfoMap, }; @@ -196,16 +193,11 @@ function getDelimiterRegex(individualDelimiters: IndividualDelimiter[]) { * Context to pass to nested call */ interface Context { - scopeType: SurroundingPairScopeType; + scopeType: SurroundingPairComplexScopeType; delimiterTextToDelimiterInfoMap: { [k: string]: IndividualDelimiter; }; delimiterRegex: RegExp; - - /** - * The allowable delimiter names - */ - delimiters: SimpleSurroundingPairName[]; } /** @@ -228,12 +220,8 @@ function getDelimiterPairOffsets( isAtStartOfFullRange: boolean, isAtEndOfFullRange: boolean ): SurroundingPairOffsets | null { - const { - scopeType, - delimiterTextToDelimiterInfoMap, - delimiterRegex, - delimiters, - } = context; + const { scopeType, delimiterTextToDelimiterInfoMap, delimiterRegex } = + context; const { forceDirection } = scopeType; // XXX: The below is a bit wasteful when there are multiple targets, because @@ -293,7 +281,6 @@ function getDelimiterPairOffsets( const surroundingPair = findSurroundingPairCore( scopeType, delimiterOccurrences, - delimiters, selectionOffsets, !isAtStartOfFullRange || !isAtEndOfFullRange ); diff --git a/src/processTargets/modifiers/surroundingPair/index.ts b/src/processTargets/modifiers/surroundingPair/index.ts index da853264a6..a8d924bcc7 100644 --- a/src/processTargets/modifiers/surroundingPair/index.ts +++ b/src/processTargets/modifiers/surroundingPair/index.ts @@ -5,7 +5,7 @@ import getTextFragmentExtractor, { } from "../../../languages/getTextFragmentExtractor"; import { ComplexSurroundingPairName, - SimpleSurroundingPairName, + SurroundingPairComplexScopeType, SurroundingPairScopeType, } from "../../../typings/targetDescriptor.types"; import { ProcessedTargetsContext } from "../../../typings/Types"; @@ -34,16 +34,14 @@ export function processSurroundingPair( range: Range, scopeType: SurroundingPairScopeType ): SurroundingPairInfo | null { + const { delimiter, ...rest } = scopeType; const delimiters = complexDelimiterMap[ - scopeType.delimiter as ComplexSurroundingPairName - ] ?? [scopeType.delimiter]; - return processSurroundingPairForDelimiters( - context, - editor, - range, - scopeType, - delimiters - ); + delimiter as ComplexSurroundingPairName + ] ?? [delimiter]; + return processSurroundingPairForDelimiters(context, editor, range, { + ...rest, + delimiters, + }); } /** @@ -64,8 +62,7 @@ export function processSurroundingPairForDelimiters( context: ProcessedTargetsContext, editor: TextEditor, range: Range, - scopeType: SurroundingPairScopeType, - delimiters: SimpleSurroundingPairName[] + scopeType: SurroundingPairComplexScopeType ): SurroundingPairInfo | null { const document = editor.document; let node: SyntaxNode | null; @@ -79,13 +76,7 @@ export function processSurroundingPairForDelimiters( if ((err as Error).name === "UnsupportedLanguageError") { // If we're in a language where we don't have a parse tree we use the text // based algorithm - return findSurroundingPairTextBased( - editor, - range, - null, - delimiters, - scopeType - ); + return findSurroundingPairTextBased(editor, range, null, scopeType); } else { throw err; } @@ -103,7 +94,6 @@ export function processSurroundingPairForDelimiters( editor, range, textFragmentRange, - delimiters, scopeType ); @@ -115,11 +105,5 @@ export function processSurroundingPairForDelimiters( // If we have a parse tree and either we are not in a string or comment or we // couldn't find a surrounding pair within a string or comment, we use the // parse tree-based algorithm - return findSurroundingPairParseTreeBased( - editor, - range, - node, - delimiters, - scopeType - ); + return findSurroundingPairParseTreeBased(editor, range, node, scopeType); } diff --git a/src/typings/targetDescriptor.types.ts b/src/typings/targetDescriptor.types.ts index cc95e9ab3f..e3b6e8f5f7 100644 --- a/src/typings/targetDescriptor.types.ts +++ b/src/typings/targetDescriptor.types.ts @@ -129,6 +129,15 @@ export interface SurroundingPairScopeType { requireStrongContainment?: boolean; } +/** The same as `SurroundingPairScopeType` but with multiple delimiters */ +export interface SurroundingPairComplexScopeType + extends Omit { + /** + * The allowable delimiter names + */ + delimiters: SimpleSurroundingPairName[]; +} + export type ScopeType = SimpleScopeType | SurroundingPairScopeType; export interface ContainingSurroundingPairModifier From 61368436a5a0d24881955be381a5aa67c6d073d4 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 4 Jul 2022 17:17:48 +0100 Subject: [PATCH 33/44] Tweaks to #709 (#823) * Tweaks to text-based item PR * More tweaks and tests * More doc string Co-authored-by: Andreas Arvidsson --- .../modifiers/ItemStage/ItemStage.ts | 22 +++-- .../modifiers/ItemStage/tokenizeRange.ts | 95 +++++++++++-------- .../recorded/itemTextual/clearItem10.yml | 26 +++++ .../recorded/itemTextual/clearItem11.yml | 26 +++++ .../recorded/itemTextual/clearItem9.yml | 26 +++++ .../recorded/itemTextual/clearItemDrip.yml | 30 ++++++ 6 files changed, 178 insertions(+), 47 deletions(-) create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearItem10.yml create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearItem11.yml create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearItem9.yml create mode 100644 src/test/suite/fixtures/recorded/itemTextual/clearItemDrip.yml diff --git a/src/processTargets/modifiers/ItemStage/ItemStage.ts b/src/processTargets/modifiers/ItemStage/ItemStage.ts index 9f3d488e0e..abba0d9fa7 100644 --- a/src/processTargets/modifiers/ItemStage/ItemStage.ts +++ b/src/processTargets/modifiers/ItemStage/ItemStage.ts @@ -104,21 +104,22 @@ function getItemInfosForIterationScope( target: Target ) { const { range, boundary } = getIterationScope(context, target); - return rangeToItemInfos(target.editor, range, boundary); + return getItemsInRange(target.editor, range, boundary); } -function rangeToItemInfos( +function getItemsInRange( editor: TextEditor, - collectionRange: Range, - collectionBoundary?: [Range, Range] + interior: Range, + boundary?: [Range, Range] ): ItemInfo[] { - const tokens = tokenizeRange(editor, collectionRange, collectionBoundary); + 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); @@ -128,6 +129,7 @@ function rangeToItemInfos( } return undefined; })(); + const trailingDelimiterRange = (() => { if (tokens[i + 2]?.type === "item") { return new Range(token.range.end, tokens[i + 2].range.start); @@ -137,24 +139,26 @@ function rangeToItemInfos( } return undefined; })(); + // Leading boundary is excluded and leading separator is included - const leadingMatchStart = + const domainStart = tokens[i - 1]?.type === "boundary" ? tokens[i - 1].range.end : tokens[i - 1]?.type === "separator" ? tokens[i - 1].range.start : token.range.start; + // Trailing boundary and separator is excluded - const trailingMatchEnd = + const domainEnd = tokens[i + 1]?.type === "boundary" || tokens[i + 1]?.type === "separator" ? tokens[i + 1].range.start : token.range.end; - const matchRange = new Range(leadingMatchStart, trailingMatchEnd); + itemInfos.push({ contentRange: token.range, leadingDelimiterRange, trailingDelimiterRange, - domain: matchRange, + domain: new Range(domainStart, domainEnd), }); }); diff --git a/src/processTargets/modifiers/ItemStage/tokenizeRange.ts b/src/processTargets/modifiers/ItemStage/tokenizeRange.ts index 4046ba9921..2f31dd3123 100644 --- a/src/processTargets/modifiers/ItemStage/tokenizeRange.ts +++ b/src/processTargets/modifiers/ItemStage/tokenizeRange.ts @@ -3,21 +3,30 @@ import { Range, TextEditor } from "vscode"; /** * Takes the range for a collection and returns a list of tokens within that collection * @param editor The editor containing the range - * @param collectionRange The range to look for tokens within - * @param collectionBoundary Optional boundaries for collections. [], {} + * @param interior The range to look for tokens within + * @param boundary Optional boundaries for collections. [], {} * @returns List of tokens */ export function tokenizeRange( editor: TextEditor, - collectionRange: Range, - collectionBoundary?: [Range, Range] + interior: Range, + boundary?: [Range, Range] ): Token[] { const { document } = editor; - const text = document.getText(collectionRange); - const lexemes = text.split(/([,(){}<>[\]"'`]|\\"|\\'|\\`)/g).filter(Boolean); + 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(/([,(){}<>[\]"'`]|\\"|\\'|\\`)/g) + .filter((lexeme) => lexeme.length > 0); const joinedLexemes = joinLexemesBySkippingMatchingPairs(lexemes); const tokens: Token[] = []; - let offset = document.offsetAt(collectionRange.start); + let offset = document.offsetAt(interior.start); joinedLexemes.forEach((lexeme) => { // Whitespace found. Just skip @@ -27,7 +36,7 @@ export function tokenizeRange( } // Separator delimiter found. - if (lexeme === delimiter) { + if (lexeme === separator) { tokens.push({ type: "separator", range: new Range( @@ -52,11 +61,11 @@ export function tokenizeRange( offset += lexeme.length; }); - if (collectionBoundary != null) { + if (boundary != null) { return [ - { type: "boundary", range: collectionBoundary[0] }, + { type: "boundary", range: boundary[0] }, ...tokens, - { type: "boundary", range: collectionBoundary[1] }, + { type: "boundary", range: boundary[1] }, ]; } @@ -70,48 +79,58 @@ export function tokenizeRange( */ export function joinLexemesBySkippingMatchingPairs(lexemes: string[]) { const result: string[] = []; - let delimiterCount = 0; + /** + * 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) => { - // We are waiting for a closing delimiter - if (delimiterCount > 0) { - // Closing delimiter found - if (closingDelimiter === lexeme) { - --delimiterCount; - } - // Additional opening delimiter found - else if (openingDelimiter === lexeme) { - ++delimiterCount; + if (delimiterBalance > 0) { + // We are waiting for a closing delimiter + + if (lexeme === closingDelimiter) { + // Closing delimiter found + --delimiterBalance; + } else if (lexeme === openingDelimiter) { + // Additional opening delimiter found + ++delimiterBalance; } + + return; } - // Starting delimiter found - else if (delimiters[lexeme] != null) { + if (leftToRightMap[lexeme] != null) { + // Starting delimiter found openingDelimiter = lexeme; - closingDelimiter = delimiters[lexeme]; - delimiterCount = 1; - // This is the first lexeme to be joined - if (startIndex < 0) { - startIndex = index; - } + closingDelimiter = leftToRightMap[lexeme]; + delimiterBalance = 1; } - // This is the first lexeme to be joined - else if (startIndex < 0) { + if (startIndex < 0) { + // This is the first lexeme to be joined startIndex = index; } - const isDelimiter = lexeme === delimiter && delimiterCount === 0; + const isSeparator = lexeme === separator && delimiterBalance === 0; - // This is the last lexeme to be joined - if (isDelimiter || index === lexemes.length - 1) { - const endIndex = isDelimiter ? index : index + 1; + 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 (isDelimiter) { + if (isSeparator) { + // Add the separator itself result.push(lexeme); } } @@ -120,11 +139,11 @@ export function joinLexemesBySkippingMatchingPairs(lexemes: string[]) { return result; } -const delimiter = ","; +const separator = ","; // Mapping between opening and closing delimiters /* eslint-disable @typescript-eslint/naming-convention */ -const delimiters: { [key: string]: string } = { +const leftToRightMap: { [key: string]: string } = { "(": ")", "{": "}", "<": ">", diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearItem10.yml b/src/test/suite/fixtures/recorded/itemTextual/clearItem10.yml new file mode 100644 index 0000000000..1ee4b4228f --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearItem10.yml @@ -0,0 +1,26 @@ +languageId: typescript +command: + spokenForm: clear item + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: false + action: {name: clearAndSetSelection} +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} + thatMark: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearItem11.yml b/src/test/suite/fixtures/recorded/itemTextual/clearItem11.yml new file mode 100644 index 0000000000..9b4e5e253d --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearItem11.yml @@ -0,0 +1,26 @@ +languageId: typescript +command: + spokenForm: clear item + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: false + action: {name: clearAndSetSelection} +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} + thatMark: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearItem9.yml b/src/test/suite/fixtures/recorded/itemTextual/clearItem9.yml new file mode 100644 index 0000000000..c6197799f0 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearItem9.yml @@ -0,0 +1,26 @@ +languageId: typescript +command: + spokenForm: clear item + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: foo(hello, world) + selections: + - anchor: {line: 0, character: 9} + active: {line: 0, character: 9} + marks: {} +finalState: + documentContents: foo(, world) + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + thatMark: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearItemDrip.yml b/src/test/suite/fixtures/recorded/itemTextual/clearItemDrip.yml new file mode 100644 index 0000000000..1a8972fcc6 --- /dev/null +++ b/src/test/suite/fixtures/recorded/itemTextual/clearItemDrip.yml @@ -0,0 +1,30 @@ +languageId: typescript +command: + spokenForm: clear item drip + version: 2 + targets: + - type: primitive + modifiers: + - type: containingScope + scopeType: {type: collectionItem} + mark: {type: decoratedSymbol, symbolColor: default, character: ','} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +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} + thatMark: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: ','}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] From c1d2bfb652ca3649c29f9dbfad8df77eef2e62bd Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 4 Jul 2022 19:40:21 +0200 Subject: [PATCH 34/44] Revert "Added complex surrounding pair scope type" This reverts commit 95acb520abd9134c683b4e52a4b055ff703a6832. --- .../modifiers/ItemStage/getIterationScope.ts | 21 +++++----- .../findDelimiterPairAdjacentToSelection.ts | 4 +- .../findSurroundingPairCore.ts | 13 +++++-- .../findSurroundingPairParseTreeBased.ts | 20 ++++++++-- .../findSurroundingPairTextBased.ts | 25 +++++++++--- .../modifiers/surroundingPair/index.ts | 38 +++++++++++++------ src/typings/targetDescriptor.types.ts | 9 ----- 7 files changed, 84 insertions(+), 46 deletions(-) diff --git a/src/processTargets/modifiers/ItemStage/getIterationScope.ts b/src/processTargets/modifiers/ItemStage/getIterationScope.ts index 068370fca4..c78a8246c6 100644 --- a/src/processTargets/modifiers/ItemStage/getIterationScope.ts +++ b/src/processTargets/modifiers/ItemStage/getIterationScope.ts @@ -75,16 +75,17 @@ function getSurroundingPair( editor: TextEditor, contentRange: Range ) { - return processSurroundingPairForDelimiters(context, editor, contentRange, { - type: "surroundingPair", - requireStrongContainment: true, - delimiters: [ - "parentheses", - "squareBrackets", - "curlyBrackets", - "angleBrackets", - ], - }); + return processSurroundingPairForDelimiters( + context, + editor, + contentRange, + { + type: "surroundingPair", + delimiter: "any", + requireStrongContainment: true, + }, + ["parentheses", "squareBrackets", "curlyBrackets", "angleBrackets"] + ); } function getStringSurroundingPair( diff --git a/src/processTargets/modifiers/surroundingPair/findDelimiterPairAdjacentToSelection.ts b/src/processTargets/modifiers/surroundingPair/findDelimiterPairAdjacentToSelection.ts index 4c1d344767..64f8e3f246 100644 --- a/src/processTargets/modifiers/surroundingPair/findDelimiterPairAdjacentToSelection.ts +++ b/src/processTargets/modifiers/surroundingPair/findDelimiterPairAdjacentToSelection.ts @@ -1,4 +1,4 @@ -import { SurroundingPairComplexScopeType } from "../../../typings/targetDescriptor.types"; +import { SurroundingPairScopeType } from "../../../typings/targetDescriptor.types"; import { findOppositeDelimiter } from "./findOppositeDelimiter"; import { getSurroundingPairOffsets } from "./getSurroundingPairOffsets"; import { @@ -30,7 +30,7 @@ export function findDelimiterPairAdjacentToSelection( initialIndex: number, delimiterOccurrences: PossibleDelimiterOccurrence[], selectionOffsets: Offsets, - scopeType: SurroundingPairComplexScopeType, + scopeType: SurroundingPairScopeType, bailOnUnmatchedAdjacent: boolean = false ): SurroundingPairOffsets | null { const indicesToTry = [initialIndex + 1, initialIndex]; diff --git a/src/processTargets/modifiers/surroundingPair/findSurroundingPairCore.ts b/src/processTargets/modifiers/surroundingPair/findSurroundingPairCore.ts index 064fe05401..6a0708aa4b 100644 --- a/src/processTargets/modifiers/surroundingPair/findSurroundingPairCore.ts +++ b/src/processTargets/modifiers/surroundingPair/findSurroundingPairCore.ts @@ -1,11 +1,14 @@ import { sortedIndexBy } from "lodash"; -import { SurroundingPairComplexScopeType } from "../../../typings/targetDescriptor.types"; +import { + SimpleSurroundingPairName, + SurroundingPairScopeType, +} from "../../../typings/targetDescriptor.types"; import { findDelimiterPairAdjacentToSelection } from "./findDelimiterPairAdjacentToSelection"; import { findDelimiterPairContainingSelection } from "./findDelimiterPairContainingSelection"; import { + SurroundingPairOffsets, Offsets, PossibleDelimiterOccurrence, - SurroundingPairOffsets, } from "./types"; /** @@ -20,6 +23,7 @@ import { * delimiter pair that contains the selection. * * @param delimiterOccurrences A list of delimiter occurrences. Expected to be sorted by offsets + * @param acceptableDelimiters A list of names of acceptable delimiters to look for * @param selectionOffsets The offsets of the selection * @param bailOnUnmatchedAdjacent If `true`, immediately return null if we find * an adjacent delimiter that we can't find a match for. This variable will @@ -29,8 +33,9 @@ import { * @returns */ export function findSurroundingPairCore( - scopeType: SurroundingPairComplexScopeType, + scopeType: SurroundingPairScopeType, delimiterOccurrences: PossibleDelimiterOccurrence[], + acceptableDelimiters: SimpleSurroundingPairName[], selectionOffsets: Offsets, bailOnUnmatchedAdjacent: boolean = false ): SurroundingPairOffsets | null { @@ -67,7 +72,7 @@ export function findSurroundingPairCore( return findDelimiterPairContainingSelection( initialIndex, delimiterOccurrences, - scopeType.delimiters, + acceptableDelimiters, selectionOffsets ); } diff --git a/src/processTargets/modifiers/surroundingPair/findSurroundingPairParseTreeBased.ts b/src/processTargets/modifiers/surroundingPair/findSurroundingPairParseTreeBased.ts index 30b2063e79..8968e5dc41 100644 --- a/src/processTargets/modifiers/surroundingPair/findSurroundingPairParseTreeBased.ts +++ b/src/processTargets/modifiers/surroundingPair/findSurroundingPairParseTreeBased.ts @@ -1,6 +1,9 @@ import { Range, TextDocument, TextEditor } from "vscode"; import { SyntaxNode } from "web-tree-sitter"; -import { SurroundingPairComplexScopeType } from "../../../typings/targetDescriptor.types"; +import { + SimpleSurroundingPairName, + SurroundingPairScopeType, +} from "../../../typings/targetDescriptor.types"; import { getNodeRange } from "../../../util/nodeSelectors"; import { isContainedInErrorNode } from "../../../util/treeSitterUtils"; import { extractSelectionFromSurroundingPairOffsets } from "./extractSelectionFromSurroundingPairOffsets"; @@ -58,11 +61,12 @@ export function findSurroundingPairParseTreeBased( editor: TextEditor, selection: Range, node: SyntaxNode, - scopeType: SurroundingPairComplexScopeType + delimiters: SimpleSurroundingPairName[], + scopeType: SurroundingPairScopeType ) { const document: TextDocument = editor.document; - const individualDelimiters = getIndividualDelimiters(scopeType.delimiters); + const individualDelimiters = getIndividualDelimiters(delimiters); const delimiterTextToDelimiterInfoMap = Object.fromEntries( individualDelimiters.map((individualDelimiter) => [ @@ -82,6 +86,7 @@ export function findSurroundingPairParseTreeBased( const context: Context = { delimiterTextToDelimiterInfoMap, individualDelimiters, + delimiters, selectionOffsets, scopeType, }; @@ -134,12 +139,17 @@ interface Context { */ individualDelimiters: IndividualDelimiter[]; + /** + * The names of the delimiters that we're considering + */ + delimiters: SimpleSurroundingPairName[]; + /** * The offsets of the selection */ selectionOffsets: Offsets; - scopeType: SurroundingPairComplexScopeType; + scopeType: SurroundingPairScopeType; } /** @@ -158,6 +168,7 @@ function findSurroundingPairContainedInNode( const { delimiterTextToDelimiterInfoMap, individualDelimiters, + delimiters, selectionOffsets, scopeType, } = context; @@ -218,6 +229,7 @@ function findSurroundingPairContainedInNode( return findSurroundingPairCore( scopeType, delimiterOccurrences, + delimiters, selectionOffsets, // If we're not the root node of the parse tree (ie `node.parent != diff --git a/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts b/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts index 343d3342bf..f395354898 100644 --- a/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts +++ b/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts @@ -1,8 +1,9 @@ import { escapeRegExp, findLast, uniq } from "lodash"; import { Range, TextDocument, TextEditor } from "vscode"; import { - SurroundingPairComplexScopeType, + SimpleSurroundingPairName, SurroundingPairName, + SurroundingPairScopeType, } from "../../../typings/targetDescriptor.types"; import { getDocumentRange } from "../../../util/range"; import { matchAll } from "../../../util/regex"; @@ -68,12 +69,13 @@ export function findSurroundingPairTextBased( editor: TextEditor, range: Range, allowableRange: Range | null, - scopeType: SurroundingPairComplexScopeType + delimiters: SimpleSurroundingPairName[], + scopeType: SurroundingPairScopeType ) { const document: TextDocument = editor.document; const fullRange = allowableRange ?? getDocumentRange(document); - const individualDelimiters = getIndividualDelimiters(scopeType.delimiters); + const individualDelimiters = getIndividualDelimiters(delimiters); const delimiterTextToDelimiterInfoMap = Object.fromEntries( individualDelimiters.map((individualDelimiter) => [ @@ -106,6 +108,7 @@ export function findSurroundingPairTextBased( const context: Context = { scopeType, delimiterRegex, + delimiters, delimiterTextToDelimiterInfoMap, }; @@ -193,11 +196,16 @@ function getDelimiterRegex(individualDelimiters: IndividualDelimiter[]) { * Context to pass to nested call */ interface Context { - scopeType: SurroundingPairComplexScopeType; + scopeType: SurroundingPairScopeType; delimiterTextToDelimiterInfoMap: { [k: string]: IndividualDelimiter; }; delimiterRegex: RegExp; + + /** + * The allowable delimiter names + */ + delimiters: SimpleSurroundingPairName[]; } /** @@ -220,8 +228,12 @@ function getDelimiterPairOffsets( isAtStartOfFullRange: boolean, isAtEndOfFullRange: boolean ): SurroundingPairOffsets | null { - const { scopeType, delimiterTextToDelimiterInfoMap, delimiterRegex } = - context; + const { + scopeType, + delimiterTextToDelimiterInfoMap, + delimiterRegex, + delimiters, + } = context; const { forceDirection } = scopeType; // XXX: The below is a bit wasteful when there are multiple targets, because @@ -281,6 +293,7 @@ function getDelimiterPairOffsets( const surroundingPair = findSurroundingPairCore( scopeType, delimiterOccurrences, + delimiters, selectionOffsets, !isAtStartOfFullRange || !isAtEndOfFullRange ); diff --git a/src/processTargets/modifiers/surroundingPair/index.ts b/src/processTargets/modifiers/surroundingPair/index.ts index a8d924bcc7..da853264a6 100644 --- a/src/processTargets/modifiers/surroundingPair/index.ts +++ b/src/processTargets/modifiers/surroundingPair/index.ts @@ -5,7 +5,7 @@ import getTextFragmentExtractor, { } from "../../../languages/getTextFragmentExtractor"; import { ComplexSurroundingPairName, - SurroundingPairComplexScopeType, + SimpleSurroundingPairName, SurroundingPairScopeType, } from "../../../typings/targetDescriptor.types"; import { ProcessedTargetsContext } from "../../../typings/Types"; @@ -34,14 +34,16 @@ export function processSurroundingPair( range: Range, scopeType: SurroundingPairScopeType ): SurroundingPairInfo | null { - const { delimiter, ...rest } = scopeType; const delimiters = complexDelimiterMap[ - delimiter as ComplexSurroundingPairName - ] ?? [delimiter]; - return processSurroundingPairForDelimiters(context, editor, range, { - ...rest, - delimiters, - }); + scopeType.delimiter as ComplexSurroundingPairName + ] ?? [scopeType.delimiter]; + return processSurroundingPairForDelimiters( + context, + editor, + range, + scopeType, + delimiters + ); } /** @@ -62,7 +64,8 @@ export function processSurroundingPairForDelimiters( context: ProcessedTargetsContext, editor: TextEditor, range: Range, - scopeType: SurroundingPairComplexScopeType + scopeType: SurroundingPairScopeType, + delimiters: SimpleSurroundingPairName[] ): SurroundingPairInfo | null { const document = editor.document; let node: SyntaxNode | null; @@ -76,7 +79,13 @@ export function processSurroundingPairForDelimiters( if ((err as Error).name === "UnsupportedLanguageError") { // If we're in a language where we don't have a parse tree we use the text // based algorithm - return findSurroundingPairTextBased(editor, range, null, scopeType); + return findSurroundingPairTextBased( + editor, + range, + null, + delimiters, + scopeType + ); } else { throw err; } @@ -94,6 +103,7 @@ export function processSurroundingPairForDelimiters( editor, range, textFragmentRange, + delimiters, scopeType ); @@ -105,5 +115,11 @@ export function processSurroundingPairForDelimiters( // If we have a parse tree and either we are not in a string or comment or we // couldn't find a surrounding pair within a string or comment, we use the // parse tree-based algorithm - return findSurroundingPairParseTreeBased(editor, range, node, scopeType); + return findSurroundingPairParseTreeBased( + editor, + range, + node, + delimiters, + scopeType + ); } diff --git a/src/typings/targetDescriptor.types.ts b/src/typings/targetDescriptor.types.ts index e3b6e8f5f7..cc95e9ab3f 100644 --- a/src/typings/targetDescriptor.types.ts +++ b/src/typings/targetDescriptor.types.ts @@ -129,15 +129,6 @@ export interface SurroundingPairScopeType { requireStrongContainment?: boolean; } -/** The same as `SurroundingPairScopeType` but with multiple delimiters */ -export interface SurroundingPairComplexScopeType - extends Omit { - /** - * The allowable delimiter names - */ - delimiters: SimpleSurroundingPairName[]; -} - export type ScopeType = SimpleScopeType | SurroundingPairScopeType; export interface ContainingSurroundingPairModifier From 7c7f3aaadb4bc70a605661e069418b3bd0202379 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 4 Jul 2022 19:46:26 +0200 Subject: [PATCH 35/44] Cleanup --- .../modifiers/ItemStage/getIterationScope.ts | 21 ++++-------- .../surroundingPair/delimiterMaps.ts | 6 ++++ .../modifiers/surroundingPair/index.ts | 32 +------------------ src/typings/targetDescriptor.types.ts | 5 ++- 4 files changed, 17 insertions(+), 47 deletions(-) diff --git a/src/processTargets/modifiers/ItemStage/getIterationScope.ts b/src/processTargets/modifiers/ItemStage/getIterationScope.ts index c78a8246c6..a01fd33075 100644 --- a/src/processTargets/modifiers/ItemStage/getIterationScope.ts +++ b/src/processTargets/modifiers/ItemStage/getIterationScope.ts @@ -2,10 +2,7 @@ import { Range, TextEditor } from "vscode"; import { Target } from "../../../typings/target.types"; import { ProcessedTargetsContext } from "../../../typings/Types"; import { fitRangeToLineContent } from "../scopeTypeStages/LineStage"; -import { - processSurroundingPair, - processSurroundingPairForDelimiters, -} from "../surroundingPair"; +import { processSurroundingPair } from "../surroundingPair"; import { SurroundingPairInfo } from "../surroundingPair/extractSelectionFromSurroundingPairOffsets"; /** @@ -75,17 +72,11 @@ function getSurroundingPair( editor: TextEditor, contentRange: Range ) { - return processSurroundingPairForDelimiters( - context, - editor, - contentRange, - { - type: "surroundingPair", - delimiter: "any", - requireStrongContainment: true, - }, - ["parentheses", "squareBrackets", "curlyBrackets", "angleBrackets"] - ); + return processSurroundingPair(context, editor, contentRange, { + type: "surroundingPair", + delimiter: "collectionBoundary", + requireStrongContainment: true, + }); } function getStringSurroundingPair( diff --git a/src/processTargets/modifiers/surroundingPair/delimiterMaps.ts b/src/processTargets/modifiers/surroundingPair/delimiterMaps.ts index b0d69574c9..a51e7e7723 100644 --- a/src/processTargets/modifiers/surroundingPair/delimiterMaps.ts +++ b/src/processTargets/modifiers/surroundingPair/delimiterMaps.ts @@ -39,4 +39,10 @@ export const complexDelimiterMap: Record< > = { any: Object.keys(delimiterToText), string: ["singleQuotes", "doubleQuotes", "backtickQuotes"], + collectionBoundary: [ + "parentheses", + "squareBrackets", + "curlyBrackets", + "angleBrackets", + ], }; diff --git a/src/processTargets/modifiers/surroundingPair/index.ts b/src/processTargets/modifiers/surroundingPair/index.ts index da853264a6..93a66ad986 100644 --- a/src/processTargets/modifiers/surroundingPair/index.ts +++ b/src/processTargets/modifiers/surroundingPair/index.ts @@ -5,7 +5,6 @@ import getTextFragmentExtractor, { } from "../../../languages/getTextFragmentExtractor"; import { ComplexSurroundingPairName, - SimpleSurroundingPairName, SurroundingPairScopeType, } from "../../../typings/targetDescriptor.types"; import { ProcessedTargetsContext } from "../../../typings/Types"; @@ -34,40 +33,11 @@ export function processSurroundingPair( range: Range, scopeType: SurroundingPairScopeType ): SurroundingPairInfo | null { + const document = editor.document; const delimiters = complexDelimiterMap[ scopeType.delimiter as ComplexSurroundingPairName ] ?? [scopeType.delimiter]; - return processSurroundingPairForDelimiters( - context, - editor, - range, - scopeType, - delimiters - ); -} -/** - * Applies the surrounding pair modifier to the given selection. First looks to - * see if the target is itself adjacent to or contained by a modifier token. If - * so it will expand the selection to the opposite delimiter token. Otherwise, - * or if the opposite token wasn't found, it will proceed by finding the - * smallest pair of delimiters which contains the selection. - * - * @param context Context to be leveraged by modifier - * @param selection The selection to process - * @param modifier The surrounding pair modifier information - * @param delimiters List of delimiter per names to use - * @returns The new selection expanded to the containing surrounding pair or - * `null` if none was found - */ -export function processSurroundingPairForDelimiters( - context: ProcessedTargetsContext, - editor: TextEditor, - range: Range, - scopeType: SurroundingPairScopeType, - delimiters: SimpleSurroundingPairName[] -): SurroundingPairInfo | null { - const document = editor.document; let node: SyntaxNode | null; let textFragmentExtractor: TextFragmentExtractor; diff --git a/src/typings/targetDescriptor.types.ts b/src/typings/targetDescriptor.types.ts index cc95e9ab3f..7c6919b27a 100644 --- a/src/typings/targetDescriptor.types.ts +++ b/src/typings/targetDescriptor.types.ts @@ -60,7 +60,10 @@ export type SimpleSurroundingPairName = | "parentheses" | "singleQuotes" | "squareBrackets"; -export type ComplexSurroundingPairName = "string" | "any"; +export type ComplexSurroundingPairName = + | "string" + | "any" + | "collectionBoundary"; export type SurroundingPairName = | SimpleSurroundingPairName | ComplexSurroundingPairName; From 7969637c97a59501e9140670336117283574c94e Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 4 Jul 2022 19:55:26 +0200 Subject: [PATCH 36/44] Create single union of items --- .../modifiers/ItemStage/ItemStage.ts | 34 +++++++++---------- src/util/rangeUtils.ts | 6 ---- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/processTargets/modifiers/ItemStage/ItemStage.ts b/src/processTargets/modifiers/ItemStage/ItemStage.ts index abba0d9fa7..4cd66081d6 100644 --- a/src/processTargets/modifiers/ItemStage/ItemStage.ts +++ b/src/processTargets/modifiers/ItemStage/ItemStage.ts @@ -8,7 +8,6 @@ import { } from "../../../typings/targetDescriptor.types"; import { ProcessedTargetsContext } from "../../../typings/Types"; import { getInsertionDelimiter } from "../../../util/nodeSelectors"; -import { rangeLength } from "../../../util/rangeUtils"; import { ModifierStage } from "../../PipelineStages.types"; import ScopeTypeTarget from "../../targets/ScopeTypeTarget"; import ContainingSyntaxScopeStage, { @@ -41,7 +40,7 @@ export default class ItemStage implements ModifierStage { // If weak expand to all items in iteration scope const filteredItemInfos = target.isWeak ? itemInfos - : filterItemInfos(target, itemInfos).map((e) => e.itemInfo); + : filterItemInfos(target, itemInfos); if (filteredItemInfos.length === 0) { throw new NoContainingScopeError(this.modifier.scopeType.type); @@ -55,19 +54,23 @@ export default class ItemStage implements ModifierStage { private getSingleTarget(context: ProcessedTargetsContext, target: Target) { const itemInfos = getItemInfosForIterationScope(context, target); - const itemInfoWithIntersections = filterItemInfos(target, itemInfos); + const filteredItemInfos = filterItemInfos(target, itemInfos); - if (itemInfoWithIntersections.length === 0) { + if (filteredItemInfos.length === 0) { throw new NoContainingScopeError(this.modifier.scopeType.type); } - itemInfoWithIntersections.sort( - (a, b) => - rangeLength(target.editor, b.intersection!) - - rangeLength(target.editor, a.intersection!) - ); + 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, + }; - return this.itemInfoToTarget(target, itemInfoWithIntersections[0].itemInfo); + return this.itemInfoToTarget(target, itemInfo); } private itemInfoToTarget(target: Target, itemInfo: ItemInfo) { @@ -90,13 +93,10 @@ export default class ItemStage implements ModifierStage { } /** Filter item infos by content range and domain intersection */ -function filterItemInfos(target: Target, itemInfos: ItemInfo[]) { - return itemInfos - .map((itemInfo) => ({ - itemInfo, - intersection: itemInfo.domain.intersection(target.contentRange), - })) - .filter((e) => e.intersection != null); +function filterItemInfos(target: Target, itemInfos: ItemInfo[]): ItemInfo[] { + return itemInfos.filter( + (itemInfo) => itemInfo.domain.intersection(target.contentRange) != null + ); } function getItemInfosForIterationScope( diff --git a/src/util/rangeUtils.ts b/src/util/rangeUtils.ts index a2629ff1c2..a812180221 100644 --- a/src/util/rangeUtils.ts +++ b/src/util/rangeUtils.ts @@ -28,9 +28,3 @@ export function expandToFullLine(editor: TextEditor, range: Range) { export function makeEmptyRange(position: Position) { return new Range(position, position); } - -export function rangeLength(editor: TextEditor, range: Range) { - return ( - editor.document.offsetAt(range.end) - editor.document.offsetAt(range.start) - ); -} From 6d074a68dacb81ee5e1b8ac10bb8951a1f3c9b91 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 4 Jul 2022 20:20:09 +0200 Subject: [PATCH 37/44] Excluded separator from domain --- src/processTargets/modifiers/ItemStage/ItemStage.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/processTargets/modifiers/ItemStage/ItemStage.ts b/src/processTargets/modifiers/ItemStage/ItemStage.ts index 4cd66081d6..bbf3cf4212 100644 --- a/src/processTargets/modifiers/ItemStage/ItemStage.ts +++ b/src/processTargets/modifiers/ItemStage/ItemStage.ts @@ -140,15 +140,13 @@ function getItemsInRange( return undefined; })(); - // Leading boundary is excluded and leading separator is included + // Leading boundary and separator are excluded const domainStart = - tokens[i - 1]?.type === "boundary" + tokens[i - 1]?.type === "boundary" || tokens[i - 1]?.type === "separator" ? tokens[i - 1].range.end - : tokens[i - 1]?.type === "separator" - ? tokens[i - 1].range.start : token.range.start; - // Trailing boundary and separator is excluded + // Trailing boundary and separator are excluded const domainEnd = tokens[i + 1]?.type === "boundary" || tokens[i + 1]?.type === "separator" ? tokens[i + 1].range.start From 7b14de43af68ac55b35e954b9bb7d23b8c1912a9 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 4 Jul 2022 20:53:12 +0200 Subject: [PATCH 38/44] Cleanup --- .../modifiers/ItemStage/tokenizeRange.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/processTargets/modifiers/ItemStage/tokenizeRange.ts b/src/processTargets/modifiers/ItemStage/tokenizeRange.ts index 2f31dd3123..b27a6fea2d 100644 --- a/src/processTargets/modifiers/ItemStage/tokenizeRange.ts +++ b/src/processTargets/modifiers/ItemStage/tokenizeRange.ts @@ -98,27 +98,29 @@ export function joinLexemesBySkippingMatchingPairs(lexemes: string[]) { lexemes.forEach((lexeme, index) => { if (delimiterBalance > 0) { // We are waiting for a closing delimiter - if (lexeme === closingDelimiter) { // Closing delimiter found --delimiterBalance; - } else if (lexeme === openingDelimiter) { - // Additional opening delimiter found + } + // Additional opening delimiter found + else if (lexeme === openingDelimiter) { ++delimiterBalance; } - - return; } - if (leftToRightMap[lexeme] != null) { - // Starting delimiter found + // Starting delimiter found + else if (leftToRightMap[lexeme] != null) { openingDelimiter = lexeme; closingDelimiter = leftToRightMap[lexeme]; delimiterBalance = 1; + if (startIndex < 0) { + // This is the first lexeme to be joined + startIndex = index; + } } - if (startIndex < 0) { - // This is the first lexeme to be joined + // This is the first lexeme to be joined + else if (startIndex < 0) { startIndex = index; } From 30d9c11ee6318f74e9059a4eb568c951e51bb79c Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 4 Jul 2022 21:58:33 +0200 Subject: [PATCH 39/44] Updated tests --- src/test/suite/fixtures/recorded/itemTextual/clearItem11.yml | 4 ++-- .../suite/fixtures/recorded/itemTextual/clearItemDrip.yml | 4 ++-- .../fixtures/recorded/languages/typescript/takeItemComma.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearItem11.yml b/src/test/suite/fixtures/recorded/itemTextual/clearItem11.yml index 9b4e5e253d..b363279425 100644 --- a/src/test/suite/fixtures/recorded/itemTextual/clearItem11.yml +++ b/src/test/suite/fixtures/recorded/itemTextual/clearItem11.yml @@ -21,6 +21,6 @@ finalState: - anchor: {line: 0, character: 4} active: {line: 0, character: 4} thatMark: - - anchor: {line: 0, character: 11} - active: {line: 0, character: 11} + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/itemTextual/clearItemDrip.yml b/src/test/suite/fixtures/recorded/itemTextual/clearItemDrip.yml index 1a8972fcc6..3ae86e27a4 100644 --- a/src/test/suite/fixtures/recorded/itemTextual/clearItemDrip.yml +++ b/src/test/suite/fixtures/recorded/itemTextual/clearItemDrip.yml @@ -25,6 +25,6 @@ finalState: - anchor: {line: 0, character: 4} active: {line: 0, character: 4} thatMark: - - anchor: {line: 0, character: 11} - active: {line: 0, character: 11} + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: ','}, modifiers: [{type: containingScope, scopeType: {type: collectionItem}}]}] diff --git a/src/test/suite/fixtures/recorded/languages/typescript/takeItemComma.yml b/src/test/suite/fixtures/recorded/languages/typescript/takeItemComma.yml index 95ee786767..de76509599 100644 --- a/src/test/suite/fixtures/recorded/languages/typescript/takeItemComma.yml +++ b/src/test/suite/fixtures/recorded/languages/typescript/takeItemComma.yml @@ -23,9 +23,9 @@ finalState: const value = { a: 1, b: 2, c: 3 }; selections: - - anchor: {line: 1, character: 22} + - anchor: {line: 1, character: 16} active: {line: 1, character: 26} thatMark: - - anchor: {line: 1, character: 22} + - anchor: {line: 1, character: 16} active: {line: 1, character: 26} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: ','}, selectionType: token, position: contents, modifier: {type: containingScope, scopeType: collectionItem}, insideOutsideType: inside}] From 9f701755d0aef66cf86e58a378bf597f13aa4ffa Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 5 Jul 2022 13:27:23 +0200 Subject: [PATCH 40/44] Update src/processTargets/modifiers/ItemStage/tokenizeRange.ts Co-authored-by: Pokey Rule <755842+pokey@users.noreply.github.com> --- src/processTargets/modifiers/ItemStage/tokenizeRange.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/processTargets/modifiers/ItemStage/tokenizeRange.ts b/src/processTargets/modifiers/ItemStage/tokenizeRange.ts index b27a6fea2d..58104a1dc1 100644 --- a/src/processTargets/modifiers/ItemStage/tokenizeRange.ts +++ b/src/processTargets/modifiers/ItemStage/tokenizeRange.ts @@ -75,7 +75,7 @@ export function tokenizeRange( /** * Takes a list of lexemes and joins them by skipping matching pairs (), {}, etc * @param lexemes List of lexemes to operate on - * @returns List of lexemes with equal or less length then {@link lexemes} + * @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[] = []; From 9a65fcffa25c1adfc37dc326ed5fe7c22c373ed3 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 5 Jul 2022 13:27:45 +0200 Subject: [PATCH 41/44] Update src/processTargets/modifiers/ItemStage/tokenizeRange.ts Co-authored-by: Pokey Rule <755842+pokey@users.noreply.github.com> --- src/processTargets/modifiers/ItemStage/tokenizeRange.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/processTargets/modifiers/ItemStage/tokenizeRange.ts b/src/processTargets/modifiers/ItemStage/tokenizeRange.ts index 58104a1dc1..30ed5b4e30 100644 --- a/src/processTargets/modifiers/ItemStage/tokenizeRange.ts +++ b/src/processTargets/modifiers/ItemStage/tokenizeRange.ts @@ -73,7 +73,7 @@ export function tokenizeRange( } /** - * Takes a list of lexemes and joins them by skipping matching pairs (), {}, etc + * 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} */ From 456c905e131d16d1dd7028b01d8d90541447fb80 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 5 Jul 2022 13:37:44 +0200 Subject: [PATCH 42/44] Update src/languages/python.ts Co-authored-by: Pokey Rule <755842+pokey@users.noreply.github.com> --- src/languages/python.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/languages/python.ts b/src/languages/python.ts index 08951b92b7..0f15d6b813 100644 --- a/src/languages/python.ts +++ b/src/languages/python.ts @@ -58,12 +58,13 @@ function importNodeFinder(): NodeFinder { return (node: SyntaxNode, selection?: Selection) => { const childNode = finder(node, selection); if ( - childNode?.type !== "dotted_name" || - childNode?.id === childNode?.parent?.namedChildren[0].id + childNode?.type === "dotted_name" && + !childNode.equals(childNode.parent.firstNamedChild) ) { - return null; + return childNode; } - return childNode; + + return null; }; } From 1d4ff8d8df82e11f6ebdd021e347d11bed1d4b3d Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 5 Jul 2022 13:54:25 +0200 Subject: [PATCH 43/44] Cleanup --- src/languages/python.ts | 2 +- src/processTargets/modifiers/ItemStage/tokenizeRange.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/languages/python.ts b/src/languages/python.ts index 0f15d6b813..c8fb1c34dd 100644 --- a/src/languages/python.ts +++ b/src/languages/python.ts @@ -59,7 +59,7 @@ function importNodeFinder(): NodeFinder { const childNode = finder(node, selection); if ( childNode?.type === "dotted_name" && - !childNode.equals(childNode.parent.firstNamedChild) + childNode.id !== childNode?.parent?.firstNamedChild?.id ) { return childNode; } diff --git a/src/processTargets/modifiers/ItemStage/tokenizeRange.ts b/src/processTargets/modifiers/ItemStage/tokenizeRange.ts index 30ed5b4e30..3237b23f4c 100644 --- a/src/processTargets/modifiers/ItemStage/tokenizeRange.ts +++ b/src/processTargets/modifiers/ItemStage/tokenizeRange.ts @@ -2,6 +2,8 @@ import { Range, TextEditor } from "vscode"; /** * Takes the range for a collection and returns a list of tokens within that collection + * A token is just an item which has a range and a type [item, separator, boundary] + * `[{range, type: "item"}, {range, type: "separator"}]` * @param editor The editor containing the range * @param interior The range to look for tokens within * @param boundary Optional boundaries for collections. [], {} From 0791c3e8de28bf57865347daf3f16439ce866d5f Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 5 Jul 2022 16:34:44 +0100 Subject: [PATCH 44/44] Tweaks --- src/languages/python.ts | 2 +- .../modifiers/ItemStage/tokenizeRange.ts | 23 ++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/languages/python.ts b/src/languages/python.ts index c8fb1c34dd..9c636b58f8 100644 --- a/src/languages/python.ts +++ b/src/languages/python.ts @@ -59,7 +59,7 @@ function importNodeFinder(): NodeFinder { const childNode = finder(node, selection); if ( childNode?.type === "dotted_name" && - childNode.id !== childNode?.parent?.firstNamedChild?.id + childNode.id !== childNode.parent?.firstNamedChild?.id ) { return childNode; } diff --git a/src/processTargets/modifiers/ItemStage/tokenizeRange.ts b/src/processTargets/modifiers/ItemStage/tokenizeRange.ts index 3237b23f4c..52186aa22e 100644 --- a/src/processTargets/modifiers/ItemStage/tokenizeRange.ts +++ b/src/processTargets/modifiers/ItemStage/tokenizeRange.ts @@ -1,9 +1,26 @@ import { Range, TextEditor } from "vscode"; /** - * Takes the range for a collection and returns a list of tokens within that collection - * A token is just an item which has a range and a type [item, separator, boundary] - * `[{range, type: "item"}, {range, type: "separator"}]` + * 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. [], {}