From 129a62ed1f35e3bf6adda20b66f23498a499def6 Mon Sep 17 00:00:00 2001 From: Rayat M Rahman <22646419+riotrah@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:08:55 -0700 Subject: [PATCH 01/19] Implement multi-cursor rewrap. Addresses #2448. --- CHANGELOG.md | 1 + src/cursor-doc/paredit.ts | 95 +++++++++-- .../unit/paredit/commands-test.ts | 150 +++++++++++++++++- src/paredit/commands.ts | 22 +++ src/paredit/extension.ts | 15 +- 5 files changed, 263 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 732060667..52fc4645d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Changes to Calva. ## [Unreleased] +- Implement experimental support for multicursor rewrap commands. Enable `calva.paredit.multicursor` in your settings to try it out. Addressing [#2448](https://github.com/BetterThanTomorrow/calva/issues/2448) - [Add Paredit Kill Left equivalent to Kill Right](https://github.com/BetterThanTomorrow/calva/issues/2426) - Fix: Certain `paredit.killRight` edges cases on Windows. diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index 1fca21cea..9689c85c7 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -670,14 +670,36 @@ export async function wrapSexpr( } } -export async function rewrapSexpr( +/** + * 'Rewraps' the lists containing each cursor/selection, as provided by `selections`, with + * the provided `open` and `close` strings. + * + * Single cursor is just the simpler special case when `selections.length` is 1 + * High level overview: + * - For each cursor, find the offsets/ranges for its containing list's open/close tokens. + * - Make 2 ModelEdits for each token's replacement + 1 Selection; record the offset change. + * - Dedupe each edit (as multi cursors could be in the same list). + * - Then, reposition the edits and selections by the preceding edits' offset changes. + * - Finally, apply the edits and update the selections. + * + * @param doc + * @param open + * @param close + * @param selections + * @returns + */ +export function rewrapSexpr( doc: EditableDocument, open: string, close: string, - start: number = doc.selections[0].anchor, - end: number = doc.selections[0].active -): Promise> { - const cursor = doc.getTokenCursor(end); + selections = [doc.selections[0]] +) { + const edits: { type: 'open' | 'close'; change: number; edit: ModelEdit<'changeRange'> }[] = [], + newSelections = _.clone(selections).map((s) => ({ selection: s, change: 0 })); + + selections.forEach((sel, index) => { + const { active } = sel; + const cursor = doc.getTokenCursor(active); if (cursor.backwardList()) { cursor.backwardUpList(); const oldOpenStart = cursor.offsetStart; @@ -686,16 +708,65 @@ export async function rewrapSexpr( if (cursor.forwardSexp()) { const oldCloseStart = cursor.offsetStart - close.length; const oldCloseEnd = cursor.offsetStart; - const d = open.length - oldOpenLength; - return doc.model.edit( - [ - new ModelEdit('changeRange', [oldCloseStart, oldCloseEnd, close]), - new ModelEdit('changeRange', [oldOpenStart, oldOpenEnd, open]), - ], - { selections: [new ModelEditSelection(end + d)] } + const openChange = open.length - oldOpenLength; + edits.push( + { + edit: new ModelEdit('changeRange', [oldCloseStart, oldCloseEnd, close]), + change: 0, + type: 'close', + }, + { + edit: new ModelEdit('changeRange', [oldOpenStart, oldOpenEnd, open]), + change: openChange, + type: 'open', + } ); + newSelections[index] = { + selection: new ModelEditSelection(active), + change: openChange, + }; } } + }); + + // Due to the nature of dealing with list boundaries, multiple cursors could be targeting + // the same lists, which will result in attempting to delete the same ranges twice. So we dedupe. + const uniqEdits = _.uniqWith(edits, _.isEqual); + + // for both edits and new selections, get the offset by which to move each based on prior edits + function getOffset(cursorOffset: number) { + return _(uniqEdits) + .filter((x) => { + const [xStart] = x.edit.args; + return xStart < cursorOffset; + }) + .map(({ change }) => change) + .sum(); + } + + const editsToApply = _(uniqEdits) + // First, importantly, sort by list open char offset + .sortBy((e) => e.edit.args[0]) + // now, let's iterate thru each cursor and adjust their positions if earlier chars are delete/added + .map((e) => { + const [oldStart, oldEnd, text] = e.edit.args; + const offset = getOffset(oldStart); + const newStart = oldStart + offset; + const newEnd = oldEnd + offset; + return { ...e.edit, args: [newStart, newEnd, text] as const }; + }) + .value(); + const selectionsToApply = newSelections.map(({ selection }) => { + const { active } = selection; + const newSel = selection.clone(); + const offset = getOffset(active); + newSel.reposition(offset); + return newSel; + }); + + return doc.model.edit(editsToApply, { + selections: selectionsToApply, + }); } export async function splitSexp(doc: EditableDocument, start: number = doc.selections[0].active) { diff --git a/src/extension-test/unit/paredit/commands-test.ts b/src/extension-test/unit/paredit/commands-test.ts index c14402819..e26ca1dff 100644 --- a/src/extension-test/unit/paredit/commands-test.ts +++ b/src/extension-test/unit/paredit/commands-test.ts @@ -1,7 +1,7 @@ import * as expect from 'expect'; import * as model from '../../../cursor-doc/model'; import * as handlers from '../../../paredit/commands'; -import { docFromTextNotation } from '../common/text-notation'; +import { docFromTextNotation, textNotationFromDoc } from '../common/text-notation'; import _ = require('lodash'); model.initScanner(20000); @@ -1009,7 +1009,6 @@ describe('paredit commands', () => { it('Single-cursor: Deals with empty lines', async () => { const a = docFromTextNotation('\n|'); const b = docFromTextNotation('|'); - // const expected = { range: textAndSelection(b)[1], editOptions: { skipFormat: false } }; await handlers.killLeft(a, false); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); @@ -1017,7 +1016,6 @@ describe('paredit commands', () => { it('Single-cursor: Deals with empty lines (Windows)', async () => { const a = docFromTextNotation('\r\n|'); const b = docFromTextNotation('|'); - // const expected = { range: textAndSelection(b)[1], editOptions: { skipFormat: false } }; await handlers.killLeft(a, false); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); @@ -1087,4 +1085,150 @@ describe('paredit commands', () => { }); }); }); + + describe('editing', () => { + describe('wrapping', () => { + describe('rewrap', () => { + it('Single-cursor: Rewraps () -> []', async () => { + const a = docFromTextNotation('a (b c|) d'); + const b = docFromTextNotation('a [b c|] d'); + await handlers.rewrapSquare(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Rewraps () -> []', async () => { + const a = docFromTextNotation('(a|2 (b c|) |1d)|3'); + const b = docFromTextNotation('[a|2 [b c|] |1d]|3'); + await handlers.rewrapSquare(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Single-cursor: Rewraps [] -> ()', async () => { + const a = docFromTextNotation('[a [b c|] d]'); + const b = docFromTextNotation('[a (b c|) d]'); + await handlers.rewrapParens(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Rewraps [] -> ()', async () => { + const a = docFromTextNotation('[a|2 [b c|] |1d]|3'); + const b = docFromTextNotation('(a|2 (b c|) |1d)|3'); + await handlers.rewrapParens(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Single-cursor: Rewraps [] -> {}', async () => { + const a = docFromTextNotation('[a [b c|] d]'); + const b = docFromTextNotation('[a {b c|} d]'); + await handlers.rewrapCurly(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Rewraps [] -> {}', async () => { + const a = docFromTextNotation('[a|2 [b c|] |1d]|3'); + const b = docFromTextNotation('{a|2 {b c|} |1d}|3'); + await handlers.rewrapCurly(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Single-cursor: Rewraps #{} -> {}', async () => { + const a = docFromTextNotation('#{a #{b c|} d}'); + const b = docFromTextNotation('#{a {b c|} d}'); + await handlers.rewrapCurly(a, false); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Rewraps #{} -> {}', async () => { + const a = docFromTextNotation('#{a|2 #{b c|} |1d}|3'); + const b = docFromTextNotation('{a|2 {b c|} |1d}|3'); + await handlers.rewrapCurly(a, true); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Single-cursor: Rewraps #{} -> ""', async () => { + const a = docFromTextNotation('#{a #{b c|} d}'); + const b = docFromTextNotation('#{a "b c|" d}'); + await handlers.rewrapQuote(a, false); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Rewraps #{} -> ""', async () => { + const a = docFromTextNotation('#{a|2 #{b c|} |1d}|3'); + const b = docFromTextNotation('"a|2 "b c|" |1d"|3'); + await handlers.rewrapQuote(a, true); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Rewraps #{} -> "" 2', async () => { + const a = docFromTextNotation('#{a|2 #{b c|} |1d}|3\n#{a|6 #{b c|4} |5d}|7'); + const b = docFromTextNotation('"a|2 "b c|" |1d"|3\n"a|6 "b c|4" |5d"|7'); + await handlers.rewrapQuote(a, true); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Rewraps #{} -> [] 3', async () => { + const a = docFromTextNotation('#{a|2 #{b c|} |1d\n#{a|6 #{b c|4} |5d}}|3'); + const b = docFromTextNotation('[a|2 [b c|] |1d\n[a|6 [b c|4] |5d]]|3'); + await handlers.rewrapSquare(a, true); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Single-cursor: Rewraps [] -> #{}', async () => { + const a = docFromTextNotation('[[b c|] d]'); + const b = docFromTextNotation('[#{b c|} d]'); + await handlers.rewrapSet(a, false); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Rewraps [] -> #{}', async () => { + const a = docFromTextNotation('[[b|2 c|] |1d]|3'); + const b = docFromTextNotation('#{#{b|2 c|} |1d}|3'); + await handlers.rewrapSet(a, true); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Rewraps [] -> #{} 2', async () => { + const a = docFromTextNotation('[[b|2 c|] |1d]|3\n[a|6 [b c|4] |5d]|7'); + const b = docFromTextNotation('#{#{b|2 c|} |1d}|3\n#{a|6 #{b c|4} |5d}|7'); + await handlers.rewrapSet(a, true); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Rewraps [] -> #{} 3', async () => { + const a = docFromTextNotation('[[b|2 c|] |1d\n[a|6 [b c|4] |5d]]|3'); + const b = docFromTextNotation('#{#{b|2 c|} |1d\n#{a|6 #{b c|4} |5d}}|3'); + await handlers.rewrapSet(a, true); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + // TODO: This tests current behavior. What should happen? + it('Single-cursor: Rewraps ^{} -> #{}', async () => { + const a = docFromTextNotation('^{^{b c|} d}'); + const b = docFromTextNotation('^{#{b c|} d}'); + await handlers.rewrapSet(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Rewraps ^{} -> #{}', async () => { + const a = docFromTextNotation('^{^{b|2 c|} |1d}|3'); + const b = docFromTextNotation('#{#{b|2 c|} |1d}|3'); + await handlers.rewrapSet(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + // TODO: This tests current behavior. What should happen? + it('Single-cursor: Rewraps ~{} -> #{}', async () => { + const a = docFromTextNotation('~{~{b c|} d}'); + const b = docFromTextNotation('~{#{b c|} d}'); + await handlers.rewrapSet(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Rewraps ~{} -> #{}', async () => { + const a = docFromTextNotation('~{~{b|2 c|} |1d}|3'); + const b = docFromTextNotation('#{#{b|2 c|} |1d}|3'); + await handlers.rewrapSet(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + }); + }); + }); }); diff --git a/src/paredit/commands.ts b/src/paredit/commands.ts index 38c07cea9..9e29d28f8 100644 --- a/src/paredit/commands.ts +++ b/src/paredit/commands.ts @@ -122,3 +122,25 @@ export async function killLeft( result.editOptions ); } + +// REWRAP + +export function rewrapQuote(doc: EditableDocument, isMulti: boolean) { + return paredit.rewrapSexpr(doc, '"', '"', isMulti ? doc.selections : [doc.selections[0]]); +} + +export function rewrapSet(doc: EditableDocument, isMulti: boolean) { + return paredit.rewrapSexpr(doc, '#{', '}', isMulti ? doc.selections : [doc.selections[0]]); +} + +export function rewrapCurly(doc: EditableDocument, isMulti: boolean) { + return paredit.rewrapSexpr(doc, '{', '}', isMulti ? doc.selections : [doc.selections[0]]); +} + +export function rewrapSquare(doc: EditableDocument, isMulti: boolean) { + return paredit.rewrapSexpr(doc, '[', ']', isMulti ? doc.selections : [doc.selections[0]]); +} + +export function rewrapParens(doc: EditableDocument, isMulti: boolean) { + return paredit.rewrapSexpr(doc, '(', ')', isMulti ? doc.selections : [doc.selections[0]]); +} diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index 1708ebf77..a30c421e0 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -392,31 +392,36 @@ const pareditCommands: PareditCommand[] = [ { command: 'paredit.rewrapParens', handler: (doc: EditableDocument) => { - return paredit.rewrapSexpr(doc, '(', ')'); + const isMulti = multiCursorEnabled(); + return handlers.rewrapParens(doc, isMulti); }, }, { command: 'paredit.rewrapSquare', handler: (doc: EditableDocument) => { - return paredit.rewrapSexpr(doc, '[', ']'); + const isMulti = multiCursorEnabled(); + return handlers.rewrapSquare(doc, isMulti); }, }, { command: 'paredit.rewrapCurly', handler: (doc: EditableDocument) => { - return paredit.rewrapSexpr(doc, '{', '}'); + const isMulti = multiCursorEnabled(); + return handlers.rewrapCurly(doc, isMulti); }, }, { command: 'paredit.rewrapSet', handler: (doc: EditableDocument) => { - return paredit.rewrapSexpr(doc, '#{', '}'); + const isMulti = multiCursorEnabled(); + return handlers.rewrapSet(doc, isMulti); }, }, { command: 'paredit.rewrapQuote', handler: (doc: EditableDocument) => { - return paredit.rewrapSexpr(doc, '"', '"'); + const isMulti = multiCursorEnabled(); + return handlers.rewrapQuote(doc, isMulti); }, }, { From 96be366be1dd268721644be8627c18a5021897a1 Mon Sep 17 00:00:00 2001 From: Rayat M Rahman <22646419+riotrah@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:10:54 -0700 Subject: [PATCH 02/19] Improve changelog entry --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52fc4645d..60aff1ac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ Changes to Calva. ## [Unreleased] -- Implement experimental support for multicursor rewrap commands. Enable `calva.paredit.multicursor` in your settings to try it out. Addressing [#2448](https://github.com/BetterThanTomorrow/calva/issues/2448) +- [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. Addressing [#2445](https://github.com/BetterThanTomorrow/calva/issues/2445) + - [Add Paredit Kill Left equivalent to Kill Right](https://github.com/BetterThanTomorrow/calva/issues/2426) - Fix: Certain `paredit.killRight` edges cases on Windows. From a95f9c618eb674f88777c5c4e8f9cda8540acfec Mon Sep 17 00:00:00 2001 From: Rayat M Rahman <22646419+riotrah@users.noreply.github.com> Date: Wed, 27 Mar 2024 16:28:11 -0700 Subject: [PATCH 03/19] start paredit multicursor wrap --- .mocharc.json | 1 + CHANGELOG.md | 2 + package.json | 9 +- src/cursor-doc/cursor-doc-utils.ts | 117 +++++++ src/cursor-doc/model.ts | 8 + src/cursor-doc/paredit.ts | 136 +++++--- .../unit/cursor-doc/paredit-test.ts | 149 +++++++++ .../unit/paredit/commands-test.ts | 303 ++++++++++++++++++ src/paredit/commands.ts | 22 ++ src/paredit/extension.ts | 19 +- src/util/array.ts | 13 + src/util/lodashMixins.ts | 91 ++++++ 12 files changed, 814 insertions(+), 56 deletions(-) create mode 100644 .mocharc.json create mode 100644 src/cursor-doc/cursor-doc-utils.ts create mode 100644 src/util/array.ts create mode 100644 src/util/lodashMixins.ts diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 000000000..e41150a91 --- /dev/null +++ b/.mocharc.json @@ -0,0 +1 @@ +{ "require": ["ts-node/register", "src/util/lodashMixins.ts"] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 60aff1ac1..c1a09935c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes to Calva. ## [Unreleased] +- Add "Wrap with Set #{}" paredit command. +- [Implement experimental support for multicursor wrap commands](https://github.com/BetterThanTomorrow/calva/issues/2448). Enable `calva.paredit.multicursor` in your settings to try it out. Addressing [#2445](https://github.com/BetterThanTomorrow/calva/issues/2445) - [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. Addressing [#2445](https://github.com/BetterThanTomorrow/calva/issues/2445) - [Add Paredit Kill Left equivalent to Kill Right](https://github.com/BetterThanTomorrow/calva/issues/2426) diff --git a/package.json b/package.json index 1311911db..7c74ae0f6 100644 --- a/package.json +++ b/package.json @@ -2551,6 +2551,11 @@ "key": "ctrl+alt+shift+q", "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/" }, + { + "command": "paredit.wrapAroundSet", + "key": "ctrl+alt+shift+h", + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/" + }, { "command": "paredit.rewrapParens", "key": "ctrl+alt+r ctrl+alt+p", @@ -3203,8 +3208,8 @@ "integration-test": "node ./out/extension-test/integration/runTests.js", "e2e-test": "node ./src/extension-test/e2e-test/launch.js", "pree2e-test": "cd ./src/extension-test/e2e-test/ && npm i", - "unit-test": "npx mocha --require ts-node/register 'src/extension-test/unit/**/*-test.ts'", - "unit-test-watch": "npx mocha --watch --require ts-node/register --watch-extensions ts --watch-files src 'src/extension-test/unit/**/*-test.ts'", + "unit-test": "npx mocha --require ts-node/register,src/util/lodashMixins.ts 'src/extension-test/unit/**/*-test.ts'", + "unit-test-watch": "npx mocha --watch --require ts-node/register,src/util/lodashMixins.ts --watch-extensions ts --watch-files src 'src/extension-test/unit/**/*-test.ts'", "prettier-format": "npx prettier --write \"./**/*.{ts,js,json}\"", "prettier-check": "npx prettier --check \"./**/*.{ts,js,json}\"", "prettier-check-watch": "onchange \"./**/*.{ts,js,json}\" -- prettier --check {{changed}}", diff --git a/src/cursor-doc/cursor-doc-utils.ts b/src/cursor-doc/cursor-doc-utils.ts new file mode 100644 index 000000000..4ba5ba70f --- /dev/null +++ b/src/cursor-doc/cursor-doc-utils.ts @@ -0,0 +1,117 @@ +import { first, isNumber, last, ListIterator, range } from 'lodash'; +import { isModelEditSelection, isModelRange, ModelEditRange, ModelEditSelection } from './model'; +import _ = require('lodash'); + +type RangeOrSelection = ModelEditRange | ModelEditSelection; +export function mapRangeOrSelectionToOffset1( + side: 'start' | 'end' | 'anchor' | 'active' = 'start' +) { + return function inner( + // support passing either range/sel or [range/sel, original list order] + t: RangeOrSelection | [rangeOrSel: RangeOrSelection, order: number] + ): number { + // const rangeOrSel = isModelEditSelection(t) || isModelRange(t) ? t : first(t); + const rangeOrSel = isModelEditSelection(t) ? t : isModelRange(t) ? t : t[0]; + + if (rangeOrSel instanceof ModelEditSelection) { + return rangeOrSel[side]; + } else if (isModelRange(rangeOrSel)) { + // let fn: (...args: number[]) => number; + switch (side) { + case 'start': + // fn = Math.min; + return Math.min(...rangeOrSel); + // break; + case 'end': + // fn = Math.max; + return Math.max(...rangeOrSel); + // break; + case 'anchor': + // fn = (...x) => first(x); + return first(rangeOrSel); + // break; + case 'active': + // fn = (...x) => last(x); + return last(rangeOrSel); + // break; + default: + // break; + return range[0]; + } + + // return fn(...rangeOrSel); + // return fn(...rangeOrSel); + } + }; +} +export function mapRangeOrSelectionToOffset(side: 'start' | 'end' | 'anchor' | 'active' = 'start') { + return function inner( + // support passing either range/sel or [range/sel, original list order] + t: RangeOrSelection | [rangeOrSel: RangeOrSelection, order: number] + ): number { + const rangeOrSel = isModelEditSelection(t) ? t : isModelRange(t) ? t : t[0]; + + if (rangeOrSel instanceof ModelEditSelection) { + return rangeOrSel[side]; + } else if (isModelRange(rangeOrSel)) { + switch (side) { + case 'start': + return Math.min(...rangeOrSel); + case 'end': + return Math.max(...rangeOrSel); + case 'anchor': + return first(rangeOrSel); + case 'active': + return last(rangeOrSel); + default: + return range[0]; + } + } + }; +} + +export function repositionSelectionByCumulativeOffsets( + /** + * Either a fixed offset to add for each cursor (eg 2 if wrapping by parens), + * or a 'getter' fn to get the value from each cursor. + */ + offsetGetter: ListIterator | number +) { + // if (true) { + return repositionSelectionWithGetterByCumulativeOffsets( + _.identity, + offsetGetter + ); +} + +export function repositionSelectionWithGetterByCumulativeOffsets( + selectionGetter: ListIterator, + /** + * Either a fixed offset to add for each cursor (eg 2 if wrapping by parens), + * or a 'getter' fn to get the value from each cursor. + */ + offsetGetter: ListIterator | number +) { + return ( + t: T, + index: number, + // array: ModelEditSelection[] + array: T[] + ): ModelEditSelection => { + const sel = selectionGetter(t, index, array); + const newSel = sel.clone(); + + const getItemOffset = isNumber(offsetGetter) ? () => offsetGetter : offsetGetter; + + const offset = _(array) + .filter((x, i, a) => { + const s = selectionGetter(x, i, a); + return s.start < sel.start; + }) + .map(getItemOffset) + .sum(); + + newSel.reposition(offset); + return newSel; + }; +} diff --git a/src/cursor-doc/model.ts b/src/cursor-doc/model.ts index a6db0d8d9..c2aedb140 100644 --- a/src/cursor-doc/model.ts +++ b/src/cursor-doc/model.ts @@ -50,6 +50,14 @@ export class ModelEdit { constructor(public editFn: T, public args: Readonly>) {} } +export function isModelRange(o: any): o is ModelEditRange { + return _.isArray(o) && o.length === 2 && isNumber(o[0]) && isNumber(o[1]); +} + +export function isModelEditSelection(o: any): o is ModelEditSelection { + return o instanceof ModelEditSelection; +} + /** * An undirected range representing a cursor/selection in a document. * Is a tuple of [start, end] where each is an offset. diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index 9689c85c7..7e3e2c528 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -12,6 +12,11 @@ import { LispTokenCursor } from './token-cursor'; import { backspaceOnWhitespace } from './backspace-on-whitespace'; import _ = require('lodash'); import { isEqual, last, property } from 'lodash'; +import { mapToItemAndOrder } from '../util/array'; +import { + mapRangeOrSelectionToOffset, + repositionSelectionByCumulativeOffsets, +} from './cursor-doc-utils'; // NB: doc.model.edit returns a Thenable, so that the vscode Editor can compose commands. // But don't put such chains in this module because that won't work in the repl-console. @@ -621,53 +626,84 @@ export function rangeToBackwardList( } } -export async function wrapSexpr( +export function wrapSexpr( doc: EditableDocument, open: string, close: string, - start: number = doc.selections[0].anchor, - end: number = doc.selections[0].active, + selections = [doc.selections[0]], options = { skipFormat: false } ) { - const cursor = doc.getTokenCursor(end); - if (cursor.withinString() && open == '"') { - open = close = '\\"'; - } - if (start == end) { - // No selection - const currentFormRange = cursor.rangeForCurrentForm(start); - if (currentFormRange) { - const range = currentFormRange; - return doc.model.edit( - [ - new ModelEdit('insertString', [range[1], close]), - new ModelEdit('insertString', [ - range[0], - open, - [end, end], - [start + open.length, start + open.length], - ]), - ], - { - selections: [new ModelEditSelection(start + open.length)], - skipFormat: options.skipFormat, + // TODO: support wrapping with Sets (#{}) + const edits: ModelEdit<'insertString'>[] = [], + // selections = clone(selections).map(mapToItemAndOrder); + newSelections = _.clone(selections); + + _(selections) + // iterate backwards to simplify dealing with cumulative cursor position offsets as document's text is added to with + // parens or whatever is being wrapped with. + .map(mapToItemAndOrder) + .orderBy(([r]) => mapRangeOrSelectionToOffset('start')(r), 'desc') + .forEach(([sel, index]) => { + // const { start, end } = sel; + const { anchor: start, active: end } = sel; + const cursor = doc.getTokenCursor(end); + if (cursor.withinString() && open == '"') { + open = close = '\\"'; + } + if (start == end) { + // No selection + const currentFormRange = cursor.rangeForCurrentForm(start); + if (currentFormRange) { + const range = currentFormRange; + const closeEdit = new ModelEdit('insertString', [range[1], close]); + const openEdit = new ModelEdit('insertString', [range[0], open]); + + const existing = _.intersectionWith(edits, [closeEdit, openEdit], _.isEqual); + const isNewEdit = _.isEmpty(existing); + + console.log(edits, range, closeEdit, openEdit, existing, isNewEdit); + + edits.push(closeEdit, openEdit); + // don't forget to include the index with the selection for later reordering to original order; + // selections[index] = [new ModelEditSelection(start + open.length), index]; + newSelections[index] = new ModelEditSelection(start + (isNewEdit ? open.length : 0)); } - ); - } - } else { - // there is a selection - const range = [Math.min(start, end), Math.max(start, end)]; - return doc.model.edit( - [ - new ModelEdit('insertString', [range[1], close]), - new ModelEdit('insertString', [range[0], open]), - ], - { - selections: [new ModelEditSelection(start + open.length)], - skipFormat: options.skipFormat, + } else { + // there is a selection + const range = [Math.min(start, end), Math.max(start, end)]; + // edits.push( + // new ModelEdit('insertString', [range[1], close]), + // new ModelEdit('insertString', [range[0], open]) + // ); + // don't forget to include the index with the selection for later reordering to original order; + // selections[index] = [new ModelEditSelection(start + open.length), index]; + // newSelections[index] = new ModelEditSelection(start + open.length); + + const closeEdit = new ModelEdit('insertString', [range[1], close]); + const openEdit = new ModelEdit('insertString', [range[0], open]); + + const existing = _.intersectionWith(edits, [closeEdit, openEdit], _.isEqual); + const isNewEdit = _.isEmpty(existing); + + console.log(edits, range, closeEdit, openEdit, existing, isNewEdit); + + edits.push(closeEdit, openEdit); + // don't forget to include the index with the selection for later reordering to original order; + // selections[index] = [new ModelEditSelection(start + open.length), index]; + newSelections[index] = new ModelEditSelection(start + (isNewEdit ? open.length : 0)); } - ); - } + return undefined; + }); + const uniqEdits = _.uniqBy(edits, (e) => e.args[0]); + // return doc.model.edit(_.uniqWith(edits, _.isEqual), { + return doc.model.edit(uniqEdits, { + // return doc.model.edit(edits, { + // selections: newSelections.map(repositionSelectionByCumulativeOffsets(2)), + selections: newSelections.map( + repositionSelectionByCumulativeOffsets(open.length + close.length) + ), + skipFormat: options.skipFormat, + }); } /** @@ -700,14 +736,14 @@ export function rewrapSexpr( selections.forEach((sel, index) => { const { active } = sel; const cursor = doc.getTokenCursor(active); - if (cursor.backwardList()) { - cursor.backwardUpList(); - const oldOpenStart = cursor.offsetStart; - const oldOpenLength = cursor.getToken().raw.length; - const oldOpenEnd = oldOpenStart + oldOpenLength; - if (cursor.forwardSexp()) { - const oldCloseStart = cursor.offsetStart - close.length; - const oldCloseEnd = cursor.offsetStart; + if (cursor.backwardList()) { + cursor.backwardUpList(); + const oldOpenStart = cursor.offsetStart; + const oldOpenLength = cursor.getToken().raw.length; + const oldOpenEnd = oldOpenStart + oldOpenLength; + if (cursor.forwardSexp()) { + const oldCloseStart = cursor.offsetStart - close.length; + const oldCloseEnd = cursor.offsetStart; const openChange = open.length - oldOpenLength; edits.push( { @@ -720,13 +756,13 @@ export function rewrapSexpr( change: openChange, type: 'open', } - ); + ); newSelections[index] = { selection: new ModelEditSelection(active), change: openChange, }; + } } - } }); // Due to the nature of dealing with list boundaries, multiple cursors could be targeting diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index 8ab3fab17..5045feb2e 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -1623,6 +1623,155 @@ describe('paredit', () => { }); }); + describe('Wrap', () => { + it('Simply wraps []', async () => { + const a = docFromTextNotation('a (b c|) d'); + const b = docFromTextNotation('a (b [c|]) d'); + await paredit.wrapSexpr(a, '[', ']'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Simply wraps ()', async () => { + const a = docFromTextNotation('a [b c|] d'); + const b = docFromTextNotation('a [b (c|)] d'); + await paredit.wrapSexpr(a, '(', ')'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Simply wraps {}', async () => { + const a = docFromTextNotation('a [b c|] d'); + const b = docFromTextNotation('a [b {c|}] d'); + await paredit.wrapSexpr(a, '{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Simply wraps {}', async () => { + const a = docFromTextNotation('a #{b c|} d'); + const b = docFromTextNotation('a #{b {c|}} d'); + await paredit.wrapSexpr(a, '{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Simply wraps ""', async () => { + const a = docFromTextNotation('a #{b c|} d'); + const b = docFromTextNotation('a #{b "c|"} d'); + await paredit.wrapSexpr(a, '"', '"'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Simply wraps #{}', async () => { + const a = docFromTextNotation('[b c|] d'); + const b = docFromTextNotation('[b #{c|}] d'); + await paredit.wrapSexpr(a, '#{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + + it('Wraps from close {}', async () => { + const a = docFromTextNotation('a [b c]| d'); + const b = docFromTextNotation('a {[b c]|} d'); + await paredit.wrapSexpr(a, '{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps from close ""', async () => { + const a = docFromTextNotation('a #{b c}| d'); + const b = docFromTextNotation('a "#{b c}|" d'); + await paredit.wrapSexpr(a, '"', '"'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps from close #{}', async () => { + const a = docFromTextNotation('a [b c]| d'); + const b = docFromTextNotation('a #{[b c]|} d'); + await paredit.wrapSexpr(a, '#{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + + it('Wraps from close from a distance w/ cursor outside {}', async () => { + const a = docFromTextNotation('a [b c] | d'); + const b = docFromTextNotation('a {[b c]}| d'); + await paredit.wrapSexpr(a, '{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps from close from a distance w/ cursor outside ""', async () => { + const a = docFromTextNotation('a #{b c} | d'); + const b = docFromTextNotation('a "#{b c}"| d'); + await paredit.wrapSexpr(a, '"', '"'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps from close from a distance w/ cursor outside #{}', async () => { + const a = docFromTextNotation('a [b c] | d'); + const b = docFromTextNotation('a #{[b c]}| d'); + await paredit.wrapSexpr(a, '#{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + + it('Wraps from opening {}', async () => { + const a = docFromTextNotation('a |[b c] d'); + const b = docFromTextNotation('a {|[b c]} d'); + await paredit.wrapSexpr(a, '{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps from opening ""', async () => { + const a = docFromTextNotation('a |#{b c} d'); + const b = docFromTextNotation('a "|#{b c}" d'); + await paredit.wrapSexpr(a, '"', '"'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps from opening #{}', async () => { + const a = docFromTextNotation('a |[b c] d'); + const b = docFromTextNotation('a #{|[b c]} d'); + await paredit.wrapSexpr(a, '#{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + + it('Wraps between directly adjacent lists preferring prior list {}', async () => { + const a = docFromTextNotation('a [b c]|[e] d'); + const b = docFromTextNotation('a {[b c]|}[e] d'); + await paredit.wrapSexpr(a, '{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps between directly adjacent lists preferring prior list ""', async () => { + const a = docFromTextNotation('a #{b c}|#{e} d'); + const b = docFromTextNotation('a "#{b c}|"#{e} d'); + await paredit.wrapSexpr(a, '"', '"'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps between directly adjacent lists preferring prior list #{}', async () => { + const a = docFromTextNotation('a [b c]|[e] d'); + const b = docFromTextNotation('a #{[b c]|}[e] d'); + await paredit.wrapSexpr(a, '#{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + + it('Wraps from selection leaving cursor at anchor inside list {}', async () => { + const a = docFromTextNotation('a [|b c|] d'); + const b = docFromTextNotation('a [{|b c}] d'); + await paredit.wrapSexpr(a, '{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps from selection leaving cursor at anchor inside list ""', async () => { + const a = docFromTextNotation('a #{ { + const a = docFromTextNotation('a [ { + const a = docFromTextNotation('^{b c|} d'); + const b = docFromTextNotation('^{b #{c|}} d'); + await paredit.wrapSexpr(a, '#{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + // TODO: This tests current behavior. What should happen? + it('Simply wraps #{}', async () => { + const a = docFromTextNotation('~{b c|} d'); + const b = docFromTextNotation('~{b #{c|}} d'); + await paredit.wrapSexpr(a, '#{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + }); + describe('Rewrap', () => { it('Rewraps () -> []', async () => { const a = docFromTextNotation('a (b c|) d'); diff --git a/src/extension-test/unit/paredit/commands-test.ts b/src/extension-test/unit/paredit/commands-test.ts index e26ca1dff..e0137365d 100644 --- a/src/extension-test/unit/paredit/commands-test.ts +++ b/src/extension-test/unit/paredit/commands-test.ts @@ -1088,6 +1088,309 @@ describe('paredit commands', () => { describe('editing', () => { describe('wrapping', () => { + describe('wrap', () => { + it('Single-cursor: Simply wraps []', async () => { + const a = docFromTextNotation('a (b c|) |1d'); + const b = docFromTextNotation('a (b [c|]) d'); + await handlers.wrapAroundSquare(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps []', async () => { + const a = docFromTextNotation('a (b c|) d|1 []|2'); + const b = docFromTextNotation('a (b [c|]) [d|1] [[]|2]'); + await handlers.wrapAroundSquare(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps [] 2 - from open/left', async () => { + const a = docFromTextNotation('a (b |c) |1d |2[]'); + const b = docFromTextNotation('a (b [|c]) [|1d] [|2[]]'); + await handlers.wrapAroundSquare(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps [] 3 - mixed', async () => { + const a = docFromTextNotation('a (b c|) |1d []|2'); + const b = docFromTextNotation('a (b [c|]) [|1d] [[]|2]'); + await handlers.wrapAroundSquare(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Handles wrapping multiple cursors around the same form []', async () => { + const a = docFromTextNotation('a (b |1c|) d'); + const b = docFromTextNotation('a (b [c|]) d'); + await handlers.wrapAroundSquare(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Handles wrapping multiple cursors around the same form []', async () => { + const a = docFromTextNotation('a (b |1c|) d'); + const b = docFromTextNotation('a (b [|1c|]) d'); + await handlers.wrapAroundSquare(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Handles wrapping multiple cursors targeting the same form []', async () => { + const a = docFromTextNotation('a (b c| |1) d'); + const b = docFromTextNotation('a (b [c|] ) d'); + await handlers.wrapAroundSquare(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Handles wrapping multiple cursors targeting the same form []', async () => { + const a = docFromTextNotation('a (b c| |1) d'); + const b = docFromTextNotation('a (b [c|] |1) d'); + await handlers.wrapAroundSquare(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Handles wrapping multiple cursors targeting the same form #{} 2', async () => { + const a = docFromTextNotation('a (b |c| |1) d'); + const b = docFromTextNotation('a (b [|c] |1) d'); + await handlers.wrapAroundSquare(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Single-cursor: Simply wraps ()', async () => { + const a = docFromTextNotation('a [b c|] |1d'); + const b = docFromTextNotation('a [b (c|)] d'); + await handlers.wrapAroundParens(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps ()', async () => { + const a = docFromTextNotation('a [b c|] |1d'); + const b = docFromTextNotation('a [b (c|)] (|1d)'); + await handlers.wrapAroundParens(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Simply wraps {}', async () => { + const a = docFromTextNotation('a [b c|] |1d'); + const b = docFromTextNotation('a [b {c|}] d'); + await handlers.wrapAroundCurly(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps {}', async () => { + const a = docFromTextNotation('a [b c|] |1d'); + const b = docFromTextNotation('a [b {c|}] {|1d}'); + await handlers.wrapAroundCurly(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Simply wraps {}', async () => { + const a = docFromTextNotation('a #{b c|} |1d'); + const b = docFromTextNotation('a #{b {c|}} d'); + await handlers.wrapAroundCurly(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps {}', async () => { + const a = docFromTextNotation('a #{b c|} |1d'); + const b = docFromTextNotation('a #{b {c|}} {|1d}'); + await handlers.wrapAroundCurly(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Simply wraps ""', async () => { + const a = docFromTextNotation('a #{b c|} |1d'); + const b = docFromTextNotation('a #{b "c|"} d'); + await handlers.wrapAroundQuote(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps ""', async () => { + const a = docFromTextNotation('a #{b c|} |1d'); + const b = docFromTextNotation('a #{b "c|"} "|1d"'); + await handlers.wrapAroundQuote(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Simply wraps #{}', async () => { + const a = docFromTextNotation('[b c|] |1d'); + const b = docFromTextNotation('[b #{c|}] d'); + await handlers.wrapAroundSet(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps #{}', async () => { + const a = docFromTextNotation('[b c|] |1d'); + const b = docFromTextNotation('[b #{c|}] #{|1d}'); + await handlers.wrapAroundSet(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Single-cursor: Wraps from close {}', async () => { + const a = docFromTextNotation('a [b c]| |1d'); + const b = docFromTextNotation('a {[b c]|} d'); + await handlers.wrapAroundCurly(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from close {}', async () => { + const a = docFromTextNotation('a [b c]| |1d'); + const b = docFromTextNotation('a {[b c]|} {|1d}'); + await handlers.wrapAroundCurly(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps from close ""', async () => { + const a = docFromTextNotation('a #{b c}| |1d'); + const b = docFromTextNotation('a "#{b c}|" d'); + await handlers.wrapAroundQuote(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from close ""', async () => { + const a = docFromTextNotation('a #{b c}| |1d'); + const b = docFromTextNotation('a "#{b c}|" "|1d"'); + await handlers.wrapAroundQuote(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps from close #{}', async () => { + const a = docFromTextNotation('a [b c]| |1d'); + const b = docFromTextNotation('a #{[b c]|} d'); + await handlers.wrapAroundSet(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from close #{}', async () => { + const a = docFromTextNotation('a [b c]| |1d'); + const b = docFromTextNotation('a #{[b c]|} #{|1d}'); + await handlers.wrapAroundSet(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Single-cursor: Wraps from close from a distance w/ cursor outside {}', async () => { + const a = docFromTextNotation('a [b c] | |1d'); + const b = docFromTextNotation('a {[b c]}| d'); + await handlers.wrapAroundCurly(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from close from a distance w/ cursor outside {}', async () => { + const a = docFromTextNotation('a [b c] | |1d'); + const b = docFromTextNotation('a {[b c]}| {|1d}'); + await handlers.wrapAroundCurly(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps from close from a distance w/ cursor outside ""', async () => { + const a = docFromTextNotation('a #{b c} | |1d'); + const b = docFromTextNotation('a "#{b c}"| d'); + await handlers.wrapAroundQuote(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from close from a distance w/ cursor outside ""', async () => { + const a = docFromTextNotation('a #{b c} | |1d'); + const b = docFromTextNotation('a "#{b c}"| "|1d"'); + await handlers.wrapAroundQuote(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps from close from a distance w/ cursor outside #{}', async () => { + const a = docFromTextNotation('a [b c] | |1d'); + const b = docFromTextNotation('a #{[b c]}| d'); + await handlers.wrapAroundSet(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from close from a distance w/ cursor outside #{}', async () => { + const a = docFromTextNotation('a [b c] | |1d'); + const b = docFromTextNotation('a #{[b c]}| #{|1d}'); + await handlers.wrapAroundSet(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Single-cursor: Wraps from opening {}', async () => { + const a = docFromTextNotation('a |[b c] |1d'); + const b = docFromTextNotation('a {|[b c]} d'); + await handlers.wrapAroundCurly(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from opening {}', async () => { + const a = docFromTextNotation('a |[b c] |1d'); + const b = docFromTextNotation('a {|[b c]} {|1d}'); + await handlers.wrapAroundCurly(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps from opening ""', async () => { + const a = docFromTextNotation('a |#{b c} |1d'); + const b = docFromTextNotation('a "|#{b c}" d'); + await handlers.wrapAroundQuote(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from opening ""', async () => { + const a = docFromTextNotation('a |#{b c} |1d'); + const b = docFromTextNotation('a "|#{b c}" "|1d"'); + await handlers.wrapAroundQuote(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps from opening #{}', async () => { + const a = docFromTextNotation('a |[b c] |1d'); + const b = docFromTextNotation('a #{|[b c]} d'); + await handlers.wrapAroundSet(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from opening #{}', async () => { + const a = docFromTextNotation('a |[b c] |1d'); + const b = docFromTextNotation('a #{|[b c]} #{|1d}'); + await handlers.wrapAroundSet(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Single-cursor: Wraps between directly adjacent lists preferring prior list {}', async () => { + const a = docFromTextNotation('a [b c]|[e] |1d'); + const b = docFromTextNotation('a {[b c]|}[e] d'); + await handlers.wrapAroundCurly(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps between directly adjacent lists preferring prior list {}', async () => { + const a = docFromTextNotation('a [b c]|[e] |1d'); + const b = docFromTextNotation('a {[b c]|}[e] {|1d}'); + await handlers.wrapAroundCurly(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps between directly adjacent lists preferring prior list ""', async () => { + const a = docFromTextNotation('a #{b c}|#{e} |1d'); + const b = docFromTextNotation('a "#{b c}|"#{e} d'); + await handlers.wrapAroundQuote(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps between directly adjacent lists preferring prior list ""', async () => { + const a = docFromTextNotation('a #{b c}|#{e} |1d'); + const b = docFromTextNotation('a "#{b c}|"#{e} "|1d"'); + await handlers.wrapAroundQuote(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps between directly adjacent lists preferring prior list #{}', async () => { + const a = docFromTextNotation('a [b c]|[e] |1d'); + const b = docFromTextNotation('a #{[b c]|}[e] d'); + await handlers.wrapAroundSet(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps between directly adjacent lists preferring prior list #{}', async () => { + const a = docFromTextNotation('a [b c]|[e] |1d'); + const b = docFromTextNotation('a #{[b c]|}[e] #{|1d}'); + await handlers.wrapAroundSet(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Single-cursor: Wraps from selection leaving cursor at anchor inside list {}', async () => { + const a = docFromTextNotation('a [|b c|] |1d'); + const b = docFromTextNotation('a [{|b c}] d'); + await handlers.wrapAroundCurly(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from selection leaving cursor at anchor inside list {}', async () => { + const a = docFromTextNotation('a [|b c|] <1d<1'); + const b = docFromTextNotation('a [{|b c}] {d|1}'); + await handlers.wrapAroundCurly(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps from selection leaving cursor at anchor inside list ""', async () => { + const a = docFromTextNotation('a #{ { + const a = docFromTextNotation('a #{ { + const a = docFromTextNotation('a [ { + const a = docFromTextNotation('a [ { it('Single-cursor: Rewraps () -> []', async () => { const a = docFromTextNotation('a (b c|) d'); diff --git a/src/paredit/commands.ts b/src/paredit/commands.ts index 9e29d28f8..eeddf4ea7 100644 --- a/src/paredit/commands.ts +++ b/src/paredit/commands.ts @@ -123,6 +123,28 @@ export async function killLeft( ); } +// WRAP + +export function wrapAroundQuote(doc: EditableDocument, isMulti: boolean) { + return paredit.wrapSexpr(doc, '"', '"', isMulti ? doc.selections : [doc.selections[0]]); +} + +export function wrapAroundCurly(doc: EditableDocument, isMulti: boolean) { + return paredit.wrapSexpr(doc, '{', '}', isMulti ? doc.selections : [doc.selections[0]]); +} + +export function wrapAroundSet(doc: EditableDocument, isMulti: boolean) { + return paredit.wrapSexpr(doc, '#{', '}', isMulti ? doc.selections : [doc.selections[0]]); +} + +export function wrapAroundSquare(doc: EditableDocument, isMulti: boolean) { + return paredit.wrapSexpr(doc, '[', ']', isMulti ? doc.selections : [doc.selections[0]]); +} + +export function wrapAroundParens(doc: EditableDocument, isMulti: boolean) { + return paredit.wrapSexpr(doc, '(', ')', isMulti ? doc.selections : [doc.selections[0]]); +} + // REWRAP export function rewrapQuote(doc: EditableDocument, isMulti: boolean) { diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index a30c421e0..a36e94a00 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -368,25 +368,36 @@ const pareditCommands: PareditCommand[] = [ { command: 'paredit.wrapAroundParens', handler: (doc: EditableDocument) => { - return paredit.wrapSexpr(doc, '(', ')'); + const isMulti = multiCursorEnabled(); + return handlers.wrapAroundParens(doc, isMulti); }, }, { command: 'paredit.wrapAroundSquare', handler: (doc: EditableDocument) => { - return paredit.wrapSexpr(doc, '[', ']'); + const isMulti = multiCursorEnabled(); + return handlers.wrapAroundSquare(doc, isMulti); }, }, { command: 'paredit.wrapAroundCurly', handler: (doc: EditableDocument) => { - return paredit.wrapSexpr(doc, '{', '}'); + const isMulti = multiCursorEnabled(); + return handlers.wrapAroundCurly(doc, isMulti); + }, + }, + { + command: 'paredit.wrapAroundSet', + handler: (doc: EditableDocument) => { + const isMulti = multiCursorEnabled(); + return handlers.wrapAroundSet(doc, isMulti); }, }, { command: 'paredit.wrapAroundQuote', handler: (doc: EditableDocument) => { - return paredit.wrapSexpr(doc, '"', '"'); + const isMulti = multiCursorEnabled(); + return handlers.wrapAroundQuote(doc, isMulti); }, }, { diff --git a/src/util/array.ts b/src/util/array.ts new file mode 100644 index 000000000..4e064e494 --- /dev/null +++ b/src/util/array.ts @@ -0,0 +1,13 @@ +/** + * Perhaps silly utility. + * Designed to help out with when you must sort an array and then resort it back to its original order. + * + * First you `.map(mapToItemAndOrder)` the array, then you sort or order it however you want, after which you can simply resort it by + * + * , specifically where `map(mapToItemAndOrder) is followed up with with a sort operation on the result, using the `idx` (2nd return array item) + */ +export const mapToItemAndOrder = (o: A, idx: number) => [o, idx] as [A, number]; + +export const mapToOriginalOrder = ([_o, idx]: [A, number]) => idx; + +export const mapToOriginalItem = ([o]: [A, number]) => o; diff --git a/src/util/lodashMixins.ts b/src/util/lodashMixins.ts new file mode 100644 index 000000000..0c3a229e9 --- /dev/null +++ b/src/util/lodashMixins.ts @@ -0,0 +1,91 @@ +import * as _ from 'lodash'; + +export const replaceAt = (array: A[], index: number, replacement: A): A[] => { + return array + .slice(0, index) + .concat([replacement]) + .concat(array.slice(index + 1)); +}; + +export function doto(x: T, ...fns: ((x: T) => any)[]): T { + for (const fn of fns) { + fn?.(x); + } + return x; +} + +export function isBlank(s: string): boolean { + return s.trim().length === 0; +} + +// like _.property combined with clojure's select-keys; +// returns a new object with only the specified keys (or nested lodash property paths) +export function properties(...keys: string[]) { + return (obj: any) => _.pick(obj, keys); +} + +// like clojure's comp fn +// export function comp(...fns: ((...args: any[]) => any)[]) { return _.flowRight(fns); } +export const comp = _.flowRight; + +declare module 'lodash' { + interface LoDashStatic { + doto: (x: T, ...fns: ((x: T) => any)[]) => T; + isBlank: (s: string) => boolean; + replaceAt: typeof replaceAt; + properties: typeof properties; + comp: typeof comp; + } + // interface LoDashImplicitWrapper { + // doto(...fns: ((x: TValue) => any)[]): LoDashImplicitWrapper; + // isBlank(): boolean; + // replaceAt(index: number, replacement: TValue): TValue[]; + // } + interface LoDashImplicitWrapper { + /** + * @see _.doto + */ + // doto( + doto( + this: LoDashImplicitWrapper, + ...fns: ((x: TValue) => any)[] + ): LoDashImplicitWrapper; + doto( + this: LoDashImplicitWrapper | null | undefined>, + ...fns: ((x: T) => any)[] + ): LoDashImplicitWrapper>; + doto( + this: LoDashImplicitWrapper | null | undefined>, + ...fns: ((x: T) => any)[] + ): LoDashImplicitWrapper>; + doto( + this: LoDashImplicitWrapper, + ...fns: ((x: T) => any)[] + ): LoDashImplicitWrapper; + isBlank(this: LoDashImplicitWrapper): boolean; + replaceAt( + this: LoDashImplicitWrapper, + index: number, + replacement: TValue + ): TValue[]; + } + interface LoDashExplicitWrapper { + /** + * @see _.doto + */ + // doto( + doto( + this: LoDashExplicitWrapper, + ...fns: ((x: T) => any)[] + ): LoDashExplicitWrapper; + isBlank(this: LoDashImplicitWrapper): boolean; + replaceAt( + this: LoDashImplicitWrapper, + index: number, + replacement: T + ): T[]; + } +} + +_.mixin({ doto, isBlank, replaceAt }, { chain: true }); +_.mixin({ properties, comp }, { chain: false }); From 0f8b404094fc66ae1d3eb64fbc37dec34466eed9 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 11 Apr 2024 10:57:02 +0000 Subject: [PATCH 04/19] Bring on version 2.0.440! --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index fb04911c7..a90080be8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "calva", - "version": "2.0.439", + "version": "2.0.440", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "calva", - "version": "2.0.439", + "version": "2.0.440", "license": "MIT", "dependencies": { "@vscode/debugadapter": "^1.64.0", diff --git a/package.json b/package.json index d96d6fb90..6620770ec 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Calva: Clojure & ClojureScript Interactive Programming", "description": "Integrated REPL, formatter, Paredit, and more. Powered by cider-nrepl and clojure-lsp.", "icon": "assets/calva.png", - "version": "2.0.439", + "version": "2.0.440", "publisher": "betterthantomorrow", "author": { "name": "Better Than Tomorrow", From ba620ee0e3673eeac29280ba958d5827e50668be Mon Sep 17 00:00:00 2001 From: Rayat M Rahman <22646419+riotrah@users.noreply.github.com> Date: Sun, 31 Mar 2024 21:21:42 -0700 Subject: [PATCH 05/19] Support command args in paredit cmds --- src/paredit/extension.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index 823f52106..91dfa21f7 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -48,8 +48,9 @@ function multiCursorEnabled() { type PareditCommand = { command: string; - handler: (doc: EditableDocument) => void | Promise; + handler: (doc: EditableDocument, ...args: readonly any[]) => void | Promise | Thenable; // do we still need to return Thenable from paredit fns? }; + const pareditCommands: PareditCommand[] = [ // NAVIGATING { @@ -470,7 +471,7 @@ const pareditCommands: PareditCommand[] = [ ]; function wrapPareditCommand(command: PareditCommand) { - return async () => { + return async (...args: readonly any[]) => { try { const textEditor = window.activeTextEditor; @@ -480,7 +481,7 @@ function wrapPareditCommand(command: PareditCommand) { if (!enabled || !languages.has(textEditor.document.languageId)) { return; } - return command.handler(mDoc); + return command.handler(mDoc, ...args); } catch (e) { console.error(e.message); } From 9ee1d3c8d730cd7aeefe7e9abb4cc7121eadad6e Mon Sep 17 00:00:00 2001 From: Rayat M Rahman <22646419+riotrah@users.noreply.github.com> Date: Sun, 31 Mar 2024 22:12:08 -0700 Subject: [PATCH 06/19] Fix types because cmds get only 1 arg --- src/paredit/extension.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index 91dfa21f7..eeb67c4e6 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -48,7 +48,8 @@ function multiCursorEnabled() { type PareditCommand = { command: string; - handler: (doc: EditableDocument, ...args: readonly any[]) => void | Promise | Thenable; // do we still need to return Thenable from paredit fns? + // do we still need to return Thenable from paredit fns? + handler: (doc: EditableDocument, arg: any) => void | Promise | Thenable; }; const pareditCommands: PareditCommand[] = [ @@ -304,6 +305,7 @@ const pareditCommands: PareditCommand[] = [ // TODO: support multicursor return handlers.killLeft( doc, + // TODO: actually implement multicursor multiCursorEnabled(), shouldKillAlsoCutToClipboard() ? copyRangeToClipboard : null ); @@ -471,7 +473,7 @@ const pareditCommands: PareditCommand[] = [ ]; function wrapPareditCommand(command: PareditCommand) { - return async (...args: readonly any[]) => { + return async (arg) => { try { const textEditor = window.activeTextEditor; @@ -481,7 +483,7 @@ function wrapPareditCommand(command: PareditCommand) { if (!enabled || !languages.has(textEditor.document.languageId)) { return; } - return command.handler(mDoc, ...args); + return command.handler(mDoc, arg); } catch (e) { console.error(e.message); } From 02e8dc8cd6156bc94bc869a9793c9d6b3bb66773 Mon Sep 17 00:00:00 2001 From: Rayat M Rahman <22646419+riotrah@users.noreply.github.com> Date: Sun, 31 Mar 2024 22:13:45 -0700 Subject: [PATCH 07/19] Make multicursor paredit cmds accept override arg Like: `args: {multicursor: true/false}` --- src/paredit/extension.ts | 134 +++++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 62 deletions(-) diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index eeb67c4e6..d77d2fdf5 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -42,8 +42,8 @@ function shouldKillAlsoCutToClipboard() { return workspace.getConfiguration().get('calva.paredit.killAlsoCutsToClipboard'); } -function multiCursorEnabled() { - return workspace.getConfiguration().get('calva.paredit.multicursor'); +function multiCursorEnabled(override?: boolean): boolean { + return override ?? workspace.getConfiguration().get('calva.paredit.multicursor'); } type PareditCommand = { @@ -56,169 +56,179 @@ const pareditCommands: PareditCommand[] = [ // NAVIGATING { command: 'paredit.forwardSexp', - handler: (doc: EditableDocument) => { - handlers.forwardSexp(doc, multiCursorEnabled()); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); + handlers.forwardSexp(doc, isMulti); }, }, { command: 'paredit.backwardSexp', - handler: (doc: EditableDocument) => { - handlers.backwardSexp(doc, multiCursorEnabled()); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); + handlers.backwardSexp(doc, isMulti); }, }, { command: 'paredit.forwardDownSexp', - handler: (doc: EditableDocument) => { - handlers.forwardDownSexp(doc, multiCursorEnabled()); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); + handlers.forwardDownSexp(doc, isMulti); }, }, { command: 'paredit.backwardDownSexp', - handler: (doc: EditableDocument) => { - handlers.backwardDownSexp(doc, multiCursorEnabled()); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); + handlers.backwardDownSexp(doc, isMulti); }, }, { command: 'paredit.forwardUpSexp', - handler: (doc: EditableDocument) => { - handlers.forwardUpSexp(doc, multiCursorEnabled()); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); + handlers.forwardUpSexp(doc, isMulti); }, }, { command: 'paredit.backwardUpSexp', - handler: (doc: EditableDocument) => { - handlers.backwardUpSexp(doc, multiCursorEnabled()); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); + handlers.backwardUpSexp(doc, isMulti); }, }, { command: 'paredit.forwardSexpOrUp', - handler: (doc: EditableDocument) => { - handlers.forwardSexpOrUp(doc, multiCursorEnabled()); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); + handlers.forwardSexpOrUp(doc, isMulti); }, }, { command: 'paredit.backwardSexpOrUp', - handler: (doc: EditableDocument) => { - handlers.backwardSexpOrUp(doc, multiCursorEnabled()); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); + handlers.backwardSexpOrUp(doc, isMulti); }, }, { command: 'paredit.closeList', - handler: (doc: EditableDocument) => { - handlers.closeList(doc, multiCursorEnabled()); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); + handlers.closeList(doc, isMulti); }, }, { command: 'paredit.openList', - handler: (doc: EditableDocument) => { - handlers.openList(doc, multiCursorEnabled()); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); + handlers.openList(doc, isMulti); }, }, // SELECTING { command: 'calva.selectCurrentForm', // legacy command id for backward compat - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); handlers.selectCurrentForm(doc, isMulti); }, }, { command: 'paredit.rangeForDefun', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); handlers.rangeForDefun(doc, isMulti); }, }, { command: 'paredit.sexpRangeExpansion', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); handlers.sexpRangeExpansion(doc, isMulti); }, }, { command: 'paredit.sexpRangeContraction', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); handlers.sexpRangeContraction(doc, isMulti); }, }, { command: 'paredit.selectForwardSexp', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); handlers.selectForwardSexp(doc, isMulti); }, }, { command: 'paredit.selectRight', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); handlers.selectRight(doc, isMulti); }, }, { command: 'paredit.selectBackwardSexp', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); handlers.selectBackwardSexp(doc, isMulti); }, }, { command: 'paredit.selectForwardDownSexp', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); handlers.selectForwardDownSexp(doc, isMulti); }, }, { command: 'paredit.selectBackwardDownSexp', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); handlers.selectBackwardDownSexp(doc, isMulti); }, }, { command: 'paredit.selectForwardUpSexp', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); handlers.selectForwardUpSexp(doc, isMulti); }, }, { command: 'paredit.selectForwardSexpOrUp', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); handlers.selectForwardSexpOrUp(doc, isMulti); }, }, { command: 'paredit.selectBackwardSexpOrUp', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); handlers.selectBackwardSexpOrUp(doc, isMulti); }, }, { command: 'paredit.selectBackwardUpSexp', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); handlers.selectBackwardUpSexp(doc, isMulti); }, }, { command: 'paredit.selectCloseList', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); handlers.selectCloseList(doc, isMulti); }, }, { command: 'paredit.selectOpenList', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); handlers.selectOpenList(doc, isMulti); }, }, @@ -401,36 +411,36 @@ const pareditCommands: PareditCommand[] = [ }, { command: 'paredit.rewrapParens', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); return handlers.rewrapParens(doc, isMulti); }, }, { command: 'paredit.rewrapSquare', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); return handlers.rewrapSquare(doc, isMulti); }, }, { command: 'paredit.rewrapCurly', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); return handlers.rewrapCurly(doc, isMulti); }, }, { command: 'paredit.rewrapSet', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); return handlers.rewrapSet(doc, isMulti); }, }, { command: 'paredit.rewrapQuote', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); return handlers.rewrapQuote(doc, isMulti); }, }, From 142992ec41468097c45ca2133ef77b3101af8c4d Mon Sep 17 00:00:00 2001 From: Rayat M Rahman <22646419+riotrah@users.noreply.github.com> Date: Sun, 31 Mar 2024 22:15:35 -0700 Subject: [PATCH 08/19] Make kill cmds accept arg override for copy Like: `"args": {copy: true/false}` --- src/paredit/extension.ts | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index d77d2fdf5..43681c9f7 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -38,8 +38,8 @@ export async function copyRangeToClipboard(doc: EditableDocument, [start, end]) * Answers true when `calva.paredit.killAlsoCutsToClipboard` is enabled. * @returns boolean */ -function shouldKillAlsoCutToClipboard() { - return workspace.getConfiguration().get('calva.paredit.killAlsoCutsToClipboard'); +function shouldKillAlsoCutToClipboard(override?: boolean): boolean { + return override ?? workspace.getConfiguration().get('calva.paredit.killAlsoCutsToClipboard'); } function multiCursorEnabled(override?: boolean): boolean { @@ -301,9 +301,9 @@ const pareditCommands: PareditCommand[] = [ }, { command: 'paredit.killRight', - handler: async (doc: EditableDocument) => { + handler: async (doc: EditableDocument, opts?: { copy: boolean }) => { const range = paredit.forwardHybridSexpRange(doc); - if (shouldKillAlsoCutToClipboard()) { + if (shouldKillAlsoCutToClipboard(opts?.copy)) { await copyRangeToClipboard(doc, range); } return paredit.killRange(doc, range); @@ -311,21 +311,20 @@ const pareditCommands: PareditCommand[] = [ }, { command: 'paredit.killLeft', - handler: async (doc: EditableDocument) => { - // TODO: support multicursor + handler: async (doc: EditableDocument, opts?: { copy: boolean }) => { return handlers.killLeft( doc, // TODO: actually implement multicursor multiCursorEnabled(), - shouldKillAlsoCutToClipboard() ? copyRangeToClipboard : null + shouldKillAlsoCutToClipboard(opts?.copy) ? copyRangeToClipboard : null ); }, }, { command: 'paredit.killSexpForward', - handler: async (doc: EditableDocument) => { + handler: async (doc: EditableDocument, opts?: { copy: boolean }) => { const range = paredit.forwardSexpRange(doc); - if (shouldKillAlsoCutToClipboard()) { + if (shouldKillAlsoCutToClipboard(opts?.copy)) { await copyRangeToClipboard(doc, range); } return paredit.killRange(doc, range); @@ -333,9 +332,9 @@ const pareditCommands: PareditCommand[] = [ }, { command: 'paredit.killSexpBackward', - handler: async (doc: EditableDocument) => { + handler: async (doc: EditableDocument, opts?: { copy: boolean }) => { const range = paredit.backwardSexpRange(doc); - if (shouldKillAlsoCutToClipboard()) { + if (shouldKillAlsoCutToClipboard(opts?.copy)) { await copyRangeToClipboard(doc, range); } return paredit.killRange(doc, range); @@ -343,9 +342,9 @@ const pareditCommands: PareditCommand[] = [ }, { command: 'paredit.killListForward', - handler: async (doc: EditableDocument) => { + handler: async (doc: EditableDocument, opts?: { copy: boolean }) => { const range = paredit.forwardListRange(doc); - if (shouldKillAlsoCutToClipboard()) { + if (shouldKillAlsoCutToClipboard(opts?.copy)) { await copyRangeToClipboard(doc, range); } return await paredit.killForwardList(doc, range); @@ -353,9 +352,9 @@ const pareditCommands: PareditCommand[] = [ }, // TODO: Implement with killRange { command: 'paredit.killListBackward', - handler: async (doc: EditableDocument) => { + handler: async (doc: EditableDocument, opts?: { copy: boolean }) => { const range = paredit.backwardListRange(doc); - if (shouldKillAlsoCutToClipboard()) { + if (shouldKillAlsoCutToClipboard(opts?.copy)) { await copyRangeToClipboard(doc, range); } return await paredit.killBackwardList(doc, range); @@ -363,9 +362,9 @@ const pareditCommands: PareditCommand[] = [ }, // TODO: Implement with killRange { command: 'paredit.spliceSexpKillForward', - handler: async (doc: EditableDocument) => { + handler: async (doc: EditableDocument, opts?: { copy: boolean }) => { const range = paredit.forwardListRange(doc); - if (shouldKillAlsoCutToClipboard()) { + if (shouldKillAlsoCutToClipboard(opts?.copy)) { await copyRangeToClipboard(doc, range); } await paredit.killForwardList(doc, range).then((isFulfilled) => { @@ -375,9 +374,9 @@ const pareditCommands: PareditCommand[] = [ }, { command: 'paredit.spliceSexpKillBackward', - handler: async (doc: EditableDocument) => { + handler: async (doc: EditableDocument, opts?: { copy: boolean }) => { const range = paredit.backwardListRange(doc); - if (shouldKillAlsoCutToClipboard()) { + if (shouldKillAlsoCutToClipboard(opts?.copy)) { await copyRangeToClipboard(doc, range); } await paredit.killBackwardList(doc, range).then((isFulfilled) => { From 20a2da6dd5be69a72fe2c452a3b9216c162c6b0d Mon Sep 17 00:00:00 2001 From: Rayat M Rahman <22646419+riotrah@users.noreply.github.com> Date: Sun, 31 Mar 2024 23:25:35 -0700 Subject: [PATCH 09/19] Add some more static typing to paredit extension code --- src/paredit/extension.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index 43681c9f7..bcf591a0e 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -48,11 +48,13 @@ function multiCursorEnabled(override?: boolean): boolean { type PareditCommand = { command: string; - // do we still need to return Thenable from paredit fns? - handler: (doc: EditableDocument, arg: any) => void | Promise | Thenable; + handler: (doc: EditableDocument, arg?: any) => void | Promise | Thenable; }; -const pareditCommands: PareditCommand[] = [ +// only grab the custom, additional args after the first doc arg from the given command's handler +type CommandArgOf = Parameters[1]; + +const pareditCommands = [ // NAVIGATING { command: 'paredit.forwardSexp', @@ -479,10 +481,12 @@ const pareditCommands: PareditCommand[] = [ await paredit.insertSemiColon(doc); }, }, -]; +] as const; +// prefer next line if we upgrade to TS v4.9+ +// ] as const satisfies readonly PareditCommand[]; -function wrapPareditCommand(command: PareditCommand) { - return async (arg) => { +function wrapPareditCommand(command: C) { + return async (arg: CommandArgOf) => { try { const textEditor = window.activeTextEditor; From 7a01a5de8311d87324cc6c2d9cd74511ce7fcc98 Mon Sep 17 00:00:00 2001 From: Rayat M Rahman <22646419+riotrah@users.noreply.github.com> Date: Mon, 1 Apr 2024 10:34:38 -0700 Subject: [PATCH 10/19] Add changelog entry for command args --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5c8eeafd..d5835b23f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes to Calva. ## [Unreleased] +- [Support command binding args to toggle multicursor per command or toggle copy per kill command. Closes #2485](https://github.com/BetterThanTomorrow/calva/issues/2485) + ## [2.0.439] - 2024-04-11 - Fix: [Refresh Changed Namespaces do not output to the selected output destination](https://github.com/BetterThanTomorrow/calva/issues/2506) From 12f2d3fb751d476fb8f9d6520beb47f65bdc8efe Mon Sep 17 00:00:00 2001 From: Rayat M Rahman <22646419+riotrah@users.noreply.github.com> Date: Tue, 2 Apr 2024 00:20:13 -0700 Subject: [PATCH 11/19] Add paredit docsite section about command args --- docs/site/paredit.md | 53 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/docs/site/paredit.md b/docs/site/paredit.md index 877586e7b..897456cd3 100644 --- a/docs/site/paredit.md +++ b/docs/site/paredit.md @@ -161,6 +161,59 @@ There are some context keys you can utilize to configure keyboard shortcuts with In some instances built-in command defaults are the same as Paredit's defaults, and Paredit's functionality in a particular case is less than what the default is. This is true of *Expand Selection* and *Shrink Selection* for Windows/Linux when multiple lines are selected. In this particular case adding `!editorHasMultipleSelections` to the `when` clause of the binding makes for a better workflow. The point is that when the bindings overlap and default functionality is desired peaceful integration can be achieved with the right `when` clause. This is left out of Paredit's defaults to respect user preference, and ease of maintenance. +### Command Args + +VSCode allows you to provide arguments to configured bindings, allowing, for example, different variants or behaviors for the same command, each bound to different shortcuts. Calva takes advantage of this by offering two commands with args: + +#### **1. `copy` for all `kill*` commands** + +When specified, will control whether killed text will be copied to the clipboard. +This is an alternative to, or supports binding-specific overrides for, `calva.paredit.killAlsoCutsToClipboard`. + +For example, here's 2 keybindings for `paredit.killRight` with different `copy` args, allowing you to choose when or if you want killed text copied at keypress-time, regardless of global `calva.paredit.killAlsoCutsToClipboard` setting: + +```json +{ + "key": "ctrl+k", + "command": "paredit.killRight", + "when": "... your when conditions ...", + "args": {"copy": false} +}, +{ + "key": "cmd+k ctrl+k", + "command": "paredit.killRight", + "when": "... your when conditions ...", + "args": {"copy": true} +}, +``` + +Or, you can even have both of them use the **same `key`**, but **separate `when` conditions** to taste, to allow context-conditional copying. + +#### **2. `multicursor` for all experimental multicursor-enabled commands** + +When specified, will control whether multiple cursors are handled in supported paredit commands, or +if cursors after the first are discarded. As mentioned in the [Multicursor section below](#experimental-feature-multicursor-support), this is an experimental feature and is not enabled by default. This arg allows you to toggle multicursor globally except for a few commands. +This is an alternative to, or supports binding-specific overrides for, `calva.paredit.multcursor`. + +For example, here's 2 keybindings for `paredit.sexpRangeExpansion` with different `multicursor` args, allowing you to choose when or if you want multicursor handling at keypress-time, regardless of global `calva.paredit.multicursor` setting: + +```json +{ + "key": "ctrl+k", + "command": "paredit.sexpRangeExpansion", + "when": "... your when conditions ...", + "args": {"multicursor": false} +}, +{ + "key": "cmd+k ctrl+k", + "command": "paredit.sexpRangeExpansion", + "when": "... your when conditions ...", + "args": {"multicursor": true} +}, +``` + +Or, you can even have both of them use the **same `key`**, but **separate `when` conditions** to taste, allowing context-conditional multicursor. + Happy Editing! ❤️ ## Experimental Feature: Multicursor support From a03220b19838608f6863849fe9944ef7b68515fb Mon Sep 17 00:00:00 2001 From: Rayat M Rahman <22646419+riotrah@users.noreply.github.com> Date: Tue, 2 Apr 2024 02:42:09 -0700 Subject: [PATCH 12/19] Move command args kill copy section up --- docs/site/paredit.md | 80 ++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 52 deletions(-) diff --git a/docs/site/paredit.md b/docs/site/paredit.md index 897456cd3..ca2d5f095 100644 --- a/docs/site/paredit.md +++ b/docs/site/paredit.md @@ -58,6 +58,34 @@ The Paredit commands are sorted into **Navigation**, **Selection**, and **Edit** To make the command descriptions a bit clearer, each entry is animated. When you try to figure out what is going on in the GIFs, focus on where the cursor is at the start of the animation loop. +### Command Args + +VSCode allows you to provide arguments to configured bindings, allowing, for example, different variants or behaviors for the same command, each bound to different shortcuts. Calva takes advantage of this by offering one official command with args: + +#### **`copy` for all `kill*` commands** + +When specified, will control whether killed text will be copied to the clipboard. +This is an alternative to, or supports binding-specific overrides for, `calva.paredit.killAlsoCutsToClipboard`. + +For example, here's 2 keybindings for `paredit.killRight` with different `copy` args, allowing you to choose when or if you want killed text copied at keypress-time, regardless of global `calva.paredit.killAlsoCutsToClipboard` setting: + +```json +{ + "key": "ctrl+k", + "command": "paredit.killRight", + "when": "... your when conditions ...", + "args": {"copy": false} +}, +{ + "key": "cmd+k ctrl+k", + "command": "paredit.killRight", + "when": "... your when conditions ...", + "args": {"copy": true} +}, +``` + +Or, you can even have both of them use the **same `key`**, but **separate `when` conditions** to taste, to allow context-conditional copying. + ### Strings are not Lists, but Anyway... In Calva Paredit, strings are treated in much the same way as lists are. Here's an example showing **Slurp** and **Barf**, **Forward/Backward List**, and **Expand Selection**. @@ -161,58 +189,6 @@ There are some context keys you can utilize to configure keyboard shortcuts with In some instances built-in command defaults are the same as Paredit's defaults, and Paredit's functionality in a particular case is less than what the default is. This is true of *Expand Selection* and *Shrink Selection* for Windows/Linux when multiple lines are selected. In this particular case adding `!editorHasMultipleSelections` to the `when` clause of the binding makes for a better workflow. The point is that when the bindings overlap and default functionality is desired peaceful integration can be achieved with the right `when` clause. This is left out of Paredit's defaults to respect user preference, and ease of maintenance. -### Command Args - -VSCode allows you to provide arguments to configured bindings, allowing, for example, different variants or behaviors for the same command, each bound to different shortcuts. Calva takes advantage of this by offering two commands with args: - -#### **1. `copy` for all `kill*` commands** - -When specified, will control whether killed text will be copied to the clipboard. -This is an alternative to, or supports binding-specific overrides for, `calva.paredit.killAlsoCutsToClipboard`. - -For example, here's 2 keybindings for `paredit.killRight` with different `copy` args, allowing you to choose when or if you want killed text copied at keypress-time, regardless of global `calva.paredit.killAlsoCutsToClipboard` setting: - -```json -{ - "key": "ctrl+k", - "command": "paredit.killRight", - "when": "... your when conditions ...", - "args": {"copy": false} -}, -{ - "key": "cmd+k ctrl+k", - "command": "paredit.killRight", - "when": "... your when conditions ...", - "args": {"copy": true} -}, -``` - -Or, you can even have both of them use the **same `key`**, but **separate `when` conditions** to taste, to allow context-conditional copying. - -#### **2. `multicursor` for all experimental multicursor-enabled commands** - -When specified, will control whether multiple cursors are handled in supported paredit commands, or -if cursors after the first are discarded. As mentioned in the [Multicursor section below](#experimental-feature-multicursor-support), this is an experimental feature and is not enabled by default. This arg allows you to toggle multicursor globally except for a few commands. -This is an alternative to, or supports binding-specific overrides for, `calva.paredit.multcursor`. - -For example, here's 2 keybindings for `paredit.sexpRangeExpansion` with different `multicursor` args, allowing you to choose when or if you want multicursor handling at keypress-time, regardless of global `calva.paredit.multicursor` setting: - -```json -{ - "key": "ctrl+k", - "command": "paredit.sexpRangeExpansion", - "when": "... your when conditions ...", - "args": {"multicursor": false} -}, -{ - "key": "cmd+k ctrl+k", - "command": "paredit.sexpRangeExpansion", - "when": "... your when conditions ...", - "args": {"multicursor": true} -}, -``` - -Or, you can even have both of them use the **same `key`**, but **separate `when` conditions** to taste, allowing context-conditional multicursor. Happy Editing! ❤️ From 280b4b6ff14a0fac341b68a74939ba41c18403f7 Mon Sep 17 00:00:00 2001 From: Rayat M Rahman <22646419+riotrah@users.noreply.github.com> Date: Tue, 2 Apr 2024 02:45:00 -0700 Subject: [PATCH 13/19] Add command arg note to multicursor section --- docs/site/paredit.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/site/paredit.md b/docs/site/paredit.md index ca2d5f095..ffb19f7fe 100644 --- a/docs/site/paredit.md +++ b/docs/site/paredit.md @@ -199,3 +199,18 @@ There is an ongoing effort to support simultaneous multicursor editing with Pare - Movement - Selection (except for `Select Current Form` - coming soon!) - Rewrap + +### Toggling Multicursor per command + +The experimental multicursor-supported commands support an optional command arg - like `copy` for the `kill*` commands [mentioned above](#command-args) - to control whether multicursor is enabled for that command. This is an alternative to, or supports binding-specific overrides for, `calva.paredit.multicursor`. + +For example: + +```json +{ + "key": "ctrl+k", + "command": "paredit.sexpRangeExpansion", + "when": "... your when conditions ...", + "args": {"multicursor": false} +} +``` From e5a0e43944499d9041927ee5b50be0cc4366d35c Mon Sep 17 00:00:00 2001 From: Rayat M Rahman <22646419+riotrah@users.noreply.github.com> Date: Tue, 2 Apr 2024 02:58:00 -0700 Subject: [PATCH 14/19] Edit and augment About Keyboard Shortcuts section --- docs/site/paredit.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/site/paredit.md b/docs/site/paredit.md index ffb19f7fe..3829db6aa 100644 --- a/docs/site/paredit.md +++ b/docs/site/paredit.md @@ -187,8 +187,31 @@ There are some context keys you can utilize to configure keyboard shortcuts with *The Nuclear Option*: You can choose to disable all default key bindings by configuring `calva.paredit.defaultKeyMap` to `none`. (Then you probably also want to register your own shortcuts for the commands you often use.) -In some instances built-in command defaults are the same as Paredit's defaults, and Paredit's functionality in a particular case is less than what the default is. This is true of *Expand Selection* and *Shrink Selection* for Windows/Linux when multiple lines are selected. In this particular case adding `!editorHasMultipleSelections` to the `when` clause of the binding makes for a better workflow. The point is that when the bindings overlap and default functionality is desired peaceful integration can be achieved with the right `when` clause. This is left out of Paredit's defaults to respect user preference, and ease of maintenance. +### When Clauses and VSCode Default Bindings +There are instances where VSCode's built-in command binding defaults are the same as Paredit's, where Paredit's version has less functionality. For example, Calva's _Expand Selection_ and _Shrink Selection_ doesn't support multiple selections (though this may change in the future - see Multicursor section below). In this particular case, adding `!editorHasMultipleSelections` to the `when` clause of the binding makes up for this gap by letting the binding fall back to VSCode's native grow/shrink selection. + +For example, here's the JSON version of the keybindings settings demonstrating the above. Note this can also specified in the Keyboard Shortcuts UI: + +```json +{ + "key": "shift+alt+right", + "command": "paredit.sexpRangeExpansion", + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !calva:cursorInComment" +} +``` + +to + +```json +{ + "key": "shift+alt+right", + "command": "paredit.sexpRangeExpansion", + "when": "!editorHasMultipleSelections && calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !calva:cursorInComment" +} +``` + +The point is that when the bindings overlap and default functionality is desired peaceful integration can be achieved with the right `when` clause. This is left out of Paredit's defaults to respect user preference, and ease of maintenance. Happy Editing! ❤️ From e9e8e89f1120aad492fa2a7ea486bf04ea9b013b Mon Sep 17 00:00:00 2001 From: Rayat Rahman <22646419+riotrah@users.noreply.github.com> Date: Wed, 10 Apr 2024 16:05:15 -0900 Subject: [PATCH 15/19] Update docs/site/paredit.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Peter Strömberg --- docs/site/paredit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/site/paredit.md b/docs/site/paredit.md index 3829db6aa..cbc33b6dd 100644 --- a/docs/site/paredit.md +++ b/docs/site/paredit.md @@ -60,7 +60,7 @@ To make the command descriptions a bit clearer, each entry is animated. When you ### Command Args -VSCode allows you to provide arguments to configured bindings, allowing, for example, different variants or behaviors for the same command, each bound to different shortcuts. Calva takes advantage of this by offering one official command with args: +Some Paredit commands accept arguments. You can utilize this in keybindings and from [Joyride](https://github.com/BetterThanTomorrow/joyride). #### **`copy` for all `kill*` commands** From 0e5715d4d5bf8109adad8198875c7bba52ba25ec Mon Sep 17 00:00:00 2001 From: Rayat M Rahman <22646419+riotrah@users.noreply.github.com> Date: Wed, 27 Mar 2024 16:28:11 -0700 Subject: [PATCH 16/19] start paredit multicursor wrap --- .mocharc.json | 1 + CHANGELOG.md | 2 + package.json | 9 +- src/cursor-doc/cursor-doc-utils.ts | 117 +++++++ src/cursor-doc/model.ts | 8 + src/cursor-doc/paredit.ts | 116 ++++--- .../unit/cursor-doc/paredit-test.ts | 149 +++++++++ .../unit/paredit/commands-test.ts | 303 ++++++++++++++++++ src/paredit/commands.ts | 22 ++ src/paredit/extension.ts | 19 +- src/util/array.ts | 13 + src/util/lodashMixins.ts | 91 ++++++ 12 files changed, 804 insertions(+), 46 deletions(-) create mode 100644 .mocharc.json create mode 100644 src/cursor-doc/cursor-doc-utils.ts create mode 100644 src/util/array.ts create mode 100644 src/util/lodashMixins.ts diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 000000000..e41150a91 --- /dev/null +++ b/.mocharc.json @@ -0,0 +1 @@ +{ "require": ["ts-node/register", "src/util/lodashMixins.ts"] } diff --git a/CHANGELOG.md b/CHANGELOG.md index d5835b23f..83f4f3a86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes to Calva. ## [Unreleased] +- Add "Wrap with Set #{}" paredit command. +- [Implement experimental support for multicursor wrap commands](https://github.com/BetterThanTomorrow/calva/issues/2448). Enable `calva.paredit.multicursor` in your settings to try it out. Addressing [#2445](https://github.com/BetterThanTomorrow/calva/issues/2445) - [Support command binding args to toggle multicursor per command or toggle copy per kill command. Closes #2485](https://github.com/BetterThanTomorrow/calva/issues/2485) ## [2.0.439] - 2024-04-11 diff --git a/package.json b/package.json index 6620770ec..09354a3d6 100644 --- a/package.json +++ b/package.json @@ -2564,6 +2564,11 @@ "key": "ctrl+alt+shift+q", "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/" }, + { + "command": "paredit.wrapAroundSet", + "key": "ctrl+alt+shift+h", + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/" + }, { "command": "paredit.rewrapParens", "key": "ctrl+alt+r ctrl+alt+p", @@ -3221,8 +3226,8 @@ "integration-test": "node ./out/extension-test/integration/runTests.js", "e2e-test": "node ./src/extension-test/e2e-test/launch.js", "pree2e-test": "cd ./src/extension-test/e2e-test/ && npm i", - "unit-test": "npx mocha --require ts-node/register 'src/extension-test/unit/**/*-test.ts'", - "unit-test-watch": "npx mocha --watch --require ts-node/register --watch-extensions ts --watch-files src 'src/extension-test/unit/**/*-test.ts'", + "unit-test": "npx mocha --require ts-node/register,src/util/lodashMixins.ts 'src/extension-test/unit/**/*-test.ts'", + "unit-test-watch": "npx mocha --watch --require ts-node/register,src/util/lodashMixins.ts --watch-extensions ts --watch-files src 'src/extension-test/unit/**/*-test.ts'", "prettier-format": "npx prettier --write \"./**/*.{ts,js,json}\"", "prettier-check": "npx prettier --check \"./**/*.{ts,js,json}\"", "prettier-check-watch": "onchange \"./**/*.{ts,js,json}\" -- prettier --check {{changed}}", diff --git a/src/cursor-doc/cursor-doc-utils.ts b/src/cursor-doc/cursor-doc-utils.ts new file mode 100644 index 000000000..4ba5ba70f --- /dev/null +++ b/src/cursor-doc/cursor-doc-utils.ts @@ -0,0 +1,117 @@ +import { first, isNumber, last, ListIterator, range } from 'lodash'; +import { isModelEditSelection, isModelRange, ModelEditRange, ModelEditSelection } from './model'; +import _ = require('lodash'); + +type RangeOrSelection = ModelEditRange | ModelEditSelection; +export function mapRangeOrSelectionToOffset1( + side: 'start' | 'end' | 'anchor' | 'active' = 'start' +) { + return function inner( + // support passing either range/sel or [range/sel, original list order] + t: RangeOrSelection | [rangeOrSel: RangeOrSelection, order: number] + ): number { + // const rangeOrSel = isModelEditSelection(t) || isModelRange(t) ? t : first(t); + const rangeOrSel = isModelEditSelection(t) ? t : isModelRange(t) ? t : t[0]; + + if (rangeOrSel instanceof ModelEditSelection) { + return rangeOrSel[side]; + } else if (isModelRange(rangeOrSel)) { + // let fn: (...args: number[]) => number; + switch (side) { + case 'start': + // fn = Math.min; + return Math.min(...rangeOrSel); + // break; + case 'end': + // fn = Math.max; + return Math.max(...rangeOrSel); + // break; + case 'anchor': + // fn = (...x) => first(x); + return first(rangeOrSel); + // break; + case 'active': + // fn = (...x) => last(x); + return last(rangeOrSel); + // break; + default: + // break; + return range[0]; + } + + // return fn(...rangeOrSel); + // return fn(...rangeOrSel); + } + }; +} +export function mapRangeOrSelectionToOffset(side: 'start' | 'end' | 'anchor' | 'active' = 'start') { + return function inner( + // support passing either range/sel or [range/sel, original list order] + t: RangeOrSelection | [rangeOrSel: RangeOrSelection, order: number] + ): number { + const rangeOrSel = isModelEditSelection(t) ? t : isModelRange(t) ? t : t[0]; + + if (rangeOrSel instanceof ModelEditSelection) { + return rangeOrSel[side]; + } else if (isModelRange(rangeOrSel)) { + switch (side) { + case 'start': + return Math.min(...rangeOrSel); + case 'end': + return Math.max(...rangeOrSel); + case 'anchor': + return first(rangeOrSel); + case 'active': + return last(rangeOrSel); + default: + return range[0]; + } + } + }; +} + +export function repositionSelectionByCumulativeOffsets( + /** + * Either a fixed offset to add for each cursor (eg 2 if wrapping by parens), + * or a 'getter' fn to get the value from each cursor. + */ + offsetGetter: ListIterator | number +) { + // if (true) { + return repositionSelectionWithGetterByCumulativeOffsets( + _.identity, + offsetGetter + ); +} + +export function repositionSelectionWithGetterByCumulativeOffsets( + selectionGetter: ListIterator, + /** + * Either a fixed offset to add for each cursor (eg 2 if wrapping by parens), + * or a 'getter' fn to get the value from each cursor. + */ + offsetGetter: ListIterator | number +) { + return ( + t: T, + index: number, + // array: ModelEditSelection[] + array: T[] + ): ModelEditSelection => { + const sel = selectionGetter(t, index, array); + const newSel = sel.clone(); + + const getItemOffset = isNumber(offsetGetter) ? () => offsetGetter : offsetGetter; + + const offset = _(array) + .filter((x, i, a) => { + const s = selectionGetter(x, i, a); + return s.start < sel.start; + }) + .map(getItemOffset) + .sum(); + + newSel.reposition(offset); + return newSel; + }; +} diff --git a/src/cursor-doc/model.ts b/src/cursor-doc/model.ts index a6db0d8d9..c2aedb140 100644 --- a/src/cursor-doc/model.ts +++ b/src/cursor-doc/model.ts @@ -50,6 +50,14 @@ export class ModelEdit { constructor(public editFn: T, public args: Readonly>) {} } +export function isModelRange(o: any): o is ModelEditRange { + return _.isArray(o) && o.length === 2 && isNumber(o[0]) && isNumber(o[1]); +} + +export function isModelEditSelection(o: any): o is ModelEditSelection { + return o instanceof ModelEditSelection; +} + /** * An undirected range representing a cursor/selection in a document. * Is a tuple of [start, end] where each is an offset. diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index 56a896fd8..e5c7d9822 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -12,6 +12,11 @@ import { LispTokenCursor } from './token-cursor'; import { backspaceOnWhitespace } from './backspace-on-whitespace'; import _ = require('lodash'); import { isEqual, last, property } from 'lodash'; +import { mapToItemAndOrder } from '../util/array'; +import { + mapRangeOrSelectionToOffset, + repositionSelectionByCumulativeOffsets, +} from './cursor-doc-utils'; // NB: doc.model.edit returns a Thenable, so that the vscode Editor can compose commands. // But don't put such chains in this module because that won't work in the repl-console. @@ -649,53 +654,84 @@ export function rangeToBackwardList( } } -export async function wrapSexpr( +export function wrapSexpr( doc: EditableDocument, open: string, close: string, - start: number = doc.selections[0].anchor, - end: number = doc.selections[0].active, + selections = [doc.selections[0]], options = { skipFormat: false } ) { - const cursor = doc.getTokenCursor(end); - if (cursor.withinString() && open == '"') { - open = close = '\\"'; - } - if (start == end) { - // No selection - const currentFormRange = cursor.rangeForCurrentForm(start); - if (currentFormRange) { - const range = currentFormRange; - return doc.model.edit( - [ - new ModelEdit('insertString', [range[1], close]), - new ModelEdit('insertString', [ - range[0], - open, - [end, end], - [start + open.length, start + open.length], - ]), - ], - { - selections: [new ModelEditSelection(start + open.length)], - skipFormat: options.skipFormat, + // TODO: support wrapping with Sets (#{}) + const edits: ModelEdit<'insertString'>[] = [], + // selections = clone(selections).map(mapToItemAndOrder); + newSelections = _.clone(selections); + + _(selections) + // iterate backwards to simplify dealing with cumulative cursor position offsets as document's text is added to with + // parens or whatever is being wrapped with. + .map(mapToItemAndOrder) + .orderBy(([r]) => mapRangeOrSelectionToOffset('start')(r), 'desc') + .forEach(([sel, index]) => { + // const { start, end } = sel; + const { anchor: start, active: end } = sel; + const cursor = doc.getTokenCursor(end); + if (cursor.withinString() && open == '"') { + open = close = '\\"'; + } + if (start == end) { + // No selection + const currentFormRange = cursor.rangeForCurrentForm(start); + if (currentFormRange) { + const range = currentFormRange; + const closeEdit = new ModelEdit('insertString', [range[1], close]); + const openEdit = new ModelEdit('insertString', [range[0], open]); + + const existing = _.intersectionWith(edits, [closeEdit, openEdit], _.isEqual); + const isNewEdit = _.isEmpty(existing); + + console.log(edits, range, closeEdit, openEdit, existing, isNewEdit); + + edits.push(closeEdit, openEdit); + // don't forget to include the index with the selection for later reordering to original order; + // selections[index] = [new ModelEditSelection(start + open.length), index]; + newSelections[index] = new ModelEditSelection(start + (isNewEdit ? open.length : 0)); } - ); - } - } else { - // there is a selection - const range = [Math.min(start, end), Math.max(start, end)]; - return doc.model.edit( - [ - new ModelEdit('insertString', [range[1], close]), - new ModelEdit('insertString', [range[0], open]), - ], - { - selections: [new ModelEditSelection(start + open.length)], - skipFormat: options.skipFormat, + } else { + // there is a selection + const range = [Math.min(start, end), Math.max(start, end)]; + // edits.push( + // new ModelEdit('insertString', [range[1], close]), + // new ModelEdit('insertString', [range[0], open]) + // ); + // don't forget to include the index with the selection for later reordering to original order; + // selections[index] = [new ModelEditSelection(start + open.length), index]; + // newSelections[index] = new ModelEditSelection(start + open.length); + + const closeEdit = new ModelEdit('insertString', [range[1], close]); + const openEdit = new ModelEdit('insertString', [range[0], open]); + + const existing = _.intersectionWith(edits, [closeEdit, openEdit], _.isEqual); + const isNewEdit = _.isEmpty(existing); + + console.log(edits, range, closeEdit, openEdit, existing, isNewEdit); + + edits.push(closeEdit, openEdit); + // don't forget to include the index with the selection for later reordering to original order; + // selections[index] = [new ModelEditSelection(start + open.length), index]; + newSelections[index] = new ModelEditSelection(start + (isNewEdit ? open.length : 0)); } - ); - } + return undefined; + }); + const uniqEdits = _.uniqBy(edits, (e) => e.args[0]); + // return doc.model.edit(_.uniqWith(edits, _.isEqual), { + return doc.model.edit(uniqEdits, { + // return doc.model.edit(edits, { + // selections: newSelections.map(repositionSelectionByCumulativeOffsets(2)), + selections: newSelections.map( + repositionSelectionByCumulativeOffsets(open.length + close.length) + ), + skipFormat: options.skipFormat, + }); } /** diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index ccfbfeb04..a298828d4 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -1623,6 +1623,155 @@ describe('paredit', () => { }); }); + describe('Wrap', () => { + it('Simply wraps []', async () => { + const a = docFromTextNotation('a (b c|) d'); + const b = docFromTextNotation('a (b [c|]) d'); + await paredit.wrapSexpr(a, '[', ']'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Simply wraps ()', async () => { + const a = docFromTextNotation('a [b c|] d'); + const b = docFromTextNotation('a [b (c|)] d'); + await paredit.wrapSexpr(a, '(', ')'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Simply wraps {}', async () => { + const a = docFromTextNotation('a [b c|] d'); + const b = docFromTextNotation('a [b {c|}] d'); + await paredit.wrapSexpr(a, '{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Simply wraps {}', async () => { + const a = docFromTextNotation('a #{b c|} d'); + const b = docFromTextNotation('a #{b {c|}} d'); + await paredit.wrapSexpr(a, '{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Simply wraps ""', async () => { + const a = docFromTextNotation('a #{b c|} d'); + const b = docFromTextNotation('a #{b "c|"} d'); + await paredit.wrapSexpr(a, '"', '"'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Simply wraps #{}', async () => { + const a = docFromTextNotation('[b c|] d'); + const b = docFromTextNotation('[b #{c|}] d'); + await paredit.wrapSexpr(a, '#{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + + it('Wraps from close {}', async () => { + const a = docFromTextNotation('a [b c]| d'); + const b = docFromTextNotation('a {[b c]|} d'); + await paredit.wrapSexpr(a, '{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps from close ""', async () => { + const a = docFromTextNotation('a #{b c}| d'); + const b = docFromTextNotation('a "#{b c}|" d'); + await paredit.wrapSexpr(a, '"', '"'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps from close #{}', async () => { + const a = docFromTextNotation('a [b c]| d'); + const b = docFromTextNotation('a #{[b c]|} d'); + await paredit.wrapSexpr(a, '#{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + + it('Wraps from close from a distance w/ cursor outside {}', async () => { + const a = docFromTextNotation('a [b c] | d'); + const b = docFromTextNotation('a {[b c]}| d'); + await paredit.wrapSexpr(a, '{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps from close from a distance w/ cursor outside ""', async () => { + const a = docFromTextNotation('a #{b c} | d'); + const b = docFromTextNotation('a "#{b c}"| d'); + await paredit.wrapSexpr(a, '"', '"'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps from close from a distance w/ cursor outside #{}', async () => { + const a = docFromTextNotation('a [b c] | d'); + const b = docFromTextNotation('a #{[b c]}| d'); + await paredit.wrapSexpr(a, '#{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + + it('Wraps from opening {}', async () => { + const a = docFromTextNotation('a |[b c] d'); + const b = docFromTextNotation('a {|[b c]} d'); + await paredit.wrapSexpr(a, '{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps from opening ""', async () => { + const a = docFromTextNotation('a |#{b c} d'); + const b = docFromTextNotation('a "|#{b c}" d'); + await paredit.wrapSexpr(a, '"', '"'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps from opening #{}', async () => { + const a = docFromTextNotation('a |[b c] d'); + const b = docFromTextNotation('a #{|[b c]} d'); + await paredit.wrapSexpr(a, '#{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + + it('Wraps between directly adjacent lists preferring prior list {}', async () => { + const a = docFromTextNotation('a [b c]|[e] d'); + const b = docFromTextNotation('a {[b c]|}[e] d'); + await paredit.wrapSexpr(a, '{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps between directly adjacent lists preferring prior list ""', async () => { + const a = docFromTextNotation('a #{b c}|#{e} d'); + const b = docFromTextNotation('a "#{b c}|"#{e} d'); + await paredit.wrapSexpr(a, '"', '"'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps between directly adjacent lists preferring prior list #{}', async () => { + const a = docFromTextNotation('a [b c]|[e] d'); + const b = docFromTextNotation('a #{[b c]|}[e] d'); + await paredit.wrapSexpr(a, '#{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + + it('Wraps from selection leaving cursor at anchor inside list {}', async () => { + const a = docFromTextNotation('a [|b c|] d'); + const b = docFromTextNotation('a [{|b c}] d'); + await paredit.wrapSexpr(a, '{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Wraps from selection leaving cursor at anchor inside list ""', async () => { + const a = docFromTextNotation('a #{ { + const a = docFromTextNotation('a [ { + const a = docFromTextNotation('^{b c|} d'); + const b = docFromTextNotation('^{b #{c|}} d'); + await paredit.wrapSexpr(a, '#{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + // TODO: This tests current behavior. What should happen? + it('Simply wraps #{}', async () => { + const a = docFromTextNotation('~{b c|} d'); + const b = docFromTextNotation('~{b #{c|}} d'); + await paredit.wrapSexpr(a, '#{', '}'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + }); + describe('Rewrap', () => { it('Rewraps () -> []', async () => { const a = docFromTextNotation('a (b c|) d'); diff --git a/src/extension-test/unit/paredit/commands-test.ts b/src/extension-test/unit/paredit/commands-test.ts index 3133d4d25..4f96e194a 100644 --- a/src/extension-test/unit/paredit/commands-test.ts +++ b/src/extension-test/unit/paredit/commands-test.ts @@ -1181,6 +1181,309 @@ describe('paredit commands', () => { describe('editing', () => { describe('wrapping', () => { + describe('wrap', () => { + it('Single-cursor: Simply wraps []', async () => { + const a = docFromTextNotation('a (b c|) |1d'); + const b = docFromTextNotation('a (b [c|]) d'); + await handlers.wrapAroundSquare(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps []', async () => { + const a = docFromTextNotation('a (b c|) d|1 []|2'); + const b = docFromTextNotation('a (b [c|]) [d|1] [[]|2]'); + await handlers.wrapAroundSquare(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps [] 2 - from open/left', async () => { + const a = docFromTextNotation('a (b |c) |1d |2[]'); + const b = docFromTextNotation('a (b [|c]) [|1d] [|2[]]'); + await handlers.wrapAroundSquare(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps [] 3 - mixed', async () => { + const a = docFromTextNotation('a (b c|) |1d []|2'); + const b = docFromTextNotation('a (b [c|]) [|1d] [[]|2]'); + await handlers.wrapAroundSquare(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Handles wrapping multiple cursors around the same form []', async () => { + const a = docFromTextNotation('a (b |1c|) d'); + const b = docFromTextNotation('a (b [c|]) d'); + await handlers.wrapAroundSquare(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Handles wrapping multiple cursors around the same form []', async () => { + const a = docFromTextNotation('a (b |1c|) d'); + const b = docFromTextNotation('a (b [|1c|]) d'); + await handlers.wrapAroundSquare(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Handles wrapping multiple cursors targeting the same form []', async () => { + const a = docFromTextNotation('a (b c| |1) d'); + const b = docFromTextNotation('a (b [c|] ) d'); + await handlers.wrapAroundSquare(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Handles wrapping multiple cursors targeting the same form []', async () => { + const a = docFromTextNotation('a (b c| |1) d'); + const b = docFromTextNotation('a (b [c|] |1) d'); + await handlers.wrapAroundSquare(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Handles wrapping multiple cursors targeting the same form #{} 2', async () => { + const a = docFromTextNotation('a (b |c| |1) d'); + const b = docFromTextNotation('a (b [|c] |1) d'); + await handlers.wrapAroundSquare(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Single-cursor: Simply wraps ()', async () => { + const a = docFromTextNotation('a [b c|] |1d'); + const b = docFromTextNotation('a [b (c|)] d'); + await handlers.wrapAroundParens(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps ()', async () => { + const a = docFromTextNotation('a [b c|] |1d'); + const b = docFromTextNotation('a [b (c|)] (|1d)'); + await handlers.wrapAroundParens(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Simply wraps {}', async () => { + const a = docFromTextNotation('a [b c|] |1d'); + const b = docFromTextNotation('a [b {c|}] d'); + await handlers.wrapAroundCurly(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps {}', async () => { + const a = docFromTextNotation('a [b c|] |1d'); + const b = docFromTextNotation('a [b {c|}] {|1d}'); + await handlers.wrapAroundCurly(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Simply wraps {}', async () => { + const a = docFromTextNotation('a #{b c|} |1d'); + const b = docFromTextNotation('a #{b {c|}} d'); + await handlers.wrapAroundCurly(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps {}', async () => { + const a = docFromTextNotation('a #{b c|} |1d'); + const b = docFromTextNotation('a #{b {c|}} {|1d}'); + await handlers.wrapAroundCurly(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Simply wraps ""', async () => { + const a = docFromTextNotation('a #{b c|} |1d'); + const b = docFromTextNotation('a #{b "c|"} d'); + await handlers.wrapAroundQuote(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps ""', async () => { + const a = docFromTextNotation('a #{b c|} |1d'); + const b = docFromTextNotation('a #{b "c|"} "|1d"'); + await handlers.wrapAroundQuote(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Simply wraps #{}', async () => { + const a = docFromTextNotation('[b c|] |1d'); + const b = docFromTextNotation('[b #{c|}] d'); + await handlers.wrapAroundSet(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps #{}', async () => { + const a = docFromTextNotation('[b c|] |1d'); + const b = docFromTextNotation('[b #{c|}] #{|1d}'); + await handlers.wrapAroundSet(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Single-cursor: Wraps from close {}', async () => { + const a = docFromTextNotation('a [b c]| |1d'); + const b = docFromTextNotation('a {[b c]|} d'); + await handlers.wrapAroundCurly(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from close {}', async () => { + const a = docFromTextNotation('a [b c]| |1d'); + const b = docFromTextNotation('a {[b c]|} {|1d}'); + await handlers.wrapAroundCurly(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps from close ""', async () => { + const a = docFromTextNotation('a #{b c}| |1d'); + const b = docFromTextNotation('a "#{b c}|" d'); + await handlers.wrapAroundQuote(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from close ""', async () => { + const a = docFromTextNotation('a #{b c}| |1d'); + const b = docFromTextNotation('a "#{b c}|" "|1d"'); + await handlers.wrapAroundQuote(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps from close #{}', async () => { + const a = docFromTextNotation('a [b c]| |1d'); + const b = docFromTextNotation('a #{[b c]|} d'); + await handlers.wrapAroundSet(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from close #{}', async () => { + const a = docFromTextNotation('a [b c]| |1d'); + const b = docFromTextNotation('a #{[b c]|} #{|1d}'); + await handlers.wrapAroundSet(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Single-cursor: Wraps from close from a distance w/ cursor outside {}', async () => { + const a = docFromTextNotation('a [b c] | |1d'); + const b = docFromTextNotation('a {[b c]}| d'); + await handlers.wrapAroundCurly(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from close from a distance w/ cursor outside {}', async () => { + const a = docFromTextNotation('a [b c] | |1d'); + const b = docFromTextNotation('a {[b c]}| {|1d}'); + await handlers.wrapAroundCurly(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps from close from a distance w/ cursor outside ""', async () => { + const a = docFromTextNotation('a #{b c} | |1d'); + const b = docFromTextNotation('a "#{b c}"| d'); + await handlers.wrapAroundQuote(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from close from a distance w/ cursor outside ""', async () => { + const a = docFromTextNotation('a #{b c} | |1d'); + const b = docFromTextNotation('a "#{b c}"| "|1d"'); + await handlers.wrapAroundQuote(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps from close from a distance w/ cursor outside #{}', async () => { + const a = docFromTextNotation('a [b c] | |1d'); + const b = docFromTextNotation('a #{[b c]}| d'); + await handlers.wrapAroundSet(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from close from a distance w/ cursor outside #{}', async () => { + const a = docFromTextNotation('a [b c] | |1d'); + const b = docFromTextNotation('a #{[b c]}| #{|1d}'); + await handlers.wrapAroundSet(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Single-cursor: Wraps from opening {}', async () => { + const a = docFromTextNotation('a |[b c] |1d'); + const b = docFromTextNotation('a {|[b c]} d'); + await handlers.wrapAroundCurly(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from opening {}', async () => { + const a = docFromTextNotation('a |[b c] |1d'); + const b = docFromTextNotation('a {|[b c]} {|1d}'); + await handlers.wrapAroundCurly(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps from opening ""', async () => { + const a = docFromTextNotation('a |#{b c} |1d'); + const b = docFromTextNotation('a "|#{b c}" d'); + await handlers.wrapAroundQuote(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from opening ""', async () => { + const a = docFromTextNotation('a |#{b c} |1d'); + const b = docFromTextNotation('a "|#{b c}" "|1d"'); + await handlers.wrapAroundQuote(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps from opening #{}', async () => { + const a = docFromTextNotation('a |[b c] |1d'); + const b = docFromTextNotation('a #{|[b c]} d'); + await handlers.wrapAroundSet(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from opening #{}', async () => { + const a = docFromTextNotation('a |[b c] |1d'); + const b = docFromTextNotation('a #{|[b c]} #{|1d}'); + await handlers.wrapAroundSet(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Single-cursor: Wraps between directly adjacent lists preferring prior list {}', async () => { + const a = docFromTextNotation('a [b c]|[e] |1d'); + const b = docFromTextNotation('a {[b c]|}[e] d'); + await handlers.wrapAroundCurly(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps between directly adjacent lists preferring prior list {}', async () => { + const a = docFromTextNotation('a [b c]|[e] |1d'); + const b = docFromTextNotation('a {[b c]|}[e] {|1d}'); + await handlers.wrapAroundCurly(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps between directly adjacent lists preferring prior list ""', async () => { + const a = docFromTextNotation('a #{b c}|#{e} |1d'); + const b = docFromTextNotation('a "#{b c}|"#{e} d'); + await handlers.wrapAroundQuote(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps between directly adjacent lists preferring prior list ""', async () => { + const a = docFromTextNotation('a #{b c}|#{e} |1d'); + const b = docFromTextNotation('a "#{b c}|"#{e} "|1d"'); + await handlers.wrapAroundQuote(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps between directly adjacent lists preferring prior list #{}', async () => { + const a = docFromTextNotation('a [b c]|[e] |1d'); + const b = docFromTextNotation('a #{[b c]|}[e] d'); + await handlers.wrapAroundSet(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps between directly adjacent lists preferring prior list #{}', async () => { + const a = docFromTextNotation('a [b c]|[e] |1d'); + const b = docFromTextNotation('a #{[b c]|}[e] #{|1d}'); + await handlers.wrapAroundSet(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Single-cursor: Wraps from selection leaving cursor at anchor inside list {}', async () => { + const a = docFromTextNotation('a [|b c|] |1d'); + const b = docFromTextNotation('a [{|b c}] d'); + await handlers.wrapAroundCurly(a, false); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Wraps from selection leaving cursor at anchor inside list {}', async () => { + const a = docFromTextNotation('a [|b c|] <1d<1'); + const b = docFromTextNotation('a [{|b c}] {d|1}'); + await handlers.wrapAroundCurly(a, true); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Single-cursor: Wraps from selection leaving cursor at anchor inside list ""', async () => { + const a = docFromTextNotation('a #{ { + const a = docFromTextNotation('a #{ { + const a = docFromTextNotation('a [ { + const a = docFromTextNotation('a [ { it('Single-cursor: Rewraps () -> []', async () => { const a = docFromTextNotation('a (b c|) d'); diff --git a/src/paredit/commands.ts b/src/paredit/commands.ts index 6ccf7208c..bbb75ce69 100644 --- a/src/paredit/commands.ts +++ b/src/paredit/commands.ts @@ -126,6 +126,28 @@ export async function killLeft( ); } +// WRAP + +export function wrapAroundQuote(doc: EditableDocument, isMulti: boolean) { + return paredit.wrapSexpr(doc, '"', '"', isMulti ? doc.selections : [doc.selections[0]]); +} + +export function wrapAroundCurly(doc: EditableDocument, isMulti: boolean) { + return paredit.wrapSexpr(doc, '{', '}', isMulti ? doc.selections : [doc.selections[0]]); +} + +export function wrapAroundSet(doc: EditableDocument, isMulti: boolean) { + return paredit.wrapSexpr(doc, '#{', '}', isMulti ? doc.selections : [doc.selections[0]]); +} + +export function wrapAroundSquare(doc: EditableDocument, isMulti: boolean) { + return paredit.wrapSexpr(doc, '[', ']', isMulti ? doc.selections : [doc.selections[0]]); +} + +export function wrapAroundParens(doc: EditableDocument, isMulti: boolean) { + return paredit.wrapSexpr(doc, '(', ')', isMulti ? doc.selections : [doc.selections[0]]); +} + // REWRAP export function rewrapQuote(doc: EditableDocument, isMulti: boolean) { diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index bcf591a0e..65119b4f2 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -389,25 +389,36 @@ const pareditCommands = [ { command: 'paredit.wrapAroundParens', handler: (doc: EditableDocument) => { - return paredit.wrapSexpr(doc, '(', ')'); + const isMulti = multiCursorEnabled(); + return handlers.wrapAroundParens(doc, isMulti); }, }, { command: 'paredit.wrapAroundSquare', handler: (doc: EditableDocument) => { - return paredit.wrapSexpr(doc, '[', ']'); + const isMulti = multiCursorEnabled(); + return handlers.wrapAroundSquare(doc, isMulti); }, }, { command: 'paredit.wrapAroundCurly', handler: (doc: EditableDocument) => { - return paredit.wrapSexpr(doc, '{', '}'); + const isMulti = multiCursorEnabled(); + return handlers.wrapAroundCurly(doc, isMulti); + }, + }, + { + command: 'paredit.wrapAroundSet', + handler: (doc: EditableDocument) => { + const isMulti = multiCursorEnabled(); + return handlers.wrapAroundSet(doc, isMulti); }, }, { command: 'paredit.wrapAroundQuote', handler: (doc: EditableDocument) => { - return paredit.wrapSexpr(doc, '"', '"'); + const isMulti = multiCursorEnabled(); + return handlers.wrapAroundQuote(doc, isMulti); }, }, { diff --git a/src/util/array.ts b/src/util/array.ts new file mode 100644 index 000000000..4e064e494 --- /dev/null +++ b/src/util/array.ts @@ -0,0 +1,13 @@ +/** + * Perhaps silly utility. + * Designed to help out with when you must sort an array and then resort it back to its original order. + * + * First you `.map(mapToItemAndOrder)` the array, then you sort or order it however you want, after which you can simply resort it by + * + * , specifically where `map(mapToItemAndOrder) is followed up with with a sort operation on the result, using the `idx` (2nd return array item) + */ +export const mapToItemAndOrder = (o: A, idx: number) => [o, idx] as [A, number]; + +export const mapToOriginalOrder = ([_o, idx]: [A, number]) => idx; + +export const mapToOriginalItem = ([o]: [A, number]) => o; diff --git a/src/util/lodashMixins.ts b/src/util/lodashMixins.ts new file mode 100644 index 000000000..0c3a229e9 --- /dev/null +++ b/src/util/lodashMixins.ts @@ -0,0 +1,91 @@ +import * as _ from 'lodash'; + +export const replaceAt = (array: A[], index: number, replacement: A): A[] => { + return array + .slice(0, index) + .concat([replacement]) + .concat(array.slice(index + 1)); +}; + +export function doto(x: T, ...fns: ((x: T) => any)[]): T { + for (const fn of fns) { + fn?.(x); + } + return x; +} + +export function isBlank(s: string): boolean { + return s.trim().length === 0; +} + +// like _.property combined with clojure's select-keys; +// returns a new object with only the specified keys (or nested lodash property paths) +export function properties(...keys: string[]) { + return (obj: any) => _.pick(obj, keys); +} + +// like clojure's comp fn +// export function comp(...fns: ((...args: any[]) => any)[]) { return _.flowRight(fns); } +export const comp = _.flowRight; + +declare module 'lodash' { + interface LoDashStatic { + doto: (x: T, ...fns: ((x: T) => any)[]) => T; + isBlank: (s: string) => boolean; + replaceAt: typeof replaceAt; + properties: typeof properties; + comp: typeof comp; + } + // interface LoDashImplicitWrapper { + // doto(...fns: ((x: TValue) => any)[]): LoDashImplicitWrapper; + // isBlank(): boolean; + // replaceAt(index: number, replacement: TValue): TValue[]; + // } + interface LoDashImplicitWrapper { + /** + * @see _.doto + */ + // doto( + doto( + this: LoDashImplicitWrapper, + ...fns: ((x: TValue) => any)[] + ): LoDashImplicitWrapper; + doto( + this: LoDashImplicitWrapper | null | undefined>, + ...fns: ((x: T) => any)[] + ): LoDashImplicitWrapper>; + doto( + this: LoDashImplicitWrapper | null | undefined>, + ...fns: ((x: T) => any)[] + ): LoDashImplicitWrapper>; + doto( + this: LoDashImplicitWrapper, + ...fns: ((x: T) => any)[] + ): LoDashImplicitWrapper; + isBlank(this: LoDashImplicitWrapper): boolean; + replaceAt( + this: LoDashImplicitWrapper, + index: number, + replacement: TValue + ): TValue[]; + } + interface LoDashExplicitWrapper { + /** + * @see _.doto + */ + // doto( + doto( + this: LoDashExplicitWrapper, + ...fns: ((x: T) => any)[] + ): LoDashExplicitWrapper; + isBlank(this: LoDashImplicitWrapper): boolean; + replaceAt( + this: LoDashImplicitWrapper, + index: number, + replacement: T + ): T[]; + } +} + +_.mixin({ doto, isBlank, replaceAt }, { chain: true }); +_.mixin({ properties, comp }, { chain: false }); From 659088826fa3f98b302d5ce7c5b505e923ea6112 Mon Sep 17 00:00:00 2001 From: Rayat M Rahman <22646419+riotrah@users.noreply.github.com> Date: Sun, 31 Mar 2024 23:48:41 -0700 Subject: [PATCH 17/19] add more wrap tests --- .../unit/paredit/commands-test.ts | 293 +++++++++++++++++- 1 file changed, 291 insertions(+), 2 deletions(-) diff --git a/src/extension-test/unit/paredit/commands-test.ts b/src/extension-test/unit/paredit/commands-test.ts index 4f96e194a..f04810707 100644 --- a/src/extension-test/unit/paredit/commands-test.ts +++ b/src/extension-test/unit/paredit/commands-test.ts @@ -1,7 +1,11 @@ import * as expect from 'expect'; import * as model from '../../../cursor-doc/model'; import * as handlers from '../../../paredit/commands'; -import { docFromTextNotation, textNotationFromDoc } from '../common/text-notation'; +import { + docFromTextNotation, + textAndSelections, + textNotationFromDoc, +} from '../common/text-notation'; import _ = require('lodash'); model.initScanner(20000); @@ -1186,54 +1190,90 @@ describe('paredit commands', () => { const a = docFromTextNotation('a (b c|) |1d'); const b = docFromTextNotation('a (b [c|]) d'); await handlers.wrapAroundSquare(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Simply wraps []', async () => { const a = docFromTextNotation('a (b c|) d|1 []|2'); const b = docFromTextNotation('a (b [c|]) [d|1] [[]|2]'); await handlers.wrapAroundSquare(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Simply wraps [] 2 - from open/left', async () => { const a = docFromTextNotation('a (b |c) |1d |2[]'); const b = docFromTextNotation('a (b [|c]) [|1d] [|2[]]'); await handlers.wrapAroundSquare(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Simply wraps [] 3 - mixed', async () => { const a = docFromTextNotation('a (b c|) |1d []|2'); const b = docFromTextNotation('a (b [c|]) [|1d] [[]|2]'); await handlers.wrapAroundSquare(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Single-cursor: Handles wrapping multiple cursors around the same form []', async () => { const a = docFromTextNotation('a (b |1c|) d'); const b = docFromTextNotation('a (b [c|]) d'); await handlers.wrapAroundSquare(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Handles wrapping multiple cursors around the same form []', async () => { const a = docFromTextNotation('a (b |1c|) d'); const b = docFromTextNotation('a (b [|1c|]) d'); await handlers.wrapAroundSquare(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Single-cursor: Handles wrapping multiple cursors targeting the same form []', async () => { const a = docFromTextNotation('a (b c| |1) d'); const b = docFromTextNotation('a (b [c|] ) d'); await handlers.wrapAroundSquare(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Handles wrapping multiple cursors targeting the same form []', async () => { const a = docFromTextNotation('a (b c| |1) d'); const b = docFromTextNotation('a (b [c|] |1) d'); await handlers.wrapAroundSquare(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); - it('Multi-cursor: Handles wrapping multiple cursors targeting the same form #{} 2', async () => { + it('Multi-cursor: Handles wrapping multiple cursors targeting the same form #{}', async () => { const a = docFromTextNotation('a (b |c| |1) d'); const b = docFromTextNotation('a (b [|c] |1) d'); await handlers.wrapAroundSquare(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); @@ -1241,60 +1281,189 @@ describe('paredit commands', () => { const a = docFromTextNotation('a [b c|] |1d'); const b = docFromTextNotation('a [b (c|)] d'); await handlers.wrapAroundParens(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Simply wraps ()', async () => { const a = docFromTextNotation('a [b c|] |1d'); const b = docFromTextNotation('a [b (c|)] (|1d)'); await handlers.wrapAroundParens(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Single-cursor: Simply wraps {}', async () => { const a = docFromTextNotation('a [b c|] |1d'); const b = docFromTextNotation('a [b {c|}] d'); await handlers.wrapAroundCurly(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Simply wraps {}', async () => { const a = docFromTextNotation('a [b c|] |1d'); const b = docFromTextNotation('a [b {c|}] {|1d}'); await handlers.wrapAroundCurly(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Single-cursor: Simply wraps {}', async () => { const a = docFromTextNotation('a #{b c|} |1d'); const b = docFromTextNotation('a #{b {c|}} d'); await handlers.wrapAroundCurly(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Simply wraps {}', async () => { const a = docFromTextNotation('a #{b c|} |1d'); const b = docFromTextNotation('a #{b {c|}} {|1d}'); await handlers.wrapAroundCurly(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Single-cursor: Simply wraps ""', async () => { const a = docFromTextNotation('a #{b c|} |1d'); const b = docFromTextNotation('a #{b "c|"} d'); await handlers.wrapAroundQuote(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Simply wraps ""', async () => { const a = docFromTextNotation('a #{b c|} |1d'); const b = docFromTextNotation('a #{b "c|"} "|1d"'); await handlers.wrapAroundQuote(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Single-cursor: Simply wraps #{}', async () => { const a = docFromTextNotation('[b c|] |1d'); const b = docFromTextNotation('[b #{c|}] d'); await handlers.wrapAroundSet(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Simply wraps #{}', async () => { const a = docFromTextNotation('[b c|] |1d'); const b = docFromTextNotation('[b #{c|}] #{|1d}'); await handlers.wrapAroundSet(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps {} over multiple lines', async () => { + const a = docFromTextNotation( + '(defn foo• |[a b]• (|2+ a b))••(def bar 1)••|1(foo bar••2)' + ); + const b = docFromTextNotation( + '(defn foo• {|[a b]}• ({|2+} a b))••(def bar 1)••{|1(foo bar••2)}' + ); + await handlers.wrapAroundCurly(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Simply wraps #{} over multiple lines', async () => { + const a = docFromTextNotation( + '(defn foo• |[a b]• (|2+ a b))••(def bar 1)••|1(foo bar••2)' + ); + const b = docFromTextNotation( + '(defn foo• #{|[a b]}• (#{|2+} a b))••(def bar 1)••#{|1(foo bar••2)}' + ); + await handlers.wrapAroundSet(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + + it('Multi-cursor: Handles wrapping nested forms {}', async () => { + const a = docFromTextNotation('(defn foo• [a b]• (+ a b|1)|)'); + const b = docFromTextNotation('(defn foo• [a b]• {(+ a {b|1})|})'); + await handlers.wrapAroundCurly(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Handles wrapping nested forms {} 2', async () => { + const a = docFromTextNotation('(defn foo• |2[a b]• (+ a b|1)|)'); + const b = docFromTextNotation('{|3(defn foo• {|2[a b]}• {(+ a {b|1})|})}'); + await handlers.wrapAroundCurly(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Handles wrapping nested forms {} 3', async () => { + const a = docFromTextNotation('|3(defn foo• |2[a b]• (+ a b|1)|)'); + const b = docFromTextNotation('{|3(defn foo• {|2[a b]}• {(+ a {b|1})|})}'); + await handlers.wrapAroundCurly(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Handles wrapping nested forms #{}', async () => { + const a = docFromTextNotation('(defn foo• [a b]• (+ a b|1)|)'); + const b = docFromTextNotation('(defn foo• [a b]• #{(+ a #{b|1})|})'); + await handlers.wrapAroundSet(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Handles wrapping nested forms #{} 2', async () => { + const a = docFromTextNotation('(defn foo• |2[a b]• (+ a b|1)|)'); + const b = docFromTextNotation('#{|3(defn foo• #{|2[a b]}• #{(+ a #{b|1})|})}'); + await handlers.wrapAroundSet(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); + expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); + }); + it('Multi-cursor: Handles wrapping nested forms #{} 3', async () => { + const a = docFromTextNotation('|3(defn foo• |2[a b]• (+ a b|1)|)'); + const b = docFromTextNotation('#{|3(defn foo• #{|2[a b]}• #{(+ a #{b|1})|})}'); + await handlers.wrapAroundSet(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); @@ -1302,36 +1471,60 @@ describe('paredit commands', () => { const a = docFromTextNotation('a [b c]| |1d'); const b = docFromTextNotation('a {[b c]|} d'); await handlers.wrapAroundCurly(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Wraps from close {}', async () => { const a = docFromTextNotation('a [b c]| |1d'); const b = docFromTextNotation('a {[b c]|} {|1d}'); await handlers.wrapAroundCurly(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Single-cursor: Wraps from close ""', async () => { const a = docFromTextNotation('a #{b c}| |1d'); const b = docFromTextNotation('a "#{b c}|" d'); await handlers.wrapAroundQuote(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Wraps from close ""', async () => { const a = docFromTextNotation('a #{b c}| |1d'); const b = docFromTextNotation('a "#{b c}|" "|1d"'); await handlers.wrapAroundQuote(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Single-cursor: Wraps from close #{}', async () => { const a = docFromTextNotation('a [b c]| |1d'); const b = docFromTextNotation('a #{[b c]|} d'); await handlers.wrapAroundSet(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Wraps from close #{}', async () => { const a = docFromTextNotation('a [b c]| |1d'); const b = docFromTextNotation('a #{[b c]|} #{|1d}'); await handlers.wrapAroundSet(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); @@ -1339,36 +1532,60 @@ describe('paredit commands', () => { const a = docFromTextNotation('a [b c] | |1d'); const b = docFromTextNotation('a {[b c]}| d'); await handlers.wrapAroundCurly(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Wraps from close from a distance w/ cursor outside {}', async () => { const a = docFromTextNotation('a [b c] | |1d'); const b = docFromTextNotation('a {[b c]}| {|1d}'); await handlers.wrapAroundCurly(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Single-cursor: Wraps from close from a distance w/ cursor outside ""', async () => { const a = docFromTextNotation('a #{b c} | |1d'); const b = docFromTextNotation('a "#{b c}"| d'); await handlers.wrapAroundQuote(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Wraps from close from a distance w/ cursor outside ""', async () => { const a = docFromTextNotation('a #{b c} | |1d'); const b = docFromTextNotation('a "#{b c}"| "|1d"'); await handlers.wrapAroundQuote(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Single-cursor: Wraps from close from a distance w/ cursor outside #{}', async () => { const a = docFromTextNotation('a [b c] | |1d'); const b = docFromTextNotation('a #{[b c]}| d'); await handlers.wrapAroundSet(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Wraps from close from a distance w/ cursor outside #{}', async () => { const a = docFromTextNotation('a [b c] | |1d'); const b = docFromTextNotation('a #{[b c]}| #{|1d}'); await handlers.wrapAroundSet(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); @@ -1376,36 +1593,60 @@ describe('paredit commands', () => { const a = docFromTextNotation('a |[b c] |1d'); const b = docFromTextNotation('a {|[b c]} d'); await handlers.wrapAroundCurly(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Wraps from opening {}', async () => { const a = docFromTextNotation('a |[b c] |1d'); const b = docFromTextNotation('a {|[b c]} {|1d}'); await handlers.wrapAroundCurly(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Single-cursor: Wraps from opening ""', async () => { const a = docFromTextNotation('a |#{b c} |1d'); const b = docFromTextNotation('a "|#{b c}" d'); await handlers.wrapAroundQuote(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Wraps from opening ""', async () => { const a = docFromTextNotation('a |#{b c} |1d'); const b = docFromTextNotation('a "|#{b c}" "|1d"'); await handlers.wrapAroundQuote(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Single-cursor: Wraps from opening #{}', async () => { const a = docFromTextNotation('a |[b c] |1d'); const b = docFromTextNotation('a #{|[b c]} d'); await handlers.wrapAroundSet(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Wraps from opening #{}', async () => { const a = docFromTextNotation('a |[b c] |1d'); const b = docFromTextNotation('a #{|[b c]} #{|1d}'); await handlers.wrapAroundSet(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); @@ -1413,36 +1654,60 @@ describe('paredit commands', () => { const a = docFromTextNotation('a [b c]|[e] |1d'); const b = docFromTextNotation('a {[b c]|}[e] d'); await handlers.wrapAroundCurly(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Wraps between directly adjacent lists preferring prior list {}', async () => { const a = docFromTextNotation('a [b c]|[e] |1d'); const b = docFromTextNotation('a {[b c]|}[e] {|1d}'); await handlers.wrapAroundCurly(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Single-cursor: Wraps between directly adjacent lists preferring prior list ""', async () => { const a = docFromTextNotation('a #{b c}|#{e} |1d'); const b = docFromTextNotation('a "#{b c}|"#{e} d'); await handlers.wrapAroundQuote(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Wraps between directly adjacent lists preferring prior list ""', async () => { const a = docFromTextNotation('a #{b c}|#{e} |1d'); const b = docFromTextNotation('a "#{b c}|"#{e} "|1d"'); await handlers.wrapAroundQuote(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Single-cursor: Wraps between directly adjacent lists preferring prior list #{}', async () => { const a = docFromTextNotation('a [b c]|[e] |1d'); const b = docFromTextNotation('a #{[b c]|}[e] d'); await handlers.wrapAroundSet(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Wraps between directly adjacent lists preferring prior list #{}', async () => { const a = docFromTextNotation('a [b c]|[e] |1d'); const b = docFromTextNotation('a #{[b c]|}[e] #{|1d}'); await handlers.wrapAroundSet(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); @@ -1450,36 +1715,60 @@ describe('paredit commands', () => { const a = docFromTextNotation('a [|b c|] |1d'); const b = docFromTextNotation('a [{|b c}] d'); await handlers.wrapAroundCurly(a, false); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Multi-cursor: Wraps from selection leaving cursor at anchor inside list {}', async () => { const a = docFromTextNotation('a [|b c|] <1d<1'); const b = docFromTextNotation('a [{|b c}] {d|1}'); await handlers.wrapAroundCurly(a, true); + expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ + textAndSelections(b), + textNotationFromDoc(b), + ]); expect(_.omit(a, defaultDocOmit)).toEqual(_.omit(b, defaultDocOmit)); }); it('Single-cursor: Wraps from selection leaving cursor at anchor inside list ""', async () => { const a = docFromTextNotation('a #{ { const a = docFromTextNotation('a #{ { const a = docFromTextNotation('a [ { const a = docFromTextNotation('a [ Date: Fri, 12 Apr 2024 14:14:52 -0700 Subject: [PATCH 18/19] accept multicursor opt in/out command arg --- src/paredit/extension.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index 65119b4f2..94ec0a870 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -388,36 +388,36 @@ const pareditCommands = [ }, { command: 'paredit.wrapAroundParens', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); return handlers.wrapAroundParens(doc, isMulti); }, }, { command: 'paredit.wrapAroundSquare', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); return handlers.wrapAroundSquare(doc, isMulti); }, }, { command: 'paredit.wrapAroundCurly', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); return handlers.wrapAroundCurly(doc, isMulti); }, }, { command: 'paredit.wrapAroundSet', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); return handlers.wrapAroundSet(doc, isMulti); }, }, { command: 'paredit.wrapAroundQuote', - handler: (doc: EditableDocument) => { - const isMulti = multiCursorEnabled(); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); return handlers.wrapAroundQuote(doc, isMulti); }, }, From 10c1b5d1ebc5843e3dadeff186d3166cb2a1fa8d Mon Sep 17 00:00:00 2001 From: Rayat M Rahman <22646419+riotrah@users.noreply.github.com> Date: Sat, 13 Apr 2024 00:57:03 -0700 Subject: [PATCH 19/19] fix wrap test expected textNotation --- src/extension-test/unit/paredit/commands-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension-test/unit/paredit/commands-test.ts b/src/extension-test/unit/paredit/commands-test.ts index f04810707..81e8f8a9f 100644 --- a/src/extension-test/unit/paredit/commands-test.ts +++ b/src/extension-test/unit/paredit/commands-test.ts @@ -1258,7 +1258,7 @@ describe('paredit commands', () => { }); it('Multi-cursor: Handles wrapping multiple cursors targeting the same form []', async () => { const a = docFromTextNotation('a (b c| |1) d'); - const b = docFromTextNotation('a (b [c|] |1) d'); + const b = docFromTextNotation('a (b [c|]|1 ) d'); await handlers.wrapAroundSquare(a, true); expect([textAndSelections(a), textNotationFromDoc(a)]).toEqual([ textAndSelections(b),