Skip to content

Commit

Permalink
Merge pull request #2477 from BetterThanTomorrow/wip/rayat/paredit/mu…
Browse files Browse the repository at this point in the history
…lticursor/select-current-form

Paredit Multicursor - SelectCurrentForm

* Fixes #2476
  • Loading branch information
PEZ committed Apr 1, 2024
2 parents 8d83117 + 5e466d8 commit 19e4689
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 35 deletions.
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"
},
{
"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);
}

0 comments on commit 19e4689

Please sign in to comment.