Skip to content
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

Paredit Multicursor - SelectCurrentForm #2477

Merged
merged 10 commits into from
Apr 1, 2024
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ Changes to Calva.

## [Unreleased]

- [Implement experimental support for multicursor rewrap commands](https://github.com/BetterThanTomorrow/calva/issues/2448). Enable `calva.paredit.multicursor` in your settings to try it out. Closes [#2473](https://github.com/BetterThanTomorrow/calva/issues/2473)
- [Implement experimental support for multicursor selectCurrentForm command](https://github.com/BetterThanTomorrow/calva/issues/2476). Enable `calva.paredit.multicursor` in your settings to try it out. Closes [#2476](https://github.com/BetterThanTomorrow/calva/issues/2476)
- [Implement experimental support for multicursor rewrap commands](https://github.com/BetterThanTomorrow/calva/issues/2473). Enable `calva.paredit.multicursor` in your settings to try it out. Closes [#2473](https://github.com/BetterThanTomorrow/calva/issues/2473)

## [2.0.432] - 2024-03-26

Expand Down
2 changes: 1 addition & 1 deletion docs/site/paredit.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Default keybinding | Action | Description
You can have the *kill* commands always copy the deleted code to the clipboard by setting `calva.paredit.killAlsoCutsToClipboard` to `true`. If you want to do this more on-demand, you can kill text by using the [selection commands](#selecting) and then *Cut* once you have the selection.

!!! Note "clojure-lsp drag fwd/back overlap"
As an experimental feature, the two commands for dragging forms forward and backward have clojure-lsp alternativs. See the [clojure-lsp](clojure-lsp.md#clojure-lsp-drag-fwdback) page.
As an experimental feature, the two commands for dragging forms forward and backward have clojure-lsp alternatives. See the [clojure-lsp](clojure-lsp.md#clojure-lsp-drag-fwdback) page.

### Drag bindings forward/backward

Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1343,12 +1343,6 @@
"enablement": "calva:connected",
"category": "Calva"
},
{
"command": "calva.selectCurrentForm",
"title": "Select Current Form",
"category": "Calva",
"enablement": "editorLangId == clojure"
},
riotrah marked this conversation as resolved.
Show resolved Hide resolved
{
"command": "calva.clearInlineResults",
"title": "Clear Inline Evaluation Results",
Expand Down Expand Up @@ -1567,6 +1561,12 @@
"title": "Move Cursor Forward to List End/Close",
"enablement": "editorLangId == clojure"
},
{
"category": "Calva Paredit",
"command": "calva.selectCurrentForm",
"title": "Select Current Form",
"enablement": "editorLangId == clojure"
},
{
"category": "Calva Paredit",
"command": "paredit.selectForwardSexp",
Expand Down
28 changes: 28 additions & 0 deletions src/cursor-doc/paredit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,34 @@ export function selectRange(doc: EditableDocument, ranges: ModelEditRange[]) {
growSelectionStack(doc, ranges);
}

export function selectCurrentForm(
doc: EditableDocument,
topLevel: boolean,
selections = doc.selections
) {
const newSels = selections.map((sel) => {
const selection = sel;
if (selection.isCursor) {
let codeSelection;
const cursor = doc.getTokenCursor(selection.active);
const range = topLevel
? cursor.rangeForDefun(selection.active)
: cursor.rangeForCurrentForm(selection.active);
if (range) {
codeSelection = new ModelEditSelection(range[0], range[1]);
} else {
codeSelection = undefined;
}
if (codeSelection) {
return codeSelection;
}
}
return sel;
});

growSelectionStack(doc, newSels.map(_.property('asDirectedRange')));
}

export function selectRangeForward(
doc: EditableDocument,
ranges: ModelEditRange[],
Expand Down
93 changes: 93 additions & 0 deletions src/extension-test/unit/paredit/commands-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,99 @@ describe('paredit commands', () => {
});

describe('selection', () => {
describe('selectCurrentForm', () => {
it('Single-cursor: handles cases like reader tags/metadata + keeps other selections ', () => {
const a = docFromTextNotation(
'(defn|1 [a b]•(let [^js a|a #p (+ a)•b b]•{:a aa•:b b}))•(:a)'
);
const aSelections = a.selections;
const b = docFromTextNotation(
'(defn [a b]•(let [|^js aa| #p (+ a)•b b]•{:a aa•:b b}))•(:a)'
);
handlers.selectCurrentForm(a, false);
expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b));
expect(a.selectionsStack).toEqual([aSelections, b.selections]);
});
it('Multi-cursor: handles cases like reader tags/metadata + keeps other selections ', () => {
const a = docFromTextNotation(
'(defn|1 |2[a b]•(let [|3^js aa |4#p (+ a)•<5b b<5]•{:a aa•:b b}))•(:|a)'
);
const aSelections = a.selections;
const b = docFromTextNotation(
'(|1defn|1 |2[a b]|2•(let [|3^js aa|3 |4#p (+ a)|4•<5b b<5]•{:a aa•:b b}))•(|:a|)'
);
handlers.selectCurrentForm(a, true);
expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b));
expect(a.selectionsStack).toEqual([aSelections, b.selections]);
});

it('Single-cursor: handles cursor at a distance from form', () => {
const a = docFromTextNotation('[|1 a b |2c d { e f}|3 g |]');
const aSelections = a.selections;
const b = docFromTextNotation('[ a b c d { e f} |g| ]');
handlers.selectCurrentForm(a, false);
expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b));
expect(a.selectionsStack).toEqual([aSelections, b.selections]);
});
it('Multi-cursor: handles cursor at a distance from form', () => {
const a = docFromTextNotation('[|1 a b |2c d { e f}|3 g |]');
const aSelections = a.selections;
const b = docFromTextNotation('[ |1a|1 b |2c|2 d |3{ e f}|3 |g| ]');
handlers.selectCurrentForm(a, true);
expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b));
expect(a.selectionsStack).toEqual([aSelections, b.selections]);
});

it('Single-cursor: collapses overlapping selections', () => {
const a = docFromTextNotation(
'(de|1fn| [a b]•(let [^js aa #p (+ a)•b b]•{:a aa•:b b}))•(:a)'
);
const aSelections = a.selections;
const b = docFromTextNotation(
'(|defn| [a b]•(let [^js aa #p (+ a)•b b]•{:a aa•:b b}))•(:a)'
);
handlers.selectCurrentForm(a, false);
expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b));
expect(a.selectionsStack).toEqual([aSelections, b.selections]);
});
it('Multi-cursor: collapses overlapping selections', () => {
const a = docFromTextNotation(
'(de|5fn|1 |2[a b]•(let [|3^js aa |4#p (+ a)•<5b b<5]•{:a aa•:b b}))•(:|a)'
);
const aSelections = a.selections;
const b = docFromTextNotation(
'(|1defn|1 |2[a b]|2•(let [|3^js aa|3 |4#p (+ a)|4•<5b b<5]•{:a aa•:b b}))•(|:a|)'
);
handlers.selectCurrentForm(a, true);
expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b));
expect(a.selectionsStack).toEqual([aSelections, b.selections]);
});

it('Single-cursor: collapses overlapping selections preferring the larger one', () => {
const a = docFromTextNotation(
'(de|1fn| [a b]•(let [^js aa #p (+ a)•b b]•{:a aa•:b b}))•(:a)'
);
const aSelections = a.selections;
const b = docFromTextNotation(
'(|defn| [a b]•(let [^js aa #p (+ a)•b b]•{:a aa•:b b}))•(:a)'
);
handlers.selectCurrentForm(a, false);
expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b));
expect(a.selectionsStack).toEqual([aSelections, b.selections]);
});
it('Multi-cursor: collapses overlapping selections preferring the larger one', () => {
const a = docFromTextNotation(
'(defn [a b]•(let [^js aa #p (+ a)•b |b]|1•|3{:a a|2a•:b b}))•(:a)'
);
const aSelections = a.selections;
const b = docFromTextNotation(
'(defn [a b]•(let |1[^js aa #p (+ a)•b b]|1•|3{:a aa•:b b}|3))•(:a)'
);
handlers.selectCurrentForm(a, true);
expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b));
expect(a.selectionsStack).toEqual([aSelections, b.selections]);
});
});
describe('rangeForDefun', () => {
it('Single-cursor:', () => {
const a = docFromTextNotation(
Expand Down
2 changes: 0 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import * as definition from './providers/definition';
import { CalvaSignatureHelpProvider } from './providers/signature';
import testRunner from './testRunner';
import annotations from './providers/annotations';
import * as select from './select';
import eval from './evaluate';
import refresh from './refresh';
import * as greetings from './greet';
Expand Down Expand Up @@ -258,7 +257,6 @@ async function activate(context: vscode.ExtensionContext) {
runCustomREPLCommand: snippets.evaluateCustomCodeSnippetCommand,
runNamespaceTests: () => testRunner.runNamespaceTestsCommand(testController),
runTestUnderCursor: () => testRunner.runTestUnderCursorCommand(testController),
selectCurrentForm: select.selectCurrentForm,
sendCurrentFormToOutputWindow: outputWindow.appendCurrentForm,
openFiddleForSourceFile: fiddleFiles.openFiddleForSourceFile,
evaluateFiddleForSourceFile: fiddleFiles.evaluateFiddleForSourceFile,
Expand Down
3 changes: 3 additions & 0 deletions src/paredit/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ export function openList(doc: EditableDocument, isMulti: boolean = false) {

// SELECTION

export function selectCurrentForm(doc: EditableDocument, isMulti: boolean = false) {
paredit.selectCurrentForm(doc, false, isMulti ? doc.selections : [doc.selections[0]]);
}
export function rangeForDefun(doc: EditableDocument, isMulti: boolean) {
const selections = isMulti ? doc.selections : [doc.selections[0]];
const ranges = selections.map((s) => paredit.rangeForDefun(doc, s.active));
Expand Down
7 changes: 7 additions & 0 deletions src/paredit/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ const pareditCommands: PareditCommand[] = [
},

// SELECTING
{
command: 'calva.selectCurrentForm', // legacy command id for backward compat
handler: (doc: EditableDocument) => {
const isMulti = multiCursorEnabled();
handlers.selectCurrentForm(doc, isMulti);
},
},
{
command: 'paredit.rangeForDefun',
handler: (doc: EditableDocument) => {
Expand Down
25 changes: 0 additions & 25 deletions src/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,28 +37,3 @@ export function getEnclosingFormSelection(
}
}
}

function selectForm(
document = {},
selectionFn: (
doc: vscode.TextDocument,
pos: vscode.Position,
topLevel: boolean
) => vscode.Selection | undefined,
toplevel: boolean
) {
const editor = util.getActiveTextEditor(),
doc = util.getDocument(document),
selection = editor.selections[0];

if (selection.isEmpty) {
const codeSelection = selectionFn(doc, selection.active, toplevel);
if (codeSelection) {
editor.selections = [codeSelection];
}
}
}

export function selectCurrentForm(document = {}) {
selectForm(document, getFormSelection, false);
}