Skip to content

Commit

Permalink
Paredit kill (Partial implementation) (#1425)
Browse files Browse the repository at this point in the history
* Implement pareditKill

https://github.com/emacsmirror/paredit/blob/d0b1a2f42fb47efc8392763d6487fd027e3a2955/paredit.el#L1409
https://github.com/emacsmirror/paredit/blob/d0b1a2f42fb47efc8392763d6487fd027e3a2955/paredit.el#L353
http://danmidwood.com/content/2014/11/21/animated-paredit.html

From the emacs documentation:

(foo bar)|     ; Useless comment!
  ->
(foo bar)|

(|foo bar)     ; Useful comment!
  ->
(|)     ; Useful comment!

|(foo bar)     ; Useless line!
  ->
|

(foo "|bar baz"
     quux)
  ->
(foo "|"
     quux)

* keybinding
  • Loading branch information
dandavison authored Aug 14, 2023
1 parent 701c69a commit bf71260
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 0 deletions.
5 changes: 5 additions & 0 deletions keybindings.json
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,11 @@
"command": "emacs-mcx.paredit.markSexp",
"when": "editorTextFocus"
},
{
"key": "ctrl+shift+k",
"command": "emacs-mcx.paredit.pareditKill",
"when": "editorTextFocus"
},
{
"key": "ctrl+meta+k",
"command": "emacs-mcx.paredit.killSexp",
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5000,6 +5000,11 @@
"command": "emacs-mcx.paredit.markSexp",
"when": "editorTextFocus && config.emacs-mcx.useMetaPrefixCtrlLeftBracket"
},
{
"key": "ctrl+shift+k",
"command": "emacs-mcx.paredit.pareditKill",
"when": "editorTextFocus"
},
{
"key": "ctrl+alt+k",
"command": "emacs-mcx.paredit.killSexp",
Expand Down
67 changes: 67 additions & 0 deletions src/commands/paredit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,70 @@ export class BackwardKillSexp extends KillYankCommand {
revealPrimaryActive(textEditor);
}
}

export class PareditKill extends KillYankCommand {
public readonly id = "paredit.pareditKill";

public async execute(
textEditor: TextEditor,
isInMarkMode: boolean,
prefixArgument: number | undefined
): Promise<void> {
const repeat = prefixArgument === undefined ? 1 : prefixArgument;
if (repeat <= 0) {
return;
}

const doc = textEditor.document;
const src = doc.getText(); // TODO: doc.getText is called a second time, in makeSexpTravelFunc

const killRanges = textEditor.selections.map((selection) => {
const navigatorFn: PareditNavigatorFn = (ast: paredit.AST, idx: number) => {
while (src[idx] == " " || src[idx] == "\t") {
idx += 1;
}
const lineNumber = selection.active.line;
const line = textEditor.document.lineAt(lineNumber);
const lineEnd = doc.offsetAt(line.range.end);
if (idx >= lineEnd) {
const nextLine = textEditor.document.lineAt(lineNumber + 1);
return doc.offsetAt(nextLine.range.start);
} else {
let curr = idx;
let prev;
do {
prev = curr;
curr = this.indexAfterKillSexp(ast, prev);
} while (prev < curr && curr < lineEnd);
return curr;
}
};
const travelSexp = makeSexpTravelFunc(doc, navigatorFn);
const newActivePosition = travelSexp(selection.active, repeat);
return new Range(selection.anchor, newActivePosition);
});

await this.killYanker.kill(killRanges.filter((range) => !range.isEmpty));

revealPrimaryActive(textEditor);
}

private indexAfterKillSexp(ast: paredit.AST, currentIdx: number): number {
const src = ""; // src argument is not used by paredit.editor.killSexp
const edits = paredit.editor.killSexp(ast, src, currentIdx, { count: 1 });
if (edits === null) {
return currentIdx;
} else if (edits.changes[0] !== undefined) {
const [op, at, nChars] = edits.changes[0];
if (op == "remove" && at == currentIdx && edits.newIndex == currentIdx && typeof nChars == "number") {
return at + nChars;
} else {
throw new Error(
"Expected paredit.editor.killSexp to return a single deletion starting at the current cursor position and leaving the cursor at the same position"
);
}
} else {
throw new Error("Expected paredit.editor.killSexp to return a single change");
}
}
}
1 change: 1 addition & 0 deletions src/emulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export class EmacsEmulator implements IEmacsController, vscode.Disposable {
this.commandRegistry.register(new PareditCommands.MarkSexp(this));
this.commandRegistry.register(new PareditCommands.KillSexp(this, killYanker));
this.commandRegistry.register(new PareditCommands.BackwardKillSexp(this, killYanker));
this.commandRegistry.register(new PareditCommands.PareditKill(this, killYanker));

this.commandRegistry.register(new AddSelectionToNextFindMatch(this));
this.commandRegistry.register(new AddSelectionToPreviousFindMatch(this));
Expand Down
4 changes: 4 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,10 @@ export function activate(context: vscode.ExtensionContext): void {
emulator.runCommand("paredit.killSexp");
});

registerEmulatorCommand("emacs-mcx.paredit.pareditKill", (emulator) => {
emulator.runCommand("paredit.pareditKill");
});

registerEmulatorCommand("emacs-mcx.paredit.backwardKillSexp", (emulator) => {
emulator.runCommand("paredit.backwardKillSexp");
});
Expand Down
133 changes: 133 additions & 0 deletions src/test/suite/commands/paredit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,139 @@ suite("paredit.backward-kill-sexp", () => {
});
});

suite("paredit.paredit-kill kill to end-of-line", () => {
// https://github.com/emacsmirror/paredit/blob/d0b1a2f42fb47efc8392763d6487fd027e3a2955/paredit.el#L353
// ("(foo bar)| ; Useless comment!"
// "(foo bar)|")
const initialText = "(foo bar) ; Useless comment!";
let activeTextEditor: TextEditor;
let emulator: EmacsEmulator;

setup(async () => {
activeTextEditor = await setupWorkspace(initialText);
const killRing = new KillRing(60);
emulator = new EmacsEmulator(activeTextEditor, killRing);
});

teardown(cleanUpWorkspace);

test("kill to end-of-line", async () => {
setEmptyCursors(activeTextEditor, [0, 9]);

await emulator.runCommand("paredit.pareditKill");

assertTextEqual(activeTextEditor, "(foo bar)");
assertCursorsEqual(activeTextEditor, [0, 9]);

await clearTextEditor(activeTextEditor);

// TODO
// await emulator.runCommand("yank");
// assertTextEqual(activeTextEditor, initialText);
});
});

suite("paredit.paredit-kill kill inside sexp", () => {
// https://github.com/emacsmirror/paredit/blob/d0b1a2f42fb47efc8392763d6487fd027e3a2955/paredit.el#L353
// ("(|foo bar) ; Useful comment!"
// "(|) ; Useful comment!")
const initialText = "(foo bar) ; Useful comment!";
let activeTextEditor: TextEditor;
let emulator: EmacsEmulator;

setup(async () => {
activeTextEditor = await setupWorkspace(initialText);
const killRing = new KillRing(60);
emulator = new EmacsEmulator(activeTextEditor, killRing);
});

teardown(cleanUpWorkspace);

test("kill inside sexp", async () => {
setEmptyCursors(activeTextEditor, [0, 1]);

await emulator.runCommand("paredit.pareditKill");

assertTextEqual(activeTextEditor, "() ; Useful comment!");
assertCursorsEqual(activeTextEditor, [0, 1]);

await clearTextEditor(activeTextEditor);

// TODO
// await emulator.runCommand("yank");
// assertTextEqual(activeTextEditor, initialText);
});
});

suite("paredit.paredit-kill kill entire line", () => {
// https://github.com/emacsmirror/paredit/blob/d0b1a2f42fb47efc8392763d6487fd027e3a2955/paredit.el#L353
// ("|(foo bar) ; Useless line!"
// "|")
const initialText = "(foo bar) ; Useless line!";
let activeTextEditor: TextEditor;
let emulator: EmacsEmulator;

setup(async () => {
activeTextEditor = await setupWorkspace(initialText);
const killRing = new KillRing(60);
emulator = new EmacsEmulator(activeTextEditor, killRing);
});

teardown(cleanUpWorkspace);

test("kill entire line", async () => {
setEmptyCursors(activeTextEditor, [0, 0]);

await emulator.runCommand("paredit.pareditKill");

assertTextEqual(activeTextEditor, "");
assertCursorsEqual(activeTextEditor, [0, 0]);

await clearTextEditor(activeTextEditor);

// TODO
// await emulator.runCommand("yank");
// assertTextEqual(activeTextEditor, initialText);
});
});

suite("paredit.paredit-kill kill inside string", () => {
// https://github.com/emacsmirror/paredit/blob/d0b1a2f42fb47efc8392763d6487fd027e3a2955/paredit.el#L353
// ("(foo \"|bar baz\"\n quux)"
// "(foo \"|\"\n quux)"))
const initialText = `(foo "bar baz"
quux)`;
let activeTextEditor: TextEditor;
let emulator: EmacsEmulator;

setup(async () => {
activeTextEditor = await setupWorkspace(initialText);
const killRing = new KillRing(60);
emulator = new EmacsEmulator(activeTextEditor, killRing);
});

teardown(cleanUpWorkspace);

test("kill inside string", async () => {
setEmptyCursors(activeTextEditor, [0, 6]);

await emulator.runCommand("paredit.pareditKill");

assertTextEqual(
activeTextEditor,
`(foo ""
quux)`
);
assertCursorsEqual(activeTextEditor, [0, 6]);

await clearTextEditor(activeTextEditor);

// TODO
// await emulator.runCommand("yank");
// assertTextEqual(activeTextEditor, initialText);
});
});

suite("paredit.mark-sexp", () => {
const initialText = `(
(
Expand Down

0 comments on commit bf71260

Please sign in to comment.