Skip to content

Commit 19e4689

Browse files
authored
Merge pull request #2477 from BetterThanTomorrow/wip/rayat/paredit/multicursor/select-current-form
Paredit Multicursor - SelectCurrentForm * Fixes #2476
2 parents 8d83117 + 5e466d8 commit 19e4689

File tree

9 files changed

+140
-35
lines changed

9 files changed

+140
-35
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ Changes to Calva.
44

55
## [Unreleased]
66

7-
- [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)
7+
- [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)
8+
- [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)
89

910
## [2.0.432] - 2024-03-26
1011

docs/site/paredit.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ Default keybinding | Action | Description
125125
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.
126126

127127
!!! Note "clojure-lsp drag fwd/back overlap"
128-
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.
128+
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.
129129

130130
### Drag bindings forward/backward
131131

package.json

+6-6
Original file line numberDiff line numberDiff line change
@@ -1343,12 +1343,6 @@
13431343
"enablement": "calva:connected",
13441344
"category": "Calva"
13451345
},
1346-
{
1347-
"command": "calva.selectCurrentForm",
1348-
"title": "Select Current Form",
1349-
"category": "Calva",
1350-
"enablement": "editorLangId == clojure"
1351-
},
13521346
{
13531347
"command": "calva.clearInlineResults",
13541348
"title": "Clear Inline Evaluation Results",
@@ -1567,6 +1561,12 @@
15671561
"title": "Move Cursor Forward to List End/Close",
15681562
"enablement": "editorLangId == clojure"
15691563
},
1564+
{
1565+
"category": "Calva Paredit",
1566+
"command": "calva.selectCurrentForm",
1567+
"title": "Select Current Form",
1568+
"enablement": "editorLangId == clojure"
1569+
},
15701570
{
15711571
"category": "Calva Paredit",
15721572
"command": "paredit.selectForwardSexp",

src/cursor-doc/paredit.ts

+28
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,34 @@ export function selectRange(doc: EditableDocument, ranges: ModelEditRange[]) {
4949
growSelectionStack(doc, ranges);
5050
}
5151

52+
export function selectCurrentForm(
53+
doc: EditableDocument,
54+
topLevel: boolean,
55+
selections = doc.selections
56+
) {
57+
const newSels = selections.map((sel) => {
58+
const selection = sel;
59+
if (selection.isCursor) {
60+
let codeSelection;
61+
const cursor = doc.getTokenCursor(selection.active);
62+
const range = topLevel
63+
? cursor.rangeForDefun(selection.active)
64+
: cursor.rangeForCurrentForm(selection.active);
65+
if (range) {
66+
codeSelection = new ModelEditSelection(range[0], range[1]);
67+
} else {
68+
codeSelection = undefined;
69+
}
70+
if (codeSelection) {
71+
return codeSelection;
72+
}
73+
}
74+
return sel;
75+
});
76+
77+
growSelectionStack(doc, newSels.map(_.property('asDirectedRange')));
78+
}
79+
5280
export function selectRangeForward(
5381
doc: EditableDocument,
5482
ranges: ModelEditRange[],

src/extension-test/unit/paredit/commands-test.ts

+93
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,99 @@ describe('paredit commands', () => {
333333
});
334334

335335
describe('selection', () => {
336+
describe('selectCurrentForm', () => {
337+
it('Single-cursor: handles cases like reader tags/metadata + keeps other selections ', () => {
338+
const a = docFromTextNotation(
339+
'(defn|1 [a b]•(let [^js a|a #p (+ a)•b b]•{:a aa•:b b}))•(:a)'
340+
);
341+
const aSelections = a.selections;
342+
const b = docFromTextNotation(
343+
'(defn [a b]•(let [|^js aa| #p (+ a)•b b]•{:a aa•:b b}))•(:a)'
344+
);
345+
handlers.selectCurrentForm(a, false);
346+
expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b));
347+
expect(a.selectionsStack).toEqual([aSelections, b.selections]);
348+
});
349+
it('Multi-cursor: handles cases like reader tags/metadata + keeps other selections ', () => {
350+
const a = docFromTextNotation(
351+
'(defn|1 |2[a b]•(let [|3^js aa |4#p (+ a)•<5b b<5]•{:a aa•:b b}))•(:|a)'
352+
);
353+
const aSelections = a.selections;
354+
const b = docFromTextNotation(
355+
'(|1defn|1 |2[a b]|2•(let [|3^js aa|3 |4#p (+ a)|4•<5b b<5]•{:a aa•:b b}))•(|:a|)'
356+
);
357+
handlers.selectCurrentForm(a, true);
358+
expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b));
359+
expect(a.selectionsStack).toEqual([aSelections, b.selections]);
360+
});
361+
362+
it('Single-cursor: handles cursor at a distance from form', () => {
363+
const a = docFromTextNotation('[|1 a b |2c d { e f}|3 g |]');
364+
const aSelections = a.selections;
365+
const b = docFromTextNotation('[ a b c d { e f} |g| ]');
366+
handlers.selectCurrentForm(a, false);
367+
expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b));
368+
expect(a.selectionsStack).toEqual([aSelections, b.selections]);
369+
});
370+
it('Multi-cursor: handles cursor at a distance from form', () => {
371+
const a = docFromTextNotation('[|1 a b |2c d { e f}|3 g |]');
372+
const aSelections = a.selections;
373+
const b = docFromTextNotation('[ |1a|1 b |2c|2 d |3{ e f}|3 |g| ]');
374+
handlers.selectCurrentForm(a, true);
375+
expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b));
376+
expect(a.selectionsStack).toEqual([aSelections, b.selections]);
377+
});
378+
379+
it('Single-cursor: collapses overlapping selections', () => {
380+
const a = docFromTextNotation(
381+
'(de|1fn| [a b]•(let [^js aa #p (+ a)•b b]•{:a aa•:b b}))•(:a)'
382+
);
383+
const aSelections = a.selections;
384+
const b = docFromTextNotation(
385+
'(|defn| [a b]•(let [^js aa #p (+ a)•b b]•{:a aa•:b b}))•(:a)'
386+
);
387+
handlers.selectCurrentForm(a, false);
388+
expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b));
389+
expect(a.selectionsStack).toEqual([aSelections, b.selections]);
390+
});
391+
it('Multi-cursor: collapses overlapping selections', () => {
392+
const a = docFromTextNotation(
393+
'(de|5fn|1 |2[a b]•(let [|3^js aa |4#p (+ a)•<5b b<5]•{:a aa•:b b}))•(:|a)'
394+
);
395+
const aSelections = a.selections;
396+
const b = docFromTextNotation(
397+
'(|1defn|1 |2[a b]|2•(let [|3^js aa|3 |4#p (+ a)|4•<5b b<5]•{:a aa•:b b}))•(|:a|)'
398+
);
399+
handlers.selectCurrentForm(a, true);
400+
expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b));
401+
expect(a.selectionsStack).toEqual([aSelections, b.selections]);
402+
});
403+
404+
it('Single-cursor: collapses overlapping selections preferring the larger one', () => {
405+
const a = docFromTextNotation(
406+
'(de|1fn| [a b]•(let [^js aa #p (+ a)•b b]•{:a aa•:b b}))•(:a)'
407+
);
408+
const aSelections = a.selections;
409+
const b = docFromTextNotation(
410+
'(|defn| [a b]•(let [^js aa #p (+ a)•b b]•{:a aa•:b b}))•(:a)'
411+
);
412+
handlers.selectCurrentForm(a, false);
413+
expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b));
414+
expect(a.selectionsStack).toEqual([aSelections, b.selections]);
415+
});
416+
it('Multi-cursor: collapses overlapping selections preferring the larger one', () => {
417+
const a = docFromTextNotation(
418+
'(defn [a b]•(let [^js aa #p (+ a)•b |b]|1•|3{:a a|2a•:b b}))•(:a)'
419+
);
420+
const aSelections = a.selections;
421+
const b = docFromTextNotation(
422+
'(defn [a b]•(let |1[^js aa #p (+ a)•b b]|1•|3{:a aa•:b b}|3))•(:a)'
423+
);
424+
handlers.selectCurrentForm(a, true);
425+
expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b));
426+
expect(a.selectionsStack).toEqual([aSelections, b.selections]);
427+
});
428+
});
336429
describe('rangeForDefun', () => {
337430
it('Single-cursor:', () => {
338431
const a = docFromTextNotation(

src/extension.ts

-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import * as definition from './providers/definition';
1616
import { CalvaSignatureHelpProvider } from './providers/signature';
1717
import testRunner from './testRunner';
1818
import annotations from './providers/annotations';
19-
import * as select from './select';
2019
import eval from './evaluate';
2120
import refresh from './refresh';
2221
import * as greetings from './greet';
@@ -258,7 +257,6 @@ async function activate(context: vscode.ExtensionContext) {
258257
runCustomREPLCommand: snippets.evaluateCustomCodeSnippetCommand,
259258
runNamespaceTests: () => testRunner.runNamespaceTestsCommand(testController),
260259
runTestUnderCursor: () => testRunner.runTestUnderCursorCommand(testController),
261-
selectCurrentForm: select.selectCurrentForm,
262260
sendCurrentFormToOutputWindow: outputWindow.appendCurrentForm,
263261
openFiddleForSourceFile: fiddleFiles.openFiddleForSourceFile,
264262
evaluateFiddleForSourceFile: fiddleFiles.evaluateFiddleForSourceFile,

src/paredit/commands.ts

+3
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ export function openList(doc: EditableDocument, isMulti: boolean = false) {
5656

5757
// SELECTION
5858

59+
export function selectCurrentForm(doc: EditableDocument, isMulti: boolean = false) {
60+
paredit.selectCurrentForm(doc, false, isMulti ? doc.selections : [doc.selections[0]]);
61+
}
5962
export function rangeForDefun(doc: EditableDocument, isMulti: boolean) {
6063
const selections = isMulti ? doc.selections : [doc.selections[0]];
6164
const ranges = selections.map((s) => paredit.rangeForDefun(doc, s.active));

src/paredit/extension.ts

+7
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ const pareditCommands: PareditCommand[] = [
114114
},
115115

116116
// SELECTING
117+
{
118+
command: 'calva.selectCurrentForm', // legacy command id for backward compat
119+
handler: (doc: EditableDocument) => {
120+
const isMulti = multiCursorEnabled();
121+
handlers.selectCurrentForm(doc, isMulti);
122+
},
123+
},
117124
{
118125
command: 'paredit.rangeForDefun',
119126
handler: (doc: EditableDocument) => {

src/select.ts

-25
Original file line numberDiff line numberDiff line change
@@ -37,28 +37,3 @@ export function getEnclosingFormSelection(
3737
}
3838
}
3939
}
40-
41-
function selectForm(
42-
document = {},
43-
selectionFn: (
44-
doc: vscode.TextDocument,
45-
pos: vscode.Position,
46-
topLevel: boolean
47-
) => vscode.Selection | undefined,
48-
toplevel: boolean
49-
) {
50-
const editor = util.getActiveTextEditor(),
51-
doc = util.getDocument(document),
52-
selection = editor.selections[0];
53-
54-
if (selection.isEmpty) {
55-
const codeSelection = selectionFn(doc, selection.active, toplevel);
56-
if (codeSelection) {
57-
editor.selections = [codeSelection];
58-
}
59-
}
60-
}
61-
62-
export function selectCurrentForm(document = {}) {
63-
selectForm(document, getFormSelection, false);
64-
}

0 commit comments

Comments
 (0)