lexgui 0.7.2 → 0.7.4

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,13 +142,16 @@ 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;
139
151
  this.type = type;
140
152
 
141
153
  this.root = document.createElement( 'div' );
142
- this.root.className = "lexcodescrollbar";
154
+ this.root.className = "lexcodescrollbar hidden";
143
155
 
144
156
  if( type & ScrollBar.SCROLLBAR_VERTICAL )
145
157
  this.root.classList.add( 'vertical' );
@@ -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,488 @@ 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
 
926
- } 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 );
969
+ if( _c0 == '{' && _c1 == '}' )
970
+ {
971
+ this.code.lines.splice( cursor.line, 0, "" );
972
+ this._addSpaceTabs( cursor, tabs + 1 );
973
+ this.code.lines[ cursor.line + 1 ] = " ".repeat(spaces) + this.code.lines[ cursor.line + 1 ];
974
+ }
975
+ else
976
+ {
977
+ this._addSpaceTabs( cursor, tabs );
932
978
  }
933
- }
934
- // Move up autocomplete selection
935
- else
936
- {
937
- this._moveArrowSelectedAutoComplete('up');
938
- }
939
- });
940
979
 
941
- this.action( 'ArrowDown', false, ( ln, cursor, e ) => {
980
+ this.processLines();
981
+ });
942
982
 
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
- }
983
+ this.action( 'ArrowUp', false, ( ln, cursor, e ) => {
984
+
985
+ // Move cursor..
986
+ if( !this.isAutoCompleteActive )
987
+ {
988
+ if( e.shiftKey ) {
989
+ if( !cursor.selection )
990
+ this.startSelection( cursor );
952
991
 
953
- const canGoDown = this.lineDown( cursor );
954
- const letter = this.getCharAtPos( cursor );
992
+ this.lineUp( cursor );
955
993
 
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
- }
994
+ var letter = this.getCharAtPos( cursor );
995
+ if( !letter ) {
996
+ this.cursorToPosition( cursor, this.code.lines[ cursor.line ].length );
997
+ }
998
+
999
+ this._processSelection( cursor, e, false );
960
1000
 
961
- if( e.shiftKey ) {
962
- this._processSelection( cursor, e );
1001
+ } else {
1002
+ this.endSelection();
1003
+ this.lineUp( cursor );
1004
+ // Go to end of line if out of line
1005
+ var letter = this.getCharAtPos( cursor );
1006
+ if( !letter ) this.actions['End'].callback( cursor.line, cursor, e );
1007
+ }
963
1008
  }
964
- }
965
- // Move down autocomplete selection
966
- else
967
- {
968
- this._moveArrowSelectedAutoComplete('down');
969
- }
970
- });
1009
+ // Move up autocomplete selection
1010
+ else
1011
+ {
1012
+ this._moveArrowSelectedAutoComplete('up');
1013
+ }
1014
+ });
971
1015
 
972
- this.action( 'ArrowLeft', false, ( ln, cursor, e ) => {
1016
+ this.action( 'ArrowDown', false, ( ln, cursor, e ) => {
973
1017
 
974
- // Nothing to do..
975
- if( cursor.line == 0 && cursor.position == 0 )
976
- return;
1018
+ // Move cursor..
1019
+ if( !this.isAutoCompleteActive )
1020
+ {
1021
+ if( e.shiftKey ) {
1022
+ if( !cursor.selection )
1023
+ this.startSelection( cursor );
1024
+ } else {
1025
+ this.endSelection();
1026
+ }
977
1027
 
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 );
1028
+ const canGoDown = this.lineDown( cursor );
1029
+ const letter = this.getCharAtPos( cursor );
996
1030
 
997
- // Selections...
998
- if( e.shiftKey ) {
999
- if( !cursor.selection )
1000
- this.startSelection( cursor );
1031
+ // Go to end of line if out of range
1032
+ if( !letter || !canGoDown ) {
1033
+ this.cursorToPosition( cursor, Math.max(this.code.lines[ cursor.line ].length, 0) );
1034
+ }
1035
+
1036
+ if( e.shiftKey ) {
1037
+ this._processSelection( cursor, e );
1038
+ }
1001
1039
  }
1040
+ // Move down autocomplete selection
1002
1041
  else
1003
- this.endSelection();
1042
+ {
1043
+ this._moveArrowSelectedAutoComplete('down');
1044
+ }
1045
+ });
1004
1046
 
1005
- this.cursorToString( cursor, substr, true );
1047
+ this.action( 'ArrowLeft', false, ( ln, cursor, e ) => {
1006
1048
 
1007
- if( e.shiftKey )
1008
- this._processSelection( cursor, e );
1009
- }
1010
- else {
1011
- var letter = this.getCharAtPos( cursor, -1 );
1012
- if( letter ) {
1049
+ // Nothing to do..
1050
+ if( cursor.line == 0 && cursor.position == 0 )
1051
+ return;
1052
+
1053
+ if( e.metaKey ) { // Apple devices (Command)
1054
+ e.preventDefault();
1055
+ this.actions[ 'Home' ].callback( ln, cursor, e );
1056
+ }
1057
+ else if( e.ctrlKey ) {
1058
+ // Get next word
1059
+ const [word, from, to] = this.getWordAtPos( cursor, -1 );
1060
+ // If no length, we change line..
1061
+ if( !word.length && this.lineUp( cursor, true ) ) {
1062
+ const cS = e.cancelShift, kS = e.keepSelection;
1063
+ e.cancelShift = true;
1064
+ e.keepSelection = true;
1065
+ this.actions[ 'End' ].callback( cursor.line, cursor, e );
1066
+ e.cancelShift = cS;
1067
+ e.keepSelection = kS;
1068
+ }
1069
+ var diff = Math.max( cursor.position - from, 1 );
1070
+ var substr = word.substr( 0, diff );
1071
+
1072
+ // Selections...
1013
1073
  if( e.shiftKey ) {
1014
- if( !cursor.selection ) this.startSelection( cursor );
1015
- this.cursorToLeft( letter, cursor );
1016
- this._processSelection( cursor, e, false, CodeEditor.SELECTION_X );
1074
+ if( !cursor.selection )
1075
+ this.startSelection( cursor );
1017
1076
  }
1018
- else {
1019
- if( !cursor.selection ) {
1077
+ else
1078
+ this.endSelection();
1079
+
1080
+ this.cursorToString( cursor, substr, true );
1081
+
1082
+ if( e.shiftKey )
1083
+ this._processSelection( cursor, e );
1084
+ }
1085
+ else {
1086
+ var letter = this.getCharAtPos( cursor, -1 );
1087
+ if( letter ) {
1088
+ if( e.shiftKey ) {
1089
+ if( !cursor.selection ) this.startSelection( cursor );
1020
1090
  this.cursorToLeft( letter, cursor );
1021
- if( this.useAutoComplete && this.isAutoCompleteActive )
1022
- this.showAutoCompleteBox( 'foo', cursor );
1091
+ this._processSelection( cursor, e, false, CodeEditor.SELECTION_X );
1023
1092
  }
1024
1093
  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();
1094
+ if( !cursor.selection ) {
1095
+ this.cursorToLeft( letter, cursor );
1096
+ if( this.useAutoComplete && this.isAutoCompleteActive )
1097
+ this.showAutoCompleteBox( 'foo', cursor );
1098
+ }
1099
+ else {
1100
+ cursor.selection.invertIfNecessary();
1101
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, cursor );
1102
+ this.cursorToLine( cursor, cursor.selection.fromY, true );
1103
+ this.cursorToPosition( cursor, cursor.selection.fromX );
1104
+ this.endSelection();
1105
+ }
1030
1106
  }
1031
1107
  }
1032
- }
1033
- else if( cursor.line > 0 ) {
1108
+ else if( cursor.line > 0 ) {
1034
1109
 
1035
- if( e.shiftKey && !cursor.selection ) this.startSelection( cursor );
1110
+ if( e.shiftKey && !cursor.selection ) this.startSelection( cursor );
1036
1111
 
1037
- this.lineUp( cursor );
1112
+ this.lineUp( cursor );
1038
1113
 
1039
- e.cancelShift = e.keepSelection = true;
1040
- this.actions[ 'End' ].callback( cursor.line, cursor, e );
1041
- delete e.cancelShift; delete e.keepSelection;
1114
+ e.cancelShift = e.keepSelection = true;
1115
+ this.actions[ 'End' ].callback( cursor.line, cursor, e );
1116
+ delete e.cancelShift; delete e.keepSelection;
1042
1117
 
1043
- if( e.shiftKey ) this._processSelection( cursor, e, false );
1118
+ if( e.shiftKey ) this._processSelection( cursor, e, false );
1119
+ }
1044
1120
  }
1045
- }
1046
- });
1047
-
1048
- this.action( 'ArrowRight', false, ( ln, cursor, e ) => {
1049
-
1050
- // Nothing to do..
1051
- if( cursor.line == this.code.lines.length - 1 &&
1052
- cursor.position == this.code.lines[ cursor.line ].length )
1053
- return;
1121
+ });
1054
1122
 
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 );
1123
+ this.action( 'ArrowRight', false, ( ln, cursor, e ) => {
1064
1124
 
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 );
1125
+ // Nothing to do..
1126
+ if( cursor.line == this.code.lines.length - 1 &&
1127
+ cursor.position == this.code.lines[ cursor.line ].length )
1128
+ return;
1069
1129
 
1070
- // Selections...
1071
- if( e.shiftKey ) {
1072
- if( !cursor.selection )
1073
- this.startSelection( cursor );
1130
+ if( e.metaKey ) // Apple devices (Command)
1131
+ {
1132
+ e.preventDefault();
1133
+ this.actions[ 'End' ].callback( ln, cursor );
1074
1134
  }
1075
- else
1076
- this.endSelection();
1077
-
1078
- this.cursorToString( cursor, substr );
1135
+ else if( e.ctrlKey ) // Next word
1136
+ {
1137
+ // Get next word
1138
+ const [ word, from, to ] = this.getWordAtPos( cursor );
1079
1139
 
1080
- if( e.shiftKey )
1081
- this._processSelection( cursor, e );
1082
- }
1083
- else // Next char
1084
- {
1085
- var letter = this.getCharAtPos( cursor );
1086
- if( letter ) {
1140
+ // If no length, we change line..
1141
+ if( !word.length ) this.lineDown( cursor, true );
1142
+ var diff = cursor.position - from;
1143
+ var substr = word.substr( diff );
1087
1144
 
1088
- // Selecting chars
1089
- if( e.shiftKey )
1090
- {
1145
+ // Selections...
1146
+ if( e.shiftKey ) {
1091
1147
  if( !cursor.selection )
1092
1148
  this.startSelection( cursor );
1093
-
1094
- this.cursorToRight( letter, cursor );
1095
- this._processSelection( cursor, e, false, CodeEditor.SELECTION_X );
1096
1149
  }
1097
1150
  else
1098
- {
1099
- if( !cursor.selection ) {
1151
+ this.endSelection();
1152
+
1153
+ this.cursorToString( cursor, substr );
1154
+
1155
+ if( e.shiftKey )
1156
+ this._processSelection( cursor, e );
1157
+ }
1158
+ else // Next char
1159
+ {
1160
+ var letter = this.getCharAtPos( cursor );
1161
+ if( letter ) {
1162
+
1163
+ // Selecting chars
1164
+ if( e.shiftKey )
1165
+ {
1166
+ if( !cursor.selection )
1167
+ this.startSelection( cursor );
1168
+
1100
1169
  this.cursorToRight( letter, cursor );
1101
- if( this.useAutoComplete && this.isAutoCompleteActive )
1102
- this.showAutoCompleteBox( 'foo', cursor );
1170
+ this._processSelection( cursor, e, false, CodeEditor.SELECTION_X );
1103
1171
  }
1104
1172
  else
1105
1173
  {
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();
1174
+ if( !cursor.selection ) {
1175
+ this.cursorToRight( letter, cursor );
1176
+ if( this.useAutoComplete && this.isAutoCompleteActive )
1177
+ this.showAutoCompleteBox( 'foo', cursor );
1178
+ }
1179
+ else
1180
+ {
1181
+ cursor.selection.invertIfNecessary();
1182
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, cursor );
1183
+ this.cursorToLine( cursor, cursor.selection.toY );
1184
+ this.cursorToPosition( cursor, cursor.selection.toX );
1185
+ this.endSelection();
1186
+ }
1111
1187
  }
1112
1188
  }
1113
- }
1114
- else if( this.code.lines[ cursor.line + 1 ] !== undefined ) {
1189
+ else if( this.code.lines[ cursor.line + 1 ] !== undefined ) {
1115
1190
 
1116
- if( e.shiftKey ) {
1117
- if( !cursor.selection ) this.startSelection( cursor );
1118
- }
1119
- else this.endSelection();
1191
+ if( e.shiftKey ) {
1192
+ if( !cursor.selection ) this.startSelection( cursor );
1193
+ }
1194
+ else this.endSelection();
1120
1195
 
1121
- this.lineDown( cursor, true );
1196
+ this.lineDown( cursor, true );
1122
1197
 
1123
- if( e.shiftKey ) this._processSelection( cursor, e, false );
1198
+ if( e.shiftKey ) this._processSelection( cursor, e, false );
1124
1199
 
1125
- this.hideAutoCompleteBox();
1200
+ this.hideAutoCompleteBox();
1201
+ }
1126
1202
  }
1127
- }
1128
- });
1203
+ });
1204
+ }
1129
1205
 
1130
1206
  // Default code tab
1131
1207
 
@@ -1133,9 +1209,10 @@ class CodeEditor {
1133
1209
  this.openedTabs = { };
1134
1210
 
1135
1211
  const onLoadAll = () => {
1212
+
1136
1213
  // Create inspector panel when the initial state is complete
1137
1214
  // and we have at least 1 tab opened
1138
- this.statusPanel = this._createStatusPanel();
1215
+ this.statusPanel = this._createStatusPanel( options );
1139
1216
  if( this.statusPanel )
1140
1217
  {
1141
1218
  area.attach( this.statusPanel );
@@ -1158,6 +1235,21 @@ class CodeEditor {
1158
1235
  }
1159
1236
 
1160
1237
  LX.emit( "@font-size", this.fontSize );
1238
+
1239
+ // Get final sizes for editor elements based on Tabs and status bar offsets
1240
+ LX.doAsync( () => {
1241
+ this._verticalTopOffset = this.tabs?.root.getBoundingClientRect().height ?? 0;
1242
+ this._verticalBottomOffset = this.statusPanel?.root.getBoundingClientRect().height ?? 0;
1243
+ this._fullVerticalOffset = this._verticalTopOffset + this._verticalBottomOffset;
1244
+
1245
+ this.gutter.style.marginTop = `${ this._verticalTopOffset }px`;
1246
+ this.gutter.style.height = `calc(100% - ${ this._fullVerticalOffset }px)`;
1247
+ this.vScrollbar.root.style.marginTop = `${ this._verticalTopOffset }px`;
1248
+ this.vScrollbar.root.style.height = `calc(100% - ${ this._fullVerticalOffset }px)`;
1249
+ this.hScrollbar.root.style.bottom = `${ this._verticalBottomOffset }px`;
1250
+ this.codeArea.root.style.height = `calc(100% - ${ this._fullVerticalOffset }px)`;
1251
+ }, 50 );
1252
+
1161
1253
  });
1162
1254
 
1163
1255
  window.editor = this;
@@ -1176,11 +1268,16 @@ class CodeEditor {
1176
1268
 
1177
1269
  for( let url of options.files )
1178
1270
  {
1179
- this.loadFile( url, { callback: () => {
1271
+ this.loadFile( url, { callback: ( name, text ) => {
1180
1272
  filesLoaded++;
1181
1273
  if( filesLoaded == numFiles )
1182
1274
  {
1183
1275
  onLoadAll();
1276
+
1277
+ if( options.onFilesLoaded )
1278
+ {
1279
+ options.onFilesLoaded( this, numFiles );
1280
+ }
1184
1281
  }
1185
1282
  }});
1186
1283
  }
@@ -1201,67 +1298,70 @@ class CodeEditor {
1201
1298
  onKeyPressed( e ) {
1202
1299
 
1203
1300
  // Toggle visibility of the file explorer
1204
- if( e.key == 'b' && e.ctrlKey && this.explorer )
1301
+ if( e.key == 'b' && e.ctrlKey && this.useFileExplorer )
1205
1302
  {
1206
1303
  this.explorerArea.root.classList.toggle( "hidden" );
1207
1304
  if( this._lastBaseareaWidth )
1208
1305
  {
1209
- this.base_area.root.style.width = this._lastBaseareaWidth;
1306
+ this.baseArea.root.style.width = this._lastBaseareaWidth;
1210
1307
  delete this._lastBaseareaWidth;
1211
1308
 
1212
1309
  } else
1213
1310
  {
1214
- this._lastBaseareaWidth = this.base_area.root.style.width;
1215
- this.base_area.root.style.width = "100%";
1311
+ this._lastBaseareaWidth = this.baseArea.root.style.width;
1312
+ this.baseArea.root.style.width = "100%";
1216
1313
  }
1217
1314
  }
1218
1315
  }
1219
1316
 
1220
1317
  getText( min ) {
1221
-
1222
1318
  return this.code.lines.join( min ? ' ' : '\n' );
1223
1319
  }
1224
1320
 
1225
1321
  // This can be used to empty all text...
1226
1322
  setText( text = "", lang ) {
1227
1323
 
1228
- let new_lines = text.split( '\n' );
1229
- this.code.lines = [].concat( new_lines );
1324
+ let newLines = text.split( '\n' );
1325
+ this.code.lines = [].concat( newLines );
1230
1326
 
1231
1327
  this._removeSecondaryCursors();
1232
1328
 
1233
- let cursor = this._getCurrentCursor( true );
1234
- let lastLine = new_lines.pop();
1329
+ let cursor = this.getCurrentCursor( true );
1330
+ let lastLine = newLines.pop();
1235
1331
 
1236
- this.cursorToLine( cursor, new_lines.length ); // Already substracted 1
1332
+ this.cursorToLine( cursor, newLines.length ); // Already substracted 1
1237
1333
  this.cursorToPosition( cursor, lastLine.length );
1238
- this.processLines();
1334
+
1335
+ this.mustProcessLines = true;
1239
1336
 
1240
1337
  if( lang )
1241
1338
  {
1242
1339
  this._changeLanguage( lang );
1243
1340
  }
1341
+
1342
+ this._processLinesIfNecessary();
1244
1343
  }
1245
1344
 
1246
1345
  appendText( text, cursor ) {
1247
1346
 
1248
1347
  let lidx = cursor.line;
1249
1348
 
1250
- if( cursor.selection ) {
1349
+ if( cursor.selection )
1350
+ {
1251
1351
  this.deleteSelection( cursor );
1252
1352
  lidx = cursor.line;
1253
1353
  }
1254
1354
 
1255
1355
  this.endSelection();
1256
1356
 
1257
- const new_lines = text.replaceAll( '\r', '' ).split( '\n' );
1357
+ const newLines = text.replaceAll( '\r', '' ).split( '\n' );
1258
1358
 
1259
1359
  // Pasting Multiline...
1260
- if( new_lines.length != 1 )
1360
+ if( newLines.length != 1 )
1261
1361
  {
1262
- let num_lines = new_lines.length;
1362
+ let num_lines = newLines.length;
1263
1363
  console.assert( num_lines > 0 );
1264
- const first_line = new_lines.shift();
1364
+ const first_line = newLines.shift();
1265
1365
  num_lines--;
1266
1366
 
1267
1367
  const remaining = this.code.lines[ lidx ].slice( cursor.position );
@@ -1278,11 +1378,11 @@ class CodeEditor {
1278
1378
 
1279
1379
  let _text = null;
1280
1380
 
1281
- for( var i = 0; i < new_lines.length; ++i ) {
1282
- _text = new_lines[ i ];
1381
+ for( var i = 0; i < newLines.length; ++i ) {
1382
+ _text = newLines[ i ];
1283
1383
  this.cursorToLine( cursor, cursor.line++, true );
1284
1384
  // Add remaining...
1285
- if( i == (new_lines.length - 1) )
1385
+ if( i == (newLines.length - 1) )
1286
1386
  _text += remaining;
1287
1387
  this.code.lines.splice( 1 + lidx + i, 0, _text );
1288
1388
  }
@@ -1296,24 +1396,26 @@ class CodeEditor {
1296
1396
  {
1297
1397
  this.code.lines[ lidx ] = [
1298
1398
  this.code.lines[ lidx ].slice( 0, cursor.position ),
1299
- new_lines[ 0 ],
1399
+ newLines[ 0 ],
1300
1400
  this.code.lines[ lidx ].slice( cursor.position )
1301
1401
  ].join('');
1302
1402
 
1303
- this.cursorToPosition( cursor, ( cursor.position + new_lines[ 0 ].length ) );
1403
+ this.cursorToPosition( cursor, ( cursor.position + newLines[ 0 ].length ) );
1304
1404
  this.processLine( lidx );
1305
1405
  }
1306
1406
 
1307
- this.resize( null, ( scrollWidth, scrollHeight ) => {
1407
+ this.resize( CodeEditor.RESIZE_SCROLLBAR_H_V, null, ( scrollWidth, scrollHeight ) => {
1308
1408
  var viewportSizeX = ( this.codeScroller.clientWidth + this.getScrollLeft() ) - CodeEditor.LINE_GUTTER_WIDTH; // Gutter offset
1309
1409
  if( ( cursor.position * this.charWidth ) >= viewportSizeX )
1410
+ {
1310
1411
  this.setScrollLeft( this.code.lines[ lidx ].length * this.charWidth );
1412
+ }
1311
1413
  } );
1312
1414
  }
1313
1415
 
1314
- loadFile( file, options = {} ) {
1416
+ async loadFile( file, options = {} ) {
1315
1417
 
1316
- const inner_add_tab = ( text, name, title ) => {
1418
+ const _innerAddTab = ( text, name, title ) => {
1317
1419
 
1318
1420
  // Remove Carriage Return in some cases and sub tabs using spaces
1319
1421
  text = text.replaceAll( '\r', '' );
@@ -1325,7 +1427,7 @@ class CodeEditor {
1325
1427
 
1326
1428
  // Add item in the explorer if used
1327
1429
 
1328
- if( this.explorer )
1430
+ if( this.useFileExplorer || this.skipTabs )
1329
1431
  {
1330
1432
  this._tabStorage[ name ] = {
1331
1433
  lines: lines,
@@ -1333,8 +1435,12 @@ class CodeEditor {
1333
1435
  };
1334
1436
 
1335
1437
  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 );
1438
+
1439
+ if( this.useFileExplorer )
1440
+ {
1441
+ this.addExplorerItem( { id: name, skipVisibility: true, icon: this._getFileIcon( name, ext ) } );
1442
+ this.explorer.innerTree.frefresh( name );
1443
+ }
1338
1444
  }
1339
1445
  else
1340
1446
  {
@@ -1350,17 +1456,19 @@ class CodeEditor {
1350
1456
 
1351
1457
  if( options.callback )
1352
1458
  {
1353
- options.callback( text );
1459
+ options.callback( name, text );
1354
1460
  }
1355
1461
  };
1356
1462
 
1357
1463
  if( file.constructor == String )
1358
1464
  {
1359
1465
  let filename = file;
1466
+
1360
1467
  LX.request({ url: filename, success: text => {
1361
1468
  const name = filename.substring(filename.lastIndexOf( '/' ) + 1);
1362
- inner_add_tab( text, name, filename );
1469
+ _innerAddTab( text, name, filename );
1363
1470
  } });
1471
+
1364
1472
  }
1365
1473
  else // File Blob
1366
1474
  {
@@ -1368,86 +1476,11 @@ class CodeEditor {
1368
1476
  fr.readAsText( file );
1369
1477
  fr.onload = e => {
1370
1478
  const text = e.currentTarget.result;
1371
- inner_add_tab( text, file.name );
1479
+ _innerAddTab( text, file.name );
1372
1480
  };
1373
1481
  }
1374
1482
  }
1375
1483
 
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
1484
  _addUndoStep( cursor, force, deleteRedo = true ) {
1452
1485
 
1453
1486
  // Only the mainc cursor stores undo steps
@@ -1459,12 +1492,18 @@ class CodeEditor {
1459
1492
 
1460
1493
  if( !force )
1461
1494
  {
1462
- if( !this._lastTime ) {
1495
+ if( !this._lastTime )
1496
+ {
1463
1497
  this._lastTime = current;
1464
- } else {
1465
- if( ( current - this._lastTime ) > 2000 ){
1498
+ }
1499
+ else
1500
+ {
1501
+ if( ( current - this._lastTime ) > 2000 )
1502
+ {
1466
1503
  this._lastTime = null;
1467
- } else {
1504
+ }
1505
+ else
1506
+ {
1468
1507
  // If time not enough, reset timer
1469
1508
  this._lastTime = current;
1470
1509
  return;
@@ -1518,7 +1557,9 @@ class CodeEditor {
1518
1557
 
1519
1558
  // Only the mainc cursor stores redo steps
1520
1559
  if( !cursor.isMain )
1560
+ {
1521
1561
  return;
1562
+ }
1522
1563
 
1523
1564
  this.code.redoSteps.push( {
1524
1565
  lines: LX.deepCopy( this.code.lines ),
@@ -1548,7 +1589,9 @@ class CodeEditor {
1548
1589
 
1549
1590
  // Generate new if needed
1550
1591
  if( !currentCursor )
1592
+ {
1551
1593
  currentCursor = this._addCursor();
1594
+ }
1552
1595
 
1553
1596
  this.restoreCursor( currentCursor, step.cursors[ i ] );
1554
1597
  }
@@ -1565,12 +1608,14 @@ class CodeEditor {
1565
1608
  }
1566
1609
 
1567
1610
  this._updateDataInfoPanel( "@highlight", lang );
1568
- this.processLines();
1611
+
1612
+ this.mustProcessLines = true;
1569
1613
 
1570
1614
  const ext = langExtension ?? CodeEditor.languages[ lang ].ext;
1571
1615
  const icon = this._getFileIcon( null, ext );
1572
1616
 
1573
1617
  // Update tab icon
1618
+ if( !this.skipTabs )
1574
1619
  {
1575
1620
  const tab = this.tabs.tabDOMs[ this.code.tabName ];
1576
1621
  tab.firstChild.remove();
@@ -1589,7 +1634,7 @@ class CodeEditor {
1589
1634
  }
1590
1635
 
1591
1636
  // Update explorer icon
1592
- if( this.explorer )
1637
+ if( this.useFileExplorer )
1593
1638
  {
1594
1639
  const item = this.explorer.innerTree.data.children.filter( (v) => v.id === this.code.tabName )[ 0 ];
1595
1640
  console.assert( item != undefined );
@@ -1629,146 +1674,159 @@ class CodeEditor {
1629
1674
  this._changeLanguage( 'Plain Text' );
1630
1675
  }
1631
1676
 
1632
- _createStatusPanel() {
1677
+ _createStatusPanel( options ) {
1633
1678
 
1634
- if( !this.skipInfo )
1679
+ if( this.skipInfo )
1635
1680
  {
1636
- let panel = new LX.Panel({ className: "lexcodetabinfo flex flex-row", height: "auto" });
1681
+ return;
1682
+ }
1637
1683
 
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
- };
1684
+ let panel = new LX.Panel({ className: "lexcodetabinfo flex flex-row", height: "auto" });
1681
1685
 
1682
- panel.root.addEventListener( "contextmenu", (e) => {
1686
+ let leftStatusPanel = new LX.Panel( { id: "FontSizeZoomStatusComponent", height: "auto" } );
1687
+ leftStatusPanel.sameLine();
1688
+
1689
+ if( this.skipTabs )
1690
+ {
1691
+ leftStatusPanel.addButton( null, "ZoomOutButton", this._decreaseFontSize.bind( this ), { icon: "ZoomOut", width: "32px", title: "Zoom Out", tooltip: true } );
1692
+ }
1683
1693
 
1684
- if( e.target && ( e.target.classList.contains( "lexpanel" ) || e.target.classList.contains( "lexinlinecomponents" ) ) )
1694
+ leftStatusPanel.addButton( null, "ZoomOutButton", this._decreaseFontSize.bind( this ), { icon: "ZoomOut", width: "32px", title: "Zoom Out", tooltip: true } );
1695
+ leftStatusPanel.addLabel( this.fontSize ?? 14, { fit: true, signal: "@font-size" });
1696
+ leftStatusPanel.addButton( null, "ZoomInButton", this._increaseFontSize.bind( this ), { icon: "ZoomIn", width: "32px", title: "Zoom In", tooltip: true } );
1697
+ leftStatusPanel.endLine( "justify-start" );
1698
+ panel.attach( leftStatusPanel.root );
1699
+
1700
+ let rightStatusPanel = new LX.Panel( { height: "auto" } );
1701
+ rightStatusPanel.sameLine();
1702
+ rightStatusPanel.addLabel( this.code?.title ?? "", { id: "EditorFilenameStatusComponent", fit: true, signal: "@tab-name" });
1703
+ rightStatusPanel.addButton( null, "Ln 1, Col 1", this.showSearchLineBox.bind( this ), { id: "EditorSelectionStatusComponent", fit: true, signal: "@cursor-data" });
1704
+ rightStatusPanel.addButton( null, "Spaces: " + this.tabSpaces, ( value, event ) => {
1705
+ LX.addContextMenu( "Spaces", event, m => {
1706
+ const options = [ 2, 4, 8 ];
1707
+ for( const n of options )
1708
+ m.add( n, (v) => {
1709
+ this.tabSpaces = v;
1710
+ this.processLines();
1711
+ this._updateDataInfoPanel( "@tab-spaces", "Spaces: " + this.tabSpaces );
1712
+ } );
1713
+ });
1714
+ }, { id: "EditorIndentationStatusComponent", nameWidth: "15%", signal: "@tab-spaces" });
1715
+ rightStatusPanel.addButton( "<b>{ }</b>", this.highlight, ( value, event ) => {
1716
+ LX.addContextMenu( "Language", event, m => {
1717
+ for( const lang of Object.keys( CodeEditor.languages ) )
1685
1718
  {
1686
- return;
1719
+ m.add( lang, v => {
1720
+ this._changeLanguage( v, null, true )
1721
+ } );
1687
1722
  }
1723
+ });
1724
+ }, { id: "EditorLanguageStatusComponent", nameWidth: "15%", signal: "@highlight" });
1725
+ rightStatusPanel.endLine( "justify-end" );
1726
+ panel.attach( rightStatusPanel.root );
1727
+
1728
+ const itemVisibilityMap = {
1729
+ "Font Size Zoom": options.statusShowFontSizeZoom ?? true,
1730
+ "Editor Filename": options.statusShowEditorFilename ?? true,
1731
+ "Editor Selection": options.statusShowEditorSelection ?? true,
1732
+ "Editor Indentation": options.statusShowEditorIndentation ?? true,
1733
+ "Editor Language": options.statusShowEditorLanguage ?? true,
1734
+ };
1688
1735
 
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
- } );
1705
-
1706
- return panel;
1736
+ const _setVisibility = ( itemName ) => {
1737
+ const b = panel.root.querySelector( `#${ itemName.replaceAll( " ", "" ) }StatusComponent` );
1738
+ console.assert( b, `${ itemName } has no status button!` );
1739
+ b.classList.toggle( "hidden", !itemVisibilityMap[ itemName ] );
1707
1740
  }
1708
- else
1741
+
1742
+ for( const [ itemName, v ] of Object.entries( itemVisibilityMap ) )
1709
1743
  {
1710
- LX.doAsync( () => {
1744
+ _setVisibility( itemName );
1745
+ }
1711
1746
 
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' );
1747
+ panel.root.addEventListener( "contextmenu", (e) => {
1718
1748
 
1719
- }, 100 );
1720
- }
1749
+ if( e.target && ( e.target.classList.contains( "lexpanel" ) || e.target.classList.contains( "lexinlinecomponents" ) ) )
1750
+ {
1751
+ return;
1752
+ }
1753
+
1754
+ const menuOptions = Object.keys( itemVisibilityMap ).map( ( itemName, idx ) => {
1755
+ const item = {
1756
+ name: itemName,
1757
+ icon: "Check",
1758
+ callback: () => {
1759
+ itemVisibilityMap[ itemName ] = !itemVisibilityMap[ itemName ];
1760
+ _setVisibility( itemName );
1761
+ }
1762
+ }
1763
+ if( !itemVisibilityMap[ itemName ] ) delete item.icon;
1764
+ return item;
1765
+ } );
1766
+ new LX.DropdownMenu( e.target, menuOptions, { side: "top", align: "start" });
1767
+ } );
1768
+
1769
+ return panel;
1721
1770
  }
1722
1771
 
1723
- _getFileIcon( name, extension ) {
1772
+ _getFileIcon( name, extension, lang ) {
1724
1773
 
1725
1774
  const isNewTabButton = name ? ( name === '+' ) : false;
1726
-
1727
- if( !extension )
1775
+ if( isNewTabButton )
1728
1776
  {
1729
- extension = LX.getExtension( name );
1777
+ return;
1730
1778
  }
1731
- else
1732
- {
1733
- const possibleExtensions = [].concat( extension );
1734
1779
 
1735
- if( name )
1780
+ if( !lang )
1781
+ {
1782
+ if( !extension )
1783
+ {
1784
+ extension = LX.getExtension( name );
1785
+ }
1786
+ else
1736
1787
  {
1737
- const fileExtension = LX.getExtension( name );
1738
- const idx = possibleExtensions.indexOf( fileExtension );
1788
+ const possibleExtensions = [].concat( extension );
1789
+
1790
+ if( name )
1791
+ {
1792
+ const fileExtension = LX.getExtension( name );
1793
+ const idx = possibleExtensions.indexOf( fileExtension );
1739
1794
 
1740
- if( idx > -1)
1795
+ if( idx > -1)
1796
+ {
1797
+ extension = possibleExtensions[ idx ];
1798
+ }
1799
+ }
1800
+ else
1741
1801
  {
1742
- extension = possibleExtensions[ idx ];
1802
+ extension = possibleExtensions[ 0 ];
1743
1803
  }
1744
1804
  }
1745
- else
1805
+
1806
+ for( const [ l, lData ] of Object.entries( CodeEditor.languages ) )
1746
1807
  {
1747
- extension = possibleExtensions[ 0 ];
1808
+ const extensions = [].concat( lData.ext );
1809
+ if( extensions.includes( extension ) )
1810
+ {
1811
+ lang = l;
1812
+ break;
1813
+ }
1748
1814
  }
1815
+
1816
+ }
1817
+
1818
+ const iconPlusClasses = CodeEditor.languages[ lang ]?.icon;
1819
+ if( iconPlusClasses )
1820
+ {
1821
+ return iconPlusClasses[ extension ] ?? iconPlusClasses;
1749
1822
  }
1750
1823
 
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;
1824
+ return "AlignLeft gray";
1767
1825
  }
1768
1826
 
1769
1827
  _onNewTab( e ) {
1770
1828
 
1771
- this.processFocus(false);
1829
+ this.processFocus( false );
1772
1830
 
1773
1831
  LX.addContextMenu( null, e, m => {
1774
1832
  m.add( "Create", this.addTab.bind( this, "unnamed.js", true, "", { language: "JavaScript" } ) );
@@ -1791,7 +1849,7 @@ class CodeEditor {
1791
1849
 
1792
1850
  this._removeSecondaryCursors();
1793
1851
 
1794
- var cursor = this._getCurrentCursor( true );
1852
+ var cursor = this.getCurrentCursor( true );
1795
1853
  this.saveCursor( cursor, this.code.cursorState );
1796
1854
  this.code = this.loadedTabs[ name ];
1797
1855
  this.restoreCursor( cursor, this.code.cursorState );
@@ -1815,12 +1873,12 @@ class CodeEditor {
1815
1873
  _onContextMenuTab( isNewTabButton, event, name, ) {
1816
1874
 
1817
1875
  if( isNewTabButton )
1818
- return;
1876
+ return;
1819
1877
 
1820
1878
  LX.addContextMenu( null, event, m => {
1821
1879
  m.add( "Close", () => { this.tabs.delete( name ) } );
1822
- m.add( "" );
1823
- m.add( "Rename", () => { console.warn( "TODO" )} );
1880
+ // m.add( "" );
1881
+ // m.add( "Rename", () => { console.warn( "TODO" )} );
1824
1882
  });
1825
1883
  }
1826
1884
 
@@ -1841,17 +1899,24 @@ class CodeEditor {
1841
1899
 
1842
1900
  // Create code content
1843
1901
  let code = document.createElement( 'div' );
1844
- code.className = 'code';
1845
- code.lines = [ "" ];
1846
- code.language = options.language ?? "Plain Text";
1847
- code.cursorState = {};
1848
- code.undoSteps = [];
1849
- code.redoSteps = [];
1850
- code.tabName = name;
1851
- code.title = title ?? name;
1852
- code.tokens = {};
1853
- code.style.left = "0px";
1854
- code.style.top = "0px";
1902
+ Object.assign( code, {
1903
+ className: 'code',
1904
+ lines: [ "" ],
1905
+ language: options.language ?? "Plain Text",
1906
+ cursorState: {},
1907
+ undoSteps: [],
1908
+ redoSteps: [],
1909
+ lineScopes: [],
1910
+ lineSymbols: [],
1911
+ lineSignatures: [],
1912
+ symbolsTable: new Map(),
1913
+ tabName: name,
1914
+ title: title ?? name,
1915
+ tokens: {}
1916
+ } );
1917
+
1918
+ code.style.left = "0px",
1919
+ code.style.top = "0px",
1855
1920
 
1856
1921
  code.addEventListener( 'dragenter', function(e) {
1857
1922
  e.preventDefault();
@@ -1873,21 +1938,24 @@ class CodeEditor {
1873
1938
 
1874
1939
  const tabIcon = this._getFileIcon( name );
1875
1940
 
1876
- if( this.explorer && !isNewTabButton )
1941
+ if( this.useFileExplorer && !isNewTabButton )
1877
1942
  {
1878
1943
  this.addExplorerItem( { id: name, skipVisibility: true, icon: tabIcon } );
1879
1944
  this.explorer.innerTree.frefresh( name );
1880
1945
  }
1881
1946
 
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
- } );
1947
+ if( !this.skipTabs )
1948
+ {
1949
+ this.tabs.add( name, code, {
1950
+ selected: selected,
1951
+ fixed: isNewTabButton,
1952
+ title: code.title,
1953
+ icon: tabIcon,
1954
+ onSelect: this._onSelectTab.bind( this, isNewTabButton ),
1955
+ onContextMenu: this._onContextMenuTab.bind( this, isNewTabButton ),
1956
+ allowDelete: true
1957
+ } );
1958
+ }
1891
1959
 
1892
1960
  // Move into the sizer..
1893
1961
  this.codeSizer.appendChild( code );
@@ -1898,32 +1966,38 @@ class CodeEditor {
1898
1966
  {
1899
1967
  this.code = code;
1900
1968
  this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP );
1901
- this.processLines();
1969
+ this.mustProcessLines = true;
1902
1970
  }
1903
1971
 
1904
1972
  if( options.language )
1905
1973
  {
1906
1974
  code.languageOverride = options.language;
1907
1975
  this._changeLanguage( code.languageOverride );
1976
+ this.mustProcessLines = true;
1908
1977
  }
1909
1978
 
1979
+ this._processLinesIfNecessary();
1980
+
1910
1981
  this._updateDataInfoPanel( "@tab-name", name );
1911
1982
 
1912
1983
  // Bc it could be overrided..
1913
1984
  return name;
1914
1985
  }
1915
1986
 
1916
- loadTab( name ) {
1987
+ loadCode( name ) {
1988
+
1989
+ // Hide all others
1990
+ this.codeSizer.querySelectorAll( ".code" ).forEach( c => c.classList.add( "hidden" ) );
1917
1991
 
1918
1992
  // Already open...
1919
1993
  if( this.openedTabs[ name ] )
1920
1994
  {
1921
- this.tabs.select( name );
1995
+ let code = this.openedTabs[ name ]
1996
+ code.classList.remove( "hidden" );
1922
1997
  return;
1923
1998
  }
1924
1999
 
1925
2000
  let code = this.loadedTabs[ name ]
1926
-
1927
2001
  if( !code )
1928
2002
  {
1929
2003
  this.addTab( name, true );
@@ -1946,24 +2020,13 @@ class CodeEditor {
1946
2020
  delete this._tabStorage[ name ];
1947
2021
  }
1948
2022
 
2023
+ this._processLinesIfNecessary();
2024
+
1949
2025
  return;
1950
2026
  }
1951
2027
 
1952
2028
  this.openedTabs[ name ] = code;
1953
2029
 
1954
- const isNewTabButton = ( name === '+' );
1955
- const tabIcon = this._getFileIcon( name );
1956
-
1957
- this.tabs.add(name, code, {
1958
- selected: true,
1959
- fixed: isNewTabButton,
1960
- title: code.title,
1961
- icon: tabIcon,
1962
- onSelect: this._onSelectTab.bind( this, isNewTabButton ),
1963
- onContextMenu: this._onContextMenuTab.bind( this, isNewTabButton ),
1964
- allowDelete: true
1965
- });
1966
-
1967
2030
  // Move into the sizer..
1968
2031
  this.codeSizer.appendChild( code );
1969
2032
 
@@ -1971,17 +2034,86 @@ class CodeEditor {
1971
2034
 
1972
2035
  // Select as current...
1973
2036
  this.code = code;
2037
+ this.mustProcessLines = true;
2038
+
1974
2039
  this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP );
1975
2040
  this.processLines();
1976
2041
  this._changeLanguageFromExtension( LX.getExtension( name ) );
2042
+ this._processLinesIfNecessary();
1977
2043
  this._updateDataInfoPanel( "@tab-name", code.tabName );
1978
2044
  }
1979
2045
 
1980
- closeTab( name, eraseAll ) {
1981
-
1982
- this.tabs.delete( name );
2046
+ loadTab( name ) {
1983
2047
 
1984
- if( eraseAll )
2048
+ // Already open...
2049
+ if( this.openedTabs[ name ] )
2050
+ {
2051
+ this.tabs.select( name );
2052
+ return;
2053
+ }
2054
+
2055
+ let code = this.loadedTabs[ name ]
2056
+
2057
+ if( !code )
2058
+ {
2059
+ this.addTab( name, true );
2060
+
2061
+ // Unload lines from storage...
2062
+ const tabData = this._tabStorage[ name ];
2063
+ if( tabData )
2064
+ {
2065
+ this.code.lines = tabData.lines;
2066
+
2067
+ if( tabData.options.language )
2068
+ {
2069
+ this._changeLanguage( tabData.options.language, null, true );
2070
+ }
2071
+ else
2072
+ {
2073
+ this._changeLanguageFromExtension( LX.getExtension( name ) );
2074
+ }
2075
+
2076
+ delete this._tabStorage[ name ];
2077
+ }
2078
+
2079
+ this._processLinesIfNecessary();
2080
+
2081
+ return;
2082
+ }
2083
+
2084
+ this.openedTabs[ name ] = code;
2085
+
2086
+ const isNewTabButton = ( name === '+' );
2087
+ const tabIcon = this._getFileIcon( name );
2088
+
2089
+ this.tabs.add(name, code, {
2090
+ selected: true,
2091
+ fixed: isNewTabButton,
2092
+ title: code.title,
2093
+ icon: tabIcon,
2094
+ onSelect: this._onSelectTab.bind( this, isNewTabButton ),
2095
+ onContextMenu: this._onContextMenuTab.bind( this, isNewTabButton ),
2096
+ allowDelete: true
2097
+ });
2098
+
2099
+ // Move into the sizer..
2100
+ this.codeSizer.appendChild( code );
2101
+
2102
+ this.endSelection();
2103
+
2104
+ // Select as current...
2105
+ this.code = code;
2106
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP );
2107
+ this.processLines();
2108
+ this._changeLanguageFromExtension( LX.getExtension( name ) );
2109
+ this._updateDataInfoPanel( "@tab-name", code.tabName );
2110
+ }
2111
+
2112
+ closeTab( name, eraseAll ) {
2113
+
2114
+ this.tabs.delete( name );
2115
+
2116
+ if( eraseAll )
1985
2117
  {
1986
2118
  delete this.openedTabs[ name ];
1987
2119
  delete this.loadedTabs[ name ];
@@ -1994,12 +2126,14 @@ class CodeEditor {
1994
2126
  }
1995
2127
 
1996
2128
  loadTabFromFile() {
2129
+
1997
2130
  const input = document.createElement( 'input' );
1998
2131
  input.type = 'file';
1999
2132
  document.body.appendChild( input );
2000
2133
  input.click();
2001
2134
  input.addEventListener('change', e => {
2002
- if (e.target.files[ 0 ]) {
2135
+ if (e.target.files[ 0 ])
2136
+ {
2003
2137
  this.loadFile( e.target.files[ 0 ] );
2004
2138
  }
2005
2139
  input.remove();
@@ -2009,8 +2143,11 @@ class CodeEditor {
2009
2143
  processFocus( active ) {
2010
2144
 
2011
2145
  if( active )
2146
+ {
2012
2147
  this.restartBlink();
2013
- else {
2148
+ }
2149
+ else
2150
+ {
2014
2151
  clearInterval( this.blinker );
2015
2152
  this.cursors.classList.remove( 'show' );
2016
2153
  }
@@ -2018,10 +2155,10 @@ class CodeEditor {
2018
2155
 
2019
2156
  processMouse( e ) {
2020
2157
 
2021
- if( !e.target.classList.contains('code') && !e.target.classList.contains('codetabsarea') ) return;
2158
+ if( !e.target.classList.contains('code') && !e.target.classList.contains('lexcodearea') ) return;
2022
2159
  if( !this.code ) return;
2023
2160
 
2024
- var cursor = this._getCurrentCursor();
2161
+ var cursor = this.getCurrentCursor();
2025
2162
  var code_rect = this.code.getBoundingClientRect();
2026
2163
  var mouse_pos = [(e.clientX - code_rect.x), (e.clientY - code_rect.y)];
2027
2164
 
@@ -2116,14 +2253,17 @@ class CodeEditor {
2116
2253
 
2117
2254
  _onMouseUp( e ) {
2118
2255
 
2119
- if( (LX.getTime() - this.lastMouseDown) < 120 ) {
2256
+ if( ( LX.getTime() - this.lastMouseDown ) < 120 )
2257
+ {
2120
2258
  this.state.selectingText = false;
2121
2259
  this.endSelection();
2122
2260
  }
2123
2261
 
2124
- const cursor = this._getCurrentCursor();
2262
+ const cursor = this.getCurrentCursor();
2125
2263
  if( cursor.selection )
2264
+ {
2126
2265
  cursor.selection.invertIfNecessary();
2266
+ }
2127
2267
 
2128
2268
  this.state.selectingText = false;
2129
2269
  delete this._lastSelectionKeyDir;
@@ -2131,7 +2271,7 @@ class CodeEditor {
2131
2271
 
2132
2272
  processClick( e ) {
2133
2273
 
2134
- var cursor = this._getCurrentCursor();
2274
+ var cursor = this.getCurrentCursor();
2135
2275
  var code_rect = this.codeScroller.getBoundingClientRect();
2136
2276
  var position = [( e.clientX - code_rect.x ) + this.getScrollLeft(), (e.clientY - code_rect.y) + this.getScrollTop()];
2137
2277
  var ln = (position[ 1 ] / this.lineHeight)|0;
@@ -2148,7 +2288,6 @@ class CodeEditor {
2148
2288
  {
2149
2289
  // Make sure we only keep the main cursor..
2150
2290
  this._removeSecondaryCursors();
2151
-
2152
2291
  this.cursorToLine( cursor, ln, true );
2153
2292
  this.cursorToPosition( cursor, string.length );
2154
2293
  }
@@ -2162,38 +2301,45 @@ class CodeEditor {
2162
2301
  this.hideAutoCompleteBox();
2163
2302
  }
2164
2303
 
2165
- updateSelections( e, keep_range, flags = CodeEditor.SELECTION_X_Y ) {
2304
+ updateSelections( e, keepRange, flags = CodeEditor.SELECTION_X_Y ) {
2166
2305
 
2167
2306
  for( let cursor of this.cursors.children )
2168
2307
  {
2169
2308
  if( !cursor.selection )
2309
+ {
2170
2310
  continue;
2311
+ }
2171
2312
 
2172
- this._processSelection( cursor, e, keep_range, flags );
2313
+ this._processSelection( cursor, e, keepRange, flags );
2173
2314
  }
2174
2315
  }
2175
2316
 
2176
- processSelections( e, keep_range, flags = CodeEditor.SELECTION_X_Y ) {
2317
+ processSelections( e, keepRange, flags = CodeEditor.SELECTION_X_Y ) {
2177
2318
 
2178
2319
  for( let cursor of this.cursors.children )
2179
2320
  {
2180
- this._processSelection( cursor, e, keep_range, flags );
2321
+ this._processSelection( cursor, e, keepRange, flags );
2181
2322
  }
2182
2323
  }
2183
2324
 
2184
- _processSelection( cursor, e, keep_range, flags = CodeEditor.SELECTION_X_Y ) {
2325
+ _processSelection( cursor, e, keepRange, flags = CodeEditor.SELECTION_X_Y ) {
2185
2326
 
2186
2327
  const isMouseEvent = e && ( e.constructor == MouseEvent );
2187
2328
 
2188
- if( isMouseEvent ) this.processClick( e );
2329
+ if( isMouseEvent )
2330
+ {
2331
+ this.processClick( e );
2332
+ }
2189
2333
 
2190
2334
  if( !cursor.selection )
2335
+ {
2191
2336
  this.startSelection( cursor );
2337
+ }
2192
2338
 
2193
2339
  this._hideActiveLine();
2194
2340
 
2195
2341
  // Update selection
2196
- if( !keep_range )
2342
+ if( !keepRange )
2197
2343
  {
2198
2344
  let ccw = true;
2199
2345
 
@@ -2260,19 +2406,19 @@ class CodeEditor {
2260
2406
  }
2261
2407
 
2262
2408
  // Compute new width and selection margins
2263
- let string;
2409
+ let string = "";
2264
2410
 
2265
- if(sId == 0) // First line 2 cases (single line, multiline)
2411
+ if( sId == 0 ) // First line 2 cases (single line, multiline)
2266
2412
  {
2267
2413
  const reverse = fromX > toX;
2268
2414
  if(deltaY == 0) string = !reverse ? this.code.lines[ i ].substring( fromX, toX ) : this.code.lines[ i ].substring(toX, fromX);
2269
2415
  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 + ")";
2416
+ const pixels = ( reverse && deltaY == 0 ? toX : fromX ) * this.charWidth;
2417
+ if( isVisible ) domEl.style.left = `calc(${ pixels }px + ${ this.xPadding })`;
2272
2418
  }
2273
2419
  else
2274
2420
  {
2275
- string = (i == toY) ? this.code.lines[ i ].substring( 0, toX ) : this.code.lines[ i ]; // Last line, any multiple line...
2421
+ string = ( i == toY ) ? this.code.lines[ i ].substring( 0, toX ) : this.code.lines[ i ]; // Last line, any multiple line...
2276
2422
  if( isVisible ) domEl.style.left = this.xPadding;
2277
2423
  }
2278
2424
 
@@ -2281,7 +2427,7 @@ class CodeEditor {
2281
2427
 
2282
2428
  if( isVisible )
2283
2429
  {
2284
- domEl.style.width = (stringWidth || 8) + "px";
2430
+ domEl.style.width = ( stringWidth || 8 ) + "px";
2285
2431
  domEl._top = i * this.lineHeight;
2286
2432
  domEl.style.top = domEl._top + "px";
2287
2433
  }
@@ -2302,7 +2448,7 @@ class CodeEditor {
2302
2448
  {
2303
2449
  // Make sure that the line selection is generated...
2304
2450
  domEl = cursorSelections.childNodes[ sId ];
2305
- if(!domEl)
2451
+ if( !domEl )
2306
2452
  {
2307
2453
  domEl = document.createElement( 'div' );
2308
2454
  domEl.className = "lexcodeselection";
@@ -2421,7 +2567,7 @@ class CodeEditor {
2421
2567
 
2422
2568
  _processGlobalKeys( e, key ) {
2423
2569
 
2424
- let cursor = this._getCurrentCursor();
2570
+ let cursor = this.getCurrentCursor();
2425
2571
 
2426
2572
  if( e.ctrlKey || e.metaKey )
2427
2573
  {
@@ -2489,7 +2635,7 @@ class CodeEditor {
2489
2635
 
2490
2636
  async _processKeyAtCursor( e, key, cursor ) {
2491
2637
 
2492
- const skip_undo = e.detail.skip_undo ?? false;
2638
+ const skipUndo = e.detail.skipUndo ?? false;
2493
2639
 
2494
2640
  // keys with length > 1 are probably special keys
2495
2641
  if( key.length > 1 && this.specialKeys.indexOf( key ) == -1 )
@@ -2579,7 +2725,7 @@ class CodeEditor {
2579
2725
 
2580
2726
  // Add undo steps
2581
2727
 
2582
- if( !skip_undo && this.code.lines.length )
2728
+ if( !skipUndo && this.code.lines.length )
2583
2729
  {
2584
2730
  this._addUndoStep( cursor );
2585
2731
  }
@@ -2640,17 +2786,7 @@ class CodeEditor {
2640
2786
  this.processLine( lidx );
2641
2787
 
2642
2788
  // 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
- }
2789
+ this.resizeIfNecessary( cursor );
2654
2790
 
2655
2791
  // Manage autocomplete
2656
2792
 
@@ -2662,6 +2798,8 @@ class CodeEditor {
2662
2798
 
2663
2799
  async _pasteContent( cursor ) {
2664
2800
 
2801
+ const mustDetectLanguage = ( !this.getText().length );
2802
+
2665
2803
  let text = await navigator.clipboard.readText();
2666
2804
 
2667
2805
  // Remove any possible tabs (\t) and add spaces
@@ -2674,9 +2812,19 @@ class CodeEditor {
2674
2812
  const currentScroll = this.getScrollTop();
2675
2813
  const scroll = Math.max( cursor.line * this.lineHeight - this.codeScroller.offsetWidth, 0 );
2676
2814
 
2677
- if( currentScroll < scroll ) {
2815
+ if( currentScroll < scroll )
2816
+ {
2678
2817
  this.codeScroller.scrollTo( 0, cursor.line * this.lineHeight );
2679
2818
  }
2819
+
2820
+ if( mustDetectLanguage )
2821
+ {
2822
+ const detectedLang = this._detectLanguage( text );
2823
+ if( detectedLang )
2824
+ {
2825
+ this._changeLanguage( detectedLang );
2826
+ }
2827
+ }
2680
2828
  }
2681
2829
 
2682
2830
  async _copyContent( cursor ) {
@@ -2699,7 +2847,9 @@ class CodeEditor {
2699
2847
  let index = 0;
2700
2848
 
2701
2849
  for( let i = 0; i <= cursor.selection.fromY; i++ )
2850
+ {
2702
2851
  index += ( i == cursor.selection.fromY ? cursor.selection.fromX : this.code.lines[ i ].length );
2852
+ }
2703
2853
 
2704
2854
  index += cursor.selection.fromY * separator.length;
2705
2855
  const num_chars = cursor.selection.chars + ( cursor.selection.toY - cursor.selection.fromY ) * separator.length;
@@ -2775,7 +2925,7 @@ class CodeEditor {
2775
2925
 
2776
2926
  if( cursor.selection )
2777
2927
  {
2778
- var cursor = this._getCurrentCursor();
2928
+ var cursor = this.getCurrentCursor();
2779
2929
  this._addUndoStep( cursor, true );
2780
2930
 
2781
2931
  const selectedLines = this.code.lines.slice( cursor.selection.fromY, cursor.selection.toY );
@@ -2834,7 +2984,7 @@ class CodeEditor {
2834
2984
 
2835
2985
  if( cursor.selection )
2836
2986
  {
2837
- var cursor = this._getCurrentCursor();
2987
+ var cursor = this.getCurrentCursor();
2838
2988
  this._addUndoStep( cursor, true );
2839
2989
 
2840
2990
  for( var i = cursor.selection.fromY; i <= cursor.selection.toY; ++i )
@@ -2888,7 +3038,6 @@ class CodeEditor {
2888
3038
  }
2889
3039
 
2890
3040
  _actionMustDelete( cursor, action, e ) {
2891
-
2892
3041
  return cursor.selection && action.deleteSelection &&
2893
3042
  ( action.eventSkipDelete ? !e[ action.eventSkipDelete ] : true );
2894
3043
  }
@@ -2906,20 +3055,32 @@ class CodeEditor {
2906
3055
  }
2907
3056
 
2908
3057
  toLocalLine( line ) {
2909
-
2910
3058
  const d = Math.max( this.firstLineInViewport - this.lineScrollMargin.x, 0 );
2911
3059
  return Math.min( Math.max( line - d, 0 ), this.code.lines.length - 1 );
2912
3060
  }
2913
3061
 
2914
3062
  getMaxLineLength() {
2915
-
2916
3063
  return Math.max(...this.code.lines.map( v => v.length ));
2917
3064
  }
2918
3065
 
3066
+ _processLinesIfNecessary() {
3067
+ if( this.mustProcessLines )
3068
+ {
3069
+ this.mustProcessLines = false;
3070
+ this.processLines();
3071
+ }
3072
+ }
3073
+
2919
3074
  processLines( mode ) {
2920
3075
 
3076
+ if( !this.code )
3077
+ {
3078
+ return;
3079
+ }
3080
+
2921
3081
  var htmlCode = "";
2922
3082
  this._blockCommentCache.length = 0;
3083
+ this.mustProcessLines = false;
2923
3084
 
2924
3085
  // Reset all lines content
2925
3086
  this.code.innerHTML = "";
@@ -2928,7 +3089,7 @@ class CodeEditor {
2928
3089
  const lastScrollTop = this.getScrollTop();
2929
3090
  this.firstLineInViewport = ( mode ?? CodeEditor.KEEP_VISIBLE_LINES ) & CodeEditor.UPDATE_VISIBLE_LINES ?
2930
3091
  ( (lastScrollTop / this.lineHeight)|0 ) : this.firstLineInViewport;
2931
- const totalLinesInViewport = ((this.codeScroller.offsetHeight - 36) / this.lineHeight)|0;
3092
+ const totalLinesInViewport = ( ( this.codeScroller.offsetHeight ) / this.lineHeight )|0;
2932
3093
  this.visibleLinesViewport = new LX.vec2(
2933
3094
  Math.max( this.firstLineInViewport - this.lineScrollMargin.x, 0 ),
2934
3095
  Math.min( this.firstLineInViewport + totalLinesInViewport + this.lineScrollMargin.y, this.code.lines.length )
@@ -2943,7 +3104,7 @@ class CodeEditor {
2943
3104
  }
2944
3105
  }
2945
3106
 
2946
- this._scopeStack = [];
3107
+ this._scopeStack = [ { name: "", type: "global" } ];
2947
3108
 
2948
3109
  // Process visible lines
2949
3110
  for( let i = this.visibleLinesViewport.x; i < this.visibleLinesViewport.y; ++i )
@@ -2965,162 +3126,617 @@ class CodeEditor {
2965
3126
  this.resize();
2966
3127
  }
2967
3128
 
2968
- processLine( lineNumber, force ) {
3129
+ processLine( lineNumber, force, skipPropagation ) {
3130
+
3131
+ if( this._scopeStack )
3132
+ {
3133
+ this.code.lineScopes[ lineNumber ] = [ ...this._scopeStack ];
3134
+ }
3135
+ else
3136
+ {
3137
+ this.code.lineScopes[ lineNumber ] = this.code.lineScopes[ lineNumber ] ?? [];
3138
+ this._scopeStack = [ ...this.code.lineScopes[ lineNumber ] ];
3139
+ }
2969
3140
 
2970
- // Check if we are in block comment sections..
2971
- if( !force && this._inBlockCommentSection( lineNumber ) )
3141
+ const lang = CodeEditor.languages[ this.highlight ];
3142
+ const localLineNum = this.toLocalLine( lineNumber );
3143
+ const lineString = this.code.lines[ lineNumber ];
3144
+ if( lineString === undefined )
2972
3145
  {
2973
- this.processLines();
2974
3146
  return;
2975
3147
  }
2976
3148
 
2977
- if( this._scopeStack )
3149
+ this._lastProcessedLine = lineNumber;
3150
+
3151
+ // multi-line strings not supported by now
3152
+ delete this._buildingString;
3153
+ delete this._pendingString;
3154
+ delete this._markdownHeader;
3155
+
3156
+ // Single line
3157
+ if( !force )
2978
3158
  {
2979
- this.lineScopes[ lineNumber ] = [ ...this._scopeStack ];
3159
+ LX.deleteElement( this.code.childNodes[ localLineNum ] );
3160
+ this.code.insertChildAtIndex( document.createElement( 'pre' ), localLineNum );
2980
3161
  }
2981
- else
3162
+
3163
+ // Early out check for no highlighting languages
3164
+ if( this.highlight == 'Plain Text' )
2982
3165
  {
2983
- this._scopeStack = [ ...this.lineScopes[ lineNumber ] ];
3166
+ const plainTextHtml = lineString.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
3167
+ return this._updateLine( force, lineNumber, plainTextHtml, skipPropagation );
3168
+ }
3169
+
3170
+ this._currentLineNumber = lineNumber;
3171
+ this._currentLineString = lineString;
3172
+
3173
+ const tokensToEvaluate = this._getTokensFromLine( lineString );
3174
+ if( !tokensToEvaluate.length )
3175
+ {
3176
+ return this._updateLine( force, lineNumber, "", skipPropagation );
3177
+ }
3178
+
3179
+ let lineInnerHtml = "";
3180
+ let pushedScope = false;
3181
+
3182
+ const newSignature = this._getLineSignatureFromTokens( tokensToEvaluate );
3183
+ const cachedSignature = this.code.lineSignatures[ lineNumber ];
3184
+ const mustUpdateScopes = ( cachedSignature !== newSignature ) && !force;
3185
+ const blockComments = lang.blockComments ?? true;
3186
+ const blockCommentsTokens = lang.blockCommentsTokens ?? this.defaultBlockCommentTokens;
3187
+
3188
+ // Reset scope stack if structural changes in current line
3189
+ if( mustUpdateScopes )
3190
+ {
3191
+ this._scopeStack = [ { name: "", type: "global" } ];
2984
3192
  }
2985
3193
 
3194
+ // Process all tokens
3195
+ for( let i = 0; i < tokensToEvaluate.length; ++i )
3196
+ {
3197
+ let it = i - 1;
3198
+ let prev = tokensToEvaluate[ it ];
3199
+ while( prev == ' ' )
3200
+ {
3201
+ it--;
3202
+ prev = tokensToEvaluate[ it ];
3203
+ }
3204
+
3205
+ it = i + 1;
3206
+ let next = tokensToEvaluate[ it ];
3207
+ while( next == ' ' || next == '"' )
3208
+ {
3209
+ it++;
3210
+ next = tokensToEvaluate[ it ];
3211
+ }
3212
+
3213
+ const token = tokensToEvaluate[ i ];
3214
+ const tokenIndex = i;
3215
+ const tokenStartIndex = this._currentTokenPositions[ tokenIndex ];;
3216
+
3217
+ if( blockComments )
3218
+ {
3219
+ if( token.substr( 0, blockCommentsTokens[ 0 ].length ) == blockCommentsTokens[ 0 ] )
3220
+ {
3221
+ this._buildingBlockComment = [ lineNumber, tokenStartIndex ];
3222
+ }
3223
+ }
3224
+
3225
+ // Compare line signature for structural changes
3226
+ // to pop current scope if necessary
3227
+ if( token === "}" && this._scopeStack.length > 1 )
3228
+ {
3229
+ this._scopeStack.pop();
3230
+ }
3231
+
3232
+ lineInnerHtml += this._evaluateToken( {
3233
+ token,
3234
+ prev,
3235
+ prevWithSpaces: tokensToEvaluate[ i - 1 ],
3236
+ next,
3237
+ nextWithSpaces: tokensToEvaluate[ i + 1 ],
3238
+ tokenIndex,
3239
+ isFirstToken: ( tokenIndex == 0 ),
3240
+ isLastToken: ( tokenIndex == tokensToEvaluate.length - 1 ),
3241
+ tokens: tokensToEvaluate
3242
+ } );
3243
+
3244
+ if( blockComments && this._buildingBlockComment != undefined
3245
+ && token.substr( 0, blockCommentsTokens[ 1 ].length ) == blockCommentsTokens[ 1 ] )
3246
+ {
3247
+ const [ commentLineNumber, tokenPos ] = this._buildingBlockComment;
3248
+ this._blockCommentCache.push( [ new LX.vec2( commentLineNumber, lineNumber ), new LX.vec2( tokenPos, tokenStartIndex ) ] );
3249
+ delete this._buildingBlockComment;
3250
+ }
3251
+
3252
+ if( token !== "{" )
3253
+ {
3254
+ continue;
3255
+ }
3256
+
3257
+ // Store current scopes
3258
+
3259
+ let contextTokens = [
3260
+ ...this._getTokensFromLine( this.code.lines[ lineNumber ].substring( 0, tokenStartIndex ) )
3261
+ ];
3262
+
3263
+ // Add token context from above lines in case we don't have information
3264
+ // in the same line to get the scope data
3265
+ if( !prev )
3266
+ {
3267
+ for( let k = 1; k < 50; k++ )
3268
+ {
3269
+ let kLineString = this.code.lines[ lineNumber - k ];
3270
+ if( !kLineString )
3271
+ {
3272
+ break;
3273
+ }
3274
+
3275
+ const openIdx = kLineString.lastIndexOf( '{' );
3276
+ const closeIdx = kLineString.lastIndexOf( '}' );
3277
+ if( openIdx > -1 )
3278
+ {
3279
+ kLineString = kLineString.substr( openIdx );
3280
+ }
3281
+ else if( closeIdx > -1 )
3282
+ {
3283
+ kLineString = kLineString.substr( closeIdx );
3284
+ }
3285
+
3286
+ contextTokens = [ ...this._getTokensFromLine( kLineString ), ...contextTokens ];
3287
+
3288
+ if( kLineString.length !== this.code.lines[ lineNumber - k ] )
3289
+ {
3290
+ break;
3291
+ }
3292
+ }
3293
+ }
3294
+
3295
+ contextTokens = contextTokens.reverse().filter( v => v.length && v != ' ' );
3296
+
3297
+ // Keywords that can open a *named* scope
3298
+ // TODO: Do this per language
3299
+ const scopeKeywords = ["class", "enum", "function", "interface", "type", "struct", "namespace"];
3300
+
3301
+ let scopeType = null; // This is the type of scope (function, class, enum, etc)
3302
+ let scopeName = null;
3303
+
3304
+ for( let i = 0; i < contextTokens.length; i++ )
3305
+ {
3306
+ const t = contextTokens[ i ];
3307
+
3308
+ if ( scopeKeywords.includes( t ) )
3309
+ {
3310
+ scopeType = t;
3311
+ scopeName = contextTokens[ i - 1 ]; // usually right before the keyword in reversed array
3312
+ break;
3313
+ }
3314
+ }
3315
+
3316
+ // Special case: enum type specification `enum Foo : int {`
3317
+ if( scopeType === "enum" && contextTokens.includes( ":" ) )
3318
+ {
3319
+ const colonIndex = contextTokens.indexOf( ":" );
3320
+ scopeName = contextTokens[ colonIndex + 1 ] || scopeName;
3321
+ }
3322
+
3323
+ if( !scopeType )
3324
+ {
3325
+ const parOpenIndex = contextTokens.indexOf( "(" );
3326
+ scopeName = contextTokens[ parOpenIndex + 1 ] || scopeName;
3327
+
3328
+ if( scopeName )
3329
+ {
3330
+ scopeType = "method";
3331
+ }
3332
+ }
3333
+
3334
+ // Only push if it's not already reflected in the cached scopes
3335
+ const lastScope = this._scopeStack.at( -1 );
3336
+ if( lastScope?.lineNumber !== lineNumber )
3337
+ {
3338
+ this._scopeStack.push( { name: scopeName ?? "", type: scopeType ?? "anonymous", lineNumber } );
3339
+ }
3340
+
3341
+ pushedScope = true;
3342
+ }
3343
+
3344
+ // Update scopes cache
3345
+ this.code.lineScopes[ lineNumber ] = [ ...this._scopeStack ];
3346
+
3347
+ const symbols = this._parseLineForSymbols( lineNumber, lineString, tokensToEvaluate, pushedScope );
3348
+
3349
+ return this._updateLine( force, lineNumber, lineInnerHtml, skipPropagation, symbols, tokensToEvaluate );
3350
+ }
3351
+
3352
+ _getLineSignatureFromTokens( tokens ) {
3353
+ const structuralChars = new Set( [ '{', '}'] );
3354
+ const sign = tokens.filter( t => structuralChars.has( t ) );
3355
+ return sign.join( "_" );
3356
+ }
3357
+
3358
+ _updateBlockComments( section, lineNumber, tokens ) {
3359
+
2986
3360
  const lang = CodeEditor.languages[ this.highlight ];
2987
- const localLineNum = this.toLocalLine( lineNumber );
3361
+ const blockCommentsTokens = lang.blockCommentsTokens ?? this.defaultBlockCommentTokens;
3362
+ const lineOpensBlock = ( section[ 0 ].x === lineNumber );
3363
+ const lineClosesBlock = ( section[ 0 ].y === lineNumber );
3364
+ const lineInsideBlock = ( section[ 0 ].x !== lineNumber ) && ( section[ 0 ].y !== lineNumber );
3365
+
3366
+ delete this._buildingBlockComment;
3367
+
3368
+ /*
3369
+ Check if delimiters have been removed and process lines backwards/forward
3370
+ until reaching new delimiters
3371
+ */
3372
+
3373
+ if( lineOpensBlock )
3374
+ {
3375
+ const r = tokens.filter( t => t.substr( 0, blockCommentsTokens[ 0 ].length ) == blockCommentsTokens[ 0 ] );
3376
+ if( !r.length )
3377
+ {
3378
+ this._buildingBlockComment = [ lineNumber - 1, 0 ];
3379
+
3380
+ this.mustProcessPreviousLine = ( tokens ) => {
3381
+ const idx = tokens.indexOf( blockCommentsTokens[ 0 ] );
3382
+ return ( idx === -1 );
3383
+ }
3384
+
3385
+ this.processLine( lineNumber - 1, false, true );
3386
+
3387
+ section[ 0 ].x = this._lastProcessedLine;
3388
+
3389
+ const lastProcessedString = this.code.lines[ this._lastProcessedLine ];
3390
+ const idx = lastProcessedString.indexOf( blockCommentsTokens[ 0 ] );
3391
+ section[ 1 ].x = idx > 0 ? idx : 0;
3392
+ }
3393
+ else
3394
+ {
3395
+ const tokenIndex = tokens.indexOf( blockCommentsTokens[ 0 ] );
3396
+ const tokenStartIndex = this._currentTokenPositions[ tokenIndex ];
3397
+ section[ 1 ].x = tokenStartIndex;
3398
+ console.log(tokenStartIndex)
3399
+ // Process current line to update new sections
3400
+ this.processLine( lineNumber, false, true );
3401
+ }
3402
+ }
3403
+ else if( lineClosesBlock )
3404
+ {
3405
+ const r = tokens.filter( t => t.substr( 0, blockCommentsTokens[ 1 ].length ) == blockCommentsTokens[ 1 ] );
3406
+ if( !r.length )
3407
+ {
3408
+ this._buildingBlockComment = [ section[ 0 ].x, section[ 1 ].x ];
3409
+
3410
+ this.mustProcessNextLine = ( tokens ) => {
3411
+ const idx = tokens.indexOf( blockCommentsTokens[ 1 ] );
3412
+ return ( idx === -1 );
3413
+ }
3414
+
3415
+ this.processLine( lineNumber + 1, false, true );
3416
+
3417
+ section[ 0 ].y = this._lastProcessedLine;
3418
+
3419
+ const lastProcessedString = this.code.lines[ this._lastProcessedLine ];
3420
+ const idx = lastProcessedString.indexOf( blockCommentsTokens[ 1 ] );
3421
+ section[ 1 ].y = idx > 0 ? idx : ( lastProcessedString.length - 1 );
3422
+ }
3423
+ else
3424
+ {
3425
+ const tokenIndex = tokens.indexOf( blockCommentsTokens[ 1 ] );
3426
+ const tokenStartIndex = this._currentTokenPositions[ tokenIndex ];
3427
+ section[ 1 ].y = tokenStartIndex;
3428
+ // Process current line to update new sections
3429
+ this.processLine( lineNumber, false, true );
3430
+ }
3431
+ }
3432
+ else if( lineInsideBlock )
3433
+ {
3434
+ // Here it can't modify delimiters..
3435
+ }
3436
+ }
3437
+
3438
+ _processExtraLineIfNecessary( lineNumber, tokens, oldSymbols, skipPropagation ) {
3439
+
3440
+ if( !this._scopeStack )
3441
+ {
3442
+ console.warn( "CodeEditor: No scope available" );
3443
+ return;
3444
+ }
3445
+
3446
+ // Update block comments if necessary
3447
+ {
3448
+ const commentBlockSection = this._inBlockCommentSection( lineNumber, 1e10, -1e10 );
3449
+ if( tokens && commentBlockSection !== undefined )
3450
+ {
3451
+ this._updateBlockComments( commentBlockSection, lineNumber, tokens );
3452
+
3453
+ // Get again correct scope
3454
+ this._scopeStack = [ ...this.code.lineScopes[ lineNumber ] ];
3455
+ }
3456
+ }
3457
+
3458
+ if( ( (lineNumber + 1) === this.code.lines.length ) || !this.code.lineScopes[ lineNumber + 1 ] )
3459
+ {
3460
+ return;
3461
+ }
3462
+
3463
+ const newSignature = this._getLineSignatureFromTokens( tokens );
3464
+ const cachedSignature = this.code.lineSignatures[ lineNumber ];
3465
+ const mustUpdateScopes = ( cachedSignature !== newSignature );
3466
+ const sameScopes = codeScopesEqual( this._scopeStack, this.code.lineScopes[ lineNumber + 1 ] );
3467
+
3468
+ // Only update scope stack if something changed when editing a single line
3469
+ // Compare line signature for structural changes
3470
+ if( ( mustUpdateScopes || this._scopesUpdated ) && ( !sameScopes && !skipPropagation ) )
3471
+ {
3472
+ if( mustUpdateScopes )
3473
+ {
3474
+ this._scopesUpdated = true;
3475
+ }
3476
+
3477
+ this.code.lineScopes[ lineNumber + 1 ] = [ ...this._scopeStack ];
3478
+ this.processLine( lineNumber + 1 );
3479
+
3480
+ delete this._scopesUpdated;
3481
+ }
3482
+ else if( sameScopes )
3483
+ {
3484
+ // In case of same scope, check for occurrencies of the old symbols, to reprocess that lines
3485
+ for( const sym of oldSymbols )
3486
+ {
3487
+ const tableSymbol = this.code.symbolsTable.get( sym.name );
3488
+ if( tableSymbol === undefined )
3489
+ {
3490
+ return;
3491
+ }
3492
+
3493
+ for( const occ of tableSymbol )
3494
+ {
3495
+ if( occ.line === lineNumber )
3496
+ {
3497
+ continue;
3498
+ }
3499
+
3500
+ this.processLine( occ.line, false, true );
3501
+ }
3502
+ }
3503
+ }
3504
+ }
3505
+
3506
+ _updateLine( force, lineNumber, html, skipPropagation, symbols = [], tokens = [] ) {
3507
+
2988
3508
  const gutterLineHtml = `<span class='line-gutter'>${ lineNumber + 1 }</span>`;
3509
+ const oldSymbols = this._updateLineSymbols( lineNumber, symbols );
3510
+ const lineScope = CodeEditor.debugScopes && this.code.lineScopes[ lineNumber ] ? this.code.lineScopes[ lineNumber ].map( s => `${ s.type }` ).join( ", " ) : "";
3511
+ const lineSymbols = CodeEditor.debugSymbols && this.code.lineSymbols[ lineNumber ] ? this.code.lineSymbols[ lineNumber ].map( s => `${ s.name }(${ s.kind })` ).join( ", " ) : "";
3512
+ const debugString = lineScope + ( lineScope.length ? " - " : "" ) + lineSymbols;
3513
+
3514
+ if( !force ) // Single line update
3515
+ {
3516
+ this.code.childNodes[ this.toLocalLine( lineNumber ) ].innerHTML = ( gutterLineHtml + html + debugString );
2989
3517
 
2990
- const _updateLine = ( html ) => {
2991
- if( !force ) // Single line update
3518
+ if( !skipPropagation )
2992
3519
  {
2993
- _processNextLineIfNecessary();
2994
- this.code.childNodes[ localLineNum ].innerHTML = gutterLineHtml + html;
2995
- this._setActiveLine( lineNumber );
2996
- this._clearTmpVariables();
3520
+ this._processExtraLineIfNecessary( lineNumber, tokens, oldSymbols, skipPropagation );
2997
3521
  }
2998
- else // Update all lines at once
3522
+
3523
+ if( this.mustProcessNextLine )
2999
3524
  {
3000
- return `<pre>${ gutterLineHtml + html }</pre>`;
3525
+ if( this.mustProcessNextLine( tokens ) && ( ( lineNumber + 1 ) < this.code.lines.length ) )
3526
+ {
3527
+ this.processLine( lineNumber + 1, false, true );
3528
+ }
3529
+ else
3530
+ {
3531
+ delete this.mustProcessNextLine;
3532
+ }
3533
+ }
3534
+
3535
+ if( this.mustProcessPreviousLine )
3536
+ {
3537
+ if( this.mustProcessPreviousLine( tokens ) && ( ( lineNumber - 1 ) >= 0 ) )
3538
+ {
3539
+ this.processLine( lineNumber - 1, false, true );
3540
+ }
3541
+ else
3542
+ {
3543
+ delete this.mustProcessPreviousLine;
3544
+ }
3545
+ }
3546
+
3547
+ if( CodeEditor.debugProcessedLines )
3548
+ {
3549
+ this.code.childNodes[ lineNumber ]?.classList.add( "debug" );
3001
3550
  }
3551
+
3552
+ this._setActiveLine( lineNumber );
3553
+ this._clearTmpVariables();
3554
+ }
3555
+
3556
+ this.code.lineSignatures[ lineNumber ] = this._getLineSignatureFromTokens( tokens );
3557
+
3558
+ // Update all lines at once
3559
+ return force ? `<pre>${ ( gutterLineHtml + html + debugString ) }</pre>` : undefined;
3560
+ }
3561
+
3562
+ /**
3563
+ * Parses a single line of code and extract declared symbols
3564
+ */
3565
+ _parseLineForSymbols( lineNumber, lineString, tokens, pushedScope ) {
3566
+
3567
+ const scope = this._scopeStack.at( pushedScope ? -2 : -1 );
3568
+
3569
+ if( !scope || this._inBlockCommentSection( lineNumber ) )
3570
+ {
3571
+ return [];
3002
3572
  }
3003
3573
 
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() )
3574
+ const scopeName = scope.name;
3575
+ const scopeType = scope.type;
3576
+ const symbols = [];
3577
+ const symbolsMap = new Map();
3578
+ const text = lineString.trim();
3579
+ const previousLineScope = this.code.lineScopes[ lineNumber - 1 ];
3580
+
3581
+ const _pushSymbol = ( s ) => {
3582
+ const signature = `${ s.name }_${ s.kind }_${ s.scope }_${ s.line }`;
3583
+ if( symbolsMap.has( signature ) )
3584
+ {
3007
3585
  return;
3586
+ }
3587
+ symbolsMap.set( signature, s );
3588
+ symbols.push( s );
3589
+ };
3590
+
3591
+ // Don't make symbols from preprocessor lines
3592
+ if( text.startsWith( "#" ) )
3593
+ {
3594
+ return [];
3595
+ }
3596
+
3597
+ const nativeTypes = CodeEditor.nativeTypes[ this.highlight ];
3008
3598
 
3009
- this.lineScopes[ lineNumber + 1 ] = [ ...this._scopeStack ];
3599
+ const topLevelRegexes = [
3600
+ [/^class\s+([A-Za-z0-9_]+)/, "class"],
3601
+ [/^struct\s+([A-Za-z0-9_]+)/, "struct"],
3602
+ [/^enum\s+([A-Za-z0-9_]+)/, "enum"],
3603
+ [/^interface\s+([A-Za-z0-9_]+)/, "interface"],
3604
+ [/^type\s+([A-Za-z0-9_]+)/, "type"],
3605
+ [/^function\s+([A-Za-z0-9_]+)/, "method"],
3606
+ [/^fn\s+([A-Za-z0-9_]+)/, "method"],
3607
+ [/^def\s+([A-Za-z0-9_]+)/, "method"],
3608
+ [/^([A-Za-z0-9_]+)\s*=\s*\(?.*\)?\s*=>/, "method"] // arrow functions
3609
+ ];
3010
3610
 
3011
- if( this.code.lines.length > lineNumber + 1 )
3611
+ // Add regexes to detect methods, variables ( including "id : nativeType" )
3612
+ {
3613
+ if( nativeTypes )
3012
3614
  {
3013
- this.processLine( lineNumber + 1 );
3615
+ topLevelRegexes.push( [ new RegExp( `^(?:${nativeTypes.join('|')})\\s+([A-Za-z0-9_]+)\s*[\(]+` ), 'method' ] );
3616
+
3617
+ if( this.highlight === "WGSL" )
3618
+ {
3619
+ topLevelRegexes.push( [ new RegExp( `[A-Za-z0-9]+(\\s*)+:(\\s*)+(${nativeTypes.join('|')})` ), 'variable', ( m ) => m[ 0 ].split( ":" )[ 0 ].trim() ] );
3620
+ }
3014
3621
  }
3622
+
3623
+ const declarationKeywords = CodeEditor.declarationKeywords[ this.highlight ] ?? [ "const", "let", "var" ];
3624
+ topLevelRegexes.push( [ new RegExp( `^(?:${ declarationKeywords.join('|') })\\s+([A-Za-z0-9_]+)` ), 'variable' ] );
3015
3625
  }
3016
3626
 
3017
- // multi-line strings not supported by now
3018
- delete this._buildingString;
3019
- delete this._pendingString;
3020
- delete this._markdownHeader;
3627
+ for( let [ regex, kind, fn ] of topLevelRegexes )
3628
+ {
3629
+ const m = text.match( regex );
3630
+ if( m )
3631
+ {
3632
+ _pushSymbol( { name: fn ? fn( m ) : m[ 1 ], kind, scope: scopeName, line: lineNumber } );
3633
+ }
3634
+ }
3021
3635
 
3022
- let lineString = this.code.lines[ lineNumber ];
3636
+ const usageRegexes = [
3637
+ [/new\s+([A-Za-z0-9_]+)\s*\(/, "constructor-call"],
3638
+ [/this.([A-Za-z_][A-Za-z0-9_]*)\s*\=/, "class-property"],
3639
+ ];
3023
3640
 
3024
- // Single line
3025
- if( !force )
3641
+ for( let [ regex, kind, fn ] of usageRegexes )
3026
3642
  {
3027
- LX.deleteElement( this.code.childNodes[ localLineNum ] );
3028
- this.code.insertChildAtIndex( document.createElement( 'pre' ), localLineNum );
3643
+ const m = text.match( regex );
3644
+ if( m )
3645
+ {
3646
+ _pushSymbol( { name: fn ? fn( m ) : m[ 1 ], kind, scope: scopeName, line: lineNumber } );
3647
+ }
3029
3648
  }
3030
3649
 
3031
- // Early out check for no highlighting languages
3032
- if( this.highlight == 'Plain Text' )
3650
+ // Detect method calls
3651
+ const regex = /([A-Za-z0-9_]+)\s*\(/g;
3652
+ let match;
3653
+ while( match = regex.exec( text ) )
3033
3654
  {
3034
- const plainTextHtml = lineString.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
3035
- return _updateLine( plainTextHtml );
3655
+ const name = match[ 1 ];
3656
+ const before = text.slice( 0, match.index );
3657
+ if( /(new|function|fn|def)\s+$/.test( before ) ) continue; // skip constructor calls
3658
+ if( [ "constructor", "location", ...( nativeTypes ?? [] ) ].indexOf( name ) > -1 ) continue; // skip hardcoded non method symbol
3659
+ if( previousLineScope && previousLineScope.at( -1 )?.type === "class" ) continue; // skip class methods
3660
+ _pushSymbol( { name, kind: "method-call", scope: scopeName, line: lineNumber } );
3036
3661
  }
3037
3662
 
3038
- this._currentLineNumber = lineNumber;
3039
- this._currentLineString = lineString;
3040
-
3041
- const tokensToEvaluate = this._getTokensFromLine( lineString );
3042
- if( !tokensToEvaluate.length )
3663
+ // Stop after matches for top-level declarations and usage symbols
3664
+ if( symbols.length )
3043
3665
  {
3044
- return _updateLine( "" );
3666
+ return symbols;
3045
3667
  }
3046
3668
 
3047
- let lineInnerHtml = "";
3669
+ const nonWhiteSpaceTokens = tokens.filter( t => t.trim().length );
3048
3670
 
3049
- // Process all tokens
3050
- for( let i = 0; i < tokensToEvaluate.length; ++i )
3671
+ for( let i = 0; i < nonWhiteSpaceTokens.length; i++ )
3051
3672
  {
3052
- let it = i - 1;
3053
- let prev = tokensToEvaluate[ it ];
3054
- while( prev == ' ' )
3673
+ const prev = nonWhiteSpaceTokens[ i - 1 ];
3674
+ const token = nonWhiteSpaceTokens[ i ];
3675
+ const next = nonWhiteSpaceTokens[ i + 1 ];
3676
+
3677
+ if( scopeType.startsWith("class") )
3055
3678
  {
3056
- it--;
3057
- prev = tokensToEvaluate[ it ];
3679
+ if( next === "(" && /^[a-zA-Z_]\w*$/.test( token ) && prev === undefined )
3680
+ {
3681
+ if( token === "constructor" ) continue; // skip constructor symbol
3682
+ _pushSymbol( { name: token, kind: "method", scope: scopeName, line: lineNumber } );
3683
+ }
3058
3684
  }
3059
-
3060
- it = i + 1;
3061
- let next = tokensToEvaluate[ it ];
3062
- while( next == ' ' || next == '"' )
3685
+ else if( scopeType.startsWith("enum") )
3063
3686
  {
3064
- it++;
3065
- next = tokensToEvaluate[ it ];
3687
+ if( !isSymbol( token ) && !this._isNumber( token ) && !this._mustHightlightWord( token, CodeEditor.statements ) )
3688
+ {
3689
+ _pushSymbol({ name: token, kind: "enum_value", scope: scopeName, line: lineNumber });
3690
+ }
3066
3691
  }
3692
+ }
3067
3693
 
3068
- const token = tokensToEvaluate[ i ];
3694
+ return symbols;
3695
+ }
3069
3696
 
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
- }
3697
+ /**
3698
+ * Updates the symbol table for a single line
3699
+ * - Removes old symbols from that line
3700
+ * - Inserts the new symbols
3701
+ */
3702
+ _updateLineSymbols( lineNumber, newSymbols ) {
3703
+
3704
+ this.code.lineSymbols[ lineNumber ] = this.code.lineSymbols[ lineNumber ] ?? [];
3705
+ const oldSymbols = LX.deepCopy( this.code.lineSymbols[ lineNumber ] );
3076
3706
 
3077
- // Pop current scope if necessary
3078
- if( token === "}" )
3707
+ // Clean old symbols from current line
3708
+ for( let sym of this.code.lineSymbols[ lineNumber ] )
3709
+ {
3710
+ let array = this.code.symbolsTable.get( sym.name );
3711
+ if( !array )
3079
3712
  {
3080
- this._scopeStack.pop();
3713
+ continue;
3081
3714
  }
3082
3715
 
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
- } );
3716
+ array = array.filter( s => s.line !== lineNumber );
3094
3717
 
3095
- // Store current scopes
3096
- if( token === "{" )
3718
+ if( array.length )
3097
3719
  {
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 );
3720
+ this.code.symbolsTable.set( sym.name, array );
3721
+ }
3722
+ else
3723
+ {
3724
+ this.code.symbolsTable.delete( sym.name );
3120
3725
  }
3121
3726
  }
3122
3727
 
3123
- return _updateLine( lineInnerHtml );
3728
+ // Add new symbols to table
3729
+ for( let sym of newSymbols )
3730
+ {
3731
+ let arr = this.code.symbolsTable.get( sym.name ) ?? [];
3732
+ arr.push(sym);
3733
+ this.code.symbolsTable.set( sym.name, arr );
3734
+ }
3735
+
3736
+ // Keep lineSymbols in sync
3737
+ this.code.lineSymbols[ lineNumber ] = newSymbols;
3738
+
3739
+ return oldSymbols;
3124
3740
  }
3125
3741
 
3126
3742
  _lineHasComment( lineString ) {
@@ -3185,13 +3801,14 @@ class CodeEditor {
3185
3801
  let idx = 0;
3186
3802
  while( subtokens.value != undefined )
3187
3803
  {
3188
- const _pt = lineString.substring(idx, subtokens.value.index);
3804
+ const _pt = lineString.substring( idx, subtokens.value.index );
3189
3805
  if( _pt.length ) pushToken( _pt );
3190
3806
  pushToken( subtokens.value[ 0 ] );
3191
3807
  idx = subtokens.value.index + subtokens.value[ 0 ].length;
3192
3808
  subtokens = iter.next();
3193
- if(!subtokens.value) {
3194
- const _at = lineString.substring(idx);
3809
+ if( !subtokens.value )
3810
+ {
3811
+ const _at = lineString.substring( idx );
3195
3812
  if( _at.length ) pushToken( _at );
3196
3813
  }
3197
3814
  }
@@ -3236,18 +3853,49 @@ class CodeEditor {
3236
3853
  }
3237
3854
  else if( this.highlight == 'PHP' )
3238
3855
  {
3239
- const dollarIdx = tokens.indexOf( '$' );
3240
- if( dollarIdx > -1 && tokens[ dollarIdx + 1 ] === 'this-' )
3856
+ let offset = 0;
3857
+ let dollarIdx = tokens.indexOf( '$' );
3858
+
3859
+ while( dollarIdx > -1 )
3860
+ {
3861
+ const offsetIdx = dollarIdx + offset;
3862
+
3863
+ if( tokens[ offsetIdx + 1 ] === 'this-' )
3864
+ {
3865
+ tokens[ offsetIdx ] = "$this";
3866
+ tokens[ offsetIdx + 1 ] = "-";
3867
+ }
3868
+ else
3869
+ {
3870
+ tokens[ offsetIdx ] += ( tokens[ offsetIdx + 1 ] ?? "" );
3871
+ tokens.splice( offsetIdx + 1, 1 );
3872
+ }
3873
+
3874
+ dollarIdx = tokens.slice( offsetIdx ).indexOf( '$' );
3875
+ offset = offsetIdx;
3876
+ }
3877
+ }
3878
+ else if( this.highlight == 'WGSL' )
3879
+ {
3880
+ let offset = 0;
3881
+ let atIdx = tokens.indexOf( '@' );
3882
+
3883
+ while( atIdx > -1 )
3241
3884
  {
3242
- tokens[ dollarIdx ] = "$this";
3243
- tokens[ dollarIdx + 1 ] = "-";
3885
+ const offsetIdx = atIdx + offset;
3886
+
3887
+ tokens[ offsetIdx ] += ( tokens[ offsetIdx + 1 ] ?? "" );
3888
+ tokens.splice( offsetIdx + 1, 1 );
3889
+
3890
+ atIdx = tokens.slice( offsetIdx ).indexOf( '$' );
3891
+ offset = offsetIdx;
3244
3892
  }
3245
3893
  }
3246
3894
 
3247
3895
  return tokens;
3248
3896
  }
3249
3897
 
3250
- _mustHightlightWord( token, kindArray, lang ) {
3898
+ _mustHightlightWord( token, wordCategory, lang ) {
3251
3899
 
3252
3900
  if( !lang )
3253
3901
  {
@@ -3261,12 +3909,12 @@ class CodeEditor {
3261
3909
  t = t.toLowerCase();
3262
3910
  }
3263
3911
 
3264
- return kindArray[ this.highlight ] && kindArray[ this.highlight ][ t ] != undefined;
3912
+ return wordCategory[ this.highlight ] && wordCategory[ this.highlight ].has( t );
3265
3913
  }
3266
3914
 
3267
3915
  _getTokenHighlighting( ctx, highlight ) {
3268
3916
 
3269
- const rules = [ ...HighlightRules.common, ...( HighlightRules[highlight] || [] ), ...HighlightRules.post_common ];
3917
+ const rules = [ ...HighlightRules.common, ...( HighlightRules[ highlight ] || [] ), ...HighlightRules.post_common ];
3270
3918
 
3271
3919
  for( const rule of rules )
3272
3920
  {
@@ -3287,9 +3935,12 @@ class CodeEditor {
3287
3935
 
3288
3936
  let { token, prev, next, tokenIndex, isFirstToken, isLastToken } = ctxData;
3289
3937
 
3290
- const lang = CodeEditor.languages[ this.highlight ],
3291
- highlight = this.highlight.replace( /\s/g, '' ).replaceAll( "+", "p" ).toLowerCase(),
3292
- customStringKeys = Object.assign( {}, this.stringKeys );
3938
+ const lang = CodeEditor.languages[ this.highlight ];
3939
+ const highlight = this.highlight.replace( /\s/g, '' ).replaceAll( "+", "p" ).toLowerCase();
3940
+ const customStringKeys = Object.assign( {}, this.stringKeys );
3941
+ const lineNumber = this._currentLineNumber;
3942
+ const tokenStartIndex = this._currentTokenPositions[ tokenIndex ];
3943
+ const inBlockComment = ( this._buildingBlockComment ?? this._inBlockCommentSection( lineNumber, tokenStartIndex, token.length ) !== undefined )
3293
3944
 
3294
3945
  var usePreviousTokenToCheckString = false;
3295
3946
 
@@ -3307,7 +3958,7 @@ class CodeEditor {
3307
3958
  // Manage strings
3308
3959
  this._stringEnded = false;
3309
3960
 
3310
- if( usePreviousTokenToCheckString || ( this._buildingBlockComment === undefined && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) ) )
3961
+ if( usePreviousTokenToCheckString || ( !inBlockComment && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) ) )
3311
3962
  {
3312
3963
  const _checkIfStringEnded = t => {
3313
3964
  const idx = Object.values( customStringKeys ).indexOf( t );
@@ -3333,26 +3984,25 @@ class CodeEditor {
3333
3984
 
3334
3985
  // Update context data for next tests
3335
3986
  ctxData.discardToken = false;
3336
- ctxData.inBlockComment = this._buildingBlockComment;
3987
+ ctxData.inBlockComment = inBlockComment;
3337
3988
  ctxData.markdownHeader = this._markdownHeader;
3338
- ctxData.inString = this._buildingString;
3989
+ ctxData.inString = ( this._buildingString !== undefined );
3339
3990
  ctxData.singleLineCommentToken = lang.singleLineCommentToken ?? this.defaultSingleLineCommentToken;
3340
3991
  ctxData.lang = lang;
3341
- ctxData.scope = this._scopeStack[ this._scopeStack.length - 1 ];
3992
+ ctxData.scope = this._scopeStack.at( -1 );
3993
+
3994
+ // Add utils functions for the rules
3995
+ ctxData.isVariableSymbol = ( token ) => this.code.symbolsTable.has( token ) && this.code.symbolsTable.get( token )[ 0 ].kind === "variable";
3996
+ ctxData.isEnumValueSymbol = ( token ) => this.code.symbolsTable.has( token ) && this.code.symbolsTable.get( token )[ 0 ].kind === "enum_value";
3997
+ ctxData.isClassSymbol = ( token ) => this.code.symbolsTable.has( token ) && this.code.symbolsTable.get( token )[ 0 ].kind === "class";
3998
+ ctxData.isStructSymbol = ( token ) => this.code.symbolsTable.has( token ) && this.code.symbolsTable.get( token )[ 0 ].kind === "struct";
3999
+ ctxData.isEnumSymbol = ( token ) => this.code.symbolsTable.has( token ) && this.code.symbolsTable.get( token )[ 0 ].kind === "enum";
3342
4000
 
3343
4001
  // Get highlighting class based on language common and specific rules
3344
4002
  let tokenClass = this._getTokenHighlighting( ctxData, highlight );
3345
4003
 
3346
- const blockCommentsTokens = lang.blockCommentsTokens ?? this.defaultBlockCommentTokens;
3347
- if( ( lang.blockComments ?? true ) && this._buildingBlockComment != undefined
3348
- && token.substr( 0, blockCommentsTokens[ 1 ].length ) == blockCommentsTokens[ 1 ] )
3349
- {
3350
- this._blockCommentCache.push( new LX.vec2( this._buildingBlockComment, this._currentLineNumber ) );
3351
- delete this._buildingBlockComment;
3352
- }
3353
-
3354
4004
  // We finished constructing a string
3355
- if( this._buildingString && ( this._stringEnded || isLastToken ) )
4005
+ if( this._buildingString && ( this._stringEnded || isLastToken ) && !inBlockComment )
3356
4006
  {
3357
4007
  token = this._getCurrentString();
3358
4008
  tokenClass = "cm-str";
@@ -3392,7 +4042,6 @@ class CodeEditor {
3392
4042
  }
3393
4043
 
3394
4044
  _getCurrentString() {
3395
-
3396
4045
  const chars = this._pendingString;
3397
4046
  delete this._pendingString;
3398
4047
  return chars;
@@ -3417,17 +4066,32 @@ class CodeEditor {
3417
4066
  }
3418
4067
  }
3419
4068
 
3420
- _inBlockCommentSection( line ) {
4069
+ _inBlockCommentSection( lineNumber, tokenPosition, tokenLength ) {
4070
+
4071
+ const lang = CodeEditor.languages[ this.highlight ];
4072
+ const blockCommentsTokens = lang.blockCommentsTokens ?? this.defaultBlockCommentTokens;
3421
4073
 
3422
- for( var section of this._blockCommentCache )
4074
+ for( let section of this._blockCommentCache )
3423
4075
  {
3424
- if( line >= section.x && line <= section.y )
4076
+ const lineRange = section[ 0 ];
4077
+ const posRange = section[ 1 ];
4078
+
4079
+ // Outside the lines range
4080
+ const meetsLineRange = ( lineNumber >= lineRange.x && lineNumber <= lineRange.y );
4081
+ if( !meetsLineRange )
3425
4082
  {
3426
- return true;
4083
+ continue;
3427
4084
  }
3428
- }
3429
4085
 
3430
- return false;
4086
+ if( ( lineNumber != lineRange.x && lineNumber != lineRange.y ) || // Inside the block, not first nor last line
4087
+ ( lineNumber == lineRange.x && tokenPosition >= posRange.x &&
4088
+ (( lineNumber == lineRange.y && ( tokenPosition + tokenLength ) <= ( posRange.y + blockCommentsTokens[ 1 ].length ) ) || lineNumber !== lineRange.y) ) ||
4089
+ ( lineNumber == lineRange.y && ( ( tokenPosition + tokenLength ) <= ( posRange.y + blockCommentsTokens[ 1 ].length ) ) ) &&
4090
+ (( lineNumber == lineRange.x && tokenPosition >= posRange.x ) || lineNumber !== lineRange.x) )
4091
+ {
4092
+ return section;
4093
+ }
4094
+ }
3431
4095
  }
3432
4096
 
3433
4097
  _isKeyword( ctxData ) {
@@ -3521,7 +4185,7 @@ class CodeEditor {
3521
4185
  this.cursorToPosition( cursor, cursor.selection.toX + 1 );
3522
4186
 
3523
4187
  // Change next key?
3524
- switch(key)
4188
+ switch( key )
3525
4189
  {
3526
4190
  case "'":
3527
4191
  case "\"":
@@ -3549,6 +4213,46 @@ class CodeEditor {
3549
4213
  return true;
3550
4214
  }
3551
4215
 
4216
+ _detectLanguage( text ) {
4217
+
4218
+ const tokenSet = new Set( this._getTokensFromLine( text, true ) );
4219
+ const scores = {};
4220
+
4221
+ for( let [ lang, wordList ] of Object.entries( CodeEditor.keywords ) )
4222
+ {
4223
+ scores[ lang ] = 0;
4224
+ for( let kw of wordList )
4225
+ if( tokenSet.has( kw ) ) scores[ lang ]++;
4226
+ }
4227
+
4228
+ for( let [ lang, wordList ] of Object.entries( CodeEditor.statements ) )
4229
+ {
4230
+ for( let kw of wordList )
4231
+ if( tokenSet.has( kw ) ) scores[ lang ]++;
4232
+ }
4233
+
4234
+ for( let [ lang, wordList ] of Object.entries( CodeEditor.utils ) )
4235
+ {
4236
+ for( let kw of wordList )
4237
+ if( tokenSet.has( kw ) ) scores[ lang ]++;
4238
+ }
4239
+
4240
+ for( let [ lang, wordList ] of Object.entries( CodeEditor.types ) )
4241
+ {
4242
+ for( let kw of wordList )
4243
+ if( tokenSet.has( kw ) ) scores[ lang ]++;
4244
+ }
4245
+
4246
+ for( let [ lang, wordList ] of Object.entries( CodeEditor.builtIn ) )
4247
+ {
4248
+ for( let kw of wordList )
4249
+ if( tokenSet.has( kw ) ) scores[ lang ]++;
4250
+ }
4251
+
4252
+ const sorted = Object.entries( scores ).sort( ( a, b ) => b[ 1 ] - a[ 1 ] );
4253
+ return sorted[0][1] > 0 ? sorted[0][0] : undefined;
4254
+ }
4255
+
3552
4256
  lineUp( cursor, resetLeft ) {
3553
4257
 
3554
4258
  if( this.code.lines[ cursor.line - 1 ] == undefined )
@@ -3664,7 +4368,7 @@ class CodeEditor {
3664
4368
  // Use main cursor
3665
4369
  this._removeSecondaryCursors();
3666
4370
 
3667
- var cursor = this._getCurrentCursor();
4371
+ var cursor = this.getCurrentCursor();
3668
4372
  this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, cursor );
3669
4373
 
3670
4374
  this.startSelection( cursor );
@@ -3754,11 +4458,8 @@ class CodeEditor {
3754
4458
  }
3755
4459
 
3756
4460
  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;
4461
+ const scrollerHeight = this.codeScroller.offsetHeight - this._fullVerticalOffset;
4462
+ const lastLine = ( ( scrollerHeight + currentScrollTop ) / this.lineHeight )|0;
3762
4463
  if( cursor.line >= lastLine )
3763
4464
  {
3764
4465
  this.setScrollTop( currentScrollTop + this.lineHeight );
@@ -3814,6 +4515,16 @@ class CodeEditor {
3814
4515
  return cursors;
3815
4516
  }
3816
4517
 
4518
+ getCurrentCursor( removeOthers ) {
4519
+
4520
+ if( removeOthers )
4521
+ {
4522
+ this._removeSecondaryCursors();
4523
+ }
4524
+
4525
+ return this.cursors.children[ 0 ];
4526
+ }
4527
+
3817
4528
  relocateCursors() {
3818
4529
 
3819
4530
  for( let cursor of this.cursors.children )
@@ -3863,7 +4574,7 @@ class CodeEditor {
3863
4574
 
3864
4575
  resetCursorPos( flag, cursor ) {
3865
4576
 
3866
- cursor = cursor ?? this._getCurrentCursor();
4577
+ cursor = cursor ?? this.getCurrentCursor();
3867
4578
 
3868
4579
  if( flag & CodeEditor.CURSOR_LEFT )
3869
4580
  {
@@ -3880,6 +4591,71 @@ class CodeEditor {
3880
4591
  }
3881
4592
  }
3882
4593
 
4594
+ _addCursor( line = 0, position = 0, force, isMain = false ) {
4595
+
4596
+ // If cursor in that position exists, remove it instead..
4597
+ const exists = Array.from( this.cursors.children ).find( v => v.position == position && v.line == line );
4598
+ if( exists && !force )
4599
+ {
4600
+ if( !exists.isMain )
4601
+ exists.remove();
4602
+
4603
+ return;
4604
+ }
4605
+
4606
+ let cursor = document.createElement( 'div' );
4607
+ cursor.name = "cursor" + this.cursors.childElementCount;
4608
+ cursor.className = "cursor";
4609
+ cursor.innerHTML = "&nbsp;";
4610
+ cursor.isMain = isMain;
4611
+ cursor._left = position * this.charWidth;
4612
+ cursor.style.left = "calc( " + cursor._left + "px + " + this.xPadding + " )";
4613
+ cursor._top = line * this.lineHeight;
4614
+ cursor.style.top = cursor._top + "px";
4615
+ cursor._position = position;
4616
+ cursor._line = line;
4617
+ cursor.print = (function() { console.log( this._line, this._position ) }).bind( cursor );
4618
+ cursor.isLast = (function() { return this.cursors.lastChild == cursor; }).bind( this );
4619
+
4620
+ Object.defineProperty( cursor, 'line', {
4621
+ get: (v) => { return cursor._line },
4622
+ set: (v) => {
4623
+ cursor._line = v;
4624
+ if( cursor.isMain ) this._setActiveLine( v );
4625
+ }
4626
+ } );
4627
+
4628
+ Object.defineProperty( cursor, 'position', {
4629
+ get: (v) => { return cursor._position },
4630
+ set: (v) => {
4631
+ cursor._position = v;
4632
+ if( cursor.isMain )
4633
+ {
4634
+ const activeLine = this.state.activeLine;
4635
+ this._updateDataInfoPanel( "@cursor-data", `Ln ${ activeLine + 1 }, Col ${ v + 1 }` );
4636
+ }
4637
+ }
4638
+ } );
4639
+
4640
+ this.cursors.appendChild( cursor );
4641
+
4642
+ return cursor;
4643
+ }
4644
+
4645
+ _removeSecondaryCursors() {
4646
+
4647
+ while( this.cursors.childElementCount > 1 )
4648
+ this.cursors.lastChild.remove();
4649
+ }
4650
+
4651
+ _logCursors() {
4652
+
4653
+ for( let cursor of this.cursors.children )
4654
+ {
4655
+ cursor.print();
4656
+ }
4657
+ }
4658
+
3883
4659
  _addSpaceTabs( cursor, n ) {
3884
4660
 
3885
4661
  for( var i = 0; i < n; ++i )
@@ -3890,9 +4666,10 @@ class CodeEditor {
3890
4666
 
3891
4667
  _addSpaces( n ) {
3892
4668
 
3893
- for( var i = 0; i < n; ++i ) {
4669
+ for( var i = 0; i < n; ++i )
4670
+ {
3894
4671
  this.root.dispatchEvent( new CustomEvent( 'keydown', { 'detail': {
3895
- skip_undo: true,
4672
+ skipUndo: true,
3896
4673
  key: ' ',
3897
4674
  targetCursor: this._lastProcessedCursorIndex
3898
4675
  }}));
@@ -3913,7 +4690,8 @@ class CodeEditor {
3913
4690
  }
3914
4691
 
3915
4692
  // Only tabs/spaces in the line...
3916
- if( lineStart == -1 ) {
4693
+ if( lineStart == -1 )
4694
+ {
3917
4695
  lineStart = this.code.lines[ lidx ].length;
3918
4696
  }
3919
4697
 
@@ -3964,24 +4742,28 @@ class CodeEditor {
3964
4742
  }, 20 );
3965
4743
  }
3966
4744
 
3967
- resize( pMaxLength, onResize ) {
4745
+ resize( flag = CodeEditor.RESIZE_SCROLLBAR_H_V, pMaxLength, onResize ) {
3968
4746
 
3969
4747
  setTimeout( () => {
3970
4748
 
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;
4749
+ let scrollWidth, scrollHeight;
3978
4750
 
3979
- this._lastMaxLineLength = maxLineLength;
4751
+ if( flag & CodeEditor.RESIZE_SCROLLBAR_H )
4752
+ {
4753
+ // Update max viewport
4754
+ const maxLineLength = pMaxLength ?? this.getMaxLineLength();
4755
+ this._lastMaxLineLength = maxLineLength;
4756
+ scrollWidth = maxLineLength * this.charWidth + CodeEditor.LINE_GUTTER_WIDTH;
4757
+ this.codeSizer.style.minWidth = scrollWidth + "px";
4758
+ }
3980
4759
 
3981
- this.codeSizer.style.minWidth = scrollWidth + "px";
3982
- this.codeSizer.style.minHeight = ( scrollHeight + tabsHeight + statusPanelHeight ) + "px";
4760
+ if( flag & CodeEditor.RESIZE_SCROLLBAR_V )
4761
+ {
4762
+ scrollHeight = this.code.lines.length * this.lineHeight;
4763
+ this.codeSizer.style.minHeight = scrollHeight + "px";
4764
+ }
3983
4765
 
3984
- this.resizeScrollBars();
4766
+ this.resizeScrollBars( flag );
3985
4767
 
3986
4768
  if( onResize )
3987
4769
  {
@@ -3991,37 +4773,52 @@ class CodeEditor {
3991
4773
  }, 10 );
3992
4774
  }
3993
4775
 
3994
- resizeScrollBars() {
4776
+ resizeIfNecessary( cursor, force ) {
3995
4777
 
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
4778
+ const maxLineLength = this.getMaxLineLength();
4779
+ const numViewportChars = Math.floor( ( this.codeScroller.clientWidth - CodeEditor.LINE_GUTTER_WIDTH ) / this.charWidth );
4780
+ if( force || ( maxLineLength >= numViewportChars && maxLineLength != this._lastMaxLineLength ) )
4004
4781
  {
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) + "%";
4782
+ this.resize( CodeEditor.RESIZE_SCROLLBAR_H, maxLineLength, () => {
4783
+ if( cursor.position > numViewportChars )
4784
+ {
4785
+ this.setScrollLeft( cursor.position * this.charWidth );
4786
+ }
4787
+ } );
4009
4788
  }
4789
+ }
4010
4790
 
4011
- const numViewportChars = Math.floor( ( this.codeScroller.clientWidth - CodeEditor.LINE_GUTTER_WIDTH ) / this.charWidth );
4012
- const maxLineLength = this._lastMaxLineLength;
4791
+ resizeScrollBars( flag = CodeEditor.RESIZE_SCROLLBAR_H_V ) {
4013
4792
 
4014
- if( numViewportChars >= maxLineLength )
4793
+ if( flag & CodeEditor.RESIZE_SCROLLBAR_V )
4015
4794
  {
4016
- this.codeScroller.classList.remove( 'with-hscrollbar' );
4017
- this.hScrollbar.root.classList.add( 'scrollbar-unused' );
4795
+ const totalLinesInViewport = (( this.codeScroller.offsetHeight ) / this.lineHeight)|0;
4796
+ const needsVerticalScrollbar = ( this.code.lines.length >= totalLinesInViewport );
4797
+ if( needsVerticalScrollbar )
4798
+ {
4799
+ this.vScrollbar.thumb.size = ( totalLinesInViewport / this.code.lines.length );
4800
+ this.vScrollbar.thumb.style.height = (this.vScrollbar.thumb.size * 100.0) + "%";
4801
+ }
4802
+
4803
+ this.vScrollbar.root.classList.toggle( 'hidden', !needsVerticalScrollbar );
4804
+ this.hScrollbar.root.style.width = `calc(100% - ${ 48 + ( needsVerticalScrollbar ? ScrollBar.SCROLLBAR_VERTICAL_WIDTH : 0 ) }px)`; // 48 is the line gutter
4805
+ this.codeArea.root.style.width = `calc(100% - ${ needsVerticalScrollbar ? ScrollBar.SCROLLBAR_VERTICAL_WIDTH : 0 }px)`;
4018
4806
  }
4019
- else
4807
+
4808
+ if( flag & CodeEditor.RESIZE_SCROLLBAR_H )
4020
4809
  {
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) + "%";
4810
+ const numViewportChars = Math.floor( ( this.codeScroller.clientWidth - CodeEditor.LINE_GUTTER_WIDTH ) / this.charWidth );
4811
+ const maxLineLength = this._lastMaxLineLength;
4812
+ const needsHorizontalScrollbar = maxLineLength >= numViewportChars;
4813
+
4814
+ if( needsHorizontalScrollbar )
4815
+ {
4816
+ this.hScrollbar.thumb.size = ( numViewportChars / maxLineLength );
4817
+ this.hScrollbar.thumb.style.width = ( this.hScrollbar.thumb.size * 100.0 ) + "%";
4818
+ }
4819
+
4820
+ this.hScrollbar.root.classList.toggle( 'hidden', !needsHorizontalScrollbar );
4821
+ this.codeArea.root.style.height = `calc(100% - ${ this._fullVerticalOffset + ( needsHorizontalScrollbar ? ScrollBar.SCROLLBAR_HORIZONTAL_HEIGHT : 0 ) }px)`;
4025
4822
  }
4026
4823
  }
4027
4824
 
@@ -4095,7 +4892,6 @@ class CodeEditor {
4095
4892
  }
4096
4893
 
4097
4894
  getCharAtPos( cursor, offset = 0 ) {
4098
-
4099
4895
  return this.code.lines[ cursor.line ][ cursor.position + offset ];
4100
4896
  }
4101
4897
 
@@ -4164,7 +4960,6 @@ class CodeEditor {
4164
4960
  }
4165
4961
 
4166
4962
  measureString( str ) {
4167
-
4168
4963
  return str.length * this.charWidth;
4169
4964
  }
4170
4965
 
@@ -4217,53 +5012,95 @@ class CodeEditor {
4217
5012
  showAutoCompleteBox( key, cursor ) {
4218
5013
 
4219
5014
  if( !cursor.isMain )
5015
+ {
4220
5016
  return;
5017
+ }
4221
5018
 
4222
- const [word, start, end] = this.getWordAtPos( cursor, -1 );
4223
- if( key == ' ' || !word.length ) {
5019
+ const [ word, start, end ] = this.getWordAtPos( cursor, -1 );
5020
+ if( key == ' ' || !word.length )
5021
+ {
4224
5022
  this.hideAutoCompleteBox();
4225
5023
  return;
4226
5024
  }
4227
5025
 
4228
5026
  this.autocomplete.innerHTML = ""; // Clear all suggestions
4229
5027
 
4230
- let suggestions = [];
4231
-
4232
5028
  // 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
- );
5029
+ let suggestions = [
5030
+ ...Array.from( CodeEditor.keywords[ this.highlight ] ?? [] ),
5031
+ ...Array.from( CodeEditor.builtIn[ this.highlight ] ?? [] ),
5032
+ ...Array.from( CodeEditor.statements[ this.highlight ] ?? [] ),
5033
+ ...Array.from( CodeEditor.types[ this.highlight ] ?? [] ),
5034
+ ...Array.from( CodeEditor.utils[ this.highlight ] ?? [] )
5035
+ ];
4240
5036
 
4241
- // Add words in current tab plus remove current word
4242
- // suggestions = suggestions.concat( Object.keys(this.code.tokens).filter( a => a != word ) );
5037
+ const scopeStack = [ ...this.code.lineScopes[ cursor.line ] ];
5038
+ const scope = scopeStack.at( -1 );
5039
+ if( scope.type.startsWith( "enum" ) )
5040
+ {
5041
+ 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 ] );
5042
+ suggestions = suggestions.concat( enumValues.slice( 0, -1 ) );
5043
+ }
5044
+ else
5045
+ {
5046
+ const otherValues = Array.from( this.code.symbolsTable ).map( s => s[ 0 ] );
5047
+ suggestions = suggestions.concat( otherValues.slice( 0, -1 ) );
5048
+ }
4243
5049
 
4244
5050
  // Remove 1/2 char words and duplicates...
4245
- suggestions = suggestions.filter( (value, index) => value.length > 2 && suggestions.indexOf(value) === index );
5051
+ suggestions = Array.from( new Set( suggestions )).filter( s => s.length > 2 && s.toLowerCase().includes( word.toLowerCase() ) );
4246
5052
 
4247
5053
  // Order...
4248
- suggestions = suggestions.sort( ( a, b ) => a.localeCompare( b ) );
5054
+
5055
+ function scoreSuggestion( s, prefix ) {
5056
+ if( s.startsWith( prefix ) ) return 0; // best option
5057
+ if( s.includes( prefix )) return 1;
5058
+ return 2; // worst
5059
+ }
5060
+
5061
+ suggestions = suggestions.sort( ( a, b ) => ( scoreSuggestion( a, word ) - scoreSuggestion( b, word ) ) || a.localeCompare( b ) );
4249
5062
 
4250
5063
  for( let s of suggestions )
4251
5064
  {
4252
- if( !s.toLowerCase().includes( word.toLowerCase() ) )
4253
- continue;
4254
-
4255
- var pre = document.createElement( 'pre' );
5065
+ const pre = document.createElement( 'pre' );
4256
5066
  this.autocomplete.appendChild( pre );
4257
5067
 
4258
- var icon = "Type";
5068
+ const symbol = this.code.symbolsTable.get( s );
4259
5069
 
4260
- if( this._mustHightlightWord( s, CodeEditor.utils ) )
4261
- icon = "Box";
4262
- else if( this._mustHightlightWord( s, CodeEditor.types ) )
4263
- icon = "Code";
5070
+ let iconName = "CaseLower";
5071
+ let iconClass = "foo";
4264
5072
 
4265
- pre.appendChild( LX.makeIcon( icon, { iconClass: "mr-1", svgClass: "xs" } ) );
5073
+ if( symbol )
5074
+ {
5075
+ switch( symbol[ 0 ].kind ) // Get first occurrence
5076
+ {
5077
+ case "variable":
5078
+ iconName = "Cuboid";
5079
+ iconClass = "lightblue";
5080
+ break;
5081
+ case "method":
5082
+ iconName = "Box";
5083
+ iconClass = "heliotrope";
5084
+ break;
5085
+ case "class":
5086
+ iconName = "CircleNodes";
5087
+ iconClass = "orange";
5088
+ break;
5089
+ }
5090
+ }
5091
+ else
5092
+ {
5093
+ if( this._mustHightlightWord( s, CodeEditor.utils ) )
5094
+ iconName = "ToolCase";
5095
+ else if( this._mustHightlightWord( s, CodeEditor.types ) )
5096
+ {
5097
+ iconName = "Type";
5098
+ iconClass = "lightblue";
5099
+ }
5100
+ }
4266
5101
 
5102
+ pre.appendChild( LX.makeIcon( iconName, { iconClass: "mr-1", svgClass: "sm " + iconClass } ) );
5103
+ s
4267
5104
  pre.addEventListener( 'click', () => {
4268
5105
  this.autoCompleteWord( s );
4269
5106
  } );
@@ -4292,11 +5129,11 @@ class CodeEditor {
4292
5129
  }
4293
5130
 
4294
5131
  // Select always first option
4295
- this.autocomplete.firstChild.classList.add('selected');
5132
+ this.autocomplete.firstChild.classList.add( 'selected' );
4296
5133
 
4297
5134
  // Show box
4298
- this.autocomplete.classList.toggle('show', true);
4299
- this.autocomplete.classList.toggle('no-scrollbar', !(this.autocomplete.scrollHeight > this.autocomplete.offsetHeight));
5135
+ this.autocomplete.classList.toggle( 'show', true );
5136
+ this.autocomplete.classList.toggle( 'no-scrollbar', !( this.autocomplete.scrollHeight > this.autocomplete.offsetHeight ) );
4300
5137
  this.autocomplete.style.left = (cursor._left + CodeEditor.LINE_GUTTER_WIDTH - this.getScrollLeft()) + "px";
4301
5138
  this.autocomplete.style.top = (cursor._top + 28 + this.lineHeight - this.getScrollTop()) + "px";
4302
5139
 
@@ -4408,7 +5245,7 @@ class CodeEditor {
4408
5245
  }
4409
5246
  else
4410
5247
  {
4411
- const cursor = this._getCurrentCursor();
5248
+ const cursor = this.getCurrentCursor();
4412
5249
 
4413
5250
  if( cursor.selection )
4414
5251
  {
@@ -4452,7 +5289,7 @@ class CodeEditor {
4452
5289
  return;
4453
5290
  }
4454
5291
 
4455
- let cursor = this._getCurrentCursor();
5292
+ let cursor = this.getCurrentCursor();
4456
5293
  let cursorData = new LX.vec2( cursor.position, cursor.line );
4457
5294
  let line = null;
4458
5295
  let char = -1;
@@ -4599,7 +5436,7 @@ class CodeEditor {
4599
5436
  this.codeScroller.scrollTo( 0, Math.max( line - 15 ) * this.lineHeight );
4600
5437
 
4601
5438
  // Select line ?
4602
- var cursor = this._getCurrentCursor( true );
5439
+ var cursor = this.getCurrentCursor( true );
4603
5440
  this.cursorToLine( cursor, line - 1, true );
4604
5441
  }
4605
5442
 
@@ -4654,7 +5491,7 @@ class CodeEditor {
4654
5491
 
4655
5492
  number = number ?? this.state.activeLine;
4656
5493
 
4657
- const cursor = this._getCurrentCursor();
5494
+ const cursor = this.getCurrentCursor();
4658
5495
  this._updateDataInfoPanel( "@cursor-data", `Ln ${ number + 1 }, Col ${ cursor.position + 1 }` );
4659
5496
 
4660
5497
  const oldLocal = this.toLocalLine( this.state.activeLine );
@@ -4731,23 +5568,33 @@ class CodeEditor {
4731
5568
  }
4732
5569
 
4733
5570
  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' },
5571
+ 'Plain Text': { ext: "txt", blockComments: false, singleLineComments: false, numbers: false, icon: "AlignLeft gray" },
5572
+ 'JavaScript': { ext: "js", icon: "Js goldenrod" },
5573
+ 'TypeScript': { ext: "ts", icon: "Ts pipelineblue" },
5574
+ 'C': { ext: [ 'c', 'h' ], usePreprocessor: true, icon: { 'c': "C pictonblue", 'h': "C heliotrope" } },
5575
+ 'C++': { ext: [ "cpp", "hpp" ], usePreprocessor: true, icon: { 'cpp': "CPlusPlus pictonblue", 'hpp': "CPlusPlus heliotrope" } },
5576
+ 'CSS': { ext: "css", icon: "Hash dodgerblue" },
5577
+ 'CMake': { ext: "cmake", singleLineCommentToken: '#', blockComments: false, ignoreCase: true },
5578
+ 'GLSL': { ext: "glsl", usePreprocessor: true },
5579
+ 'WGSL': { ext: "wgsl", usePreprocessor: true },
5580
+ 'JSON': { ext: "json", blockComments: false, singleLineComments: false, icon: "Braces fg-primary" },
5581
+ 'XML': { ext: "xml", tags: true, icon: "Rss orange" },
5582
+ 'Rust': { ext: "rs", icon: "Rust fg-primary" },
5583
+ 'Python': { ext: "py", singleLineCommentToken: '#', icon: "Python munsellblue" },
5584
+ 'HTML': { ext: "html", tags: true, singleLineComments: false, blockCommentsTokens: [ '<!--', '-->' ], numbers: false, icon: "Code orange" },
5585
+ 'Batch': { ext: "bat", blockComments: false, singleLineCommentToken: '::', ignoreCase: true, icon: "Windows lightblue" },
5586
+ 'Markdown': { ext: "md", blockComments: false, singleLineCommentToken: '::', tags: true, numbers: false, icon: "Markdown fg-primary" },
5587
+ 'PHP': { ext: "php", icon: "Php blueviolet" },
5588
+ };
5589
+
5590
+ CodeEditor.nativeTypes = {
5591
+ 'C++': ['int', 'float', 'double', 'bool', 'long', 'short', 'char', 'wchar_t', 'void'],
5592
+ 'WGSL': ['bool', 'u32', 'i32', 'f16', 'f32', 'vec2', 'vec3', 'vec4', 'vec2f', 'vec3f', 'vec4f', 'mat2x2f', 'mat3x3f', 'mat4x4f', 'array', 'vec2u', 'vec3u', 'vec4u', 'ptr', 'sampler']
5593
+ };
5594
+
5595
+ CodeEditor.declarationKeywords = {
5596
+ 'JavaScript': ['var', 'let', 'const', 'this', 'static', 'class'],
5597
+ 'C++': [...CodeEditor.nativeTypes["C++"], 'const', 'auto', 'class', 'struct', 'namespace', 'enum', 'extern']
4751
5598
  };
4752
5599
 
4753
5600
  CodeEditor.keywords = {
@@ -4757,26 +5604,26 @@ CodeEditor.keywords = {
4757
5604
  'enum', 'type'],
4758
5605
  'C': ['int', 'float', 'double', 'long', 'short', 'char', 'const', 'void', 'true', 'false', 'auto', 'struct', 'typedef', 'signed', 'volatile', 'unsigned', 'static', 'extern', 'enum', 'register',
4759
5606
  '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',
5607
+ 'C++': [...CodeEditor.nativeTypes["C++"], 'const', 'static_cast', 'dynamic_cast', 'new', 'delete', 'true', 'false', 'auto', 'class', 'struct', 'typedef', 'nullptr',
4761
5608
  'NULL', 'signed', 'unsigned', 'namespace', 'enum', 'extern', 'union', 'sizeof', 'static', 'private', 'public'],
4762
5609
  'CMake': ['cmake_minimum_required', 'set', 'not', 'if', 'endif', 'exists', 'string', 'strequal', 'add_definitions', 'macro', 'endmacro', 'file', 'list', 'source_group', 'add_executable',
4763
5610
  'target_include_directories', 'set_target_properties', 'set_property', 'add_compile_options', 'add_link_options', 'include_directories', 'add_library', 'target_link_libraries',
4764
5611
  'target_link_options', 'add_subdirectory', 'add_compile_definitions', 'project', 'cache'],
4765
5612
  'JSON': ['true', 'false'],
4766
5613
  'GLSL': ['true', 'false', 'function', 'int', 'float', 'vec2', 'vec3', 'vec4', 'mat2x2', 'mat3x3', 'mat4x4', 'struct'],
4767
- 'CSS': ['body', 'html', 'canvas', 'div', 'input', 'span', '.'],
4768
- 'WGSL': ['var', 'let', 'true', 'false', 'fn', 'bool', 'u32', 'i32', 'f16', 'f32', 'vec2', 'vec3', 'vec4', 'vec2f', 'vec3f', 'vec4f', 'mat2x2f', 'mat3x3f', 'mat4x4f', 'array', 'atomic', 'struct',
4769
- 'sampler', 'sampler_comparison', 'texture_depth_2d', 'texture_depth_2d_array', 'texture_depth_cube', 'texture_depth_cube_array', 'texture_depth_multisampled_2d',
4770
- 'texture_external', 'texture_1d', 'texture_2d', 'texture_2d_array', 'texture_3d', 'texture_cube', 'texture_cube_array', 'texture_storage_1d', 'texture_storage_2d',
4771
- 'texture_storage_2d_array', 'texture_storage_3d', 'vec2u', 'vec3u', 'vec4u', 'ptr'],
5614
+ 'CSS': ['body', 'html', 'canvas', 'div', 'input', 'span', '.', 'table', 'tr', 'td', 'th', 'label', 'video', 'img', 'code', 'button', 'select', 'option', 'svg', 'media', 'all',
5615
+ 'i', 'a', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'last-child', 'tbody', 'pre', 'monospace', 'font-face'],
5616
+ 'WGSL': [...CodeEditor.nativeTypes["WGSL"], 'var', 'let', 'true', 'false', 'fn', 'atomic', 'struct', 'sampler_comparison', 'texture_depth_2d', 'texture_depth_2d_array', 'texture_depth_cube',
5617
+ 'texture_depth_cube_array', 'texture_depth_multisampled_2d', 'texture_external', 'texture_1d', 'texture_2d', 'texture_2d_array', 'texture_3d', 'texture_cube', 'texture_cube_array',
5618
+ 'texture_storage_1d', 'texture_storage_2d', 'texture_storage_2d_array', 'texture_storage_3d'],
4772
5619
  'Rust': ['as', 'const', 'crate', 'enum', 'extern', 'false', 'fn', 'impl', 'in', 'let', 'mod', 'move', 'mut', 'pub', 'ref', 'self', 'Self', 'static', 'struct', 'super', 'trait', 'true',
4773
5620
  'type', 'unsafe', 'use', 'where', 'abstract', 'become', 'box', 'final', 'macro', 'override', 'priv', 'typeof', 'unsized', 'virtual'],
4774
5621
  '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'],
5622
+ 'Batch': ['set', 'echo', 'off', 'del', 'defined', 'setlocal', 'enabledelayedexpansion', 'driverquery', 'print'],
4777
5623
  'HTML': ['html', 'meta', 'title', 'link', 'script', 'body', 'DOCTYPE', 'head', 'br', 'i', 'a', 'li', 'img', 'tr', 'td', 'h1', 'h2', 'h3', 'h4', 'h5'],
4778
5624
  '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'],
5625
+ 'PHP': ['const', 'function', 'array', 'new', 'int', 'string', '$this', 'public', 'null', 'private', 'protected', 'implements', 'class', 'use', 'namespace', 'abstract', 'clone', 'final',
5626
+ 'enum'],
4780
5627
  };
4781
5628
 
4782
5629
  CodeEditor.utils = { // These ones don't have hightlight, used as suggestions to autocomplete only...
@@ -4787,7 +5634,14 @@ CodeEditor.utils = { // These ones don't have hightlight, used as suggestions to
4787
5634
  'Python': ['abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'delattr', 'dict', 'dir', 'divmod',
4788
5635
  'enumerate', 'eval', 'exec', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance',
4789
5636
  'issubclass', 'iter', 'len', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'range', 'repr',
4790
- 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
5637
+ 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip'],
5638
+ 'CSS': [ ...Object.keys( document.body.style ).map( LX.toKebabCase ), 'block', 'inline', 'inline-block', 'flex', 'grid', 'none', 'inherit', 'initial', 'unset', 'revert', 'sticky',
5639
+ 'relative', 'absolute', 'fixed', 'static', 'auto', 'visible', 'hidden', 'scroll', 'clip', 'ellipsis', 'nowrap', 'wrap', 'break-word', 'solid', 'dashed', 'dotted', 'double',
5640
+ 'groove', 'ridge', 'inset', 'outset', 'left', 'right', 'center', 'top', 'bottom', 'start', 'end', 'justify', 'stretch', 'space-between', 'space-around', 'space-evenly',
5641
+ 'baseline', 'middle', 'normal', 'bold', 'lighter', 'bolder', 'italic', 'blur', 'uppercase', 'lowercase', 'capitalize', 'transparent', 'currentColor', 'pointer', 'default',
5642
+ 'move', 'grab', 'grabbing', 'not-allowed', 'none', 'cover', 'contain', 'repeat', 'no-repeat', 'repeat-x', 'repeat-y', 'round', 'space', 'linear-gradient', 'radial-gradient',
5643
+ 'conic-gradient', 'url', 'calc', 'min', 'max', 'clamp', 'red', 'blue', 'green', 'black', 'white', 'gray', 'silver', 'yellow', 'orange', 'purple', 'pink', 'cyan', 'magenta',
5644
+ 'lime', 'teal', 'navy', 'transparent', 'currentcolor', 'inherit', 'initial', 'unset', 'revert', 'none', 'auto', 'fit-content', 'min-content', 'max-content']
4791
5645
  };
4792
5646
 
4793
5647
  CodeEditor.types = {
@@ -4808,12 +5662,13 @@ CodeEditor.builtIn = {
4808
5662
  'JavaScript': ['document', 'console', 'window', 'navigator', 'performance'],
4809
5663
  'CSS': ['*', '!important'],
4810
5664
  'C++': ['vector', 'list', 'map'],
5665
+ 'WGSL': ['@vertex', '@fragment'],
4811
5666
  'HTML': ['type', 'xmlns', 'PUBLIC', 'http-equiv', 'src', 'style', 'lang', 'href', 'rel', 'content', 'xml', 'alt'], // attributes
4812
5667
  'Markdown': ['type', 'src', 'style', 'lang', 'href', 'rel', 'content', 'valign', 'alt'], // attributes
4813
5668
  'PHP': ['echo', 'print'],
4814
5669
  };
4815
5670
 
4816
- CodeEditor.statementsAndDeclarations = {
5671
+ CodeEditor.statements = {
4817
5672
  'JavaScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await'],
4818
5673
  'TypeScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await', 'as'],
4819
5674
  'CSS': ['@', 'import'],
@@ -4831,6 +5686,7 @@ CodeEditor.statementsAndDeclarations = {
4831
5686
 
4832
5687
  CodeEditor.symbols = {
4833
5688
  'JavaScript': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '??'],
5689
+ 'TypeScript': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '??'],
4834
5690
  'C': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '*', '-', '+'],
4835
5691
  'C++': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '::', '*', '-', '+'],
4836
5692
  'CMake': ['{', '}'],
@@ -4843,19 +5699,19 @@ CodeEditor.symbols = {
4843
5699
  'Batch': ['[', ']', '(', ')', '%'],
4844
5700
  'HTML': ['<', '>', '/'],
4845
5701
  'XML': ['<', '>', '/'],
4846
- 'PHP': ['{', '}', '(', ')'],
5702
+ 'PHP': ['[', ']', '{', '}', '(', ')'],
4847
5703
  };
4848
5704
 
4849
5705
  CodeEditor.REGISTER_LANGUAGE = function( name, options = {}, def, rules )
4850
5706
  {
4851
5707
  CodeEditor.languages[ name ] = options;
4852
5708
 
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}), {});
5709
+ if( def?.keywords ) CodeEditor.keywords[ name ] = new Set( def.keywords );
5710
+ if( def?.utils ) CodeEditor.utils[ name ] = new Set( def.utils );
5711
+ if( def?.types ) CodeEditor.types[ name ] = new Set( def.types );
5712
+ if( def?.builtIn ) CodeEditor.builtIn[ name ] = new Set( def.builtIn );
5713
+ if( def?.statements ) CodeEditor.statements[ name ] = new Set( def.statements );
5714
+ if( def?.symbols ) CodeEditor.symbols[ name ] = new Set( def.symbols );
4859
5715
 
4860
5716
  if( rules ) HighlightRules[ name ] = rules;
4861
5717
  };