1
1
import { isEqual , last , pick , property , clone , isBoolean , orderBy } from 'lodash' ;
2
+ import _ = require( 'lodash' ) ;
2
3
import { validPair } from './clojure-lexer' ;
3
4
import {
4
5
EditableDocument ,
@@ -20,15 +21,60 @@ import { replaceAt } from '../util/array';
20
21
// Example: paredit.moveToRangeRight(this.readline, paredit.forwardSexpRange(this.readline))
21
22
// => paredit.moveForwardSexp(this.readline)
22
23
23
- export function killRange (
24
- doc : EditableDocument ,
25
- range : [ number , number ] ,
26
- start = doc . selections [ 0 ] . anchor ,
27
- end = doc . selections [ 0 ] . active
28
- ) {
29
- const [ left , right ] = [ Math . min ( ...range ) , Math . max ( ...range ) ] ;
30
- void doc . model . edit ( [ new ModelEdit ( 'deleteRange' , [ left , right - left , [ start , end ] ] ) ] , {
31
- selections : [ new ModelEditSelection ( left ) ] ,
24
+ export function killRange ( doc : EditableDocument , ranges : Array < [ number , number ] > ) {
25
+ const edits = [ ] ,
26
+ // use tuple to track
27
+ // [selection, original selection/cursor order before sorting by location, and amount of text deleted]
28
+ selections : [ ModelEditSelection , number , number ] [ ] = [ ] ;
29
+
30
+ /**
31
+ * Sorting by location backwards simplifies the underlying "deleteRange" operation,
32
+ * as the operation logic doesn't have to translate the range by the sum of each prior deletion.
33
+ *
34
+ * We still however have to do the aforementioned translation/relocation for the post-delete cursor replacement,
35
+ * but that happens ONCE, at the end of this whole range killing series,
36
+ * and also seems to not be subject to as many strange bugs as
37
+ * when we do the translation/relocation for deletion operations.
38
+ *
39
+ * For example, it appears that sometimes a series of deletions DOESN'T take into account
40
+ * prior (ascending order of location) deletions,
41
+ * so in order to prevent incorrect text being deleted, we might want to precalculate the updated offsets
42
+ * on behalf of the delete operations, as we suggested NOT doing above.
43
+ *
44
+ * However, sometimes, it DOES take into account the prior deletions (or at least some of them)
45
+ *
46
+ * Of course, if the latter occurs, yet we thought the former would, our preventative
47
+ * precalculated offset adjustments would then cause
48
+ * incorrect text to be deleted anyways.
49
+ */
50
+
51
+ _ ( ranges )
52
+ . map ( ( r , idx ) => [ r , idx ] as const )
53
+ . orderBy ( ( [ r ] ) => Math . min ( ...r ) , 'desc' )
54
+ . forEach ( ( [ range , idx ] ) => {
55
+ // we assume the ranges passed above are some transformation of the current selections
56
+ // therefore doc.selections[index] should be the range before the transformation... maybe
57
+ const [ left , right ] = [ Math . min ( ...range ) , Math . max ( ...range ) ] ;
58
+ const length = right - left ;
59
+ edits . push ( new ModelEdit ( 'deleteRange' , [ left , length ] ) ) ;
60
+ selections . push ( [ new ModelEditSelection ( left ) , idx , length ] ) ;
61
+ } ) ;
62
+
63
+ return doc . model . edit ( edits , {
64
+ selections : _ ( selections )
65
+ // return to original selection/cursor order
66
+ . orderBy ( ( [ _ , idx ] ) => idx )
67
+ // pull each cursor backwards by the amount of text deleted by every prior (by location) cursor's delete operation
68
+ . map (
69
+ ( [ selection ] , _index , others ) =>
70
+ new ModelEditSelection (
71
+ selection . start -
72
+ _ ( others )
73
+ . filter ( ( [ s ] ) => s . start < selection . start )
74
+ . reduce ( ( sum , [ _ , __ , length ] ) => sum + length , 0 )
75
+ )
76
+ )
77
+ . value ( ) ,
32
78
} ) ;
33
79
}
34
80
@@ -683,38 +729,132 @@ export function spliceSexp(
683
729
return doc . model . edit ( edits , { undoStopBefore, selections } ) ;
684
730
}
685
731
732
+ export function killSexpBackward (
733
+ doc : EditableDocument ,
734
+ shouldKillAlsoCutToClipboard = ( ) => false ,
735
+ copyRangeToClipboard = ( doc : EditableDocument , range : Array < [ number , number ] > ) => undefined
736
+ ) {
737
+ const ranges = backwardSexpRange (
738
+ doc ,
739
+ doc . selections . map ( ( s ) => s . active )
740
+ ) ;
741
+ if ( shouldKillAlsoCutToClipboard ( ) ) {
742
+ copyRangeToClipboard ( doc , ranges ) ;
743
+ }
744
+ return killRange ( doc , ranges ) ;
745
+ }
746
+
747
+ export function killSexpForward (
748
+ doc : EditableDocument ,
749
+ shouldKillAlsoCutToClipboard = ( ) => false ,
750
+ copyRangeToClipboard = ( doc : EditableDocument , range : Array < [ number , number ] > ) => undefined
751
+ ) {
752
+ const ranges = forwardSexpRange ( doc ) ;
753
+ if ( shouldKillAlsoCutToClipboard ( ) ) {
754
+ copyRangeToClipboard ( doc , ranges ) ;
755
+ }
756
+ return killRange ( doc , ranges ) ;
757
+ }
758
+
759
+ /**
760
+ * In making this compatible with multi-cursor,
761
+ * we had to complicate the logic somewhat to make sure
762
+ * deletions by prior cursors are taken into account by
763
+ * later ones, and are relocated accordingly.
764
+ *
765
+ * See comments in paredit.killRange() for more details.
766
+ */
686
767
export function killBackwardList (
687
768
doc : EditableDocument ,
688
- [ start , end ] : [ number , number ]
769
+ ranges : Array < [ number , number ] > = doc . selections . map ( ( s ) => [ s . start , s . end ] )
689
770
) : Thenable < ModelEditResult > {
690
- return doc . model . edit (
691
- [ new ModelEdit ( 'changeRange' , [ start , end , '' , [ end , end ] , [ start , start ] ] ) ] ,
692
- {
693
- selections : [ new ModelEditSelection ( start ) ] ,
694
- }
695
- ) ;
771
+ const edits : ModelEdit < 'deleteRange' > [ ] = [ ] ,
772
+ selections : [ ModelEditSelection , number , number ] [ ] = [ ] ;
773
+
774
+ _ ( ranges )
775
+ . map ( ( r , idx ) => [ r , idx ] as const )
776
+ . orderBy ( ( [ r ] ) => Math . min ( ...r ) , 'desc' )
777
+ . forEach ( ( [ r , originalIndex ] ) => {
778
+ const [ left , right ] = r ;
779
+ const cursor = doc . getTokenCursor ( left ) ;
780
+ cursor . backwardList ( ) ;
781
+ // const offset = selections
782
+ // .filter((s) => s.start < left)
783
+ // .reduce((sum, s) => sum + s.distance, 0);
784
+ // const start = cursor.offsetStart - offset;
785
+ // const end = right - offset;
786
+ // edits.push(new ModelEdit('deleteRange', [start, Math.abs(end - start)]));
787
+ const start = cursor . offsetStart ;
788
+ const end = right ;
789
+ const length = Math . abs ( end - start ) ;
790
+ edits . push ( new ModelEdit ( 'deleteRange' , [ start , length ] ) ) ;
791
+ // selections.push([new ModelEditSelection(start, end), originalIndex, length]);
792
+ selections . push ( [ new ModelEditSelection ( start , end ) , originalIndex , length ] ) ;
793
+ } ) ;
794
+
795
+ return doc . model . edit ( edits , {
796
+ selections : _ ( selections )
797
+ . orderBy ( ( [ _ , originalIndex ] ) => originalIndex )
798
+ . map (
799
+ ( [ selection ] , _idx , others ) =>
800
+ new ModelEditSelection (
801
+ selection . start -
802
+ _ ( others )
803
+ . filter ( ( [ s ] ) => s . start < selection . start )
804
+ . reduce ( ( sum , [ _ , __ , lengthDeleted ] ) => sum + lengthDeleted , 0 )
805
+ )
806
+ )
807
+ . value ( ) ,
808
+ } ) ;
696
809
}
697
810
811
+ /**
812
+ * In making this compatible with multi-cursor,
813
+ * we had to complicate the logic somewhat to make sure
814
+ * deletions by prior cursors are taken into account by
815
+ * later ones, and are relocated accordingly.
816
+ *
817
+ * See comments in paredit.killRange() for more details.
818
+ */
698
819
export function killForwardList (
699
820
doc : EditableDocument ,
700
- [ start , end ] : [ number , number ]
821
+ ranges : Array < [ number , number ] > = doc . selections . map ( ( s ) => [ s . start , s . end ] )
701
822
) : Thenable < ModelEditResult > {
702
- const cursor = doc . getTokenCursor ( start ) ;
703
- const inComment =
704
- ( cursor . getToken ( ) . type == 'comment' && start > cursor . offsetStart ) ||
705
- cursor . getPrevToken ( ) . type == 'comment' ;
706
- return doc . model . edit (
707
- [
708
- new ModelEdit ( 'changeRange' , [
709
- start ,
710
- end ,
711
- inComment ? '\n' : '' ,
712
- [ start , start ] ,
713
- [ start , start ] ,
714
- ] ) ,
715
- ] ,
716
- { selections : [ new ModelEditSelection ( start ) ] }
717
- ) ;
823
+ const edits : ModelEdit < 'changeRange' > [ ] = [ ] ,
824
+ selections : [ ModelEditSelection , number , number ] [ ] = [ ] ;
825
+
826
+ _ ( ranges )
827
+ . map ( ( r , idx ) => [ r , idx ] as const )
828
+ . orderBy ( ( [ r ] ) => Math . min ( ...r ) , 'desc' )
829
+ . forEach ( ( [ r , originalIndex ] ) => {
830
+ const [ left , right ] = r ;
831
+ const cursor = doc . getTokenCursor ( left ) ;
832
+ cursor . forwardList ( ) ;
833
+ const inComment =
834
+ ( cursor . getToken ( ) . type == 'comment' && left > cursor . offsetStart ) ||
835
+ cursor . getPrevToken ( ) . type == 'comment' ;
836
+
837
+ const start = cursor . offsetStart ;
838
+ const end = right ;
839
+ const length = Math . abs ( end - start ) ;
840
+ edits . push ( new ModelEdit ( 'changeRange' , [ start , end , inComment ? '\n' : '' ] ) ) ;
841
+ selections . push ( [ new ModelEditSelection ( start , end ) , originalIndex , length ] ) ;
842
+ } ) ;
843
+
844
+ return doc . model . edit ( edits , {
845
+ selections : _ ( selections )
846
+ . orderBy ( ( [ _ , originalIndex ] ) => originalIndex )
847
+ . map (
848
+ ( [ selection ] , _idx , others ) =>
849
+ new ModelEditSelection (
850
+ selection . start -
851
+ _ ( others )
852
+ . filter ( ( [ s ] ) => s . start < selection . start )
853
+ . reduce ( ( sum , [ _ , __ , lengthDeleted ] ) => sum + lengthDeleted , 0 )
854
+ )
855
+ )
856
+ . value ( ) ,
857
+ } ) ;
718
858
}
719
859
720
860
// FIXME: check if this forEach solution works vs map into modelEdit batch
@@ -1272,40 +1412,64 @@ export function setSelectionStack(
1272
1412
doc . selectionsStack = selections ;
1273
1413
}
1274
1414
1275
- export function raiseSexp (
1276
- doc : EditableDocument
1277
- // start = doc.selections.anchor,
1278
- // end = doc.selections.active
1279
- ) {
1280
- const edits = [ ] ,
1281
- selections = clone ( doc . selections ) ;
1282
- doc . selections . forEach ( ( selection , index ) => {
1283
- const { start, end } = selection ;
1415
+ /**
1416
+ * In making this compatible with multi-cursor,
1417
+ * we had to complicate the logic somewhat to make sure
1418
+ * deletions by prior cursors are taken into account by
1419
+ * later ones, and are relocated accordingly.
1420
+ *
1421
+ * See comments in paredit.killRange() for more details.
1422
+ */
1423
+ export function raiseSexp ( doc : EditableDocument ) {
1424
+ const edits : ModelEdit < 'changeRange' > [ ] = [ ] ,
1425
+ selections = doc . selections . map ( ( s ) => [ 0 , s . clone ( ) ] as [ number , ModelEditSelection ] ) ;
1426
+
1427
+ _ ( doc . selections )
1428
+ . map ( ( s , index ) => [ s , index ] as const )
1429
+ . orderBy ( ( [ s ] ) => s . start , 'desc' )
1430
+ . forEach ( ( [ selection , originalIndex ] , index ) => {
1431
+ const { start, end } = selection ;
1284
1432
1285
- const cursor = doc . getTokenCursor ( end ) ;
1286
- const [ formStart , formEnd ] = cursor . rangeForCurrentForm ( start ) ;
1287
- const isCaretTrailing = formEnd - start < start - formStart ;
1288
- const startCursor = doc . getTokenCursor ( formStart ) ;
1289
- const endCursor = startCursor . clone ( ) ;
1290
- if ( endCursor . forwardSexp ( ) ) {
1291
- const raised = doc . model . getText ( startCursor . offsetStart , endCursor . offsetStart ) ;
1292
- startCursor . backwardList ( ) ;
1293
- endCursor . forwardList ( ) ;
1294
- if ( startCursor . getPrevToken ( ) . type == 'open' ) {
1295
- startCursor . previous ( ) ;
1296
- if ( endCursor . getToken ( ) . type == 'close' ) {
1297
- edits . push (
1298
- new ModelEdit ( 'changeRange' , [ startCursor . offsetStart , endCursor . offsetEnd , raised ] )
1299
- ) ;
1300
- selections [ index ] = new ModelEditSelection (
1301
- isCaretTrailing ? startCursor . offsetStart + raised . length : startCursor . offsetStart
1302
- ) ;
1433
+ const cursor = doc . getTokenCursor ( end ) ;
1434
+ const [ formStart , formEnd ] = cursor . rangeForCurrentForm ( start ) ;
1435
+ const isCaretTrailing = formEnd - start < start - formStart ;
1436
+ const startCursor = doc . getTokenCursor ( formStart ) ;
1437
+ const endCursor = startCursor . clone ( ) ;
1438
+ if ( endCursor . forwardSexp ( ) ) {
1439
+ const raised = doc . model . getText ( startCursor . offsetStart , endCursor . offsetStart ) ;
1440
+ startCursor . backwardList ( ) ;
1441
+ endCursor . forwardList ( ) ;
1442
+ if ( startCursor . getPrevToken ( ) . type == 'open' ) {
1443
+ startCursor . previous ( ) ;
1444
+ if ( endCursor . getToken ( ) . type == 'close' ) {
1445
+ edits . push (
1446
+ new ModelEdit ( 'changeRange' , [ startCursor . offsetStart , endCursor . offsetEnd , raised ] )
1447
+ ) ;
1448
+ const cursorPos = isCaretTrailing
1449
+ ? startCursor . offsetStart + raised . length
1450
+ : startCursor . offsetStart ;
1451
+
1452
+ selections [ originalIndex ] = [
1453
+ endCursor . offsetEnd - startCursor . offsetStart - raised . length ,
1454
+ new ModelEditSelection ( cursorPos ) ,
1455
+ ] ;
1456
+ }
1303
1457
}
1304
1458
}
1305
- }
1306
- } ) ;
1459
+ } ) ;
1307
1460
return doc . model . edit ( edits , {
1308
- selections,
1461
+ selections : selections . map ( ( [ _ , selection ] , __ , others ) => {
1462
+ const s = selection . clone ( ) ;
1463
+
1464
+ const offsetSum = others
1465
+ . filter ( ( [ _ , otherSel ] ) => otherSel . start < selection . start )
1466
+ . reduce ( ( sum , o ) => sum + o [ 0 ] , 0 ) ;
1467
+
1468
+ s . anchor -= offsetSum ;
1469
+ s . active -= offsetSum ;
1470
+
1471
+ return s ;
1472
+ } ) ,
1309
1473
} ) ;
1310
1474
}
1311
1475
0 commit comments