lexgui 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -201,8 +201,8 @@ class CodeEditor {
201
201
  static WORD_TYPE_METHOD = 0;
202
202
  static WORD_TYPE_CLASS = 1;
203
203
 
204
- static CODE_MAX_FONT_SIZE = 20;
205
- static CODE_MIN_FONT_SIZE = 11;
204
+ static CODE_MAX_FONT_SIZE = 22;
205
+ static CODE_MIN_FONT_SIZE = 9;
206
206
 
207
207
  /**
208
208
  * @param {*} options
@@ -333,42 +333,15 @@ class CodeEditor {
333
333
 
334
334
  // Add main cursor
335
335
  {
336
- var cursor = document.createElement( 'div' );
337
- cursor.className = "cursor";
338
- cursor.innerHTML = " ";
339
- cursor._left = 0;
340
- cursor.style.left = this.xPadding;
341
- cursor._top = 0;
342
- cursor.style.top = cursor._top + "px";
343
- cursor._position = 0;
344
- cursor._line = 0;
345
- cursor.print = (function() { console.log( this.line, this.position ) }).bind( cursor );
336
+ this._addCursor( 0, 0, true, true );
346
337
 
347
338
  Object.defineProperty( this, 'line', {
348
- get: (v) => { return cursor.line }
339
+ get: (v) => { return this._getCurrentCursor().line }
349
340
  } );
350
-
341
+
351
342
  Object.defineProperty( this, 'position', {
352
- get: (v) => { return cursor.position }
353
- } );
354
-
355
- Object.defineProperty( cursor, 'line', {
356
- get: (v) => { return this._line },
357
- set: (v) => {
358
- this._line = v;
359
- this._setActiveLine( v );
360
- }
361
- } );
362
-
363
- Object.defineProperty( cursor, 'position', {
364
- get: (v) => { return this._position },
365
- set: (v) => {
366
- this._position = v;
367
- this._updateDataInfoPanel( "@cursor-pos", "Col " + v );
368
- }
343
+ get: (v) => { return this._getCurrentCursor().position }
369
344
  } );
370
-
371
- this.cursors.appendChild( cursor );
372
345
  }
373
346
 
374
347
  // Scroll stuff
@@ -534,6 +507,7 @@ class CodeEditor {
534
507
  this.tabSpaces = 4;
535
508
  this.maxUndoSteps = 16;
536
509
  this.lineHeight = 20;
510
+ this.charWidth = 7; // To update later depending on size..
537
511
  this.defaultSingleLineCommentToken = '//';
538
512
  this.defaultBlockCommentTokens = [ '/*', '*/' ];
539
513
  this._lastTime = null;
@@ -624,6 +598,7 @@ class CodeEditor {
624
598
  };
625
599
  this.statementsAndDeclarations = {
626
600
  'JavaScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await'],
601
+ 'CSS': ['@', 'import'],
627
602
  'C++': ['std', 'for', 'if', 'else', 'return', 'continue', 'break', 'case', 'switch', 'while', 'using', 'glm', 'spdlog'],
628
603
  'GLSL': ['for', 'if', 'else', 'return', 'continue', 'break'],
629
604
  'WGSL': ['const','for', 'if', 'else', 'return', 'continue', 'break', 'storage', 'read', 'uniform'],
@@ -659,6 +634,7 @@ class CodeEditor {
659
634
  this.action( 'Escape', false, ( ln, cursor, e ) => {
660
635
  this.hideAutoCompleteBox();
661
636
  this.hideSearchBox();
637
+ this._removeSecondaryCursors();
662
638
  });
663
639
 
664
640
  this.action( 'Backspace', false, ( ln, cursor, e ) => {
@@ -704,7 +680,7 @@ class CodeEditor {
704
680
  }
705
681
  else if( this.code.lines[ ln - 1 ] != undefined ) {
706
682
 
707
- this.lineUp();
683
+ this.lineUp( cursor );
708
684
  e.cancelShift = true;
709
685
  this.actions[ 'End' ].callback( cursor.line, cursor, e );
710
686
  // Move line on top
@@ -759,18 +735,22 @@ class CodeEditor {
759
735
  const prestring = this.code.lines[ ln ].substring( 0, idx );
760
736
  let lastX = cursor.position;
761
737
 
762
- this.resetCursorPos( CodeEditor.CURSOR_LEFT );
738
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
763
739
  if(idx > 0) this.cursorToString( cursor, prestring );
764
740
  this.setScrollLeft( 0 );
765
741
 
742
+ // Merge cursors
743
+ this.mergeCursors( ln );
744
+
766
745
  if( e.shiftKey && !e.cancelShift )
767
746
  {
768
747
  // Get last selection range
769
748
  if( this.selection )
770
- lastX += this.selection.chars;
749
+ lastX += this.selection.chars;
771
750
 
772
751
  if( !this.selection )
773
752
  this.startSelection( cursor );
753
+
774
754
  var string = this.code.lines[ ln ].substring( idx, lastX );
775
755
  if( this.selection.sameLine() )
776
756
  this.selection.selectInline( idx, cursor.line, this.measureString( string ) );
@@ -793,18 +773,21 @@ class CodeEditor {
793
773
  this.selection.selectInline(cursor.position, cursor.line, this.measureString( string ));
794
774
  else
795
775
  {
796
- this.resetCursorPos( CodeEditor.CURSOR_LEFT );
776
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
797
777
  this.cursorToString( cursor, this.code.lines[ ln ] );
798
778
  this.processSelection( e );
799
779
  }
800
780
  } else if( !e.keepSelection )
801
781
  this.endSelection();
802
782
 
803
- this.resetCursorPos( CodeEditor.CURSOR_LEFT );
783
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
804
784
  this.cursorToString( cursor, this.code.lines[ ln ] );
805
785
 
806
786
  const last_char = ( this.code.clientWidth / this.charWidth )|0;
807
787
  this.setScrollLeft( cursor.position >= last_char ? ( cursor.position - last_char ) * this.charWidth : 0 );
788
+
789
+ // Merge cursors
790
+ this.mergeCursors( ln );
808
791
  });
809
792
 
810
793
  this.action( 'Enter', true, ( ln, cursor, e ) => {
@@ -830,6 +813,7 @@ class CodeEditor {
830
813
  this.code.lines.splice( cursor.line + 1, 0, "" );
831
814
  this.code.lines[cursor.line + 1] = this.code.lines[ ln ].substr( cursor.position ); // new line (below)
832
815
  this.code.lines[ ln ] = this.code.lines[ ln ].substr( 0, cursor.position ); // line above
816
+
833
817
  this.lineDown( cursor, true );
834
818
 
835
819
  // Check indentation
@@ -856,7 +840,7 @@ class CodeEditor {
856
840
  if( !this.selection )
857
841
  this.startSelection( cursor );
858
842
 
859
- this.lineUp();
843
+ this.lineUp( cursor );
860
844
 
861
845
  var letter = this.getCharAtPos( cursor );
862
846
  if( !letter ) {
@@ -867,7 +851,7 @@ class CodeEditor {
867
851
 
868
852
  } else {
869
853
  this.endSelection();
870
- this.lineUp();
854
+ this.lineUp( cursor );
871
855
  // Go to end of line if out of line
872
856
  var letter = this.getCharAtPos( cursor );
873
857
  if( !letter ) this.actions['End'].callback( cursor.line, cursor, e );
@@ -888,24 +872,20 @@ class CodeEditor {
888
872
  if( e.shiftKey ) {
889
873
  if( !this.selection )
890
874
  this.startSelection( cursor );
875
+ } else {
876
+ this.endSelection();
877
+ }
891
878
 
892
- this.lineDown( cursor );
893
-
894
- var letter = this.getCharAtPos( cursor );
895
- if( !letter ) {
896
- this.cursorToPosition( cursor, Math.max(this.code.lines[ cursor.line ].length - 1, 0) );
897
- }
879
+ const canGoDown = this.lineDown( cursor );
880
+ const letter = this.getCharAtPos( cursor );
881
+
882
+ // Go to end of line if out of range
883
+ if( !letter || !canGoDown ) {
884
+ this.actions[ 'End' ].callback( cursor.line, cursor, e );
885
+ }
898
886
 
887
+ if( e.shiftKey ) {
899
888
  this.processSelection( e );
900
- } else {
901
-
902
- if( this.code.lines[ ln + 1 ] == undefined )
903
- return;
904
- this.endSelection();
905
- this.lineDown( cursor );
906
- // Go to end of line if out of line
907
- var letter = this.getCharAtPos( cursor );
908
- if( !letter ) this.actions['End'].callback(cursor.line, cursor, e);
909
889
  }
910
890
  }
911
891
  // Move down autocomplete selection
@@ -960,7 +940,7 @@ class CodeEditor {
960
940
  }
961
941
  else {
962
942
  this.selection.invertIfNecessary();
963
- this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP );
943
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP, cursor );
964
944
  this.cursorToLine( cursor, this.selection.fromY, true );
965
945
  this.cursorToPosition( cursor, this.selection.fromX );
966
946
  this.endSelection();
@@ -986,7 +966,7 @@ class CodeEditor {
986
966
 
987
967
  // Nothing to do..
988
968
  if( cursor.line == this.code.lines.length - 1 &&
989
- cursor.position == this.code.lines[ cursor.line - 1 ].length )
969
+ cursor.position == this.code.lines[ cursor.line ].length )
990
970
  return;
991
971
 
992
972
  if( e.metaKey ) { // Apple devices (Command)
@@ -1002,7 +982,7 @@ class CodeEditor {
1002
982
  // Selections...
1003
983
  if( e.shiftKey ) { if( !this.selection ) this.startSelection( cursor ); }
1004
984
  else this.endSelection();
1005
- this.cursorToString( cursor, substr);
985
+ this.cursorToString( cursor, substr );
1006
986
  if( e.shiftKey ) this.processSelection( e );
1007
987
  } else {
1008
988
  var letter = this.getCharAtPos( cursor );
@@ -1020,7 +1000,7 @@ class CodeEditor {
1020
1000
  else
1021
1001
  {
1022
1002
  this.selection.invertIfNecessary();
1023
- this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP );
1003
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP, cursor );
1024
1004
  this.cursorToLine( cursor, this.selection.toY );
1025
1005
  this.cursorToPosition( cursor, this.selection.toX );
1026
1006
  this.endSelection();
@@ -1106,7 +1086,8 @@ class CodeEditor {
1106
1086
  }
1107
1087
 
1108
1088
  getText( min ) {
1109
- return this.code.lines.join(min ? ' ' : '\n');
1089
+
1090
+ return this.code.lines.join( min ? ' ' : '\n' );
1110
1091
  }
1111
1092
 
1112
1093
  // This can be used to empty all text...
@@ -1115,7 +1096,9 @@ class CodeEditor {
1115
1096
  let new_lines = text.split( '\n' );
1116
1097
  this.code.lines = [].concat( new_lines );
1117
1098
 
1118
- let cursor = this.cursors.children[ 0 ];
1099
+ this._removeSecondaryCursors();
1100
+
1101
+ let cursor = this._getCurrentCursor( true );
1119
1102
  let lastLine = new_lines.pop();
1120
1103
 
1121
1104
  this.cursorToLine( cursor, new_lines.length ); // Already substracted 1
@@ -1128,9 +1111,8 @@ class CodeEditor {
1128
1111
  }
1129
1112
  }
1130
1113
 
1131
- appendText( text ) {
1114
+ appendText( text, cursor ) {
1132
1115
 
1133
- let cursor = this.cursors.children[ 0 ];
1134
1116
  let lidx = cursor.line;
1135
1117
 
1136
1118
  if( this.selection ) {
@@ -1234,8 +1216,82 @@ class CodeEditor {
1234
1216
  }
1235
1217
  }
1236
1218
 
1219
+ _addCursor( line = 0, position = 0, force, isMain = false ) {
1220
+
1221
+ // If cursor in that position exists, remove it instead..
1222
+ const exists = Array.from( this.cursors.children ).find( v => v.position == position && v.line == line );
1223
+ if( exists && !force )
1224
+ {
1225
+ if( !exists.isMain )
1226
+ exists.remove();
1227
+
1228
+ return;
1229
+ }
1230
+
1231
+ let cursor = document.createElement( 'div' );
1232
+ cursor.className = "cursor";
1233
+ cursor.innerHTML = " ";
1234
+ cursor.isMain = isMain;
1235
+ cursor._left = position * this.charWidth;
1236
+ cursor.style.left = "calc( " + cursor._left + "px + " + this.xPadding + " )";
1237
+ cursor._top = line * this.lineHeight;
1238
+ cursor.style.top = cursor._top + "px";
1239
+ cursor._position = position;
1240
+ cursor._line = line;
1241
+ cursor.print = (function() { console.log( this._line, this._position ) }).bind( cursor );
1242
+ cursor.isLast = (function() { return this.cursors.lastChild == cursor; }).bind( this );
1243
+
1244
+ Object.defineProperty( cursor, 'line', {
1245
+ get: (v) => { return cursor._line },
1246
+ set: (v) => {
1247
+ cursor._line = v;
1248
+ if( cursor.isMain ) this._setActiveLine( v );
1249
+ }
1250
+ } );
1251
+
1252
+ Object.defineProperty( cursor, 'position', {
1253
+ get: (v) => { return cursor._position },
1254
+ set: (v) => {
1255
+ cursor._position = v;
1256
+ if( cursor.isMain ) this._updateDataInfoPanel( "@cursor-pos", "Col " + v );
1257
+ }
1258
+ } );
1259
+
1260
+ this.cursors.appendChild( cursor );
1261
+
1262
+ return cursor;
1263
+ }
1264
+
1265
+ _getCurrentCursor( removeOthers ) {
1266
+
1267
+ if( removeOthers )
1268
+ {
1269
+ this._removeSecondaryCursors();
1270
+ }
1271
+
1272
+ return this.cursors.children[ 0 ];
1273
+ }
1274
+
1275
+ _removeSecondaryCursors() {
1276
+
1277
+ while( this.cursors.childElementCount > 1 )
1278
+ this.cursors.lastChild.remove();
1279
+ }
1280
+
1281
+ _logCursors() {
1282
+
1283
+ for( let cursor of this.cursors.children )
1284
+ {
1285
+ cursor.print();
1286
+ }
1287
+ }
1288
+
1237
1289
  _addUndoStep( cursor, force, deleteRedo = true ) {
1238
1290
 
1291
+ // Only the mainc cursor stores undo steps
1292
+ if( !cursor.isMain )
1293
+ return;
1294
+
1239
1295
  const d = new Date();
1240
1296
  const current = d.getTime();
1241
1297
 
@@ -1244,7 +1300,7 @@ class CodeEditor {
1244
1300
  if( !this._lastTime ) {
1245
1301
  this._lastTime = current;
1246
1302
  } else {
1247
- if( ( current - this._lastTime ) > 3000 ){
1303
+ if( ( current - this._lastTime ) > 2000 ){
1248
1304
  this._lastTime = null;
1249
1305
  } else {
1250
1306
  // If time not enough, reset timer
@@ -1260,28 +1316,80 @@ class CodeEditor {
1260
1316
  this.code.redoSteps.length = 0;
1261
1317
  }
1262
1318
 
1263
- var cursor = cursor ?? this.cursors.children[ 0 ];
1264
-
1265
1319
  this.code.undoSteps.push( {
1266
1320
  lines: LX.deepCopy( this.code.lines ),
1267
- cursor: this.saveCursor( cursor ),
1268
- line: cursor.line,
1269
- position: cursor.position
1321
+ cursors: this.saveCursors()
1270
1322
  } );
1271
1323
  }
1272
1324
 
1325
+ _doUndo( cursor ) {
1326
+
1327
+ if( !this.code.undoSteps.length )
1328
+ return;
1329
+
1330
+ this._addRedoStep( cursor );
1331
+
1332
+ // Extract info from the last code state
1333
+ const step = this.code.undoSteps.pop();
1334
+
1335
+ // Set old state lines
1336
+ this.code.lines = step.lines;
1337
+ this.processLines();
1338
+
1339
+ this._removeSecondaryCursors();
1340
+
1341
+ for( let i = 0; i < step.cursors.length; ++i )
1342
+ {
1343
+ var currentCursor = this.cursors.children[ i ];
1344
+
1345
+ // Generate new if needed
1346
+ if( !currentCursor )
1347
+ currentCursor = this._addCursor();
1348
+
1349
+ this.restoreCursor( currentCursor, step.cursors[ i ] );
1350
+ }
1351
+ }
1352
+
1273
1353
  _addRedoStep( cursor ) {
1274
1354
 
1275
- var cursor = cursor ?? this.cursors.children[ 0 ];
1355
+ // Only the mainc cursor stores redo steps
1356
+ if( !cursor.isMain )
1357
+ return;
1276
1358
 
1277
1359
  this.code.redoSteps.push( {
1278
1360
  lines: LX.deepCopy( this.code.lines ),
1279
- cursor: this.saveCursor( cursor ),
1280
- line: cursor.line,
1281
- position: cursor.position
1361
+ cursors: this.saveCursors()
1282
1362
  } );
1283
1363
  }
1284
1364
 
1365
+ _doRedo( cursor ) {
1366
+
1367
+ if( !this.code.redoSteps.length )
1368
+ return;
1369
+
1370
+ this._addUndoStep( cursor, true, false);
1371
+
1372
+ // Extract info from the next saved code state
1373
+ const step = this.code.redoSteps.pop();
1374
+
1375
+ // Set old state lines
1376
+ this.code.lines = step.lines;
1377
+ this.processLines();
1378
+
1379
+ this._removeSecondaryCursors();
1380
+
1381
+ for( let i = 0; i < step.cursors.length; ++i )
1382
+ {
1383
+ var currentCursor = this.cursors.children[ i ];
1384
+
1385
+ // Generate new if needed
1386
+ if( !currentCursor )
1387
+ currentCursor = this._addCursor();
1388
+
1389
+ this.restoreCursor( currentCursor, step.cursors[ i ] );
1390
+ }
1391
+ }
1392
+
1285
1393
  _changeLanguage( lang ) {
1286
1394
 
1287
1395
  this.code.language = lang;
@@ -1374,7 +1482,7 @@ class CodeEditor {
1374
1482
  ext == 'css' ? "fa-solid fa-hashtag dodgerblue" :
1375
1483
  ext == 'xml' ? "fa-solid fa-rss orange" :
1376
1484
  ext == 'bat' ? "fa-brands fa-windows lightblue" :
1377
- [ 'js', 'py', 'json', 'cpp', 'rs' ].indexOf( ext ) > -1 ? "images/" + ext + ".png" :
1485
+ [ 'js', 'py', 'json', 'cpp', 'rs', 'md' ].indexOf( ext ) > -1 ? "images/" + ext + ".png" :
1378
1486
  !isNewTabButton ? "fa-solid fa-align-left gray" : undefined;
1379
1487
  }
1380
1488
 
@@ -1388,7 +1496,7 @@ class CodeEditor {
1388
1496
  });
1389
1497
  }
1390
1498
 
1391
- _onSelectTab( isNewTabButton, event, name, ) {
1499
+ _onSelectTab( isNewTabButton, event, name ) {
1392
1500
 
1393
1501
  if( isNewTabButton )
1394
1502
  {
@@ -1396,7 +1504,9 @@ class CodeEditor {
1396
1504
  return;
1397
1505
  }
1398
1506
 
1399
- var cursor = cursor ?? this.cursors.children[ 0 ];
1507
+ this._removeSecondaryCursors();
1508
+ var cursor = this._getCurrentCursor( true );
1509
+
1400
1510
  this.saveCursor( cursor, this.code.cursorState );
1401
1511
 
1402
1512
  this.code = this.loadedTabs[ name ];
@@ -1573,21 +1683,18 @@ class CodeEditor {
1573
1683
  }
1574
1684
  }
1575
1685
 
1576
- processMouse(e) {
1686
+ processMouse( e ) {
1577
1687
 
1578
- if( !e.target.classList.contains('code') ) return;
1688
+ if( !e.target.classList.contains('code') && !e.target.classList.contains('codetabsarea') ) return;
1579
1689
  if( !this.code ) return;
1580
1690
 
1581
- var cursor = this.cursors.children[ 0 ];
1691
+ var cursor = this._getCurrentCursor();
1582
1692
  var code_rect = this.code.getBoundingClientRect();
1583
1693
  var mouse_pos = [(e.clientX - code_rect.x), (e.clientY - code_rect.y)];
1584
1694
 
1585
1695
  // Discard out of lines click...
1586
- if( e.type != 'contextmenu' )
1587
- {
1588
- var ln = (mouse_pos[1] / this.lineHeight)|0;
1589
- if(this.code.lines[ ln ] == undefined) return;
1590
- }
1696
+ var ln = ( mouse_pos[ 1 ] / this.lineHeight ) | 0;
1697
+ if( ln < 0 ) return;
1591
1698
 
1592
1699
  if( e.type == 'mousedown' )
1593
1700
  {
@@ -1630,7 +1737,7 @@ class CodeEditor {
1630
1737
  {
1631
1738
  case LX.MOUSE_DOUBLE_CLICK:
1632
1739
  const [word, from, to] = this.getWordAtPos( cursor );
1633
- this.resetCursorPos( CodeEditor.CURSOR_LEFT );
1740
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
1634
1741
  this.cursorToPosition( cursor, from );
1635
1742
  this.startSelection( cursor );
1636
1743
  this.selection.selectInline( from, cursor.line, this.measureString( word ) );
@@ -1638,7 +1745,7 @@ class CodeEditor {
1638
1745
  break;
1639
1746
  // Select entire line
1640
1747
  case LX.MOUSE_TRIPLE_CLICK:
1641
- this.resetCursorPos( CodeEditor.CURSOR_LEFT );
1748
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
1642
1749
  e._shiftKey = true;
1643
1750
  this.actions['End'].callback(cursor.line, cursor, e);
1644
1751
  this._tripleClickSelection = true;
@@ -1654,11 +1761,11 @@ class CodeEditor {
1654
1761
  return;
1655
1762
 
1656
1763
  LX.addContextMenu( null, e, m => {
1657
- m.add( "Copy", () => { this._copyContent(); } );
1764
+ m.add( "Copy", () => { this._copyContent( cursor ); } );
1658
1765
  if( !this.disableEdition )
1659
1766
  {
1660
- m.add( "Cut", () => { this._cutContent(); } );
1661
- m.add( "Paste", () => { this._pasteContent(); } );
1767
+ m.add( "Cut", () => { this._cutContent( cursor ); } );
1768
+ m.add( "Paste", () => { this._pasteContent( cursor ); } );
1662
1769
  m.add( "" );
1663
1770
  m.add( "Format/JSON", () => {
1664
1771
  let json = this.toJSONFormat( this.getText() );
@@ -1690,26 +1797,40 @@ class CodeEditor {
1690
1797
 
1691
1798
  processClick( e ) {
1692
1799
 
1693
- var cursor = this.cursors.children[ 0 ];
1800
+ var cursor = this._getCurrentCursor();
1694
1801
  var code_rect = this.codeScroller.getBoundingClientRect();
1695
1802
  var position = [( e.clientX - code_rect.x ) + this.getScrollLeft(), (e.clientY - code_rect.y) + this.getScrollTop()];
1696
1803
  var ln = (position[ 1 ] / this.lineHeight)|0;
1697
1804
 
1698
- if( this.code.lines[ ln ] == undefined )
1699
- return;
1700
-
1701
- this.cursorToLine( cursor, ln, true );
1702
-
1805
+ // Check out of range line
1806
+ const outOfRange = ln > this.code.lines.length - 1;
1807
+ ln = Math.min( ln, this.code.lines.length - 1 );
1808
+
1703
1809
  var ch = ( ( position[ 0 ] - parseInt( this.xPadding ) + 3) / this.charWidth )|0;
1704
- var string = this.code.lines[ ln ].slice( 0, ch );
1705
- this.cursorToPosition( cursor, string.length );
1810
+ var string = outOfRange ? this.code.lines[ ln ] : this.code.lines[ ln ].slice( 0, ch );
1811
+
1812
+ // Move main cursor there...
1813
+ if( !e.altKey )
1814
+ {
1815
+ // Make sure we only keep the main cursor..
1816
+ this._removeSecondaryCursors();
1817
+
1818
+ this.cursorToLine( cursor, ln, true );
1819
+ this.cursorToPosition( cursor, string.length );
1820
+ }
1821
+
1822
+ // Add new cursor
1823
+ else
1824
+ {
1825
+ this._addCursor( ln, string.length );
1826
+ }
1706
1827
 
1707
1828
  this.hideAutoCompleteBox();
1708
1829
  }
1709
1830
 
1710
1831
  processSelection( e, keep_range, flags = CodeEditor.SELECTION_X_Y ) {
1711
1832
 
1712
- var cursor = this.cursors.children[ 0 ];
1833
+ var cursor = this._getCurrentCursor();
1713
1834
  const isMouseEvent = e && ( e.constructor == MouseEvent );
1714
1835
 
1715
1836
  if( isMouseEvent ) this.processClick( e );
@@ -1864,11 +1985,113 @@ class CodeEditor {
1864
1985
  }
1865
1986
 
1866
1987
  async processKey( e ) {
1988
+
1989
+ const numCursors = this.cursors.childElementCount;
1867
1990
 
1868
1991
  if( !this.code || e.srcElement.constructor != HTMLDivElement )
1992
+ return;
1993
+
1994
+ const key = e.key ?? e.detail.key;
1995
+ const target = e.detail.targetCursor;
1996
+
1997
+ if( target !== undefined )
1998
+ {
1999
+ this.processKeyAtTargetCursor( e, key, target );
2000
+ return;
2001
+ }
2002
+
2003
+ // By cursor keys
2004
+
2005
+ this._lastProcessedCursorIndex = null;
2006
+
2007
+ var lastProcessedCursor = null;
2008
+ var cursorOffset = new LX.vec2( 0, 0 );
2009
+
2010
+ for( var i = 0; i < numCursors; i++ )
2011
+ {
2012
+ let cursor = this.cursors.children[ i ];
2013
+
2014
+ // We could delete secondary cursor while iterating..
2015
+ if( !cursor )
2016
+ break;
2017
+
2018
+ // Arrows don't modify code lines.. And only add offset if in the same line
2019
+ if( lastProcessedCursor && lastProcessedCursor.line == cursor.line && !key.includes( 'Arrow' ) )
2020
+ {
2021
+ cursor.position += cursorOffset.x;
2022
+ cursor.line += cursorOffset.y;
2023
+
2024
+ this.relocateCursors();
2025
+ }
2026
+
2027
+ lastProcessedCursor = this.saveCursor( cursor );
2028
+ this._lastProcessedCursorIndex = i;
2029
+
2030
+ this._processKeyAtCursor( e, key, cursor );
2031
+
2032
+ cursorOffset.x += ( cursor.position - lastProcessedCursor.position );
2033
+ cursorOffset.y += ( cursor.line - lastProcessedCursor.line );
2034
+ }
2035
+
2036
+ // Global keys
2037
+
2038
+ this._processGlobalKeys( e, key );
2039
+
2040
+ // Clear tmp
2041
+
2042
+ delete this._lastProcessedCursorIndex;
2043
+ }
2044
+
2045
+ async processKeyAtTargetCursor( e, key, targetIdx ) {
2046
+
2047
+ let cursor = this.cursors.children[ targetIdx ];
2048
+
2049
+ // We could delete secondary cursor while iterating..
2050
+ if( !cursor )
1869
2051
  return;
1870
2052
 
1871
- var key = e.key ?? e.detail.key;
2053
+ this._processKeyAtCursor( e, key, cursor );
2054
+ this._processGlobalKeys( e, key );
2055
+ }
2056
+
2057
+ async _processGlobalKeys( e, key ) {
2058
+
2059
+ let cursor = this._getCurrentCursor();
2060
+
2061
+ if( e.ctrlKey || e.metaKey )
2062
+ {
2063
+ e.preventDefault();
2064
+
2065
+ switch( key.toLowerCase() ) {
2066
+ case 'a': // select all
2067
+ this.selectAll();
2068
+ break;
2069
+ case 'f': // find/search
2070
+ this.showSearchBox();
2071
+ break;
2072
+ case 'g': // find line
2073
+ this.showSearchLineBox();
2074
+ break;
2075
+ case 's': // save
2076
+ this.onsave( this.getText() );
2077
+ break;
2078
+ case 'y': // redo
2079
+ this._doRedo( cursor );
2080
+ break;
2081
+ case 'z': // undo
2082
+ this._doUndo( cursor );
2083
+ break;
2084
+ case '+': // increase size
2085
+ this._increaseFontSize();
2086
+ break;
2087
+ case '-': // decrease size
2088
+ this._decreaseFontSize();
2089
+ break;
2090
+ }
2091
+ }
2092
+ }
2093
+
2094
+ async _processKeyAtCursor( e, key, cursor ) {
1872
2095
 
1873
2096
  const skip_undo = e.detail.skip_undo ?? false;
1874
2097
 
@@ -1876,82 +2099,38 @@ class CodeEditor {
1876
2099
  if( key.length > 1 && this.specialKeys.indexOf( key ) == -1 )
1877
2100
  return;
1878
2101
 
1879
- let cursor = this.cursors.children[ 0 ];
1880
2102
  let lidx = cursor.line;
1881
2103
  this.code.lines[ lidx ] = this.code.lines[ lidx ] ?? "";
1882
2104
 
1883
2105
  // Check combinations
1884
2106
 
2107
+ const isLastCursor = cursor.isLast();
2108
+
1885
2109
  if( e.ctrlKey || e.metaKey )
1886
2110
  {
1887
2111
  switch( key.toLowerCase() ) {
1888
- case 'a': // select all
1889
- e.preventDefault();
1890
- this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP );
1891
- this.startSelection( cursor );
1892
- const nlines = this.code.lines.length - 1;
1893
- this.selection.toX = this.code.lines[ nlines ].length;
1894
- this.selection.toY = nlines;
1895
- this.cursorToPosition( cursor, this.selection.toX );
1896
- this.cursorToLine( cursor, this.selection.toY );
1897
- this.processSelection( null, true );
1898
- this.hideAutoCompleteBox();
1899
- break;
1900
2112
  case 'c': // copy
1901
- this._copyContent();
2113
+ // TODO: COPY TEXT FROM EVERY CURSOR
2114
+ this._copyContent( cursor );
1902
2115
  return;
1903
2116
  case 'd': // duplicate line
1904
2117
  e.preventDefault();
1905
- this.endSelection();
1906
- this.code.lines.splice( lidx, 0, this.code.lines[ lidx ] );
1907
- this.lineDown( cursor );
1908
- this.processLines();
1909
- this.hideAutoCompleteBox();
1910
- return;
1911
- case 'f': // find/search
1912
- e.preventDefault();
1913
- this.showSearchBox();
1914
- return;
1915
- case 'g': // find line
1916
- e.preventDefault();
1917
- this.showSearchLineBox();
1918
- return;
1919
- case 's': // save
1920
- e.preventDefault();
1921
- this.onsave( this.getText() );
2118
+ // TODO: UPDATE NEXT CURSOR ON MODIFY STATE
2119
+ this._duplicateLine( lidx, cursor );
1922
2120
  return;
1923
2121
  case 'v': // paste
1924
- this._pasteContent();
2122
+ this._pasteContent( cursor );
1925
2123
  return;
1926
2124
  case 'x': // cut line
1927
- this._cutContent();
2125
+ this._cutContent( cursor );
1928
2126
  this.hideAutoCompleteBox();
1929
2127
  return;
1930
- case 'y': // redo
1931
- if(!this.code.redoSteps.length)
1932
- return;
1933
- this._addUndoStep( cursor, true, false);
1934
- const redo_step = this.code.redoSteps.pop();
1935
- this.code.lines = redo_step.lines;
1936
- this.processLines();
1937
- this.restoreCursor( cursor, redo_step.cursor );
1938
- return;
1939
- case 'z': // undo
1940
- if(!this.code.undoSteps.length)
1941
- return;
1942
- this._addRedoStep( cursor );
1943
- const undo_step = this.code.undoSteps.pop();
1944
- this.code.lines = undo_step.lines;
1945
- this.processLines();
1946
- this.restoreCursor( cursor, undo_step.cursor );
1947
- return;
1948
- case '+': // increase size
1949
- e.preventDefault();
1950
- this._increaseFontSize();
1951
- return;
1952
- case '-': // decrease size
1953
- e.preventDefault();
1954
- this._decreaseFontSize();
2128
+ case 'arrowdown': // add cursor below only for the main cursor..
2129
+ if( isLastCursor && this.code.lines[ lidx + 1 ] != undefined )
2130
+ {
2131
+ var new_cursor = this._addCursor( cursor.line, cursor.position, true );
2132
+ this.lineDown( new_cursor );
2133
+ }
1955
2134
  return;
1956
2135
  }
1957
2136
  }
@@ -1962,8 +2141,9 @@ class CodeEditor {
1962
2141
  case 'ArrowUp':
1963
2142
  if(this.code.lines[ lidx - 1 ] == undefined)
1964
2143
  return;
1965
- swapArrayElements(this.code.lines, lidx - 1, lidx);
1966
- this.lineUp();
2144
+ this._addUndoStep( cursor, true );
2145
+ swapArrayElements( this.code.lines, lidx - 1, lidx );
2146
+ this.lineUp( cursor );
1967
2147
  this.processLine( lidx - 1 );
1968
2148
  this.processLine( lidx );
1969
2149
  this.hideAutoCompleteBox();
@@ -1971,8 +2151,9 @@ class CodeEditor {
1971
2151
  case 'ArrowDown':
1972
2152
  if(this.code.lines[ lidx + 1 ] == undefined)
1973
2153
  return;
1974
- swapArrayElements(this.code.lines, lidx, lidx + 1);
1975
- this.lineDown();
2154
+ this._addUndoStep( cursor, true );
2155
+ swapArrayElements( this.code.lines, lidx, lidx + 1 );
2156
+ this.lineDown( cursor );
1976
2157
  this.processLine( lidx );
1977
2158
  this.processLine( lidx + 1 );
1978
2159
  this.hideAutoCompleteBox();
@@ -1983,17 +2164,18 @@ class CodeEditor {
1983
2164
  // Apply binded actions...
1984
2165
 
1985
2166
  for( const actKey in this.actions ) {
2167
+
1986
2168
  if( key != actKey ) continue;
1987
2169
  e.preventDefault();
1988
2170
 
1989
- if(this.actions[ key ].deleteSelection && this.selection)
1990
- this.actions['Backspace'].callback(lidx, cursor, e);
2171
+ if( this.actions[ key ].deleteSelection && this.selection )
2172
+ this.actions['Backspace'].callback( lidx, cursor, e );
1991
2173
 
1992
2174
  return this.actions[ key ].callback( lidx, cursor, e );
1993
2175
  }
1994
2176
 
1995
2177
  // From now on, don't allow ctrl, shift or meta (mac) combinations
1996
- if( (e.ctrlKey || e.metaKey) )
2178
+ if( e.ctrlKey || e.metaKey )
1997
2179
  return;
1998
2180
 
1999
2181
  // Add undo steps
@@ -2005,7 +2187,7 @@ class CodeEditor {
2005
2187
 
2006
2188
  // Some custom cases for word enclosing (), {}, "", '', ...
2007
2189
 
2008
- const enclosableKeys = ["\"", "'", "(", "{"];
2190
+ const enclosableKeys = [ "\"", "'", "(", "{" ];
2009
2191
  if( enclosableKeys.indexOf( key ) > -1 )
2010
2192
  {
2011
2193
  if( this._encloseSelectedWordWithKey( key, lidx, cursor ) )
@@ -2023,19 +2205,19 @@ class CodeEditor {
2023
2205
 
2024
2206
  // Append key
2025
2207
 
2026
- const isPairKey = (Object.values( this.pairKeys ).indexOf( key ) > -1) && !this.wasKeyPaired;
2027
- const sameKeyNext = isPairKey && (this.code.lines[ lidx ][cursor.position] === key);
2208
+ const isPairKey = ( Object.values( this.pairKeys ).indexOf( key ) > -1 ) && !this.wasKeyPaired;
2209
+ const sameKeyNext = isPairKey && ( this.code.lines[ lidx ][ cursor.position ] === key );
2028
2210
 
2029
2211
  if( !sameKeyNext )
2030
2212
  {
2031
2213
  this.code.lines[ lidx ] = [
2032
- this.code.lines[ lidx ].slice(0, cursor.position),
2214
+ this.code.lines[ lidx ].slice( 0, cursor.position ),
2033
2215
  key,
2034
- this.code.lines[ lidx ].slice(cursor.position)
2216
+ this.code.lines[ lidx ].slice( cursor.position )
2035
2217
  ].join('');
2036
2218
  }
2037
2219
 
2038
- this.cursorToRight( key );
2220
+ this.cursorToRight( key, cursor );
2039
2221
 
2040
2222
  // Some custom cases for auto key pair (), {}, "", '', ...
2041
2223
 
@@ -2045,7 +2227,7 @@ class CodeEditor {
2045
2227
  // Make sure to detect later that the key is paired automatically to avoid loops...
2046
2228
  this.wasKeyPaired = true;
2047
2229
 
2048
- if(sameKeyNext) return;
2230
+ if( sameKeyNext ) return;
2049
2231
 
2050
2232
  this.root.dispatchEvent(new KeyboardEvent('keydown', { 'key': this.pairKeys[ key ] }));
2051
2233
  this.cursorToLeft( key, cursor );
@@ -2072,14 +2254,17 @@ class CodeEditor {
2072
2254
  this.showAutoCompleteBox( key, cursor );
2073
2255
  }
2074
2256
 
2075
- async _pasteContent() {
2257
+ async _pasteContent( cursor ) {
2258
+
2076
2259
  let text = await navigator.clipboard.readText();
2077
- this.appendText(text);
2260
+
2261
+ this._addUndoStep( cursor, true );
2262
+
2263
+ this.appendText( text, cursor );
2078
2264
  }
2079
2265
 
2080
- async _copyContent() {
2266
+ async _copyContent( cursor ) {
2081
2267
 
2082
- let cursor = this.cursors.children[ 0 ];
2083
2268
  let text_to_copy = "";
2084
2269
 
2085
2270
  if( !this.selection ) {
@@ -2091,27 +2276,26 @@ class CodeEditor {
2091
2276
  if( this.selection ) this.selection.invertIfNecessary();
2092
2277
 
2093
2278
  const separator = "_NEWLINE_";
2094
- let code = this.code.lines.join(separator);
2279
+ let code = this.code.lines.join( separator );
2095
2280
 
2096
2281
  // Get linear start index
2097
2282
  let index = 0;
2098
2283
 
2099
- for(let i = 0; i <= this.selection.fromY; i++)
2100
- index += (i == this.selection.fromY ? this.selection.fromX : this.code.lines[ i ].length);
2284
+ for( let i = 0; i <= this.selection.fromY; i++ )
2285
+ index += ( i == this.selection.fromY ? this.selection.fromX : this.code.lines[ i ].length );
2101
2286
 
2102
2287
  index += this.selection.fromY * separator.length;
2103
- const num_chars = this.selection.chars + (this.selection.toY - this.selection.fromY) * separator.length;
2104
- const text = code.substr(index, num_chars);
2105
- const lines = text.split(separator);
2288
+ const num_chars = this.selection.chars + ( this.selection.toY - this.selection.fromY ) * separator.length;
2289
+ const text = code.substr( index, num_chars );
2290
+ const lines = text.split( separator );
2106
2291
  text_to_copy = lines.join('\n');
2107
2292
  }
2108
2293
 
2109
2294
  navigator.clipboard.writeText( text_to_copy ).then(() => console.log("Successfully copied"), (err) => console.error("Error"));
2110
2295
  }
2111
2296
 
2112
- async _cutContent() {
2297
+ async _cutContent( cursor ) {
2113
2298
 
2114
- let cursor = this.cursors.children[ 0 ];
2115
2299
  let lidx = cursor.line;
2116
2300
  let text_to_cut = "";
2117
2301
 
@@ -2121,9 +2305,9 @@ class CodeEditor {
2121
2305
  text_to_cut = "\n" + this.code.lines[ cursor.line ];
2122
2306
  this.code.lines.splice( lidx, 1 );
2123
2307
  this.processLines();
2124
- this.resetCursorPos( CodeEditor.CURSOR_LEFT );
2308
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
2125
2309
  if( this.code.lines[ lidx ] == undefined )
2126
- this.lineUp();
2310
+ this.lineUp( cursor );
2127
2311
  }
2128
2312
  else {
2129
2313
 
@@ -2151,6 +2335,16 @@ class CodeEditor {
2151
2335
  navigator.clipboard.writeText( text_to_cut ).then(() => console.log("Successfully cut"), (err) => console.error("Error"));
2152
2336
  }
2153
2337
 
2338
+ _duplicateLine( lidx, cursor ) {
2339
+
2340
+ this.endSelection();
2341
+ this._addUndoStep( cursor, true );
2342
+ this.code.lines.splice( lidx, 0, this.code.lines[ lidx ] );
2343
+ this.lineDown( cursor );
2344
+ this.processLines();
2345
+ this.hideAutoCompleteBox();
2346
+ }
2347
+
2154
2348
  action( key, deleteSelection, fn ) {
2155
2349
 
2156
2350
  this.actions[ key ] = {
@@ -2184,8 +2378,6 @@ class CodeEditor {
2184
2378
 
2185
2379
  processLines( mode ) {
2186
2380
 
2187
- const start = performance.now();
2188
-
2189
2381
  var code_html = "";
2190
2382
 
2191
2383
  // Reset all lines content
@@ -2618,7 +2810,13 @@ class CodeEditor {
2618
2810
  }
2619
2811
 
2620
2812
  _isCSSClass( token, prev, next ) {
2621
- return this.highlight == 'CSS' && prev == '.';
2813
+
2814
+ if( this.highlight != 'CSS' )
2815
+ return false;
2816
+
2817
+ return ( prev == '.' || prev == '::'
2818
+ || ( prev == ':' && next == '{' )
2819
+ || ( token[ 0 ] == '#' && prev != ':' ) );
2622
2820
  }
2623
2821
 
2624
2822
  isNumber( token ) {
@@ -2712,8 +2910,6 @@ class CodeEditor {
2712
2910
 
2713
2911
  lineUp( cursor, resetLeft ) {
2714
2912
 
2715
- cursor = cursor ?? this.cursors.children[ 0 ];
2716
-
2717
2913
  if( this.code.lines[ cursor.line - 1 ] == undefined )
2718
2914
  return false;
2719
2915
 
@@ -2725,13 +2921,12 @@ class CodeEditor {
2725
2921
  }
2726
2922
 
2727
2923
  lineDown( cursor, resetLeft ) {
2728
-
2729
- cursor = cursor ?? this.cursors.children[ 0 ];
2730
2924
 
2731
2925
  if( this.code.lines[ cursor.line + 1 ] == undefined )
2732
2926
  return false;
2733
2927
 
2734
2928
  cursor.line++;
2929
+
2735
2930
  this.cursorToBottom( cursor, resetLeft );
2736
2931
 
2737
2932
  return true;
@@ -2804,10 +2999,32 @@ class CodeEditor {
2804
2999
  delete this._lastSelectionKeyDir;
2805
3000
  }
2806
3001
 
3002
+ selectAll() {
3003
+
3004
+ // Use main cursor
3005
+ this._removeSecondaryCursors();
3006
+
3007
+ var cursor = this._getCurrentCursor();
3008
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP, cursor );
3009
+
3010
+ this.startSelection( cursor );
3011
+
3012
+ const nlines = this.code.lines.length - 1;
3013
+ this.selection.toX = this.code.lines[ nlines ].length;
3014
+ this.selection.toY = nlines;
3015
+
3016
+ this.cursorToPosition( cursor, this.selection.toX );
3017
+ this.cursorToLine( cursor, this.selection.toY );
3018
+
3019
+ this.processSelection( null, true );
3020
+
3021
+ this.hideAutoCompleteBox();
3022
+ }
3023
+
2807
3024
  cursorToRight( key, cursor ) {
2808
3025
 
2809
3026
  if( !key ) return;
2810
- cursor = cursor ?? this.cursors.children[ 0 ];
3027
+
2811
3028
  cursor._left += this.charWidth;
2812
3029
  cursor.style.left = "calc( " + cursor._left + "px + " + this.xPadding + " )";
2813
3030
  cursor.position++;
@@ -2826,7 +3043,7 @@ class CodeEditor {
2826
3043
  cursorToLeft( key, cursor ) {
2827
3044
 
2828
3045
  if( !key ) return;
2829
- cursor = cursor ?? this.cursors.children[ 0 ];
3046
+
2830
3047
  cursor._left -= this.charWidth;
2831
3048
  cursor._left = Math.max( cursor._left, 0 );
2832
3049
  cursor.style.left = "calc( " + cursor._left + "px + " + this.xPadding + " )";
@@ -2845,13 +3062,12 @@ class CodeEditor {
2845
3062
 
2846
3063
  cursorToTop( cursor, resetLeft = false ) {
2847
3064
 
2848
- cursor = cursor ?? this.cursors.children[ 0 ];
2849
3065
  cursor._top -= this.lineHeight;
2850
3066
  cursor._top = Math.max(cursor._top, 0);
2851
3067
  cursor.style.top = "calc(" + cursor._top + "px)";
2852
3068
  this.restartBlink();
2853
3069
 
2854
- if(resetLeft)
3070
+ if( resetLeft )
2855
3071
  this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
2856
3072
 
2857
3073
  doAsync(() => {
@@ -2863,12 +3079,12 @@ class CodeEditor {
2863
3079
 
2864
3080
  cursorToBottom( cursor, resetLeft = false ) {
2865
3081
 
2866
- cursor = cursor ?? this.cursors.children[ 0 ];
2867
3082
  cursor._top += this.lineHeight;
2868
3083
  cursor.style.top = "calc(" + cursor._top + "px)";
3084
+
2869
3085
  this.restartBlink();
2870
3086
 
2871
- if(resetLeft)
3087
+ if( resetLeft )
2872
3088
  this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
2873
3089
 
2874
3090
  doAsync(() => {
@@ -2880,10 +3096,11 @@ class CodeEditor {
2880
3096
 
2881
3097
  cursorToString( cursor, text, reverse ) {
2882
3098
 
2883
- if( !text.length ) return;
2884
- cursor = cursor ?? this.cursors.children[ 0 ];
3099
+ if( !text.length )
3100
+ return;
3101
+
2885
3102
  for( let char of text )
2886
- reverse ? this.cursorToLeft( char ) : this.cursorToRight( char );
3103
+ reverse ? this.cursorToLeft( char, cursor ) : this.cursorToRight( char, cursor );
2887
3104
  }
2888
3105
 
2889
3106
  cursorToPosition( cursor, position ) {
@@ -2903,29 +3120,57 @@ class CodeEditor {
2903
3120
 
2904
3121
  saveCursor( cursor, state = {} ) {
2905
3122
 
2906
- var cursor = cursor ?? this.cursors.children[ 0 ];
2907
- state.top = cursor._top;
2908
- state.left = cursor._left;
2909
- state.line = cursor.line;
2910
3123
  state.position = cursor.position;
3124
+ state.line = cursor.line;
2911
3125
  return state;
2912
3126
  }
2913
3127
 
3128
+ saveCursors() {
3129
+
3130
+ var cursors = [];
3131
+
3132
+ for( let cursor of this.cursors.children )
3133
+ {
3134
+ cursors.push( this.saveCursor( cursor ) );
3135
+ }
3136
+
3137
+ return cursors;
3138
+ }
3139
+
3140
+ relocateCursors() {
3141
+
3142
+ for( let cursor of this.cursors.children )
3143
+ {
3144
+ cursor._left = cursor.position * this.charWidth;
3145
+ cursor.style.left = "calc(" + cursor._left + "px + " + this.xPadding + ")";
3146
+ cursor._top = cursor.line * this.lineHeight;
3147
+ cursor.style.top = "calc(" + cursor._top + "px)";
3148
+ }
3149
+ }
3150
+
3151
+ mergeCursors( line ) {
3152
+
3153
+ console.assert( line >= 0 );
3154
+ const cursorsInLine = Array.from( this.cursors.children ).filter( v => v.line == line );
3155
+
3156
+ while( cursorsInLine.length > 1 )
3157
+ cursorsInLine.pop().remove();
3158
+ }
3159
+
2914
3160
  restoreCursor( cursor, state ) {
2915
3161
 
2916
- cursor = cursor ?? this.cursors.children[ 0 ];
2917
- cursor.line = state.line ?? 0;
2918
3162
  cursor.position = state.position ?? 0;
3163
+ cursor.line = state.line ?? 0;
2919
3164
 
2920
- cursor._left = state.left ?? 0;
3165
+ cursor._left = cursor.position * this.charWidth;
2921
3166
  cursor.style.left = "calc(" + cursor._left + "px + " + this.xPadding + ")";
2922
- cursor._top = state.top ?? 0;
3167
+ cursor._top = cursor.line * this.lineHeight;
2923
3168
  cursor.style.top = "calc(" + cursor._top + "px)";
2924
3169
  }
2925
3170
 
2926
3171
  resetCursorPos( flag, cursor ) {
2927
3172
 
2928
- cursor = cursor ?? this.cursors.children[ 0 ];
3173
+ cursor = cursor ?? this._getCurrentCursor();
2929
3174
 
2930
3175
  if( flag & CodeEditor.CURSOR_LEFT )
2931
3176
  {
@@ -2942,19 +3187,20 @@ class CodeEditor {
2942
3187
  }
2943
3188
  }
2944
3189
 
2945
- addSpaceTabs(n) {
3190
+ addSpaceTabs( n ) {
2946
3191
 
2947
3192
  for( var i = 0; i < n; ++i ) {
2948
3193
  this.actions[ 'Tab' ].callback();
2949
3194
  }
2950
3195
  }
2951
3196
 
2952
- addSpaces(n) {
3197
+ addSpaces( n ) {
2953
3198
 
2954
3199
  for( var i = 0; i < n; ++i ) {
2955
3200
  this.root.dispatchEvent( new CustomEvent( 'keydown', { 'detail': {
2956
3201
  skip_undo: true,
2957
- key: ' '
3202
+ key: ' ',
3203
+ targetCursor: this._lastProcessedCursorIndex
2958
3204
  }}));
2959
3205
  }
2960
3206
  }
@@ -3109,14 +3355,11 @@ class CodeEditor {
3109
3355
 
3110
3356
  getCharAtPos( cursor, offset = 0 ) {
3111
3357
 
3112
- cursor = cursor ?? this.cursors.children[ 0 ];
3113
3358
  return this.code.lines[ cursor.line ][ cursor.position + offset ];
3114
3359
  }
3115
3360
 
3116
- getWordAtPos( cursor, loffset = 0, roffset ) {
3361
+ getWordAtPos( cursor, offset = 0 ) {
3117
3362
 
3118
- roffset = roffset ?? loffset;
3119
- cursor = cursor ?? this.cursors.children[ 0 ];
3120
3363
  const col = cursor.line;
3121
3364
  const words = this.code.lines[ col ];
3122
3365
 
@@ -3126,8 +3369,8 @@ class CodeEditor {
3126
3369
  return (exceptions.indexOf( char ) > - 1) || (code > 47 && code < 58) || (code > 64 && code < 91) || (code > 96 && code < 123);
3127
3370
  }
3128
3371
 
3129
- let from = cursor.position + roffset;
3130
- let to = cursor.position + loffset;
3372
+ let from = cursor.position + offset;
3373
+ let to = cursor.position + offset;
3131
3374
 
3132
3375
  // Check left ...
3133
3376
 
@@ -3146,7 +3389,7 @@ class CodeEditor {
3146
3389
  let word = words.substring( from, to );
3147
3390
  if( word == ' ' )
3148
3391
  {
3149
- if( loffset < 0 )
3392
+ if( offset < 0 )
3150
3393
  {
3151
3394
  while( words[ from - 1 ] != undefined && words[ from - 1 ] == ' ' )
3152
3395
  from--;
@@ -3319,8 +3562,10 @@ class CodeEditor {
3319
3562
 
3320
3563
  hideAutoCompleteBox() {
3321
3564
 
3565
+ const isActive = this.isAutoCompleteActive;
3322
3566
  this.isAutoCompleteActive = false;
3323
3567
  this.autocomplete.classList.remove( 'show' );
3568
+ return isActive != this.isAutoCompleteActive;
3324
3569
  }
3325
3570
 
3326
3571
  autoCompleteWord( cursor, suggestion ) {
@@ -3521,12 +3766,21 @@ class CodeEditor {
3521
3766
  return;
3522
3767
 
3523
3768
  this.codeScroller.scrollTo( 0, Math.max( line - 15 ) * this.lineHeight );
3769
+
3770
+ // Select line ?
3771
+ var cursor = this._getCurrentCursor( true );
3772
+ this.cursorToLine( cursor, line - 1, true );
3524
3773
  }
3525
3774
 
3526
3775
  _updateDataInfoPanel( signal, value ) {
3527
3776
 
3528
3777
  if( !this.skipCodeInfo )
3529
3778
  {
3779
+ if( this.cursors.childElementCount > 1 )
3780
+ {
3781
+ value = "";
3782
+ }
3783
+
3530
3784
  LX.emit( signal, value );
3531
3785
  }
3532
3786
  }
@@ -3541,7 +3795,7 @@ class CodeEditor {
3541
3795
  let line = this.code.childNodes[ old_local ];
3542
3796
 
3543
3797
  if( !line )
3544
- return;
3798
+ return;
3545
3799
 
3546
3800
  line.classList.remove( 'active-line' );
3547
3801
 
@@ -3572,7 +3826,13 @@ class CodeEditor {
3572
3826
  r.style.setProperty( "--code-editor-row-height", row_pixels + "px" );
3573
3827
  this.lineHeight = row_pixels;
3574
3828
 
3575
- this.processLines(); // ... it's necessary?
3829
+ // Relocate cursors
3830
+
3831
+ this.relocateCursors();
3832
+
3833
+ // Resize the code area
3834
+
3835
+ this.processLines();
3576
3836
  }
3577
3837
 
3578
3838
  _decreaseFontSize() {
@@ -3592,7 +3852,13 @@ class CodeEditor {
3592
3852
  r.style.setProperty( "--code-editor-row-height", row_pixels + "px" );
3593
3853
  this.lineHeight = row_pixels;
3594
3854
 
3595
- this.processLines(); // ... it's necessary?
3855
+ // Relocate cursors
3856
+
3857
+ this.relocateCursors();
3858
+
3859
+ // Resize the code area
3860
+
3861
+ this.processLines();
3596
3862
  }
3597
3863
 
3598
3864
  _clearTmpVariables() {