Skip to content

Support delimiter insertion for "paste after" #771

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/actions/Actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import {
CopyContentAfter as InsertCopyAfter,
CopyContentBefore as InsertCopyBefore,
} from "./InsertCopy";
import { Copy, Cut, Paste } from "./CutCopyPaste";
import { Copy, Cut } from "./CutCopy";
import { Paste } from "./Paste";
import Deselect from "./Deselect";
import { EditNewBefore, EditNewAfter } from "./EditNew";
import { EditNewBefore, EditNewAfter, EditNew } from "./EditNew";
import ExecuteCommand from "./ExecuteCommand";
import ExtractVariable from "./ExtractVariable";
import { FindInFiles } from "./Find";
Expand Down Expand Up @@ -46,6 +47,7 @@ class Actions implements ActionRecord {
copyToClipboard = new Copy(this.graph);
cutToClipboard = new Cut(this.graph);
deselect = new Deselect(this.graph);
editNew = new EditNew(this.graph);
editNewLineAfter = new EditNewAfter(this.graph);
editNewLineBefore = new EditNewBefore(this.graph);
executeCommand = new ExecuteCommand(this.graph);
Expand Down
10 changes: 0 additions & 10 deletions src/actions/CutCopyPaste.ts → src/actions/CutCopy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,3 @@ export class Copy extends CommandAction {
});
}
}

export class Paste extends CommandAction {
constructor(graph: Graph) {
super(graph, {
command: "editor.action.clipboardPasteAction",
ensureSingleEditor: true,
showDecorations: true,
});
}
}
81 changes: 81 additions & 0 deletions src/actions/Paste.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { commands, DecorationRangeBehavior, window } from "vscode";
import {
callFunctionAndUpdateSelections,
callFunctionAndUpdateSelectionsWithBehavior,
} from "../core/updateSelections/updateSelections";
import { Target } from "../typings/target.types";
import { Graph } from "../typings/Types";
import {
focusEditor,
setSelectionsWithoutFocusingEditor,
} from "../util/setSelectionsAndFocusEditor";
import { ensureSingleEditor } from "../util/targetUtils";
import { ActionReturnValue } from "./actions.types";

export class Paste {
constructor(private graph: Graph) {}

async run([targets]: [Target[]]): Promise<ActionReturnValue> {
const targetEditor = ensureSingleEditor(targets);
const originalEditor = window.activeTextEditor;

// First call editNew in order to insert delimiters if necessary and leave
// the cursor in the right position. Note that this action will focus the
// editor containing the targets
const [originalCursorSelections] = await callFunctionAndUpdateSelections(
this.graph.rangeUpdater,
async () => {
await this.graph.actions.editNew.run([targets]);
},
targetEditor.document,
[targetEditor.selections]
);

// Then use VSCode paste command, using open ranges at the place where we
// paste in order to capture the pasted text for highlights and `that` mark
const [updatedCursorSelections, updatedTargetSelections] =
await callFunctionAndUpdateSelectionsWithBehavior(
this.graph.rangeUpdater,
() => commands.executeCommand("editor.action.clipboardPasteAction"),
targetEditor.document,
[
{
selections: originalCursorSelections,
},
{
selections: targetEditor.selections,
rangeBehavior: DecorationRangeBehavior.OpenOpen,
},
]
);

// Reset cursors on the editor where the edits took place.
// NB: We don't focus the editor here because we want to focus the original
// editor, not the one where the edits took place
setSelectionsWithoutFocusingEditor(targetEditor, updatedCursorSelections);

// If necessary focus back original editor
if (originalEditor != null && originalEditor !== window.activeTextEditor) {
// NB: We just do one editor focus at the end, instead of using
// setSelectionsAndFocusEditor because the command might operate on
// multiple editors, so we just do one focus at the end.
await focusEditor(originalEditor);
}

this.graph.editStyles.displayPendingEditDecorationsForRanges(
updatedTargetSelections.map((selection) => ({
editor: targetEditor,
range: selection,
})),
this.graph.editStyles.justAdded,
true
);

return {
thatMark: updatedTargetSelections.map((selection) => ({
editor: targetEditor,
selection,
})),
};
}
}
1 change: 1 addition & 0 deletions src/actions/actions.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type ActionType =
| "copyToClipboard"
| "cutToClipboard"
| "deselect"
| "editNew"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this so that paste and insert snippet can use it; could be useful to map a spoken form at some point 🤷. Kinda like a fancy "change" that will insert delimiters if you give it a positional modifier

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then maybe we should just merge this with change and allowed that option?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my suggestion at meet-up, but the issue is that highlighting becomes a bit complex, as the targets will need to own a bit more. I'm not opposed to it, but maybe out of scope for this PR? We'll need to think a bit about giving targets more autonomy wrt highlighting

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can do that in a follow up pr

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean we already have removal highlight range in the targets so we could just add content highlight range if we want.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is a fairly clean solution here that would also simplify the highlight graph component, but it's worth pairing on and I think out of scope

If you prefer for this PR I can just have the paste action keep its own EditNew object; that was my original solution

| "editNewLineAfter"
| "editNewLineBefore"
| "executeCommand"
Expand Down
34 changes: 33 additions & 1 deletion src/core/updateSelections/updateSelections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export async function callFunctionAndUpdateRanges(
* @param rangeUpdater A RangeUpdate instance that will perform actual range updating
* @param func The function to call
* @param document The document containing the selections
* @param selectionMatrix A matrix of selection info objects to update
* @param selectionInfoMatrix A matrix of selection info objects to update
* @returns The initial selections updated based upon what happened in the function
*/
async function callFunctionAndUpdateSelectionInfos(
Expand All @@ -207,6 +207,38 @@ async function callFunctionAndUpdateSelectionInfos(
return selectionInfosToSelections(selectionInfoMatrix);
}

/**
* Performs a list of edits and returns the given selections updated based on
* the applied edits
* @param rangeUpdater A RangeUpdate instance that will perform actual range updating
* @param func The function to call
* @param document The document containing the selections
* @param originalSelections The selections to update
* @returns The updated selections
*/
export function callFunctionAndUpdateSelectionsWithBehavior(
rangeUpdater: RangeUpdater,
func: () => Thenable<void>,
document: TextDocument,
originalSelections: SelectionsWithBehavior[]
) {
return callFunctionAndUpdateSelectionInfos(
rangeUpdater,
func,
document,
originalSelections.map((selectionsWithBehavior) =>
selectionsWithBehavior.selections.map((selection) =>
getSelectionInfo(
document,
selection,
selectionsWithBehavior.rangeBehavior ??
DecorationRangeBehavior.ClosedClosed
)
)
)
);
}

/**
* Performs a list of edits and returns the given selections updated based on
* the applied edits
Expand Down
23 changes: 20 additions & 3 deletions src/processTargets/targets/BaseTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,30 @@ export default abstract class BaseTarget implements Target {
);
}

isEqual(target: Target): boolean {
isEqual(otherTarget: Target): boolean {
return (
target instanceof BaseTarget &&
isEqual(this.getCloneParameters(), target.getCloneParameters())
otherTarget instanceof BaseTarget &&
isEqual(this.getEqualityParameters(), otherTarget.getEqualityParameters())
);
}

/**
* @returns An object that can be used for determining equality between two
* `BaseTarget`s
*/
protected getEqualityParameters(): object {
const { thatTarget, ...otherCloneParameters } =
this.getCloneParameters() as { thatTarget?: Target };
if (!(thatTarget instanceof BaseTarget)) {
return { thatTarget, ...otherCloneParameters };
}

return {
thatTarget: thatTarget ? thatTarget.getEqualityParameters() : undefined,
...otherCloneParameters,
};
}

toPositionTarget(position: Position): Target {
return toPositionTarget(this, position);
}
Expand Down
37 changes: 37 additions & 0 deletions src/test/suite/fixtures/recorded/actions/pasteAfterArgueBat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
languageId: typescript
command:
spokenForm: paste after argue bat
version: 2
targets:
- type: primitive
mark: {type: decoratedSymbol, symbolColor: default, character: b}
modifiers:
- {type: position, position: after}
- type: containingScope
scopeType: {type: argumentOrParameter}
usePrePhraseSnapshot: true
action: {name: pasteFromClipboard}
initialState:
documentContents: foo(bar, baz, bongo)
selections:
- anchor: {line: 0, character: 6}
active: {line: 0, character: 6}
marks:
default.b:
start: {line: 0, character: 14}
end: {line: 0, character: 19}
clipboard: baz
finalState:
documentContents: foo(bar, baz, bongo, baz)
selections:
- anchor: {line: 0, character: 6}
active: {line: 0, character: 6}
thatMark:
- anchor: {line: 0, character: 21}
active: {line: 0, character: 24}
decorations:
- name: justAddedBackground
type: token
start: {line: 0, character: 21}
end: {line: 0, character: 24}
fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: b}, modifiers: [{type: position, position: after}, {type: containingScope, scopeType: {type: argumentOrParameter}}]}]
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
languageId: plaintext
command:
spokenForm: paste after line spun and after block look and before line spun
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shockingly, Cursorless handled this one with no problem, inserting the right delimiters and leaving cursors in the right spot 😯

version: 2
targets:
- type: list
elements:
- type: primitive
mark: {type: decoratedSymbol, symbolColor: default, character: s}
modifiers:
- {type: position, position: after}
- type: containingScope
scopeType: {type: line}
- type: primitive
mark: {type: decoratedSymbol, symbolColor: default, character: l}
modifiers:
- {type: position, position: after}
- type: containingScope
scopeType: {type: paragraph}
- type: primitive
mark: {type: decoratedSymbol, symbolColor: default, character: s}
modifiers:
- {type: position, position: before}
- type: containingScope
scopeType: {type: line}
usePrePhraseSnapshot: true
action: {name: pasteFromClipboard}
initialState:
documentContents: |-
testing

hello
there
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
- anchor: {line: 3, character: 5}
active: {line: 3, character: 5}
marks:
default.s:
start: {line: 0, character: 0}
end: {line: 0, character: 7}
default.l:
start: {line: 2, character: 0}
end: {line: 2, character: 5}
clipboard: baz
finalState:
documentContents: |-
baz
testing
baz

hello
there

baz
selections:
- anchor: {line: 1, character: 0}
active: {line: 1, character: 0}
- anchor: {line: 5, character: 5}
active: {line: 5, character: 5}
thatMark:
- anchor: {line: 2, character: 0}
active: {line: 2, character: 3}
- anchor: {line: 7, character: 0}
active: {line: 7, character: 3}
- anchor: {line: 0, character: 0}
active: {line: 0, character: 3}
decorations:
- name: justAddedBackground
type: token
start: {line: 2, character: 0}
end: {line: 2, character: 3}
- name: justAddedBackground
type: token
start: {line: 7, character: 0}
end: {line: 7, character: 3}
- name: justAddedBackground
type: token
start: {line: 0, character: 0}
end: {line: 0, character: 3}
fullTargets: [{type: list, elements: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: s}, modifiers: [{type: position, position: after}, {type: containingScope, scopeType: {type: line}}]}, {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: l}, modifiers: [{type: position, position: after}, {type: containingScope, scopeType: {type: paragraph}}]}, {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: s}, modifiers: [{type: position, position: before}, {type: containingScope, scopeType: {type: line}}]}]}]
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
languageId: python
command:
spokenForm: paste after line trap and after block trap
version: 2
targets:
- type: list
elements:
- type: primitive
mark: {type: decoratedSymbol, symbolColor: default, character: t}
modifiers:
- {type: position, position: after}
- type: containingScope
scopeType: {type: line}
- type: primitive
mark: {type: decoratedSymbol, symbolColor: default, character: t}
modifiers:
- {type: position, position: after}
- type: containingScope
scopeType: {type: paragraph}
usePrePhraseSnapshot: true
action: {name: pasteFromClipboard}
initialState:
documentContents: |
if True:
pass
selections:
- anchor: {line: 2, character: 0}
active: {line: 2, character: 0}
marks:
default.t:
start: {line: 0, character: 3}
end: {line: 0, character: 7}
clipboard: print("hello")
finalState:
documentContents: |
if True:
print("hello")
pass

print("hello")
selections:
- anchor: {line: 5, character: 0}
active: {line: 5, character: 0}
thatMark:
- anchor: {line: 1, character: 4}
active: {line: 1, character: 18}
- anchor: {line: 4, character: 4}
active: {line: 4, character: 18}
fullTargets: [{type: list, elements: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: t}, modifiers: [{type: position, position: after}, {type: containingScope, scopeType: {type: line}}]}, {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: t}, modifiers: [{type: position, position: after}, {type: containingScope, scopeType: {type: paragraph}}]}]}]
Loading