-
-
Notifications
You must be signed in to change notification settings - Fork 83
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
For IDEs that don't have vscode specific copy and paste behavior I have now implemented a text edit basted copy and paste. This also gives us a starting platform if we want to try to implement vscodes behavior ourself. Fixes #2522 ## Checklist - [/] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [/] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [/] I have not broken the cheatsheet --------- Co-authored-by: Pokey Rule <[email protected]>
- Loading branch information
1 parent
44a36e6
commit 94e9fd0
Showing
17 changed files
with
290 additions
and
103 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,26 @@ | ||
import { CommandId } from "./CommandId"; | ||
|
||
export interface Capabilities { | ||
/** | ||
* Capabilities of the commands that the IDE supports. Note that for many of | ||
* these commands, if the IDE does not support them, Cursorless will have a | ||
* fairly sophisticated fallback, so it may actually better to report | ||
* `undefined`. This will vary per action, though. In the future We will | ||
* improve our per-action types / docstrings to make this more clear; see | ||
* #1233 | ||
*/ | ||
readonly commands: CommandCapabilityMap; | ||
} | ||
|
||
export type CommandCapabilityMap = Record< | ||
type SimpleCommandCapabilityMap = Record< | ||
CommandId, | ||
CommandCapabilities | undefined | ||
>; | ||
|
||
export interface CommandCapabilityMap extends SimpleCommandCapabilityMap { | ||
clipboardPaste: boolean | undefined; | ||
} | ||
|
||
export interface CommandCapabilities { | ||
acceptsLocation: boolean; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export function zipStrict<T1, T2>(list1: T1[], list2: T2[]): [T1, T2][] { | ||
if (list1.length !== list2.length) { | ||
throw new Error("Lists must have the same length"); | ||
} | ||
|
||
return list1.map((value, index) => [value, list2[index]]); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { FlashStyle } from "@cursorless/common"; | ||
import type { RangeUpdater } from "../core/updateSelections/RangeUpdater"; | ||
import { ide } from "../singletons/ide.singleton"; | ||
import type { Target } from "../typings/target.types"; | ||
import { flashTargets } from "../util/targetUtils"; | ||
import type { Actions } from "./Actions"; | ||
import { CopyToClipboardSimple } from "./SimpleIdeCommandActions"; | ||
import type { ActionReturnValue, SimpleAction } from "./actions.types"; | ||
|
||
interface Options { | ||
showDecorations?: boolean; | ||
} | ||
|
||
export class CopyToClipboard implements SimpleAction { | ||
constructor( | ||
private actions: Actions, | ||
private rangeUpdater: RangeUpdater, | ||
) { | ||
this.run = this.run.bind(this); | ||
} | ||
|
||
async run( | ||
targets: Target[], | ||
options: Options = { showDecorations: true }, | ||
): Promise<ActionReturnValue> { | ||
if (ide().capabilities.commands.clipboardCopy != null) { | ||
const simpleAction = new CopyToClipboardSimple(this.rangeUpdater); | ||
return simpleAction.run(targets, options); | ||
} | ||
|
||
if (options.showDecorations) { | ||
await flashTargets( | ||
ide(), | ||
targets, | ||
FlashStyle.referenced, | ||
(target) => target.contentRange, | ||
); | ||
} | ||
|
||
// FIXME: We should really keep track of the number of targets from the | ||
// original copy, as is done in VSCode. | ||
const text = targets.map((t) => t.contentText).join("\n"); | ||
|
||
await ide().clipboard.writeText(text); | ||
|
||
return { thatTargets: targets }; | ||
} | ||
} |
104 changes: 21 additions & 83 deletions
104
packages/cursorless-engine/src/actions/PasteFromClipboard.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,90 +1,28 @@ | ||
import { | ||
FlashStyle, | ||
RangeExpansionBehavior, | ||
toCharacterRange, | ||
} from "@cursorless/common"; | ||
import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; | ||
import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; | ||
import { ide } from "../singletons/ide.singleton"; | ||
import { Destination } from "../typings/target.types"; | ||
import { ensureSingleEditor } from "../util/targetUtils"; | ||
import { Actions } from "./Actions"; | ||
import { ActionReturnValue } from "./actions.types"; | ||
import type { Destination } from "../typings/target.types"; | ||
import type { Actions } from "./Actions"; | ||
import type { ActionReturnValue } from "./actions.types"; | ||
import { PasteFromClipboardUsingCommand } from "./PasteFromClipboardUsingCommand"; | ||
import { PasteFromClipboardDirectly } from "./PasteFromClipboardDirectly"; | ||
|
||
export interface DestinationWithText { | ||
destination: Destination; | ||
text: string; | ||
} | ||
|
||
export class PasteFromClipboard { | ||
constructor( | ||
private rangeUpdater: RangeUpdater, | ||
private actions: Actions, | ||
) {} | ||
|
||
async run(destinations: Destination[]): Promise<ActionReturnValue> { | ||
const editor = ide().getEditableTextEditor( | ||
ensureSingleEditor(destinations), | ||
); | ||
const originalEditor = ide().activeEditableTextEditor; | ||
|
||
// 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 callbackEdit = async () => { | ||
await this.actions.editNew.run(destinations); | ||
}; | ||
|
||
const { cursorSelections: originalCursorSelections } = | ||
await performEditsAndUpdateSelections({ | ||
rangeUpdater: this.rangeUpdater, | ||
editor, | ||
preserveCursorSelections: true, | ||
callback: callbackEdit, | ||
selections: { | ||
cursorSelections: editor.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 { | ||
originalCursorSelections: updatedCursorSelections, | ||
editorSelections: updatedTargetSelections, | ||
} = await performEditsAndUpdateSelections({ | ||
rangeUpdater: this.rangeUpdater, | ||
editor, | ||
callback: () => editor.clipboardPaste(), | ||
selections: { | ||
originalCursorSelections, | ||
editorSelections: { | ||
selections: editor.selections, | ||
behavior: RangeExpansionBehavior.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 | ||
await editor.setSelections(updatedCursorSelections); | ||
|
||
// If necessary focus back original editor | ||
if (originalEditor != null && !originalEditor.isActive) { | ||
// 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 originalEditor.focus(); | ||
} | ||
|
||
await ide().flashRanges( | ||
updatedTargetSelections.map((selection) => ({ | ||
editor, | ||
range: toCharacterRange(selection), | ||
style: FlashStyle.justAdded, | ||
})), | ||
); | ||
private runner: PasteFromClipboardDirectly | PasteFromClipboardUsingCommand; | ||
|
||
constructor(rangeUpdater: RangeUpdater, actions: Actions) { | ||
this.run = this.run.bind(this); | ||
this.runner = | ||
ide().capabilities.commands.clipboardPaste != null | ||
? new PasteFromClipboardUsingCommand(rangeUpdater, actions) | ||
: new PasteFromClipboardDirectly(rangeUpdater); | ||
} | ||
|
||
return { | ||
thatSelections: updatedTargetSelections.map((selection) => ({ | ||
editor: editor, | ||
selection, | ||
})), | ||
}; | ||
run(destinations: Destination[]): Promise<ActionReturnValue> { | ||
return this.runner.run(destinations); | ||
} | ||
} |
90 changes: 90 additions & 0 deletions
90
packages/cursorless-engine/src/actions/PasteFromClipboardDirectly.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { | ||
FlashStyle, | ||
RangeExpansionBehavior, | ||
toCharacterRange, | ||
zipStrict, | ||
type TextEditor, | ||
} from "@cursorless/common"; | ||
import { flatten } from "lodash-es"; | ||
import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; | ||
import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; | ||
import { ide } from "../singletons/ide.singleton"; | ||
import type { Destination } from "../typings/target.types"; | ||
import { runForEachEditor } from "../util/targetUtils"; | ||
import type { ActionReturnValue } from "./actions.types"; | ||
import { DestinationWithText } from "./PasteFromClipboard"; | ||
|
||
/** | ||
* This action pastes the text from the clipboard into the target editor directly | ||
* by reading the clipboard and inserting the text directly into the editor. | ||
*/ | ||
export class PasteFromClipboardDirectly { | ||
constructor(private rangeUpdater: RangeUpdater) { | ||
this.runForEditor = this.runForEditor.bind(this); | ||
} | ||
|
||
async run(destinations: Destination[]): Promise<ActionReturnValue> { | ||
const text = await ide().clipboard.readText(); | ||
const textLines = text.split(/\r?\n/g); | ||
|
||
// FIXME: We should really use the number of targets from the original copy | ||
// action, as is done in VSCode. | ||
const destinationsWithText: DestinationWithText[] = | ||
destinations.length === textLines.length | ||
? zipStrict(destinations, textLines).map(([destination, text]) => ({ | ||
destination, | ||
text, | ||
})) | ||
: destinations.map((destination) => ({ destination, text })); | ||
|
||
const thatSelections = flatten( | ||
await runForEachEditor( | ||
destinationsWithText, | ||
({ destination }) => destination.editor, | ||
this.runForEditor, | ||
), | ||
); | ||
|
||
return { thatSelections }; | ||
} | ||
|
||
private async runForEditor( | ||
editor: TextEditor, | ||
destinationsWithText: DestinationWithText[], | ||
) { | ||
const edits = destinationsWithText.map(({ destination, text }) => | ||
destination.constructChangeEdit(text), | ||
); | ||
|
||
const { editSelections: updatedEditSelections } = | ||
await performEditsAndUpdateSelections({ | ||
rangeUpdater: this.rangeUpdater, | ||
editor: ide().getEditableTextEditor(editor), | ||
edits, | ||
selections: { | ||
editSelections: { | ||
selections: edits.map(({ range }) => range), | ||
behavior: RangeExpansionBehavior.openOpen, | ||
}, | ||
}, | ||
}); | ||
|
||
const thatTargetSelections = zipStrict(edits, updatedEditSelections).map( | ||
([edit, selection]) => | ||
edit.updateRange(selection).toSelection(selection.isReversed), | ||
); | ||
|
||
await ide().flashRanges( | ||
thatTargetSelections.map((selection) => ({ | ||
editor, | ||
range: toCharacterRange(selection), | ||
style: FlashStyle.justAdded, | ||
})), | ||
); | ||
|
||
return thatTargetSelections.map((selection) => ({ | ||
editor: editor, | ||
selection, | ||
})); | ||
} | ||
} |
Oops, something went wrong.