diff --git a/src/processTargets/modifiers/HeadTailStage.ts b/src/processTargets/modifiers/HeadTailStage.ts index 1b246a436b..74babdd55b 100644 --- a/src/processTargets/modifiers/HeadTailStage.ts +++ b/src/processTargets/modifiers/HeadTailStage.ts @@ -1,4 +1,4 @@ -import { Range, TextEditor } from "vscode"; +import { Range } from "vscode"; import { Target } from "../../typings/target.types"; import { HeadTailModifier, diff --git a/src/processTargets/targets/BaseTarget.ts b/src/processTargets/targets/BaseTarget.ts index 58ea6f4573..e4e16c29f3 100644 --- a/src/processTargets/targets/BaseTarget.ts +++ b/src/processTargets/targets/BaseTarget.ts @@ -11,7 +11,7 @@ import { createContinuousRangeWeakTarget, } from "../targetUtil/createContinuousRange"; -/** Parameters supported by all target classes */ +/** Parameters supported by most target classes */ export interface CommonTargetParameters { readonly editor: TextEditor; readonly isReversed: boolean; diff --git a/src/processTargets/targets/DocumentTarget.ts b/src/processTargets/targets/DocumentTarget.ts index 7df946566c..4379e55081 100644 --- a/src/processTargets/targets/DocumentTarget.ts +++ b/src/processTargets/targets/DocumentTarget.ts @@ -1,7 +1,7 @@ -import { Range, TextEditor } from "vscode"; -import { fitRangeToLineContent } from "../modifiers/scopeTypeStages/LineStage"; +import { Range } from "vscode"; +import { shrinkRangeToFitContent } from "../../util/selectionUtils"; import BaseTarget, { CommonTargetParameters } from "./BaseTarget"; -import WeakTarget from "./WeakTarget"; +import PlainTarget from "./PlainTarget"; export default class DocumentTarget extends BaseTarget { insertionDelimiter = "\n"; @@ -23,10 +23,11 @@ export default class DocumentTarget extends BaseTarget { getInteriorStrict() { return [ - new WeakTarget({ + // Use plain target instead of interior target since we want the same content and removal range for a document interior. + new PlainTarget({ editor: this.editor, isReversed: this.isReversed, - contentRange: getDocumentContentRange(this.editor), + contentRange: shrinkRangeToFitContent(this.editor, this.contentRange), }), ]; } @@ -35,31 +36,3 @@ export default class DocumentTarget extends BaseTarget { return this.state; } } - -function getDocumentContentRange(editor: TextEditor) { - const { document } = editor; - let firstLineNum = 0; - let lastLineNum = document.lineCount - 1; - - for (let i = firstLineNum; i < document.lineCount; ++i) { - if (!document.lineAt(i).isEmptyOrWhitespace) { - firstLineNum = i; - break; - } - } - - for (let i = lastLineNum; i > -1; --i) { - if (!document.lineAt(i).isEmptyOrWhitespace) { - lastLineNum = i; - break; - } - } - - const firstLine = document.lineAt(firstLineNum); - const lastLine = document.lineAt(lastLineNum); - - return fitRangeToLineContent( - editor, - new Range(firstLine.range.start, lastLine.range.end) - ); -} diff --git a/src/processTargets/targets/InteriorTarget.ts b/src/processTargets/targets/InteriorTarget.ts new file mode 100644 index 0000000000..629a22fc5c --- /dev/null +++ b/src/processTargets/targets/InteriorTarget.ts @@ -0,0 +1,35 @@ +import { Range } from "vscode"; +import { shrinkRangeToFitContent } from "../../util/selectionUtils"; +import BaseTarget, { CommonTargetParameters } from "./BaseTarget"; + +export interface InteriorTargetParameters + extends Omit { + readonly fullInteriorRange: Range; +} + +export default class InteriorTarget extends BaseTarget { + insertionDelimiter = " "; + private readonly fullInteriorRange: Range; + + constructor(parameters: InteriorTargetParameters) { + super({ + ...parameters, + contentRange: shrinkRangeToFitContent( + parameters.editor, + parameters.fullInteriorRange + ), + }); + this.fullInteriorRange = parameters.fullInteriorRange; + } + + getLeadingDelimiterTarget = () => undefined; + getTrailingDelimiterTarget = () => undefined; + getRemovalRange = () => this.fullInteriorRange; + + protected getCloneParameters() { + return { + ...this.state, + fullInteriorRange: this.fullInteriorRange, + }; + } +} diff --git a/src/processTargets/targets/SurroundingPairTarget.ts b/src/processTargets/targets/SurroundingPairTarget.ts index c3bc151d74..d089bad289 100644 --- a/src/processTargets/targets/SurroundingPairTarget.ts +++ b/src/processTargets/targets/SurroundingPairTarget.ts @@ -6,8 +6,8 @@ import { getTokenTrailingDelimiterTarget, } from "../targetUtil/insertionRemovalBehaviors/TokenInsertionRemovalBehavior"; import BaseTarget, { CommonTargetParameters } from "./BaseTarget"; +import InteriorTarget from "./InteriorTarget"; import TokenTarget from "./TokenTarget"; -import WeakTarget from "./WeakTarget"; interface SurroundingPairTargetParameters extends CommonTargetParameters { /** @@ -48,10 +48,10 @@ export default class SurroundingPairTarget extends BaseTarget { getInteriorStrict() { return [ - new WeakTarget({ + new InteriorTarget({ editor: this.editor, isReversed: this.isReversed, - contentRange: this.interiorRange_, + fullInteriorRange: this.interiorRange_, }), ]; } diff --git a/src/test/suite/fixtures/recorded/lineEndings/clearCoreFileLFCR.yml b/src/test/suite/fixtures/recorded/lineEndings/clearCoreFileLFCR.yml new file mode 100644 index 0000000000..9021d73122 --- /dev/null +++ b/src/test/suite/fixtures/recorded/lineEndings/clearCoreFileLFCR.yml @@ -0,0 +1,27 @@ +languageId: plaintext +command: + spokenForm: clear core file + version: 2 + targets: + - type: primitive + modifiers: + - {type: interiorOnly} + - type: containingScope + scopeType: {type: document} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: "\r\n abc \r\n" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: "\r\n \r\n" + selections: + - anchor: {line: 1, character: 1} + active: {line: 1, character: 1} + thatMark: + - anchor: {line: 1, character: 1} + active: {line: 1, character: 1} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: interiorOnly}, {type: containingScope, scopeType: {type: document}}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearCore.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearCore.yml new file mode 100644 index 0000000000..a343f2313a --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearCore.yml @@ -0,0 +1,25 @@ +languageId: plaintext +command: + spokenForm: clear core + version: 2 + targets: + - type: primitive + modifiers: + - {type: interiorOnly} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: ( ) + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + 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: interiorOnly}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearCore2.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearCore2.yml new file mode 100644 index 0000000000..ba4959b448 --- /dev/null +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearCore2.yml @@ -0,0 +1,25 @@ +languageId: plaintext +command: + spokenForm: clear core + version: 2 + targets: + - type: primitive + modifiers: + - {type: interiorOnly} + usePrePhraseSnapshot: true + action: {name: clearAndSetSelection} +initialState: + documentContents: () + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + 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: interiorOnly}]}] diff --git a/src/test/suite/fixtures/recorded/selectionTypes/clearHeadCoreCurly.yml b/src/test/suite/fixtures/recorded/selectionTypes/clearHeadCoreCurly.yml index eba314f3a4..02ac7443c1 100644 --- a/src/test/suite/fixtures/recorded/selectionTypes/clearHeadCoreCurly.yml +++ b/src/test/suite/fixtures/recorded/selectionTypes/clearHeadCoreCurly.yml @@ -23,12 +23,13 @@ initialState: marks: {} finalState: documentContents: |- - {z) + { + z) } selections: - - anchor: {line: 0, character: 1} - active: {line: 0, character: 1} + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} thatMark: - - anchor: {line: 0, character: 1} - active: {line: 0, character: 1} + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: extendThroughStartOf, modifiers: [{type: interiorOnly}, {type: containingScope, scopeType: {type: surroundingPair, delimiter: curlyBrackets}}]}]}] diff --git a/src/test/suite/fixtures/recorded/surroundingPair/chuckCore.yml b/src/test/suite/fixtures/recorded/surroundingPair/chuckCore.yml new file mode 100644 index 0000000000..95daa00b2f --- /dev/null +++ b/src/test/suite/fixtures/recorded/surroundingPair/chuckCore.yml @@ -0,0 +1,29 @@ +languageId: typescript +command: + spokenForm: chuck core + version: 2 + targets: + - type: primitive + modifiers: + - {type: interiorOnly} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: |- + [ + "foo", + "bar" + ] + selections: + - anchor: {line: 1, character: 10} + active: {line: 1, character: 10} + 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: interiorOnly}]}] diff --git a/src/test/suite/fixtures/recorded/surroundingPair/chuckCore2.yml b/src/test/suite/fixtures/recorded/surroundingPair/chuckCore2.yml new file mode 100644 index 0000000000..ba6fc0f9ed --- /dev/null +++ b/src/test/suite/fixtures/recorded/surroundingPair/chuckCore2.yml @@ -0,0 +1,25 @@ +languageId: plaintext +command: + spokenForm: chuck core + version: 2 + targets: + - type: primitive + modifiers: + - {type: interiorOnly} + usePrePhraseSnapshot: true + action: {name: remove} +initialState: + documentContents: ( hello ) + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + 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: interiorOnly}]}] diff --git a/src/test/suite/fixtures/recorded/surroundingPair/parseTree/python/clearMatching9.yml b/src/test/suite/fixtures/recorded/surroundingPair/parseTree/python/clearInside.yml similarity index 91% rename from src/test/suite/fixtures/recorded/surroundingPair/parseTree/python/clearMatching9.yml rename to src/test/suite/fixtures/recorded/surroundingPair/parseTree/python/clearInside.yml index c034896041..4a6692ff58 100644 --- a/src/test/suite/fixtures/recorded/surroundingPair/parseTree/python/clearMatching9.yml +++ b/src/test/suite/fixtures/recorded/surroundingPair/parseTree/python/clearInside.yml @@ -1,7 +1,7 @@ languageId: python command: version: 1 - spokenForm: clear matching + spokenForm: clear inside action: clearAndSetSelection targets: - type: primitive @@ -17,7 +17,9 @@ initialState: active: {line: 1, character: 0} marks: {} finalState: - documentContents: "\"\"\"\"\"\"" + documentContents: |- + """ + """ selections: - anchor: {line: 0, character: 3} active: {line: 0, character: 3} diff --git a/src/test/suite/fixtures/recorded/surroundingPair/parseTree/typescript/takeCore.yml b/src/test/suite/fixtures/recorded/surroundingPair/parseTree/typescript/takeCore.yml index cf8ae2f724..11382b9bb1 100644 --- a/src/test/suite/fixtures/recorded/surroundingPair/parseTree/typescript/takeCore.yml +++ b/src/test/suite/fixtures/recorded/surroundingPair/parseTree/typescript/takeCore.yml @@ -29,9 +29,9 @@ finalState: ) } selections: - - anchor: {line: 1, character: 45} - active: {line: 3, character: 4} + - anchor: {line: 2, character: 4} + active: {line: 2, character: 30} thatMark: - - anchor: {line: 1, character: 45} - active: {line: 3, character: 4} + - anchor: {line: 2, character: 4} + active: {line: 2, character: 30} fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: surroundingPair, delimiter: any}}, {type: interiorOnly}]}] diff --git a/src/test/suite/fixtures/recorded/surroundingPair/takeCore.yml b/src/test/suite/fixtures/recorded/surroundingPair/takeCore.yml new file mode 100644 index 0000000000..516048dcd8 --- /dev/null +++ b/src/test/suite/fixtures/recorded/surroundingPair/takeCore.yml @@ -0,0 +1,33 @@ +languageId: typescript +command: + spokenForm: take core + version: 2 + targets: + - type: primitive + modifiers: + - {type: interiorOnly} + usePrePhraseSnapshot: true + action: {name: setSelection} +initialState: + documentContents: |- + [ + "foo", + "bar" + ] + selections: + - anchor: {line: 1, character: 10} + active: {line: 1, character: 10} + marks: {} +finalState: + documentContents: |- + [ + "foo", + "bar" + ] + selections: + - anchor: {line: 1, character: 4} + active: {line: 2, character: 9} + thatMark: + - anchor: {line: 1, character: 4} + active: {line: 2, character: 9} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: interiorOnly}]}] diff --git a/src/test/suite/fixtures/recorded/surroundingPair/takeCore2.yml b/src/test/suite/fixtures/recorded/surroundingPair/takeCore2.yml new file mode 100644 index 0000000000..6e978c8f4e --- /dev/null +++ b/src/test/suite/fixtures/recorded/surroundingPair/takeCore2.yml @@ -0,0 +1,25 @@ +languageId: plaintext +command: + spokenForm: take core + version: 2 + targets: + - type: primitive + modifiers: + - {type: interiorOnly} + usePrePhraseSnapshot: true + action: {name: setSelection} +initialState: + documentContents: ( hello ) + selections: + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + marks: {} +finalState: + documentContents: ( hello ) + selections: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 7} + thatMark: + - anchor: {line: 0, character: 2} + active: {line: 0, character: 7} +fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: interiorOnly}]}] diff --git a/src/util/selectionUtils.ts b/src/util/selectionUtils.ts index 8e443b0ef4..18d490c8f6 100644 --- a/src/util/selectionUtils.ts +++ b/src/util/selectionUtils.ts @@ -1,4 +1,4 @@ -import { Position, Range, Selection } from "vscode"; +import { Position, Range, Selection, TextEditor } from "vscode"; import { SelectionWithEditor } from "../typings/Types"; export function isForward(selection: Selection) { @@ -42,3 +42,22 @@ export function selectionFromRange(isReversed: boolean, range: Range) { const { start, end } = range; return isReversed ? new Selection(end, start) : new Selection(start, end); } + +/** + * Return a copy of {@link range} excluding any leading or trailing whitespace. + * If {@link range} contains only whitespace or is empty {@link range} will be returned unchanged. + * @param editor The text editor to use + * @param range The range to shrink down + * @returns A new range equal or smaller to {@link range} + */ +export function shrinkRangeToFitContent(editor: TextEditor, range: Range) { + const { document } = editor; + const text = document.getText(range); + const startDelta = text.length - text.trimStart().length; + const endDelta = text.length - text.trimEnd().length; + const startOffset = document.offsetAt(range.start) + startDelta; + const endOffset = document.offsetAt(range.end) - endDelta; + const start = document.positionAt(startOffset); + const end = document.positionAt(endOffset); + return new Range(start, end); +}