Added action replace (#135)

This commit is contained in:
Andreas Arvidsson 2021-07-27 02:53:07 +02:00 committed by GitHub
parent 563ed19392
commit 9dcf9c0528
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 263 additions and 195 deletions

View File

@ -63,8 +63,7 @@ export default class CommandAction implements Action {
]): Promise<ActionReturnValue> {
await displayPendingEditDecorations(
targets,
this.graph.editStyles.referenced,
this.graph.editStyles.referencedLine
this.graph.editStyles.referenced
);
const originalEditor = window.activeTextEditor;

View File

@ -2,7 +2,7 @@ import { SyntaxNode } from "web-tree-sitter";
import * as vscode from "vscode";
import { Location } from "vscode";
import { SymbolColor } from "./constants";
import EditStyles from "./editStyles";
import { EditStyles } from "./editStyles";
import NavigationMap from "./NavigationMap";
/**
@ -256,6 +256,7 @@ export type ActionType =
| "move"
| "outdentLines"
| "paste"
| "replaceWithText"
| "scrollToBottom"
| "scrollToCenter"
| "scrollToTop"

View File

@ -9,17 +9,11 @@ import {
import { runForEachEditor } from "../targetUtils";
import update from "immutability-helper";
import displayPendingEditDecorations from "../editDisplayUtils";
import { performInsideOutsideAdjustment } from "../performInsideOutsideAdjustment";
import { performOutsideAdjustment } from "../performInsideOutsideAdjustment";
import { flatten, zip } from "lodash";
import { Selection, TextEditorDecorationType, TextEditor, Range } from "vscode";
import { Selection, TextEditor, Range } from "vscode";
import { performEditsAndUpdateSelections } from "../updateSelections";
interface DecorationTypes {
sourceStyle: TextEditorDecorationType;
sourceLineStyle: TextEditorDecorationType;
destinationStyle: TextEditorDecorationType;
destinationLineStyle: TextEditorDecorationType;
}
import { getTextWithPossibleDelimiter } from "../getTextWithPossibleDelimiter";
interface ExtendedEdit extends Edit {
editor: TextEditor;
@ -37,7 +31,7 @@ interface ThatMarkEntry {
class BringMoveSwap implements Action {
targetPreferences: ActionPreferences[] = [
{ insideOutsideType: null },
{ insideOutsideType: "inside" },
{ insideOutsideType: null },
];
constructor(private graph: Graph, private type: string) {
@ -56,25 +50,20 @@ class BringMoveSwap implements Action {
return sources;
}
private getDecorationStyles(): DecorationTypes {
let sourceStyle, sourceLineStyle;
private getDecorationStyles() {
let sourceStyle;
if (this.type === "bring") {
sourceStyle = this.graph.editStyles.referenced;
sourceLineStyle = this.graph.editStyles.referencedLine;
} else if (this.type === "swap") {
sourceStyle = this.graph.editStyles.pendingModification1;
sourceLineStyle = this.graph.editStyles.pendingLineModification1;
} else if (this.type === "move") {
sourceStyle = this.graph.editStyles.pendingDelete;
sourceLineStyle = this.graph.editStyles.pendingLineDelete;
} else {
throw Error(`Unknown type "${this.type}"`);
}
// NB this.type === "swap"
else {
sourceStyle = this.graph.editStyles.pendingModification1;
}
return {
sourceStyle,
sourceLineStyle,
destinationStyle: this.graph.editStyles.pendingModification0,
destinationLineStyle: this.graph.editStyles.pendingLineModification0,
};
}
@ -84,15 +73,10 @@ class BringMoveSwap implements Action {
) {
const decorationTypes = this.getDecorationStyles();
await Promise.all([
displayPendingEditDecorations(
sources,
decorationTypes.sourceStyle,
decorationTypes.sourceLineStyle
),
displayPendingEditDecorations(sources, decorationTypes.sourceStyle),
displayPendingEditDecorations(
destinations,
decorationTypes.destinationStyle,
decorationTypes.destinationLineStyle
decorationTypes.destinationStyle
),
]);
}
@ -107,54 +91,45 @@ class BringMoveSwap implements Action {
throw new Error("Targets must have same number of args");
}
const sourceText = source.selection.editor.document.getText(
source.selection.selection
);
const { containingListDelimiter } = destination.selectionContext;
const newText =
containingListDelimiter == null || destination.position === "contents"
? sourceText
: destination.position === "after"
? containingListDelimiter + sourceText
: sourceText + containingListDelimiter;
// Get text adjusting for destination position
const text = getTextWithPossibleDelimiter(source, destination);
// Add destination edit
const result = [
{
isSource: false,
range: destination.selection.selection as Range,
text,
editor: destination.selection.editor,
originalSelection: destination,
range: destination.selection.selection as Range,
text: newText,
isSource: false,
},
];
// Add source edit for move and swap
// Prevent multiple instances of the same expanded source.
if (this.type !== "bring" && !usedSources.includes(source)) {
let newText: string;
let text: string;
let range: Range;
if (this.type === "swap") {
newText = destination.selection.editor.document.getText(
text = destination.selection.editor.document.getText(
destination.selection.selection
);
range = source.selection.selection;
} else {
// NB: this.type === "move"
newText = "";
range = performInsideOutsideAdjustment(source, "outside").selection
.selection;
}
// NB: this.type === "move"
else {
text = "";
range = performOutsideAdjustment(source).selection.selection;
}
usedSources.push(source);
result.push({
isSource: true,
range,
text,
editor: source.selection.editor,
originalSelection: source,
range,
text: newText,
isSource: true,
});
}
@ -206,15 +181,13 @@ class BringMoveSwap implements Action {
thatMark
.filter(({ isSource }) => isSource)
.map(({ typedSelection }) => typedSelection),
decorationTypes.sourceStyle,
decorationTypes.sourceLineStyle
decorationTypes.sourceStyle
),
displayPendingEditDecorations(
thatMark
.filter(({ isSource }) => !isSource)
.map(({ typedSelection }) => typedSelection),
decorationTypes.destinationStyle,
decorationTypes.destinationLineStyle
decorationTypes.destinationStyle
),
]);
}

View File

@ -46,8 +46,7 @@ class CopyLines implements Action {
async run([targets]: [TypedSelection[]]): Promise<ActionReturnValue> {
await displayPendingEditDecorations(
targets,
this.graph.editStyles.referenced,
this.graph.editStyles.referencedLine
this.graph.editStyles.referenced
);
const thatMark = flatten(

View File

@ -18,10 +18,14 @@ export class FindInFiles implements Action {
async run([targets]: [TypedSelection[]]): Promise<ActionReturnValue> {
ensureSingleTarget(targets);
const { returnValue: query, thatMark } =
await this.graph.actions.getText.run([targets]);
const {
returnValue: [query],
thatMark,
} = await this.graph.actions.getText.run([targets]);
await commands.executeCommand("workbench.action.findInFiles", { query });
await commands.executeCommand("workbench.action.findInFiles", {
query,
});
return { returnValue: null, thatMark };
}

View File

@ -16,24 +16,21 @@ export default class GetText implements Action {
async run(
[targets]: [TypedSelection[]],
showDecorations = true
{ showDecorations = true } = {}
): Promise<ActionReturnValue> {
if (showDecorations) {
await displayPendingEditDecorations(
targets,
this.graph.editStyles.referenced,
this.graph.editStyles.referencedLine
this.graph.editStyles.referenced
);
}
const text = targets
.map((target) =>
target.selection.editor.document.getText(target.selection.selection)
)
.join("\n");
const returnValue = targets.map((target) =>
target.selection.editor.document.getText(target.selection.selection)
);
return {
returnValue: text,
returnValue,
thatMark: targets.map((target) => target.selection),
};
}

View File

@ -24,7 +24,6 @@ class InsertEmptyLines implements Action {
displayPendingEditDecorations(
targets,
this.graph.editStyles.referenced,
this.graph.editStyles.referencedLine
);
const edits = await runForEachEditor(

View File

@ -23,7 +23,6 @@ export default class Paste implements Action {
await displayPendingEditDecorations(
targets,
this.graph.editStyles.pendingModification0,
this.graph.editStyles.pendingLineModification0
);
const text = await env.clipboard.readText();
@ -78,7 +77,6 @@ export default class Paste implements Action {
await displayPendingEditDecorations(
thatMark.map(({ typedSelection }) => typedSelection),
this.graph.editStyles.pendingModification0,
this.graph.editStyles.pendingLineModification0
);
return { returnValue: null, thatMark };

View File

@ -0,0 +1,66 @@
import {
Action,
ActionPreferences,
ActionReturnValue,
Graph,
TypedSelection,
} from "../Types";
import displayPendingEditDecorations from "../editDisplayUtils";
import { runForEachEditor } from "../targetUtils";
import { flatten, zip } from "lodash";
import { maybeAddDelimiter } from "../getTextWithPossibleDelimiter";
import { performEditsAndUpdateSelections } from "../updateSelections";
export default class implements Action {
targetPreferences: ActionPreferences[] = [{ insideOutsideType: null }];
constructor(private graph: Graph) {
this.run = this.run.bind(this);
}
async run(
[targets]: [TypedSelection[]],
texts: string[]
): Promise<ActionReturnValue> {
await displayPendingEditDecorations(
targets,
this.graph.editStyles.pendingModification0
);
// Broadcast single text for each target
if (texts.length === 1) {
texts = Array(targets.length).fill(texts[0]);
}
if (targets.length !== texts.length) {
throw new Error("Targets and texts must have same length");
}
const edits = zip(targets, texts).map(([target, text]) => ({
editor: target!.selection.editor,
range: target!.selection.selection,
text: maybeAddDelimiter(text!, target!),
}));
const thatMark = flatten(
await runForEachEditor(
edits,
(edit) => edit.editor,
async (editor, edits) => {
const [updatedSelections] = await performEditsAndUpdateSelections(
editor,
edits,
[targets.map((target) => target.selection.selection)]
);
return updatedSelections.map((selection) => ({
editor,
selection,
}));
}
)
);
return { returnValue: null, thatMark };
}
}

View File

@ -56,7 +56,7 @@ class Scroll implements Action {
await displayDecorationsWhileRunningFunc(
targets.map((target) => target.selection),
this.graph.editStyles.referencedLine,
this.graph.editStyles.referenced.line,
scrollCallback,
showAdditionalHighlightBeforeScroll
);

View File

@ -41,8 +41,7 @@ export default class SetBreakpoint implements Action {
]): Promise<ActionReturnValue> {
await displayPendingEditDecorations(
targets,
this.graph.editStyles.referenced,
this.graph.editStyles.referencedLine
this.graph.editStyles.referenced
);
const lines = targets.flatMap((target) => {

View File

@ -18,8 +18,9 @@ export default class Copy implements Action {
const { returnValue, thatMark } = await this.graph.actions.getText.run([
targets,
]);
const text = returnValue.join("\n");
await env.clipboard.writeText(returnValue);
await env.clipboard.writeText(text);
return { returnValue: null, thatMark };
}

View File

@ -5,7 +5,10 @@ import {
Graph,
TypedSelection,
} from "../Types";
import { performInsideOutsideAdjustment } from "../performInsideOutsideAdjustment";
import {
performInsideAdjustment,
performOutsideAdjustment,
} from "../performInsideOutsideAdjustment";
export default class Cut implements Action {
targetPreferences: ActionPreferences[] = [{ insideOutsideType: null }];
@ -15,14 +18,10 @@ export default class Cut implements Action {
}
async run([targets]: [TypedSelection[]]): Promise<ActionReturnValue> {
await this.graph.actions.copy.run([
targets.map((target) => performInsideOutsideAdjustment(target, "inside")),
]);
await this.graph.actions.copy.run([targets.map(performInsideAdjustment)]);
const { thatMark } = await this.graph.actions.delete.run([
targets.map((target) =>
performInsideOutsideAdjustment(target, "outside")
),
targets.map(performOutsideAdjustment),
]);
return {

View File

@ -21,7 +21,6 @@ export default class Delete implements Action {
await displayPendingEditDecorations(
targets,
this.graph.editStyles.pendingDelete,
this.graph.editStyles.pendingLineDelete
);
const thatMark = flatten(

View File

@ -24,6 +24,7 @@ import {
} from "./InsertEmptyLines";
import GetText from "./GetText";
import { FindInFiles } from "./Find";
import ReplaceWithText from "./ReplaceWithText";
import { CopyLinesUp, CopyLinesDown } from "./CopyLines";
import SetBreakpoint from "./SetBreakpoint";
@ -56,6 +57,7 @@ class Actions implements ActionRecord {
move = new Move(this.graph);
outdentLines = new OutdentLines(this.graph);
paste = new Paste(this.graph);
replaceWithText = new ReplaceWithText(this.graph);
scrollToBottom = new ScrollToBottom(this.graph);
scrollToCenter = new ScrollToCenter(this.graph);
scrollToTop = new ScrollToTop(this.graph);

View File

@ -51,11 +51,12 @@ export default class Wrap implements Action {
);
editor.setDecorations(
this.graph.editStyles.justAdded,
this.graph.editStyles.justAdded.token,
updatedSelections
);
await decorationSleep();
editor.setDecorations(this.graph.editStyles.justAdded, []);
editor.setDecorations(this.graph.editStyles.justAdded.token, []);
return targets.map((target, index) => {
const start = updatedSelections[index * 2].start;

View File

@ -3,6 +3,7 @@ import { TypedSelection, SelectionWithEditor } from "./Types";
import { isLineSelectionType } from "./selectionType";
import { promisify } from "util";
import { runOnTargetsForEachEditor, runForEachEditor } from "./targetUtils";
import { EditStyle } from "./editStyles";
const sleep = promisify(setTimeout);
@ -17,19 +18,18 @@ export async function decorationSleep() {
export default async function displayPendingEditDecorations(
targets: TypedSelection[],
tokenStyle: TextEditorDecorationType,
lineStyle: TextEditorDecorationType
editStyle: EditStyle
) {
await runOnTargetsForEachEditor(targets, async (editor, selections) => {
editor.setDecorations(
tokenStyle,
editStyle.token,
selections
.filter((selection) => !isLineSelectionType(selection.selectionType))
.map((selection) => selection.selection.selection)
);
editor.setDecorations(
lineStyle,
editStyle.line,
selections
.filter((selection) => isLineSelectionType(selection.selectionType))
.map((selection) => {
@ -59,8 +59,8 @@ export default async function displayPendingEditDecorations(
await decorationSleep();
await runOnTargetsForEachEditor(targets, async (editor) => {
editor.setDecorations(tokenStyle, []);
editor.setDecorations(lineStyle, []);
editor.setDecorations(editStyle.token, []);
editor.setDecorations(editStyle.line, []);
});
}

View File

@ -1,80 +1,34 @@
import * as vscode from "vscode";
export default class EditStyles {
pendingDelete: vscode.TextEditorDecorationType;
pendingLineDelete: vscode.TextEditorDecorationType;
referenced: vscode.TextEditorDecorationType;
referencedLine: vscode.TextEditorDecorationType;
pendingModification0: vscode.TextEditorDecorationType;
pendingLineModification0: vscode.TextEditorDecorationType;
pendingModification1: vscode.TextEditorDecorationType;
pendingLineModification1: vscode.TextEditorDecorationType;
justAdded: vscode.TextEditorDecorationType;
export class EditStyle {
token: vscode.TextEditorDecorationType;
line: vscode.TextEditorDecorationType;
constructor() {
this.pendingDelete = vscode.window.createTextEditorDecorationType({
backgroundColor: new vscode.ThemeColor(
"cursorless.pendingDeleteBackground"
),
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
});
this.pendingLineDelete = vscode.window.createTextEditorDecorationType({
backgroundColor: new vscode.ThemeColor(
"cursorless.pendingDeleteBackground"
),
constructor(colorName: string) {
const options = {
backgroundColor: new vscode.ThemeColor(`cursorless.${colorName}`),
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
};
this.token = vscode.window.createTextEditorDecorationType(options);
this.line = vscode.window.createTextEditorDecorationType({
...options,
isWholeLine: true,
});
this.justAdded = vscode.window.createTextEditorDecorationType({
backgroundColor: new vscode.ThemeColor("cursorless.justAddedBackground"),
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
});
this.referenced = vscode.window.createTextEditorDecorationType({
backgroundColor: new vscode.ThemeColor("cursorless.referencedBackground"),
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
});
this.referencedLine = vscode.window.createTextEditorDecorationType({
backgroundColor: new vscode.ThemeColor("cursorless.referencedBackground"),
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
isWholeLine: true,
});
this.pendingModification0 = vscode.window.createTextEditorDecorationType({
backgroundColor: new vscode.ThemeColor(
"cursorless.pendingModification0Background"
),
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
});
this.pendingLineModification0 = vscode.window.createTextEditorDecorationType(
{
backgroundColor: new vscode.ThemeColor(
"cursorless.pendingModification0Background"
),
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
isWholeLine: true,
}
);
this.pendingModification1 = vscode.window.createTextEditorDecorationType({
backgroundColor: new vscode.ThemeColor(
"cursorless.pendingModification1Background"
),
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
});
this.pendingLineModification1 = vscode.window.createTextEditorDecorationType(
{
backgroundColor: new vscode.ThemeColor(
"cursorless.pendingModification1Background"
),
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
isWholeLine: true,
}
);
}
}
export class EditStyles {
pendingDelete: EditStyle;
referenced: EditStyle;
pendingModification0: EditStyle;
pendingModification1: EditStyle;
justAdded: EditStyle;
constructor() {
this.pendingDelete = new EditStyle("pendingDeleteBackground");
this.justAdded = new EditStyle("justAddedBackground");
this.referenced = new EditStyle("referencedBackground");
this.pendingModification0 = new EditStyle("pendingModification0Background");
this.pendingModification1 = new EditStyle("pendingModification1Background");
}
}

View File

@ -0,0 +1,29 @@
import { TypedSelection } from "./Types";
/** Get text from selection. Possibly add delimiter for positions before/after */
export function getTextWithPossibleDelimiter(
source: TypedSelection,
destination: TypedSelection
) {
const sourceText = source.selection.editor.document.getText(
source.selection.selection
);
return maybeAddDelimiter(sourceText, destination);
}
/** Possibly add delimiter for positions before/after */
export function maybeAddDelimiter(
sourceText: string,
destination: TypedSelection
) {
const { insideOutsideType, position } = destination;
const containingListDelimiter =
destination.selectionContext.containingListDelimiter;
return containingListDelimiter == null ||
position === "contents" ||
insideOutsideType === "inside"
? sourceText
: destination.position === "after"
? containingListDelimiter + sourceText
: sourceText + containingListDelimiter;
}

View File

@ -1,5 +1,5 @@
import Actions from "./actions";
import EditStyles from "./editStyles";
import { EditStyles } from "./editStyles";
import { Graph } from "./Types";
import { ConstructorMap } from "./makeGraph";

View File

@ -1,6 +1,5 @@
import update from "immutability-helper";
import { Selection } from "vscode";
import { InsideOutsideType, TypedSelection } from "./Types";
import { updateTypedSelectionRange } from "./selectionUtils";
export function performInsideOutsideAdjustment(
selection: TypedSelection,
@ -10,6 +9,17 @@ export function performInsideOutsideAdjustment(
selection.insideOutsideType ?? preferredInsideOutsideType;
if (insideOutsideType === "outside") {
if (selection.position !== "contents") {
const delimiterRange =
selection.position === "before"
? selection.selectionContext.leadingDelimiterRange
: selection.selectionContext.trailingDelimiterRange;
return delimiterRange == null
? selection
: updateTypedSelectionRange(selection, delimiterRange);
}
const usedSelection =
selection.selectionContext.outerSelection ??
selection.selection.selection;
@ -23,15 +33,16 @@ export function performInsideOutsideAdjustment(
? usedSelection.union(delimiterRange)
: usedSelection;
return update(selection, {
selection: {
selection: (s) =>
s.isReversed
? new Selection(range.end, range.start)
: new Selection(range.start, range.end),
},
});
return updateTypedSelectionRange(selection, range);
}
return selection;
}
export function performInsideAdjustment(selection: TypedSelection) {
return performInsideOutsideAdjustment(selection, "inside");
}
export function performOutsideAdjustment(selection: TypedSelection) {
return performInsideOutsideAdjustment(selection, "outside");
}

View File

@ -1,8 +1,7 @@
import { concat, range, zip } from "lodash";
import update from "immutability-helper";
import { SyntaxNode } from "web-tree-sitter";
import * as vscode from "vscode";
import { Selection, Range, Position } from "vscode";
import { Selection, Range, Position, Location } from "vscode";
import { nodeMatchers } from "./languages";
import {
Mark,
@ -14,6 +13,7 @@ import {
SelectionWithEditor,
Target,
TypedSelection,
Modifier,
} from "./Types";
import { performInsideOutsideAdjustment } from "./performInsideOutsideAdjustment";
import { SUBWORD_MATCHER } from "./constants";
@ -219,7 +219,7 @@ function transformSelection(
return [{ selection, context: {} }];
case "containingScope":
var node: SyntaxNode | null = context.getNodeAtLocation(
new vscode.Location(selection.editor.document.uri, selection.selection)
new Location(selection.editor.document.uri, selection.selection)
);
const nodeMatcher =
@ -304,7 +304,7 @@ function createTypedSelection(
selection: SelectionWithEditor,
selectionContext: SelectionContext
): TypedSelection {
const { selectionType, insideOutsideType, position } = target;
const { selectionType, insideOutsideType, position, modifier } = target;
const { document } = selection.editor;
switch (selectionType) {
@ -314,7 +314,11 @@ function createTypedSelection(
selectionType,
position,
insideOutsideType,
selectionContext: getTokenSelectionContext(selection, selectionContext),
selectionContext: getTokenSelectionContext(
selection,
modifier,
selectionContext
),
};
case "line": {
@ -445,6 +449,7 @@ function performPositionAdjustment(
function getTokenSelectionContext(
selection: SelectionWithEditor,
modifier: Modifier,
selectionContext: SelectionContext
): SelectionContext {
if (!isSelectionContextEmpty(selectionContext)) {
@ -482,19 +487,20 @@ function getTokenSelectionContext(
)
: null;
// Didn't find any delimiters
if (leadingDelimiterRange == null && trailingDelimiterRange == null) {
return selectionContext;
if (
leadingDelimiterRange != null ||
trailingDelimiterRange != null ||
modifier.type !== "subpiece"
) {
return {
isInDelimitedList: true,
containingListDelimiter: " ",
leadingDelimiterRange,
trailingDelimiterRange,
};
}
return {
isInDelimitedList: true,
containingListDelimiter: document.getText(
(trailingDelimiterRange ?? leadingDelimiterRange)!
),
leadingDelimiterRange,
trailingDelimiterRange,
};
return selectionContext;
}
// TODO Clean this up once we have rich targets and better polymorphic

31
src/selectionUtils.ts Normal file
View File

@ -0,0 +1,31 @@
import { Range, Selection, Position } from "vscode";
import update from "immutability-helper";
import { TypedSelection } from "./Types";
export function selectionFromRange(
range: Range,
isReversed: boolean
): Selection {
const { start, end } = range;
return isReversed ? new Selection(end, start) : new Selection(start, end);
}
/**
* Returns a copy of the given typed selection so that the new selection has the new given range
* preserving the direction of the original selection
*
* @param selection The original typed selection to Update
* @param range The new range for the given selection
* @returns The updated typed selection
*/
export function updateTypedSelectionRange(
selection: TypedSelection,
range: Range
): TypedSelection {
return update(selection, {
selection: {
selection: () =>
selectionFromRange(range, selection.selection.selection.isReversed),
},
});
}