Skip to content

Commit 66464a1

Browse files
committed
Merge branch dev into published
2 parents 4cfd77e + 56376f5 commit 66464a1

File tree

5 files changed

+78
-62
lines changed

5 files changed

+78
-62
lines changed

CHANGELOG.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ Changes to Calva.
44

55
## [Unreleased]
66

7+
## [2.0.486] - 2025-02-16
8+
9+
- Fix: [Rewrapping to or from a Set introduces imbalance](https://github.com/BetterThanTomorrow/calva/issues/2726)
10+
711
## [2.0.485] - 2025-01-27
812

9-
- Fix: [Stop considering a clj-kondo config as a valid project to start LSP processes](https://github.com/
10-
BetterThanTomorrow/calva/issues/2712)
13+
- Fix: [Stop considering a clj-kondo config as a valid project to start LSP processes](https://github.com/BetterThanTomorrow/calva/issues/2712)
1114
- Bump deps.clj to v1.12.0.1495-2
1215

1316
## [2.0.484] - 2025-01-26

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "Calva: Clojure & ClojureScript Interactive Programming",
44
"description": "Integrated REPL, formatter, Paredit, and more. Powered by cider-nrepl and clojure-lsp.",
55
"icon": "assets/calva.png",
6-
"version": "2.0.485",
6+
"version": "2.0.486",
77
"publisher": "betterthantomorrow",
88
"author": {
99
"name": "Better Than Tomorrow",
@@ -848,7 +848,7 @@
848848
},
849849
"calva.enableJSCompletions": {
850850
"type": "boolean",
851-
"description": "Should Calva use suitible and bring you JavaScript completions? This is an experimental cider-nrepl feature. Disable if completions start to throw errors.",
851+
"description": "Should Calva use suitable and bring you JavaScript completions? This is an experimental cider-nrepl feature. Disable if completions start to throw errors.",
852852
"default": true
853853
},
854854
"calva.autoOpenREPLWindow": {
@@ -863,7 +863,7 @@
863863
},
864864
"calva.autoOpenResultOutputDestination": {
865865
"type": "boolean",
866-
"markdownDescription": "Automatically open the the Jack-in Terminal on Jack-in or Connect.",
866+
"markdownDescription": "Automatically open the the result output destination on Jack-in or Connect.",
867867
"default": true
868868
},
869869
"calva.autoOpenInspector": {

src/cursor-doc/model.ts

+57-2
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,57 @@ export interface EditableDocument {
287287
getSelectionText: () => string;
288288
}
289289

290+
// An editing transaction - array of ModelEdit - shifts the selection(s)
291+
// to compensate for insertions or deletions to their left.
292+
// Here we predict how edits will affect selections.
293+
const selectionsAfterEdits = (function () {
294+
const decodeChangeRange = function (edit): [any, any] {
295+
return [edit.args[0], edit.args[2].length - (edit.args[1] - edit.args[0])];
296+
};
297+
const decodeDeleteRange = function (edit): [any, any] {
298+
return [edit.args[0], 0 - edit.args[1]];
299+
};
300+
const decodeInsertString = function (edit): [any, any] {
301+
return [edit.args[0] + edit.args[1].length, edit.args[1].length];
302+
};
303+
const bump = function (n, [point, delta]) {
304+
return n != undefined ? (n > point ? n + delta : n) : undefined;
305+
};
306+
return function (edits, selections: ModelEditSelection[]) {
307+
// The ModelEdit array is in order by end-of-doc to start.
308+
// Traverse it, bumping selections
309+
// according to the growth or shrinkage of each edit.
310+
let monotonicallyDecreasing = -1; // check edit order
311+
let retSelections: ModelEditSelection[] = [...selections];
312+
for (let ic = 0; ic < edits.length; ic++) {
313+
const affected: [any, any] =
314+
edits[ic].editFn == 'deleteRange'
315+
? decodeDeleteRange(edits[ic])
316+
: edits[ic].editFn == 'changeRange'
317+
? decodeChangeRange(edits[ic])
318+
: decodeInsertString(edits[ic]);
319+
const [point, delta] = affected;
320+
if (monotonicallyDecreasing != -1 && point >= monotonicallyDecreasing) {
321+
console.error(
322+
'Edits not back-to-front. Inference of resulting selection might be inaccurate'
323+
); // TBD take the time to sort? or should commands emit edits in back-to-front order?
324+
}
325+
monotonicallyDecreasing = point;
326+
if (delta != 0) {
327+
retSelections = retSelections.map(function (s: ModelEditSelection) {
328+
return new ModelEditSelection(
329+
bump(s.end, affected),
330+
bump(s.active, affected),
331+
bump(s.start, affected),
332+
bump(s.end, affected)
333+
);
334+
});
335+
}
336+
}
337+
return retSelections;
338+
};
339+
})();
340+
290341
/** The underlying model for the REPL readline. */
291342
export class LineInputModel implements EditableModel {
292343
/** How many characters in the line endings of the text of this model? */
@@ -536,8 +587,12 @@ export class LineInputModel implements EditableModel {
536587
edit(edits: ModelEdit<ModelEditFunction>[], options: ModelEditOptions): Thenable<boolean> {
537588
return new Promise((resolve, reject) => {
538589
this.editTextNow(edits, options);
539-
if (this.document && options.selections) {
540-
this.document.selections = options.selections;
590+
if (this.document) {
591+
if (options.selections) {
592+
this.document.selections = options.selections;
593+
} else {
594+
this.document.selections = selectionsAfterEdits(edits, this.document.selections);
595+
}
541596
}
542597
resolve(true);
543598
});

src/cursor-doc/paredit.ts

+11-53
Original file line numberDiff line numberDiff line change
@@ -708,8 +708,7 @@ export async function wrapSexpr(
708708
* - For each cursor, find the offsets/ranges for its containing list's open/close tokens.
709709
* - Make 2 ModelEdits for each token's replacement + 1 Selection; record the offset change.
710710
* - Dedupe each edit (as multi cursors could be in the same list).
711-
* - Then, reposition the edits and selections by the preceding edits' offset changes.
712-
* - Finally, apply the edits and update the selections.
711+
* - Finally, apply the edits.
713712
*
714713
* @param doc
715714
* @param open
@@ -723,37 +722,23 @@ export function rewrapSexpr(
723722
close: string,
724723
selections = [doc.selections[0]]
725724
) {
726-
const edits: { type: 'open' | 'close'; change: number; edit: ModelEdit<'changeRange'> }[] = [],
727-
newSelections = _.clone(selections).map((s) => ({ selection: s, change: 0 }));
725+
const edits: ModelEdit<'changeRange'>[] = [];
728726

729727
selections.forEach((sel, index) => {
730728
const { active } = sel;
731729
const cursor = doc.getTokenCursor(active);
732730
if (cursor.backwardList()) {
733731
cursor.backwardUpList();
734-
const oldOpenStart = cursor.offsetStart;
732+
const openStart = cursor.offsetStart;
735733
const oldOpenLength = cursor.getToken().raw.length;
736-
const oldOpenEnd = oldOpenStart + oldOpenLength;
734+
const oldOpenEnd = openStart + oldOpenLength;
737735
if (cursor.forwardSexp()) {
738-
const oldCloseStart = cursor.offsetStart - close.length;
739-
const oldCloseEnd = cursor.offsetStart;
740-
const openChange = open.length - oldOpenLength;
736+
const closeStart = cursor.offsetStart - close.length;
737+
const closeEnd = cursor.offsetStart;
741738
edits.push(
742-
{
743-
edit: new ModelEdit('changeRange', [oldCloseStart, oldCloseEnd, close]),
744-
change: 0,
745-
type: 'close',
746-
},
747-
{
748-
edit: new ModelEdit('changeRange', [oldOpenStart, oldOpenEnd, open]),
749-
change: openChange,
750-
type: 'open',
751-
}
739+
new ModelEdit('changeRange', [closeStart, closeEnd, close]),
740+
new ModelEdit('changeRange', [openStart, oldOpenEnd, open])
752741
);
753-
newSelections[index] = {
754-
selection: new ModelEditSelection(active),
755-
change: openChange,
756-
};
757742
}
758743
}
759744
});
@@ -762,39 +747,12 @@ export function rewrapSexpr(
762747
// the same lists, which will result in attempting to delete the same ranges twice. So we dedupe.
763748
const uniqEdits = _.uniqWith(edits, _.isEqual);
764749

765-
// for both edits and new selections, get the offset by which to move each based on prior edits
766-
function getOffset(cursorOffset: number) {
767-
return _(uniqEdits)
768-
.filter((x) => {
769-
const [xStart] = x.edit.args;
770-
return xStart < cursorOffset;
771-
})
772-
.map(({ change }) => change)
773-
.sum();
774-
}
775-
750+
// edit needs the ModelEdit array in order from end-of-doc to start
776751
const editsToApply = _(uniqEdits)
777-
// First, importantly, sort by list open char offset
778-
.sortBy((e) => e.edit.args[0])
779-
// now, let's iterate thru each cursor and adjust their positions if earlier chars are delete/added
780-
.map((e) => {
781-
const [oldStart, oldEnd, text] = e.edit.args;
782-
const offset = getOffset(oldStart);
783-
const newStart = oldStart + offset;
784-
const newEnd = oldEnd + offset;
785-
return { ...e.edit, args: [newStart, newEnd, text] as const };
786-
})
752+
.sortBy((e) => -e.args[0])
787753
.value();
788-
const selectionsToApply = newSelections.map(({ selection }) => {
789-
const { active } = selection;
790-
const newSel = selection.clone();
791-
const offset = getOffset(active);
792-
newSel.reposition(offset);
793-
return newSel;
794-
});
795-
796754
return doc.model.edit(editsToApply, {
797-
selections: selectionsToApply,
755+
//skipFormat: selections.length > 1, // reformat-as-you-type works with only 1 selection
798756
});
799757
}
800758

0 commit comments

Comments
 (0)