Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP - DON'T MERGE] Multicursor wrap #2472

Open
wants to merge 22 commits into
base: published
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
129a62e
Implement multi-cursor rewrap. Addresses #2448.
riotrah Mar 22, 2024
96be366
Improve changelog entry
riotrah Mar 22, 2024
a95f9c6
start paredit multicursor wrap
riotrah Mar 27, 2024
0f8b404
Bring on version 2.0.440!
PEZ Apr 11, 2024
b22a74b
Merge branch published into dev [skip ci]
PEZ Apr 11, 2024
ba620ee
Support command args in paredit cmds
riotrah Apr 1, 2024
9ee1d3c
Fix types because cmds get only 1 arg
riotrah Apr 1, 2024
02e8dc8
Make multicursor paredit cmds accept override arg
riotrah Apr 1, 2024
142992e
Make kill cmds accept arg override for copy
riotrah Apr 1, 2024
20a2da6
Add some more static typing to paredit extension code
riotrah Apr 1, 2024
7a01a5d
Add changelog entry for command args
riotrah Apr 1, 2024
12f2d3f
Add paredit docsite section about command args
riotrah Apr 2, 2024
a03220b
Move command args kill copy section up
riotrah Apr 2, 2024
280b4b6
Add command arg note to multicursor section
riotrah Apr 2, 2024
e5a0e43
Edit and augment About Keyboard Shortcuts section
riotrah Apr 2, 2024
e9e8e89
Update docs/site/paredit.md
riotrah Apr 11, 2024
03e79e1
Merge pull request #2482 from BetterThanTomorrow/wip/rayat/paredit/mu…
riotrah Apr 12, 2024
0e5715d
start paredit multicursor wrap
riotrah Mar 27, 2024
6590888
add more wrap tests
riotrah Apr 1, 2024
d2a8bea
Merge branch 'wip/rayat/paredit/multicursor/wrap' of https://github.c…
riotrah Apr 12, 2024
84bdca3
accept multicursor opt in/out command arg
riotrah Apr 12, 2024
10c1b5d
fix wrap test expected textNotation
riotrah Apr 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "require": ["ts-node/register", "src/util/lodashMixins.ts"] }
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
69 changes: 68 additions & 1 deletion docs/site/paredit.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**.
Expand Down Expand Up @@ -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! ❤️

Expand All @@ -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}
}
```
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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}}",
Expand Down
117 changes: 117 additions & 0 deletions src/cursor-doc/cursor-doc-utils.ts
Original file line number Diff line number Diff line change
@@ -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<ModelEditSelection, number> | number
) {
// if (true) {
return repositionSelectionWithGetterByCumulativeOffsets<ModelEditSelection>(
_.identity,
offsetGetter
);
}

export function repositionSelectionWithGetterByCumulativeOffsets<T>(
selectionGetter: ListIterator<T, ModelEditSelection>,
/**
* 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<ModelEditSelection, number> | 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;
};
}
8 changes: 8 additions & 0 deletions src/cursor-doc/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ export class ModelEdit<T extends ModelEditFunction> {
constructor(public editFn: T, public args: Readonly<ModelEditArgs<T>>) {}
}

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.
Expand Down
Loading