lexgui 0.7.12 → 0.7.14

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.
@@ -356,6 +356,7 @@ class CodeEditor
356
356
  this.highlight = options.highlight ?? 'Plain Text';
357
357
  this.newTabOptions = options.newTabOptions;
358
358
  this.customSuggestions = options.customSuggestions ?? [];
359
+ this.explorerName = options.explorerName ?? "EXPLORER";
359
360
 
360
361
  // Editor callbacks
361
362
  this.onSave = options.onSave ?? options.onsave; // LEGACY onsave
@@ -375,45 +376,43 @@ class CodeEditor
375
376
 
376
377
  let panel = new LX.Panel();
377
378
 
378
- panel.addTitle( options.explorerName ?? "EXPLORER" );
379
+ panel.addTitle( this.explorerName );
379
380
 
380
- let sceneData = {
381
- 'id': 'WORKSPACE',
382
- 'skipVisibility': true,
383
- 'children': []
384
- };
381
+ let sceneData = [];
385
382
 
386
383
  this.explorer = panel.addTree( null, sceneData, {
387
384
  filter: false,
388
385
  rename: false,
389
386
  skipDefaultIcon: true,
390
- onevent: (event) => {
391
- switch(event.type) {
392
- // case LX.TreeEvent.NODE_SELECTED:
393
- // if( !this.tabs.tabDOMs[ event.node.id ] ) break;
394
- case LX.TreeEvent.NODE_DBLCLICKED:
395
- this.loadTab( event.node.id );
396
- break;
397
- case LX.TreeEvent.NODE_DELETED:
398
- this.closeTab( event.node.id );
399
- break;
400
- // case LX.TreeEvent.NODE_CONTEXTMENU:
401
- // LX.addContextMenu( event.multiple ? "Selected Nodes" : event.node.id, event.value, m => {
402
- //
403
- // });
404
- // break;
405
- // case LX.TreeEvent.NODE_DRAGGED:
406
- // console.log(event.node.id + " is now child of " + event.value.id);
407
- // break;
387
+ onevent: (event) =>
388
+ {
389
+ switch( event.type )
390
+ {
391
+ // case LX.TreeEvent.NODE_SELECTED:
392
+ // if( !this.tabs.tabDOMs[ event.node.id ] ) break;
393
+ case LX.TreeEvent.NODE_DBLCLICKED:
394
+ this.loadTab( event.node.id );
395
+ break;
396
+ case LX.TreeEvent.NODE_DELETED:
397
+ this.closeTab( event.node.id );
398
+ break;
399
+ // case LX.TreeEvent.NODE_CONTEXTMENU:
400
+ // LX.addContextMenu( event.multiple ? "Selected Nodes" : event.node.id, event.value, m => {
401
+ //
402
+ // });
403
+ // break;
404
+ // case LX.TreeEvent.NODE_DRAGGED:
405
+ // console.log(event.node.id + " is now child of " + event.value.id);
406
+ // break;
408
407
  }
409
408
  }
410
409
  });
411
410
 
412
411
  this.addExplorerItem = function( item )
413
412
  {
414
- if( !this.explorer.innerTree.data.children.find( ( value, index ) => value.id === item.id ) )
413
+ if( !this.explorer.innerTree.data.find( ( value, index ) => value.id === item.id ) )
415
414
  {
416
- this.explorer.innerTree.data.children.push( item );
415
+ this.explorer.innerTree.data.push( item );
417
416
  }
418
417
  };
419
418
 
@@ -487,6 +486,17 @@ class CodeEditor
487
486
 
488
487
  this.codeArea.root.classList.add( 'lexcodearea' );
489
488
 
489
+ const codeResizeObserver = new ResizeObserver( entries => {
490
+
491
+ if( !this.code )
492
+ {
493
+ return;
494
+ }
495
+
496
+ this.resize();
497
+ });
498
+ codeResizeObserver.observe( this.codeArea.root );
499
+
490
500
  // Full editor
491
501
  area.root.classList.add('codebasearea');
492
502
 
@@ -894,11 +904,13 @@ class CodeEditor
894
904
  }
895
905
  else
896
906
  {
897
- const indentSpaces = this.tabSpaces - (cursor.position % this.tabSpaces);
898
- this._addSpaces( indentSpaces );
907
+ const indentSpaces = this.tabSpaces - ( cursor.position % this.tabSpaces );
908
+ this._addSpaces( cursor, indentSpaces );
899
909
  }
900
910
  }
901
- }, "shiftKey");
911
+ }, ( cursor, e ) => {
912
+ return e.shiftKey || ( cursor.selection && !cursor.selection.sameLine() );
913
+ });
902
914
 
903
915
  this.action( 'Home', false, ( ln, cursor, e ) => {
904
916
 
@@ -910,7 +922,7 @@ class CodeEditor
910
922
  const prestring = this.code.lines[ ln ].substring( 0, idx );
911
923
  let lastX = cursor.position;
912
924
 
913
- this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
925
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor, true );
914
926
  if( idx > 0 )
915
927
  {
916
928
  this.cursorToString( cursor, prestring );
@@ -921,7 +933,6 @@ class CodeEditor
921
933
  idx = 0;
922
934
  }
923
935
 
924
- this.setScrollLeft( 0 );
925
936
  this.mergeCursors( ln );
926
937
 
927
938
  if( e.shiftKey && !e.cancelShift )
@@ -946,14 +957,17 @@ class CodeEditor
946
957
  {
947
958
  this._processSelection( cursor, e );
948
959
  }
949
- } else if( !e.keepSelection )
960
+ }
961
+ else if( !e.keepSelection )
962
+ {
950
963
  this.endSelection();
964
+ }
951
965
  });
952
966
 
953
- this.action( 'End', false, ( ln, cursor, e ) => {
954
-
955
- if( ( e.shiftKey || e._shiftKey ) && !e.cancelShift ) {
956
-
967
+ this.action( 'End', false, ( ln, cursor, e ) =>
968
+ {
969
+ if( ( e.shiftKey || e._shiftKey ) && !e.cancelShift )
970
+ {
957
971
  var string = this.code.lines[ ln ].substring( cursor.position );
958
972
  if( !cursor.selection )
959
973
  this.startSelection( cursor );
@@ -1072,11 +1086,14 @@ class CodeEditor
1072
1086
  const letter = this.getCharAtPos( cursor );
1073
1087
 
1074
1088
  // Go to end of line if out of range
1075
- if( !letter || !canGoDown ) {
1076
- this.cursorToPosition( cursor, Math.max(this.code.lines[ cursor.line ].length, 0) );
1089
+ if( !letter || !canGoDown )
1090
+ {
1091
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
1092
+ this.cursorToRight( this.code.lines[ cursor.line ], cursor );
1077
1093
  }
1078
1094
 
1079
- if( e.shiftKey ) {
1095
+ if( e.shiftKey )
1096
+ {
1080
1097
  this._processSelection( cursor, e );
1081
1098
  }
1082
1099
  }
@@ -1142,8 +1159,8 @@ class CodeEditor
1142
1159
  else {
1143
1160
  cursor.selection.invertIfNecessary();
1144
1161
  this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, cursor );
1145
- this.cursorToLine( cursor, cursor.selection.fromY, true );
1146
- this.cursorToPosition( cursor, cursor.selection.fromX );
1162
+ this.cursorToLine( cursor, cursor.selection.fromY );
1163
+ this.cursorToPosition( cursor, cursor.selection.fromX, true );
1147
1164
  this.endSelection();
1148
1165
  }
1149
1166
  }
@@ -1225,7 +1242,7 @@ class CodeEditor
1225
1242
  cursor.selection.invertIfNecessary();
1226
1243
  this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, cursor );
1227
1244
  this.cursorToLine( cursor, cursor.selection.toY );
1228
- this.cursorToPosition( cursor, cursor.selection.toX );
1245
+ this.cursorToPosition( cursor, cursor.selection.toX, true );
1229
1246
  this.endSelection( cursor );
1230
1247
  }
1231
1248
  }
@@ -1256,8 +1273,8 @@ class CodeEditor
1256
1273
  this.loadedTabs = { };
1257
1274
  this.openedTabs = { };
1258
1275
 
1259
- const onLoadAll = () => {
1260
-
1276
+ const onLoadAll = async () =>
1277
+ {
1261
1278
  // Create inspector panel when the initial state is complete
1262
1279
  // and we have at least 1 tab opened
1263
1280
  this.statusPanel = this._createStatusPanel( options );
@@ -1266,44 +1283,46 @@ class CodeEditor
1266
1283
  area.attach( this.statusPanel );
1267
1284
  }
1268
1285
 
1269
- // Wait until the fonts are all loaded
1270
- document.fonts.ready.then(() => {
1271
- // Load any font size from local storage
1272
- const savedFontSize = window.localStorage.getItem( "lexcodeeditor-font-size" );
1273
- if( savedFontSize )
1274
- {
1275
- this._setFontSize( parseInt( savedFontSize ) );
1276
- }
1277
- else // Use default size
1278
- {
1279
- const r = document.querySelector( ':root' );
1280
- const s = getComputedStyle( r );
1281
- this.fontSize = parseInt( s.getPropertyValue( "--code-editor-font-size" ) );
1282
- this.charWidth = this._measureChar( "a", true );
1283
- this.processLines();
1284
- }
1286
+ if( document.fonts.status == "loading" )
1287
+ {
1288
+ await document.fonts.ready;
1289
+ }
1285
1290
 
1286
- LX.emit( "@font-size", this.fontSize );
1291
+ // Load any font size from local storage
1292
+ const savedFontSize = window.localStorage.getItem( "lexcodeeditor-font-size" );
1293
+ if( savedFontSize )
1294
+ {
1295
+ this._setFontSize( parseInt( savedFontSize ) );
1296
+ }
1297
+ else // Use default size
1298
+ {
1299
+ const r = document.querySelector( ':root' );
1300
+ const s = getComputedStyle( r );
1301
+ this.fontSize = parseInt( s.getPropertyValue( "--code-editor-font-size" ) );
1302
+ this.charWidth = this._measureChar();
1303
+ this.processLines();
1304
+ }
1287
1305
 
1288
- // Get final sizes for editor elements based on Tabs and status bar offsets
1289
- LX.doAsync( () => {
1290
- this._verticalTopOffset = this.tabs?.root.getBoundingClientRect().height ?? 0;
1291
- this._verticalBottomOffset = this.statusPanel?.root.getBoundingClientRect().height ?? 0;
1292
- this._fullVerticalOffset = this._verticalTopOffset + this._verticalBottomOffset;
1306
+ LX.emit( "@font-size", this.fontSize );
1293
1307
 
1294
- this.gutter.style.marginTop = `${ this._verticalTopOffset }px`;
1295
- this.gutter.style.height = `calc(100% - ${ this._fullVerticalOffset }px)`;
1296
- this.vScrollbar.root.style.marginTop = `${ this._verticalTopOffset }px`;
1297
- this.vScrollbar.root.style.height = `calc(100% - ${ this._fullVerticalOffset }px)`;
1298
- this.hScrollbar.root.style.bottom = `${ this._verticalBottomOffset }px`;
1299
- this.codeArea.root.style.height = `calc(100% - ${ this._fullVerticalOffset }px)`;
1300
- }, 50 );
1308
+ // Get final sizes for editor elements based on Tabs and status bar offsets
1309
+ LX.doAsync( () => {
1310
+ this._verticalTopOffset = this.tabs?.root.getBoundingClientRect().height ?? 0;
1311
+ this._verticalBottomOffset = this.statusPanel?.root.getBoundingClientRect().height ?? 0;
1312
+ this._fullVerticalOffset = this._verticalTopOffset + this._verticalBottomOffset;
1301
1313
 
1302
- if( options.callback )
1303
- {
1304
- options.callback.call( this, this );
1305
- }
1306
- });
1314
+ this.gutter.style.marginTop = `${ this._verticalTopOffset }px`;
1315
+ this.gutter.style.height = `calc(100% - ${ this._fullVerticalOffset }px)`;
1316
+ this.vScrollbar.root.style.marginTop = `${ this._verticalTopOffset }px`;
1317
+ this.vScrollbar.root.style.height = `calc(100% - ${ this._fullVerticalOffset }px)`;
1318
+ this.hScrollbar.root.style.bottom = `${ this._verticalBottomOffset }px`;
1319
+ this.codeArea.root.style.height = `calc(100% - ${ this._fullVerticalOffset }px)`;
1320
+ }, 50 );
1321
+
1322
+ if( options.callback )
1323
+ {
1324
+ options.callback.call( this, this );
1325
+ }
1307
1326
 
1308
1327
  window.editor = this;
1309
1328
  };
@@ -1339,13 +1358,13 @@ class CodeEditor
1339
1358
  }});
1340
1359
  }
1341
1360
  }
1342
- else if( options.defaultTab ?? true )
1343
- {
1344
- this.addTab( options.name || "untitled", true, options.title, { language: options.highlight ?? "Plain Text" } );
1345
- onLoadAll();
1346
- }
1347
1361
  else
1348
1362
  {
1363
+ if( options.defaultTab ?? true )
1364
+ {
1365
+ this.addTab( options.name || "untitled", true, options.title, { language: options.highlight ?? "Plain Text" } );
1366
+ }
1367
+
1349
1368
  onLoadAll();
1350
1369
  }
1351
1370
  }
@@ -1400,7 +1419,7 @@ class CodeEditor
1400
1419
  let lastLine = newLines.pop();
1401
1420
 
1402
1421
  this.cursorToLine( cursor, newLines.length ); // Already substracted 1
1403
- this.cursorToPosition( cursor, lastLine.length );
1422
+ this.cursorToPosition( cursor, lastLine.length, true );
1404
1423
 
1405
1424
  this.mustProcessLines = true;
1406
1425
 
@@ -1724,7 +1743,7 @@ class CodeEditor
1724
1743
  // Update explorer icon
1725
1744
  if( this.useFileExplorer )
1726
1745
  {
1727
- const item = this.explorer.innerTree.data.children.filter( (v) => v.id === this.code.tabName )[ 0 ];
1746
+ const item = this.explorer.innerTree.data.filter( (v) => v.id === this.code.tabName )[ 0 ];
1728
1747
  console.assert( item != undefined );
1729
1748
  item.icon = icon;
1730
1749
  this.explorer.innerTree.frefresh( this.code.tabName );
@@ -1810,7 +1829,8 @@ class CodeEditor
1810
1829
  for( const lang of Object.keys( CodeEditor.languages ) )
1811
1830
  {
1812
1831
  m.add( lang, v => {
1813
- this._changeLanguage( v, null, true )
1832
+ this._changeLanguage( v, null, true );
1833
+ this.processLines();
1814
1834
  } );
1815
1835
  }
1816
1836
  });
@@ -2075,7 +2095,7 @@ class CodeEditor
2075
2095
  });
2076
2096
  code.addEventListener( 'dragleave', function(e) {
2077
2097
  e.preventDefault();
2078
- this.parentElement.remove( 'dragging' );
2098
+ this.parentElement.classList.remove( 'dragging' );
2079
2099
  });
2080
2100
  code.addEventListener( 'drop', e => {
2081
2101
  e.preventDefault();
@@ -2133,7 +2153,7 @@ class CodeEditor
2133
2153
 
2134
2154
  if( selected )
2135
2155
  {
2136
- this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP );
2156
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, undefined, true );
2137
2157
  this.mustProcessLines = true;
2138
2158
  }
2139
2159
  else
@@ -2201,7 +2221,7 @@ class CodeEditor
2201
2221
  this.code = code;
2202
2222
  this.mustProcessLines = true;
2203
2223
 
2204
- this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP );
2224
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, undefined, true );
2205
2225
  this.processLines();
2206
2226
  this._changeLanguageFromExtension( LX.getExtension( name ) );
2207
2227
  this._processLinesIfNecessary();
@@ -2246,6 +2266,9 @@ class CodeEditor
2246
2266
  return;
2247
2267
  }
2248
2268
 
2269
+ // Reset visibility
2270
+ code.style.display = "block";
2271
+
2249
2272
  this.openedTabs[ name ] = code;
2250
2273
 
2251
2274
  const isNewTabButton = ( name === '+' );
@@ -2268,10 +2291,11 @@ class CodeEditor
2268
2291
 
2269
2292
  // Select as current...
2270
2293
  this.code = code;
2271
- this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP );
2272
- this.processLines();
2294
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, undefined, true );
2273
2295
  this._changeLanguageFromExtension( LX.getExtension( name ) );
2274
2296
  this._updateDataInfoPanel( "@tab-name", code.tabName );
2297
+
2298
+ this.processLines();
2275
2299
  }
2276
2300
 
2277
2301
  closeTab( name, eraseAll )
@@ -2355,8 +2379,8 @@ class CodeEditor
2355
2379
  }
2356
2380
  }
2357
2381
 
2382
+ this._mouseDown = true;
2358
2383
  this.lastMouseDown = LX.getTime();
2359
- this.state.selectingText = true;
2360
2384
  this.endSelection();
2361
2385
  this.processClick( e );
2362
2386
  }
@@ -2368,8 +2392,11 @@ class CodeEditor
2368
2392
 
2369
2393
  else if( e.type == 'mousemove' )
2370
2394
  {
2371
- if( this.state.selectingText )
2395
+ if( this._mouseDown )
2396
+ {
2397
+ this.state.selectingText = true;
2372
2398
  this.processSelections( e );
2399
+ }
2373
2400
  }
2374
2401
 
2375
2402
  else if ( e.type == 'click' ) // trip
@@ -2388,7 +2415,7 @@ class CodeEditor
2388
2415
  case LX.MOUSE_TRIPLE_CLICK:
2389
2416
  this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
2390
2417
  e._shiftKey = true;
2391
- this.actions['End'].callback(cursor.line, cursor, e);
2418
+ this.actions['End'].callback( cursor.line, cursor, e );
2392
2419
  this._tripleClickSelection = true;
2393
2420
  break;
2394
2421
  }
@@ -2477,8 +2504,16 @@ class CodeEditor
2477
2504
  if( cursor.selection )
2478
2505
  {
2479
2506
  cursor.selection.invertIfNecessary();
2507
+
2508
+ // If single line selection and no chars selected, remove selection
2509
+ const deltaY = cursor.selection.toY - cursor.selection.fromY;
2510
+ if( cursor.selection.chars == 0 && deltaY == 0 )
2511
+ {
2512
+ this.endSelection();
2513
+ }
2480
2514
  }
2481
2515
 
2516
+ this._mouseDown = false;
2482
2517
  this.state.selectingText = false;
2483
2518
  delete this._lastSelectionKeyDir;
2484
2519
  }
@@ -2502,7 +2537,7 @@ class CodeEditor
2502
2537
  {
2503
2538
  // Make sure we only keep the main cursor..
2504
2539
  this._removeSecondaryCursors();
2505
- this.cursorToLine( cursor, ln, true );
2540
+ this.cursorToLine( cursor, ln );
2506
2541
  this.cursorToPosition( cursor, string.length );
2507
2542
  }
2508
2543
 
@@ -2641,7 +2676,7 @@ class CodeEditor
2641
2676
 
2642
2677
  if( isVisible )
2643
2678
  {
2644
- domEl.style.width = ( stringWidth || 8 ) + "px";
2679
+ domEl.style.width = ( stringWidth || ( deltaY == 0 ? 0 : 8 ) ) + "px";
2645
2680
  domEl._top = i * this.lineHeight;
2646
2681
  domEl.style.top = domEl._top + "px";
2647
2682
  }
@@ -2760,10 +2795,15 @@ class CodeEditor
2760
2795
 
2761
2796
  cursorOffset.x += ( cursor.position - lastProcessedCursor.position );
2762
2797
  cursorOffset.y += ( cursor.line - lastProcessedCursor.line );
2798
+
2799
+ // Set active line in case it's blurred
2800
+ if( !cursor.selection )
2801
+ {
2802
+ cursor.line = cursor.line;
2803
+ }
2763
2804
  }
2764
2805
 
2765
2806
  // Clear tmp
2766
-
2767
2807
  delete this._lastProcessedCursorIndex;
2768
2808
  }
2769
2809
 
@@ -2865,6 +2905,7 @@ class CodeEditor
2865
2905
  async _processKeyAtCursor( e, key, cursor )
2866
2906
  {
2867
2907
  const skipUndo = e.detail.skipUndo ?? false;
2908
+ const skipDeleteSelection = e.detail.skipDeleteSelection ?? false;
2868
2909
 
2869
2910
  // keys with length > 1 are probably special keys
2870
2911
  if( key.length > 1 && this.specialKeys.indexOf( key ) == -1 )
@@ -2902,33 +2943,19 @@ class CodeEditor
2902
2943
  }
2903
2944
  }
2904
2945
  }
2905
-
2906
2946
  else if( e.altKey )
2907
2947
  {
2908
- switch( key ) {
2909
- case 'd': // duplicate line
2948
+ switch( key )
2949
+ {
2950
+ case 'd': // duplicate line
2910
2951
  e.preventDefault();
2911
2952
  this._duplicateLine( lidx, cursor );
2912
2953
  return;
2913
- case 'ArrowUp':
2914
- if(this.code.lines[ lidx - 1 ] == undefined)
2915
- return;
2916
- this._addUndoStep( cursor, true );
2917
- swapArrayElements( this.code.lines, lidx - 1, lidx );
2918
- this.lineUp( cursor );
2919
- this.processLine( lidx - 1 );
2920
- this.processLine( lidx );
2921
- this.hideAutoCompleteBox();
2954
+ case 'ArrowUp':
2955
+ this.swapLines( lidx, -1, cursor );
2922
2956
  return;
2923
2957
  case 'ArrowDown':
2924
- if(this.code.lines[ lidx + 1 ] == undefined)
2925
- return;
2926
- this._addUndoStep( cursor, true );
2927
- swapArrayElements( this.code.lines, lidx, lidx + 1 );
2928
- this.lineDown( cursor );
2929
- this.processLine( lidx );
2930
- this.processLine( lidx + 1 );
2931
- this.hideAutoCompleteBox();
2958
+ this.swapLines( lidx, 1, cursor );
2932
2959
  return;
2933
2960
  }
2934
2961
  }
@@ -2943,7 +2970,9 @@ class CodeEditor
2943
2970
  e.preventDefault();
2944
2971
 
2945
2972
  if( this._actionMustDelete( cursor, this.actions[ key ], e ) )
2973
+ {
2946
2974
  this.actions['Backspace'].callback( lidx, cursor, e );
2975
+ }
2947
2976
 
2948
2977
  return this.actions[ key ].callback( lidx, cursor, e );
2949
2978
  }
@@ -2971,7 +3000,7 @@ class CodeEditor
2971
3000
  // Until this point, if there was a selection, we need
2972
3001
  // to delete the content..
2973
3002
 
2974
- if( cursor.selection )
3003
+ if( cursor.selection && !skipDeleteSelection )
2975
3004
  {
2976
3005
  this.actions['Backspace'].callback( lidx, cursor, e );
2977
3006
  lidx = cursor.line;
@@ -3305,20 +3334,20 @@ class CodeEditor
3305
3334
  }
3306
3335
  }
3307
3336
 
3308
- action( key, deleteSelection, fn, eventSkipDelete )
3337
+ action( key, deleteSelection, fn, eventSkipDeleteFn )
3309
3338
  {
3310
3339
  this.actions[ key ] = {
3311
3340
  "key": key,
3312
3341
  "callback": fn,
3313
3342
  "deleteSelection": deleteSelection,
3314
- "eventSkipDelete": eventSkipDelete
3343
+ "eventSkipDeleteFn": eventSkipDeleteFn
3315
3344
  };
3316
3345
  }
3317
3346
 
3318
3347
  _actionMustDelete( cursor, action, e )
3319
3348
  {
3320
3349
  return cursor.selection && action.deleteSelection &&
3321
- ( action.eventSkipDelete ? !e[ action.eventSkipDelete ] : true );
3350
+ !( action.eventSkipDeleteFn ? action.eventSkipDeleteFn( cursor, e ) : false );
3322
3351
  }
3323
3352
 
3324
3353
  scanWordSuggestions()
@@ -4077,7 +4106,7 @@ class CodeEditor
4077
4106
  charCounter += t.length;
4078
4107
  };
4079
4108
 
4080
- let iter = lineString.matchAll(/(<!--|-->|\*\/|\/\*|::|[\[\](){}<>.,;:*"'%@$!/= ])/g);
4109
+ let iter = lineString.matchAll(/(<!--|-->|\*\/|\/\*|::|[\[\](){}<>.,;:*"'`%@$!/= ])/g);
4081
4110
  let subtokens = iter.next();
4082
4111
  if( subtokens.value )
4083
4112
  {
@@ -4237,13 +4266,29 @@ class CodeEditor
4237
4266
  usePreviousTokenToCheckString = true;
4238
4267
  customStringKeys['@['] = ']';
4239
4268
  }
4269
+ else if( highlight == 'javascript' || highlight == 'typescript' )
4270
+ {
4271
+ customStringKeys["@`"] = "`";
4272
+ }
4240
4273
 
4241
4274
  // Manage strings
4242
4275
  this._stringEnded = false;
4243
4276
 
4244
4277
  if( usePreviousTokenToCheckString || ( !inBlockComment && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) ) )
4245
4278
  {
4246
- const _checkIfStringEnded = t => {
4279
+ const _checkIfStringEnded = ( t ) => {
4280
+
4281
+ if( this._stringInterpolation )
4282
+ {
4283
+ if( token == "$" && next == "{" )
4284
+ {
4285
+ delete this._stringInterpolation;
4286
+ this._stringInterpolationOpened = true;
4287
+ this._stringEnded = true;
4288
+ return;
4289
+ }
4290
+ }
4291
+
4247
4292
  const idx = Object.values( customStringKeys ).indexOf( t );
4248
4293
  this._stringEnded = (idx > -1) && (idx == Object.values(customStringKeys).indexOf( customStringKeys[ '@' + this._buildingString ] ));
4249
4294
  };
@@ -4257,12 +4302,23 @@ class CodeEditor
4257
4302
  // Start new string
4258
4303
  this._buildingString = ( usePreviousTokenToCheckString ? ctxData.prevWithSpaces : token );
4259
4304
 
4305
+ if( ( highlight == 'javascript' || highlight == 'typescript' ) && token == "`" )
4306
+ {
4307
+ this._stringInterpolation = true;
4308
+ }
4309
+
4260
4310
  // Check if string ended in same token using next...
4261
4311
  if( usePreviousTokenToCheckString )
4262
4312
  {
4263
4313
  _checkIfStringEnded( ctxData.nextWithSpaces );
4264
4314
  }
4265
4315
  }
4316
+ else if( this._stringInterpolationOpened && prev == "}" )
4317
+ {
4318
+ delete this._stringInterpolationOpened;
4319
+ this._stringInterpolation = true;
4320
+ this._buildingString = "`";
4321
+ }
4266
4322
  }
4267
4323
 
4268
4324
  // Update context data for next tests
@@ -4284,6 +4340,16 @@ class CodeEditor
4284
4340
  // Get highlighting class based on language common and specific rules
4285
4341
  let tokenClass = this._getTokenHighlighting( ctxData, highlight );
4286
4342
 
4343
+ if( this._stringInterpolationOpened && this._pendingString )
4344
+ {
4345
+ this._pendingString = this._pendingString.substring( 0, this._pendingString.indexOf( "$" ) );
4346
+
4347
+ if( ctxData.tokens[ tokenIndex + 1 ] == "{" )
4348
+ {
4349
+ ctxData.tokens[ tokenIndex + 1 ] = "${";
4350
+ }
4351
+ }
4352
+
4287
4353
  // We finished constructing a string
4288
4354
  if( this._buildingString && ( this._stringEnded || isLastToken ) && !inBlockComment )
4289
4355
  {
@@ -4502,35 +4568,43 @@ class CodeEditor
4502
4568
  const tokenSet = new Set( this._getTokensFromLine( text, true ) );
4503
4569
  const scores = {};
4504
4570
 
4505
- for( let [ lang, wordList ] of Object.entries( CodeEditor.keywords ) )
4506
- {
4507
- scores[ lang ] = 0;
4508
- for( let kw of wordList )
4509
- if( tokenSet.has( kw ) ) scores[ lang ]++;
4510
- }
4511
-
4512
- for( let [ lang, wordList ] of Object.entries( CodeEditor.statements ) )
4513
- {
4514
- for( let kw of wordList )
4515
- if( tokenSet.has( kw ) ) scores[ lang ]++;
4516
- }
4571
+ // Check strong indicators first
4572
+ const strongIndicators = {
4573
+ "JavaScript": ["import ", "export default", "console.", "=>", "document.", "window."],
4574
+ "TypeScript": ["import ", "export default", "console.", "=>", "document.", "window."],
4575
+ "C++": ["#include", "::", "std::", "template <", "using namespace"],
4576
+ "Python": ["def ", "import ", "print(", "self", "None", "True", "False"],
4577
+ "HTML": ["<html", "<div", "<body", "<script", "<style"],
4578
+ "CSS": ["@media"],
4579
+ "Markdown": ["#", "##", "###", "](", "![", "**"],
4580
+ };
4517
4581
 
4518
- for( let [ lang, wordList ] of Object.entries( CodeEditor.utils ) )
4582
+ for( const [ lang, indicators ] of Object.entries( strongIndicators ) )
4519
4583
  {
4520
- for( let kw of wordList )
4521
- if( tokenSet.has( kw ) ) scores[ lang ]++;
4584
+ scores[ lang ] = scores[ lang ] ?? 0;
4585
+ for( const key of indicators )
4586
+ {
4587
+ if( text.includes( key ) ) scores[ lang ] += 20;
4588
+ }
4522
4589
  }
4523
4590
 
4524
- for( let [ lang, wordList ] of Object.entries( CodeEditor.types ) )
4525
- {
4526
- for( let kw of wordList )
4527
- if( tokenSet.has( kw ) ) scores[ lang ]++;
4528
- }
4591
+ // Check groups' words now with less importance score
4592
+ const groups = [
4593
+ CodeEditor.keywords,
4594
+ CodeEditor.statements,
4595
+ CodeEditor.utils,
4596
+ CodeEditor.types,
4597
+ CodeEditor.builtIn,
4598
+ ];
4529
4599
 
4530
- for( let [ lang, wordList ] of Object.entries( CodeEditor.builtIn ) )
4600
+ for( const group of groups )
4531
4601
  {
4532
- for( let kw of wordList )
4533
- if( tokenSet.has( kw ) ) scores[ lang ]++;
4602
+ for( let [ lang, wordList ] of Object.entries( group ) )
4603
+ {
4604
+ scores[ lang ] = scores[ lang ] ?? 0;
4605
+ for( let kw of wordList )
4606
+ if( tokenSet.has( kw ) ) scores[ lang ]++;
4607
+ }
4534
4608
  }
4535
4609
 
4536
4610
  const sorted = Object.entries( scores ).sort( ( a, b ) => b[ 1 ] - a[ 1 ] );
@@ -4561,6 +4635,33 @@ class CodeEditor
4561
4635
  return true;
4562
4636
  }
4563
4637
 
4638
+ swapLines( lidx, offset, cursor )
4639
+ {
4640
+ if( this.code.lines[ lidx + offset ] == undefined )
4641
+ {
4642
+ return;
4643
+ }
4644
+
4645
+ this._addUndoStep( cursor, true );
4646
+
4647
+ swapArrayElements( this.code.lines, lidx + offset, lidx );
4648
+
4649
+ // Process both lines
4650
+ this.processLine( lidx + offset );
4651
+ this.processLine( lidx );
4652
+
4653
+ if( offset > 0 )
4654
+ {
4655
+ this.lineDown( cursor );
4656
+ }
4657
+ else
4658
+ {
4659
+ this.lineUp( cursor );
4660
+ }
4661
+
4662
+ this.hideAutoCompleteBox();
4663
+ }
4664
+
4564
4665
  restartBlink()
4565
4666
  {
4566
4667
  if( !this.code ) return;
@@ -4635,11 +4736,13 @@ class CodeEditor
4635
4736
  LX.deleteElement( this.selections[ cursor.name ] );
4636
4737
  delete this.selections[ cursor.name ];
4637
4738
  delete cursor.selection;
4739
+ cursor.line = cursor.line; // Update current line
4638
4740
  }
4639
4741
  else
4640
4742
  {
4641
4743
  for( let cursor of this.cursors.children )
4642
4744
  {
4745
+ cursor.line = cursor.line; // Update current line
4643
4746
  LX.deleteElement( this.selections[ cursor.name ] );
4644
4747
  delete this.selections[ cursor.name ];
4645
4748
  delete cursor.selection;
@@ -4655,7 +4758,7 @@ class CodeEditor
4655
4758
  this._removeSecondaryCursors();
4656
4759
 
4657
4760
  var cursor = this.getCurrentCursor();
4658
- this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, cursor );
4761
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, cursor, true );
4659
4762
 
4660
4763
  this.startSelection( cursor );
4661
4764
 
@@ -4671,43 +4774,49 @@ class CodeEditor
4671
4774
  this.hideAutoCompleteBox();
4672
4775
  }
4673
4776
 
4674
- cursorToRight( key, cursor )
4777
+ cursorToRight( text, cursor )
4675
4778
  {
4676
- if( !key ) return;
4779
+ if( !text || !text.length ) return;
4677
4780
 
4678
- cursor._left += this.charWidth;
4781
+ const chars = text.length;
4782
+ const offset = chars * this.charWidth;
4783
+
4784
+ cursor._left += offset;
4679
4785
  cursor.style.left = `calc( ${ cursor._left }px + ${ this.xPadding } )`;
4680
- cursor.position++;
4786
+ cursor.position += chars;
4681
4787
 
4682
4788
  this.restartBlink();
4683
4789
 
4684
- // Add horizontal scroll
4685
- const currentScrollLeft = this.getScrollLeft();
4686
- var viewportSizeX = ( this.codeScroller.clientWidth + currentScrollLeft ) - CodeEditor.LINE_GUTTER_WIDTH; // Gutter offset
4687
- if( (cursor.position * this.charWidth) >= viewportSizeX )
4688
- {
4689
- this.setScrollLeft( currentScrollLeft + this.charWidth );
4690
- }
4790
+ this.updateScrollLeft( cursor );
4691
4791
  }
4692
4792
 
4693
- cursorToLeft( key, cursor )
4793
+ cursorToLeft( text, cursor )
4694
4794
  {
4695
- if( !key ) return;
4795
+ if( !text || !text.length ) return;
4696
4796
 
4697
- cursor._left -= this.charWidth;
4797
+ const chars = text.length;
4798
+ const offset = chars * this.charWidth;
4799
+
4800
+ cursor._left -= offset;
4698
4801
  cursor._left = Math.max( cursor._left, 0 );
4699
4802
  cursor.style.left = `calc( ${ cursor._left }px + ${ this.xPadding } )`;
4700
- cursor.position--;
4803
+ cursor.position -= chars;
4701
4804
  cursor.position = Math.max( cursor.position, 0 );
4702
- this.restartBlink();
4703
4805
 
4704
- // Add horizontal scroll
4806
+ this.restartBlink();
4705
4807
 
4706
- const currentScrollLeft = this.getScrollLeft();
4707
- var viewportSizeX = currentScrollLeft; // Gutter offset
4708
- if( ( ( cursor.position - 1 ) * this.charWidth ) < viewportSizeX )
4808
+ // Check if we need to add scroll; if not then we might have to reduce it
4809
+ if( !this.updateScrollLeft( cursor ) )
4709
4810
  {
4710
- this.setScrollLeft( currentScrollLeft - this.charWidth );
4811
+ const leftMargin = this.charWidth;
4812
+ const cursorX = ( cursor.position * this.charWidth );
4813
+ const currentScrollLeft = this.getScrollLeft();
4814
+
4815
+ if( cursorX < ( currentScrollLeft + leftMargin ) )
4816
+ {
4817
+ const scroll = Math.max( cursorX - leftMargin, 0 );
4818
+ this.setScrollLeft( scroll );
4819
+ }
4711
4820
  }
4712
4821
  }
4713
4822
 
@@ -4720,7 +4829,7 @@ class CodeEditor
4720
4829
 
4721
4830
  if( resetLeft )
4722
4831
  {
4723
- this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
4832
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor, true );
4724
4833
  }
4725
4834
 
4726
4835
  const currentScrollTop = this.getScrollTop();
@@ -4740,7 +4849,7 @@ class CodeEditor
4740
4849
 
4741
4850
  if( resetLeft )
4742
4851
  {
4743
- this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
4852
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor, true );
4744
4853
  }
4745
4854
 
4746
4855
  const currentScrollTop = this.getScrollTop();
@@ -4759,17 +4868,26 @@ class CodeEditor
4759
4868
  return;
4760
4869
  }
4761
4870
 
4762
- for( let char of text )
4871
+ if( reverse )
4872
+ {
4873
+ this.cursorToLeft( text, cursor )
4874
+ }
4875
+ else
4763
4876
  {
4764
- reverse ? this.cursorToLeft( char, cursor ) : this.cursorToRight( char, cursor );
4877
+ this.cursorToRight( text, cursor );
4765
4878
  }
4766
4879
  }
4767
4880
 
4768
- cursorToPosition( cursor, position )
4881
+ cursorToPosition( cursor, position, updateScroll = false )
4769
4882
  {
4770
4883
  cursor.position = position;
4771
4884
  cursor._left = position * this.charWidth;
4772
4885
  cursor.style.left = `calc( ${ cursor._left }px + ${ this.xPadding } )`;
4886
+
4887
+ if( updateScroll )
4888
+ {
4889
+ this.updateScrollLeft( cursor );
4890
+ }
4773
4891
  }
4774
4892
 
4775
4893
  cursorToLine( cursor, line, resetLeft = false )
@@ -4860,22 +4978,32 @@ class CodeEditor
4860
4978
  LX.deleteElement( cursor );
4861
4979
  }
4862
4980
 
4863
- resetCursorPos( flag, cursor )
4981
+ resetCursorPos( flag, cursor, resetScroll = false )
4864
4982
  {
4865
4983
  cursor = cursor ?? this.getCurrentCursor();
4866
4984
 
4867
4985
  if( flag & CodeEditor.CURSOR_LEFT )
4868
4986
  {
4869
4987
  cursor._left = 0;
4870
- cursor.style.left = "calc(" + ( -this.getScrollLeft() ) + "px + " + this.xPadding + ")";
4988
+ cursor.style.left = "calc(" + this.xPadding + ")";
4871
4989
  cursor.position = 0;
4990
+
4991
+ if( resetScroll )
4992
+ {
4993
+ this.setScrollLeft( 0 );
4994
+ }
4872
4995
  }
4873
4996
 
4874
4997
  if( flag & CodeEditor.CURSOR_TOP )
4875
4998
  {
4876
4999
  cursor._top = 0;
4877
- cursor.style.top = ( -this.getScrollTop() ) + "px";
5000
+ cursor.style.top = "0px";
4878
5001
  cursor.line = 0;
5002
+
5003
+ if( resetScroll )
5004
+ {
5005
+ this.setScrollTop( 0 );
5006
+ }
4879
5007
  }
4880
5008
  }
4881
5009
 
@@ -4964,8 +5092,56 @@ class CodeEditor
4964
5092
  }
4965
5093
  }
4966
5094
 
4967
- _addSpaces( n )
5095
+ _addSpaces( cursor, n )
4968
5096
  {
5097
+ if( cursor.selection && !cursor.selection.sameLine() )
5098
+ {
5099
+ cursor.selection.invertIfNecessary();
5100
+
5101
+ for( let lidx = cursor.selection.fromY; lidx <= cursor.selection.toY; ++lidx )
5102
+ {
5103
+ // Remove indentation
5104
+ let lineStart = firstNonspaceIndex( this.code.lines[ lidx ] );
5105
+
5106
+ // Only tabs/spaces in the line...
5107
+ if( lineStart == -1 )
5108
+ {
5109
+ lineStart = this.code.lines[ lidx ].length;
5110
+ }
5111
+
5112
+ let indentSpaces = lineStart % this.tabSpaces;
5113
+ indentSpaces = indentSpaces == 0 ? this.tabSpaces : this.tabSpaces - indentSpaces;
5114
+
5115
+ const spacesString = " ".repeat( indentSpaces );
5116
+
5117
+ this.code.lines[ lidx ] = [
5118
+ this.code.lines[ lidx ].slice( 0, lineStart ),
5119
+ spacesString,
5120
+ this.code.lines[ lidx ].slice( lineStart )
5121
+ ].join('');
5122
+
5123
+ this.processLine( lidx );
5124
+
5125
+ if( cursor.line === lidx )
5126
+ {
5127
+ this.cursorToString( cursor, spacesString );
5128
+ }
5129
+
5130
+ if( cursor.selection.fromY === lidx )
5131
+ {
5132
+ cursor.selection.fromX = Math.max( cursor.selection.fromX + indentSpaces, 0 );
5133
+ }
5134
+ if( cursor.selection.toY === lidx )
5135
+ {
5136
+ cursor.selection.toX = Math.max( cursor.selection.toX + indentSpaces, 0 );
5137
+ }
5138
+
5139
+ this._processSelection( cursor, undefined, true );
5140
+ }
5141
+
5142
+ return;
5143
+ }
5144
+
4969
5145
  for( var i = 0; i < n; ++i )
4970
5146
  {
4971
5147
  this.root.dispatchEvent( new CustomEvent( 'keydown', { 'detail': {
@@ -4978,41 +5154,79 @@ class CodeEditor
4978
5154
 
4979
5155
  _removeSpaces( cursor )
4980
5156
  {
4981
- const lidx = cursor.line;
4982
-
4983
- // Remove indentation
4984
- let lineStart = firstNonspaceIndex( this.code.lines[ lidx ] );
4985
-
4986
- // Nothing to remove... we are at the start of the line
4987
- if( lineStart == 0 )
5157
+ if( cursor.selection )
4988
5158
  {
4989
- return;
5159
+ cursor.selection.invertIfNecessary();
4990
5160
  }
4991
5161
 
4992
- // Only tabs/spaces in the line...
4993
- if( lineStart == -1 )
5162
+ const lCount = cursor.selection ? 1 + ( cursor.selection.toY - cursor.selection.fromY ) : 1;
5163
+
5164
+ for( let i = 0; i < lCount; ++i )
4994
5165
  {
4995
- lineStart = this.code.lines[ lidx ].length;
4996
- }
5166
+ const lidx = ( cursor.selection ? cursor.selection.fromY : cursor.line ) + i;
4997
5167
 
4998
- let indentSpaces = lineStart % this.tabSpaces;
4999
- indentSpaces = indentSpaces == 0 ? this.tabSpaces : indentSpaces;
5000
- const newStart = Math.max( lineStart - indentSpaces, 0 );
5168
+ // Remove indentation
5169
+ let lineStart = firstNonspaceIndex( this.code.lines[ lidx ] );
5001
5170
 
5002
- this.code.lines[ lidx ] = [
5003
- this.code.lines[ lidx ].slice( 0, newStart ),
5004
- this.code.lines[ lidx ].slice( lineStart )
5005
- ].join('');
5171
+ // Nothing to remove... we are at the start of the line
5172
+ if( lineStart == 0 )
5173
+ {
5174
+ continue;
5175
+ }
5006
5176
 
5007
- this.processLine( lidx );
5177
+ // Only tabs/spaces in the line...
5178
+ if( lineStart == -1 )
5179
+ {
5180
+ lineStart = this.code.lines[ lidx ].length;
5181
+ }
5008
5182
 
5009
- this.cursorToString( cursor, " ".repeat( indentSpaces ), true );
5183
+ let indentSpaces = lineStart % this.tabSpaces;
5184
+ indentSpaces = indentSpaces == 0 ? this.tabSpaces : indentSpaces;
5185
+ const newStart = Math.max( lineStart - indentSpaces, 0 );
5010
5186
 
5011
- if( cursor.selection )
5187
+ this.code.lines[ lidx ] = [
5188
+ this.code.lines[ lidx ].slice( 0, newStart ),
5189
+ this.code.lines[ lidx ].slice( lineStart )
5190
+ ].join('');
5191
+
5192
+ this.processLine( lidx );
5193
+
5194
+ if( cursor.line === lidx )
5195
+ {
5196
+ this.cursorToString( cursor, " ".repeat( indentSpaces ), true );
5197
+ }
5198
+
5199
+ if( cursor.selection )
5200
+ {
5201
+ if( cursor.selection.fromY === lidx )
5202
+ {
5203
+ cursor.selection.fromX = Math.max( cursor.selection.fromX - indentSpaces, 0 );
5204
+ }
5205
+ if( cursor.selection.toY === lidx )
5206
+ {
5207
+ cursor.selection.toX = Math.max( cursor.selection.toX - indentSpaces, 0 );
5208
+ }
5209
+
5210
+ this._processSelection( cursor, undefined, true );
5211
+ }
5212
+ }
5213
+ }
5214
+
5215
+ updateScrollLeft( cursor )
5216
+ {
5217
+ cursor = cursor ?? this.getCurrentCursor();
5218
+
5219
+ const rightMargin = this.charWidth;
5220
+ const cursorX = ( cursor.position * this.charWidth );
5221
+ const currentScrollLeft = this.getScrollLeft();
5222
+ const viewportSizeX = this.codeScroller.clientWidth - CodeEditor.LINE_GUTTER_WIDTH; // Gutter offset
5223
+ const viewportX = viewportSizeX + currentScrollLeft;
5224
+
5225
+ if( cursorX >= ( viewportX - rightMargin ) )
5012
5226
  {
5013
- cursor.selection.invertIfNecessary();
5014
- cursor.selection.fromX = Math.max( cursor.selection.fromX - indentSpaces, 0 );
5015
- this._processSelection( cursor );
5227
+ const scroll = Math.max( cursorX - ( viewportSizeX - rightMargin ), 0 );
5228
+ this.setScrollLeft( scroll );
5229
+ return true;
5016
5230
  }
5017
5231
  }
5018
5232
 
@@ -5034,7 +5248,7 @@ class CodeEditor
5034
5248
  LX.doAsync( () => {
5035
5249
  this.codeScroller.scrollLeft = value;
5036
5250
  this.setScrollBarValue( 'horizontal', 0 );
5037
- }, 20 );
5251
+ }, 10 );
5038
5252
  }
5039
5253
 
5040
5254
  setScrollTop( value )
@@ -5043,7 +5257,7 @@ class CodeEditor
5043
5257
  LX.doAsync( () => {
5044
5258
  this.codeScroller.scrollTop = value;
5045
5259
  this.setScrollBarValue( 'vertical' );
5046
- }, 20 );
5260
+ }, 10 );
5047
5261
  }
5048
5262
 
5049
5263
  resize( flag = CodeEditor.RESIZE_SCROLLBAR_H_V, pMaxLength, onResize )
@@ -5130,14 +5344,16 @@ class CodeEditor
5130
5344
  {
5131
5345
  if( type == 'vertical' )
5132
5346
  {
5133
- const scrollBarHeight = this.vScrollbar.thumb.parentElement.offsetHeight;
5134
- const scrollThumbHeight = this.vScrollbar.thumb.offsetHeight;
5135
-
5136
5347
  const scrollHeight = this.codeScroller.scrollHeight - this.codeScroller.clientHeight;
5137
- const currentScroll = this.codeScroller.scrollTop;
5138
5348
 
5139
- this.vScrollbar.thumb._top = ( currentScroll / scrollHeight ) * ( scrollBarHeight - scrollThumbHeight );
5140
- this.vScrollbar.thumb.style.top = this.vScrollbar.thumb._top + "px";
5349
+ if( scrollHeight > 0 )
5350
+ {
5351
+ const scrollBarHeight = this.vScrollbar.thumb.parentElement.offsetHeight;
5352
+ const scrollThumbHeight = this.vScrollbar.thumb.offsetHeight;
5353
+ const currentScroll = this.codeScroller.scrollTop;
5354
+ this.vScrollbar.thumb._top = ( currentScroll / scrollHeight ) * ( scrollBarHeight - scrollThumbHeight );
5355
+ this.vScrollbar.thumb.style.top = this.vScrollbar.thumb._top + "px";
5356
+ }
5141
5357
  }
5142
5358
  else
5143
5359
  {
@@ -5146,14 +5362,16 @@ class CodeEditor
5146
5362
  this.codeScroller.scrollLeft += value;
5147
5363
  }
5148
5364
 
5149
- const scrollBarWidth = this.hScrollbar.thumb.parentElement.offsetWidth;
5150
- const scrollThumbWidth = this.hScrollbar.thumb.offsetWidth;
5151
-
5365
+ // Only when scroll is needed
5152
5366
  const scrollWidth = this.codeScroller.scrollWidth - this.codeScroller.clientWidth;
5153
- const currentScroll = this.codeScroller.scrollLeft;
5154
-
5155
- this.hScrollbar.thumb._left = ( currentScroll / scrollWidth ) * ( scrollBarWidth - scrollThumbWidth );
5156
- this.hScrollbar.thumb.style.left = this.hScrollbar.thumb._left + "px";
5367
+ if( scrollWidth > 0 )
5368
+ {
5369
+ const scrollBarWidth = this.hScrollbar.thumb.parentElement.offsetWidth;
5370
+ const scrollThumbWidth = this.hScrollbar.thumb.offsetWidth;
5371
+ const currentScroll = this.codeScroller.scrollLeft;
5372
+ this.hScrollbar.thumb._left = ( currentScroll / scrollWidth ) * ( scrollBarWidth - scrollThumbWidth );
5373
+ this.hScrollbar.thumb.style.left = this.hScrollbar.thumb._left + "px";
5374
+ }
5157
5375
  }
5158
5376
  }
5159
5377
 
@@ -5175,7 +5393,6 @@ class CodeEditor
5175
5393
  const currentScroll = (this.hScrollbar.thumb._left * scrollWidth) / ( scrollBarWidth - scrollThumbWidth );
5176
5394
  this.codeScroller.scrollLeft = currentScroll;
5177
5395
 
5178
-
5179
5396
  this._discardScroll = true;
5180
5397
  }
5181
5398
 
@@ -5253,7 +5470,7 @@ class CodeEditor
5253
5470
  return [ word, from, to ];
5254
5471
  }
5255
5472
 
5256
- _measureChar( char = "a", useFloating = false, getBB = false )
5473
+ _measureChar( char = "M", useFloating = true, getBB = false )
5257
5474
  {
5258
5475
  const parentContainer = LX.makeContainer( null, "lexcodeeditor", "", document.body );
5259
5476
  const container = LX.makeContainer( null, "code", "", parentContainer );
@@ -5440,14 +5657,16 @@ class CodeEditor
5440
5657
  return;
5441
5658
  }
5442
5659
 
5660
+ const maxX = this.codeScroller.clientWidth - 256; // Viewport - box width
5661
+
5443
5662
  // Select always first option
5444
5663
  this.autocomplete.firstChild.classList.add( 'selected' );
5445
5664
 
5446
5665
  // Show box
5447
5666
  this.autocomplete.classList.toggle( 'show', true );
5448
5667
  this.autocomplete.classList.toggle( 'no-scrollbar', !( this.autocomplete.scrollHeight > this.autocomplete.offsetHeight ) );
5449
- this.autocomplete.style.left = (cursor._left + CodeEditor.LINE_GUTTER_WIDTH - this.getScrollLeft()) + "px";
5450
- this.autocomplete.style.top = (cursor._top + 28 + this.lineHeight - this.getScrollTop()) + "px";
5668
+ this.autocomplete.style.left = `${ Math.min( cursor._left + CodeEditor.LINE_GUTTER_WIDTH - this.getScrollLeft(), maxX ) }px`;
5669
+ this.autocomplete.style.top = `${ ( cursor._top + this._verticalTopOffset + this.lineHeight - this.getScrollTop() ) }px`;
5451
5670
 
5452
5671
  this.isAutoCompleteActive = true;
5453
5672
  }
@@ -5528,21 +5747,33 @@ class CodeEditor
5528
5747
 
5529
5748
  const [ word, idx ] = this._getSelectedAutoComplete();
5530
5749
  const offset = dir == 'down' ? 1 : -1;
5750
+ const fIdx = idx + offset;
5751
+
5752
+ const autocompleteRowHeight = 22;
5753
+ const autocompleteHeight = 132;
5531
5754
 
5532
5755
  if( dir == 'down' )
5533
5756
  {
5534
- if( ( idx + offset ) >= this.autocomplete.childElementCount ) return;
5757
+ if( fIdx >= this.autocomplete.childElementCount ) return;
5758
+
5759
+ if( ( ( idx + offset * 2 ) * autocompleteRowHeight ) - this.autocomplete.scrollTop > autocompleteHeight )
5760
+ {
5761
+ this.autocomplete.scrollTop += autocompleteRowHeight;
5762
+ }
5535
5763
  }
5536
5764
  else if( dir == 'up')
5537
5765
  {
5538
- if( ( idx + offset ) < 0 ) return;
5539
- }
5766
+ if( fIdx < 0 ) return;
5540
5767
 
5541
- this.autocomplete.scrollTop += offset * 20;
5768
+ if( ( fIdx * autocompleteRowHeight ) < this.autocomplete.scrollTop )
5769
+ {
5770
+ this.autocomplete.scrollTop -= autocompleteRowHeight;
5771
+ }
5772
+ }
5542
5773
 
5543
5774
  // Remove selected from the current word and add it to the next one
5544
- this.autocomplete.childNodes[ idx ].classList.remove('selected');
5545
- this.autocomplete.childNodes[ idx + offset ].classList.add('selected');
5775
+ this.autocomplete.childNodes[ idx ].classList.remove( 'selected' );
5776
+ this.autocomplete.childNodes[ idx + offset ].classList.add( 'selected' );
5546
5777
  }
5547
5778
 
5548
5779
  showSearchBox( clear )
@@ -5844,7 +6075,7 @@ class CodeEditor
5844
6075
  this.fontSize = size;
5845
6076
  const r = document.querySelector( ':root' );
5846
6077
  r.style.setProperty( "--code-editor-font-size", `${ this.fontSize }px` );
5847
- this.charWidth = this._measureChar( "a", true );
6078
+ this.charWidth = this._measureChar();
5848
6079
 
5849
6080
  window.localStorage.setItem( "lexcodeeditor-font-size", this.fontSize );
5850
6081
 
@@ -5959,9 +6190,9 @@ CodeEditor.declarationKeywords =
5959
6190
  CodeEditor.keywords =
5960
6191
  {
5961
6192
  'JavaScript': ['var', 'let', 'const', 'this', 'in', 'of', 'true', 'false', 'new', 'function', 'NaN', 'static', 'class', 'constructor', 'null', 'typeof', 'debugger', 'abstract',
5962
- 'arguments', 'extends', 'instanceof', 'Infinity'],
6193
+ 'arguments', 'extends', 'instanceof', 'Infinity', 'get'],
5963
6194
  'TypeScript': ['var', 'let', 'const', 'this', 'in', 'of', 'true', 'false', 'new', 'function', 'class', 'extends', 'instanceof', 'Infinity', 'private', 'public', 'protected', 'interface',
5964
- 'enum', 'type'],
6195
+ 'enum', 'type', 'get'],
5965
6196
  'C': ['int', 'float', 'double', 'long', 'short', 'char', 'const', 'void', 'true', 'false', 'auto', 'struct', 'typedef', 'signed', 'volatile', 'unsigned', 'static', 'extern', 'enum', 'register',
5966
6197
  'union'],
5967
6198
  'C++': [...CodeEditor.nativeTypes["C++"], 'const', 'static_cast', 'dynamic_cast', 'new', 'delete', 'true', 'false', 'auto', 'class', 'struct', 'typedef', 'nullptr',
@@ -6011,7 +6242,8 @@ CodeEditor.types =
6011
6242
  'JavaScript': ['Object', 'String', 'Function', 'Boolean', 'Symbol', 'Error', 'Number', 'TextEncoder', 'TextDecoder', 'Array', 'ArrayBuffer', 'InputEvent', 'MouseEvent',
6012
6243
  'Int8Array', 'Int16Array', 'Int32Array', 'Float32Array', 'Float64Array', 'Element'],
6013
6244
  'TypeScript': ['arguments', 'constructor', 'null', 'typeof', 'debugger', 'abstract', 'Object', 'string', 'String', 'Function', 'Boolean', 'boolean', 'Error', 'Number', 'number', 'TextEncoder',
6014
- 'TextDecoder', 'Array', 'ArrayBuffer', 'InputEvent', 'MouseEvent', 'Int8Array', 'Int16Array', 'Int32Array', 'Float32Array', 'Float64Array', 'Element'],
6245
+ 'TextDecoder', 'Array', 'ArrayBuffer', 'InputEvent', 'MouseEvent', 'Int8Array', 'Int16Array', 'Int32Array', 'Float32Array', 'Float64Array', 'Element', 'bigint', 'unknown', 'any',
6246
+ 'Record'],
6015
6247
  'Rust': ['u128'],
6016
6248
  'Python': ['int', 'type', 'float', 'map', 'list', 'ArithmeticError', 'AssertionError', 'AttributeError', 'Exception', 'EOFError', 'FloatingPointError', 'GeneratorExit',
6017
6249
  'ImportError', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'NotImplementedError', 'OSError',
@@ -6034,8 +6266,8 @@ CodeEditor.builtIn =
6034
6266
 
6035
6267
  CodeEditor.statements =
6036
6268
  {
6037
- 'JavaScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await'],
6038
- 'TypeScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await', 'as'],
6269
+ 'JavaScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'default', 'export', 'from', 'throw', 'async', 'try', 'catch', 'await', 'as'],
6270
+ 'TypeScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'default', 'export', 'from', 'throw', 'async', 'try', 'catch', 'await', 'as'],
6039
6271
  'CSS': ['@', 'import'],
6040
6272
  'C': ['for', 'if', 'else', 'return', 'continue', 'break', 'case', 'switch', 'while', 'using', 'default', 'goto', 'do'],
6041
6273
  'C++': ['std', 'for', 'if', 'else', 'return', 'continue', 'break', 'case', 'switch', 'while', 'using', 'glm', 'spdlog', 'default'],