lexgui 0.7.2 → 0.7.3

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.
@@ -33,6 +33,15 @@ function indexOfFrom( str, reg, from, reverse ) {
33
33
  }
34
34
  }
35
35
 
36
+ function codeScopesEqual( a, b ) {
37
+ if( a.length !== b.length ) return false;
38
+ for( let i = 0; i < a.length; i++ )
39
+ {
40
+ if( a[ i ].type !== b[ i ].type ) return false;
41
+ }
42
+ return true;
43
+ }
44
+
36
45
  class CodeSelection {
37
46
 
38
47
  constructor( editor, cursor, className = "lexcodeselection" ) {
@@ -133,6 +142,9 @@ class ScrollBar {
133
142
  static SCROLLBAR_VERTICAL = 1;
134
143
  static SCROLLBAR_HORIZONTAL = 2;
135
144
 
145
+ static SCROLLBAR_VERTICAL_WIDTH = 10;
146
+ static SCROLLBAR_HORIZONTAL_HEIGHT = 10;
147
+
136
148
  constructor( editor, type ) {
137
149
 
138
150
  this.editor = editor;
@@ -206,7 +218,7 @@ const HighlightRules = {
206
218
  { test: ctx => ctx.token.substr( 0, ctx.singleLineCommentToken.length ) == ctx.singleLineCommentToken, className: "cm-com" },
207
219
  { test: (ctx, editor) => editor._isKeyword( ctx ), className: "cm-kwd" },
208
220
  { test: (ctx, editor) => editor._mustHightlightWord( ctx.token, CodeEditor.builtIn, ctx.lang ) && ( ctx.lang.tags ?? false ? ( editor._enclosedByTokens( ctx.token, ctx.tokenIndex, '<', '>' ) ) : true ), className: "cm-bln" },
209
- { test: (ctx, editor) => editor._mustHightlightWord( ctx.token, CodeEditor.statementsAndDeclarations, ctx.lang ), className: "cm-std" },
221
+ { test: (ctx, editor) => editor._mustHightlightWord( ctx.token, CodeEditor.statements, ctx.lang ), className: "cm-std" },
210
222
  { test: (ctx, editor) => editor._mustHightlightWord( ctx.token, CodeEditor.symbols, ctx.lang ), className: "cm-sym" },
211
223
  { test: (ctx, editor) => editor._mustHightlightWord( ctx.token, CodeEditor.types, ctx.lang ), className: "cm-typ" },
212
224
  { test: (ctx, editor) => editor._isNumber( ctx.token ) || editor._isNumber( ctx.token.replace(/[px]|[em]|%/g,'') ), className: "cm-dec" },
@@ -214,21 +226,23 @@ const HighlightRules = {
214
226
  ],
215
227
 
216
228
  javascript: [
217
- { test: ctx => (ctx.prev === 'class' && ctx.next === '{') || (ctx.prev === 'new' && ctx.next === '('), className: "cm-typ" }
229
+ { test: ctx => (ctx.prev === 'class' && ctx.next === '{') , className: "cm-typ" },
218
230
  ],
219
231
 
220
232
  typescript: [
221
- { test: ctx => ctx.scope && (ctx.token !== ',' && ctx.scope == "enum"), className: "cm-enu" },
233
+ { test: ctx => ctx.scope && (ctx.token !== ',' && ctx.scope.type == "enum"), className: "cm-enu" },
222
234
  { test: ctx => (ctx.prev === ':' && ctx.next !== undefined && isLetter(ctx.token) ) || (ctx.prev === 'interface' && ctx.next === '{') || (ctx.prev === 'enum' && ctx.next === '{'), className: "cm-typ" },
223
235
  { test: ctx => (ctx.prev === 'class' && ctx.next === '{') || (ctx.prev === 'class' && ctx.next === '<') || (ctx.prev === 'new' && ctx.next === '(') || (ctx.prev === 'new' && ctx.next === '<'), className: "cm-typ" },
224
236
  { test: (ctx, editor) => ctx.token !== ',' && editor._enclosedByTokens( ctx.token, ctx.tokenIndex, '<', '>' ), className: "cm-typ" },
225
237
  ],
226
238
 
227
239
  cpp: [
228
- { test: ctx => ctx.scope && (ctx.token !== ',' && ctx.scope == "enum"), className: "cm-enu" },
240
+ { test: ctx => ctx.scope && (ctx.token !== ',' && ctx.scope.type == "enum"), className: "cm-enu" },
241
+ { test: ctx => ctx.isEnumValueSymbol( ctx.token ), className: "cm-enu" },
229
242
  { test: ctx => (ctx.prev === 'class' && ctx.next === '{') || (ctx.prev === 'struct' && ctx.next === '{'), className: "cm-typ" },
230
243
  { test: ctx => ctx.prev === "<" && (ctx.next === ">" || ctx.next === "*"), className: "cm-typ" }, // Defining template type in C++
231
- { test: ctx => ctx.next === "::" || (ctx.prev === "::" && ctx.next !== "("), className: "cm-typ" } // C++ Class
244
+ { test: ctx => ctx.next === "::" || (ctx.prev === "::" && ctx.next !== "("), className: "cm-typ" }, // C++ Class
245
+ { test: ctx => ctx.isClassSymbol( ctx.token ) || ctx.isStructSymbol( ctx.token ), className: "cm-typ" },
232
246
  ],
233
247
 
234
248
  wgsl: [
@@ -240,7 +254,8 @@ const HighlightRules = {
240
254
  css: [
241
255
  { test: ctx => ( ctx.prev == '.' || ctx.prev == '::' || ( ctx.prev == ':' && ctx.next == '{' ) || ( ctx.token[ 0 ] == '#' && ctx.prev != ':' ) ), className: "cm-kwd" },
242
256
  { test: ctx => ctx.prev === ':' && (ctx.next === ';' || ctx.next === '!important'), className: "cm-str" }, // CSS value
243
- { test: ctx => ctx.prev === undefined && ctx.next === ":", className: "cm-typ" }, // CSS attribute
257
+ { test: ctx => ( ctx.prev === undefined || ctx.prev === '{' || ctx.prev === ';' ) && ctx.next === ":", className: "cm-typ" }, // CSS attribute
258
+ { test: ctx => ctx.prev === "(" && ctx.next === ")" && ctx.token.startsWith( "--" ), className: "cm-typ" }, // CSS vars
244
259
  ],
245
260
 
246
261
  batch: [
@@ -252,7 +267,8 @@ const HighlightRules = {
252
267
  ],
253
268
 
254
269
  php: [
255
- { test: ctx => (ctx.prev === 'class' && ctx.next === '{') || (ctx.prev === 'class' && ctx.next === 'implements'), className: "cm-typ" },
270
+ { test: ctx => ctx.token.startsWith( '$' ), className: "cm-var" },
271
+ { test: ctx => (ctx.prev === 'class' && (ctx.next === '{' || ctx.next === 'implements') ) || (ctx.prev === 'enum'), className: "cm-typ" },
256
272
  ],
257
273
 
258
274
  post_common: [
@@ -285,8 +301,13 @@ class CodeEditor {
285
301
  static CODE_MIN_FONT_SIZE = 9;
286
302
  static CODE_MAX_FONT_SIZE = 22;
287
303
 
304
+ static LINE_GUTTER_WIDTH = 48;
288
305
  static LINE_GUTTER_WIDTH = 48;
289
306
 
307
+ static RESIZE_SCROLLBAR_H = 1;
308
+ static RESIZE_SCROLLBAR_V = 2;
309
+ static RESIZE_SCROLLBAR_H_V = CodeEditor.RESIZE_SCROLLBAR_H | CodeEditor.RESIZE_SCROLLBAR_V;
310
+
290
311
  /**
291
312
  * @param {*} options
292
313
  * name:
@@ -302,11 +323,17 @@ class CodeEditor {
302
323
 
303
324
  CodeEditor.__instances.push( this );
304
325
 
326
+ this.skipInfo = options.skipInfo ?? false;
327
+ this.disableEdition = options.disableEdition ?? false;
328
+ this.skipTabs = options.skipTabs ?? false;
329
+ this.useFileExplorer = ( options.fileExplorer ?? false ) && !this.skipTabs;
330
+ this.useAutoComplete = options.autocomplete ?? true;
331
+
305
332
  // File explorer
306
- if( options.fileExplorer ?? false )
333
+ if( this.useFileExplorer )
307
334
  {
308
- var [ explorerArea, codeArea ] = area.split({ sizes:[ "15%","85%" ] });
309
- explorerArea.setLimitBox( 180, 20, 512 );
335
+ let [ explorerArea, editorArea ] = area.split({ sizes:[ "15%","85%" ] });
336
+ // explorerArea.setLimitBox( 180, 20, 512 );
310
337
  this.explorerArea = explorerArea;
311
338
 
312
339
  let panel = new LX.Panel();
@@ -357,35 +384,73 @@ class CodeEditor {
357
384
  explorerArea.attach( panel );
358
385
 
359
386
  // Update area
360
- area = codeArea;
387
+ area = editorArea;
361
388
  }
362
389
 
363
- this.base_area = area;
390
+ this.baseArea = area;
364
391
  this.area = new LX.Area( { className: "lexcodeeditor", height: "100%", skipAppend: true } );
365
392
 
366
- this.skipInfo = options.skipInfo ?? false;
367
- this.disableEdition = options.disableEdition ?? false;
393
+ if( !this.skipTabs )
394
+ {
395
+ this.tabs = this.area.addTabs( { onclose: (name) => {
396
+ delete this.openedTabs[ name ];
397
+ if( Object.keys( this.openedTabs ).length < 2 )
398
+ {
399
+ clearInterval( this.blinker );
400
+ this.cursors.classList.remove( 'show' );
401
+ }
402
+ } } );
368
403
 
369
- this._tabStorage = {};
370
- this.tabs = this.area.addTabs( { onclose: (name) => {
371
- delete this.openedTabs[ name ];
372
- if( Object.keys( this.openedTabs ).length < 2 )
404
+ if( !this.disableEdition )
373
405
  {
374
- clearInterval( this.blinker );
375
- this.cursors.classList.remove( 'show' );
406
+ this.tabs.root.addEventListener( 'dblclick', (e) => {
407
+ if( options.allowAddScripts ?? true )
408
+ {
409
+ e.preventDefault();
410
+ this.addTab( "unnamed.js", true );
411
+ }
412
+ } );
376
413
  }
377
- } } );
378
414
 
379
- if( !this.disableEdition )
415
+ this.codeArea = this.tabs.area;
416
+ }
417
+ else
380
418
  {
381
- this.tabs.root.addEventListener( 'dblclick', (e) => {
382
- if( options.allowAddScripts ?? true ) {
383
- e.preventDefault();
384
- this.addTab("unnamed.js", true);
419
+ this.codeArea = new LX.Area( { skipAppend: true } );
420
+ this.area.attach( this.codeArea );
421
+ this._loadFileButton = LX.makeElement( "button",
422
+ "grid absolute self-center z-100 p-3 rounded-full bg-secondary hover:bg-tertiary cursor-pointer border",
423
+ LX.makeIcon( "FolderOpen" ).innerHTML,
424
+ this.area,
425
+ {
426
+ bottom: "8px"
427
+ }
428
+ );
429
+ this._loadFileButton.addEventListener( "click", e => {
430
+
431
+ const dropdownOptions = [];
432
+
433
+ for( const [ key, value ] of [ ...Object.entries( this.loadedTabs ).slice( 1 ), ...Object.entries( this._tabStorage ) ] )
434
+ {
435
+ const icon = this._getFileIcon( key );
436
+ const classes = icon ? icon.split( ' ' ) : [];
437
+ dropdownOptions.push( {
438
+ name: key,
439
+ icon: classes[ 0 ],
440
+ svgClass: classes.slice( 0 ).join( ' ' ),
441
+ callback: (v) => {
442
+ this.loadCode( v );
443
+ }
444
+ } );
385
445
  }
446
+
447
+ new LX.DropdownMenu( this._loadFileButton, dropdownOptions, { side: "top", align: "center" });
448
+
386
449
  } );
387
450
  }
388
451
 
452
+ this.codeArea.root.classList.add( 'lexcodearea' );
453
+
389
454
  // Full editor
390
455
  area.root.classList.add('codebasearea');
391
456
 
@@ -401,9 +466,6 @@ class CodeEditor {
401
466
  attributeFilter: ['class', 'style'],
402
467
  });
403
468
 
404
- // Code area
405
- this.tabs.area.root.classList.add( 'codetabsarea' );
406
-
407
469
  this.root = this.area.root;
408
470
  this.root.tabIndex = -1;
409
471
  area.attach( this.root );
@@ -432,12 +494,12 @@ class CodeEditor {
432
494
 
433
495
  this.cursors = document.createElement( 'div' );
434
496
  this.cursors.className = 'cursors';
435
- this.tabs.area.attach( this.cursors );
497
+ this.codeArea.attach( this.cursors );
436
498
 
437
499
  this.searchResultSelections = document.createElement( 'div' );
438
500
  this.searchResultSelections.id = 'search-selections';
439
501
  this.searchResultSelections.className = 'selections';
440
- this.tabs.area.attach( this.searchResultSelections );
502
+ this.codeArea.attach( this.searchResultSelections );
441
503
 
442
504
  // Store here selections per cursor
443
505
  this.selections = {};
@@ -450,7 +512,7 @@ class CodeEditor {
450
512
 
451
513
  // Scroll stuff
452
514
  {
453
- this.codeScroller = this.tabs.area.root;
515
+ this.codeScroller = this.codeArea.root;
454
516
  this.firstLineInViewport = 0;
455
517
  this.lineScrollMargin = new LX.vec2( 20, 20 ); // [ mUp, mDown ]
456
518
 
@@ -475,12 +537,14 @@ class CodeEditor {
475
537
  {
476
538
  if( this.visibleLinesViewport.y < (this.code.lines.length - 1) )
477
539
  {
478
- const totalLinesInViewport = ((this.codeScroller.offsetHeight - 36) / this.lineHeight)|0;
540
+ const totalLinesInViewport = ( ( this.codeScroller.offsetHeight ) / this.lineHeight )|0;
479
541
  const scrollDownBoundary =
480
542
  ( Math.max( this.visibleLinesViewport.y - totalLinesInViewport, 0 ) - 1 ) * this.lineHeight;
481
543
 
482
544
  if( scrollTop >= scrollDownBoundary )
545
+ {
483
546
  this.processLines( CodeEditor.UPDATE_VISIBLE_LINES );
547
+ }
484
548
  }
485
549
  }
486
550
  // Scroll up...
@@ -488,7 +552,9 @@ class CodeEditor {
488
552
  {
489
553
  const scrollUpBoundary = parseInt( this.code.style.top );
490
554
  if( scrollTop < scrollUpBoundary )
555
+ {
491
556
  this.processLines( CodeEditor.UPDATE_VISIBLE_LINES );
557
+ }
492
558
  }
493
559
 
494
560
  lastScrollTopValue = scrollTop;
@@ -513,34 +579,31 @@ class CodeEditor {
513
579
  }
514
580
  }
515
581
 
516
- // This is only the container, line numbers are in the same line div
582
+ // Line numbers and scrollbars
517
583
  {
584
+ // This is only the container, line numbers are in the same line div
518
585
  this.gutter = document.createElement( 'div' );
519
586
  this.gutter.className = "lexcodegutter";
520
587
  area.attach( this.gutter );
521
- }
522
588
 
523
- // Add custom vertical scroll bar
524
- {
589
+ // Add custom vertical scroll bar
525
590
  this.vScrollbar = new ScrollBar( this, ScrollBar.SCROLLBAR_VERTICAL );
526
591
  area.attach( this.vScrollbar.root );
527
- }
528
592
 
529
- // Add custom horizontal scroll bar
530
- {
593
+ // Add custom horizontal scroll bar
531
594
  this.hScrollbar = new ScrollBar( this, ScrollBar.SCROLLBAR_HORIZONTAL );
532
595
  area.attach( this.hScrollbar.root );
533
596
  }
534
597
 
598
+ // Add autocomplete, search boxes (IF edition enabled)
535
599
  if( !this.disableEdition )
536
600
  {
537
601
  // Add autocomplete box
538
602
  {
539
- var box = document.createElement( 'div' );
603
+ const box = document.createElement( 'div' );
540
604
  box.className = "autocomplete";
541
605
  this.autocomplete = box;
542
- this.tabs.area.attach( box );
543
-
606
+ this.codeArea.attach( box );
544
607
  this.isAutoCompleteActive = false;
545
608
  }
546
609
 
@@ -564,7 +627,7 @@ class CodeEditor {
564
627
  } );
565
628
 
566
629
  this.searchbox = box;
567
- this.tabs.area.attach( box );
630
+ this.codeArea.attach( box );
568
631
  }
569
632
 
570
633
  // Add search LINE box
@@ -586,7 +649,7 @@ class CodeEditor {
586
649
  } );
587
650
 
588
651
  this.searchlinebox = box;
589
- this.tabs.area.attach( box );
652
+ this.codeArea.attach( box );
590
653
  }
591
654
  }
592
655
 
@@ -597,7 +660,9 @@ class CodeEditor {
597
660
 
598
661
  // Append all childs
599
662
  while( this.codeScroller.firstChild )
663
+ {
600
664
  this.codeSizer.appendChild( this.codeScroller.firstChild );
665
+ }
601
666
 
602
667
  this.codeScroller.appendChild( this.codeSizer );
603
668
  }
@@ -613,7 +678,6 @@ class CodeEditor {
613
678
 
614
679
  // Code
615
680
 
616
- this.useAutoComplete = options.autocomplete ?? true;
617
681
  this.highlight = options.highlight ?? 'Plain Text';
618
682
  this.onsave = options.onsave ?? ((code) => { console.log( code, "save" ) });
619
683
  this.onrun = options.onrun ?? ((code) => { this.runScript(code) });
@@ -625,8 +689,8 @@ class CodeEditor {
625
689
  this.charWidth = 7; // To update later depending on size..
626
690
  this.defaultSingleLineCommentToken = '//';
627
691
  this.defaultBlockCommentTokens = [ '/*', '*/' ];
628
- this.lineScopes = [];
629
692
  this._lastTime = null;
693
+ this._tabStorage = {};
630
694
 
631
695
  this.pairKeys = {
632
696
  "\"": "\"",
@@ -656,476 +720,485 @@ class CodeEditor {
656
720
 
657
721
  if( !CodeEditor._staticReady )
658
722
  {
659
- for( let lang in CodeEditor.keywords ) CodeEditor.keywords[lang] = CodeEditor.keywords[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
660
- for( let lang in CodeEditor.utils ) CodeEditor.utils[lang] = CodeEditor.utils[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
661
- for( let lang in CodeEditor.types ) CodeEditor.types[lang] = CodeEditor.types[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
662
- for( let lang in CodeEditor.builtIn ) CodeEditor.builtIn[lang] = CodeEditor.builtIn[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
663
- for( let lang in CodeEditor.statementsAndDeclarations ) CodeEditor.statementsAndDeclarations[lang] = CodeEditor.statementsAndDeclarations[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
664
- for( let lang in CodeEditor.symbols ) CodeEditor.symbols[lang] = CodeEditor.symbols[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
723
+ for( let lang in CodeEditor.keywords ) CodeEditor.keywords[lang] = new Set( CodeEditor.keywords[lang] );
724
+ for( let lang in CodeEditor.utils ) CodeEditor.utils[lang] = new Set( CodeEditor.utils[lang] );
725
+ for( let lang in CodeEditor.types ) CodeEditor.types[lang] = new Set( CodeEditor.types[lang] );
726
+ for( let lang in CodeEditor.builtIn ) CodeEditor.builtIn[lang] = new Set( CodeEditor.builtIn[lang] );
727
+ for( let lang in CodeEditor.statements ) CodeEditor.statements[lang] = new Set( CodeEditor.statements[lang] );
728
+ for( let lang in CodeEditor.symbols ) CodeEditor.symbols[lang] = new Set( CodeEditor.symbols[lang] );
665
729
 
666
730
  CodeEditor._staticReady = true;
667
731
  }
668
732
 
669
733
  // Action keys
734
+ {
735
+ this.action( 'Escape', false, ( ln, cursor, e ) => {
736
+ if( this.hideAutoCompleteBox() )
737
+ return;
738
+ if( this.hideSearchBox() )
739
+ return;
740
+ // Remove selections and cursors
741
+ this.endSelection();
742
+ this._removeSecondaryCursors();
743
+ });
670
744
 
671
- this.action( 'Escape', false, ( ln, cursor, e ) => {
672
- if( this.hideAutoCompleteBox() )
673
- return;
674
- if( this.hideSearchBox() )
675
- return;
676
- // Remove selections and cursors
677
- this.endSelection();
678
- this._removeSecondaryCursors();
679
- });
680
-
681
- this.action( 'Backspace', false, ( ln, cursor, e ) => {
745
+ this.action( 'Backspace', false, ( ln, cursor, e ) => {
682
746
 
683
- this._addUndoStep( cursor );
747
+ this._addUndoStep( cursor );
684
748
 
685
- if( cursor.selection ) {
686
- this.deleteSelection( cursor );
687
- // Remove entire line when selecting with triple click
688
- if( this._tripleClickSelection )
749
+ if( cursor.selection )
689
750
  {
690
- this.actions['Backspace'].callback( ln, cursor, e );
691
- this.lineDown( cursor, true );
751
+ this.deleteSelection( cursor );
752
+ // Remove entire line when selecting with triple click
753
+ if( this._tripleClickSelection )
754
+ {
755
+ this.actions['Backspace'].callback( ln, cursor, e );
756
+ this.lineDown( cursor, true );
757
+ }
692
758
  }
693
- }
694
- else {
695
-
696
- var letter = this.getCharAtPos( cursor, -1 );
697
- if( letter ) {
759
+ else {
698
760
 
699
- var deleteFromPosition = cursor.position - 1;
700
- var numCharsDeleted = 1;
701
-
702
- // Delete full word
703
- if( e.shiftKey )
761
+ var letter = this.getCharAtPos( cursor, -1 );
762
+ if( letter )
704
763
  {
705
- const [word, from, to] = this.getWordAtPos( cursor, -1 );
764
+ var deleteFromPosition = cursor.position - 1;
765
+ var numCharsDeleted = 1;
706
766
 
707
- if( word.length > 1 )
767
+ // Delete full word
768
+ if( e.shiftKey )
708
769
  {
709
- deleteFromPosition = from;
710
- numCharsDeleted = word.length;
770
+ const [word, from, to] = this.getWordAtPos( cursor, -1 );
771
+
772
+ if( word.length > 1 )
773
+ {
774
+ deleteFromPosition = from;
775
+ numCharsDeleted = word.length;
776
+ }
711
777
  }
712
- }
713
778
 
714
- this.code.lines[ ln ] = sliceChars( this.code.lines[ ln ], deleteFromPosition, numCharsDeleted );
715
- this.processLine( ln );
779
+ this.code.lines[ ln ] = sliceChars( this.code.lines[ ln ], deleteFromPosition, numCharsDeleted );
780
+ this.processLine( ln );
716
781
 
717
- this.cursorToPosition( cursor, deleteFromPosition );
782
+ this.cursorToPosition( cursor, deleteFromPosition );
718
783
 
719
- if( this.useAutoComplete )
784
+ if( this.useAutoComplete )
785
+ {
786
+ this.showAutoCompleteBox( 'foo', cursor );
787
+ }
788
+ }
789
+ else if( this.code.lines[ ln - 1 ] != undefined )
720
790
  {
721
- this.showAutoCompleteBox( 'foo', cursor );
791
+ this.lineUp( cursor );
792
+ e.cancelShift = true;
793
+ this.actions[ 'End' ].callback( cursor.line, cursor, e );
794
+ // Move line on top
795
+ this.code.lines[ ln - 1 ] += this.code.lines[ ln ];
796
+ this.code.lines.splice( ln, 1 );
797
+ this.processLines();
722
798
  }
723
799
  }
724
- else if( this.code.lines[ ln - 1 ] != undefined ) {
725
-
726
- this.lineUp( cursor );
727
- e.cancelShift = true;
728
- this.actions[ 'End' ].callback( cursor.line, cursor, e );
729
- // Move line on top
730
- this.code.lines[ ln - 1 ] += this.code.lines[ ln ];
731
- this.code.lines.splice( ln, 1 );
732
- this.processLines();
733
- }
734
- }
735
- });
736
-
737
- this.action( 'Delete', false, ( ln, cursor, e ) => {
738
-
739
- this._addUndoStep( cursor );
740
800
 
741
- if( cursor.selection ) {
742
- // Use 'Backspace' as it's the same callback...
743
- this.actions['Backspace'].callback( ln, cursor, e );
744
- }
745
- else
746
- {
747
- var letter = this.getCharAtPos( cursor );
748
- if( letter ) {
749
- this.code.lines[ ln ] = sliceChars( this.code.lines[ ln ], cursor.position );
750
- this.processLine( ln );
751
- }
752
- else if( this.code.lines[ ln + 1 ] != undefined ) {
753
- this.code.lines[ ln ] += this.code.lines[ ln + 1 ];
754
- this.code.lines.splice( ln + 1, 1 );
755
- this.processLines();
756
- }
757
- }
758
- });
801
+ this.resizeIfNecessary( cursor, true );
802
+ });
759
803
 
760
- this.action( 'Tab', true, ( ln, cursor, e ) => {
804
+ this.action( 'Delete', false, ( ln, cursor, e ) => {
761
805
 
762
- if( this._skipTabs )
763
- {
764
- this._skipTabs--;
765
- if( !this._skipTabs )
766
- delete this._skipTabs;
767
- }
768
- else if( this.isAutoCompleteActive )
769
- {
770
- this.autoCompleteWord();
771
- }
772
- else
773
- {
774
806
  this._addUndoStep( cursor );
775
807
 
776
- if( e && e.shiftKey )
808
+ if( cursor.selection )
777
809
  {
778
- this._removeSpaces( cursor );
810
+ // Use 'Backspace' as it's the same callback...
811
+ this.actions['Backspace'].callback( ln, cursor, e );
779
812
  }
780
813
  else
781
814
  {
782
- const indentSpaces = this.tabSpaces - (cursor.position % this.tabSpaces);
783
- this._addSpaces( indentSpaces );
815
+ var letter = this.getCharAtPos( cursor );
816
+ if( letter )
817
+ {
818
+ this.code.lines[ ln ] = sliceChars( this.code.lines[ ln ], cursor.position );
819
+ this.processLine( ln );
820
+ }
821
+ else if( this.code.lines[ ln + 1 ] != undefined )
822
+ {
823
+ this.code.lines[ ln ] += this.code.lines[ ln + 1 ];
824
+ this.code.lines.splice( ln + 1, 1 );
825
+ this.processLines();
826
+ }
784
827
  }
785
- }
786
- }, "shiftKey");
787
-
788
- this.action( 'Home', false, ( ln, cursor, e ) => {
789
-
790
- let idx = firstNonspaceIndex( this.code.lines[ ln ] );
791
-
792
- // We already are in the first non space index...
793
- if( idx == cursor.position ) idx = 0;
794
-
795
- const prestring = this.code.lines[ ln ].substring( 0, idx );
796
- let lastX = cursor.position;
797
-
798
- this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
799
- if( idx > 0 )
800
- {
801
- this.cursorToString( cursor, prestring );
802
- }
803
- else
804
- {
805
- // No spaces, start from char 0
806
- idx = 0;
807
- }
808
828
 
809
- this.setScrollLeft( 0 );
810
- this.mergeCursors( ln );
829
+ this.resizeIfNecessary( cursor, true );
830
+ });
811
831
 
812
- if( e.shiftKey && !e.cancelShift )
813
- {
814
- // Get last selection range
815
- if( cursor.selection )
816
- {
817
- lastX += cursor.selection.chars;
818
- }
832
+ this.action( 'Tab', true, ( ln, cursor, e ) => {
819
833
 
820
- if( !cursor.selection )
834
+ if( this._skipTabs )
821
835
  {
822
- this.startSelection( cursor );
836
+ this._skipTabs--;
837
+ if( !this._skipTabs )
838
+ delete this._skipTabs;
823
839
  }
824
-
825
- var string = this.code.lines[ ln ].substring( idx, lastX );
826
- if( cursor.selection.sameLine() )
840
+ else if( this.isAutoCompleteActive )
827
841
  {
828
- cursor.selection.selectInline( cursor, idx, cursor.line, this.measureString( string ) );
842
+ this.autoCompleteWord();
829
843
  }
830
844
  else
831
845
  {
832
- this._processSelection( cursor, e );
846
+ this._addUndoStep( cursor );
847
+
848
+ if( e && e.shiftKey )
849
+ {
850
+ this._removeSpaces( cursor );
851
+ }
852
+ else
853
+ {
854
+ const indentSpaces = this.tabSpaces - (cursor.position % this.tabSpaces);
855
+ this._addSpaces( indentSpaces );
856
+ }
833
857
  }
834
- } else if( !e.keepSelection )
835
- this.endSelection();
836
- });
858
+ }, "shiftKey");
837
859
 
838
- this.action( 'End', false, ( ln, cursor, e ) => {
860
+ this.action( 'Home', false, ( ln, cursor, e ) => {
839
861
 
840
- if( ( e.shiftKey || e._shiftKey ) && !e.cancelShift ) {
862
+ let idx = firstNonspaceIndex( this.code.lines[ ln ] );
841
863
 
842
- var string = this.code.lines[ ln ].substring( cursor.position );
843
- if( !cursor.selection )
844
- this.startSelection( cursor );
845
- if( cursor.selection.sameLine() )
846
- cursor.selection.selectInline( cursor, cursor.position, cursor.line, this.measureString( string ));
864
+ // We already are in the first non space index...
865
+ if( idx == cursor.position ) idx = 0;
866
+
867
+ const prestring = this.code.lines[ ln ].substring( 0, idx );
868
+ let lastX = cursor.position;
869
+
870
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
871
+ if( idx > 0 )
872
+ {
873
+ this.cursorToString( cursor, prestring );
874
+ }
847
875
  else
848
876
  {
849
- this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
850
- this.cursorToString( cursor, this.code.lines[ ln ] );
851
- this._processSelection( cursor, e );
877
+ // No spaces, start from char 0
878
+ idx = 0;
852
879
  }
853
- } else if( !e.keepSelection )
854
- this.endSelection();
855
880
 
856
- this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
857
- this.cursorToString( cursor, this.code.lines[ ln ] );
881
+ this.setScrollLeft( 0 );
882
+ this.mergeCursors( ln );
858
883
 
859
- var viewportSizeX = ( this.codeScroller.clientWidth + this.getScrollLeft() ) - CodeEditor.LINE_GUTTER_WIDTH; // Gutter offset
860
- if( ( cursor.position * this.charWidth ) >= viewportSizeX )
861
- this.setScrollLeft( this.code.lines[ ln ].length * this.charWidth );
884
+ if( e.shiftKey && !e.cancelShift )
885
+ {
886
+ // Get last selection range
887
+ if( cursor.selection )
888
+ {
889
+ lastX += cursor.selection.chars;
890
+ }
862
891
 
863
- // Merge cursors
864
- this.mergeCursors( ln );
865
- });
892
+ if( !cursor.selection )
893
+ {
894
+ this.startSelection( cursor );
895
+ }
866
896
 
867
- this.action( 'Enter', true, ( ln, cursor, e ) => {
897
+ var string = this.code.lines[ ln ].substring( idx, lastX );
898
+ if( cursor.selection.sameLine() )
899
+ {
900
+ cursor.selection.selectInline( cursor, idx, cursor.line, this.measureString( string ) );
901
+ }
902
+ else
903
+ {
904
+ this._processSelection( cursor, e );
905
+ }
906
+ } else if( !e.keepSelection )
907
+ this.endSelection();
908
+ });
868
909
 
869
- // Add word
870
- if( this.isAutoCompleteActive )
871
- {
872
- this.autoCompleteWord();
873
- return;
874
- }
910
+ this.action( 'End', false, ( ln, cursor, e ) => {
875
911
 
876
- if( e.ctrlKey )
877
- {
878
- this.onrun( this.getText() );
879
- return;
880
- }
912
+ if( ( e.shiftKey || e._shiftKey ) && !e.cancelShift ) {
881
913
 
882
- this._addUndoStep( cursor, true );
914
+ var string = this.code.lines[ ln ].substring( cursor.position );
915
+ if( !cursor.selection )
916
+ this.startSelection( cursor );
917
+ if( cursor.selection.sameLine() )
918
+ cursor.selection.selectInline( cursor, cursor.position, cursor.line, this.measureString( string ));
919
+ else
920
+ {
921
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
922
+ this.cursorToString( cursor, this.code.lines[ ln ] );
923
+ this._processSelection( cursor, e );
924
+ }
925
+ } else if( !e.keepSelection )
926
+ this.endSelection();
883
927
 
884
- var _c0 = this.getCharAtPos( cursor, -1 );
885
- var _c1 = this.getCharAtPos( cursor );
928
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
929
+ this.cursorToString( cursor, this.code.lines[ ln ] );
886
930
 
887
- this.code.lines.splice( cursor.line + 1, 0, "" );
888
- this.code.lines[cursor.line + 1] = this.code.lines[ ln ].substr( cursor.position ); // new line (below)
889
- this.code.lines[ ln ] = this.code.lines[ ln ].substr( 0, cursor.position ); // line above
931
+ var viewportSizeX = ( this.codeScroller.clientWidth + this.getScrollLeft() ) - CodeEditor.LINE_GUTTER_WIDTH; // Gutter offset
932
+ if( ( cursor.position * this.charWidth ) >= viewportSizeX )
933
+ this.setScrollLeft( this.code.lines[ ln ].length * this.charWidth );
890
934
 
891
- this.lineDown( cursor, true );
935
+ // Merge cursors
936
+ this.mergeCursors( ln );
937
+ });
892
938
 
893
- // Check indentation
894
- var spaces = firstNonspaceIndex( this.code.lines[ ln ]);
895
- var tabs = Math.floor( spaces / this.tabSpaces );
939
+ this.action( 'Enter', true, ( ln, cursor, e ) => {
896
940
 
897
- if( _c0 == '{' && _c1 == '}' ) {
898
- this.code.lines.splice( cursor.line, 0, "" );
899
- this._addSpaceTabs( cursor, tabs + 1 );
900
- this.code.lines[ cursor.line + 1 ] = " ".repeat(spaces) + this.code.lines[ cursor.line + 1 ];
901
- } else {
902
- this._addSpaceTabs( cursor, tabs );
903
- }
941
+ // Add word
942
+ if( this.isAutoCompleteActive )
943
+ {
944
+ this.autoCompleteWord();
945
+ return;
946
+ }
904
947
 
905
- this.processLines();
906
- });
948
+ if( e.ctrlKey )
949
+ {
950
+ this.onrun( this.getText() );
951
+ return;
952
+ }
907
953
 
908
- this.action( 'ArrowUp', false, ( ln, cursor, e ) => {
954
+ this._addUndoStep( cursor, true );
909
955
 
910
- // Move cursor..
911
- if( !this.isAutoCompleteActive )
912
- {
913
- if( e.shiftKey ) {
914
- if( !cursor.selection )
915
- this.startSelection( cursor );
956
+ var _c0 = this.getCharAtPos( cursor, -1 );
957
+ var _c1 = this.getCharAtPos( cursor );
916
958
 
917
- this.lineUp( cursor );
959
+ this.code.lines.splice( cursor.line + 1, 0, "" );
960
+ this.code.lines[cursor.line + 1] = this.code.lines[ ln ].substr( cursor.position ); // new line (below)
961
+ this.code.lines[ ln ] = this.code.lines[ ln ].substr( 0, cursor.position ); // line above
918
962
 
919
- var letter = this.getCharAtPos( cursor );
920
- if( !letter ) {
921
- this.cursorToPosition( cursor, this.code.lines[ cursor.line ].length );
922
- }
963
+ this.lineDown( cursor, true );
923
964
 
924
- this._processSelection( cursor, e, false );
965
+ // Check indentation
966
+ var spaces = firstNonspaceIndex( this.code.lines[ ln ]);
967
+ var tabs = Math.floor( spaces / this.tabSpaces );
925
968
 
969
+ if( _c0 == '{' && _c1 == '}' ) {
970
+ this.code.lines.splice( cursor.line, 0, "" );
971
+ this._addSpaceTabs( cursor, tabs + 1 );
972
+ this.code.lines[ cursor.line + 1 ] = " ".repeat(spaces) + this.code.lines[ cursor.line + 1 ];
926
973
  } else {
927
- this.endSelection();
928
- this.lineUp( cursor );
929
- // Go to end of line if out of line
930
- var letter = this.getCharAtPos( cursor );
931
- if( !letter ) this.actions['End'].callback( cursor.line, cursor, e );
974
+ this._addSpaceTabs( cursor, tabs );
932
975
  }
933
- }
934
- // Move up autocomplete selection
935
- else
936
- {
937
- this._moveArrowSelectedAutoComplete('up');
938
- }
939
- });
940
976
 
941
- this.action( 'ArrowDown', false, ( ln, cursor, e ) => {
977
+ this.processLines();
978
+ });
942
979
 
943
- // Move cursor..
944
- if( !this.isAutoCompleteActive )
945
- {
946
- if( e.shiftKey ) {
947
- if( !cursor.selection )
948
- this.startSelection( cursor );
949
- } else {
950
- this.endSelection();
951
- }
980
+ this.action( 'ArrowUp', false, ( ln, cursor, e ) => {
981
+
982
+ // Move cursor..
983
+ if( !this.isAutoCompleteActive )
984
+ {
985
+ if( e.shiftKey ) {
986
+ if( !cursor.selection )
987
+ this.startSelection( cursor );
952
988
 
953
- const canGoDown = this.lineDown( cursor );
954
- const letter = this.getCharAtPos( cursor );
989
+ this.lineUp( cursor );
955
990
 
956
- // Go to end of line if out of range
957
- if( !letter || !canGoDown ) {
958
- this.cursorToPosition( cursor, Math.max(this.code.lines[ cursor.line ].length, 0) );
959
- }
991
+ var letter = this.getCharAtPos( cursor );
992
+ if( !letter ) {
993
+ this.cursorToPosition( cursor, this.code.lines[ cursor.line ].length );
994
+ }
960
995
 
961
- if( e.shiftKey ) {
962
- this._processSelection( cursor, e );
996
+ this._processSelection( cursor, e, false );
997
+
998
+ } else {
999
+ this.endSelection();
1000
+ this.lineUp( cursor );
1001
+ // Go to end of line if out of line
1002
+ var letter = this.getCharAtPos( cursor );
1003
+ if( !letter ) this.actions['End'].callback( cursor.line, cursor, e );
1004
+ }
963
1005
  }
964
- }
965
- // Move down autocomplete selection
966
- else
967
- {
968
- this._moveArrowSelectedAutoComplete('down');
969
- }
970
- });
1006
+ // Move up autocomplete selection
1007
+ else
1008
+ {
1009
+ this._moveArrowSelectedAutoComplete('up');
1010
+ }
1011
+ });
971
1012
 
972
- this.action( 'ArrowLeft', false, ( ln, cursor, e ) => {
1013
+ this.action( 'ArrowDown', false, ( ln, cursor, e ) => {
973
1014
 
974
- // Nothing to do..
975
- if( cursor.line == 0 && cursor.position == 0 )
976
- return;
1015
+ // Move cursor..
1016
+ if( !this.isAutoCompleteActive )
1017
+ {
1018
+ if( e.shiftKey ) {
1019
+ if( !cursor.selection )
1020
+ this.startSelection( cursor );
1021
+ } else {
1022
+ this.endSelection();
1023
+ }
977
1024
 
978
- if( e.metaKey ) { // Apple devices (Command)
979
- e.preventDefault();
980
- this.actions[ 'Home' ].callback( ln, cursor, e );
981
- }
982
- else if( e.ctrlKey ) {
983
- // Get next word
984
- const [word, from, to] = this.getWordAtPos( cursor, -1 );
985
- // If no length, we change line..
986
- if( !word.length && this.lineUp( cursor, true ) ) {
987
- const cS = e.cancelShift, kS = e.keepSelection;
988
- e.cancelShift = true;
989
- e.keepSelection = true;
990
- this.actions[ 'End' ].callback( cursor.line, cursor, e );
991
- e.cancelShift = cS;
992
- e.keepSelection = kS;
993
- }
994
- var diff = Math.max( cursor.position - from, 1 );
995
- var substr = word.substr( 0, diff );
1025
+ const canGoDown = this.lineDown( cursor );
1026
+ const letter = this.getCharAtPos( cursor );
996
1027
 
997
- // Selections...
998
- if( e.shiftKey ) {
999
- if( !cursor.selection )
1000
- this.startSelection( cursor );
1028
+ // Go to end of line if out of range
1029
+ if( !letter || !canGoDown ) {
1030
+ this.cursorToPosition( cursor, Math.max(this.code.lines[ cursor.line ].length, 0) );
1031
+ }
1032
+
1033
+ if( e.shiftKey ) {
1034
+ this._processSelection( cursor, e );
1035
+ }
1001
1036
  }
1037
+ // Move down autocomplete selection
1002
1038
  else
1003
- this.endSelection();
1039
+ {
1040
+ this._moveArrowSelectedAutoComplete('down');
1041
+ }
1042
+ });
1004
1043
 
1005
- this.cursorToString( cursor, substr, true );
1044
+ this.action( 'ArrowLeft', false, ( ln, cursor, e ) => {
1006
1045
 
1007
- if( e.shiftKey )
1008
- this._processSelection( cursor, e );
1009
- }
1010
- else {
1011
- var letter = this.getCharAtPos( cursor, -1 );
1012
- if( letter ) {
1046
+ // Nothing to do..
1047
+ if( cursor.line == 0 && cursor.position == 0 )
1048
+ return;
1049
+
1050
+ if( e.metaKey ) { // Apple devices (Command)
1051
+ e.preventDefault();
1052
+ this.actions[ 'Home' ].callback( ln, cursor, e );
1053
+ }
1054
+ else if( e.ctrlKey ) {
1055
+ // Get next word
1056
+ const [word, from, to] = this.getWordAtPos( cursor, -1 );
1057
+ // If no length, we change line..
1058
+ if( !word.length && this.lineUp( cursor, true ) ) {
1059
+ const cS = e.cancelShift, kS = e.keepSelection;
1060
+ e.cancelShift = true;
1061
+ e.keepSelection = true;
1062
+ this.actions[ 'End' ].callback( cursor.line, cursor, e );
1063
+ e.cancelShift = cS;
1064
+ e.keepSelection = kS;
1065
+ }
1066
+ var diff = Math.max( cursor.position - from, 1 );
1067
+ var substr = word.substr( 0, diff );
1068
+
1069
+ // Selections...
1013
1070
  if( e.shiftKey ) {
1014
- if( !cursor.selection ) this.startSelection( cursor );
1015
- this.cursorToLeft( letter, cursor );
1016
- this._processSelection( cursor, e, false, CodeEditor.SELECTION_X );
1071
+ if( !cursor.selection )
1072
+ this.startSelection( cursor );
1017
1073
  }
1018
- else {
1019
- if( !cursor.selection ) {
1074
+ else
1075
+ this.endSelection();
1076
+
1077
+ this.cursorToString( cursor, substr, true );
1078
+
1079
+ if( e.shiftKey )
1080
+ this._processSelection( cursor, e );
1081
+ }
1082
+ else {
1083
+ var letter = this.getCharAtPos( cursor, -1 );
1084
+ if( letter ) {
1085
+ if( e.shiftKey ) {
1086
+ if( !cursor.selection ) this.startSelection( cursor );
1020
1087
  this.cursorToLeft( letter, cursor );
1021
- if( this.useAutoComplete && this.isAutoCompleteActive )
1022
- this.showAutoCompleteBox( 'foo', cursor );
1088
+ this._processSelection( cursor, e, false, CodeEditor.SELECTION_X );
1023
1089
  }
1024
1090
  else {
1025
- cursor.selection.invertIfNecessary();
1026
- this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, cursor );
1027
- this.cursorToLine( cursor, cursor.selection.fromY, true );
1028
- this.cursorToPosition( cursor, cursor.selection.fromX );
1029
- this.endSelection();
1091
+ if( !cursor.selection ) {
1092
+ this.cursorToLeft( letter, cursor );
1093
+ if( this.useAutoComplete && this.isAutoCompleteActive )
1094
+ this.showAutoCompleteBox( 'foo', cursor );
1095
+ }
1096
+ else {
1097
+ cursor.selection.invertIfNecessary();
1098
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, cursor );
1099
+ this.cursorToLine( cursor, cursor.selection.fromY, true );
1100
+ this.cursorToPosition( cursor, cursor.selection.fromX );
1101
+ this.endSelection();
1102
+ }
1030
1103
  }
1031
1104
  }
1032
- }
1033
- else if( cursor.line > 0 ) {
1105
+ else if( cursor.line > 0 ) {
1034
1106
 
1035
- if( e.shiftKey && !cursor.selection ) this.startSelection( cursor );
1107
+ if( e.shiftKey && !cursor.selection ) this.startSelection( cursor );
1036
1108
 
1037
- this.lineUp( cursor );
1109
+ this.lineUp( cursor );
1038
1110
 
1039
- e.cancelShift = e.keepSelection = true;
1040
- this.actions[ 'End' ].callback( cursor.line, cursor, e );
1041
- delete e.cancelShift; delete e.keepSelection;
1111
+ e.cancelShift = e.keepSelection = true;
1112
+ this.actions[ 'End' ].callback( cursor.line, cursor, e );
1113
+ delete e.cancelShift; delete e.keepSelection;
1042
1114
 
1043
- if( e.shiftKey ) this._processSelection( cursor, e, false );
1115
+ if( e.shiftKey ) this._processSelection( cursor, e, false );
1116
+ }
1044
1117
  }
1045
- }
1046
- });
1047
-
1048
- this.action( 'ArrowRight', false, ( ln, cursor, e ) => {
1118
+ });
1049
1119
 
1050
- // Nothing to do..
1051
- if( cursor.line == this.code.lines.length - 1 &&
1052
- cursor.position == this.code.lines[ cursor.line ].length )
1053
- return;
1120
+ this.action( 'ArrowRight', false, ( ln, cursor, e ) => {
1054
1121
 
1055
- if( e.metaKey ) // Apple devices (Command)
1056
- {
1057
- e.preventDefault();
1058
- this.actions[ 'End' ].callback( ln, cursor );
1059
- }
1060
- else if( e.ctrlKey ) // Next word
1061
- {
1062
- // Get next word
1063
- const [ word, from, to ] = this.getWordAtPos( cursor );
1064
-
1065
- // If no length, we change line..
1066
- if( !word.length ) this.lineDown( cursor, true );
1067
- var diff = cursor.position - from;
1068
- var substr = word.substr( diff );
1122
+ // Nothing to do..
1123
+ if( cursor.line == this.code.lines.length - 1 &&
1124
+ cursor.position == this.code.lines[ cursor.line ].length )
1125
+ return;
1069
1126
 
1070
- // Selections...
1071
- if( e.shiftKey ) {
1072
- if( !cursor.selection )
1073
- this.startSelection( cursor );
1127
+ if( e.metaKey ) // Apple devices (Command)
1128
+ {
1129
+ e.preventDefault();
1130
+ this.actions[ 'End' ].callback( ln, cursor );
1074
1131
  }
1075
- else
1076
- this.endSelection();
1077
-
1078
- this.cursorToString( cursor, substr );
1132
+ else if( e.ctrlKey ) // Next word
1133
+ {
1134
+ // Get next word
1135
+ const [ word, from, to ] = this.getWordAtPos( cursor );
1079
1136
 
1080
- if( e.shiftKey )
1081
- this._processSelection( cursor, e );
1082
- }
1083
- else // Next char
1084
- {
1085
- var letter = this.getCharAtPos( cursor );
1086
- if( letter ) {
1137
+ // If no length, we change line..
1138
+ if( !word.length ) this.lineDown( cursor, true );
1139
+ var diff = cursor.position - from;
1140
+ var substr = word.substr( diff );
1087
1141
 
1088
- // Selecting chars
1089
- if( e.shiftKey )
1090
- {
1142
+ // Selections...
1143
+ if( e.shiftKey ) {
1091
1144
  if( !cursor.selection )
1092
1145
  this.startSelection( cursor );
1093
-
1094
- this.cursorToRight( letter, cursor );
1095
- this._processSelection( cursor, e, false, CodeEditor.SELECTION_X );
1096
1146
  }
1097
1147
  else
1098
- {
1099
- if( !cursor.selection ) {
1148
+ this.endSelection();
1149
+
1150
+ this.cursorToString( cursor, substr );
1151
+
1152
+ if( e.shiftKey )
1153
+ this._processSelection( cursor, e );
1154
+ }
1155
+ else // Next char
1156
+ {
1157
+ var letter = this.getCharAtPos( cursor );
1158
+ if( letter ) {
1159
+
1160
+ // Selecting chars
1161
+ if( e.shiftKey )
1162
+ {
1163
+ if( !cursor.selection )
1164
+ this.startSelection( cursor );
1165
+
1100
1166
  this.cursorToRight( letter, cursor );
1101
- if( this.useAutoComplete && this.isAutoCompleteActive )
1102
- this.showAutoCompleteBox( 'foo', cursor );
1167
+ this._processSelection( cursor, e, false, CodeEditor.SELECTION_X );
1103
1168
  }
1104
1169
  else
1105
1170
  {
1106
- cursor.selection.invertIfNecessary();
1107
- this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, cursor );
1108
- this.cursorToLine( cursor, cursor.selection.toY );
1109
- this.cursorToPosition( cursor, cursor.selection.toX );
1110
- this.endSelection();
1171
+ if( !cursor.selection ) {
1172
+ this.cursorToRight( letter, cursor );
1173
+ if( this.useAutoComplete && this.isAutoCompleteActive )
1174
+ this.showAutoCompleteBox( 'foo', cursor );
1175
+ }
1176
+ else
1177
+ {
1178
+ cursor.selection.invertIfNecessary();
1179
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, cursor );
1180
+ this.cursorToLine( cursor, cursor.selection.toY );
1181
+ this.cursorToPosition( cursor, cursor.selection.toX );
1182
+ this.endSelection();
1183
+ }
1111
1184
  }
1112
1185
  }
1113
- }
1114
- else if( this.code.lines[ cursor.line + 1 ] !== undefined ) {
1186
+ else if( this.code.lines[ cursor.line + 1 ] !== undefined ) {
1115
1187
 
1116
- if( e.shiftKey ) {
1117
- if( !cursor.selection ) this.startSelection( cursor );
1118
- }
1119
- else this.endSelection();
1188
+ if( e.shiftKey ) {
1189
+ if( !cursor.selection ) this.startSelection( cursor );
1190
+ }
1191
+ else this.endSelection();
1120
1192
 
1121
- this.lineDown( cursor, true );
1193
+ this.lineDown( cursor, true );
1122
1194
 
1123
- if( e.shiftKey ) this._processSelection( cursor, e, false );
1195
+ if( e.shiftKey ) this._processSelection( cursor, e, false );
1124
1196
 
1125
- this.hideAutoCompleteBox();
1197
+ this.hideAutoCompleteBox();
1198
+ }
1126
1199
  }
1127
- }
1128
- });
1200
+ });
1201
+ }
1129
1202
 
1130
1203
  // Default code tab
1131
1204
 
@@ -1158,6 +1231,21 @@ class CodeEditor {
1158
1231
  }
1159
1232
 
1160
1233
  LX.emit( "@font-size", this.fontSize );
1234
+
1235
+ // Get final sizes for editor elements based on Tabs and status bar offsets
1236
+ LX.doAsync( () => {
1237
+ this._verticalTopOffset = this.tabs?.root.getBoundingClientRect().height ?? 0;
1238
+ this._verticalBottomOffset = this.statusPanel?.root.getBoundingClientRect().height ?? 0;
1239
+ this._fullVerticalOffset = this._verticalTopOffset + this._verticalBottomOffset;
1240
+
1241
+ this.gutter.style.marginTop = `${ this._verticalTopOffset }px`;
1242
+ this.gutter.style.height = `calc(100% - ${ this._fullVerticalOffset }px)`;
1243
+ this.vScrollbar.root.style.marginTop = `${ this.tabs?.root.getBoundingClientRect().height ?? 0 }px`;
1244
+ this.vScrollbar.root.style.height = `calc(100% - ${ this._fullVerticalOffset }px)`;
1245
+ this.hScrollbar.root.style.bottom = `${ this._verticalBottomOffset }px`;
1246
+ this.codeArea.root.style.height = `calc(100% - ${ this._verticalBottomOffset }px)`;
1247
+ }, 50 );
1248
+
1161
1249
  });
1162
1250
 
1163
1251
  window.editor = this;
@@ -1201,39 +1289,38 @@ class CodeEditor {
1201
1289
  onKeyPressed( e ) {
1202
1290
 
1203
1291
  // Toggle visibility of the file explorer
1204
- if( e.key == 'b' && e.ctrlKey && this.explorer )
1292
+ if( e.key == 'b' && e.ctrlKey && this.useFileExplorer )
1205
1293
  {
1206
1294
  this.explorerArea.root.classList.toggle( "hidden" );
1207
1295
  if( this._lastBaseareaWidth )
1208
1296
  {
1209
- this.base_area.root.style.width = this._lastBaseareaWidth;
1297
+ this.baseArea.root.style.width = this._lastBaseareaWidth;
1210
1298
  delete this._lastBaseareaWidth;
1211
1299
 
1212
1300
  } else
1213
1301
  {
1214
- this._lastBaseareaWidth = this.base_area.root.style.width;
1215
- this.base_area.root.style.width = "100%";
1302
+ this._lastBaseareaWidth = this.baseArea.root.style.width;
1303
+ this.baseArea.root.style.width = "100%";
1216
1304
  }
1217
1305
  }
1218
1306
  }
1219
1307
 
1220
1308
  getText( min ) {
1221
-
1222
1309
  return this.code.lines.join( min ? ' ' : '\n' );
1223
1310
  }
1224
1311
 
1225
1312
  // This can be used to empty all text...
1226
1313
  setText( text = "", lang ) {
1227
1314
 
1228
- let new_lines = text.split( '\n' );
1229
- this.code.lines = [].concat( new_lines );
1315
+ let newLines = text.split( '\n' );
1316
+ this.code.lines = [].concat( newLines );
1230
1317
 
1231
1318
  this._removeSecondaryCursors();
1232
1319
 
1233
- let cursor = this._getCurrentCursor( true );
1234
- let lastLine = new_lines.pop();
1320
+ let cursor = this.getCurrentCursor( true );
1321
+ let lastLine = newLines.pop();
1235
1322
 
1236
- this.cursorToLine( cursor, new_lines.length ); // Already substracted 1
1323
+ this.cursorToLine( cursor, newLines.length ); // Already substracted 1
1237
1324
  this.cursorToPosition( cursor, lastLine.length );
1238
1325
  this.processLines();
1239
1326
 
@@ -1247,21 +1334,22 @@ class CodeEditor {
1247
1334
 
1248
1335
  let lidx = cursor.line;
1249
1336
 
1250
- if( cursor.selection ) {
1337
+ if( cursor.selection )
1338
+ {
1251
1339
  this.deleteSelection( cursor );
1252
1340
  lidx = cursor.line;
1253
1341
  }
1254
1342
 
1255
1343
  this.endSelection();
1256
1344
 
1257
- const new_lines = text.replaceAll( '\r', '' ).split( '\n' );
1345
+ const newLines = text.replaceAll( '\r', '' ).split( '\n' );
1258
1346
 
1259
1347
  // Pasting Multiline...
1260
- if( new_lines.length != 1 )
1348
+ if( newLines.length != 1 )
1261
1349
  {
1262
- let num_lines = new_lines.length;
1350
+ let num_lines = newLines.length;
1263
1351
  console.assert( num_lines > 0 );
1264
- const first_line = new_lines.shift();
1352
+ const first_line = newLines.shift();
1265
1353
  num_lines--;
1266
1354
 
1267
1355
  const remaining = this.code.lines[ lidx ].slice( cursor.position );
@@ -1278,11 +1366,11 @@ class CodeEditor {
1278
1366
 
1279
1367
  let _text = null;
1280
1368
 
1281
- for( var i = 0; i < new_lines.length; ++i ) {
1282
- _text = new_lines[ i ];
1369
+ for( var i = 0; i < newLines.length; ++i ) {
1370
+ _text = newLines[ i ];
1283
1371
  this.cursorToLine( cursor, cursor.line++, true );
1284
1372
  // Add remaining...
1285
- if( i == (new_lines.length - 1) )
1373
+ if( i == (newLines.length - 1) )
1286
1374
  _text += remaining;
1287
1375
  this.code.lines.splice( 1 + lidx + i, 0, _text );
1288
1376
  }
@@ -1296,24 +1384,26 @@ class CodeEditor {
1296
1384
  {
1297
1385
  this.code.lines[ lidx ] = [
1298
1386
  this.code.lines[ lidx ].slice( 0, cursor.position ),
1299
- new_lines[ 0 ],
1387
+ newLines[ 0 ],
1300
1388
  this.code.lines[ lidx ].slice( cursor.position )
1301
1389
  ].join('');
1302
1390
 
1303
- this.cursorToPosition( cursor, ( cursor.position + new_lines[ 0 ].length ) );
1391
+ this.cursorToPosition( cursor, ( cursor.position + newLines[ 0 ].length ) );
1304
1392
  this.processLine( lidx );
1305
1393
  }
1306
1394
 
1307
- this.resize( null, ( scrollWidth, scrollHeight ) => {
1395
+ this.resize( CodeEditor.RESIZE_SCROLLBAR_H_V, null, ( scrollWidth, scrollHeight ) => {
1308
1396
  var viewportSizeX = ( this.codeScroller.clientWidth + this.getScrollLeft() ) - CodeEditor.LINE_GUTTER_WIDTH; // Gutter offset
1309
1397
  if( ( cursor.position * this.charWidth ) >= viewportSizeX )
1398
+ {
1310
1399
  this.setScrollLeft( this.code.lines[ lidx ].length * this.charWidth );
1400
+ }
1311
1401
  } );
1312
1402
  }
1313
1403
 
1314
1404
  loadFile( file, options = {} ) {
1315
1405
 
1316
- const inner_add_tab = ( text, name, title ) => {
1406
+ const _innerAddTab = ( text, name, title ) => {
1317
1407
 
1318
1408
  // Remove Carriage Return in some cases and sub tabs using spaces
1319
1409
  text = text.replaceAll( '\r', '' );
@@ -1325,7 +1415,7 @@ class CodeEditor {
1325
1415
 
1326
1416
  // Add item in the explorer if used
1327
1417
 
1328
- if( this.explorer )
1418
+ if( this.useFileExplorer || this.skipTabs )
1329
1419
  {
1330
1420
  this._tabStorage[ name ] = {
1331
1421
  lines: lines,
@@ -1333,8 +1423,16 @@ class CodeEditor {
1333
1423
  };
1334
1424
 
1335
1425
  const ext = CodeEditor.languages[ options.language ] ?. ext;
1336
- this.addExplorerItem( { id: name, skipVisibility: true, icon: this._getFileIcon( name, ext ) } );
1337
- this.explorer.innerTree.frefresh( name );
1426
+
1427
+ if( this.useFileExplorer )
1428
+ {
1429
+ this.addExplorerItem( { id: name, skipVisibility: true, icon: this._getFileIcon( name, ext ) } );
1430
+ this.explorer.innerTree.frefresh( name );
1431
+ }
1432
+ else
1433
+ {
1434
+
1435
+ }
1338
1436
  }
1339
1437
  else
1340
1438
  {
@@ -1359,7 +1457,7 @@ class CodeEditor {
1359
1457
  let filename = file;
1360
1458
  LX.request({ url: filename, success: text => {
1361
1459
  const name = filename.substring(filename.lastIndexOf( '/' ) + 1);
1362
- inner_add_tab( text, name, filename );
1460
+ _innerAddTab( text, name, filename );
1363
1461
  } });
1364
1462
  }
1365
1463
  else // File Blob
@@ -1368,86 +1466,11 @@ class CodeEditor {
1368
1466
  fr.readAsText( file );
1369
1467
  fr.onload = e => {
1370
1468
  const text = e.currentTarget.result;
1371
- inner_add_tab( text, file.name );
1469
+ _innerAddTab( text, file.name );
1372
1470
  };
1373
1471
  }
1374
1472
  }
1375
1473
 
1376
- _addCursor( line = 0, position = 0, force, isMain = false ) {
1377
-
1378
- // If cursor in that position exists, remove it instead..
1379
- const exists = Array.from( this.cursors.children ).find( v => v.position == position && v.line == line );
1380
- if( exists && !force )
1381
- {
1382
- if( !exists.isMain )
1383
- exists.remove();
1384
-
1385
- return;
1386
- }
1387
-
1388
- let cursor = document.createElement( 'div' );
1389
- cursor.name = "cursor" + this.cursors.childElementCount;
1390
- cursor.className = "cursor";
1391
- cursor.innerHTML = "&nbsp;";
1392
- cursor.isMain = isMain;
1393
- cursor._left = position * this.charWidth;
1394
- cursor.style.left = "calc( " + cursor._left + "px + " + this.xPadding + " )";
1395
- cursor._top = line * this.lineHeight;
1396
- cursor.style.top = cursor._top + "px";
1397
- cursor._position = position;
1398
- cursor._line = line;
1399
- cursor.print = (function() { console.log( this._line, this._position ) }).bind( cursor );
1400
- cursor.isLast = (function() { return this.cursors.lastChild == cursor; }).bind( this );
1401
-
1402
- Object.defineProperty( cursor, 'line', {
1403
- get: (v) => { return cursor._line },
1404
- set: (v) => {
1405
- cursor._line = v;
1406
- if( cursor.isMain ) this._setActiveLine( v );
1407
- }
1408
- } );
1409
-
1410
- Object.defineProperty( cursor, 'position', {
1411
- get: (v) => { return cursor._position },
1412
- set: (v) => {
1413
- cursor._position = v;
1414
- if( cursor.isMain )
1415
- {
1416
- const activeLine = this.state.activeLine;
1417
- this._updateDataInfoPanel( "@cursor-data", `Ln ${ activeLine + 1 }, Col ${ v + 1 }` );
1418
- }
1419
- }
1420
- } );
1421
-
1422
- this.cursors.appendChild( cursor );
1423
-
1424
- return cursor;
1425
- }
1426
-
1427
- _getCurrentCursor( removeOthers ) {
1428
-
1429
- if( removeOthers )
1430
- {
1431
- this._removeSecondaryCursors();
1432
- }
1433
-
1434
- return this.cursors.children[ 0 ];
1435
- }
1436
-
1437
- _removeSecondaryCursors() {
1438
-
1439
- while( this.cursors.childElementCount > 1 )
1440
- this.cursors.lastChild.remove();
1441
- }
1442
-
1443
- _logCursors() {
1444
-
1445
- for( let cursor of this.cursors.children )
1446
- {
1447
- cursor.print();
1448
- }
1449
- }
1450
-
1451
1474
  _addUndoStep( cursor, force, deleteRedo = true ) {
1452
1475
 
1453
1476
  // Only the mainc cursor stores undo steps
@@ -1459,12 +1482,18 @@ class CodeEditor {
1459
1482
 
1460
1483
  if( !force )
1461
1484
  {
1462
- if( !this._lastTime ) {
1485
+ if( !this._lastTime )
1486
+ {
1463
1487
  this._lastTime = current;
1464
- } else {
1465
- if( ( current - this._lastTime ) > 2000 ){
1488
+ }
1489
+ else
1490
+ {
1491
+ if( ( current - this._lastTime ) > 2000 )
1492
+ {
1466
1493
  this._lastTime = null;
1467
- } else {
1494
+ }
1495
+ else
1496
+ {
1468
1497
  // If time not enough, reset timer
1469
1498
  this._lastTime = current;
1470
1499
  return;
@@ -1518,7 +1547,9 @@ class CodeEditor {
1518
1547
 
1519
1548
  // Only the mainc cursor stores redo steps
1520
1549
  if( !cursor.isMain )
1550
+ {
1521
1551
  return;
1552
+ }
1522
1553
 
1523
1554
  this.code.redoSteps.push( {
1524
1555
  lines: LX.deepCopy( this.code.lines ),
@@ -1548,7 +1579,9 @@ class CodeEditor {
1548
1579
 
1549
1580
  // Generate new if needed
1550
1581
  if( !currentCursor )
1582
+ {
1551
1583
  currentCursor = this._addCursor();
1584
+ }
1552
1585
 
1553
1586
  this.restoreCursor( currentCursor, step.cursors[ i ] );
1554
1587
  }
@@ -1571,6 +1604,7 @@ class CodeEditor {
1571
1604
  const icon = this._getFileIcon( null, ext );
1572
1605
 
1573
1606
  // Update tab icon
1607
+ if( !this.skipTabs )
1574
1608
  {
1575
1609
  const tab = this.tabs.tabDOMs[ this.code.tabName ];
1576
1610
  tab.firstChild.remove();
@@ -1589,7 +1623,7 @@ class CodeEditor {
1589
1623
  }
1590
1624
 
1591
1625
  // Update explorer icon
1592
- if( this.explorer )
1626
+ if( this.useFileExplorer )
1593
1627
  {
1594
1628
  const item = this.explorer.innerTree.data.children.filter( (v) => v.id === this.code.tabName )[ 0 ];
1595
1629
  console.assert( item != undefined );
@@ -1631,144 +1665,148 @@ class CodeEditor {
1631
1665
 
1632
1666
  _createStatusPanel() {
1633
1667
 
1634
- if( !this.skipInfo )
1668
+ if( this.skipInfo )
1635
1669
  {
1636
- let panel = new LX.Panel({ className: "lexcodetabinfo flex flex-row", height: "auto" });
1670
+ return;
1671
+ }
1637
1672
 
1638
- let leftStatusPanel = new LX.Panel( { id: "FontSizeZoomStatusComponent", height: "auto" } );
1639
- leftStatusPanel.sameLine();
1640
- leftStatusPanel.addButton( null, "ZoomOutButton", this._decreaseFontSize.bind( this ), { icon: "ZoomOut", width: "32px", title: "Zoom Out", tooltip: true } );
1641
- leftStatusPanel.addLabel( this.fontSize ?? 14, { fit: true, signal: "@font-size" });
1642
- leftStatusPanel.addButton( null, "ZoomInButton", this._increaseFontSize.bind( this ), { icon: "ZoomIn", width: "32px", title: "Zoom In", tooltip: true } );
1643
- leftStatusPanel.endLine( "justify-start" );
1644
- panel.attach( leftStatusPanel.root );
1645
-
1646
- let rightStatusPanel = new LX.Panel( { height: "auto" } );
1647
- rightStatusPanel.sameLine();
1648
- rightStatusPanel.addLabel( this.code.title, { id: "EditorFilenameStatusComponent", fit: true, signal: "@tab-name" });
1649
- rightStatusPanel.addButton( null, "Ln 1, Col 1", this.showSearchLineBox.bind( this ), { id: "EditorSelectionStatusComponent", fit: true, signal: "@cursor-data" });
1650
- rightStatusPanel.addButton( null, "Spaces: " + this.tabSpaces, ( value, event ) => {
1651
- LX.addContextMenu( "Spaces", event, m => {
1652
- const options = [ 2, 4, 8 ];
1653
- for( const n of options )
1654
- m.add( n, (v) => {
1655
- this.tabSpaces = v;
1656
- this.processLines();
1657
- this._updateDataInfoPanel( "@tab-spaces", "Spaces: " + this.tabSpaces );
1658
- } );
1659
- });
1660
- }, { id: "EditorIndentationStatusComponent", nameWidth: "15%", signal: "@tab-spaces" });
1661
- rightStatusPanel.addButton( "<b>{ }</b>", this.highlight, ( value, event ) => {
1662
- LX.addContextMenu( "Language", event, m => {
1663
- for( const lang of Object.keys( CodeEditor.languages ) )
1664
- {
1665
- m.add( lang, v => {
1666
- this._changeLanguage( v, null, true )
1667
- } );
1668
- }
1669
- });
1670
- }, { id: "EditorLanguageStatusComponent", nameWidth: "15%", signal: "@highlight" });
1671
- rightStatusPanel.endLine( "justify-end" );
1672
- panel.attach( rightStatusPanel.root );
1673
-
1674
- const itemVisibilityMap = {
1675
- "Font Size Zoom": true,
1676
- "Editor Filename": true,
1677
- "Editor Selection": true,
1678
- "Editor Indentation": true,
1679
- "Editor Language": true,
1680
- };
1673
+ let panel = new LX.Panel({ className: "lexcodetabinfo flex flex-row", height: "auto" });
1674
+
1675
+ let leftStatusPanel = new LX.Panel( { id: "FontSizeZoomStatusComponent", height: "auto" } );
1676
+ leftStatusPanel.sameLine();
1681
1677
 
1682
- panel.root.addEventListener( "contextmenu", (e) => {
1678
+ if( this.skipTabs )
1679
+ {
1680
+ leftStatusPanel.addButton( null, "ZoomOutButton", this._decreaseFontSize.bind( this ), { icon: "ZoomOut", width: "32px", title: "Zoom Out", tooltip: true } );
1681
+ }
1683
1682
 
1684
- if( e.target && ( e.target.classList.contains( "lexpanel" ) || e.target.classList.contains( "lexinlinecomponents" ) ) )
1683
+ leftStatusPanel.addButton( null, "ZoomOutButton", this._decreaseFontSize.bind( this ), { icon: "ZoomOut", width: "32px", title: "Zoom Out", tooltip: true } );
1684
+ leftStatusPanel.addLabel( this.fontSize ?? 14, { fit: true, signal: "@font-size" });
1685
+ leftStatusPanel.addButton( null, "ZoomInButton", this._increaseFontSize.bind( this ), { icon: "ZoomIn", width: "32px", title: "Zoom In", tooltip: true } );
1686
+ leftStatusPanel.endLine( "justify-start" );
1687
+ panel.attach( leftStatusPanel.root );
1688
+
1689
+ let rightStatusPanel = new LX.Panel( { height: "auto" } );
1690
+ rightStatusPanel.sameLine();
1691
+ rightStatusPanel.addLabel( this.code.title, { id: "EditorFilenameStatusComponent", fit: true, signal: "@tab-name" });
1692
+ rightStatusPanel.addButton( null, "Ln 1, Col 1", this.showSearchLineBox.bind( this ), { id: "EditorSelectionStatusComponent", fit: true, signal: "@cursor-data" });
1693
+ rightStatusPanel.addButton( null, "Spaces: " + this.tabSpaces, ( value, event ) => {
1694
+ LX.addContextMenu( "Spaces", event, m => {
1695
+ const options = [ 2, 4, 8 ];
1696
+ for( const n of options )
1697
+ m.add( n, (v) => {
1698
+ this.tabSpaces = v;
1699
+ this.processLines();
1700
+ this._updateDataInfoPanel( "@tab-spaces", "Spaces: " + this.tabSpaces );
1701
+ } );
1702
+ });
1703
+ }, { id: "EditorIndentationStatusComponent", nameWidth: "15%", signal: "@tab-spaces" });
1704
+ rightStatusPanel.addButton( "<b>{ }</b>", this.highlight, ( value, event ) => {
1705
+ LX.addContextMenu( "Language", event, m => {
1706
+ for( const lang of Object.keys( CodeEditor.languages ) )
1685
1707
  {
1686
- return;
1708
+ m.add( lang, v => {
1709
+ this._changeLanguage( v, null, true )
1710
+ } );
1687
1711
  }
1712
+ });
1713
+ }, { id: "EditorLanguageStatusComponent", nameWidth: "15%", signal: "@highlight" });
1714
+ rightStatusPanel.endLine( "justify-end" );
1715
+ panel.attach( rightStatusPanel.root );
1716
+
1717
+ const itemVisibilityMap = {
1718
+ "Font Size Zoom": true,
1719
+ "Editor Filename": true,
1720
+ "Editor Selection": true,
1721
+ "Editor Indentation": true,
1722
+ "Editor Language": true,
1723
+ };
1688
1724
 
1689
- const menuOptions = Object.keys( itemVisibilityMap ).map( ( itemName, idx ) => {
1690
- const item = {
1691
- name: itemName,
1692
- icon: "Check",
1693
- callback: () => {
1694
- itemVisibilityMap[ itemName ] = !itemVisibilityMap[ itemName ];
1695
- const b = panel.root.querySelector( `#${ itemName.replaceAll( " ", "" ) }StatusComponent` );
1696
- console.assert( b, `${ itemName } has no status button!` );
1697
- b.classList.toggle( "hidden", !itemVisibilityMap[ itemName ] );
1698
- }
1699
- }
1700
- if( !itemVisibilityMap[ itemName ] ) delete item.icon;
1701
- return item;
1702
- } );
1703
- new LX.DropdownMenu( e.target, menuOptions, { side: "top", align: "start" });
1704
- } );
1725
+ panel.root.addEventListener( "contextmenu", (e) => {
1705
1726
 
1706
- return panel;
1707
- }
1708
- else
1709
- {
1710
- LX.doAsync( () => {
1727
+ if( e.target && ( e.target.classList.contains( "lexpanel" ) || e.target.classList.contains( "lexinlinecomponents" ) ) )
1728
+ {
1729
+ return;
1730
+ }
1711
1731
 
1712
- // Change css a little bit...
1713
- this.gutter.style.height = "calc(100% - 28px)";
1714
- this.root.querySelectorAll( '.code' ).forEach( e => e.style.height = "calc(100% - 6px)" );
1715
- this.root.querySelector( '.lexareatabscontent' ).style.height = "calc(100% - 23px)";
1716
- this.base_area.root.querySelector( '.lexcodescrollbar.vertical' ).style.height = "calc(100% - 27px)";
1717
- this.tabs.area.root.classList.add( 'no-code-info' );
1732
+ const menuOptions = Object.keys( itemVisibilityMap ).map( ( itemName, idx ) => {
1733
+ const item = {
1734
+ name: itemName,
1735
+ icon: "Check",
1736
+ callback: () => {
1737
+ itemVisibilityMap[ itemName ] = !itemVisibilityMap[ itemName ];
1738
+ const b = panel.root.querySelector( `#${ itemName.replaceAll( " ", "" ) }StatusComponent` );
1739
+ console.assert( b, `${ itemName } has no status button!` );
1740
+ b.classList.toggle( "hidden", !itemVisibilityMap[ itemName ] );
1741
+ }
1742
+ }
1743
+ if( !itemVisibilityMap[ itemName ] ) delete item.icon;
1744
+ return item;
1745
+ } );
1746
+ new LX.DropdownMenu( e.target, menuOptions, { side: "top", align: "start" });
1747
+ } );
1718
1748
 
1719
- }, 100 );
1720
- }
1749
+ return panel;
1721
1750
  }
1722
1751
 
1723
- _getFileIcon( name, extension ) {
1752
+ _getFileIcon( name, extension, lang ) {
1724
1753
 
1725
1754
  const isNewTabButton = name ? ( name === '+' ) : false;
1726
-
1727
- if( !extension )
1755
+ if( isNewTabButton )
1728
1756
  {
1729
- extension = LX.getExtension( name );
1757
+ return;
1730
1758
  }
1731
- else
1732
- {
1733
- const possibleExtensions = [].concat( extension );
1734
1759
 
1735
- if( name )
1760
+ if( !lang )
1761
+ {
1762
+ if( !extension )
1736
1763
  {
1737
- const fileExtension = LX.getExtension( name );
1738
- const idx = possibleExtensions.indexOf( fileExtension );
1764
+ extension = LX.getExtension( name );
1765
+ }
1766
+ else
1767
+ {
1768
+ const possibleExtensions = [].concat( extension );
1769
+
1770
+ if( name )
1771
+ {
1772
+ const fileExtension = LX.getExtension( name );
1773
+ const idx = possibleExtensions.indexOf( fileExtension );
1739
1774
 
1740
- if( idx > -1)
1775
+ if( idx > -1)
1776
+ {
1777
+ extension = possibleExtensions[ idx ];
1778
+ }
1779
+ }
1780
+ else
1741
1781
  {
1742
- extension = possibleExtensions[ idx ];
1782
+ extension = possibleExtensions[ 0 ];
1743
1783
  }
1744
1784
  }
1745
- else
1785
+
1786
+ for( const [ l, lData ] of Object.entries( CodeEditor.languages ) )
1746
1787
  {
1747
- extension = possibleExtensions[ 0 ];
1788
+ const extensions = [].concat( lData.ext );
1789
+ if( extensions.includes( extension ) )
1790
+ {
1791
+ lang = l;
1792
+ break;
1793
+ }
1748
1794
  }
1795
+
1796
+ }
1797
+
1798
+ const iconPlusClasses = CodeEditor.languages[ lang ]?.icon;
1799
+ if( iconPlusClasses )
1800
+ {
1801
+ return iconPlusClasses[ extension ] ?? iconPlusClasses;
1749
1802
  }
1750
1803
 
1751
- return extension == "html" ? "Code orange" :
1752
- extension == "css" ? "Hash dodgerblue" :
1753
- extension == "xml" ? "Rss orange" :
1754
- extension == "bat" ? "Windows lightblue" :
1755
- extension == "json" ? "Braces fg-primary" :
1756
- extension == "js" ? "Js goldenrod" :
1757
- extension == "ts" ? "Ts pipelineblue" :
1758
- extension == "py" ? "Python munsellblue" :
1759
- extension == "rs" ? "Rust fg-primary" :
1760
- extension == "md" ? "Markdown fg-primary" :
1761
- extension == "cpp" ? "CPlusPlus pictonblue" :
1762
- extension == "hpp" ? "CPlusPlus heliotrope" :
1763
- extension == "c" ? "C pictonblue" :
1764
- extension == "h" ? "C heliotrope" :
1765
- extension == "php" ? "Php blueviolet" :
1766
- !isNewTabButton ? "AlignLeft gray" : undefined;
1804
+ return "AlignLeft gray";
1767
1805
  }
1768
1806
 
1769
1807
  _onNewTab( e ) {
1770
1808
 
1771
- this.processFocus(false);
1809
+ this.processFocus( false );
1772
1810
 
1773
1811
  LX.addContextMenu( null, e, m => {
1774
1812
  m.add( "Create", this.addTab.bind( this, "unnamed.js", true, "", { language: "JavaScript" } ) );
@@ -1791,7 +1829,7 @@ class CodeEditor {
1791
1829
 
1792
1830
  this._removeSecondaryCursors();
1793
1831
 
1794
- var cursor = this._getCurrentCursor( true );
1832
+ var cursor = this.getCurrentCursor( true );
1795
1833
  this.saveCursor( cursor, this.code.cursorState );
1796
1834
  this.code = this.loadedTabs[ name ];
1797
1835
  this.restoreCursor( cursor, this.code.cursorState );
@@ -1815,12 +1853,12 @@ class CodeEditor {
1815
1853
  _onContextMenuTab( isNewTabButton, event, name, ) {
1816
1854
 
1817
1855
  if( isNewTabButton )
1818
- return;
1856
+ return;
1819
1857
 
1820
1858
  LX.addContextMenu( null, event, m => {
1821
1859
  m.add( "Close", () => { this.tabs.delete( name ) } );
1822
- m.add( "" );
1823
- m.add( "Rename", () => { console.warn( "TODO" )} );
1860
+ // m.add( "" );
1861
+ // m.add( "Rename", () => { console.warn( "TODO" )} );
1824
1862
  });
1825
1863
  }
1826
1864
 
@@ -1847,6 +1885,9 @@ class CodeEditor {
1847
1885
  code.cursorState = {};
1848
1886
  code.undoSteps = [];
1849
1887
  code.redoSteps = [];
1888
+ code.lineScopes = [];
1889
+ code.lineSymbols = [];
1890
+ code.symbolsTable = new Map();
1850
1891
  code.tabName = name;
1851
1892
  code.title = title ?? name;
1852
1893
  code.tokens = {};
@@ -1873,44 +1914,101 @@ class CodeEditor {
1873
1914
 
1874
1915
  const tabIcon = this._getFileIcon( name );
1875
1916
 
1876
- if( this.explorer && !isNewTabButton )
1877
- {
1878
- this.addExplorerItem( { id: name, skipVisibility: true, icon: tabIcon } );
1879
- this.explorer.innerTree.frefresh( name );
1917
+ if( this.useFileExplorer && !isNewTabButton )
1918
+ {
1919
+ this.addExplorerItem( { id: name, skipVisibility: true, icon: tabIcon } );
1920
+ this.explorer.innerTree.frefresh( name );
1921
+ }
1922
+
1923
+ if( !this.skipTabs )
1924
+ {
1925
+ this.tabs.add( name, code, {
1926
+ selected: selected,
1927
+ fixed: isNewTabButton,
1928
+ title: code.title,
1929
+ icon: tabIcon,
1930
+ onSelect: this._onSelectTab.bind( this, isNewTabButton ),
1931
+ onContextMenu: this._onContextMenuTab.bind( this, isNewTabButton ),
1932
+ allowDelete: true
1933
+ } );
1934
+ }
1935
+
1936
+ // Move into the sizer..
1937
+ this.codeSizer.appendChild( code );
1938
+
1939
+ this.endSelection();
1940
+
1941
+ if( selected )
1942
+ {
1943
+ this.code = code;
1944
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP );
1945
+ this.processLines();
1946
+ }
1947
+
1948
+ if( options.language )
1949
+ {
1950
+ code.languageOverride = options.language;
1951
+ this._changeLanguage( code.languageOverride );
1952
+ }
1953
+
1954
+ this._updateDataInfoPanel( "@tab-name", name );
1955
+
1956
+ // Bc it could be overrided..
1957
+ return name;
1958
+ }
1959
+
1960
+ loadCode( name ) {
1961
+
1962
+ // Hide all others
1963
+ this.codeSizer.querySelectorAll( ".code" ).forEach( c => c.classList.add( "hidden" ) );
1964
+
1965
+ // Already open...
1966
+ if( this.openedTabs[ name ] )
1967
+ {
1968
+ let code = this.openedTabs[ name ]
1969
+ code.classList.remove( "hidden" );
1970
+ return;
1971
+ }
1972
+
1973
+ let code = this.loadedTabs[ name ]
1974
+ if( !code )
1975
+ {
1976
+ this.addTab( name, true );
1977
+
1978
+ // Unload lines from storage...
1979
+ const tabData = this._tabStorage[ name ];
1980
+ if( tabData )
1981
+ {
1982
+ this.code.lines = tabData.lines;
1983
+
1984
+ if( tabData.options.language )
1985
+ {
1986
+ this._changeLanguage( tabData.options.language, null, true );
1987
+ }
1988
+ else
1989
+ {
1990
+ this._changeLanguageFromExtension( LX.getExtension( name ) );
1991
+ }
1992
+
1993
+ delete this._tabStorage[ name ];
1994
+ }
1995
+
1996
+ return;
1880
1997
  }
1881
1998
 
1882
- this.tabs.add( name, code, {
1883
- selected: selected,
1884
- fixed: isNewTabButton,
1885
- title: code.title,
1886
- icon: tabIcon,
1887
- onSelect: this._onSelectTab.bind( this, isNewTabButton ),
1888
- onContextMenu: this._onContextMenuTab.bind( this, isNewTabButton ),
1889
- allowDelete: true
1890
- } );
1999
+ this.openedTabs[ name ] = code;
1891
2000
 
1892
2001
  // Move into the sizer..
1893
2002
  this.codeSizer.appendChild( code );
1894
2003
 
1895
2004
  this.endSelection();
1896
2005
 
1897
- if( selected )
1898
- {
1899
- this.code = code;
1900
- this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP );
1901
- this.processLines();
1902
- }
1903
-
1904
- if( options.language )
1905
- {
1906
- code.languageOverride = options.language;
1907
- this._changeLanguage( code.languageOverride );
1908
- }
1909
-
1910
- this._updateDataInfoPanel( "@tab-name", name );
1911
-
1912
- // Bc it could be overrided..
1913
- return name;
2006
+ // Select as current...
2007
+ this.code = code;
2008
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP );
2009
+ this.processLines();
2010
+ this._changeLanguageFromExtension( LX.getExtension( name ) );
2011
+ this._updateDataInfoPanel( "@tab-name", code.tabName );
1914
2012
  }
1915
2013
 
1916
2014
  loadTab( name ) {
@@ -1994,12 +2092,14 @@ class CodeEditor {
1994
2092
  }
1995
2093
 
1996
2094
  loadTabFromFile() {
2095
+
1997
2096
  const input = document.createElement( 'input' );
1998
2097
  input.type = 'file';
1999
2098
  document.body.appendChild( input );
2000
2099
  input.click();
2001
2100
  input.addEventListener('change', e => {
2002
- if (e.target.files[ 0 ]) {
2101
+ if (e.target.files[ 0 ])
2102
+ {
2003
2103
  this.loadFile( e.target.files[ 0 ] );
2004
2104
  }
2005
2105
  input.remove();
@@ -2009,8 +2109,11 @@ class CodeEditor {
2009
2109
  processFocus( active ) {
2010
2110
 
2011
2111
  if( active )
2112
+ {
2012
2113
  this.restartBlink();
2013
- else {
2114
+ }
2115
+ else
2116
+ {
2014
2117
  clearInterval( this.blinker );
2015
2118
  this.cursors.classList.remove( 'show' );
2016
2119
  }
@@ -2018,10 +2121,10 @@ class CodeEditor {
2018
2121
 
2019
2122
  processMouse( e ) {
2020
2123
 
2021
- if( !e.target.classList.contains('code') && !e.target.classList.contains('codetabsarea') ) return;
2124
+ if( !e.target.classList.contains('code') && !e.target.classList.contains('lexcodearea') ) return;
2022
2125
  if( !this.code ) return;
2023
2126
 
2024
- var cursor = this._getCurrentCursor();
2127
+ var cursor = this.getCurrentCursor();
2025
2128
  var code_rect = this.code.getBoundingClientRect();
2026
2129
  var mouse_pos = [(e.clientX - code_rect.x), (e.clientY - code_rect.y)];
2027
2130
 
@@ -2116,14 +2219,17 @@ class CodeEditor {
2116
2219
 
2117
2220
  _onMouseUp( e ) {
2118
2221
 
2119
- if( (LX.getTime() - this.lastMouseDown) < 120 ) {
2222
+ if( ( LX.getTime() - this.lastMouseDown ) < 120 )
2223
+ {
2120
2224
  this.state.selectingText = false;
2121
2225
  this.endSelection();
2122
2226
  }
2123
2227
 
2124
- const cursor = this._getCurrentCursor();
2228
+ const cursor = this.getCurrentCursor();
2125
2229
  if( cursor.selection )
2230
+ {
2126
2231
  cursor.selection.invertIfNecessary();
2232
+ }
2127
2233
 
2128
2234
  this.state.selectingText = false;
2129
2235
  delete this._lastSelectionKeyDir;
@@ -2131,7 +2237,7 @@ class CodeEditor {
2131
2237
 
2132
2238
  processClick( e ) {
2133
2239
 
2134
- var cursor = this._getCurrentCursor();
2240
+ var cursor = this.getCurrentCursor();
2135
2241
  var code_rect = this.codeScroller.getBoundingClientRect();
2136
2242
  var position = [( e.clientX - code_rect.x ) + this.getScrollLeft(), (e.clientY - code_rect.y) + this.getScrollTop()];
2137
2243
  var ln = (position[ 1 ] / this.lineHeight)|0;
@@ -2148,7 +2254,6 @@ class CodeEditor {
2148
2254
  {
2149
2255
  // Make sure we only keep the main cursor..
2150
2256
  this._removeSecondaryCursors();
2151
-
2152
2257
  this.cursorToLine( cursor, ln, true );
2153
2258
  this.cursorToPosition( cursor, string.length );
2154
2259
  }
@@ -2162,38 +2267,45 @@ class CodeEditor {
2162
2267
  this.hideAutoCompleteBox();
2163
2268
  }
2164
2269
 
2165
- updateSelections( e, keep_range, flags = CodeEditor.SELECTION_X_Y ) {
2270
+ updateSelections( e, keepRange, flags = CodeEditor.SELECTION_X_Y ) {
2166
2271
 
2167
2272
  for( let cursor of this.cursors.children )
2168
2273
  {
2169
2274
  if( !cursor.selection )
2275
+ {
2170
2276
  continue;
2277
+ }
2171
2278
 
2172
- this._processSelection( cursor, e, keep_range, flags );
2279
+ this._processSelection( cursor, e, keepRange, flags );
2173
2280
  }
2174
2281
  }
2175
2282
 
2176
- processSelections( e, keep_range, flags = CodeEditor.SELECTION_X_Y ) {
2283
+ processSelections( e, keepRange, flags = CodeEditor.SELECTION_X_Y ) {
2177
2284
 
2178
2285
  for( let cursor of this.cursors.children )
2179
2286
  {
2180
- this._processSelection( cursor, e, keep_range, flags );
2287
+ this._processSelection( cursor, e, keepRange, flags );
2181
2288
  }
2182
2289
  }
2183
2290
 
2184
- _processSelection( cursor, e, keep_range, flags = CodeEditor.SELECTION_X_Y ) {
2291
+ _processSelection( cursor, e, keepRange, flags = CodeEditor.SELECTION_X_Y ) {
2185
2292
 
2186
2293
  const isMouseEvent = e && ( e.constructor == MouseEvent );
2187
2294
 
2188
- if( isMouseEvent ) this.processClick( e );
2295
+ if( isMouseEvent )
2296
+ {
2297
+ this.processClick( e );
2298
+ }
2189
2299
 
2190
2300
  if( !cursor.selection )
2301
+ {
2191
2302
  this.startSelection( cursor );
2303
+ }
2192
2304
 
2193
2305
  this._hideActiveLine();
2194
2306
 
2195
2307
  // Update selection
2196
- if( !keep_range )
2308
+ if( !keepRange )
2197
2309
  {
2198
2310
  let ccw = true;
2199
2311
 
@@ -2260,19 +2372,19 @@ class CodeEditor {
2260
2372
  }
2261
2373
 
2262
2374
  // Compute new width and selection margins
2263
- let string;
2375
+ let string = "";
2264
2376
 
2265
- if(sId == 0) // First line 2 cases (single line, multiline)
2377
+ if( sId == 0 ) // First line 2 cases (single line, multiline)
2266
2378
  {
2267
2379
  const reverse = fromX > toX;
2268
2380
  if(deltaY == 0) string = !reverse ? this.code.lines[ i ].substring( fromX, toX ) : this.code.lines[ i ].substring(toX, fromX);
2269
2381
  else string = this.code.lines[ i ].substr( fromX );
2270
- const pixels = (reverse && deltaY == 0 ? toX : fromX) * this.charWidth;
2271
- if( isVisible ) domEl.style.left = "calc(" + pixels + "px + " + this.xPadding + ")";
2382
+ const pixels = ( reverse && deltaY == 0 ? toX : fromX ) * this.charWidth;
2383
+ if( isVisible ) domEl.style.left = `calc(${ pixels }px + ${ this.xPadding })`;
2272
2384
  }
2273
2385
  else
2274
2386
  {
2275
- string = (i == toY) ? this.code.lines[ i ].substring( 0, toX ) : this.code.lines[ i ]; // Last line, any multiple line...
2387
+ string = ( i == toY ) ? this.code.lines[ i ].substring( 0, toX ) : this.code.lines[ i ]; // Last line, any multiple line...
2276
2388
  if( isVisible ) domEl.style.left = this.xPadding;
2277
2389
  }
2278
2390
 
@@ -2281,7 +2393,7 @@ class CodeEditor {
2281
2393
 
2282
2394
  if( isVisible )
2283
2395
  {
2284
- domEl.style.width = (stringWidth || 8) + "px";
2396
+ domEl.style.width = ( stringWidth || 8 ) + "px";
2285
2397
  domEl._top = i * this.lineHeight;
2286
2398
  domEl.style.top = domEl._top + "px";
2287
2399
  }
@@ -2302,7 +2414,7 @@ class CodeEditor {
2302
2414
  {
2303
2415
  // Make sure that the line selection is generated...
2304
2416
  domEl = cursorSelections.childNodes[ sId ];
2305
- if(!domEl)
2417
+ if( !domEl )
2306
2418
  {
2307
2419
  domEl = document.createElement( 'div' );
2308
2420
  domEl.className = "lexcodeselection";
@@ -2421,7 +2533,7 @@ class CodeEditor {
2421
2533
 
2422
2534
  _processGlobalKeys( e, key ) {
2423
2535
 
2424
- let cursor = this._getCurrentCursor();
2536
+ let cursor = this.getCurrentCursor();
2425
2537
 
2426
2538
  if( e.ctrlKey || e.metaKey )
2427
2539
  {
@@ -2489,7 +2601,7 @@ class CodeEditor {
2489
2601
 
2490
2602
  async _processKeyAtCursor( e, key, cursor ) {
2491
2603
 
2492
- const skip_undo = e.detail.skip_undo ?? false;
2604
+ const skipUndo = e.detail.skipUndo ?? false;
2493
2605
 
2494
2606
  // keys with length > 1 are probably special keys
2495
2607
  if( key.length > 1 && this.specialKeys.indexOf( key ) == -1 )
@@ -2579,7 +2691,7 @@ class CodeEditor {
2579
2691
 
2580
2692
  // Add undo steps
2581
2693
 
2582
- if( !skip_undo && this.code.lines.length )
2694
+ if( !skipUndo && this.code.lines.length )
2583
2695
  {
2584
2696
  this._addUndoStep( cursor );
2585
2697
  }
@@ -2640,17 +2752,7 @@ class CodeEditor {
2640
2752
  this.processLine( lidx );
2641
2753
 
2642
2754
  // We are out of the viewport and max length is different? Resize scrollbars...
2643
- const maxLineLength = this.getMaxLineLength();
2644
- const numViewportChars = Math.floor( ( this.codeScroller.clientWidth - CodeEditor.LINE_GUTTER_WIDTH ) / this.charWidth );
2645
- if( maxLineLength >= numViewportChars && maxLineLength != this._lastMaxLineLength )
2646
- {
2647
- this.resize( maxLineLength, () => {
2648
- if( cursor.position > numViewportChars )
2649
- {
2650
- this.setScrollLeft( cursor.position * this.charWidth );
2651
- }
2652
- } );
2653
- }
2755
+ this.resizeIfNecessary( cursor );
2654
2756
 
2655
2757
  // Manage autocomplete
2656
2758
 
@@ -2662,6 +2764,8 @@ class CodeEditor {
2662
2764
 
2663
2765
  async _pasteContent( cursor ) {
2664
2766
 
2767
+ const mustDetectLanguage = ( !this.getText().length );
2768
+
2665
2769
  let text = await navigator.clipboard.readText();
2666
2770
 
2667
2771
  // Remove any possible tabs (\t) and add spaces
@@ -2674,9 +2778,19 @@ class CodeEditor {
2674
2778
  const currentScroll = this.getScrollTop();
2675
2779
  const scroll = Math.max( cursor.line * this.lineHeight - this.codeScroller.offsetWidth, 0 );
2676
2780
 
2677
- if( currentScroll < scroll ) {
2781
+ if( currentScroll < scroll )
2782
+ {
2678
2783
  this.codeScroller.scrollTo( 0, cursor.line * this.lineHeight );
2679
2784
  }
2785
+
2786
+ if( mustDetectLanguage )
2787
+ {
2788
+ const detectedLang = this._detectLanguage( text );
2789
+ if( detectedLang )
2790
+ {
2791
+ this._changeLanguage( detectedLang );
2792
+ }
2793
+ }
2680
2794
  }
2681
2795
 
2682
2796
  async _copyContent( cursor ) {
@@ -2699,7 +2813,9 @@ class CodeEditor {
2699
2813
  let index = 0;
2700
2814
 
2701
2815
  for( let i = 0; i <= cursor.selection.fromY; i++ )
2816
+ {
2702
2817
  index += ( i == cursor.selection.fromY ? cursor.selection.fromX : this.code.lines[ i ].length );
2818
+ }
2703
2819
 
2704
2820
  index += cursor.selection.fromY * separator.length;
2705
2821
  const num_chars = cursor.selection.chars + ( cursor.selection.toY - cursor.selection.fromY ) * separator.length;
@@ -2775,7 +2891,7 @@ class CodeEditor {
2775
2891
 
2776
2892
  if( cursor.selection )
2777
2893
  {
2778
- var cursor = this._getCurrentCursor();
2894
+ var cursor = this.getCurrentCursor();
2779
2895
  this._addUndoStep( cursor, true );
2780
2896
 
2781
2897
  const selectedLines = this.code.lines.slice( cursor.selection.fromY, cursor.selection.toY );
@@ -2834,7 +2950,7 @@ class CodeEditor {
2834
2950
 
2835
2951
  if( cursor.selection )
2836
2952
  {
2837
- var cursor = this._getCurrentCursor();
2953
+ var cursor = this.getCurrentCursor();
2838
2954
  this._addUndoStep( cursor, true );
2839
2955
 
2840
2956
  for( var i = cursor.selection.fromY; i <= cursor.selection.toY; ++i )
@@ -2888,7 +3004,6 @@ class CodeEditor {
2888
3004
  }
2889
3005
 
2890
3006
  _actionMustDelete( cursor, action, e ) {
2891
-
2892
3007
  return cursor.selection && action.deleteSelection &&
2893
3008
  ( action.eventSkipDelete ? !e[ action.eventSkipDelete ] : true );
2894
3009
  }
@@ -2906,13 +3021,11 @@ class CodeEditor {
2906
3021
  }
2907
3022
 
2908
3023
  toLocalLine( line ) {
2909
-
2910
3024
  const d = Math.max( this.firstLineInViewport - this.lineScrollMargin.x, 0 );
2911
3025
  return Math.min( Math.max( line - d, 0 ), this.code.lines.length - 1 );
2912
3026
  }
2913
3027
 
2914
3028
  getMaxLineLength() {
2915
-
2916
3029
  return Math.max(...this.code.lines.map( v => v.length ));
2917
3030
  }
2918
3031
 
@@ -2928,7 +3041,7 @@ class CodeEditor {
2928
3041
  const lastScrollTop = this.getScrollTop();
2929
3042
  this.firstLineInViewport = ( mode ?? CodeEditor.KEEP_VISIBLE_LINES ) & CodeEditor.UPDATE_VISIBLE_LINES ?
2930
3043
  ( (lastScrollTop / this.lineHeight)|0 ) : this.firstLineInViewport;
2931
- const totalLinesInViewport = ((this.codeScroller.offsetHeight - 36) / this.lineHeight)|0;
3044
+ const totalLinesInViewport = ( ( this.codeScroller.offsetHeight ) / this.lineHeight )|0;
2932
3045
  this.visibleLinesViewport = new LX.vec2(
2933
3046
  Math.max( this.firstLineInViewport - this.lineScrollMargin.x, 0 ),
2934
3047
  Math.min( this.firstLineInViewport + totalLinesInViewport + this.lineScrollMargin.y, this.code.lines.length )
@@ -2943,7 +3056,7 @@ class CodeEditor {
2943
3056
  }
2944
3057
  }
2945
3058
 
2946
- this._scopeStack = [];
3059
+ this._scopeStack = [ { name: "", type: "global" } ];
2947
3060
 
2948
3061
  // Process visible lines
2949
3062
  for( let i = this.visibleLinesViewport.x; i < this.visibleLinesViewport.y; ++i )
@@ -2965,7 +3078,7 @@ class CodeEditor {
2965
3078
  this.resize();
2966
3079
  }
2967
3080
 
2968
- processLine( lineNumber, force ) {
3081
+ processLine( lineNumber, force, skipPropagation ) {
2969
3082
 
2970
3083
  // Check if we are in block comment sections..
2971
3084
  if( !force && this._inBlockCommentSection( lineNumber ) )
@@ -2976,151 +3089,400 @@ class CodeEditor {
2976
3089
 
2977
3090
  if( this._scopeStack )
2978
3091
  {
2979
- this.lineScopes[ lineNumber ] = [ ...this._scopeStack ];
3092
+ this.code.lineScopes[ lineNumber ] = [ ...this._scopeStack ];
2980
3093
  }
2981
3094
  else
2982
3095
  {
2983
- this._scopeStack = [ ...this.lineScopes[ lineNumber ] ];
3096
+ this.code.lineScopes[ lineNumber ] = this.code.lineScopes[ lineNumber ] ?? [];
3097
+ this._scopeStack = [ ...this.code.lineScopes[ lineNumber ] ];
3098
+ }
3099
+
3100
+ const lang = CodeEditor.languages[ this.highlight ];
3101
+ const localLineNum = this.toLocalLine( lineNumber );
3102
+ const lineString = this.code.lines[ lineNumber ];
3103
+
3104
+ // multi-line strings not supported by now
3105
+ delete this._buildingString;
3106
+ delete this._pendingString;
3107
+ delete this._markdownHeader;
3108
+
3109
+ // Single line
3110
+ if( !force )
3111
+ {
3112
+ LX.deleteElement( this.code.childNodes[ localLineNum ] );
3113
+ this.code.insertChildAtIndex( document.createElement( 'pre' ), localLineNum );
3114
+ }
3115
+
3116
+ // Early out check for no highlighting languages
3117
+ if( this.highlight == 'Plain Text' )
3118
+ {
3119
+ const plainTextHtml = lineString.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
3120
+ return this._updateLine( force, lineNumber, plainTextHtml, skipPropagation );
3121
+ }
3122
+
3123
+ this._currentLineNumber = lineNumber;
3124
+ this._currentLineString = lineString;
3125
+
3126
+ const tokensToEvaluate = this._getTokensFromLine( lineString );
3127
+ if( !tokensToEvaluate.length )
3128
+ {
3129
+ return this._updateLine( force, lineNumber, "", skipPropagation );
3130
+ }
3131
+
3132
+ let lineInnerHtml = "";
3133
+ let pushedScope = false;
3134
+
3135
+ // Process all tokens
3136
+ for( let i = 0; i < tokensToEvaluate.length; ++i )
3137
+ {
3138
+ let it = i - 1;
3139
+ let prev = tokensToEvaluate[ it ];
3140
+ while( prev == ' ' )
3141
+ {
3142
+ it--;
3143
+ prev = tokensToEvaluate[ it ];
3144
+ }
3145
+
3146
+ it = i + 1;
3147
+ let next = tokensToEvaluate[ it ];
3148
+ while( next == ' ' || next == '"' )
3149
+ {
3150
+ it++;
3151
+ next = tokensToEvaluate[ it ];
3152
+ }
3153
+
3154
+ const token = tokensToEvaluate[ i ];
3155
+
3156
+ if( lang.blockComments ?? true )
3157
+ {
3158
+ const blockCommentsToken = ( lang.blockCommentsTokens ?? this.defaultBlockCommentTokens )[ 0 ];
3159
+ if( token.substr( 0, blockCommentsToken.length ) == blockCommentsToken )
3160
+ {
3161
+ this._buildingBlockComment = lineNumber;
3162
+ }
3163
+ }
3164
+
3165
+ // Pop current scope if necessary
3166
+ if( token === "}" && this._scopeStack.length > 1 )
3167
+ {
3168
+ this._scopeStack.pop();
3169
+ }
3170
+
3171
+ lineInnerHtml += this._evaluateToken( {
3172
+ token: token,
3173
+ prev: prev,
3174
+ prevWithSpaces: tokensToEvaluate[ i - 1 ],
3175
+ next: next,
3176
+ nextWithSpaces: tokensToEvaluate[ i + 1 ],
3177
+ tokenIndex: i,
3178
+ isFirstToken: (i == 0),
3179
+ isLastToken: (i == tokensToEvaluate.length - 1),
3180
+ tokens: tokensToEvaluate
3181
+ } );
3182
+
3183
+ if( token !== "{" )
3184
+ {
3185
+ continue;
3186
+ }
3187
+
3188
+ // Store current scopes
3189
+
3190
+ // Get some context about the scope from previous lines
3191
+ let contextTokens = [
3192
+ ...this._getTokensFromLine( this.code.lines[ lineNumber ].substring( 0, lineString.indexOf( token ) ) )
3193
+ ];
3194
+
3195
+ for( let k = 1; k < 50; k++ )
3196
+ {
3197
+ let kLineString = this.code.lines[ lineNumber - k ];
3198
+ if( !kLineString ) break;
3199
+ const closeIdx = kLineString.lastIndexOf( '}' );
3200
+ if( closeIdx > -1 )
3201
+ {
3202
+ kLineString = kLineString.substr( closeIdx );
3203
+ }
3204
+
3205
+ contextTokens = [ ...this._getTokensFromLine( kLineString ), ...contextTokens ];
3206
+
3207
+ if( kLineString.length !== this.code.lines[ lineNumber - k ] )
3208
+ {
3209
+ break;
3210
+ }
3211
+ }
3212
+
3213
+ contextTokens = contextTokens.reverse().filter( v => v.length && v != ' ' );
3214
+
3215
+ // Keywords that can open a *named* scope
3216
+ // TODO: Do this per language
3217
+ const scopeKeywords = ["class", "enum", "function", "interface", "type", "struct", "namespace"];
3218
+
3219
+ let scopeType = null; // This is the type of scope (function, class, enum, etc)
3220
+ let scopeName = null;
3221
+
3222
+ for( let i = 0; i < contextTokens.length; i++ )
3223
+ {
3224
+ const t = contextTokens[ i ];
3225
+
3226
+ if ( scopeKeywords.includes( t ) )
3227
+ {
3228
+ scopeType = t;
3229
+ scopeName = contextTokens[ i - 1 ]; // usually right before the keyword in reversed array
3230
+ break;
3231
+ }
3232
+ }
3233
+
3234
+ // Special case: enum type specification `enum Foo : int {`
3235
+ if( scopeType === "enum" && contextTokens.includes( ":" ) )
3236
+ {
3237
+ const colonIndex = contextTokens.indexOf( ":" );
3238
+ scopeName = contextTokens[ colonIndex + 1 ] || scopeName;
3239
+ }
3240
+
3241
+ if( !scopeType )
3242
+ {
3243
+ const parOpenIndex = contextTokens.indexOf( "(" );
3244
+ scopeName = contextTokens[ parOpenIndex + 1 ] || scopeName;
3245
+
3246
+ if( scopeName )
3247
+ {
3248
+ scopeType = "method";
3249
+ }
3250
+ }
3251
+
3252
+ if( scopeType )
3253
+ {
3254
+ this._scopeStack.push( { name: scopeName ?? "", type: scopeType } );
3255
+ }
3256
+ else
3257
+ {
3258
+ this._scopeStack.push( { name: "", type: "anonymous" } ); // anonymous scope
3259
+ }
3260
+
3261
+ pushedScope = true;
3262
+ }
3263
+
3264
+ const symbols = this._parseLineForSymbols( lineNumber, lineString, tokensToEvaluate, pushedScope );
3265
+ return this._updateLine( force, lineNumber, lineInnerHtml, skipPropagation, symbols );
3266
+ }
3267
+
3268
+ _processExtraLineIfNecessary( lineNumber, oldSymbols ) {
3269
+
3270
+ if( ( (lineNumber + 1) === this.code.lines.length ) || !this.code.lineScopes[ lineNumber + 1 ] )
3271
+ {
3272
+ return;
3273
+ }
3274
+
3275
+ if( !this._scopeStack )
3276
+ {
3277
+ console.warn( "CodeEditor: No scope available" );
3278
+ return;
3279
+ }
3280
+
3281
+ // Only update scope stack if something changed when editing a single line
3282
+ if( codeScopesEqual( this._scopeStack, this.code.lineScopes[ lineNumber + 1 ] ) )
3283
+ {
3284
+ // First check for occurrencies of the old symbols, to reprocess that lines
3285
+
3286
+ for( const sym of oldSymbols )
3287
+ {
3288
+ const tableSymbol = this.code.symbolsTable.get( sym.name );
3289
+ if( tableSymbol === undefined )
3290
+ {
3291
+ return;
3292
+ }
3293
+
3294
+ for( const occ of tableSymbol )
3295
+ {
3296
+ if( occ.line === lineNumber )
3297
+ {
3298
+ continue;
3299
+ }
3300
+
3301
+ this.processLine( occ.line, false, true );
3302
+ }
3303
+ }
3304
+
3305
+ return;
3306
+ }
3307
+
3308
+ this.code.lineScopes[ lineNumber + 1 ] = [ ...this._scopeStack ];
3309
+ this.processLine( lineNumber + 1 );
3310
+ }
3311
+
3312
+ _updateLine( force, lineNumber, html, skipPropagation, symbols = [] ) {
3313
+
3314
+ const gutterLineHtml = `<span class='line-gutter'>${ lineNumber + 1 }</span>`;
3315
+ const oldSymbols = this._updateLineSymbols( lineNumber, symbols );
3316
+ const lineScope = CodeEditor.debugScopes ? this.code.lineScopes[ lineNumber ].map( s => `${ s.type }` ).join( ", " ) : "";
3317
+ const lineSymbols = CodeEditor.debugSymbols ? this.code.lineSymbols[ lineNumber ].map( s => `${ s.name }(${ s.kind })` ).join( ", " ) : "";
3318
+ const debugString = lineScope + ( lineScope.length ? " - " : "" ) + lineSymbols;
3319
+
3320
+ if( !force ) // Single line update
3321
+ {
3322
+ this.code.childNodes[ this.toLocalLine( lineNumber ) ].innerHTML = ( gutterLineHtml + html + debugString );
3323
+
3324
+ if( !skipPropagation )
3325
+ {
3326
+ this._processExtraLineIfNecessary( lineNumber, oldSymbols );
3327
+ }
3328
+
3329
+ this._setActiveLine( lineNumber );
3330
+ this._clearTmpVariables();
3331
+ }
3332
+ else // Update all lines at once
3333
+ {
3334
+
3335
+ return `<pre>${ ( gutterLineHtml + html + debugString ) }</pre>`;
3336
+ }
3337
+ }
3338
+
3339
+ /**
3340
+ * Parses a single line of code and extract declared symbols
3341
+ */
3342
+ _parseLineForSymbols( lineNumber, lineString, tokens, pushedScope ) {
3343
+
3344
+ const scope = this._scopeStack.at( pushedScope ? -2 : -1 );
3345
+
3346
+ if( !scope )
3347
+ {
3348
+ return [];
3349
+ }
3350
+
3351
+ const scopeName = scope.name;
3352
+ const scopeType = scope.type;
3353
+ const symbols = [];
3354
+ const text = lineString.trim();
3355
+
3356
+ // Don't make symbols from preprocessor lines
3357
+ if( text.startsWith( "#" ) )
3358
+ {
3359
+ return [];
2984
3360
  }
2985
3361
 
2986
- const lang = CodeEditor.languages[ this.highlight ];
2987
- const localLineNum = this.toLocalLine( lineNumber );
2988
- const gutterLineHtml = `<span class='line-gutter'>${ lineNumber + 1 }</span>`;
3362
+ const topLevelRegexes = [
3363
+ [/^class\s+([A-Za-z0-9_]+)/, "class"],
3364
+ [/^struct\s+([A-Za-z0-9_]+)/, "struct"],
3365
+ [/^enum\s+([A-Za-z0-9_]+)/, "enum"],
3366
+ [/^interface\s+([A-Za-z0-9_]+)/, "interface"],
3367
+ [/^type\s+([A-Za-z0-9_]+)/, "type"],
3368
+ [/^function\s+([A-Za-z0-9_]+)/, "method"],
3369
+ [/^([A-Za-z0-9_]+)\s*=\s*\(?.*\)?\s*=>/, "method"] // arrow functions
3370
+ ];
2989
3371
 
2990
- const _updateLine = ( html ) => {
2991
- if( !force ) // Single line update
2992
- {
2993
- _processNextLineIfNecessary();
2994
- this.code.childNodes[ localLineNum ].innerHTML = gutterLineHtml + html;
2995
- this._setActiveLine( lineNumber );
2996
- this._clearTmpVariables();
2997
- }
2998
- else // Update all lines at once
3372
+ {
3373
+ const nativeTypes = CodeEditor.nativeTypes[ this.highlight ];
3374
+ if( nativeTypes )
2999
3375
  {
3000
- return `<pre>${ gutterLineHtml + html }</pre>`;
3376
+ const nativeTypes = ['int', 'float', 'double', 'bool', 'long', 'short', 'char', 'wchar_t', 'void'];
3377
+ const regex = `^(?:${nativeTypes.join('|')})\\s+([A-Za-z0-9_]+)\s*[\(]+`;
3378
+ topLevelRegexes.push( [ new RegExp( regex ), 'method' ] );
3001
3379
  }
3002
- }
3003
-
3004
- const _processNextLineIfNecessary = () => {
3005
- // Only update scope stack if something changed when editing a single line
3006
- if( this._scopeStack.toString() == this.lineScopes[ lineNumber + 1 ]?.toString() )
3007
- return;
3008
3380
 
3009
- this.lineScopes[ lineNumber + 1 ] = [ ...this._scopeStack ];
3381
+ const declarationKeywords = CodeEditor.declarationKeywords[ this.highlight ] ?? [ "const", "let", "var" ];
3382
+ const regex = `^(?:${declarationKeywords.join('|')})\\s+([A-Za-z0-9_]+)`;
3383
+ topLevelRegexes.push( [ new RegExp( regex ), 'variable' ] );
3384
+ }
3010
3385
 
3011
- if( this.code.lines.length > lineNumber + 1 )
3386
+ for( let [ regex, kind ] of topLevelRegexes )
3387
+ {
3388
+ const m = text.match( regex );
3389
+ if( m )
3012
3390
  {
3013
- this.processLine( lineNumber + 1 );
3391
+ symbols.push( { name: m[ 1 ], kind, scope: scopeName, line: lineNumber } );
3392
+ break;
3014
3393
  }
3015
3394
  }
3016
3395
 
3017
- // multi-line strings not supported by now
3018
- delete this._buildingString;
3019
- delete this._pendingString;
3020
- delete this._markdownHeader;
3021
-
3022
- let lineString = this.code.lines[ lineNumber ];
3396
+ const usageRegexes = [
3397
+ [/new\s+([A-Za-z0-9_]+)\s*\(/, "constructor-call"],
3398
+ [/this.([A-Za-z_][A-Za-z0-9_]*)\s*\=/, "class-property"],
3399
+ ];
3023
3400
 
3024
- // Single line
3025
- if( !force )
3401
+ for( let [ regex, kind ] of usageRegexes )
3026
3402
  {
3027
- LX.deleteElement( this.code.childNodes[ localLineNum ] );
3028
- this.code.insertChildAtIndex( document.createElement( 'pre' ), localLineNum );
3403
+ const m = text.match( regex );
3404
+ if( m )
3405
+ {
3406
+ symbols.push( { name: m[ 1 ], kind, scope: scopeName, line: lineNumber } );
3407
+ }
3029
3408
  }
3030
3409
 
3031
- // Early out check for no highlighting languages
3032
- if( this.highlight == 'Plain Text' )
3410
+ // Stop after matches for top-level declarations and usage symbols
3411
+ if( symbols.length )
3033
3412
  {
3034
- const plainTextHtml = lineString.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
3035
- return _updateLine( plainTextHtml );
3413
+ return symbols;
3036
3414
  }
3037
3415
 
3038
- this._currentLineNumber = lineNumber;
3039
- this._currentLineString = lineString;
3416
+ const nonWhiteSpaceTokens = tokens.filter( t => t.trim().length );
3040
3417
 
3041
- const tokensToEvaluate = this._getTokensFromLine( lineString );
3042
- if( !tokensToEvaluate.length )
3418
+ for( let i = 0; i < nonWhiteSpaceTokens.length; i++ )
3043
3419
  {
3044
- return _updateLine( "" );
3045
- }
3046
-
3047
- let lineInnerHtml = "";
3420
+ const prev = nonWhiteSpaceTokens[ i - 1 ];
3421
+ const token = nonWhiteSpaceTokens[ i ];
3422
+ const next = nonWhiteSpaceTokens[ i + 1 ];
3048
3423
 
3049
- // Process all tokens
3050
- for( let i = 0; i < tokensToEvaluate.length; ++i )
3051
- {
3052
- let it = i - 1;
3053
- let prev = tokensToEvaluate[ it ];
3054
- while( prev == ' ' )
3424
+ if( scopeType.startsWith("class") )
3055
3425
  {
3056
- it--;
3057
- prev = tokensToEvaluate[ it ];
3426
+ if( next === "(" && /^[a-zA-Z_]\w*$/.test( token ) && prev === undefined )
3427
+ {
3428
+ symbols.push( { name: token, kind: "method", scope: scopeName, line: lineNumber } );
3429
+ }
3058
3430
  }
3059
-
3060
- it = i + 1;
3061
- let next = tokensToEvaluate[ it ];
3062
- while( next == ' ' || next == '"' )
3431
+ else if( scopeType.startsWith("enum") )
3063
3432
  {
3064
- it++;
3065
- next = tokensToEvaluate[ it ];
3433
+ if( !isSymbol( token ) && !this._isNumber( token ) && !this._mustHightlightWord( token, CodeEditor.statements ) )
3434
+ {
3435
+ symbols.push({ name: token, kind: "enum_value", scope: scopeName, line: lineNumber });
3436
+ }
3066
3437
  }
3438
+ }
3067
3439
 
3068
- const token = tokensToEvaluate[ i ];
3440
+ return symbols;
3441
+ }
3069
3442
 
3070
- if( lang.blockComments ?? true )
3071
- {
3072
- const blockCommentsToken = ( lang.blockCommentsTokens ?? this.defaultBlockCommentTokens )[ 0 ];
3073
- if( token.substr( 0, blockCommentsToken.length ) == blockCommentsToken )
3074
- this._buildingBlockComment = lineNumber;
3075
- }
3443
+ /**
3444
+ * Updates the symbol table for a single line
3445
+ * - Removes old symbols from that line
3446
+ * - Inserts the new symbols
3447
+ */
3448
+ _updateLineSymbols( lineNumber, newSymbols ) {
3076
3449
 
3077
- // Pop current scope if necessary
3078
- if( token === "}" )
3450
+ this.code.lineSymbols[ lineNumber ] = this.code.lineSymbols[ lineNumber ] ?? [];
3451
+ const oldSymbols = LX.deepCopy( this.code.lineSymbols[ lineNumber ] );
3452
+
3453
+ // Clean old symbols from current line
3454
+ for( let sym of this.code.lineSymbols[ lineNumber ] )
3455
+ {
3456
+ let array = this.code.symbolsTable.get( sym.name );
3457
+ if( !array )
3079
3458
  {
3080
- this._scopeStack.pop();
3459
+ continue;
3081
3460
  }
3082
3461
 
3083
- lineInnerHtml += this._evaluateToken( {
3084
- token: token,
3085
- prev: prev,
3086
- prevWithSpaces: tokensToEvaluate[ i - 1 ],
3087
- next: next,
3088
- nextWithSpaces: tokensToEvaluate[ i + 1 ],
3089
- tokenIndex: i,
3090
- isFirstToken: (i == 0),
3091
- isLastToken: (i == tokensToEvaluate.length - 1),
3092
- tokens: tokensToEvaluate
3093
- } );
3462
+ array = array.filter( s => s.line !== lineNumber );
3094
3463
 
3095
- // Store current scopes
3096
- if( token === "{" )
3464
+ if( array.length )
3097
3465
  {
3098
- // Get some context about the scope from previous lines
3099
- const contextTokens = [
3100
- ...this._getTokensFromLine( this.code.lines[ lineNumber - 2 ] ),
3101
- ...this._getTokensFromLine( this.code.lines[ lineNumber - 1 ] ),
3102
- ...this._getTokensFromLine( this.code.lines[ lineNumber ].substring( 0, lineString.indexOf( token ) ) )
3103
- ].reverse().filter( v => v.length && v != ' ' );
3104
-
3105
- let scopeType = contextTokens[ 1 ]; // This is the type of scope (function, class, enum, etc)
3106
-
3107
- // Special cases
3108
- if( scopeType === ":" ) // enum type specification
3109
- {
3110
- scopeType = contextTokens[ 3 ];
3111
- }
3112
-
3113
- if( !this._mustHightlightWord( scopeType, CodeEditor.keywords, lang ) )
3114
- {
3115
- // If the scope type is not a keyword, use an empty string
3116
- scopeType = "";
3117
- }
3118
-
3119
- this._scopeStack.push( scopeType );
3466
+ this.code.symbolsTable.set( sym.name, array );
3467
+ }
3468
+ else
3469
+ {
3470
+ this.code.symbolsTable.delete( sym.name );
3120
3471
  }
3121
3472
  }
3122
3473
 
3123
- return _updateLine( lineInnerHtml );
3474
+ // Add new symbols to table
3475
+ for( let sym of newSymbols )
3476
+ {
3477
+ let arr = this.code.symbolsTable.get( sym.name ) ?? [];
3478
+ arr.push(sym);
3479
+ this.code.symbolsTable.set( sym.name, arr );
3480
+ }
3481
+
3482
+ // Keep lineSymbols in sync
3483
+ this.code.lineSymbols[ lineNumber ] = newSymbols;
3484
+
3485
+ return oldSymbols;
3124
3486
  }
3125
3487
 
3126
3488
  _lineHasComment( lineString ) {
@@ -3236,18 +3598,33 @@ class CodeEditor {
3236
3598
  }
3237
3599
  else if( this.highlight == 'PHP' )
3238
3600
  {
3239
- const dollarIdx = tokens.indexOf( '$' );
3240
- if( dollarIdx > -1 && tokens[ dollarIdx + 1 ] === 'this-' )
3601
+ let offset = 0;
3602
+ let dollarIdx = tokens.indexOf( '$' );
3603
+
3604
+ while( dollarIdx > -1 )
3241
3605
  {
3242
- tokens[ dollarIdx ] = "$this";
3243
- tokens[ dollarIdx + 1 ] = "-";
3606
+ const offsetIdx = dollarIdx + offset;
3607
+
3608
+ if( tokens[ offsetIdx + 1 ] === 'this-' )
3609
+ {
3610
+ tokens[ offsetIdx ] = "$this";
3611
+ tokens[ offsetIdx + 1 ] = "-";
3612
+ }
3613
+ else
3614
+ {
3615
+ tokens[ offsetIdx ] += ( tokens[ offsetIdx + 1 ] ?? "" );
3616
+ tokens.splice( offsetIdx + 1, 1 );
3617
+ }
3618
+
3619
+ dollarIdx = tokens.slice( offsetIdx ).indexOf( '$' );
3620
+ offset = offsetIdx;
3244
3621
  }
3245
3622
  }
3246
3623
 
3247
3624
  return tokens;
3248
3625
  }
3249
3626
 
3250
- _mustHightlightWord( token, kindArray, lang ) {
3627
+ _mustHightlightWord( token, wordCategory, lang ) {
3251
3628
 
3252
3629
  if( !lang )
3253
3630
  {
@@ -3261,12 +3638,12 @@ class CodeEditor {
3261
3638
  t = t.toLowerCase();
3262
3639
  }
3263
3640
 
3264
- return kindArray[ this.highlight ] && kindArray[ this.highlight ][ t ] != undefined;
3641
+ return wordCategory[ this.highlight ] && wordCategory[ this.highlight ].has( t );
3265
3642
  }
3266
3643
 
3267
3644
  _getTokenHighlighting( ctx, highlight ) {
3268
3645
 
3269
- const rules = [ ...HighlightRules.common, ...( HighlightRules[highlight] || [] ), ...HighlightRules.post_common ];
3646
+ const rules = [ ...HighlightRules.common, ...( HighlightRules[ highlight ] || [] ), ...HighlightRules.post_common ];
3270
3647
 
3271
3648
  for( const rule of rules )
3272
3649
  {
@@ -3289,7 +3666,8 @@ class CodeEditor {
3289
3666
 
3290
3667
  const lang = CodeEditor.languages[ this.highlight ],
3291
3668
  highlight = this.highlight.replace( /\s/g, '' ).replaceAll( "+", "p" ).toLowerCase(),
3292
- customStringKeys = Object.assign( {}, this.stringKeys );
3669
+ customStringKeys = Object.assign( {}, this.stringKeys ),
3670
+ lineNumber = this._currentLineNumber;
3293
3671
 
3294
3672
  var usePreviousTokenToCheckString = false;
3295
3673
 
@@ -3338,7 +3716,14 @@ class CodeEditor {
3338
3716
  ctxData.inString = this._buildingString;
3339
3717
  ctxData.singleLineCommentToken = lang.singleLineCommentToken ?? this.defaultSingleLineCommentToken;
3340
3718
  ctxData.lang = lang;
3341
- ctxData.scope = this._scopeStack[ this._scopeStack.length - 1 ];
3719
+ ctxData.scope = this._scopeStack.at( -1 );
3720
+
3721
+ // Add utils functions for the rules
3722
+ ctxData.isVariableSymbol = ( token ) => this.code.symbolsTable.has( token ) && this.code.symbolsTable.get( token )[ 0 ].kind === "variable";
3723
+ ctxData.isEnumValueSymbol = ( token ) => this.code.symbolsTable.has( token ) && this.code.symbolsTable.get( token )[ 0 ].kind === "enum_value";
3724
+ ctxData.isClassSymbol = ( token ) => this.code.symbolsTable.has( token ) && this.code.symbolsTable.get( token )[ 0 ].kind === "class";
3725
+ ctxData.isStructSymbol = ( token ) => this.code.symbolsTable.has( token ) && this.code.symbolsTable.get( token )[ 0 ].kind === "struct";
3726
+ ctxData.isEnumSymbol = ( token ) => this.code.symbolsTable.has( token ) && this.code.symbolsTable.get( token )[ 0 ].kind === "enum";
3342
3727
 
3343
3728
  // Get highlighting class based on language common and specific rules
3344
3729
  let tokenClass = this._getTokenHighlighting( ctxData, highlight );
@@ -3347,7 +3732,7 @@ class CodeEditor {
3347
3732
  if( ( lang.blockComments ?? true ) && this._buildingBlockComment != undefined
3348
3733
  && token.substr( 0, blockCommentsTokens[ 1 ].length ) == blockCommentsTokens[ 1 ] )
3349
3734
  {
3350
- this._blockCommentCache.push( new LX.vec2( this._buildingBlockComment, this._currentLineNumber ) );
3735
+ this._blockCommentCache.push( new LX.vec2( this._buildingBlockComment, lineNumber ) );
3351
3736
  delete this._buildingBlockComment;
3352
3737
  }
3353
3738
 
@@ -3392,7 +3777,6 @@ class CodeEditor {
3392
3777
  }
3393
3778
 
3394
3779
  _getCurrentString() {
3395
-
3396
3780
  const chars = this._pendingString;
3397
3781
  delete this._pendingString;
3398
3782
  return chars;
@@ -3521,7 +3905,7 @@ class CodeEditor {
3521
3905
  this.cursorToPosition( cursor, cursor.selection.toX + 1 );
3522
3906
 
3523
3907
  // Change next key?
3524
- switch(key)
3908
+ switch( key )
3525
3909
  {
3526
3910
  case "'":
3527
3911
  case "\"":
@@ -3549,6 +3933,46 @@ class CodeEditor {
3549
3933
  return true;
3550
3934
  }
3551
3935
 
3936
+ _detectLanguage( text ) {
3937
+
3938
+ const tokenSet = new Set( this._getTokensFromLine( text, true ) );
3939
+ const scores = {};
3940
+
3941
+ for( let [ lang, wordList ] of Object.entries( CodeEditor.keywords ) )
3942
+ {
3943
+ scores[ lang ] = 0;
3944
+ for( let kw of wordList )
3945
+ if( tokenSet.has( kw ) ) scores[ lang ]++;
3946
+ }
3947
+
3948
+ for( let [ lang, wordList ] of Object.entries( CodeEditor.statements ) )
3949
+ {
3950
+ for( let kw of wordList )
3951
+ if( tokenSet.has( kw ) ) scores[ lang ]++;
3952
+ }
3953
+
3954
+ for( let [ lang, wordList ] of Object.entries( CodeEditor.utils ) )
3955
+ {
3956
+ for( let kw of wordList )
3957
+ if( tokenSet.has( kw ) ) scores[ lang ]++;
3958
+ }
3959
+
3960
+ for( let [ lang, wordList ] of Object.entries( CodeEditor.types ) )
3961
+ {
3962
+ for( let kw of wordList )
3963
+ if( tokenSet.has( kw ) ) scores[ lang ]++;
3964
+ }
3965
+
3966
+ for( let [ lang, wordList ] of Object.entries( CodeEditor.builtIn ) )
3967
+ {
3968
+ for( let kw of wordList )
3969
+ if( tokenSet.has( kw ) ) scores[ lang ]++;
3970
+ }
3971
+
3972
+ const sorted = Object.entries( scores ).sort( ( a, b ) => b[ 1 ] - a[ 1 ] );
3973
+ return sorted[0][1] > 0 ? sorted[0][0] : undefined;
3974
+ }
3975
+
3552
3976
  lineUp( cursor, resetLeft ) {
3553
3977
 
3554
3978
  if( this.code.lines[ cursor.line - 1 ] == undefined )
@@ -3664,7 +4088,7 @@ class CodeEditor {
3664
4088
  // Use main cursor
3665
4089
  this._removeSecondaryCursors();
3666
4090
 
3667
- var cursor = this._getCurrentCursor();
4091
+ var cursor = this.getCurrentCursor();
3668
4092
  this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, cursor );
3669
4093
 
3670
4094
  this.startSelection( cursor );
@@ -3754,11 +4178,8 @@ class CodeEditor {
3754
4178
  }
3755
4179
 
3756
4180
  const currentScrollTop = this.getScrollTop();
3757
- const tabsHeight = this.tabs.root.getBoundingClientRect().height;
3758
- const statusPanelHeight = this.skipInfo ? 0 : this.statusPanel.root.getBoundingClientRect().height;
3759
- const scrollerHeight = this.codeScroller.offsetHeight;
3760
-
3761
- var lastLine = ( ( scrollerHeight - tabsHeight - statusPanelHeight + currentScrollTop ) / this.lineHeight )|0;
4181
+ const scrollerHeight = this.codeScroller.offsetHeight - this._fullVerticalOffset;
4182
+ const lastLine = ( ( scrollerHeight + currentScrollTop ) / this.lineHeight )|0;
3762
4183
  if( cursor.line >= lastLine )
3763
4184
  {
3764
4185
  this.setScrollTop( currentScrollTop + this.lineHeight );
@@ -3814,6 +4235,16 @@ class CodeEditor {
3814
4235
  return cursors;
3815
4236
  }
3816
4237
 
4238
+ getCurrentCursor( removeOthers ) {
4239
+
4240
+ if( removeOthers )
4241
+ {
4242
+ this._removeSecondaryCursors();
4243
+ }
4244
+
4245
+ return this.cursors.children[ 0 ];
4246
+ }
4247
+
3817
4248
  relocateCursors() {
3818
4249
 
3819
4250
  for( let cursor of this.cursors.children )
@@ -3863,7 +4294,7 @@ class CodeEditor {
3863
4294
 
3864
4295
  resetCursorPos( flag, cursor ) {
3865
4296
 
3866
- cursor = cursor ?? this._getCurrentCursor();
4297
+ cursor = cursor ?? this.getCurrentCursor();
3867
4298
 
3868
4299
  if( flag & CodeEditor.CURSOR_LEFT )
3869
4300
  {
@@ -3880,6 +4311,71 @@ class CodeEditor {
3880
4311
  }
3881
4312
  }
3882
4313
 
4314
+ _addCursor( line = 0, position = 0, force, isMain = false ) {
4315
+
4316
+ // If cursor in that position exists, remove it instead..
4317
+ const exists = Array.from( this.cursors.children ).find( v => v.position == position && v.line == line );
4318
+ if( exists && !force )
4319
+ {
4320
+ if( !exists.isMain )
4321
+ exists.remove();
4322
+
4323
+ return;
4324
+ }
4325
+
4326
+ let cursor = document.createElement( 'div' );
4327
+ cursor.name = "cursor" + this.cursors.childElementCount;
4328
+ cursor.className = "cursor";
4329
+ cursor.innerHTML = "&nbsp;";
4330
+ cursor.isMain = isMain;
4331
+ cursor._left = position * this.charWidth;
4332
+ cursor.style.left = "calc( " + cursor._left + "px + " + this.xPadding + " )";
4333
+ cursor._top = line * this.lineHeight;
4334
+ cursor.style.top = cursor._top + "px";
4335
+ cursor._position = position;
4336
+ cursor._line = line;
4337
+ cursor.print = (function() { console.log( this._line, this._position ) }).bind( cursor );
4338
+ cursor.isLast = (function() { return this.cursors.lastChild == cursor; }).bind( this );
4339
+
4340
+ Object.defineProperty( cursor, 'line', {
4341
+ get: (v) => { return cursor._line },
4342
+ set: (v) => {
4343
+ cursor._line = v;
4344
+ if( cursor.isMain ) this._setActiveLine( v );
4345
+ }
4346
+ } );
4347
+
4348
+ Object.defineProperty( cursor, 'position', {
4349
+ get: (v) => { return cursor._position },
4350
+ set: (v) => {
4351
+ cursor._position = v;
4352
+ if( cursor.isMain )
4353
+ {
4354
+ const activeLine = this.state.activeLine;
4355
+ this._updateDataInfoPanel( "@cursor-data", `Ln ${ activeLine + 1 }, Col ${ v + 1 }` );
4356
+ }
4357
+ }
4358
+ } );
4359
+
4360
+ this.cursors.appendChild( cursor );
4361
+
4362
+ return cursor;
4363
+ }
4364
+
4365
+ _removeSecondaryCursors() {
4366
+
4367
+ while( this.cursors.childElementCount > 1 )
4368
+ this.cursors.lastChild.remove();
4369
+ }
4370
+
4371
+ _logCursors() {
4372
+
4373
+ for( let cursor of this.cursors.children )
4374
+ {
4375
+ cursor.print();
4376
+ }
4377
+ }
4378
+
3883
4379
  _addSpaceTabs( cursor, n ) {
3884
4380
 
3885
4381
  for( var i = 0; i < n; ++i )
@@ -3890,9 +4386,10 @@ class CodeEditor {
3890
4386
 
3891
4387
  _addSpaces( n ) {
3892
4388
 
3893
- for( var i = 0; i < n; ++i ) {
4389
+ for( var i = 0; i < n; ++i )
4390
+ {
3894
4391
  this.root.dispatchEvent( new CustomEvent( 'keydown', { 'detail': {
3895
- skip_undo: true,
4392
+ skipUndo: true,
3896
4393
  key: ' ',
3897
4394
  targetCursor: this._lastProcessedCursorIndex
3898
4395
  }}));
@@ -3913,7 +4410,8 @@ class CodeEditor {
3913
4410
  }
3914
4411
 
3915
4412
  // Only tabs/spaces in the line...
3916
- if( lineStart == -1 ) {
4413
+ if( lineStart == -1 )
4414
+ {
3917
4415
  lineStart = this.code.lines[ lidx ].length;
3918
4416
  }
3919
4417
 
@@ -3964,24 +4462,28 @@ class CodeEditor {
3964
4462
  }, 20 );
3965
4463
  }
3966
4464
 
3967
- resize( pMaxLength, onResize ) {
4465
+ resize( flag = CodeEditor.RESIZE_SCROLLBAR_H_V, pMaxLength, onResize ) {
3968
4466
 
3969
4467
  setTimeout( () => {
3970
4468
 
3971
- // Update max viewport
3972
- const maxLineLength = pMaxLength ?? this.getMaxLineLength();
3973
- const scrollWidth = maxLineLength * this.charWidth + CodeEditor.LINE_GUTTER_WIDTH;
3974
-
3975
- const tabsHeight = this.tabs.root.getBoundingClientRect().height;
3976
- const statusPanelHeight = this.skipInfo ? 0 : this.statusPanel.root.getBoundingClientRect().height;
3977
- const scrollHeight = this.code.lines.length * this.lineHeight;
4469
+ let scrollWidth, scrollHeight;
3978
4470
 
3979
- this._lastMaxLineLength = maxLineLength;
4471
+ if( flag & CodeEditor.RESIZE_SCROLLBAR_H )
4472
+ {
4473
+ // Update max viewport
4474
+ const maxLineLength = pMaxLength ?? this.getMaxLineLength();
4475
+ this._lastMaxLineLength = maxLineLength;
4476
+ scrollWidth = maxLineLength * this.charWidth + CodeEditor.LINE_GUTTER_WIDTH;
4477
+ this.codeSizer.style.minWidth = scrollWidth + "px";
4478
+ }
3980
4479
 
3981
- this.codeSizer.style.minWidth = scrollWidth + "px";
3982
- this.codeSizer.style.minHeight = ( scrollHeight + tabsHeight + statusPanelHeight ) + "px";
4480
+ if( flag & CodeEditor.RESIZE_SCROLLBAR_V )
4481
+ {
4482
+ scrollHeight = this.code.lines.length * this.lineHeight + this._fullVerticalOffset;
4483
+ this.codeSizer.style.minHeight = scrollHeight + "px";
4484
+ }
3983
4485
 
3984
- this.resizeScrollBars();
4486
+ this.resizeScrollBars( flag );
3985
4487
 
3986
4488
  if( onResize )
3987
4489
  {
@@ -3991,37 +4493,52 @@ class CodeEditor {
3991
4493
  }, 10 );
3992
4494
  }
3993
4495
 
3994
- resizeScrollBars() {
4496
+ resizeIfNecessary( cursor, force ) {
3995
4497
 
3996
- const totalLinesInViewport = ((this.codeScroller.offsetHeight) / this.lineHeight)|0;
3997
-
3998
- if( totalLinesInViewport >= this.code.lines.length )
3999
- {
4000
- this.codeScroller.classList.remove( 'with-vscrollbar' );
4001
- this.vScrollbar.root.classList.add( 'scrollbar-unused' );
4002
- }
4003
- else
4498
+ const maxLineLength = this.getMaxLineLength();
4499
+ const numViewportChars = Math.floor( ( this.codeScroller.clientWidth - CodeEditor.LINE_GUTTER_WIDTH ) / this.charWidth );
4500
+ if( force || ( maxLineLength >= numViewportChars && maxLineLength != this._lastMaxLineLength ) )
4004
4501
  {
4005
- this.codeScroller.classList.add( 'with-vscrollbar' );
4006
- this.vScrollbar.root.classList.remove( 'scrollbar-unused' );
4007
- this.vScrollbar.thumb.size = (totalLinesInViewport / this.code.lines.length);
4008
- this.vScrollbar.thumb.style.height = (this.vScrollbar.thumb.size * 100.0) + "%";
4502
+ this.resize( CodeEditor.RESIZE_SCROLLBAR_H, maxLineLength, () => {
4503
+ if( cursor.position > numViewportChars )
4504
+ {
4505
+ this.setScrollLeft( cursor.position * this.charWidth );
4506
+ }
4507
+ } );
4009
4508
  }
4509
+ }
4010
4510
 
4011
- const numViewportChars = Math.floor( ( this.codeScroller.clientWidth - CodeEditor.LINE_GUTTER_WIDTH ) / this.charWidth );
4012
- const maxLineLength = this._lastMaxLineLength;
4511
+ resizeScrollBars( flag = CodeEditor.RESIZE_SCROLLBAR_H_V ) {
4013
4512
 
4014
- if( numViewportChars >= maxLineLength )
4513
+ if( flag & CodeEditor.RESIZE_SCROLLBAR_V )
4015
4514
  {
4016
- this.codeScroller.classList.remove( 'with-hscrollbar' );
4017
- this.hScrollbar.root.classList.add( 'scrollbar-unused' );
4515
+ const totalLinesInViewport = (( this.codeScroller.offsetHeight ) / this.lineHeight)|0;
4516
+ const needsVerticalScrollbar = ( this.code.lines.length >= totalLinesInViewport );
4517
+ if( needsVerticalScrollbar )
4518
+ {
4519
+ this.vScrollbar.thumb.size = ( totalLinesInViewport / this.code.lines.length );
4520
+ this.vScrollbar.thumb.style.height = (this.vScrollbar.thumb.size * 100.0) + "%";
4521
+ }
4522
+
4523
+ this.vScrollbar.root.classList.toggle( 'hidden', !needsVerticalScrollbar );
4524
+ this.hScrollbar.root.style.width = `calc(100% - ${ 48 + ( needsVerticalScrollbar ? ScrollBar.SCROLLBAR_VERTICAL_WIDTH : 0 ) }px)`; // 48 is the line gutter
4525
+ this.codeArea.root.style.width = `calc(100% - ${ needsVerticalScrollbar ? ScrollBar.SCROLLBAR_VERTICAL_WIDTH : 0 }px)`;
4018
4526
  }
4019
- else
4527
+
4528
+ if( flag & CodeEditor.RESIZE_SCROLLBAR_H )
4020
4529
  {
4021
- this.codeScroller.classList.add( 'with-hscrollbar' );
4022
- this.hScrollbar.root.classList.remove( 'scrollbar-unused' );
4023
- this.hScrollbar.thumb.size = (numViewportChars / maxLineLength);
4024
- this.hScrollbar.thumb.style.width = (this.hScrollbar.thumb.size * 100.0) + "%";
4530
+ const numViewportChars = Math.floor( ( this.codeScroller.clientWidth - CodeEditor.LINE_GUTTER_WIDTH ) / this.charWidth );
4531
+ const maxLineLength = this._lastMaxLineLength;
4532
+ const needsHorizontalScrollbar = maxLineLength >= numViewportChars;
4533
+
4534
+ if( needsHorizontalScrollbar )
4535
+ {
4536
+ this.hScrollbar.thumb.size = ( numViewportChars / maxLineLength );
4537
+ this.hScrollbar.thumb.style.width = ( this.hScrollbar.thumb.size * 100.0 ) + "%";
4538
+ }
4539
+
4540
+ this.hScrollbar.root.classList.toggle( 'hidden', !needsHorizontalScrollbar );
4541
+ this.codeArea.root.style.height = `calc(100% - ${ this._verticalBottomOffset + ( needsHorizontalScrollbar ? ScrollBar.SCROLLBAR_HORIZONTAL_HEIGHT : 0 ) }px)`;
4025
4542
  }
4026
4543
  }
4027
4544
 
@@ -4095,7 +4612,6 @@ class CodeEditor {
4095
4612
  }
4096
4613
 
4097
4614
  getCharAtPos( cursor, offset = 0 ) {
4098
-
4099
4615
  return this.code.lines[ cursor.line ][ cursor.position + offset ];
4100
4616
  }
4101
4617
 
@@ -4164,7 +4680,6 @@ class CodeEditor {
4164
4680
  }
4165
4681
 
4166
4682
  measureString( str ) {
4167
-
4168
4683
  return str.length * this.charWidth;
4169
4684
  }
4170
4685
 
@@ -4217,46 +4732,64 @@ class CodeEditor {
4217
4732
  showAutoCompleteBox( key, cursor ) {
4218
4733
 
4219
4734
  if( !cursor.isMain )
4735
+ {
4220
4736
  return;
4737
+ }
4221
4738
 
4222
- const [word, start, end] = this.getWordAtPos( cursor, -1 );
4223
- if( key == ' ' || !word.length ) {
4739
+ const [ word, start, end ] = this.getWordAtPos( cursor, -1 );
4740
+ if( key == ' ' || !word.length )
4741
+ {
4224
4742
  this.hideAutoCompleteBox();
4225
4743
  return;
4226
4744
  }
4227
4745
 
4228
4746
  this.autocomplete.innerHTML = ""; // Clear all suggestions
4229
4747
 
4230
- let suggestions = [];
4231
-
4232
4748
  // Add language special keys...
4233
- suggestions = suggestions.concat(
4234
- Object.keys( CodeEditor.builtIn[ this.highlight ] ?? {} ),
4235
- Object.keys( CodeEditor.keywords[ this.highlight ] ?? {} ),
4236
- Object.keys( CodeEditor.statementsAndDeclarations[ this.highlight ] ?? {} ),
4237
- Object.keys( CodeEditor.types[ this.highlight ] ?? {} ),
4238
- Object.keys( CodeEditor.utils[ this.highlight ] ?? {} )
4239
- );
4749
+ let suggestions = [
4750
+ ...Array.from( CodeEditor.keywords[ this.highlight ] ?? [] ),
4751
+ ...Array.from( CodeEditor.builtIn[ this.highlight ] ?? [] ),
4752
+ ...Array.from( CodeEditor.statements[ this.highlight ] ?? [] ),
4753
+ ...Array.from( CodeEditor.types[ this.highlight ] ?? [] ),
4754
+ ...Array.from( CodeEditor.utils[ this.highlight ] ?? [] )
4755
+ ];
4240
4756
 
4241
- // Add words in current tab plus remove current word
4242
- // suggestions = suggestions.concat( Object.keys(this.code.tokens).filter( a => a != word ) );
4757
+ suggestions = suggestions.concat( Object.keys(this.code.tokens).filter( a => a != word ) );
4758
+
4759
+ const scopeStack = [ ...this.code.lineScopes[ cursor.line ] ];
4760
+ const scope = scopeStack.at( -1 );
4761
+ if( scope.type.startsWith( "enum" ) )
4762
+ {
4763
+ const enumValues = Array.from( this.code.symbolsTable ).filter( s => s[ 1 ][ 0 ].kind === "enum_value" && s[ 1 ][ 0 ].scope === scope.name ).map( s => s[ 0 ] );
4764
+ suggestions = suggestions.concat( enumValues.slice( 0, -1 ) );
4765
+ }
4766
+ else
4767
+ {
4768
+ const otherValues = Array.from( this.code.symbolsTable ).map( s => s[ 0 ] );
4769
+ suggestions = suggestions.concat( otherValues.slice( 0, -1 ) );
4770
+ }
4771
+
4772
+ const prefix = word.toLowerCase();
4243
4773
 
4244
4774
  // Remove 1/2 char words and duplicates...
4245
- suggestions = suggestions.filter( (value, index) => value.length > 2 && suggestions.indexOf(value) === index );
4775
+ suggestions = Array.from( new Set( suggestions )).filter( s => s.length > 2 && s.toLowerCase().includes( prefix ) );
4246
4776
 
4247
4777
  // Order...
4248
- suggestions = suggestions.sort( ( a, b ) => a.localeCompare( b ) );
4778
+
4779
+ function scoreSuggestion( s, prefix ) {
4780
+ if( s.startsWith( prefix ) ) return 0; // best option
4781
+ if( s.includes( prefix )) return 1;
4782
+ return 2; // worst
4783
+ }
4784
+
4785
+ suggestions = suggestions.sort( ( a, b ) => scoreSuggestion( a, prefix ) - scoreSuggestion( b, prefix ) || a.localeCompare( b ) );
4249
4786
 
4250
4787
  for( let s of suggestions )
4251
4788
  {
4252
- if( !s.toLowerCase().includes( word.toLowerCase() ) )
4253
- continue;
4254
-
4255
4789
  var pre = document.createElement( 'pre' );
4256
4790
  this.autocomplete.appendChild( pre );
4257
4791
 
4258
4792
  var icon = "Type";
4259
-
4260
4793
  if( this._mustHightlightWord( s, CodeEditor.utils ) )
4261
4794
  icon = "Box";
4262
4795
  else if( this._mustHightlightWord( s, CodeEditor.types ) )
@@ -4292,11 +4825,11 @@ class CodeEditor {
4292
4825
  }
4293
4826
 
4294
4827
  // Select always first option
4295
- this.autocomplete.firstChild.classList.add('selected');
4828
+ this.autocomplete.firstChild.classList.add( 'selected' );
4296
4829
 
4297
4830
  // Show box
4298
- this.autocomplete.classList.toggle('show', true);
4299
- this.autocomplete.classList.toggle('no-scrollbar', !(this.autocomplete.scrollHeight > this.autocomplete.offsetHeight));
4831
+ this.autocomplete.classList.toggle( 'show', true );
4832
+ this.autocomplete.classList.toggle( 'no-scrollbar', !( this.autocomplete.scrollHeight > this.autocomplete.offsetHeight ) );
4300
4833
  this.autocomplete.style.left = (cursor._left + CodeEditor.LINE_GUTTER_WIDTH - this.getScrollLeft()) + "px";
4301
4834
  this.autocomplete.style.top = (cursor._top + 28 + this.lineHeight - this.getScrollTop()) + "px";
4302
4835
 
@@ -4408,7 +4941,7 @@ class CodeEditor {
4408
4941
  }
4409
4942
  else
4410
4943
  {
4411
- const cursor = this._getCurrentCursor();
4944
+ const cursor = this.getCurrentCursor();
4412
4945
 
4413
4946
  if( cursor.selection )
4414
4947
  {
@@ -4452,7 +4985,7 @@ class CodeEditor {
4452
4985
  return;
4453
4986
  }
4454
4987
 
4455
- let cursor = this._getCurrentCursor();
4988
+ let cursor = this.getCurrentCursor();
4456
4989
  let cursorData = new LX.vec2( cursor.position, cursor.line );
4457
4990
  let line = null;
4458
4991
  let char = -1;
@@ -4599,7 +5132,7 @@ class CodeEditor {
4599
5132
  this.codeScroller.scrollTo( 0, Math.max( line - 15 ) * this.lineHeight );
4600
5133
 
4601
5134
  // Select line ?
4602
- var cursor = this._getCurrentCursor( true );
5135
+ var cursor = this.getCurrentCursor( true );
4603
5136
  this.cursorToLine( cursor, line - 1, true );
4604
5137
  }
4605
5138
 
@@ -4654,7 +5187,7 @@ class CodeEditor {
4654
5187
 
4655
5188
  number = number ?? this.state.activeLine;
4656
5189
 
4657
- const cursor = this._getCurrentCursor();
5190
+ const cursor = this.getCurrentCursor();
4658
5191
  this._updateDataInfoPanel( "@cursor-data", `Ln ${ number + 1 }, Col ${ cursor.position + 1 }` );
4659
5192
 
4660
5193
  const oldLocal = this.toLocalLine( this.state.activeLine );
@@ -4731,23 +5264,32 @@ class CodeEditor {
4731
5264
  }
4732
5265
 
4733
5266
  CodeEditor.languages = {
4734
- 'Plain Text': { ext: 'txt', blockComments: false, singleLineComments: false, numbers: false },
4735
- 'JavaScript': { ext: 'js' },
4736
- 'TypeScript': { ext: 'ts' },
4737
- 'C': { ext: [ 'c', 'h' ], usePreprocessor: true },
4738
- 'C++': { ext: [ 'cpp', 'hpp' ], usePreprocessor: true },
4739
- 'CSS': { ext: 'css' },
4740
- 'CMake': { ext: 'cmake', singleLineCommentToken: '#', blockComments: false, ignoreCase: true },
4741
- 'GLSL': { ext: 'glsl', usePreprocessor: true },
4742
- 'WGSL': { ext: 'wgsl', usePreprocessor: true },
4743
- 'JSON': { ext: 'json', blockComments: false, singleLineComments: false },
4744
- 'XML': { ext: 'xml', tags: true },
4745
- 'Rust': { ext: 'rs' },
4746
- 'Python': { ext: 'py', singleLineCommentToken: '#' },
4747
- 'HTML': { ext: 'html', tags: true, singleLineComments: false, blockCommentsTokens: [ '<!--', '-->' ], numbers: false },
4748
- 'Batch': { ext: 'bat', blockComments: false, singleLineCommentToken: '::' },
4749
- 'Markdown': { ext: 'md', blockComments: false, singleLineCommentToken: '::', tags: true, numbers: false },
4750
- 'PHP': { ext: 'php' },
5267
+ 'Plain Text': { ext: "txt", blockComments: false, singleLineComments: false, numbers: false, icon: "AlignLeft gray" },
5268
+ 'JavaScript': { ext: "js", icon: "Js goldenrod" },
5269
+ 'TypeScript': { ext: "ts", icon: "Ts pipelineblue" },
5270
+ 'C': { ext: [ 'c', 'h' ], usePreprocessor: true, icon: { 'c': "C pictonblue", 'h': "C heliotrope" } },
5271
+ 'C++': { ext: [ "cpp", "hpp" ], usePreprocessor: true, icon: { 'cpp': "CPlusPlus pictonblue", 'hpp': "CPlusPlus heliotrope" } },
5272
+ 'CSS': { ext: "css", icon: "Hash dodgerblue" },
5273
+ 'CMake': { ext: "cmake", singleLineCommentToken: '#', blockComments: false, ignoreCase: true },
5274
+ 'GLSL': { ext: "glsl", usePreprocessor: true },
5275
+ 'WGSL': { ext: "wgsl", usePreprocessor: true },
5276
+ 'JSON': { ext: "json", blockComments: false, singleLineComments: false, icon: "Braces fg-primary" },
5277
+ 'XML': { ext: "xml", tags: true, icon: "Rss orange" },
5278
+ 'Rust': { ext: "rs", icon: "Rust fg-primary" },
5279
+ 'Python': { ext: "py", singleLineCommentToken: '#', icon: "Python munsellblue" },
5280
+ 'HTML': { ext: "html", tags: true, singleLineComments: false, blockCommentsTokens: [ '<!--', '-->' ], numbers: false, icon: "Code orange" },
5281
+ 'Batch': { ext: "bat", blockComments: false, singleLineCommentToken: '::', ignoreCase: true, icon: "Windows lightblue" },
5282
+ 'Markdown': { ext: "md", blockComments: false, singleLineCommentToken: '::', tags: true, numbers: false, icon: "Markdown fg-primary" },
5283
+ 'PHP': { ext: "php", icon: "Php blueviolet" },
5284
+ };
5285
+
5286
+ CodeEditor.nativeTypes = {
5287
+ 'C++': ['int', 'float', 'double', 'bool', 'long', 'short', 'char', 'wchar_t', 'void']
5288
+ };
5289
+
5290
+ CodeEditor.declarationKeywords = {
5291
+ 'JavaScript': ['var', 'let', 'const', 'this', 'static', 'class'],
5292
+ 'C++': [...CodeEditor.nativeTypes["C++"], 'const', 'auto', 'class', 'struct', 'namespace', 'enum', 'extern']
4751
5293
  };
4752
5294
 
4753
5295
  CodeEditor.keywords = {
@@ -4757,14 +5299,15 @@ CodeEditor.keywords = {
4757
5299
  'enum', 'type'],
4758
5300
  'C': ['int', 'float', 'double', 'long', 'short', 'char', 'const', 'void', 'true', 'false', 'auto', 'struct', 'typedef', 'signed', 'volatile', 'unsigned', 'static', 'extern', 'enum', 'register',
4759
5301
  'union'],
4760
- 'C++': ['int', 'float', 'double', 'bool', 'long', 'short', 'char', 'wchar_t', 'const', 'static_cast', 'dynamic_cast', 'new', 'delete', 'void', 'true', 'false', 'auto', 'class', 'struct', 'typedef', 'nullptr',
5302
+ 'C++': [...CodeEditor.nativeTypes["C++"], 'const', 'static_cast', 'dynamic_cast', 'new', 'delete', 'true', 'false', 'auto', 'class', 'struct', 'typedef', 'nullptr',
4761
5303
  'NULL', 'signed', 'unsigned', 'namespace', 'enum', 'extern', 'union', 'sizeof', 'static', 'private', 'public'],
4762
5304
  'CMake': ['cmake_minimum_required', 'set', 'not', 'if', 'endif', 'exists', 'string', 'strequal', 'add_definitions', 'macro', 'endmacro', 'file', 'list', 'source_group', 'add_executable',
4763
5305
  'target_include_directories', 'set_target_properties', 'set_property', 'add_compile_options', 'add_link_options', 'include_directories', 'add_library', 'target_link_libraries',
4764
5306
  'target_link_options', 'add_subdirectory', 'add_compile_definitions', 'project', 'cache'],
4765
5307
  'JSON': ['true', 'false'],
4766
5308
  'GLSL': ['true', 'false', 'function', 'int', 'float', 'vec2', 'vec3', 'vec4', 'mat2x2', 'mat3x3', 'mat4x4', 'struct'],
4767
- 'CSS': ['body', 'html', 'canvas', 'div', 'input', 'span', '.'],
5309
+ 'CSS': ['body', 'html', 'canvas', 'div', 'input', 'span', '.', 'table', 'tr', 'td', 'th', 'label', 'video', 'img', 'code', 'button', 'select', 'option', 'svg', 'media', 'all',
5310
+ 'i', 'a', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'last-child', 'tbody', 'pre', 'monospace', 'font-face'],
4768
5311
  'WGSL': ['var', 'let', 'true', 'false', 'fn', 'bool', 'u32', 'i32', 'f16', 'f32', 'vec2', 'vec3', 'vec4', 'vec2f', 'vec3f', 'vec4f', 'mat2x2f', 'mat3x3f', 'mat4x4f', 'array', 'atomic', 'struct',
4769
5312
  'sampler', 'sampler_comparison', 'texture_depth_2d', 'texture_depth_2d_array', 'texture_depth_cube', 'texture_depth_cube_array', 'texture_depth_multisampled_2d',
4770
5313
  'texture_external', 'texture_1d', 'texture_2d', 'texture_2d_array', 'texture_3d', 'texture_cube', 'texture_cube_array', 'texture_storage_1d', 'texture_storage_2d',
@@ -4772,11 +5315,11 @@ CodeEditor.keywords = {
4772
5315
  'Rust': ['as', 'const', 'crate', 'enum', 'extern', 'false', 'fn', 'impl', 'in', 'let', 'mod', 'move', 'mut', 'pub', 'ref', 'self', 'Self', 'static', 'struct', 'super', 'trait', 'true',
4773
5316
  'type', 'unsafe', 'use', 'where', 'abstract', 'become', 'box', 'final', 'macro', 'override', 'priv', 'typeof', 'unsized', 'virtual'],
4774
5317
  'Python': ['False', 'def', 'None', 'True', 'in', 'is', 'and', 'lambda', 'nonlocal', 'not', 'or'],
4775
- 'Batch': ['set', 'SET', 'echo', 'ECHO', 'off', 'OFF', 'del', 'DEL', 'defined', 'DEFINED', 'setlocal', 'SETLOCAL', 'enabledelayedexpansion', 'ENABLEDELAYEDEXPANSION', 'driverquery',
4776
- 'DRIVERQUERY', 'print', 'PRINT'],
5318
+ 'Batch': ['set', 'echo', 'off', 'del', 'defined', 'setlocal', 'enabledelayedexpansion', 'driverquery', 'print'],
4777
5319
  'HTML': ['html', 'meta', 'title', 'link', 'script', 'body', 'DOCTYPE', 'head', 'br', 'i', 'a', 'li', 'img', 'tr', 'td', 'h1', 'h2', 'h3', 'h4', 'h5'],
4778
5320
  'Markdown': ['br', 'i', 'a', 'li', 'img', 'table', 'title', 'tr', 'td', 'h1', 'h2', 'h3', 'h4', 'h5'],
4779
- 'PHP': ['const', 'function', 'array', 'new', 'int', 'string', '$this', 'public', 'null', 'private', 'protected', 'implements', 'class', 'use', 'namespace', 'abstract', 'clone', 'final'],
5321
+ 'PHP': ['const', 'function', 'array', 'new', 'int', 'string', '$this', 'public', 'null', 'private', 'protected', 'implements', 'class', 'use', 'namespace', 'abstract', 'clone', 'final',
5322
+ 'enum'],
4780
5323
  };
4781
5324
 
4782
5325
  CodeEditor.utils = { // These ones don't have hightlight, used as suggestions to autocomplete only...
@@ -4813,7 +5356,7 @@ CodeEditor.builtIn = {
4813
5356
  'PHP': ['echo', 'print'],
4814
5357
  };
4815
5358
 
4816
- CodeEditor.statementsAndDeclarations = {
5359
+ CodeEditor.statements = {
4817
5360
  'JavaScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await'],
4818
5361
  'TypeScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await', 'as'],
4819
5362
  'CSS': ['@', 'import'],
@@ -4831,6 +5374,7 @@ CodeEditor.statementsAndDeclarations = {
4831
5374
 
4832
5375
  CodeEditor.symbols = {
4833
5376
  'JavaScript': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '??'],
5377
+ 'TypeScript': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '??'],
4834
5378
  'C': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '*', '-', '+'],
4835
5379
  'C++': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '::', '*', '-', '+'],
4836
5380
  'CMake': ['{', '}'],
@@ -4843,19 +5387,19 @@ CodeEditor.symbols = {
4843
5387
  'Batch': ['[', ']', '(', ')', '%'],
4844
5388
  'HTML': ['<', '>', '/'],
4845
5389
  'XML': ['<', '>', '/'],
4846
- 'PHP': ['{', '}', '(', ')'],
5390
+ 'PHP': ['[', ']', '{', '}', '(', ')'],
4847
5391
  };
4848
5392
 
4849
5393
  CodeEditor.REGISTER_LANGUAGE = function( name, options = {}, def, rules )
4850
5394
  {
4851
5395
  CodeEditor.languages[ name ] = options;
4852
5396
 
4853
- if( def?.keywords ) CodeEditor.keywords[ name ] = def.keywords.reduce((a, v) => ({ ...a, [v]: true}), {});
4854
- if( def?.utils ) CodeEditor.utils[ name ] = def.utils.reduce((a, v) => ({ ...a, [v]: true}), {});
4855
- if( def?.types ) CodeEditor.types[ name ] = def.types.reduce((a, v) => ({ ...a, [v]: true}), {});
4856
- if( def?.builtIn ) CodeEditor.builtIn[ name ] = def.builtIn.reduce((a, v) => ({ ...a, [v]: true}), {});
4857
- if( def?.statementsAndDeclarations ) CodeEditor.statementsAndDeclarations[ name ] = def.statementsAndDeclarations.reduce((a, v) => ({ ...a, [v]: true}), {});
4858
- if( def?.symbols ) CodeEditor.symbols[ name ] = def.symbols.reduce((a, v) => ({ ...a, [v]: true}), {});
5397
+ if( def?.keywords ) CodeEditor.keywords[ name ] = new Set( def.keywords );
5398
+ if( def?.utils ) CodeEditor.utils[ name ] = new Set( def.utils );
5399
+ if( def?.types ) CodeEditor.types[ name ] = new Set( def.types );
5400
+ if( def?.builtIn ) CodeEditor.builtIn[ name ] = new Set( def.builtIn );
5401
+ if( def?.statements ) CodeEditor.statements[ name ] = new Set( def.statements );
5402
+ if( def?.symbols ) CodeEditor.symbols[ name ] = new Set( def.symbols );
4859
5403
 
4860
5404
  if( rules ) HighlightRules[ name ] = rules;
4861
5405
  };