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 c5c8eeafd..83f4f3a86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ 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 - Fix: [Refresh Changed Namespaces do not output to the selected output destination](https://github.com/BetterThanTomorrow/calva/issues/2506) diff --git a/docs/site/paredit.md b/docs/site/paredit.md index 877586e7b..cbc33b6dd 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 + +Some Paredit commands accept arguments. You can utilize this in keybindings and from [Joyride](https://github.com/BetterThanTomorrow/joyride). + +#### **`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**. @@ -159,7 +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! ❤️ @@ -170,3 +222,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} +} +``` 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..09354a3d6 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", @@ -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..81e8f8a9f 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); @@ -1181,6 +1185,594 @@ 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([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 #{}', 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('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([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)); + }); + + 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([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)); + }); + + 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([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)); + }); + + 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([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)); + }); + + 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([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)); + }); + + 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([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 [ { 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 823f52106..94ec0a870 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -38,185 +38,199 @@ 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() { - return workspace.getConfiguration().get('calva.paredit.multicursor'); +function multiCursorEnabled(override?: boolean): boolean { + return override ?? workspace.getConfiguration().get('calva.paredit.multicursor'); } type PareditCommand = { command: string; - handler: (doc: EditableDocument) => void | Promise; + 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', - 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); }, }, @@ -289,9 +303,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); @@ -299,20 +313,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); @@ -320,9 +334,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); @@ -330,9 +344,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); @@ -340,9 +354,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); @@ -350,9 +364,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) => { @@ -362,9 +376,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) => { @@ -374,60 +388,71 @@ const pareditCommands: PareditCommand[] = [ }, { command: 'paredit.wrapAroundParens', - handler: (doc: EditableDocument) => { - return paredit.wrapSexpr(doc, '(', ')'); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); + return handlers.wrapAroundParens(doc, isMulti); }, }, { command: 'paredit.wrapAroundSquare', - handler: (doc: EditableDocument) => { - return paredit.wrapSexpr(doc, '[', ']'); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); + return handlers.wrapAroundSquare(doc, isMulti); }, }, { command: 'paredit.wrapAroundCurly', - handler: (doc: EditableDocument) => { - return paredit.wrapSexpr(doc, '{', '}'); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); + return handlers.wrapAroundCurly(doc, isMulti); + }, + }, + { + command: 'paredit.wrapAroundSet', + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); + return handlers.wrapAroundSet(doc, isMulti); }, }, { command: 'paredit.wrapAroundQuote', - handler: (doc: EditableDocument) => { - return paredit.wrapSexpr(doc, '"', '"'); + handler: (doc: EditableDocument, opts?: { multicursor: boolean }) => { + const isMulti = multiCursorEnabled(opts?.multicursor); + return handlers.wrapAroundQuote(doc, isMulti); }, }, { 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); }, }, @@ -467,10 +492,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 () => { +function wrapPareditCommand(command: C) { + return async (arg: CommandArgOf) => { try { const textEditor = window.activeTextEditor; @@ -480,7 +507,7 @@ function wrapPareditCommand(command: PareditCommand) { if (!enabled || !languages.has(textEditor.document.languageId)) { return; } - return command.handler(mDoc); + return command.handler(mDoc, arg); } catch (e) { console.error(e.message); } 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 });