From b6b22f87f6dc7cbe0f96bb802a37ba7773d467a4 Mon Sep 17 00:00:00 2001 From: Pokey Rule Date: Tue, 7 Dec 2021 15:34:33 +0000 Subject: [PATCH] Add rewrap action (#365) * Initial attempt * Initial working version * Add tests * Add another test Co-authored-by: Andreas Arvidsson --- images/squareRepackHarp.gif | Bin 0 -> 16855 bytes src/actions/Rewrap.ts | 65 ++++++++++++++ src/actions/index.ts | 2 + ...ractSelectionFromSurroundingPairOffsets.ts | 83 ++++++++++-------- src/processTargets/processSelectionType.ts | 2 +- .../recorded/actions/curlyRepackRound.yml | 38 ++++++++ .../recorded/actions/squareRepackHarp.yml | 31 +++++++ .../recorded/actions/squareRepackLeper.yml | 31 +++++++ .../recorded/actions/squareRepackPair.yml | 28 ++++++ .../recorded/actions/squareRepackThis.yml | 26 ++++++ .../surroundingPair/textual/chuckPairHarp.yml | 29 ++++++ src/typings/Types.ts | 15 ++++ src/util/array.ts | 11 +++ 13 files changed, 322 insertions(+), 39 deletions(-) create mode 100644 images/squareRepackHarp.gif create mode 100644 src/actions/Rewrap.ts create mode 100644 src/test/suite/fixtures/recorded/actions/curlyRepackRound.yml create mode 100644 src/test/suite/fixtures/recorded/actions/squareRepackHarp.yml create mode 100644 src/test/suite/fixtures/recorded/actions/squareRepackLeper.yml create mode 100644 src/test/suite/fixtures/recorded/actions/squareRepackPair.yml create mode 100644 src/test/suite/fixtures/recorded/actions/squareRepackThis.yml create mode 100644 src/test/suite/fixtures/recorded/surroundingPair/textual/chuckPairHarp.yml create mode 100644 src/util/array.ts diff --git a/images/squareRepackHarp.gif b/images/squareRepackHarp.gif new file mode 100644 index 0000000000000000000000000000000000000000..dbc5cec57374040aed036802be574e7ba09b4b31 GIT binary patch literal 16855 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 Tt#bcPhJVivApdt;tz`ZO>c?Co literal 0 HcmV?d00001 diff --git a/src/actions/Rewrap.ts b/src/actions/Rewrap.ts new file mode 100644 index 0000000000..475fa471f9 --- /dev/null +++ b/src/actions/Rewrap.ts @@ -0,0 +1,65 @@ +import { TextEditor } from "vscode"; +import { + Action, + ActionPreferences, + ActionReturnValue, + Graph, + SelectionWithContext, + TypedSelection, +} from "../typings/Types"; +import { repeat } from "../util/array"; + +export default class Rewrap implements Action { + getTargetPreferences: () => ActionPreferences[] = () => [ + { + insideOutsideType: "inside", + modifier: { + type: "surroundingPair", + delimiter: "any", + delimiterInclusion: undefined, + }, + }, + ]; + + constructor(private graph: Graph) { + this.run = this.run.bind(this); + } + + run( + [targets]: [TypedSelection[]], + left: string, + right: string + ): Promise { + const boundaries: TypedSelection[] = targets.flatMap((target) => { + const boundary = target.selectionContext.boundary; + + if (boundary == null || boundary.length !== 2) { + throw Error("Target must have an opening and closing delimiter"); + } + + return boundary.map((edge) => + constructSimpleTypedSelection(target.selection.editor, edge) + ); + }); + + const replacementTexts = repeat([left, right], targets.length); + + return this.graph.actions.replace.run([boundaries], replacementTexts); + } +} + +function constructSimpleTypedSelection( + editor: TextEditor, + selection: SelectionWithContext +): TypedSelection { + return { + selection: { + selection: selection.selection, + editor, + }, + selectionType: "token", + selectionContext: selection.context, + insideOutsideType: null, + position: "contents", + }; +} diff --git a/src/actions/index.ts b/src/actions/index.ts index 77e0a969dd..7ae79a593a 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -29,6 +29,7 @@ import { Sort, Reverse } from "./Sort"; import Call from "./Call"; import WrapWithSnippet from "./WrapWithSnippet"; import Deselect from "./Deselect"; +import Rewrap from "./Rewrap"; class Actions implements ActionRecord { constructor(private graph: Graph) {} @@ -57,6 +58,7 @@ class Actions implements ActionRecord { replace = new Replace(this.graph); replaceWithTarget = new Bring(this.graph); reverseTargets = new Reverse(this.graph); + rewrapWithPairedDelimiter = new Rewrap(this.graph); scrollToBottom = new ScrollToBottom(this.graph); scrollToCenter = new ScrollToCenter(this.graph); scrollToTop = new ScrollToTop(this.graph); diff --git a/src/processTargets/modifiers/surroundingPair/extractSelectionFromSurroundingPairOffsets.ts b/src/processTargets/modifiers/surroundingPair/extractSelectionFromSurroundingPairOffsets.ts index 65f655eeb7..236e0d8c96 100644 --- a/src/processTargets/modifiers/surroundingPair/extractSelectionFromSurroundingPairOffsets.ts +++ b/src/processTargets/modifiers/surroundingPair/extractSelectionFromSurroundingPairOffsets.ts @@ -20,6 +20,45 @@ export function extractSelectionFromSurroundingPairOffsets( surroundingPairOffsets: SurroundingPairOffsets, delimiterInclusion: DelimiterInclusion ): SelectionWithContext[] { + const interior = [ + { + selection: new Selection( + document.positionAt( + baseOffset + surroundingPairOffsets.leftDelimiter.end + ), + document.positionAt( + baseOffset + surroundingPairOffsets.rightDelimiter.start + ) + ), + context: {}, + }, + ]; + + const boundary = [ + { + selection: new Selection( + document.positionAt( + baseOffset + surroundingPairOffsets.leftDelimiter.start + ), + document.positionAt( + baseOffset + surroundingPairOffsets.leftDelimiter.end + ) + ), + context: {}, + }, + { + selection: new Selection( + document.positionAt( + baseOffset + surroundingPairOffsets.rightDelimiter.start + ), + document.positionAt( + baseOffset + surroundingPairOffsets.rightDelimiter.end + ) + ), + context: {}, + }, + ]; + // If delimiter inclusion is null, do default behavior and include the // delimiters if (delimiterInclusion == null) { @@ -33,50 +72,18 @@ export function extractSelectionFromSurroundingPairOffsets( baseOffset + surroundingPairOffsets.rightDelimiter.end ) ), - context: {}, + context: { + boundary, + interior, + }, }, ]; } switch (delimiterInclusion) { case "interiorOnly": - return [ - { - selection: new Selection( - document.positionAt( - baseOffset + surroundingPairOffsets.leftDelimiter.end - ), - document.positionAt( - baseOffset + surroundingPairOffsets.rightDelimiter.start - ) - ), - context: {}, - }, - ]; + return interior; case "excludeInterior": - return [ - { - selection: new Selection( - document.positionAt( - baseOffset + surroundingPairOffsets.leftDelimiter.start - ), - document.positionAt( - baseOffset + surroundingPairOffsets.leftDelimiter.end - ) - ), - context: {}, - }, - { - selection: new Selection( - document.positionAt( - baseOffset + surroundingPairOffsets.rightDelimiter.start - ), - document.positionAt( - baseOffset + surroundingPairOffsets.rightDelimiter.end - ) - ), - context: {}, - }, - ]; + return boundary; } } diff --git a/src/processTargets/processSelectionType.ts b/src/processTargets/processSelectionType.ts index 59574b4d55..a58e394ae7 100644 --- a/src/processTargets/processSelectionType.ts +++ b/src/processTargets/processSelectionType.ts @@ -226,11 +226,11 @@ function getTokenSelectionContext( } return { + ...selectionContext, isInDelimitedList, containingListDelimiter: " ", leadingDelimiterRange: isInDelimitedList ? leadingDelimiterRange : null, trailingDelimiterRange: isInDelimitedList ? trailingDelimiterRange : null, - outerSelection: selectionContext.outerSelection, }; } diff --git a/src/test/suite/fixtures/recorded/actions/curlyRepackRound.yml b/src/test/suite/fixtures/recorded/actions/curlyRepackRound.yml new file mode 100644 index 0000000000..79f8a5c968 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/curlyRepackRound.yml @@ -0,0 +1,38 @@ +languageId: plaintext +command: + version: 1 + spokenForm: curly repack round + action: rewrapWithPairedDelimiter + targets: + - type: primitive + modifier: {type: surroundingPair, delimiter: parentheses} + extraArgs: ['{', '}'] +initialState: + documentContents: |- + ([hello]) + (there) + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + - anchor: {line: 1, character: 5} + active: {line: 1, character: 5} + marks: {} +finalState: + documentContents: |- + {[hello]} + {there} + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + - anchor: {line: 1, character: 5} + active: {line: 1, character: 5} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 1} + - anchor: {line: 0, character: 8} + active: {line: 0, character: 9} + - anchor: {line: 1, character: 0} + active: {line: 1, character: 1} + - anchor: {line: 1, character: 6} + active: {line: 1, character: 7} +fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: surroundingPair, delimiter: parentheses}}] diff --git a/src/test/suite/fixtures/recorded/actions/squareRepackHarp.yml b/src/test/suite/fixtures/recorded/actions/squareRepackHarp.yml new file mode 100644 index 0000000000..db35d3a243 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/squareRepackHarp.yml @@ -0,0 +1,31 @@ +languageId: plaintext +command: + version: 1 + spokenForm: square repack harp + action: rewrapWithPairedDelimiter + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: h} + extraArgs: ['[', ']'] +initialState: + documentContents: | + (hello) + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + marks: + default.h: + start: {line: 0, character: 1} + end: {line: 0, character: 6} +finalState: + documentContents: | + [hello] + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 1} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 7} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: h}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: surroundingPair, delimiter: any}}] diff --git a/src/test/suite/fixtures/recorded/actions/squareRepackLeper.yml b/src/test/suite/fixtures/recorded/actions/squareRepackLeper.yml new file mode 100644 index 0000000000..c31058814b --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/squareRepackLeper.yml @@ -0,0 +1,31 @@ +languageId: plaintext +command: + version: 1 + spokenForm: square repack leper + action: rewrapWithPairedDelimiter + targets: + - type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: (} + extraArgs: ['[', ']'] +initialState: + documentContents: | + (hello) + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + marks: + default.(: + start: {line: 0, character: 0} + end: {line: 0, character: 1} +finalState: + documentContents: | + [hello] + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 1} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 7} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: (}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: surroundingPair, delimiter: any}}] diff --git a/src/test/suite/fixtures/recorded/actions/squareRepackPair.yml b/src/test/suite/fixtures/recorded/actions/squareRepackPair.yml new file mode 100644 index 0000000000..4240ef6046 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/squareRepackPair.yml @@ -0,0 +1,28 @@ +languageId: plaintext +command: + version: 1 + spokenForm: square repack pair + action: rewrapWithPairedDelimiter + targets: + - type: primitive + modifier: {type: surroundingPair, delimiter: any} + extraArgs: ['[', ']'] +initialState: + documentContents: | + (hello) + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + marks: {} +finalState: + documentContents: | + [hello] + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 1} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 7} +fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: surroundingPair, delimiter: any}}] diff --git a/src/test/suite/fixtures/recorded/actions/squareRepackThis.yml b/src/test/suite/fixtures/recorded/actions/squareRepackThis.yml new file mode 100644 index 0000000000..d01e156217 --- /dev/null +++ b/src/test/suite/fixtures/recorded/actions/squareRepackThis.yml @@ -0,0 +1,26 @@ +languageId: plaintext +command: + version: 1 + spokenForm: square repack this + action: rewrapWithPairedDelimiter + targets: + - type: primitive + mark: {type: cursor} + extraArgs: ['[', ']'] +initialState: + documentContents: (hello) + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: "[hello]" + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 1} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 7} +fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: surroundingPair, delimiter: any}}] diff --git a/src/test/suite/fixtures/recorded/surroundingPair/textual/chuckPairHarp.yml b/src/test/suite/fixtures/recorded/surroundingPair/textual/chuckPairHarp.yml new file mode 100644 index 0000000000..4f8d8c8885 --- /dev/null +++ b/src/test/suite/fixtures/recorded/surroundingPair/textual/chuckPairHarp.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + version: 1 + spokenForm: chuck pair harp + action: remove + targets: + - type: primitive + modifier: {type: surroundingPair, delimiter: any} + mark: {type: decoratedSymbol, symbolColor: default, character: h} +initialState: + documentContents: | + (hello) (there) + selections: + - anchor: {line: 0, character: 13} + active: {line: 0, character: 13} + marks: + default.h: + start: {line: 0, character: 1} + end: {line: 0, character: 6} +finalState: + documentContents: | + (there) + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: h}, selectionType: token, position: contents, insideOutsideType: outside, modifier: {type: surroundingPair, delimiter: any}}] diff --git a/src/typings/Types.ts b/src/typings/Types.ts index deea857001..f53a9833ad 100644 --- a/src/typings/Types.ts +++ b/src/typings/Types.ts @@ -265,6 +265,20 @@ export interface SelectionContext { trailingDelimiterRange?: vscode.Range | null; isNotebookCell?: boolean; + + /** + * Represents the boundary ranges of this selection. For example, for a + * surrounding pair this would be the opening and closing delimiter. For an if + * statement this would be the line of the guard as well as the closing brace. + */ + boundary?: SelectionWithContext[]; + + /** + * Represents the interior ranges of this selection. For example, for a + * surrounding pair this would exclude the opening and closing delimiter. For an if + * statement this would be the statements in the body. + */ + interior?: SelectionWithContext[]; } export interface TypedSelection { @@ -340,6 +354,7 @@ export type ActionType = | "replace" | "replaceWithTarget" | "reverseTargets" + | "rewrapWithPairedDelimiter" | "scrollToBottom" | "scrollToCenter" | "scrollToTop" diff --git a/src/util/array.ts b/src/util/array.ts new file mode 100644 index 0000000000..138b611772 --- /dev/null +++ b/src/util/array.ts @@ -0,0 +1,11 @@ +import { range } from "lodash"; + +/** + * Creates a new array repeating the given array n times + * @param array The array to repeat + * @param n The number of times to repeat the array + * @returns The new array + */ +export function repeat(array: T[], n: number) { + return range(n).flatMap(() => array); +}