lexgui 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,26 +6,13 @@ if(!LX) {
6
6
 
7
7
  LX.extensions.push( 'CodeEditor' );
8
8
 
9
- function swapElements( obj, a, b ) {
10
- [obj[a], obj[b]] = [obj[b], obj[a]];
11
- }
12
-
13
- function swapArrayElements( array, id0, id1 ) {
14
- [array[id0], array[id1]] = [array[id1], array[id0]];
15
- };
16
-
17
- function sliceChars( str, idx, n = 1 ) {
18
- return str.substr(0, idx) + str.substr(idx + n);
19
- }
20
-
21
- function firstNonspaceIndex( str ) {
22
- const index = str.search(/\S|$/)
23
- return index < str.length ? index : -1;
24
- }
25
-
26
- function strReverse( str ) {
27
- return str.split( "" ).reverse().join( "" );
28
- }
9
+ function swapElements( obj, a, b ) { [obj[a], obj[b]] = [obj[b], obj[a]]; }
10
+ function swapArrayElements( array, id0, id1 ) { [array[id0], array[id1]] = [array[id1], array[id0]]; };
11
+ function sliceChars( str, idx, n = 1 ) { return str.substr(0, idx) + str.substr(idx + n); }
12
+ function firstNonspaceIndex( str ) { const index = str.search(/\S|$/); return index < str.length ? index : -1; }
13
+ function strReverse( str ) { return str.split( "" ).reverse().join( "" ); }
14
+ function isLetter( c ){ return /[a-zA-Z]/.test( c ); };
15
+ function isSymbol( c ){ return /[^\w\s]/.test( c ); };
29
16
 
30
17
  function indexOfFrom( str, reg, from, reverse ) {
31
18
 
@@ -46,13 +33,13 @@ function indexOfFrom( str, reg, from, reverse ) {
46
33
  }
47
34
  }
48
35
 
49
- let ASYNC_ENABLED = true;
50
-
51
- function doAsync( fn, ms ) {
52
- if( ASYNC_ENABLED )
53
- setTimeout( fn, ms ?? 0 );
54
- else
55
- fn();
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;
56
43
  }
57
44
 
58
45
  class CodeSelection {
@@ -104,7 +91,6 @@ class CodeSelection {
104
91
 
105
92
  var domEl = document.createElement( 'div' );
106
93
  domEl.className = this.className;
107
-
108
94
  domEl._top = y * this.editor.lineHeight;
109
95
  domEl.style.top = domEl._top + "px";
110
96
  domEl._left = x * this.editor.charWidth;
@@ -156,6 +142,9 @@ class ScrollBar {
156
142
  static SCROLLBAR_VERTICAL = 1;
157
143
  static SCROLLBAR_HORIZONTAL = 2;
158
144
 
145
+ static SCROLLBAR_VERTICAL_WIDTH = 10;
146
+ static SCROLLBAR_HORIZONTAL_HEIGHT = 10;
147
+
159
148
  constructor( editor, type ) {
160
149
 
161
150
  this.editor = editor;
@@ -213,6 +202,80 @@ class ScrollBar {
213
202
 
214
203
  }
215
204
 
205
+ /* Highlight rules
206
+ - test: function that receives a context object and returns true or false
207
+ - className: class to apply if test is true
208
+ - action: optional function to execute if test is true, receives context and editor as parameter
209
+ - discard: optional boolean, if true the token is discarded, action value is returned
210
+ to "ctx.discardToken" and no class is applied
211
+ */
212
+
213
+ const HighlightRules = {
214
+
215
+ common: [
216
+ { test: ctx => ctx.inBlockComment, className: "cm-com" },
217
+ { test: ctx => ctx.inString, action: (ctx, editor) => editor._appendStringToken( ctx.token ), discard: true },
218
+ { test: ctx => ctx.token.substr( 0, ctx.singleLineCommentToken.length ) == ctx.singleLineCommentToken, className: "cm-com" },
219
+ { test: (ctx, editor) => editor._isKeyword( ctx ), className: "cm-kwd" },
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" },
221
+ { test: (ctx, editor) => editor._mustHightlightWord( ctx.token, CodeEditor.statements, ctx.lang ), className: "cm-std" },
222
+ { test: (ctx, editor) => editor._mustHightlightWord( ctx.token, CodeEditor.symbols, ctx.lang ), className: "cm-sym" },
223
+ { test: (ctx, editor) => editor._mustHightlightWord( ctx.token, CodeEditor.types, ctx.lang ), className: "cm-typ" },
224
+ { test: (ctx, editor) => editor._isNumber( ctx.token ) || editor._isNumber( ctx.token.replace(/[px]|[em]|%/g,'') ), className: "cm-dec" },
225
+ { test: ctx => ctx.lang.usePreprocessor && ctx.token.includes( '#' ), className: "cm-ppc" },
226
+ ],
227
+
228
+ javascript: [
229
+ { test: ctx => (ctx.prev === 'class' && ctx.next === '{') , className: "cm-typ" },
230
+ ],
231
+
232
+ typescript: [
233
+ { test: ctx => ctx.scope && (ctx.token !== ',' && ctx.scope.type == "enum"), className: "cm-enu" },
234
+ { test: ctx => (ctx.prev === ':' && ctx.next !== undefined && isLetter(ctx.token) ) || (ctx.prev === 'interface' && ctx.next === '{') || (ctx.prev === 'enum' && ctx.next === '{'), className: "cm-typ" },
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" },
236
+ { test: (ctx, editor) => ctx.token !== ',' && editor._enclosedByTokens( ctx.token, ctx.tokenIndex, '<', '>' ), className: "cm-typ" },
237
+ ],
238
+
239
+ cpp: [
240
+ { test: ctx => ctx.scope && (ctx.token !== ',' && ctx.scope.type == "enum"), className: "cm-enu" },
241
+ { test: ctx => ctx.isEnumValueSymbol( ctx.token ), className: "cm-enu" },
242
+ { test: ctx => (ctx.prev === 'class' && ctx.next === '{') || (ctx.prev === 'struct' && ctx.next === '{'), className: "cm-typ" },
243
+ { test: ctx => ctx.prev === "<" && (ctx.next === ">" || ctx.next === "*"), className: "cm-typ" }, // Defining template type in C++
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" },
246
+ ],
247
+
248
+ wgsl: [
249
+ { test: ctx => ctx.prev === '>' && (!ctx.next || ctx.next === '{'), className: "cm-typ" }, // Function return type
250
+ { test: ctx => (ctx.prev === ':' && ctx.next !== undefined) || (ctx.prev === 'struct' && ctx.next === '{'), className: "cm-typ" },
251
+ { test: (ctx, editor) => ctx.token !== ',' && editor._enclosedByTokens( ctx.token, ctx.tokenIndex, '<', '>' ), className: "cm-typ" },
252
+ ],
253
+
254
+ css: [
255
+ { test: ctx => ( ctx.prev == '.' || ctx.prev == '::' || ( ctx.prev == ':' && ctx.next == '{' ) || ( ctx.token[ 0 ] == '#' && ctx.prev != ':' ) ), className: "cm-kwd" },
256
+ { test: ctx => ctx.prev === ':' && (ctx.next === ';' || ctx.next === '!important'), className: "cm-str" }, // CSS value
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
259
+ ],
260
+
261
+ batch: [
262
+ { test: ctx => ctx.token === '@' || ctx.prev === ':' || ctx.prev === '@', className: "cm-kwd" }
263
+ ],
264
+
265
+ markdown: [
266
+ { test: ctx => ctx.isFirstToken && ctx.token.replaceAll('#', '').length != ctx.token.length, action: (ctx, editor) => editor._markdownHeader = true, className: "cm-kwd" }
267
+ ],
268
+
269
+ php: [
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" },
272
+ ],
273
+
274
+ post_common: [
275
+ { test: ctx => isLetter(ctx.token) && (ctx.token[ 0 ] != '@') && (ctx.token[ 0 ] != ',') && (ctx.next === '('), className: "cm-mtd" }
276
+ ],
277
+ };
278
+
216
279
  /**
217
280
  * @class CodeEditor
218
281
  */
@@ -238,8 +301,13 @@ class CodeEditor {
238
301
  static CODE_MIN_FONT_SIZE = 9;
239
302
  static CODE_MAX_FONT_SIZE = 22;
240
303
 
304
+ static LINE_GUTTER_WIDTH = 48;
241
305
  static LINE_GUTTER_WIDTH = 48;
242
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
+
243
311
  /**
244
312
  * @param {*} options
245
313
  * name:
@@ -255,11 +323,17 @@ class CodeEditor {
255
323
 
256
324
  CodeEditor.__instances.push( this );
257
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
+
258
332
  // File explorer
259
- if( options.fileExplorer ?? false )
333
+ if( this.useFileExplorer )
260
334
  {
261
- var [ explorerArea, codeArea ] = area.split({ sizes:[ "15%","85%" ] });
262
- explorerArea.setLimitBox( 180, 20, 512 );
335
+ let [ explorerArea, editorArea ] = area.split({ sizes:[ "15%","85%" ] });
336
+ // explorerArea.setLimitBox( 180, 20, 512 );
263
337
  this.explorerArea = explorerArea;
264
338
 
265
339
  let panel = new LX.Panel();
@@ -310,35 +384,73 @@ class CodeEditor {
310
384
  explorerArea.attach( panel );
311
385
 
312
386
  // Update area
313
- area = codeArea;
387
+ area = editorArea;
314
388
  }
315
389
 
316
- this.base_area = area;
390
+ this.baseArea = area;
317
391
  this.area = new LX.Area( { className: "lexcodeeditor", height: "100%", skipAppend: true } );
318
392
 
319
- this.skipInfo = options.skipInfo ?? false;
320
- 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
+ } } );
321
403
 
322
- this._tabStorage = {};
323
- this.tabs = this.area.addTabs( { onclose: (name) => {
324
- delete this.openedTabs[ name ];
325
- if( Object.keys( this.openedTabs ).length < 2 )
404
+ if( !this.disableEdition )
326
405
  {
327
- clearInterval( this.blinker );
328
- 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
+ } );
329
413
  }
330
- } } );
331
414
 
332
- if( !this.disableEdition )
415
+ this.codeArea = this.tabs.area;
416
+ }
417
+ else
333
418
  {
334
- this.tabs.root.addEventListener( 'dblclick', (e) => {
335
- if( options.allowAddScripts ?? true ) {
336
- e.preventDefault();
337
- 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"
338
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
+ } );
445
+ }
446
+
447
+ new LX.DropdownMenu( this._loadFileButton, dropdownOptions, { side: "top", align: "center" });
448
+
339
449
  } );
340
450
  }
341
451
 
452
+ this.codeArea.root.classList.add( 'lexcodearea' );
453
+
342
454
  // Full editor
343
455
  area.root.classList.add('codebasearea');
344
456
 
@@ -354,9 +466,6 @@ class CodeEditor {
354
466
  attributeFilter: ['class', 'style'],
355
467
  });
356
468
 
357
- // Code area
358
- this.tabs.area.root.classList.add( 'codetabsarea' );
359
-
360
469
  this.root = this.area.root;
361
470
  this.root.tabIndex = -1;
362
471
  area.attach( this.root );
@@ -385,12 +494,12 @@ class CodeEditor {
385
494
 
386
495
  this.cursors = document.createElement( 'div' );
387
496
  this.cursors.className = 'cursors';
388
- this.tabs.area.attach( this.cursors );
497
+ this.codeArea.attach( this.cursors );
389
498
 
390
499
  this.searchResultSelections = document.createElement( 'div' );
391
500
  this.searchResultSelections.id = 'search-selections';
392
501
  this.searchResultSelections.className = 'selections';
393
- this.tabs.area.attach( this.searchResultSelections );
502
+ this.codeArea.attach( this.searchResultSelections );
394
503
 
395
504
  // Store here selections per cursor
396
505
  this.selections = {};
@@ -403,7 +512,7 @@ class CodeEditor {
403
512
 
404
513
  // Scroll stuff
405
514
  {
406
- this.codeScroller = this.tabs.area.root;
515
+ this.codeScroller = this.codeArea.root;
407
516
  this.firstLineInViewport = 0;
408
517
  this.lineScrollMargin = new LX.vec2( 20, 20 ); // [ mUp, mDown ]
409
518
 
@@ -428,12 +537,14 @@ class CodeEditor {
428
537
  {
429
538
  if( this.visibleLinesViewport.y < (this.code.lines.length - 1) )
430
539
  {
431
- const totalLinesInViewport = ((this.codeScroller.offsetHeight - 36) / this.lineHeight)|0;
540
+ const totalLinesInViewport = ( ( this.codeScroller.offsetHeight ) / this.lineHeight )|0;
432
541
  const scrollDownBoundary =
433
542
  ( Math.max( this.visibleLinesViewport.y - totalLinesInViewport, 0 ) - 1 ) * this.lineHeight;
434
543
 
435
544
  if( scrollTop >= scrollDownBoundary )
545
+ {
436
546
  this.processLines( CodeEditor.UPDATE_VISIBLE_LINES );
547
+ }
437
548
  }
438
549
  }
439
550
  // Scroll up...
@@ -441,7 +552,9 @@ class CodeEditor {
441
552
  {
442
553
  const scrollUpBoundary = parseInt( this.code.style.top );
443
554
  if( scrollTop < scrollUpBoundary )
555
+ {
444
556
  this.processLines( CodeEditor.UPDATE_VISIBLE_LINES );
557
+ }
445
558
  }
446
559
 
447
560
  lastScrollTopValue = scrollTop;
@@ -451,9 +564,13 @@ class CodeEditor {
451
564
  if( e.ctrlKey )
452
565
  {
453
566
  e.preventDefault();
567
+ e.stopPropagation();
454
568
  ( e.deltaY > 0.0 ? this._decreaseFontSize() : this._increaseFontSize() );
455
569
  }
456
- else
570
+ } );
571
+
572
+ this.codeScroller.addEventListener( 'wheel', e => {
573
+ if( !e.ctrlKey )
457
574
  {
458
575
  const dX = ( e.deltaY > 0.0 ? 10.0 : -10.0 ) * ( e.shiftKey ? 1.0 : 0.0 );
459
576
  if( dX != 0.0 ) this.setScrollBarValue( 'horizontal', dX );
@@ -462,50 +579,47 @@ class CodeEditor {
462
579
  }
463
580
  }
464
581
 
465
- // This is only the container, line numbers are in the same line div
582
+ // Line numbers and scrollbars
466
583
  {
584
+ // This is only the container, line numbers are in the same line div
467
585
  this.gutter = document.createElement( 'div' );
468
586
  this.gutter.className = "lexcodegutter";
469
587
  area.attach( this.gutter );
470
- }
471
588
 
472
- // Add custom vertical scroll bar
473
- {
589
+ // Add custom vertical scroll bar
474
590
  this.vScrollbar = new ScrollBar( this, ScrollBar.SCROLLBAR_VERTICAL );
475
591
  area.attach( this.vScrollbar.root );
476
- }
477
592
 
478
- // Add custom horizontal scroll bar
479
- {
593
+ // Add custom horizontal scroll bar
480
594
  this.hScrollbar = new ScrollBar( this, ScrollBar.SCROLLBAR_HORIZONTAL );
481
595
  area.attach( this.hScrollbar.root );
482
596
  }
483
597
 
598
+ // Add autocomplete, search boxes (IF edition enabled)
484
599
  if( !this.disableEdition )
485
600
  {
486
601
  // Add autocomplete box
487
602
  {
488
- var box = document.createElement( 'div' );
603
+ const box = document.createElement( 'div' );
489
604
  box.className = "autocomplete";
490
605
  this.autocomplete = box;
491
- this.tabs.area.attach( box );
492
-
606
+ this.codeArea.attach( box );
493
607
  this.isAutoCompleteActive = false;
494
608
  }
495
609
 
496
610
  // Add search box
497
611
  {
498
- var box = document.createElement( 'div' );
612
+ const box = document.createElement( 'div' );
499
613
  box.className = "searchbox";
500
614
 
501
- var searchPanel = new LX.Panel();
615
+ const searchPanel = new LX.Panel();
502
616
  box.appendChild( searchPanel.root );
503
617
 
504
618
  searchPanel.sameLine( 4 );
505
619
  searchPanel.addText( null, "", null, { placeholder: "Find" } );
506
- searchPanel.addButton( null, "up", () => this.search( null, true ), { icon: "ArrowUp" } );
507
- searchPanel.addButton( null, "down", () => this.search(), { icon: "ArrowDown" } );
508
- searchPanel.addButton( null, "x", this.hideSearchBox.bind( this ), { icon: "X" } );
620
+ searchPanel.addButton( null, "up", () => this.search( null, true ), { icon: "ArrowUp", title: "Previous Match", tooltip: true } );
621
+ searchPanel.addButton( null, "down", () => this.search(), { icon: "ArrowDown", title: "Next Match", tooltip: true } );
622
+ searchPanel.addButton( null, "x", this.hideSearchBox.bind( this ), { icon: "X", title: "Close", tooltip: true } );
509
623
 
510
624
  box.querySelector( 'input' ).addEventListener( 'keyup', e => {
511
625
  if( e.key == 'Escape' ) this.hideSearchBox();
@@ -513,7 +627,7 @@ class CodeEditor {
513
627
  } );
514
628
 
515
629
  this.searchbox = box;
516
- this.tabs.area.attach( box );
630
+ this.codeArea.attach( box );
517
631
  }
518
632
 
519
633
  // Add search LINE box
@@ -535,7 +649,7 @@ class CodeEditor {
535
649
  } );
536
650
 
537
651
  this.searchlinebox = box;
538
- this.tabs.area.attach( box );
652
+ this.codeArea.attach( box );
539
653
  }
540
654
  }
541
655
 
@@ -546,7 +660,9 @@ class CodeEditor {
546
660
 
547
661
  // Append all childs
548
662
  while( this.codeScroller.firstChild )
663
+ {
549
664
  this.codeSizer.appendChild( this.codeScroller.firstChild );
665
+ }
550
666
 
551
667
  this.codeScroller.appendChild( this.codeSizer );
552
668
  }
@@ -562,7 +678,6 @@ class CodeEditor {
562
678
 
563
679
  // Code
564
680
 
565
- this.useAutoComplete = options.autocomplete ?? true;
566
681
  this.highlight = options.highlight ?? 'Plain Text';
567
682
  this.onsave = options.onsave ?? ((code) => { console.log( code, "save" ) });
568
683
  this.onrun = options.onrun ?? ((code) => { this.runScript(code) });
@@ -575,6 +690,7 @@ class CodeEditor {
575
690
  this.defaultSingleLineCommentToken = '//';
576
691
  this.defaultBlockCommentTokens = [ '/*', '*/' ];
577
692
  this._lastTime = null;
693
+ this._tabStorage = {};
578
694
 
579
695
  this.pairKeys = {
580
696
  "\"": "\"",
@@ -592,24 +708,6 @@ class CodeEditor {
592
708
  // Scan tokens..
593
709
  // setInterval( this.scanWordSuggestions.bind( this ), 2000 );
594
710
 
595
- this.languages = {
596
- 'Plain Text': { ext: 'txt', blockComments: false, singleLineComments: false },
597
- 'JavaScript': { ext: 'js' },
598
- 'C': { ext: [ 'c', 'h' ] },
599
- 'C++': { ext: [ 'cpp', 'hpp' ] },
600
- 'CSS': { ext: 'css' },
601
- 'CMake': { ext: 'cmake', singleLineCommentToken: '#', blockComments: false, ignoreCase: true },
602
- 'GLSL': { ext: 'glsl' },
603
- 'WGSL': { ext: 'wgsl' },
604
- 'JSON': { ext: 'json', blockComments: false, singleLineComments: false },
605
- 'XML': { ext: 'xml', tags: true },
606
- 'Rust': { ext: 'rs' },
607
- 'Python': { ext: 'py', singleLineCommentToken: '#' },
608
- 'HTML': { ext: 'html', tags: true, singleLineComments: false, blockCommentsTokens: [ '<!--', '-->' ] },
609
- 'Batch': { ext: 'bat', blockComments: false, singleLineCommentToken: '::' },
610
- 'Markdown': { ext: 'md', blockComments: false, singleLineCommentToken: '::', tags: true }
611
- };
612
-
613
711
  this.specialKeys = [
614
712
  'Backspace', 'Enter', 'ArrowUp', 'ArrowDown',
615
713
  'ArrowRight', 'ArrowLeft', 'Delete', 'Home',
@@ -622,476 +720,485 @@ class CodeEditor {
622
720
 
623
721
  if( !CodeEditor._staticReady )
624
722
  {
625
- for( let lang in CodeEditor.keywords ) CodeEditor.keywords[lang] = CodeEditor.keywords[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
626
- for( let lang in CodeEditor.utils ) CodeEditor.utils[lang] = CodeEditor.utils[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
627
- for( let lang in CodeEditor.types ) CodeEditor.types[lang] = CodeEditor.types[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
628
- for( let lang in CodeEditor.builtIn ) CodeEditor.builtIn[lang] = CodeEditor.builtIn[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
629
- for( let lang in CodeEditor.statementsAndDeclarations ) CodeEditor.statementsAndDeclarations[lang] = CodeEditor.statementsAndDeclarations[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
630
- 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] );
631
729
 
632
730
  CodeEditor._staticReady = true;
633
731
  }
634
732
 
635
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
+ });
636
744
 
637
- this.action( 'Escape', false, ( ln, cursor, e ) => {
638
- if( this.hideAutoCompleteBox() )
639
- return;
640
- if( this.hideSearchBox() )
641
- return;
642
- // Remove selections and cursors
643
- this.endSelection();
644
- this._removeSecondaryCursors();
645
- });
646
-
647
- this.action( 'Backspace', false, ( ln, cursor, e ) => {
745
+ this.action( 'Backspace', false, ( ln, cursor, e ) => {
648
746
 
649
- this._addUndoStep( cursor );
747
+ this._addUndoStep( cursor );
650
748
 
651
- if( cursor.selection ) {
652
- this.deleteSelection( cursor );
653
- // Remove entire line when selecting with triple click
654
- if( this._tripleClickSelection )
749
+ if( cursor.selection )
655
750
  {
656
- this.actions['Backspace'].callback( ln, cursor, e );
657
- 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
+ }
658
758
  }
659
- }
660
- else {
759
+ else {
661
760
 
662
- var letter = this.getCharAtPos( cursor, -1 );
663
- if( letter ) {
664
-
665
- var deleteFromPosition = cursor.position - 1;
666
- var numCharsDeleted = 1;
667
-
668
- // Delete full word
669
- if( e.shiftKey )
761
+ var letter = this.getCharAtPos( cursor, -1 );
762
+ if( letter )
670
763
  {
671
- const [word, from, to] = this.getWordAtPos( cursor, -1 );
764
+ var deleteFromPosition = cursor.position - 1;
765
+ var numCharsDeleted = 1;
672
766
 
673
- if( word.length > 1 )
767
+ // Delete full word
768
+ if( e.shiftKey )
674
769
  {
675
- deleteFromPosition = from;
676
- 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
+ }
677
777
  }
678
- }
679
778
 
680
- this.code.lines[ ln ] = sliceChars( this.code.lines[ ln ], deleteFromPosition, numCharsDeleted );
681
- this.processLine( ln );
779
+ this.code.lines[ ln ] = sliceChars( this.code.lines[ ln ], deleteFromPosition, numCharsDeleted );
780
+ this.processLine( ln );
682
781
 
683
- this.cursorToPosition( cursor, deleteFromPosition );
782
+ this.cursorToPosition( cursor, deleteFromPosition );
684
783
 
685
- if( this.useAutoComplete )
784
+ if( this.useAutoComplete )
785
+ {
786
+ this.showAutoCompleteBox( 'foo', cursor );
787
+ }
788
+ }
789
+ else if( this.code.lines[ ln - 1 ] != undefined )
686
790
  {
687
- 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();
688
798
  }
689
799
  }
690
- else if( this.code.lines[ ln - 1 ] != undefined ) {
691
-
692
- this.lineUp( cursor );
693
- e.cancelShift = true;
694
- this.actions[ 'End' ].callback( cursor.line, cursor, e );
695
- // Move line on top
696
- this.code.lines[ ln - 1 ] += this.code.lines[ ln ];
697
- this.code.lines.splice( ln, 1 );
698
- this.processLines();
699
- }
700
- }
701
- });
702
800
 
703
- this.action( 'Delete', false, ( ln, cursor, e ) => {
704
-
705
- this._addUndoStep( cursor );
706
-
707
- if( cursor.selection ) {
708
- // Use 'Backspace' as it's the same callback...
709
- this.actions['Backspace'].callback( ln, cursor, e );
710
- }
711
- else
712
- {
713
- var letter = this.getCharAtPos( cursor );
714
- if( letter ) {
715
- this.code.lines[ ln ] = sliceChars( this.code.lines[ ln ], cursor.position );
716
- this.processLine( ln );
717
- }
718
- else if( this.code.lines[ ln + 1 ] != undefined ) {
719
- this.code.lines[ ln ] += this.code.lines[ ln + 1 ];
720
- this.code.lines.splice( ln + 1, 1 );
721
- this.processLines();
722
- }
723
- }
724
- });
801
+ this.resizeIfNecessary( cursor, true );
802
+ });
725
803
 
726
- this.action( 'Tab', true, ( ln, cursor, e ) => {
804
+ this.action( 'Delete', false, ( ln, cursor, e ) => {
727
805
 
728
- if( this._skipTabs )
729
- {
730
- this._skipTabs--;
731
- if( !this._skipTabs )
732
- delete this._skipTabs;
733
- }
734
- else if( this.isAutoCompleteActive )
735
- {
736
- this.autoCompleteWord();
737
- }
738
- else
739
- {
740
806
  this._addUndoStep( cursor );
741
807
 
742
- if( e && e.shiftKey )
808
+ if( cursor.selection )
743
809
  {
744
- this._removeSpaces( cursor );
810
+ // Use 'Backspace' as it's the same callback...
811
+ this.actions['Backspace'].callback( ln, cursor, e );
745
812
  }
746
813
  else
747
814
  {
748
- const indentSpaces = this.tabSpaces - (cursor.position % this.tabSpaces);
749
- 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
+ }
750
827
  }
751
- }
752
- }, "shiftKey");
753
-
754
- this.action( 'Home', false, ( ln, cursor, e ) => {
755
-
756
- let idx = firstNonspaceIndex( this.code.lines[ ln ] );
757
828
 
758
- // We already are in the first non space index...
759
- if( idx == cursor.position ) idx = 0;
760
-
761
- const prestring = this.code.lines[ ln ].substring( 0, idx );
762
- let lastX = cursor.position;
763
-
764
- this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
765
- if( idx > 0 )
766
- {
767
- this.cursorToString( cursor, prestring );
768
- }
769
- else
770
- {
771
- // No spaces, start from char 0
772
- idx = 0;
773
- }
774
-
775
- this.setScrollLeft( 0 );
776
- this.mergeCursors( ln );
829
+ this.resizeIfNecessary( cursor, true );
830
+ });
777
831
 
778
- if( e.shiftKey && !e.cancelShift )
779
- {
780
- // Get last selection range
781
- if( cursor.selection )
782
- {
783
- lastX += cursor.selection.chars;
784
- }
832
+ this.action( 'Tab', true, ( ln, cursor, e ) => {
785
833
 
786
- if( !cursor.selection )
834
+ if( this._skipTabs )
787
835
  {
788
- this.startSelection( cursor );
836
+ this._skipTabs--;
837
+ if( !this._skipTabs )
838
+ delete this._skipTabs;
789
839
  }
790
-
791
- var string = this.code.lines[ ln ].substring( idx, lastX );
792
- if( cursor.selection.sameLine() )
840
+ else if( this.isAutoCompleteActive )
793
841
  {
794
- cursor.selection.selectInline( cursor, idx, cursor.line, this.measureString( string ) );
842
+ this.autoCompleteWord();
795
843
  }
796
844
  else
797
845
  {
798
- 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
+ }
799
857
  }
800
- } else if( !e.keepSelection )
801
- this.endSelection();
802
- });
858
+ }, "shiftKey");
803
859
 
804
- this.action( 'End', false, ( ln, cursor, e ) => {
860
+ this.action( 'Home', false, ( ln, cursor, e ) => {
805
861
 
806
- if( ( e.shiftKey || e._shiftKey ) && !e.cancelShift ) {
862
+ let idx = firstNonspaceIndex( this.code.lines[ ln ] );
807
863
 
808
- var string = this.code.lines[ ln ].substring( cursor.position );
809
- if( !cursor.selection )
810
- this.startSelection( cursor );
811
- if( cursor.selection.sameLine() )
812
- 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
+ }
813
875
  else
814
876
  {
815
- this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
816
- this.cursorToString( cursor, this.code.lines[ ln ] );
817
- this._processSelection( cursor, e );
877
+ // No spaces, start from char 0
878
+ idx = 0;
818
879
  }
819
- } else if( !e.keepSelection )
820
- this.endSelection();
821
-
822
- this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
823
- this.cursorToString( cursor, this.code.lines[ ln ] );
824
-
825
- var viewportSizeX = ( this.codeScroller.clientWidth + this.getScrollLeft() ) - CodeEditor.LINE_GUTTER_WIDTH; // Gutter offset
826
- if( ( cursor.position * this.charWidth ) >= viewportSizeX )
827
- this.setScrollLeft( this.code.lines[ ln ].length * this.charWidth );
828
880
 
829
- // Merge cursors
830
- this.mergeCursors( ln );
831
- });
881
+ this.setScrollLeft( 0 );
882
+ this.mergeCursors( ln );
832
883
 
833
- this.action( 'Enter', true, ( ln, cursor, e ) => {
884
+ if( e.shiftKey && !e.cancelShift )
885
+ {
886
+ // Get last selection range
887
+ if( cursor.selection )
888
+ {
889
+ lastX += cursor.selection.chars;
890
+ }
834
891
 
835
- // Add word
836
- if( this.isAutoCompleteActive )
837
- {
838
- this.autoCompleteWord();
839
- return;
840
- }
892
+ if( !cursor.selection )
893
+ {
894
+ this.startSelection( cursor );
895
+ }
841
896
 
842
- if( e.ctrlKey )
843
- {
844
- this.onrun( this.getText() );
845
- return;
846
- }
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
+ });
847
909
 
848
- this._addUndoStep( cursor, true );
910
+ this.action( 'End', false, ( ln, cursor, e ) => {
849
911
 
850
- var _c0 = this.getCharAtPos( cursor, -1 );
851
- var _c1 = this.getCharAtPos( cursor );
912
+ if( ( e.shiftKey || e._shiftKey ) && !e.cancelShift ) {
852
913
 
853
- this.code.lines.splice( cursor.line + 1, 0, "" );
854
- this.code.lines[cursor.line + 1] = this.code.lines[ ln ].substr( cursor.position ); // new line (below)
855
- this.code.lines[ ln ] = this.code.lines[ ln ].substr( 0, cursor.position ); // line above
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();
856
927
 
857
- this.lineDown( cursor, true );
928
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
929
+ this.cursorToString( cursor, this.code.lines[ ln ] );
858
930
 
859
- // Check indentation
860
- var spaces = firstNonspaceIndex( this.code.lines[ ln ]);
861
- var tabs = Math.floor( spaces / this.tabSpaces );
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 );
862
934
 
863
- if( _c0 == '{' && _c1 == '}' ) {
864
- this.code.lines.splice( cursor.line, 0, "" );
865
- this._addSpaceTabs( cursor, tabs + 1 );
866
- this.code.lines[ cursor.line + 1 ] = " ".repeat(spaces) + this.code.lines[ cursor.line + 1 ];
867
- } else {
868
- this._addSpaceTabs( cursor, tabs );
869
- }
935
+ // Merge cursors
936
+ this.mergeCursors( ln );
937
+ });
870
938
 
871
- this.processLines();
872
- });
939
+ this.action( 'Enter', true, ( ln, cursor, e ) => {
873
940
 
874
- this.action( 'ArrowUp', false, ( ln, cursor, e ) => {
941
+ // Add word
942
+ if( this.isAutoCompleteActive )
943
+ {
944
+ this.autoCompleteWord();
945
+ return;
946
+ }
875
947
 
876
- // Move cursor..
877
- if( !this.isAutoCompleteActive )
878
- {
879
- if( e.shiftKey ) {
880
- if( !cursor.selection )
881
- this.startSelection( cursor );
948
+ if( e.ctrlKey )
949
+ {
950
+ this.onrun( this.getText() );
951
+ return;
952
+ }
882
953
 
883
- this.lineUp( cursor );
954
+ this._addUndoStep( cursor, true );
884
955
 
885
- var letter = this.getCharAtPos( cursor );
886
- if( !letter ) {
887
- this.cursorToPosition( cursor, this.code.lines[ cursor.line ].length );
888
- }
956
+ var _c0 = this.getCharAtPos( cursor, -1 );
957
+ var _c1 = this.getCharAtPos( cursor );
889
958
 
890
- this._processSelection( cursor, e, false );
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
891
962
 
892
- } else {
893
- this.endSelection();
894
- this.lineUp( cursor );
895
- // Go to end of line if out of line
896
- var letter = this.getCharAtPos( cursor );
897
- if( !letter ) this.actions['End'].callback( cursor.line, cursor, e );
898
- }
899
- }
900
- // Move up autocomplete selection
901
- else
902
- {
903
- this._moveArrowSelectedAutoComplete('up');
904
- }
905
- });
963
+ this.lineDown( cursor, true );
906
964
 
907
- this.action( 'ArrowDown', false, ( ln, cursor, e ) => {
965
+ // Check indentation
966
+ var spaces = firstNonspaceIndex( this.code.lines[ ln ]);
967
+ var tabs = Math.floor( spaces / this.tabSpaces );
908
968
 
909
- // Move cursor..
910
- if( !this.isAutoCompleteActive )
911
- {
912
- if( e.shiftKey ) {
913
- if( !cursor.selection )
914
- this.startSelection( cursor );
969
+ if( _c0 == '{' && _c1 == '}' ) {
970
+ this.code.lines.splice( cursor.line, 0, "" );
971
+ this._addSpaceTabs( cursor, tabs + 1 );
972
+ this.code.lines[ cursor.line + 1 ] = " ".repeat(spaces) + this.code.lines[ cursor.line + 1 ];
915
973
  } else {
916
- this.endSelection();
974
+ this._addSpaceTabs( cursor, tabs );
917
975
  }
918
976
 
919
- const canGoDown = this.lineDown( cursor );
920
- const letter = this.getCharAtPos( cursor );
977
+ this.processLines();
978
+ });
921
979
 
922
- // Go to end of line if out of range
923
- if( !letter || !canGoDown ) {
924
- this.cursorToPosition( cursor, Math.max(this.code.lines[ cursor.line ].length, 0) );
925
- }
980
+ this.action( 'ArrowUp', false, ( ln, cursor, e ) => {
926
981
 
927
- if( e.shiftKey ) {
928
- this._processSelection( cursor, e );
929
- }
930
- }
931
- // Move down autocomplete selection
932
- else
933
- {
934
- this._moveArrowSelectedAutoComplete('down');
935
- }
936
- });
982
+ // Move cursor..
983
+ if( !this.isAutoCompleteActive )
984
+ {
985
+ if( e.shiftKey ) {
986
+ if( !cursor.selection )
987
+ this.startSelection( cursor );
937
988
 
938
- this.action( 'ArrowLeft', false, ( ln, cursor, e ) => {
989
+ this.lineUp( cursor );
939
990
 
940
- // Nothing to do..
941
- if( cursor.line == 0 && cursor.position == 0 )
942
- return;
991
+ var letter = this.getCharAtPos( cursor );
992
+ if( !letter ) {
993
+ this.cursorToPosition( cursor, this.code.lines[ cursor.line ].length );
994
+ }
943
995
 
944
- if( e.metaKey ) { // Apple devices (Command)
945
- e.preventDefault();
946
- this.actions[ 'Home' ].callback( ln, cursor, e );
947
- }
948
- else if( e.ctrlKey ) {
949
- // Get next word
950
- const [word, from, to] = this.getWordAtPos( cursor, -1 );
951
- // If no length, we change line..
952
- if( !word.length && this.lineUp( cursor, true ) ) {
953
- const cS = e.cancelShift, kS = e.keepSelection;
954
- e.cancelShift = true;
955
- e.keepSelection = true;
956
- this.actions[ 'End' ].callback( cursor.line, cursor, e );
957
- e.cancelShift = cS;
958
- e.keepSelection = kS;
959
- }
960
- var diff = Math.max( cursor.position - from, 1 );
961
- var substr = word.substr( 0, diff );
996
+ this._processSelection( cursor, e, false );
962
997
 
963
- // Selections...
964
- if( e.shiftKey ) {
965
- if( !cursor.selection )
966
- this.startSelection( cursor );
998
+ } else {
999
+ this.endSelection();
1000
+ this.lineUp( cursor );
1001
+ // Go to end of line if out of line
1002
+ var letter = this.getCharAtPos( cursor );
1003
+ if( !letter ) this.actions['End'].callback( cursor.line, cursor, e );
1004
+ }
967
1005
  }
1006
+ // Move up autocomplete selection
968
1007
  else
969
- this.endSelection();
1008
+ {
1009
+ this._moveArrowSelectedAutoComplete('up');
1010
+ }
1011
+ });
970
1012
 
971
- this.cursorToString( cursor, substr, true );
1013
+ this.action( 'ArrowDown', false, ( ln, cursor, e ) => {
972
1014
 
973
- if( e.shiftKey )
974
- this._processSelection( cursor, e );
975
- }
976
- else {
977
- var letter = this.getCharAtPos( cursor, -1 );
978
- if( letter ) {
1015
+ // Move cursor..
1016
+ if( !this.isAutoCompleteActive )
1017
+ {
979
1018
  if( e.shiftKey ) {
980
- if( !cursor.selection ) this.startSelection( cursor );
981
- this.cursorToLeft( letter, cursor );
982
- this._processSelection( cursor, e, false, CodeEditor.SELECTION_X );
983
- }
984
- else {
985
- if( !cursor.selection ) {
986
- this.cursorToLeft( letter, cursor );
987
- if( this.useAutoComplete && this.isAutoCompleteActive )
988
- this.showAutoCompleteBox( 'foo', cursor );
989
- }
990
- else {
991
- cursor.selection.invertIfNecessary();
992
- this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, cursor );
993
- this.cursorToLine( cursor, cursor.selection.fromY, true );
994
- this.cursorToPosition( cursor, cursor.selection.fromX );
995
- this.endSelection();
996
- }
1019
+ if( !cursor.selection )
1020
+ this.startSelection( cursor );
1021
+ } else {
1022
+ this.endSelection();
997
1023
  }
998
- }
999
- else if( cursor.line > 0 ) {
1000
1024
 
1001
- if( e.shiftKey && !cursor.selection ) this.startSelection( cursor );
1025
+ const canGoDown = this.lineDown( cursor );
1026
+ const letter = this.getCharAtPos( cursor );
1002
1027
 
1003
- this.lineUp( cursor );
1028
+ // Go to end of line if out of range
1029
+ if( !letter || !canGoDown ) {
1030
+ this.cursorToPosition( cursor, Math.max(this.code.lines[ cursor.line ].length, 0) );
1031
+ }
1004
1032
 
1005
- e.cancelShift = e.keepSelection = true;
1006
- this.actions[ 'End' ].callback( cursor.line, cursor, e );
1007
- delete e.cancelShift; delete e.keepSelection;
1033
+ if( e.shiftKey ) {
1034
+ this._processSelection( cursor, e );
1035
+ }
1036
+ }
1037
+ // Move down autocomplete selection
1038
+ else
1039
+ {
1040
+ this._moveArrowSelectedAutoComplete('down');
1041
+ }
1042
+ });
1043
+
1044
+ this.action( 'ArrowLeft', false, ( ln, cursor, e ) => {
1008
1045
 
1009
- if( e.shiftKey ) this._processSelection( cursor, e, false );
1046
+ // Nothing to do..
1047
+ if( cursor.line == 0 && cursor.position == 0 )
1048
+ return;
1049
+
1050
+ if( e.metaKey ) { // Apple devices (Command)
1051
+ e.preventDefault();
1052
+ this.actions[ 'Home' ].callback( ln, cursor, e );
1010
1053
  }
1011
- }
1012
- });
1054
+ else if( e.ctrlKey ) {
1055
+ // Get next word
1056
+ const [word, from, to] = this.getWordAtPos( cursor, -1 );
1057
+ // If no length, we change line..
1058
+ if( !word.length && this.lineUp( cursor, true ) ) {
1059
+ const cS = e.cancelShift, kS = e.keepSelection;
1060
+ e.cancelShift = true;
1061
+ e.keepSelection = true;
1062
+ this.actions[ 'End' ].callback( cursor.line, cursor, e );
1063
+ e.cancelShift = cS;
1064
+ e.keepSelection = kS;
1065
+ }
1066
+ var diff = Math.max( cursor.position - from, 1 );
1067
+ var substr = word.substr( 0, diff );
1013
1068
 
1014
- this.action( 'ArrowRight', false, ( ln, cursor, e ) => {
1069
+ // Selections...
1070
+ if( e.shiftKey ) {
1071
+ if( !cursor.selection )
1072
+ this.startSelection( cursor );
1073
+ }
1074
+ else
1075
+ this.endSelection();
1015
1076
 
1016
- // Nothing to do..
1017
- if( cursor.line == this.code.lines.length - 1 &&
1018
- cursor.position == this.code.lines[ cursor.line ].length )
1019
- return;
1077
+ this.cursorToString( cursor, substr, true );
1020
1078
 
1021
- if( e.metaKey ) // Apple devices (Command)
1022
- {
1023
- e.preventDefault();
1024
- this.actions[ 'End' ].callback( ln, cursor );
1025
- }
1026
- else if( e.ctrlKey ) // Next word
1027
- {
1028
- // Get next word
1029
- const [ word, from, to ] = this.getWordAtPos( cursor );
1079
+ if( e.shiftKey )
1080
+ this._processSelection( cursor, e );
1081
+ }
1082
+ else {
1083
+ var letter = this.getCharAtPos( cursor, -1 );
1084
+ if( letter ) {
1085
+ if( e.shiftKey ) {
1086
+ if( !cursor.selection ) this.startSelection( cursor );
1087
+ this.cursorToLeft( letter, cursor );
1088
+ this._processSelection( cursor, e, false, CodeEditor.SELECTION_X );
1089
+ }
1090
+ else {
1091
+ if( !cursor.selection ) {
1092
+ this.cursorToLeft( letter, cursor );
1093
+ if( this.useAutoComplete && this.isAutoCompleteActive )
1094
+ this.showAutoCompleteBox( 'foo', cursor );
1095
+ }
1096
+ else {
1097
+ cursor.selection.invertIfNecessary();
1098
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, cursor );
1099
+ this.cursorToLine( cursor, cursor.selection.fromY, true );
1100
+ this.cursorToPosition( cursor, cursor.selection.fromX );
1101
+ this.endSelection();
1102
+ }
1103
+ }
1104
+ }
1105
+ else if( cursor.line > 0 ) {
1030
1106
 
1031
- // If no length, we change line..
1032
- if( !word.length ) this.lineDown( cursor, true );
1033
- var diff = cursor.position - from;
1034
- var substr = word.substr( diff );
1107
+ if( e.shiftKey && !cursor.selection ) this.startSelection( cursor );
1035
1108
 
1036
- // Selections...
1037
- if( e.shiftKey ) {
1038
- if( !cursor.selection )
1039
- this.startSelection( cursor );
1109
+ this.lineUp( cursor );
1110
+
1111
+ e.cancelShift = e.keepSelection = true;
1112
+ this.actions[ 'End' ].callback( cursor.line, cursor, e );
1113
+ delete e.cancelShift; delete e.keepSelection;
1114
+
1115
+ if( e.shiftKey ) this._processSelection( cursor, e, false );
1116
+ }
1040
1117
  }
1041
- else
1042
- this.endSelection();
1118
+ });
1043
1119
 
1044
- this.cursorToString( cursor, substr );
1120
+ this.action( 'ArrowRight', false, ( ln, cursor, e ) => {
1045
1121
 
1046
- if( e.shiftKey )
1047
- this._processSelection( cursor, e );
1048
- }
1049
- else // Next char
1050
- {
1051
- var letter = this.getCharAtPos( cursor );
1052
- if( letter ) {
1122
+ // Nothing to do..
1123
+ if( cursor.line == this.code.lines.length - 1 &&
1124
+ cursor.position == this.code.lines[ cursor.line ].length )
1125
+ return;
1053
1126
 
1054
- // Selecting chars
1055
- if( e.shiftKey )
1056
- {
1127
+ if( e.metaKey ) // Apple devices (Command)
1128
+ {
1129
+ e.preventDefault();
1130
+ this.actions[ 'End' ].callback( ln, cursor );
1131
+ }
1132
+ else if( e.ctrlKey ) // Next word
1133
+ {
1134
+ // Get next word
1135
+ const [ word, from, to ] = this.getWordAtPos( cursor );
1136
+
1137
+ // If no length, we change line..
1138
+ if( !word.length ) this.lineDown( cursor, true );
1139
+ var diff = cursor.position - from;
1140
+ var substr = word.substr( diff );
1141
+
1142
+ // Selections...
1143
+ if( e.shiftKey ) {
1057
1144
  if( !cursor.selection )
1058
1145
  this.startSelection( cursor );
1059
-
1060
- this.cursorToRight( letter, cursor );
1061
- this._processSelection( cursor, e, false, CodeEditor.SELECTION_X );
1062
1146
  }
1063
1147
  else
1064
- {
1065
- if( !cursor.selection ) {
1148
+ this.endSelection();
1149
+
1150
+ this.cursorToString( cursor, substr );
1151
+
1152
+ if( e.shiftKey )
1153
+ this._processSelection( cursor, e );
1154
+ }
1155
+ else // Next char
1156
+ {
1157
+ var letter = this.getCharAtPos( cursor );
1158
+ if( letter ) {
1159
+
1160
+ // Selecting chars
1161
+ if( e.shiftKey )
1162
+ {
1163
+ if( !cursor.selection )
1164
+ this.startSelection( cursor );
1165
+
1066
1166
  this.cursorToRight( letter, cursor );
1067
- if( this.useAutoComplete && this.isAutoCompleteActive )
1068
- this.showAutoCompleteBox( 'foo', cursor );
1167
+ this._processSelection( cursor, e, false, CodeEditor.SELECTION_X );
1069
1168
  }
1070
1169
  else
1071
1170
  {
1072
- cursor.selection.invertIfNecessary();
1073
- this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, cursor );
1074
- this.cursorToLine( cursor, cursor.selection.toY );
1075
- this.cursorToPosition( cursor, cursor.selection.toX );
1076
- this.endSelection();
1171
+ if( !cursor.selection ) {
1172
+ this.cursorToRight( letter, cursor );
1173
+ if( this.useAutoComplete && this.isAutoCompleteActive )
1174
+ this.showAutoCompleteBox( 'foo', cursor );
1175
+ }
1176
+ else
1177
+ {
1178
+ cursor.selection.invertIfNecessary();
1179
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, cursor );
1180
+ this.cursorToLine( cursor, cursor.selection.toY );
1181
+ this.cursorToPosition( cursor, cursor.selection.toX );
1182
+ this.endSelection();
1183
+ }
1077
1184
  }
1078
1185
  }
1079
- }
1080
- else if( this.code.lines[ cursor.line + 1 ] !== undefined ) {
1186
+ else if( this.code.lines[ cursor.line + 1 ] !== undefined ) {
1081
1187
 
1082
- if( e.shiftKey ) {
1083
- if( !cursor.selection ) this.startSelection( cursor );
1084
- }
1085
- else this.endSelection();
1188
+ if( e.shiftKey ) {
1189
+ if( !cursor.selection ) this.startSelection( cursor );
1190
+ }
1191
+ else this.endSelection();
1086
1192
 
1087
- this.lineDown( cursor, true );
1193
+ this.lineDown( cursor, true );
1088
1194
 
1089
- if( e.shiftKey ) this._processSelection( cursor, e, false );
1195
+ if( e.shiftKey ) this._processSelection( cursor, e, false );
1090
1196
 
1091
- this.hideAutoCompleteBox();
1197
+ this.hideAutoCompleteBox();
1198
+ }
1092
1199
  }
1093
- }
1094
- });
1200
+ });
1201
+ }
1095
1202
 
1096
1203
  // Default code tab
1097
1204
 
@@ -1101,15 +1208,44 @@ class CodeEditor {
1101
1208
  const onLoadAll = () => {
1102
1209
  // Create inspector panel when the initial state is complete
1103
1210
  // and we have at least 1 tab opened
1104
- this.infoPanel = this._createInfoPanel();
1105
- if( this.infoPanel )
1211
+ this.statusPanel = this._createStatusPanel();
1212
+ if( this.statusPanel )
1106
1213
  {
1107
- area.attach( this.infoPanel );
1214
+ area.attach( this.statusPanel );
1108
1215
  }
1109
1216
 
1110
1217
  // Wait until the fonts are all loaded
1111
1218
  document.fonts.ready.then(() => {
1112
- this.charWidth = this._measureChar( "a", true );
1219
+ // Load any font size from local storage
1220
+ const savedFontSize = window.localStorage.getItem( "lexcodeeditor-font-size" );
1221
+ if( savedFontSize )
1222
+ {
1223
+ this._setFontSize( parseInt( savedFontSize ) );
1224
+ }
1225
+ else // Use default size
1226
+ {
1227
+ const r = document.querySelector( ':root' );
1228
+ const s = getComputedStyle( r );
1229
+ this.fontSize = parseInt( s.getPropertyValue( "--code-editor-font-size" ) );
1230
+ this.charWidth = this._measureChar( "a", true );
1231
+ }
1232
+
1233
+ LX.emit( "@font-size", this.fontSize );
1234
+
1235
+ // Get final sizes for editor elements based on Tabs and status bar offsets
1236
+ LX.doAsync( () => {
1237
+ this._verticalTopOffset = this.tabs?.root.getBoundingClientRect().height ?? 0;
1238
+ this._verticalBottomOffset = this.statusPanel?.root.getBoundingClientRect().height ?? 0;
1239
+ this._fullVerticalOffset = this._verticalTopOffset + this._verticalBottomOffset;
1240
+
1241
+ this.gutter.style.marginTop = `${ this._verticalTopOffset }px`;
1242
+ this.gutter.style.height = `calc(100% - ${ this._fullVerticalOffset }px)`;
1243
+ this.vScrollbar.root.style.marginTop = `${ this.tabs?.root.getBoundingClientRect().height ?? 0 }px`;
1244
+ this.vScrollbar.root.style.height = `calc(100% - ${ this._fullVerticalOffset }px)`;
1245
+ this.hScrollbar.root.style.bottom = `${ this._verticalBottomOffset }px`;
1246
+ this.codeArea.root.style.height = `calc(100% - ${ this._verticalBottomOffset }px)`;
1247
+ }, 50 );
1248
+
1113
1249
  });
1114
1250
 
1115
1251
  window.editor = this;
@@ -1153,39 +1289,38 @@ class CodeEditor {
1153
1289
  onKeyPressed( e ) {
1154
1290
 
1155
1291
  // Toggle visibility of the file explorer
1156
- if( e.key == 'b' && e.ctrlKey && this.explorer )
1292
+ if( e.key == 'b' && e.ctrlKey && this.useFileExplorer )
1157
1293
  {
1158
1294
  this.explorerArea.root.classList.toggle( "hidden" );
1159
1295
  if( this._lastBaseareaWidth )
1160
1296
  {
1161
- this.base_area.root.style.width = this._lastBaseareaWidth;
1297
+ this.baseArea.root.style.width = this._lastBaseareaWidth;
1162
1298
  delete this._lastBaseareaWidth;
1163
1299
 
1164
1300
  } else
1165
1301
  {
1166
- this._lastBaseareaWidth = this.base_area.root.style.width;
1167
- this.base_area.root.style.width = "100%";
1302
+ this._lastBaseareaWidth = this.baseArea.root.style.width;
1303
+ this.baseArea.root.style.width = "100%";
1168
1304
  }
1169
1305
  }
1170
1306
  }
1171
1307
 
1172
1308
  getText( min ) {
1173
-
1174
1309
  return this.code.lines.join( min ? ' ' : '\n' );
1175
1310
  }
1176
1311
 
1177
1312
  // This can be used to empty all text...
1178
1313
  setText( text = "", lang ) {
1179
1314
 
1180
- let new_lines = text.split( '\n' );
1181
- this.code.lines = [].concat( new_lines );
1315
+ let newLines = text.split( '\n' );
1316
+ this.code.lines = [].concat( newLines );
1182
1317
 
1183
1318
  this._removeSecondaryCursors();
1184
1319
 
1185
- let cursor = this._getCurrentCursor( true );
1186
- let lastLine = new_lines.pop();
1320
+ let cursor = this.getCurrentCursor( true );
1321
+ let lastLine = newLines.pop();
1187
1322
 
1188
- this.cursorToLine( cursor, new_lines.length ); // Already substracted 1
1323
+ this.cursorToLine( cursor, newLines.length ); // Already substracted 1
1189
1324
  this.cursorToPosition( cursor, lastLine.length );
1190
1325
  this.processLines();
1191
1326
 
@@ -1199,21 +1334,22 @@ class CodeEditor {
1199
1334
 
1200
1335
  let lidx = cursor.line;
1201
1336
 
1202
- if( cursor.selection ) {
1337
+ if( cursor.selection )
1338
+ {
1203
1339
  this.deleteSelection( cursor );
1204
1340
  lidx = cursor.line;
1205
1341
  }
1206
1342
 
1207
1343
  this.endSelection();
1208
1344
 
1209
- const new_lines = text.replaceAll( '\r', '' ).split( '\n' );
1345
+ const newLines = text.replaceAll( '\r', '' ).split( '\n' );
1210
1346
 
1211
1347
  // Pasting Multiline...
1212
- if( new_lines.length != 1 )
1348
+ if( newLines.length != 1 )
1213
1349
  {
1214
- let num_lines = new_lines.length;
1350
+ let num_lines = newLines.length;
1215
1351
  console.assert( num_lines > 0 );
1216
- const first_line = new_lines.shift();
1352
+ const first_line = newLines.shift();
1217
1353
  num_lines--;
1218
1354
 
1219
1355
  const remaining = this.code.lines[ lidx ].slice( cursor.position );
@@ -1230,11 +1366,11 @@ class CodeEditor {
1230
1366
 
1231
1367
  let _text = null;
1232
1368
 
1233
- for( var i = 0; i < new_lines.length; ++i ) {
1234
- _text = new_lines[ i ];
1369
+ for( var i = 0; i < newLines.length; ++i ) {
1370
+ _text = newLines[ i ];
1235
1371
  this.cursorToLine( cursor, cursor.line++, true );
1236
1372
  // Add remaining...
1237
- if( i == (new_lines.length - 1) )
1373
+ if( i == (newLines.length - 1) )
1238
1374
  _text += remaining;
1239
1375
  this.code.lines.splice( 1 + lidx + i, 0, _text );
1240
1376
  }
@@ -1248,24 +1384,26 @@ class CodeEditor {
1248
1384
  {
1249
1385
  this.code.lines[ lidx ] = [
1250
1386
  this.code.lines[ lidx ].slice( 0, cursor.position ),
1251
- new_lines[ 0 ],
1387
+ newLines[ 0 ],
1252
1388
  this.code.lines[ lidx ].slice( cursor.position )
1253
1389
  ].join('');
1254
1390
 
1255
- this.cursorToPosition( cursor, ( cursor.position + new_lines[ 0 ].length ) );
1391
+ this.cursorToPosition( cursor, ( cursor.position + newLines[ 0 ].length ) );
1256
1392
  this.processLine( lidx );
1257
1393
  }
1258
1394
 
1259
- this.resize( null, ( scrollWidth, scrollHeight ) => {
1395
+ this.resize( CodeEditor.RESIZE_SCROLLBAR_H_V, null, ( scrollWidth, scrollHeight ) => {
1260
1396
  var viewportSizeX = ( this.codeScroller.clientWidth + this.getScrollLeft() ) - CodeEditor.LINE_GUTTER_WIDTH; // Gutter offset
1261
1397
  if( ( cursor.position * this.charWidth ) >= viewportSizeX )
1398
+ {
1262
1399
  this.setScrollLeft( this.code.lines[ lidx ].length * this.charWidth );
1400
+ }
1263
1401
  } );
1264
1402
  }
1265
1403
 
1266
1404
  loadFile( file, options = {} ) {
1267
1405
 
1268
- const inner_add_tab = ( text, name, title ) => {
1406
+ const _innerAddTab = ( text, name, title ) => {
1269
1407
 
1270
1408
  // Remove Carriage Return in some cases and sub tabs using spaces
1271
1409
  text = text.replaceAll( '\r', '' );
@@ -1277,16 +1415,24 @@ class CodeEditor {
1277
1415
 
1278
1416
  // Add item in the explorer if used
1279
1417
 
1280
- if( this.explorer )
1418
+ if( this.useFileExplorer || this.skipTabs )
1281
1419
  {
1282
1420
  this._tabStorage[ name ] = {
1283
1421
  lines: lines,
1284
1422
  options: options
1285
1423
  };
1286
1424
 
1287
- const ext = this.languages[ options.language ] ?. ext;
1288
- this.addExplorerItem( { id: name, skipVisibility: true, icon: this._getFileIcon( name, ext ) } );
1289
- this.explorer.innerTree.frefresh( name );
1425
+ const ext = CodeEditor.languages[ options.language ] ?. ext;
1426
+
1427
+ if( this.useFileExplorer )
1428
+ {
1429
+ this.addExplorerItem( { id: name, skipVisibility: true, icon: this._getFileIcon( name, ext ) } );
1430
+ this.explorer.innerTree.frefresh( name );
1431
+ }
1432
+ else
1433
+ {
1434
+
1435
+ }
1290
1436
  }
1291
1437
  else
1292
1438
  {
@@ -1311,7 +1457,7 @@ class CodeEditor {
1311
1457
  let filename = file;
1312
1458
  LX.request({ url: filename, success: text => {
1313
1459
  const name = filename.substring(filename.lastIndexOf( '/' ) + 1);
1314
- inner_add_tab( text, name, filename );
1460
+ _innerAddTab( text, name, filename );
1315
1461
  } });
1316
1462
  }
1317
1463
  else // File Blob
@@ -1320,82 +1466,11 @@ class CodeEditor {
1320
1466
  fr.readAsText( file );
1321
1467
  fr.onload = e => {
1322
1468
  const text = e.currentTarget.result;
1323
- inner_add_tab( text, file.name );
1469
+ _innerAddTab( text, file.name );
1324
1470
  };
1325
1471
  }
1326
1472
  }
1327
1473
 
1328
- _addCursor( line = 0, position = 0, force, isMain = false ) {
1329
-
1330
- // If cursor in that position exists, remove it instead..
1331
- const exists = Array.from( this.cursors.children ).find( v => v.position == position && v.line == line );
1332
- if( exists && !force )
1333
- {
1334
- if( !exists.isMain )
1335
- exists.remove();
1336
-
1337
- return;
1338
- }
1339
-
1340
- let cursor = document.createElement( 'div' );
1341
- cursor.name = "cursor" + this.cursors.childElementCount;
1342
- cursor.className = "cursor";
1343
- cursor.innerHTML = "&nbsp;";
1344
- cursor.isMain = isMain;
1345
- cursor._left = position * this.charWidth;
1346
- cursor.style.left = "calc( " + cursor._left + "px + " + this.xPadding + " )";
1347
- cursor._top = line * this.lineHeight;
1348
- cursor.style.top = cursor._top + "px";
1349
- cursor._position = position;
1350
- cursor._line = line;
1351
- cursor.print = (function() { console.log( this._line, this._position ) }).bind( cursor );
1352
- cursor.isLast = (function() { return this.cursors.lastChild == cursor; }).bind( this );
1353
-
1354
- Object.defineProperty( cursor, 'line', {
1355
- get: (v) => { return cursor._line },
1356
- set: (v) => {
1357
- cursor._line = v;
1358
- if( cursor.isMain ) this._setActiveLine( v );
1359
- }
1360
- } );
1361
-
1362
- Object.defineProperty( cursor, 'position', {
1363
- get: (v) => { return cursor._position },
1364
- set: (v) => {
1365
- cursor._position = v;
1366
- if( cursor.isMain ) this._updateDataInfoPanel( "@cursor-pos", "Col " + ( v + 1 ) );
1367
- }
1368
- } );
1369
-
1370
- this.cursors.appendChild( cursor );
1371
-
1372
- return cursor;
1373
- }
1374
-
1375
- _getCurrentCursor( removeOthers ) {
1376
-
1377
- if( removeOthers )
1378
- {
1379
- this._removeSecondaryCursors();
1380
- }
1381
-
1382
- return this.cursors.children[ 0 ];
1383
- }
1384
-
1385
- _removeSecondaryCursors() {
1386
-
1387
- while( this.cursors.childElementCount > 1 )
1388
- this.cursors.lastChild.remove();
1389
- }
1390
-
1391
- _logCursors() {
1392
-
1393
- for( let cursor of this.cursors.children )
1394
- {
1395
- cursor.print();
1396
- }
1397
- }
1398
-
1399
1474
  _addUndoStep( cursor, force, deleteRedo = true ) {
1400
1475
 
1401
1476
  // Only the mainc cursor stores undo steps
@@ -1407,12 +1482,18 @@ class CodeEditor {
1407
1482
 
1408
1483
  if( !force )
1409
1484
  {
1410
- if( !this._lastTime ) {
1485
+ if( !this._lastTime )
1486
+ {
1411
1487
  this._lastTime = current;
1412
- } else {
1413
- if( ( current - this._lastTime ) > 2000 ){
1488
+ }
1489
+ else
1490
+ {
1491
+ if( ( current - this._lastTime ) > 2000 )
1492
+ {
1414
1493
  this._lastTime = null;
1415
- } else {
1494
+ }
1495
+ else
1496
+ {
1416
1497
  // If time not enough, reset timer
1417
1498
  this._lastTime = current;
1418
1499
  return;
@@ -1466,7 +1547,9 @@ class CodeEditor {
1466
1547
 
1467
1548
  // Only the mainc cursor stores redo steps
1468
1549
  if( !cursor.isMain )
1550
+ {
1469
1551
  return;
1552
+ }
1470
1553
 
1471
1554
  this.code.redoSteps.push( {
1472
1555
  lines: LX.deepCopy( this.code.lines ),
@@ -1496,7 +1579,9 @@ class CodeEditor {
1496
1579
 
1497
1580
  // Generate new if needed
1498
1581
  if( !currentCursor )
1582
+ {
1499
1583
  currentCursor = this._addCursor();
1584
+ }
1500
1585
 
1501
1586
  this.restoreCursor( currentCursor, step.cursors[ i ] );
1502
1587
  }
@@ -1515,10 +1600,11 @@ class CodeEditor {
1515
1600
  this._updateDataInfoPanel( "@highlight", lang );
1516
1601
  this.processLines();
1517
1602
 
1518
- const ext = langExtension ?? this.languages[ lang ].ext;
1603
+ const ext = langExtension ?? CodeEditor.languages[ lang ].ext;
1519
1604
  const icon = this._getFileIcon( null, ext );
1520
1605
 
1521
1606
  // Update tab icon
1607
+ if( !this.skipTabs )
1522
1608
  {
1523
1609
  const tab = this.tabs.tabDOMs[ this.code.tabName ];
1524
1610
  tab.firstChild.remove();
@@ -1537,7 +1623,7 @@ class CodeEditor {
1537
1623
  }
1538
1624
 
1539
1625
  // Update explorer icon
1540
- if( this.explorer )
1626
+ if( this.useFileExplorer )
1541
1627
  {
1542
1628
  const item = this.explorer.innerTree.data.children.filter( (v) => v.id === this.code.tabName )[ 0 ];
1543
1629
  console.assert( item != undefined );
@@ -1553,9 +1639,9 @@ class CodeEditor {
1553
1639
  return this._changeLanguage( this.code.language );
1554
1640
  }
1555
1641
 
1556
- for( let l in this.languages )
1642
+ for( let l in CodeEditor.languages )
1557
1643
  {
1558
- const langExtension = this.languages[ l ].ext;
1644
+ const langExtension = CodeEditor.languages[ l ].ext;
1559
1645
 
1560
1646
  if( langExtension.constructor == Array )
1561
1647
  {
@@ -1577,103 +1663,150 @@ class CodeEditor {
1577
1663
  this._changeLanguage( 'Plain Text' );
1578
1664
  }
1579
1665
 
1580
- _createInfoPanel() {
1666
+ _createStatusPanel() {
1581
1667
 
1582
- if( !this.skipInfo )
1668
+ if( this.skipInfo )
1583
1669
  {
1584
- let panel = new LX.Panel({ className: "lexcodetabinfo", width: "calc(100%)", height: "auto" });
1585
-
1586
- panel.sameLine();
1587
- panel.addLabel( this.code.title, { fit: true, signal: "@tab-name" });
1588
- panel.addLabel( "Ln " + 1, { fit: true, signal: "@cursor-line" });
1589
- panel.addLabel( "Col " + 1, { fit: true, signal: "@cursor-pos" });
1590
- panel.addButton( null, "Spaces: " + this.tabSpaces, ( value, event ) => {
1591
- LX.addContextMenu( "Spaces", event, m => {
1592
- const options = [ 2, 4, 8 ];
1593
- for( const n of options )
1594
- m.add( n, (v) => {
1595
- this.tabSpaces = v;
1596
- this.processLines();
1597
- this._updateDataInfoPanel( "@tab-spaces", "Spaces: " + this.tabSpaces );
1598
- } );
1599
- });
1600
- }, { nameWidth: "15%", signal: "@tab-spaces" });
1601
- panel.addButton( "<b>{ }</b>", this.highlight, ( value, event ) => {
1602
- LX.addContextMenu( "Language", event, m => {
1603
- for( const lang of Object.keys( this.languages ) )
1604
- {
1605
- m.add( lang, v => {
1606
- this._changeLanguage( v, null, true )
1607
- } );
1608
- }
1609
- });
1610
- }, { nameWidth: "15%", signal: "@highlight" });
1611
- panel.endLine();
1612
-
1613
- return panel;
1670
+ return;
1614
1671
  }
1615
- else
1616
- {
1617
- doAsync( () => {
1618
1672
 
1619
- // Change css a little bit...
1620
- this.gutter.style.height = "calc(100% - 28px)";
1621
- this.root.querySelectorAll( '.code' ).forEach( e => e.style.height = "calc(100% - 6px)" );
1622
- this.root.querySelector( '.lexareatabscontent' ).style.height = "calc(100% - 23px)";
1623
- this.base_area.root.querySelector( '.lexcodescrollbar.vertical' ).style.height = "calc(100% - 27px)";
1624
- this.tabs.area.root.classList.add( 'no-code-info' );
1673
+ let panel = new LX.Panel({ className: "lexcodetabinfo flex flex-row", height: "auto" });
1625
1674
 
1626
- }, 100);
1675
+ let leftStatusPanel = new LX.Panel( { id: "FontSizeZoomStatusComponent", height: "auto" } );
1676
+ leftStatusPanel.sameLine();
1677
+
1678
+ if( this.skipTabs )
1679
+ {
1680
+ leftStatusPanel.addButton( null, "ZoomOutButton", this._decreaseFontSize.bind( this ), { icon: "ZoomOut", width: "32px", title: "Zoom Out", tooltip: true } );
1627
1681
  }
1682
+
1683
+ leftStatusPanel.addButton( null, "ZoomOutButton", this._decreaseFontSize.bind( this ), { icon: "ZoomOut", width: "32px", title: "Zoom Out", tooltip: true } );
1684
+ leftStatusPanel.addLabel( this.fontSize ?? 14, { fit: true, signal: "@font-size" });
1685
+ leftStatusPanel.addButton( null, "ZoomInButton", this._increaseFontSize.bind( this ), { icon: "ZoomIn", width: "32px", title: "Zoom In", tooltip: true } );
1686
+ leftStatusPanel.endLine( "justify-start" );
1687
+ panel.attach( leftStatusPanel.root );
1688
+
1689
+ let rightStatusPanel = new LX.Panel( { height: "auto" } );
1690
+ rightStatusPanel.sameLine();
1691
+ rightStatusPanel.addLabel( this.code.title, { id: "EditorFilenameStatusComponent", fit: true, signal: "@tab-name" });
1692
+ rightStatusPanel.addButton( null, "Ln 1, Col 1", this.showSearchLineBox.bind( this ), { id: "EditorSelectionStatusComponent", fit: true, signal: "@cursor-data" });
1693
+ rightStatusPanel.addButton( null, "Spaces: " + this.tabSpaces, ( value, event ) => {
1694
+ LX.addContextMenu( "Spaces", event, m => {
1695
+ const options = [ 2, 4, 8 ];
1696
+ for( const n of options )
1697
+ m.add( n, (v) => {
1698
+ this.tabSpaces = v;
1699
+ this.processLines();
1700
+ this._updateDataInfoPanel( "@tab-spaces", "Spaces: " + this.tabSpaces );
1701
+ } );
1702
+ });
1703
+ }, { id: "EditorIndentationStatusComponent", nameWidth: "15%", signal: "@tab-spaces" });
1704
+ rightStatusPanel.addButton( "<b>{ }</b>", this.highlight, ( value, event ) => {
1705
+ LX.addContextMenu( "Language", event, m => {
1706
+ for( const lang of Object.keys( CodeEditor.languages ) )
1707
+ {
1708
+ m.add( lang, v => {
1709
+ this._changeLanguage( v, null, true )
1710
+ } );
1711
+ }
1712
+ });
1713
+ }, { id: "EditorLanguageStatusComponent", nameWidth: "15%", signal: "@highlight" });
1714
+ rightStatusPanel.endLine( "justify-end" );
1715
+ panel.attach( rightStatusPanel.root );
1716
+
1717
+ const itemVisibilityMap = {
1718
+ "Font Size Zoom": true,
1719
+ "Editor Filename": true,
1720
+ "Editor Selection": true,
1721
+ "Editor Indentation": true,
1722
+ "Editor Language": true,
1723
+ };
1724
+
1725
+ panel.root.addEventListener( "contextmenu", (e) => {
1726
+
1727
+ if( e.target && ( e.target.classList.contains( "lexpanel" ) || e.target.classList.contains( "lexinlinecomponents" ) ) )
1728
+ {
1729
+ return;
1730
+ }
1731
+
1732
+ const menuOptions = Object.keys( itemVisibilityMap ).map( ( itemName, idx ) => {
1733
+ const item = {
1734
+ name: itemName,
1735
+ icon: "Check",
1736
+ callback: () => {
1737
+ itemVisibilityMap[ itemName ] = !itemVisibilityMap[ itemName ];
1738
+ const b = panel.root.querySelector( `#${ itemName.replaceAll( " ", "" ) }StatusComponent` );
1739
+ console.assert( b, `${ itemName } has no status button!` );
1740
+ b.classList.toggle( "hidden", !itemVisibilityMap[ itemName ] );
1741
+ }
1742
+ }
1743
+ if( !itemVisibilityMap[ itemName ] ) delete item.icon;
1744
+ return item;
1745
+ } );
1746
+ new LX.DropdownMenu( e.target, menuOptions, { side: "top", align: "start" });
1747
+ } );
1748
+
1749
+ return panel;
1628
1750
  }
1629
1751
 
1630
- _getFileIcon( name, extension ) {
1752
+ _getFileIcon( name, extension, lang ) {
1631
1753
 
1632
1754
  const isNewTabButton = name ? ( name === '+' ) : false;
1633
-
1634
- if( !extension )
1755
+ if( isNewTabButton )
1635
1756
  {
1636
- extension = LX.getExtension( name );
1757
+ return;
1637
1758
  }
1638
- else
1639
- {
1640
- const possibleExtensions = [].concat( extension );
1641
1759
 
1642
- if( name )
1760
+ if( !lang )
1761
+ {
1762
+ if( !extension )
1643
1763
  {
1644
- const fileExtension = LX.getExtension( name );
1645
- const idx = possibleExtensions.indexOf( fileExtension );
1764
+ extension = LX.getExtension( name );
1765
+ }
1766
+ else
1767
+ {
1768
+ const possibleExtensions = [].concat( extension );
1769
+
1770
+ if( name )
1771
+ {
1772
+ const fileExtension = LX.getExtension( name );
1773
+ const idx = possibleExtensions.indexOf( fileExtension );
1646
1774
 
1647
- if( idx > -1)
1775
+ if( idx > -1)
1776
+ {
1777
+ extension = possibleExtensions[ idx ];
1778
+ }
1779
+ }
1780
+ else
1648
1781
  {
1649
- extension = possibleExtensions[ idx ];
1782
+ extension = possibleExtensions[ 0 ];
1650
1783
  }
1651
1784
  }
1652
- else
1785
+
1786
+ for( const [ l, lData ] of Object.entries( CodeEditor.languages ) )
1653
1787
  {
1654
- extension = possibleExtensions[ 0 ];
1788
+ const extensions = [].concat( lData.ext );
1789
+ if( extensions.includes( extension ) )
1790
+ {
1791
+ lang = l;
1792
+ break;
1793
+ }
1655
1794
  }
1795
+
1796
+ }
1797
+
1798
+ const iconPlusClasses = CodeEditor.languages[ lang ]?.icon;
1799
+ if( iconPlusClasses )
1800
+ {
1801
+ return iconPlusClasses[ extension ] ?? iconPlusClasses;
1656
1802
  }
1657
1803
 
1658
- return extension == "html" ? "Code orange" :
1659
- extension == "css" ? "Hash dodgerblue" :
1660
- extension == "xml" ? "Rss orange" :
1661
- extension == "bat" ? "Windows lightblue" :
1662
- extension == "json" ? "Braces fg-primary" :
1663
- extension == "js" ? "Js goldenrod" :
1664
- extension == "py" ? "Python munsellblue" :
1665
- extension == "rs" ? "Rust fg-primary" :
1666
- extension == "md" ? "Markdown fg-primary" :
1667
- extension == "cpp" ? "CPlusPlus pictonblue" :
1668
- extension == "hpp" ? "CPlusPlus heliotrope" :
1669
- extension == "c" ? "C pictonblue" :
1670
- extension == "h" ? "C heliotrope" :
1671
- !isNewTabButton ? "AlignLeft gray" : undefined;
1804
+ return "AlignLeft gray";
1672
1805
  }
1673
1806
 
1674
1807
  _onNewTab( e ) {
1675
1808
 
1676
- this.processFocus(false);
1809
+ this.processFocus( false );
1677
1810
 
1678
1811
  LX.addContextMenu( null, e, m => {
1679
1812
  m.add( "Create", this.addTab.bind( this, "unnamed.js", true, "", { language: "JavaScript" } ) );
@@ -1696,7 +1829,7 @@ class CodeEditor {
1696
1829
 
1697
1830
  this._removeSecondaryCursors();
1698
1831
 
1699
- var cursor = this._getCurrentCursor( true );
1832
+ var cursor = this.getCurrentCursor( true );
1700
1833
  this.saveCursor( cursor, this.code.cursorState );
1701
1834
  this.code = this.loadedTabs[ name ];
1702
1835
  this.restoreCursor( cursor, this.code.cursorState );
@@ -1720,12 +1853,12 @@ class CodeEditor {
1720
1853
  _onContextMenuTab( isNewTabButton, event, name, ) {
1721
1854
 
1722
1855
  if( isNewTabButton )
1723
- return;
1856
+ return;
1724
1857
 
1725
1858
  LX.addContextMenu( null, event, m => {
1726
1859
  m.add( "Close", () => { this.tabs.delete( name ) } );
1727
- m.add( "" );
1728
- m.add( "Rename", () => { console.warn( "TODO" )} );
1860
+ // m.add( "" );
1861
+ // m.add( "Rename", () => { console.warn( "TODO" )} );
1729
1862
  });
1730
1863
  }
1731
1864
 
@@ -1752,6 +1885,9 @@ class CodeEditor {
1752
1885
  code.cursorState = {};
1753
1886
  code.undoSteps = [];
1754
1887
  code.redoSteps = [];
1888
+ code.lineScopes = [];
1889
+ code.lineSymbols = [];
1890
+ code.symbolsTable = new Map();
1755
1891
  code.tabName = name;
1756
1892
  code.title = title ?? name;
1757
1893
  code.tokens = {};
@@ -1778,21 +1914,24 @@ class CodeEditor {
1778
1914
 
1779
1915
  const tabIcon = this._getFileIcon( name );
1780
1916
 
1781
- if( this.explorer && !isNewTabButton )
1917
+ if( this.useFileExplorer && !isNewTabButton )
1782
1918
  {
1783
1919
  this.addExplorerItem( { id: name, skipVisibility: true, icon: tabIcon } );
1784
1920
  this.explorer.innerTree.frefresh( name );
1785
1921
  }
1786
1922
 
1787
- this.tabs.add( name, code, {
1788
- selected: selected,
1789
- fixed: isNewTabButton,
1790
- title: code.title,
1791
- icon: tabIcon,
1792
- onSelect: this._onSelectTab.bind( this, isNewTabButton ),
1793
- onContextMenu: this._onContextMenuTab.bind( this, isNewTabButton ),
1794
- allowDelete: true
1795
- } );
1923
+ if( !this.skipTabs )
1924
+ {
1925
+ this.tabs.add( name, code, {
1926
+ selected: selected,
1927
+ fixed: isNewTabButton,
1928
+ title: code.title,
1929
+ icon: tabIcon,
1930
+ onSelect: this._onSelectTab.bind( this, isNewTabButton ),
1931
+ onContextMenu: this._onContextMenuTab.bind( this, isNewTabButton ),
1932
+ allowDelete: true
1933
+ } );
1934
+ }
1796
1935
 
1797
1936
  // Move into the sizer..
1798
1937
  this.codeSizer.appendChild( code );
@@ -1818,17 +1957,20 @@ class CodeEditor {
1818
1957
  return name;
1819
1958
  }
1820
1959
 
1821
- loadTab( name ) {
1960
+ loadCode( name ) {
1961
+
1962
+ // Hide all others
1963
+ this.codeSizer.querySelectorAll( ".code" ).forEach( c => c.classList.add( "hidden" ) );
1822
1964
 
1823
1965
  // Already open...
1824
1966
  if( this.openedTabs[ name ] )
1825
1967
  {
1826
- this.tabs.select( name );
1968
+ let code = this.openedTabs[ name ]
1969
+ code.classList.remove( "hidden" );
1827
1970
  return;
1828
1971
  }
1829
1972
 
1830
1973
  let code = this.loadedTabs[ name ]
1831
-
1832
1974
  if( !code )
1833
1975
  {
1834
1976
  this.addTab( name, true );
@@ -1856,15 +1998,66 @@ class CodeEditor {
1856
1998
 
1857
1999
  this.openedTabs[ name ] = code;
1858
2000
 
1859
- const isNewTabButton = ( name === '+' );
1860
- const tabIcon = this._getFileIcon( name );
2001
+ // Move into the sizer..
2002
+ this.codeSizer.appendChild( code );
1861
2003
 
1862
- this.tabs.add(name, code, {
1863
- selected: true,
1864
- fixed: isNewTabButton,
1865
- title: code.title,
1866
- icon: tabIcon,
1867
- onSelect: this._onSelectTab.bind( this, isNewTabButton ),
2004
+ this.endSelection();
2005
+
2006
+ // Select as current...
2007
+ this.code = code;
2008
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP );
2009
+ this.processLines();
2010
+ this._changeLanguageFromExtension( LX.getExtension( name ) );
2011
+ this._updateDataInfoPanel( "@tab-name", code.tabName );
2012
+ }
2013
+
2014
+ loadTab( name ) {
2015
+
2016
+ // Already open...
2017
+ if( this.openedTabs[ name ] )
2018
+ {
2019
+ this.tabs.select( name );
2020
+ return;
2021
+ }
2022
+
2023
+ let code = this.loadedTabs[ name ]
2024
+
2025
+ if( !code )
2026
+ {
2027
+ this.addTab( name, true );
2028
+
2029
+ // Unload lines from storage...
2030
+ const tabData = this._tabStorage[ name ];
2031
+ if( tabData )
2032
+ {
2033
+ this.code.lines = tabData.lines;
2034
+
2035
+ if( tabData.options.language )
2036
+ {
2037
+ this._changeLanguage( tabData.options.language, null, true );
2038
+ }
2039
+ else
2040
+ {
2041
+ this._changeLanguageFromExtension( LX.getExtension( name ) );
2042
+ }
2043
+
2044
+ delete this._tabStorage[ name ];
2045
+ }
2046
+
2047
+ return;
2048
+ }
2049
+
2050
+ this.openedTabs[ name ] = code;
2051
+
2052
+ const isNewTabButton = ( name === '+' );
2053
+ const tabIcon = this._getFileIcon( name );
2054
+
2055
+ this.tabs.add(name, code, {
2056
+ selected: true,
2057
+ fixed: isNewTabButton,
2058
+ title: code.title,
2059
+ icon: tabIcon,
2060
+ onSelect: this._onSelectTab.bind( this, isNewTabButton ),
1868
2061
  onContextMenu: this._onContextMenuTab.bind( this, isNewTabButton ),
1869
2062
  allowDelete: true
1870
2063
  });
@@ -1899,12 +2092,14 @@ class CodeEditor {
1899
2092
  }
1900
2093
 
1901
2094
  loadTabFromFile() {
2095
+
1902
2096
  const input = document.createElement( 'input' );
1903
2097
  input.type = 'file';
1904
2098
  document.body.appendChild( input );
1905
2099
  input.click();
1906
2100
  input.addEventListener('change', e => {
1907
- if (e.target.files[ 0 ]) {
2101
+ if (e.target.files[ 0 ])
2102
+ {
1908
2103
  this.loadFile( e.target.files[ 0 ] );
1909
2104
  }
1910
2105
  input.remove();
@@ -1914,8 +2109,11 @@ class CodeEditor {
1914
2109
  processFocus( active ) {
1915
2110
 
1916
2111
  if( active )
2112
+ {
1917
2113
  this.restartBlink();
1918
- else {
2114
+ }
2115
+ else
2116
+ {
1919
2117
  clearInterval( this.blinker );
1920
2118
  this.cursors.classList.remove( 'show' );
1921
2119
  }
@@ -1923,10 +2121,10 @@ class CodeEditor {
1923
2121
 
1924
2122
  processMouse( e ) {
1925
2123
 
1926
- if( !e.target.classList.contains('code') && !e.target.classList.contains('codetabsarea') ) return;
2124
+ if( !e.target.classList.contains('code') && !e.target.classList.contains('lexcodearea') ) return;
1927
2125
  if( !this.code ) return;
1928
2126
 
1929
- var cursor = this._getCurrentCursor();
2127
+ var cursor = this.getCurrentCursor();
1930
2128
  var code_rect = this.code.getBoundingClientRect();
1931
2129
  var mouse_pos = [(e.clientX - code_rect.x), (e.clientY - code_rect.y)];
1932
2130
 
@@ -2021,14 +2219,17 @@ class CodeEditor {
2021
2219
 
2022
2220
  _onMouseUp( e ) {
2023
2221
 
2024
- if( (LX.getTime() - this.lastMouseDown) < 120 ) {
2222
+ if( ( LX.getTime() - this.lastMouseDown ) < 120 )
2223
+ {
2025
2224
  this.state.selectingText = false;
2026
2225
  this.endSelection();
2027
2226
  }
2028
2227
 
2029
- const cursor = this._getCurrentCursor();
2228
+ const cursor = this.getCurrentCursor();
2030
2229
  if( cursor.selection )
2230
+ {
2031
2231
  cursor.selection.invertIfNecessary();
2232
+ }
2032
2233
 
2033
2234
  this.state.selectingText = false;
2034
2235
  delete this._lastSelectionKeyDir;
@@ -2036,7 +2237,7 @@ class CodeEditor {
2036
2237
 
2037
2238
  processClick( e ) {
2038
2239
 
2039
- var cursor = this._getCurrentCursor();
2240
+ var cursor = this.getCurrentCursor();
2040
2241
  var code_rect = this.codeScroller.getBoundingClientRect();
2041
2242
  var position = [( e.clientX - code_rect.x ) + this.getScrollLeft(), (e.clientY - code_rect.y) + this.getScrollTop()];
2042
2243
  var ln = (position[ 1 ] / this.lineHeight)|0;
@@ -2053,7 +2254,6 @@ class CodeEditor {
2053
2254
  {
2054
2255
  // Make sure we only keep the main cursor..
2055
2256
  this._removeSecondaryCursors();
2056
-
2057
2257
  this.cursorToLine( cursor, ln, true );
2058
2258
  this.cursorToPosition( cursor, string.length );
2059
2259
  }
@@ -2067,38 +2267,45 @@ class CodeEditor {
2067
2267
  this.hideAutoCompleteBox();
2068
2268
  }
2069
2269
 
2070
- updateSelections( e, keep_range, flags = CodeEditor.SELECTION_X_Y ) {
2270
+ updateSelections( e, keepRange, flags = CodeEditor.SELECTION_X_Y ) {
2071
2271
 
2072
2272
  for( let cursor of this.cursors.children )
2073
2273
  {
2074
2274
  if( !cursor.selection )
2275
+ {
2075
2276
  continue;
2277
+ }
2076
2278
 
2077
- this._processSelection( cursor, e, keep_range, flags );
2279
+ this._processSelection( cursor, e, keepRange, flags );
2078
2280
  }
2079
2281
  }
2080
2282
 
2081
- processSelections( e, keep_range, flags = CodeEditor.SELECTION_X_Y ) {
2283
+ processSelections( e, keepRange, flags = CodeEditor.SELECTION_X_Y ) {
2082
2284
 
2083
2285
  for( let cursor of this.cursors.children )
2084
2286
  {
2085
- this._processSelection( cursor, e, keep_range, flags );
2287
+ this._processSelection( cursor, e, keepRange, flags );
2086
2288
  }
2087
2289
  }
2088
2290
 
2089
- _processSelection( cursor, e, keep_range, flags = CodeEditor.SELECTION_X_Y ) {
2291
+ _processSelection( cursor, e, keepRange, flags = CodeEditor.SELECTION_X_Y ) {
2090
2292
 
2091
2293
  const isMouseEvent = e && ( e.constructor == MouseEvent );
2092
2294
 
2093
- if( isMouseEvent ) this.processClick( e );
2295
+ if( isMouseEvent )
2296
+ {
2297
+ this.processClick( e );
2298
+ }
2094
2299
 
2095
2300
  if( !cursor.selection )
2301
+ {
2096
2302
  this.startSelection( cursor );
2303
+ }
2097
2304
 
2098
2305
  this._hideActiveLine();
2099
2306
 
2100
2307
  // Update selection
2101
- if( !keep_range )
2308
+ if( !keepRange )
2102
2309
  {
2103
2310
  let ccw = true;
2104
2311
 
@@ -2165,19 +2372,19 @@ class CodeEditor {
2165
2372
  }
2166
2373
 
2167
2374
  // Compute new width and selection margins
2168
- let string;
2375
+ let string = "";
2169
2376
 
2170
- if(sId == 0) // First line 2 cases (single line, multiline)
2377
+ if( sId == 0 ) // First line 2 cases (single line, multiline)
2171
2378
  {
2172
2379
  const reverse = fromX > toX;
2173
2380
  if(deltaY == 0) string = !reverse ? this.code.lines[ i ].substring( fromX, toX ) : this.code.lines[ i ].substring(toX, fromX);
2174
2381
  else string = this.code.lines[ i ].substr( fromX );
2175
- const pixels = (reverse && deltaY == 0 ? toX : fromX) * this.charWidth;
2176
- if( isVisible ) domEl.style.left = "calc(" + pixels + "px + " + this.xPadding + ")";
2382
+ const pixels = ( reverse && deltaY == 0 ? toX : fromX ) * this.charWidth;
2383
+ if( isVisible ) domEl.style.left = `calc(${ pixels }px + ${ this.xPadding })`;
2177
2384
  }
2178
2385
  else
2179
2386
  {
2180
- string = (i == toY) ? this.code.lines[ i ].substring( 0, toX ) : this.code.lines[ i ]; // Last line, any multiple line...
2387
+ string = ( i == toY ) ? this.code.lines[ i ].substring( 0, toX ) : this.code.lines[ i ]; // Last line, any multiple line...
2181
2388
  if( isVisible ) domEl.style.left = this.xPadding;
2182
2389
  }
2183
2390
 
@@ -2186,7 +2393,7 @@ class CodeEditor {
2186
2393
 
2187
2394
  if( isVisible )
2188
2395
  {
2189
- domEl.style.width = (stringWidth || 8) + "px";
2396
+ domEl.style.width = ( stringWidth || 8 ) + "px";
2190
2397
  domEl._top = i * this.lineHeight;
2191
2398
  domEl.style.top = domEl._top + "px";
2192
2399
  }
@@ -2207,7 +2414,7 @@ class CodeEditor {
2207
2414
  {
2208
2415
  // Make sure that the line selection is generated...
2209
2416
  domEl = cursorSelections.childNodes[ sId ];
2210
- if(!domEl)
2417
+ if( !domEl )
2211
2418
  {
2212
2419
  domEl = document.createElement( 'div' );
2213
2420
  domEl.className = "lexcodeselection";
@@ -2326,7 +2533,7 @@ class CodeEditor {
2326
2533
 
2327
2534
  _processGlobalKeys( e, key ) {
2328
2535
 
2329
- let cursor = this._getCurrentCursor();
2536
+ let cursor = this.getCurrentCursor();
2330
2537
 
2331
2538
  if( e.ctrlKey || e.metaKey )
2332
2539
  {
@@ -2394,7 +2601,7 @@ class CodeEditor {
2394
2601
 
2395
2602
  async _processKeyAtCursor( e, key, cursor ) {
2396
2603
 
2397
- const skip_undo = e.detail.skip_undo ?? false;
2604
+ const skipUndo = e.detail.skipUndo ?? false;
2398
2605
 
2399
2606
  // keys with length > 1 are probably special keys
2400
2607
  if( key.length > 1 && this.specialKeys.indexOf( key ) == -1 )
@@ -2484,7 +2691,7 @@ class CodeEditor {
2484
2691
 
2485
2692
  // Add undo steps
2486
2693
 
2487
- if( !skip_undo && this.code.lines.length )
2694
+ if( !skipUndo && this.code.lines.length )
2488
2695
  {
2489
2696
  this._addUndoStep( cursor );
2490
2697
  }
@@ -2545,17 +2752,7 @@ class CodeEditor {
2545
2752
  this.processLine( lidx );
2546
2753
 
2547
2754
  // We are out of the viewport and max length is different? Resize scrollbars...
2548
- const maxLineLength = this.getMaxLineLength();
2549
- const numViewportChars = Math.floor( ( this.codeScroller.clientWidth - CodeEditor.LINE_GUTTER_WIDTH ) / this.charWidth );
2550
- if( maxLineLength >= numViewportChars && maxLineLength != this._lastMaxLineLength )
2551
- {
2552
- this.resize( maxLineLength, () => {
2553
- if( cursor.position > numViewportChars )
2554
- {
2555
- this.setScrollLeft( cursor.position * this.charWidth );
2556
- }
2557
- } );
2558
- }
2755
+ this.resizeIfNecessary( cursor );
2559
2756
 
2560
2757
  // Manage autocomplete
2561
2758
 
@@ -2567,6 +2764,8 @@ class CodeEditor {
2567
2764
 
2568
2765
  async _pasteContent( cursor ) {
2569
2766
 
2767
+ const mustDetectLanguage = ( !this.getText().length );
2768
+
2570
2769
  let text = await navigator.clipboard.readText();
2571
2770
 
2572
2771
  // Remove any possible tabs (\t) and add spaces
@@ -2579,9 +2778,19 @@ class CodeEditor {
2579
2778
  const currentScroll = this.getScrollTop();
2580
2779
  const scroll = Math.max( cursor.line * this.lineHeight - this.codeScroller.offsetWidth, 0 );
2581
2780
 
2582
- if( currentScroll < scroll ) {
2781
+ if( currentScroll < scroll )
2782
+ {
2583
2783
  this.codeScroller.scrollTo( 0, cursor.line * this.lineHeight );
2584
2784
  }
2785
+
2786
+ if( mustDetectLanguage )
2787
+ {
2788
+ const detectedLang = this._detectLanguage( text );
2789
+ if( detectedLang )
2790
+ {
2791
+ this._changeLanguage( detectedLang );
2792
+ }
2793
+ }
2585
2794
  }
2586
2795
 
2587
2796
  async _copyContent( cursor ) {
@@ -2604,7 +2813,9 @@ class CodeEditor {
2604
2813
  let index = 0;
2605
2814
 
2606
2815
  for( let i = 0; i <= cursor.selection.fromY; i++ )
2816
+ {
2607
2817
  index += ( i == cursor.selection.fromY ? cursor.selection.fromX : this.code.lines[ i ].length );
2818
+ }
2608
2819
 
2609
2820
  index += cursor.selection.fromY * separator.length;
2610
2821
  const num_chars = cursor.selection.chars + ( cursor.selection.toY - cursor.selection.fromY ) * separator.length;
@@ -2680,7 +2891,7 @@ class CodeEditor {
2680
2891
 
2681
2892
  if( cursor.selection )
2682
2893
  {
2683
- var cursor = this._getCurrentCursor();
2894
+ var cursor = this.getCurrentCursor();
2684
2895
  this._addUndoStep( cursor, true );
2685
2896
 
2686
2897
  const selectedLines = this.code.lines.slice( cursor.selection.fromY, cursor.selection.toY );
@@ -2709,7 +2920,7 @@ class CodeEditor {
2709
2920
 
2710
2921
  _commentLine( cursor, line, minNonspaceIdx ) {
2711
2922
 
2712
- const lang = this.languages[ this.highlight ];
2923
+ const lang = CodeEditor.languages[ this.highlight ];
2713
2924
 
2714
2925
  if( !( lang.singleLineComments ?? true ))
2715
2926
  return;
@@ -2739,7 +2950,7 @@ class CodeEditor {
2739
2950
 
2740
2951
  if( cursor.selection )
2741
2952
  {
2742
- var cursor = this._getCurrentCursor();
2953
+ var cursor = this.getCurrentCursor();
2743
2954
  this._addUndoStep( cursor, true );
2744
2955
 
2745
2956
  for( var i = cursor.selection.fromY; i <= cursor.selection.toY; ++i )
@@ -2762,7 +2973,7 @@ class CodeEditor {
2762
2973
 
2763
2974
  _uncommentLine( cursor, line ) {
2764
2975
 
2765
- const lang = this.languages[ this.highlight ];
2976
+ const lang = CodeEditor.languages[ this.highlight ];
2766
2977
 
2767
2978
  if( !( lang.singleLineComments ?? true ))
2768
2979
  return;
@@ -2793,7 +3004,6 @@ class CodeEditor {
2793
3004
  }
2794
3005
 
2795
3006
  _actionMustDelete( cursor, action, e ) {
2796
-
2797
3007
  return cursor.selection && action.deleteSelection &&
2798
3008
  ( action.eventSkipDelete ? !e[ action.eventSkipDelete ] : true );
2799
3009
  }
@@ -2804,26 +3014,24 @@ class CodeEditor {
2804
3014
 
2805
3015
  for( let i = 0; i < this.code.lines.length; ++i )
2806
3016
  {
2807
- const linestring = this.code.lines[ i ];
2808
- const tokens = this._getTokensFromLine( linestring, true );
3017
+ const lineString = this.code.lines[ i ];
3018
+ const tokens = this._getTokensFromLine( lineString, true );
2809
3019
  tokens.forEach( t => this.code.tokens[ t ] = 1 );
2810
3020
  }
2811
3021
  }
2812
3022
 
2813
3023
  toLocalLine( line ) {
2814
-
2815
3024
  const d = Math.max( this.firstLineInViewport - this.lineScrollMargin.x, 0 );
2816
3025
  return Math.min( Math.max( line - d, 0 ), this.code.lines.length - 1 );
2817
3026
  }
2818
3027
 
2819
3028
  getMaxLineLength() {
2820
-
2821
3029
  return Math.max(...this.code.lines.map( v => v.length ));
2822
3030
  }
2823
3031
 
2824
3032
  processLines( mode ) {
2825
3033
 
2826
- var code_html = "";
3034
+ var htmlCode = "";
2827
3035
  this._blockCommentCache.length = 0;
2828
3036
 
2829
3037
  // Reset all lines content
@@ -2833,7 +3041,7 @@ class CodeEditor {
2833
3041
  const lastScrollTop = this.getScrollTop();
2834
3042
  this.firstLineInViewport = ( mode ?? CodeEditor.KEEP_VISIBLE_LINES ) & CodeEditor.UPDATE_VISIBLE_LINES ?
2835
3043
  ( (lastScrollTop / this.lineHeight)|0 ) : this.firstLineInViewport;
2836
- const totalLinesInViewport = ((this.codeScroller.offsetHeight - 36) / this.lineHeight)|0;
3044
+ const totalLinesInViewport = ( ( this.codeScroller.offsetHeight ) / this.lineHeight )|0;
2837
3045
  this.visibleLinesViewport = new LX.vec2(
2838
3046
  Math.max( this.firstLineInViewport - this.lineScrollMargin.x, 0 ),
2839
3047
  Math.min( this.firstLineInViewport + totalLinesInViewport + this.lineScrollMargin.y, this.code.lines.length )
@@ -2843,16 +3051,20 @@ class CodeEditor {
2843
3051
  {
2844
3052
  const diff = Math.max( this.code.lines.length - this.visibleLinesViewport.y, 0 );
2845
3053
  if( diff <= this.lineScrollMargin.y )
3054
+ {
2846
3055
  this.visibleLinesViewport.y += diff;
3056
+ }
2847
3057
  }
2848
3058
 
3059
+ this._scopeStack = [ { name: "", type: "global" } ];
3060
+
2849
3061
  // Process visible lines
2850
3062
  for( let i = this.visibleLinesViewport.x; i < this.visibleLinesViewport.y; ++i )
2851
3063
  {
2852
- code_html += this.processLine( i, true );
3064
+ htmlCode += this.processLine( i, true );
2853
3065
  }
2854
3066
 
2855
- this.code.innerHTML = code_html;
3067
+ this.code.innerHTML = htmlCode;
2856
3068
 
2857
3069
  // Update scroll data
2858
3070
  this.codeScroller.scrollTop = lastScrollTop;
@@ -2866,37 +3078,34 @@ class CodeEditor {
2866
3078
  this.resize();
2867
3079
  }
2868
3080
 
2869
- processLine( linenum, force ) {
3081
+ processLine( lineNumber, force, skipPropagation ) {
2870
3082
 
2871
3083
  // Check if we are in block comment sections..
2872
- if( !force && this._inBlockCommentSection( linenum ) )
3084
+ if( !force && this._inBlockCommentSection( lineNumber ) )
2873
3085
  {
2874
3086
  this.processLines();
2875
3087
  return;
2876
3088
  }
2877
3089
 
2878
- const lang = this.languages[ this.highlight ];
2879
- const localLineNum = this.toLocalLine( linenum );
2880
- const gutterLineHtml = "<span class='line-gutter'>" + (linenum + 1) + "</span>";
2881
-
2882
- const UPDATE_LINE = ( html ) => {
2883
- if( !force ) // Single line update
2884
- {
2885
- this.code.childNodes[ localLineNum ].innerHTML = gutterLineHtml + html;
2886
- this._setActiveLine( linenum );
2887
- this._clearTmpVariables();
2888
- }
2889
- else // Update all lines at once
2890
- return "<pre>" + ( gutterLineHtml + html ) + "</pre>";
3090
+ if( this._scopeStack )
3091
+ {
3092
+ this.code.lineScopes[ lineNumber ] = [ ...this._scopeStack ];
3093
+ }
3094
+ else
3095
+ {
3096
+ this.code.lineScopes[ lineNumber ] = this.code.lineScopes[ lineNumber ] ?? [];
3097
+ this._scopeStack = [ ...this.code.lineScopes[ lineNumber ] ];
2891
3098
  }
2892
3099
 
3100
+ const lang = CodeEditor.languages[ this.highlight ];
3101
+ const localLineNum = this.toLocalLine( lineNumber );
3102
+ const lineString = this.code.lines[ lineNumber ];
3103
+
2893
3104
  // multi-line strings not supported by now
2894
3105
  delete this._buildingString;
2895
3106
  delete this._pendingString;
2896
3107
  delete this._markdownHeader;
2897
3108
 
2898
- let linestring = this.code.lines[ linenum ];
2899
-
2900
3109
  // Single line
2901
3110
  if( !force )
2902
3111
  {
@@ -2907,35 +3116,37 @@ class CodeEditor {
2907
3116
  // Early out check for no highlighting languages
2908
3117
  if( this.highlight == 'Plain Text' )
2909
3118
  {
2910
- const plainTextHtml = linestring.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
2911
- return UPDATE_LINE( plainTextHtml );
3119
+ const plainTextHtml = lineString.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
3120
+ return this._updateLine( force, lineNumber, plainTextHtml, skipPropagation );
2912
3121
  }
2913
3122
 
2914
- this._currentLineNumber = linenum;
2915
- this._currentLineString = linestring;
2916
-
2917
- const tokensToEvaluate = this._getTokensFromLine( linestring );
3123
+ this._currentLineNumber = lineNumber;
3124
+ this._currentLineString = lineString;
2918
3125
 
3126
+ const tokensToEvaluate = this._getTokensFromLine( lineString );
2919
3127
  if( !tokensToEvaluate.length )
2920
3128
  {
2921
- return "<pre><span class='line-gutter'>" + linenum + "</span></pre>";
3129
+ return this._updateLine( force, lineNumber, "", skipPropagation );
2922
3130
  }
2923
3131
 
2924
- var lineInnerHtml = "";
3132
+ let lineInnerHtml = "";
3133
+ let pushedScope = false;
2925
3134
 
2926
3135
  // Process all tokens
2927
- for( var i = 0; i < tokensToEvaluate.length; ++i )
3136
+ for( let i = 0; i < tokensToEvaluate.length; ++i )
2928
3137
  {
2929
3138
  let it = i - 1;
2930
3139
  let prev = tokensToEvaluate[ it ];
2931
- while( prev == ' ' ) {
3140
+ while( prev == ' ' )
3141
+ {
2932
3142
  it--;
2933
3143
  prev = tokensToEvaluate[ it ];
2934
3144
  }
2935
3145
 
2936
3146
  it = i + 1;
2937
3147
  let next = tokensToEvaluate[ it ];
2938
- while( next == ' ' || next == '"' ) {
3148
+ while( next == ' ' || next == '"' )
3149
+ {
2939
3150
  it++;
2940
3151
  next = tokensToEvaluate[ it ];
2941
3152
  }
@@ -2946,7 +3157,15 @@ class CodeEditor {
2946
3157
  {
2947
3158
  const blockCommentsToken = ( lang.blockCommentsTokens ?? this.defaultBlockCommentTokens )[ 0 ];
2948
3159
  if( token.substr( 0, blockCommentsToken.length ) == blockCommentsToken )
2949
- this._buildingBlockComment = linenum;
3160
+ {
3161
+ this._buildingBlockComment = lineNumber;
3162
+ }
3163
+ }
3164
+
3165
+ // Pop current scope if necessary
3166
+ if( token === "}" && this._scopeStack.length > 1 )
3167
+ {
3168
+ this._scopeStack.pop();
2950
3169
  }
2951
3170
 
2952
3171
  lineInnerHtml += this._evaluateToken( {
@@ -2960,20 +3179,323 @@ class CodeEditor {
2960
3179
  isLastToken: (i == tokensToEvaluate.length - 1),
2961
3180
  tokens: tokensToEvaluate
2962
3181
  } );
3182
+
3183
+ if( token !== "{" )
3184
+ {
3185
+ continue;
3186
+ }
3187
+
3188
+ // Store current scopes
3189
+
3190
+ // Get some context about the scope from previous lines
3191
+ let contextTokens = [
3192
+ ...this._getTokensFromLine( this.code.lines[ lineNumber ].substring( 0, lineString.indexOf( token ) ) )
3193
+ ];
3194
+
3195
+ for( let k = 1; k < 50; k++ )
3196
+ {
3197
+ let kLineString = this.code.lines[ lineNumber - k ];
3198
+ if( !kLineString ) break;
3199
+ const closeIdx = kLineString.lastIndexOf( '}' );
3200
+ if( closeIdx > -1 )
3201
+ {
3202
+ kLineString = kLineString.substr( closeIdx );
3203
+ }
3204
+
3205
+ contextTokens = [ ...this._getTokensFromLine( kLineString ), ...contextTokens ];
3206
+
3207
+ if( kLineString.length !== this.code.lines[ lineNumber - k ] )
3208
+ {
3209
+ break;
3210
+ }
3211
+ }
3212
+
3213
+ contextTokens = contextTokens.reverse().filter( v => v.length && v != ' ' );
3214
+
3215
+ // Keywords that can open a *named* scope
3216
+ // TODO: Do this per language
3217
+ const scopeKeywords = ["class", "enum", "function", "interface", "type", "struct", "namespace"];
3218
+
3219
+ let scopeType = null; // This is the type of scope (function, class, enum, etc)
3220
+ let scopeName = null;
3221
+
3222
+ for( let i = 0; i < contextTokens.length; i++ )
3223
+ {
3224
+ const t = contextTokens[ i ];
3225
+
3226
+ if ( scopeKeywords.includes( t ) )
3227
+ {
3228
+ scopeType = t;
3229
+ scopeName = contextTokens[ i - 1 ]; // usually right before the keyword in reversed array
3230
+ break;
3231
+ }
3232
+ }
3233
+
3234
+ // Special case: enum type specification `enum Foo : int {`
3235
+ if( scopeType === "enum" && contextTokens.includes( ":" ) )
3236
+ {
3237
+ const colonIndex = contextTokens.indexOf( ":" );
3238
+ scopeName = contextTokens[ colonIndex + 1 ] || scopeName;
3239
+ }
3240
+
3241
+ if( !scopeType )
3242
+ {
3243
+ const parOpenIndex = contextTokens.indexOf( "(" );
3244
+ scopeName = contextTokens[ parOpenIndex + 1 ] || scopeName;
3245
+
3246
+ if( scopeName )
3247
+ {
3248
+ scopeType = "method";
3249
+ }
3250
+ }
3251
+
3252
+ if( scopeType )
3253
+ {
3254
+ this._scopeStack.push( { name: scopeName ?? "", type: scopeType } );
3255
+ }
3256
+ else
3257
+ {
3258
+ this._scopeStack.push( { name: "", type: "anonymous" } ); // anonymous scope
3259
+ }
3260
+
3261
+ pushedScope = true;
3262
+ }
3263
+
3264
+ const symbols = this._parseLineForSymbols( lineNumber, lineString, tokensToEvaluate, pushedScope );
3265
+ return this._updateLine( force, lineNumber, lineInnerHtml, skipPropagation, symbols );
3266
+ }
3267
+
3268
+ _processExtraLineIfNecessary( lineNumber, oldSymbols ) {
3269
+
3270
+ if( ( (lineNumber + 1) === this.code.lines.length ) || !this.code.lineScopes[ lineNumber + 1 ] )
3271
+ {
3272
+ return;
3273
+ }
3274
+
3275
+ if( !this._scopeStack )
3276
+ {
3277
+ console.warn( "CodeEditor: No scope available" );
3278
+ return;
3279
+ }
3280
+
3281
+ // Only update scope stack if something changed when editing a single line
3282
+ if( codeScopesEqual( this._scopeStack, this.code.lineScopes[ lineNumber + 1 ] ) )
3283
+ {
3284
+ // First check for occurrencies of the old symbols, to reprocess that lines
3285
+
3286
+ for( const sym of oldSymbols )
3287
+ {
3288
+ const tableSymbol = this.code.symbolsTable.get( sym.name );
3289
+ if( tableSymbol === undefined )
3290
+ {
3291
+ return;
3292
+ }
3293
+
3294
+ for( const occ of tableSymbol )
3295
+ {
3296
+ if( occ.line === lineNumber )
3297
+ {
3298
+ continue;
3299
+ }
3300
+
3301
+ this.processLine( occ.line, false, true );
3302
+ }
3303
+ }
3304
+
3305
+ return;
3306
+ }
3307
+
3308
+ this.code.lineScopes[ lineNumber + 1 ] = [ ...this._scopeStack ];
3309
+ this.processLine( lineNumber + 1 );
3310
+ }
3311
+
3312
+ _updateLine( force, lineNumber, html, skipPropagation, symbols = [] ) {
3313
+
3314
+ const gutterLineHtml = `<span class='line-gutter'>${ lineNumber + 1 }</span>`;
3315
+ const oldSymbols = this._updateLineSymbols( lineNumber, symbols );
3316
+ const lineScope = CodeEditor.debugScopes ? this.code.lineScopes[ lineNumber ].map( s => `${ s.type }` ).join( ", " ) : "";
3317
+ const lineSymbols = CodeEditor.debugSymbols ? this.code.lineSymbols[ lineNumber ].map( s => `${ s.name }(${ s.kind })` ).join( ", " ) : "";
3318
+ const debugString = lineScope + ( lineScope.length ? " - " : "" ) + lineSymbols;
3319
+
3320
+ if( !force ) // Single line update
3321
+ {
3322
+ this.code.childNodes[ this.toLocalLine( lineNumber ) ].innerHTML = ( gutterLineHtml + html + debugString );
3323
+
3324
+ if( !skipPropagation )
3325
+ {
3326
+ this._processExtraLineIfNecessary( lineNumber, oldSymbols );
3327
+ }
3328
+
3329
+ this._setActiveLine( lineNumber );
3330
+ this._clearTmpVariables();
3331
+ }
3332
+ else // Update all lines at once
3333
+ {
3334
+
3335
+ return `<pre>${ ( gutterLineHtml + html + debugString ) }</pre>`;
3336
+ }
3337
+ }
3338
+
3339
+ /**
3340
+ * Parses a single line of code and extract declared symbols
3341
+ */
3342
+ _parseLineForSymbols( lineNumber, lineString, tokens, pushedScope ) {
3343
+
3344
+ const scope = this._scopeStack.at( pushedScope ? -2 : -1 );
3345
+
3346
+ if( !scope )
3347
+ {
3348
+ return [];
3349
+ }
3350
+
3351
+ const scopeName = scope.name;
3352
+ const scopeType = scope.type;
3353
+ const symbols = [];
3354
+ const text = lineString.trim();
3355
+
3356
+ // Don't make symbols from preprocessor lines
3357
+ if( text.startsWith( "#" ) )
3358
+ {
3359
+ return [];
3360
+ }
3361
+
3362
+ const topLevelRegexes = [
3363
+ [/^class\s+([A-Za-z0-9_]+)/, "class"],
3364
+ [/^struct\s+([A-Za-z0-9_]+)/, "struct"],
3365
+ [/^enum\s+([A-Za-z0-9_]+)/, "enum"],
3366
+ [/^interface\s+([A-Za-z0-9_]+)/, "interface"],
3367
+ [/^type\s+([A-Za-z0-9_]+)/, "type"],
3368
+ [/^function\s+([A-Za-z0-9_]+)/, "method"],
3369
+ [/^([A-Za-z0-9_]+)\s*=\s*\(?.*\)?\s*=>/, "method"] // arrow functions
3370
+ ];
3371
+
3372
+ {
3373
+ const nativeTypes = CodeEditor.nativeTypes[ this.highlight ];
3374
+ if( nativeTypes )
3375
+ {
3376
+ const nativeTypes = ['int', 'float', 'double', 'bool', 'long', 'short', 'char', 'wchar_t', 'void'];
3377
+ const regex = `^(?:${nativeTypes.join('|')})\\s+([A-Za-z0-9_]+)\s*[\(]+`;
3378
+ topLevelRegexes.push( [ new RegExp( regex ), 'method' ] );
3379
+ }
3380
+
3381
+ const declarationKeywords = CodeEditor.declarationKeywords[ this.highlight ] ?? [ "const", "let", "var" ];
3382
+ const regex = `^(?:${declarationKeywords.join('|')})\\s+([A-Za-z0-9_]+)`;
3383
+ topLevelRegexes.push( [ new RegExp( regex ), 'variable' ] );
3384
+ }
3385
+
3386
+ for( let [ regex, kind ] of topLevelRegexes )
3387
+ {
3388
+ const m = text.match( regex );
3389
+ if( m )
3390
+ {
3391
+ symbols.push( { name: m[ 1 ], kind, scope: scopeName, line: lineNumber } );
3392
+ break;
3393
+ }
3394
+ }
3395
+
3396
+ const usageRegexes = [
3397
+ [/new\s+([A-Za-z0-9_]+)\s*\(/, "constructor-call"],
3398
+ [/this.([A-Za-z_][A-Za-z0-9_]*)\s*\=/, "class-property"],
3399
+ ];
3400
+
3401
+ for( let [ regex, kind ] of usageRegexes )
3402
+ {
3403
+ const m = text.match( regex );
3404
+ if( m )
3405
+ {
3406
+ symbols.push( { name: m[ 1 ], kind, scope: scopeName, line: lineNumber } );
3407
+ }
3408
+ }
3409
+
3410
+ // Stop after matches for top-level declarations and usage symbols
3411
+ if( symbols.length )
3412
+ {
3413
+ return symbols;
3414
+ }
3415
+
3416
+ const nonWhiteSpaceTokens = tokens.filter( t => t.trim().length );
3417
+
3418
+ for( let i = 0; i < nonWhiteSpaceTokens.length; i++ )
3419
+ {
3420
+ const prev = nonWhiteSpaceTokens[ i - 1 ];
3421
+ const token = nonWhiteSpaceTokens[ i ];
3422
+ const next = nonWhiteSpaceTokens[ i + 1 ];
3423
+
3424
+ if( scopeType.startsWith("class") )
3425
+ {
3426
+ if( next === "(" && /^[a-zA-Z_]\w*$/.test( token ) && prev === undefined )
3427
+ {
3428
+ symbols.push( { name: token, kind: "method", scope: scopeName, line: lineNumber } );
3429
+ }
3430
+ }
3431
+ else if( scopeType.startsWith("enum") )
3432
+ {
3433
+ if( !isSymbol( token ) && !this._isNumber( token ) && !this._mustHightlightWord( token, CodeEditor.statements ) )
3434
+ {
3435
+ symbols.push({ name: token, kind: "enum_value", scope: scopeName, line: lineNumber });
3436
+ }
3437
+ }
3438
+ }
3439
+
3440
+ return symbols;
3441
+ }
3442
+
3443
+ /**
3444
+ * Updates the symbol table for a single line
3445
+ * - Removes old symbols from that line
3446
+ * - Inserts the new symbols
3447
+ */
3448
+ _updateLineSymbols( lineNumber, newSymbols ) {
3449
+
3450
+ this.code.lineSymbols[ lineNumber ] = this.code.lineSymbols[ lineNumber ] ?? [];
3451
+ const oldSymbols = LX.deepCopy( this.code.lineSymbols[ lineNumber ] );
3452
+
3453
+ // Clean old symbols from current line
3454
+ for( let sym of this.code.lineSymbols[ lineNumber ] )
3455
+ {
3456
+ let array = this.code.symbolsTable.get( sym.name );
3457
+ if( !array )
3458
+ {
3459
+ continue;
3460
+ }
3461
+
3462
+ array = array.filter( s => s.line !== lineNumber );
3463
+
3464
+ if( array.length )
3465
+ {
3466
+ this.code.symbolsTable.set( sym.name, array );
3467
+ }
3468
+ else
3469
+ {
3470
+ this.code.symbolsTable.delete( sym.name );
3471
+ }
2963
3472
  }
2964
3473
 
2965
- return UPDATE_LINE( lineInnerHtml );
3474
+ // Add new symbols to table
3475
+ for( let sym of newSymbols )
3476
+ {
3477
+ let arr = this.code.symbolsTable.get( sym.name ) ?? [];
3478
+ arr.push(sym);
3479
+ this.code.symbolsTable.set( sym.name, arr );
3480
+ }
3481
+
3482
+ // Keep lineSymbols in sync
3483
+ this.code.lineSymbols[ lineNumber ] = newSymbols;
3484
+
3485
+ return oldSymbols;
2966
3486
  }
2967
3487
 
2968
- _lineHasComment( linestring ) {
3488
+ _lineHasComment( lineString ) {
2969
3489
 
2970
- const lang = this.languages[ this.highlight ];
3490
+ const lang = CodeEditor.languages[ this.highlight ];
2971
3491
 
2972
3492
  if( !(lang.singleLineComments ?? true) )
3493
+ {
2973
3494
  return;
3495
+ }
2974
3496
 
2975
3497
  const singleLineCommentToken = lang.singleLineCommentToken ?? this.defaultSingleLineCommentToken;
2976
- const idx = linestring.indexOf( singleLineCommentToken );
3498
+ const idx = lineString.indexOf( singleLineCommentToken );
2977
3499
 
2978
3500
  if( idx > -1 )
2979
3501
  {
@@ -2982,22 +3504,27 @@ class CodeEditor {
2982
3504
  var err = false;
2983
3505
  err |= stringKeys.some( function(v) {
2984
3506
  var re = new RegExp( v, "g" );
2985
- var matches = (linestring.substring( 0, idx ).match( re ) || []);
3507
+ var matches = (lineString.substring( 0, idx ).match( re ) || []);
2986
3508
  return (matches.length % 2) !== 0;
2987
3509
  } );
2988
3510
  return err ? undefined : idx;
2989
3511
  }
2990
3512
  }
2991
3513
 
2992
- _getTokensFromLine( linestring, skipNonWords ) {
3514
+ _getTokensFromLine( lineString, skipNonWords ) {
3515
+
3516
+ if( !lineString || !lineString.length )
3517
+ {
3518
+ return [];
3519
+ }
2993
3520
 
2994
3521
  // Check if line comment
2995
- const ogLine = linestring;
2996
- const hasCommentIdx = this._lineHasComment( linestring );
3522
+ const ogLine = lineString;
3523
+ const hasCommentIdx = this._lineHasComment( lineString );
2997
3524
 
2998
3525
  if( hasCommentIdx != undefined )
2999
3526
  {
3000
- linestring = ogLine.substring( 0, hasCommentIdx );
3527
+ lineString = ogLine.substring( 0, hasCommentIdx );
3001
3528
  }
3002
3529
 
3003
3530
  let tokensToEvaluate = []; // store in a temp array so we know prev and next tokens...
@@ -3013,25 +3540,25 @@ class CodeEditor {
3013
3540
  charCounter += t.length;
3014
3541
  };
3015
3542
 
3016
- let iter = linestring.matchAll(/(<!--|-->|\*\/|\/\*|::|[\[\](){}<>.,;:*"'%@!/= ])/g);
3543
+ let iter = lineString.matchAll(/(<!--|-->|\*\/|\/\*|::|[\[\](){}<>.,;:*"'%@$!/= ])/g);
3017
3544
  let subtokens = iter.next();
3018
3545
  if( subtokens.value )
3019
3546
  {
3020
3547
  let idx = 0;
3021
3548
  while( subtokens.value != undefined )
3022
3549
  {
3023
- const _pt = linestring.substring(idx, subtokens.value.index);
3550
+ const _pt = lineString.substring(idx, subtokens.value.index);
3024
3551
  if( _pt.length ) pushToken( _pt );
3025
3552
  pushToken( subtokens.value[ 0 ] );
3026
3553
  idx = subtokens.value.index + subtokens.value[ 0 ].length;
3027
3554
  subtokens = iter.next();
3028
3555
  if(!subtokens.value) {
3029
- const _at = linestring.substring(idx);
3556
+ const _at = lineString.substring(idx);
3030
3557
  if( _at.length ) pushToken( _at );
3031
3558
  }
3032
3559
  }
3033
3560
  }
3034
- else pushToken( linestring );
3561
+ else pushToken( lineString );
3035
3562
 
3036
3563
  if( hasCommentIdx != undefined )
3037
3564
  {
@@ -3061,16 +3588,47 @@ class CodeEditor {
3061
3588
  // Scan for numbers again
3062
3589
  return this._processTokens( tokens, idx );
3063
3590
  }
3591
+
3592
+ const importantIdx = tokens.indexOf( 'important' );
3593
+ if( this.highlight == 'CSS' && importantIdx > -1 && tokens[ importantIdx - 1 ] === '!' )
3594
+ {
3595
+ tokens[ importantIdx - 1 ] = "!important";
3596
+ tokens.splice( importantIdx, 1 );
3597
+ }
3598
+ }
3599
+ else if( this.highlight == 'PHP' )
3600
+ {
3601
+ let offset = 0;
3602
+ let dollarIdx = tokens.indexOf( '$' );
3603
+
3604
+ while( dollarIdx > -1 )
3605
+ {
3606
+ const offsetIdx = dollarIdx + offset;
3607
+
3608
+ if( tokens[ offsetIdx + 1 ] === 'this-' )
3609
+ {
3610
+ tokens[ offsetIdx ] = "$this";
3611
+ tokens[ offsetIdx + 1 ] = "-";
3612
+ }
3613
+ else
3614
+ {
3615
+ tokens[ offsetIdx ] += ( tokens[ offsetIdx + 1 ] ?? "" );
3616
+ tokens.splice( offsetIdx + 1, 1 );
3617
+ }
3618
+
3619
+ dollarIdx = tokens.slice( offsetIdx ).indexOf( '$' );
3620
+ offset = offsetIdx;
3621
+ }
3064
3622
  }
3065
3623
 
3066
3624
  return tokens;
3067
3625
  }
3068
3626
 
3069
- _mustHightlightWord( token, kindArray, lang ) {
3627
+ _mustHightlightWord( token, wordCategory, lang ) {
3070
3628
 
3071
3629
  if( !lang )
3072
3630
  {
3073
- lang = this.languages[ this.highlight ];
3631
+ lang = CodeEditor.languages[ this.highlight ];
3074
3632
  }
3075
3633
 
3076
3634
  let t = token;
@@ -3080,21 +3638,36 @@ class CodeEditor {
3080
3638
  t = t.toLowerCase();
3081
3639
  }
3082
3640
 
3083
- return kindArray[ this.highlight ] && kindArray[ this.highlight ][ t ] != undefined;
3641
+ return wordCategory[ this.highlight ] && wordCategory[ this.highlight ].has( t );
3642
+ }
3643
+
3644
+ _getTokenHighlighting( ctx, highlight ) {
3645
+
3646
+ const rules = [ ...HighlightRules.common, ...( HighlightRules[ highlight ] || [] ), ...HighlightRules.post_common ];
3647
+
3648
+ for( const rule of rules )
3649
+ {
3650
+ if( !rule.test( ctx, this ) )
3651
+ {
3652
+ continue;
3653
+ }
3654
+
3655
+ const r = rule.action ? rule.action( ctx, this ) : undefined;
3656
+ if( rule.discard ) ctx.discardToken = r;
3657
+ return rule.className;
3658
+ }
3659
+
3660
+ return null;
3084
3661
  }
3085
3662
 
3086
3663
  _evaluateToken( ctxData ) {
3087
3664
 
3088
- let token = ctxData.token,
3089
- prev = ctxData.prev,
3090
- next = ctxData.next,
3091
- tokenIndex = ctxData.tokenIndex,
3092
- isFirstToken = ctxData.isFirstToken,
3093
- isLastToken = ctxData.isLastToken;
3665
+ let { token, prev, next, tokenIndex, isFirstToken, isLastToken } = ctxData;
3094
3666
 
3095
- const lang = this.languages[ this.highlight ],
3667
+ const lang = CodeEditor.languages[ this.highlight ],
3096
3668
  highlight = this.highlight.replace( /\s/g, '' ).replaceAll( "+", "p" ).toLowerCase(),
3097
- customStringKeys = Object.assign( {}, this.stringKeys );
3669
+ customStringKeys = Object.assign( {}, this.stringKeys ),
3670
+ lineNumber = this._currentLineNumber;
3098
3671
 
3099
3672
  var usePreviousTokenToCheckString = false;
3100
3673
 
@@ -3109,102 +3682,57 @@ class CodeEditor {
3109
3682
  customStringKeys['@['] = ']';
3110
3683
  }
3111
3684
 
3112
- // Manage strings
3113
- this._stringEnded = false;
3114
-
3115
- if( usePreviousTokenToCheckString || ( this._buildingBlockComment === undefined && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) ) )
3116
- {
3117
- const checkIfStringEnded = t => {
3118
- const idx = Object.values( customStringKeys ).indexOf( t );
3119
- this._stringEnded = (idx > -1) && (idx == Object.values(customStringKeys).indexOf( customStringKeys[ '@' + this._buildingString ] ));
3120
- };
3121
-
3122
- if( this._buildingString != undefined )
3123
- {
3124
- checkIfStringEnded( usePreviousTokenToCheckString ? ctxData.nextWithSpaces : token );
3125
- }
3126
- else if( customStringKeys[ '@' + ( usePreviousTokenToCheckString ? ctxData.prevWithSpaces : token ) ] )
3127
- {
3128
- // Start new string
3129
- this._buildingString = ( usePreviousTokenToCheckString ? ctxData.prevWithSpaces : token );
3130
-
3131
- // Check if string ended in same token using next...
3132
- if( usePreviousTokenToCheckString )
3133
- {
3134
- checkIfStringEnded( ctxData.nextWithSpaces );
3135
- }
3136
- }
3137
- }
3138
-
3139
- const usesBlockComments = lang.blockComments ?? true;
3140
- const blockCommentsTokens = lang.blockCommentsTokens ?? this.defaultBlockCommentTokens;
3141
- const singleLineCommentToken = lang.singleLineCommentToken ?? this.defaultSingleLineCommentToken;
3142
-
3143
- let token_classname = "";
3144
- let discardToken = false;
3145
-
3146
- if( this._buildingBlockComment != undefined )
3147
- token_classname = "cm-com";
3148
-
3149
- else if( this._buildingString != undefined )
3150
- discardToken = this._appendStringToken( token );
3151
-
3152
- else if( this._isKeyword( ctxData, lang ) )
3153
- token_classname = "cm-kwd";
3154
-
3155
- else if( this._mustHightlightWord( token, CodeEditor.builtIn, lang ) && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) )
3156
- token_classname = "cm-bln";
3157
-
3158
- else if( this._mustHightlightWord( token, CodeEditor.statementsAndDeclarations, lang ) )
3159
- token_classname = "cm-std";
3160
-
3161
- else if( this._mustHightlightWord( token, CodeEditor.symbols, lang ) )
3162
- token_classname = "cm-sym";
3163
-
3164
- else if( token.substr( 0, singleLineCommentToken.length ) == singleLineCommentToken )
3165
- token_classname = "cm-com";
3166
-
3167
- else if( this._isNumber( token ) || this._isNumber( token.replace(/[px]|[em]|%/g,'') ) )
3168
- token_classname = "cm-dec";
3169
-
3170
- else if( this._isCSSClass( ctxData ) )
3171
- token_classname = "cm-kwd";
3172
-
3173
- else if ( this._isType( ctxData, lang ) )
3174
- token_classname = "cm-typ";
3175
-
3176
- else if ( highlight == 'batch' && ( token == '@' || prev == ':' || prev == '@' ) )
3177
- token_classname = "cm-kwd";
3178
-
3179
- else if ( [ 'cpp', 'c', 'wgsl', 'glsl' ].indexOf( highlight ) > -1 && token.includes( '#' ) ) // C++ preprocessor
3180
- token_classname = "cm-ppc";
3181
-
3182
- else if ( highlight == 'cpp' && prev == '<' && (next == '>' || next == '*') ) // Defining template type in C++
3183
- token_classname = "cm-typ";
3184
-
3185
- else if ( highlight == 'cpp' && (next == '::' || prev == '::' && next != '(' )) // C++ Class
3186
- token_classname = "cm-typ";
3685
+ // Manage strings
3686
+ this._stringEnded = false;
3187
3687
 
3188
- else if ( highlight == 'css' && prev == ':' && (next == ';' || next == '!important') ) // CSS value
3189
- token_classname = "cm-str";
3688
+ if( usePreviousTokenToCheckString || ( this._buildingBlockComment === undefined && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) ) )
3689
+ {
3690
+ const _checkIfStringEnded = t => {
3691
+ const idx = Object.values( customStringKeys ).indexOf( t );
3692
+ this._stringEnded = (idx > -1) && (idx == Object.values(customStringKeys).indexOf( customStringKeys[ '@' + this._buildingString ] ));
3693
+ };
3190
3694
 
3191
- else if ( highlight == 'css' && prev == undefined && next == ':' ) // CSS attribute
3192
- token_classname = "cm-typ";
3695
+ if( this._buildingString != undefined )
3696
+ {
3697
+ _checkIfStringEnded( usePreviousTokenToCheckString ? ctxData.nextWithSpaces : token );
3698
+ }
3699
+ else if( customStringKeys[ '@' + ( usePreviousTokenToCheckString ? ctxData.prevWithSpaces : token ) ] )
3700
+ {
3701
+ // Start new string
3702
+ this._buildingString = ( usePreviousTokenToCheckString ? ctxData.prevWithSpaces : token );
3193
3703
 
3194
- else if ( this._markdownHeader || ( highlight == 'markdown' && isFirstToken && token.replaceAll('#', '').length != token.length ) ) // Header
3195
- {
3196
- token_classname = "cm-kwd";
3197
- this._markdownHeader = true;
3704
+ // Check if string ended in same token using next...
3705
+ if( usePreviousTokenToCheckString )
3706
+ {
3707
+ _checkIfStringEnded( ctxData.nextWithSpaces );
3708
+ }
3709
+ }
3198
3710
  }
3199
3711
 
3200
- else if ( token[ 0 ] != '@' && token[ 0 ] != ',' && next == '(' )
3201
- token_classname = "cm-mtd";
3712
+ // Update context data for next tests
3713
+ ctxData.discardToken = false;
3714
+ ctxData.inBlockComment = this._buildingBlockComment;
3715
+ ctxData.markdownHeader = this._markdownHeader;
3716
+ ctxData.inString = this._buildingString;
3717
+ ctxData.singleLineCommentToken = lang.singleLineCommentToken ?? this.defaultSingleLineCommentToken;
3718
+ ctxData.lang = lang;
3719
+ ctxData.scope = this._scopeStack.at( -1 );
3202
3720
 
3721
+ // Add utils functions for the rules
3722
+ ctxData.isVariableSymbol = ( token ) => this.code.symbolsTable.has( token ) && this.code.symbolsTable.get( token )[ 0 ].kind === "variable";
3723
+ ctxData.isEnumValueSymbol = ( token ) => this.code.symbolsTable.has( token ) && this.code.symbolsTable.get( token )[ 0 ].kind === "enum_value";
3724
+ ctxData.isClassSymbol = ( token ) => this.code.symbolsTable.has( token ) && this.code.symbolsTable.get( token )[ 0 ].kind === "class";
3725
+ ctxData.isStructSymbol = ( token ) => this.code.symbolsTable.has( token ) && this.code.symbolsTable.get( token )[ 0 ].kind === "struct";
3726
+ ctxData.isEnumSymbol = ( token ) => this.code.symbolsTable.has( token ) && this.code.symbolsTable.get( token )[ 0 ].kind === "enum";
3203
3727
 
3204
- if( usesBlockComments && this._buildingBlockComment != undefined
3728
+ // Get highlighting class based on language common and specific rules
3729
+ let tokenClass = this._getTokenHighlighting( ctxData, highlight );
3730
+
3731
+ const blockCommentsTokens = lang.blockCommentsTokens ?? this.defaultBlockCommentTokens;
3732
+ if( ( lang.blockComments ?? true ) && this._buildingBlockComment != undefined
3205
3733
  && token.substr( 0, blockCommentsTokens[ 1 ].length ) == blockCommentsTokens[ 1 ] )
3206
3734
  {
3207
- this._blockCommentCache.push( new LX.vec2( this._buildingBlockComment, this._currentLineNumber ) );
3735
+ this._blockCommentCache.push( new LX.vec2( this._buildingBlockComment, lineNumber ) );
3208
3736
  delete this._buildingBlockComment;
3209
3737
  }
3210
3738
 
@@ -3212,34 +3740,36 @@ class CodeEditor {
3212
3740
  if( this._buildingString && ( this._stringEnded || isLastToken ) )
3213
3741
  {
3214
3742
  token = this._getCurrentString();
3215
- token_classname = "cm-str";
3216
- discardToken = false;
3743
+ tokenClass = "cm-str";
3744
+ ctxData.discardToken = false;
3217
3745
  }
3218
3746
 
3219
3747
  // Update state
3220
3748
  this._buildingString = this._stringEnded ? undefined : this._buildingString;
3221
3749
 
3222
- if( discardToken )
3750
+ if( ctxData.discardToken )
3223
3751
  {
3224
3752
  return "";
3225
3753
  }
3226
3754
 
3227
- token = token.replace( "<", "&lt;" );
3228
- token = token.replace( ">", "&gt;" );
3755
+ // Replace html chars
3756
+ token = token.replace( "<", "&lt;" ).replace( ">", "&gt;" );
3229
3757
 
3230
3758
  // No highlighting, no need to put it inside another span..
3231
- if( !token_classname.length )
3759
+ if( !tokenClass )
3232
3760
  {
3233
3761
  return token;
3234
3762
  }
3235
3763
 
3236
- return "<span class='" + highlight + " " + token_classname + "'>" + token + "</span>";
3764
+ return `<span class="${ highlight } ${ tokenClass }">${ token }</span>`;
3237
3765
  }
3238
3766
 
3239
3767
  _appendStringToken( token ) {
3240
3768
 
3241
3769
  if( !this._pendingString )
3770
+ {
3242
3771
  this._pendingString = "";
3772
+ }
3243
3773
 
3244
3774
  this._pendingString += token;
3245
3775
 
@@ -3247,7 +3777,6 @@ class CodeEditor {
3247
3777
  }
3248
3778
 
3249
3779
  _getCurrentString() {
3250
-
3251
3780
  const chars = this._pendingString;
3252
3781
  delete this._pendingString;
3253
3782
  return chars;
@@ -3285,11 +3814,9 @@ class CodeEditor {
3285
3814
  return false;
3286
3815
  }
3287
3816
 
3288
- _isKeyword( ctxData, lang ) {
3817
+ _isKeyword( ctxData ) {
3289
3818
 
3290
- const token = ctxData.token;
3291
- const tokenIndex = ctxData.tokenIndex;
3292
- const tokens = ctxData.tokens;
3819
+ const { token, tokenIndex, tokens, lang } = ctxData;
3293
3820
 
3294
3821
  let isKwd = this._mustHightlightWord( token, CodeEditor.keywords ) || this.highlight == 'XML';
3295
3822
 
@@ -3306,6 +3833,10 @@ class CodeEditor {
3306
3833
  isKwd |= ( ctxData.tokens[ tokenIndex - 2 ] == '$' );
3307
3834
  }
3308
3835
  }
3836
+ if( this.highlight == 'Markdown' )
3837
+ {
3838
+ isKwd = ( this._markdownHeader !== undefined );
3839
+ }
3309
3840
  else if( lang.tags )
3310
3841
  {
3311
3842
  isKwd &= ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) != undefined );
@@ -3315,24 +3846,14 @@ class CodeEditor {
3315
3846
  return isKwd;
3316
3847
  }
3317
3848
 
3318
- _isCSSClass( ctxData ) {
3319
-
3320
- const token = ctxData.token;
3321
- const prev = ctxData.prev;
3322
- const next = ctxData.next;
3849
+ _isNumber( token ) {
3323
3850
 
3324
- if( this.highlight != 'CSS' )
3851
+ const lang = CodeEditor.languages[ this.highlight ];
3852
+ if( !( lang.numbers ?? true ) )
3325
3853
  {
3326
3854
  return false;
3327
3855
  }
3328
3856
 
3329
- return ( prev == '.' || prev == '::'
3330
- || ( prev == ':' && next == '{' )
3331
- || ( token[ 0 ] == '#' && prev != ':' ) );
3332
- }
3333
-
3334
- _isNumber( token ) {
3335
-
3336
3857
  const subToken = token.substring( 0, token.length - 1 );
3337
3858
 
3338
3859
  if( this.highlight == 'C++' )
@@ -3364,56 +3885,27 @@ class CodeEditor {
3364
3885
  return token.length && token != ' ' && !Number.isNaN( +token );
3365
3886
  }
3366
3887
 
3367
- _isType( ctxData, lang ) {
3368
-
3369
- const token = ctxData.token;
3370
- const prev = ctxData.prev;
3371
- const next = ctxData.next;
3372
-
3373
- // Common case
3374
- if( this._mustHightlightWord( token, CodeEditor.types, lang ) )
3375
- {
3376
- return true;
3377
- }
3888
+ _encloseSelectedWordWithKey( key, lidx, cursor ) {
3378
3889
 
3379
- if( this.highlight == 'JavaScript' )
3380
- {
3381
- return (prev == 'class' && next == '{') || (prev == 'new' && next == '(');
3382
- }
3383
- else if( this.highlight == 'C++' )
3890
+ if( !cursor.selection || ( cursor.selection.fromY != cursor.selection.toY ) )
3384
3891
  {
3385
- return (prev == 'class' && next == '{') || (prev == 'struct' && next == '{');
3386
- }
3387
- else if ( this.highlight == 'WGSL' )
3388
- {
3389
- const not_kwd = !this._mustHightlightWord( token, CodeEditor.keywords, lang );
3390
- return (prev == 'struct' && next == '{') ||
3391
- (not_kwd && prev == ':' && next == ';') ||
3392
- ( not_kwd &&
3393
- ( prev == ':' && next == ')' || prev == ':' && next == ',' || prev == '>' && next == '{'
3394
- || prev == '<' && next == ',' || prev == '<' && next == '>' || prev == '>' && token != ';' && !next ));
3892
+ return false;
3395
3893
  }
3396
- }
3397
-
3398
- _encloseSelectedWordWithKey( key, lidx, cursor ) {
3399
-
3400
- if( !cursor.selection || (cursor.selection.fromY != cursor.selection.toY) )
3401
- return false;
3402
3894
 
3403
3895
  cursor.selection.invertIfNecessary();
3404
3896
 
3405
3897
  // Insert first..
3406
3898
  this.code.lines[ lidx ] = [
3407
- this.code.lines[ lidx ].slice(0, cursor.selection.fromX),
3899
+ this.code.lines[ lidx ].slice( 0, cursor.selection.fromX ),
3408
3900
  key,
3409
- this.code.lines[ lidx ].slice(cursor.selection.fromX)
3901
+ this.code.lines[ lidx ].slice( cursor.selection.fromX )
3410
3902
  ].join('');
3411
3903
 
3412
3904
  // Go to the end of the word
3413
- this.cursorToPosition(cursor, cursor.selection.toX + 1);
3905
+ this.cursorToPosition( cursor, cursor.selection.toX + 1 );
3414
3906
 
3415
3907
  // Change next key?
3416
- switch(key)
3908
+ switch( key )
3417
3909
  {
3418
3910
  case "'":
3419
3911
  case "\"":
@@ -3441,6 +3933,46 @@ class CodeEditor {
3441
3933
  return true;
3442
3934
  }
3443
3935
 
3936
+ _detectLanguage( text ) {
3937
+
3938
+ const tokenSet = new Set( this._getTokensFromLine( text, true ) );
3939
+ const scores = {};
3940
+
3941
+ for( let [ lang, wordList ] of Object.entries( CodeEditor.keywords ) )
3942
+ {
3943
+ scores[ lang ] = 0;
3944
+ for( let kw of wordList )
3945
+ if( tokenSet.has( kw ) ) scores[ lang ]++;
3946
+ }
3947
+
3948
+ for( let [ lang, wordList ] of Object.entries( CodeEditor.statements ) )
3949
+ {
3950
+ for( let kw of wordList )
3951
+ if( tokenSet.has( kw ) ) scores[ lang ]++;
3952
+ }
3953
+
3954
+ for( let [ lang, wordList ] of Object.entries( CodeEditor.utils ) )
3955
+ {
3956
+ for( let kw of wordList )
3957
+ if( tokenSet.has( kw ) ) scores[ lang ]++;
3958
+ }
3959
+
3960
+ for( let [ lang, wordList ] of Object.entries( CodeEditor.types ) )
3961
+ {
3962
+ for( let kw of wordList )
3963
+ if( tokenSet.has( kw ) ) scores[ lang ]++;
3964
+ }
3965
+
3966
+ for( let [ lang, wordList ] of Object.entries( CodeEditor.builtIn ) )
3967
+ {
3968
+ for( let kw of wordList )
3969
+ if( tokenSet.has( kw ) ) scores[ lang ]++;
3970
+ }
3971
+
3972
+ const sorted = Object.entries( scores ).sort( ( a, b ) => b[ 1 ] - a[ 1 ] );
3973
+ return sorted[0][1] > 0 ? sorted[0][0] : undefined;
3974
+ }
3975
+
3444
3976
  lineUp( cursor, resetLeft ) {
3445
3977
 
3446
3978
  if( this.code.lines[ cursor.line - 1 ] == undefined )
@@ -3556,7 +4088,7 @@ class CodeEditor {
3556
4088
  // Use main cursor
3557
4089
  this._removeSecondaryCursors();
3558
4090
 
3559
- var cursor = this._getCurrentCursor();
4091
+ var cursor = this.getCurrentCursor();
3560
4092
  this.resetCursorPos( CodeEditor.CURSOR_LEFT_TOP, cursor );
3561
4093
 
3562
4094
  this.startSelection( cursor );
@@ -3578,7 +4110,7 @@ class CodeEditor {
3578
4110
  if( !key ) return;
3579
4111
 
3580
4112
  cursor._left += this.charWidth;
3581
- cursor.style.left = "calc( " + cursor._left + "px + " + this.xPadding + " )";
4113
+ cursor.style.left = `calc( ${ cursor._left }px + ${ this.xPadding } )`;
3582
4114
  cursor.position++;
3583
4115
 
3584
4116
  this.restartBlink();
@@ -3598,7 +4130,7 @@ class CodeEditor {
3598
4130
 
3599
4131
  cursor._left -= this.charWidth;
3600
4132
  cursor._left = Math.max( cursor._left, 0 );
3601
- cursor.style.left = "calc( " + cursor._left + "px + " + this.xPadding + " )";
4133
+ cursor.style.left = `calc( ${ cursor._left }px + ${ this.xPadding } )`;
3602
4134
  cursor.position--;
3603
4135
  cursor.position = Math.max( cursor.position, 0 );
3604
4136
  this.restartBlink();
@@ -3616,8 +4148,8 @@ class CodeEditor {
3616
4148
  cursorToTop( cursor, resetLeft = false ) {
3617
4149
 
3618
4150
  cursor._top -= this.lineHeight;
3619
- cursor._top = Math.max(cursor._top, 0);
3620
- cursor.style.top = "calc(" + cursor._top + "px)";
4151
+ cursor._top = Math.max( cursor._top, 0 );
4152
+ cursor.style.top = `calc(${ cursor._top }px)`;
3621
4153
  this.restartBlink();
3622
4154
 
3623
4155
  if( resetLeft )
@@ -3646,11 +4178,8 @@ class CodeEditor {
3646
4178
  }
3647
4179
 
3648
4180
  const currentScrollTop = this.getScrollTop();
3649
- const tabsHeight = this.tabs.root.getBoundingClientRect().height;
3650
- const infoPanelHeight = this.skipInfo ? 0 : this.infoPanel.root.getBoundingClientRect().height;
3651
- const scrollerHeight = this.codeScroller.offsetHeight;
3652
-
3653
- var lastLine = ( ( scrollerHeight - tabsHeight - infoPanelHeight + currentScrollTop ) / this.lineHeight )|0;
4181
+ const scrollerHeight = this.codeScroller.offsetHeight - this._fullVerticalOffset;
4182
+ const lastLine = ( ( scrollerHeight + currentScrollTop ) / this.lineHeight )|0;
3654
4183
  if( cursor.line >= lastLine )
3655
4184
  {
3656
4185
  this.setScrollTop( currentScrollTop + this.lineHeight );
@@ -3660,7 +4189,9 @@ class CodeEditor {
3660
4189
  cursorToString( cursor, text, reverse ) {
3661
4190
 
3662
4191
  if( !text.length )
4192
+ {
3663
4193
  return;
4194
+ }
3664
4195
 
3665
4196
  for( let char of text )
3666
4197
  {
@@ -3672,7 +4203,7 @@ class CodeEditor {
3672
4203
 
3673
4204
  cursor.position = position;
3674
4205
  cursor._left = position * this.charWidth;
3675
- cursor.style.left = "calc(" + cursor._left + "px + " + this.xPadding + ")";
4206
+ cursor.style.left = `calc( ${ cursor._left }px + ${ this.xPadding } )`;
3676
4207
  }
3677
4208
 
3678
4209
  cursorToLine( cursor, line, resetLeft = false ) {
@@ -3704,14 +4235,24 @@ class CodeEditor {
3704
4235
  return cursors;
3705
4236
  }
3706
4237
 
4238
+ getCurrentCursor( removeOthers ) {
4239
+
4240
+ if( removeOthers )
4241
+ {
4242
+ this._removeSecondaryCursors();
4243
+ }
4244
+
4245
+ return this.cursors.children[ 0 ];
4246
+ }
4247
+
3707
4248
  relocateCursors() {
3708
4249
 
3709
4250
  for( let cursor of this.cursors.children )
3710
4251
  {
3711
4252
  cursor._left = cursor.position * this.charWidth;
3712
- cursor.style.left = "calc(" + cursor._left + "px + " + this.xPadding + ")";
4253
+ cursor.style.left = `calc( ${ cursor._left }px + ${ this.xPadding } )`;
3713
4254
  cursor._top = cursor.line * this.lineHeight;
3714
- cursor.style.top = "calc(" + cursor._top + "px)";
4255
+ cursor.style.top = `calc(${ cursor._top }px)`;
3715
4256
  }
3716
4257
  }
3717
4258
 
@@ -3730,9 +4271,9 @@ class CodeEditor {
3730
4271
  cursor.line = state.line ?? 0;
3731
4272
 
3732
4273
  cursor._left = cursor.position * this.charWidth;
3733
- cursor.style.left = "calc(" + cursor._left + "px + " + this.xPadding + ")";
4274
+ cursor.style.left = `calc( ${ cursor._left }px + ${ this.xPadding } )`;
3734
4275
  cursor._top = cursor.line * this.lineHeight;
3735
- cursor.style.top = "calc(" + cursor._top + "px)";
4276
+ cursor.style.top = `calc(${ cursor._top }px)`;
3736
4277
 
3737
4278
  if( state.selection )
3738
4279
  {
@@ -3753,7 +4294,7 @@ class CodeEditor {
3753
4294
 
3754
4295
  resetCursorPos( flag, cursor ) {
3755
4296
 
3756
- cursor = cursor ?? this._getCurrentCursor();
4297
+ cursor = cursor ?? this.getCurrentCursor();
3757
4298
 
3758
4299
  if( flag & CodeEditor.CURSOR_LEFT )
3759
4300
  {
@@ -3770,18 +4311,85 @@ class CodeEditor {
3770
4311
  }
3771
4312
  }
3772
4313
 
4314
+ _addCursor( line = 0, position = 0, force, isMain = false ) {
4315
+
4316
+ // If cursor in that position exists, remove it instead..
4317
+ const exists = Array.from( this.cursors.children ).find( v => v.position == position && v.line == line );
4318
+ if( exists && !force )
4319
+ {
4320
+ if( !exists.isMain )
4321
+ exists.remove();
4322
+
4323
+ return;
4324
+ }
4325
+
4326
+ let cursor = document.createElement( 'div' );
4327
+ cursor.name = "cursor" + this.cursors.childElementCount;
4328
+ cursor.className = "cursor";
4329
+ cursor.innerHTML = "&nbsp;";
4330
+ cursor.isMain = isMain;
4331
+ cursor._left = position * this.charWidth;
4332
+ cursor.style.left = "calc( " + cursor._left + "px + " + this.xPadding + " )";
4333
+ cursor._top = line * this.lineHeight;
4334
+ cursor.style.top = cursor._top + "px";
4335
+ cursor._position = position;
4336
+ cursor._line = line;
4337
+ cursor.print = (function() { console.log( this._line, this._position ) }).bind( cursor );
4338
+ cursor.isLast = (function() { return this.cursors.lastChild == cursor; }).bind( this );
4339
+
4340
+ Object.defineProperty( cursor, 'line', {
4341
+ get: (v) => { return cursor._line },
4342
+ set: (v) => {
4343
+ cursor._line = v;
4344
+ if( cursor.isMain ) this._setActiveLine( v );
4345
+ }
4346
+ } );
4347
+
4348
+ Object.defineProperty( cursor, 'position', {
4349
+ get: (v) => { return cursor._position },
4350
+ set: (v) => {
4351
+ cursor._position = v;
4352
+ if( cursor.isMain )
4353
+ {
4354
+ const activeLine = this.state.activeLine;
4355
+ this._updateDataInfoPanel( "@cursor-data", `Ln ${ activeLine + 1 }, Col ${ v + 1 }` );
4356
+ }
4357
+ }
4358
+ } );
4359
+
4360
+ this.cursors.appendChild( cursor );
4361
+
4362
+ return cursor;
4363
+ }
4364
+
4365
+ _removeSecondaryCursors() {
4366
+
4367
+ while( this.cursors.childElementCount > 1 )
4368
+ this.cursors.lastChild.remove();
4369
+ }
4370
+
4371
+ _logCursors() {
4372
+
4373
+ for( let cursor of this.cursors.children )
4374
+ {
4375
+ cursor.print();
4376
+ }
4377
+ }
4378
+
3773
4379
  _addSpaceTabs( cursor, n ) {
3774
4380
 
3775
- for( var i = 0; i < n; ++i ) {
4381
+ for( var i = 0; i < n; ++i )
4382
+ {
3776
4383
  this.actions[ 'Tab' ].callback( cursor.line, cursor, null );
3777
4384
  }
3778
4385
  }
3779
4386
 
3780
4387
  _addSpaces( n ) {
3781
4388
 
3782
- for( var i = 0; i < n; ++i ) {
4389
+ for( var i = 0; i < n; ++i )
4390
+ {
3783
4391
  this.root.dispatchEvent( new CustomEvent( 'keydown', { 'detail': {
3784
- skip_undo: true,
4392
+ skipUndo: true,
3785
4393
  key: ' ',
3786
4394
  targetCursor: this._lastProcessedCursorIndex
3787
4395
  }}));
@@ -3789,16 +4397,21 @@ class CodeEditor {
3789
4397
  }
3790
4398
 
3791
4399
  _removeSpaces( cursor ) {
4400
+
3792
4401
  const lidx = cursor.line;
4402
+
3793
4403
  // Remove indentation
3794
4404
  let lineStart = firstNonspaceIndex( this.code.lines[ lidx ] );
3795
4405
 
3796
4406
  // Nothing to remove... we are at the start of the line
3797
4407
  if( lineStart == 0 )
4408
+ {
3798
4409
  return;
4410
+ }
3799
4411
 
3800
4412
  // Only tabs/spaces in the line...
3801
- if( lineStart == -1 ) {
4413
+ if( lineStart == -1 )
4414
+ {
3802
4415
  lineStart = this.code.lines[ lidx ].length;
3803
4416
  }
3804
4417
 
@@ -3835,7 +4448,7 @@ class CodeEditor {
3835
4448
 
3836
4449
  setScrollLeft( value ) {
3837
4450
  if( !this.codeScroller ) return;
3838
- doAsync( () => {
4451
+ LX.doAsync( () => {
3839
4452
  this.codeScroller.scrollLeft = value;
3840
4453
  this.setScrollBarValue( 'horizontal', 0 );
3841
4454
  }, 20 );
@@ -3843,30 +4456,34 @@ class CodeEditor {
3843
4456
 
3844
4457
  setScrollTop( value ) {
3845
4458
  if( !this.codeScroller ) return;
3846
- doAsync( () => {
4459
+ LX.doAsync( () => {
3847
4460
  this.codeScroller.scrollTop = value;
3848
4461
  this.setScrollBarValue( 'vertical' );
3849
4462
  }, 20 );
3850
4463
  }
3851
4464
 
3852
- resize( pMaxLength, onResize ) {
4465
+ resize( flag = CodeEditor.RESIZE_SCROLLBAR_H_V, pMaxLength, onResize ) {
3853
4466
 
3854
4467
  setTimeout( () => {
3855
4468
 
3856
- // Update max viewport
3857
- const maxLineLength = pMaxLength ?? this.getMaxLineLength();
3858
- const scrollWidth = maxLineLength * this.charWidth + CodeEditor.LINE_GUTTER_WIDTH;
4469
+ let scrollWidth, scrollHeight;
3859
4470
 
3860
- const tabsHeight = this.tabs.root.getBoundingClientRect().height;
3861
- const infoPanelHeight = this.skipInfo ? 0 : this.infoPanel.root.getBoundingClientRect().height;
3862
- const scrollHeight = this.code.lines.length * this.lineHeight;
3863
-
3864
- this._lastMaxLineLength = maxLineLength;
4471
+ if( flag & CodeEditor.RESIZE_SCROLLBAR_H )
4472
+ {
4473
+ // Update max viewport
4474
+ const maxLineLength = pMaxLength ?? this.getMaxLineLength();
4475
+ this._lastMaxLineLength = maxLineLength;
4476
+ scrollWidth = maxLineLength * this.charWidth + CodeEditor.LINE_GUTTER_WIDTH;
4477
+ this.codeSizer.style.minWidth = scrollWidth + "px";
4478
+ }
3865
4479
 
3866
- this.codeSizer.style.minWidth = scrollWidth + "px";
3867
- this.codeSizer.style.minHeight = ( scrollHeight + tabsHeight + infoPanelHeight ) + "px";
4480
+ if( flag & CodeEditor.RESIZE_SCROLLBAR_V )
4481
+ {
4482
+ scrollHeight = this.code.lines.length * this.lineHeight + this._fullVerticalOffset;
4483
+ this.codeSizer.style.minHeight = scrollHeight + "px";
4484
+ }
3868
4485
 
3869
- this.resizeScrollBars();
4486
+ this.resizeScrollBars( flag );
3870
4487
 
3871
4488
  if( onResize )
3872
4489
  {
@@ -3876,37 +4493,52 @@ class CodeEditor {
3876
4493
  }, 10 );
3877
4494
  }
3878
4495
 
3879
- resizeScrollBars() {
3880
-
3881
- const totalLinesInViewport = ((this.codeScroller.offsetHeight) / this.lineHeight)|0;
4496
+ resizeIfNecessary( cursor, force ) {
3882
4497
 
3883
- if( totalLinesInViewport >= this.code.lines.length )
3884
- {
3885
- this.codeScroller.classList.remove( 'with-vscrollbar' );
3886
- this.vScrollbar.root.classList.add( 'scrollbar-unused' );
3887
- }
3888
- else
4498
+ const maxLineLength = this.getMaxLineLength();
4499
+ const numViewportChars = Math.floor( ( this.codeScroller.clientWidth - CodeEditor.LINE_GUTTER_WIDTH ) / this.charWidth );
4500
+ if( force || ( maxLineLength >= numViewportChars && maxLineLength != this._lastMaxLineLength ) )
3889
4501
  {
3890
- this.codeScroller.classList.add( 'with-vscrollbar' );
3891
- this.vScrollbar.root.classList.remove( 'scrollbar-unused' );
3892
- this.vScrollbar.thumb.size = (totalLinesInViewport / this.code.lines.length);
3893
- this.vScrollbar.thumb.style.height = (this.vScrollbar.thumb.size * 100.0) + "%";
4502
+ this.resize( CodeEditor.RESIZE_SCROLLBAR_H, maxLineLength, () => {
4503
+ if( cursor.position > numViewportChars )
4504
+ {
4505
+ this.setScrollLeft( cursor.position * this.charWidth );
4506
+ }
4507
+ } );
3894
4508
  }
4509
+ }
3895
4510
 
3896
- const numViewportChars = Math.floor( ( this.codeScroller.clientWidth - CodeEditor.LINE_GUTTER_WIDTH ) / this.charWidth );
3897
- const maxLineLength = this._lastMaxLineLength;
4511
+ resizeScrollBars( flag = CodeEditor.RESIZE_SCROLLBAR_H_V ) {
3898
4512
 
3899
- if( numViewportChars >= maxLineLength )
4513
+ if( flag & CodeEditor.RESIZE_SCROLLBAR_V )
3900
4514
  {
3901
- this.codeScroller.classList.remove( 'with-hscrollbar' );
3902
- this.hScrollbar.root.classList.add( 'scrollbar-unused' );
4515
+ const totalLinesInViewport = (( this.codeScroller.offsetHeight ) / this.lineHeight)|0;
4516
+ const needsVerticalScrollbar = ( this.code.lines.length >= totalLinesInViewport );
4517
+ if( needsVerticalScrollbar )
4518
+ {
4519
+ this.vScrollbar.thumb.size = ( totalLinesInViewport / this.code.lines.length );
4520
+ this.vScrollbar.thumb.style.height = (this.vScrollbar.thumb.size * 100.0) + "%";
4521
+ }
4522
+
4523
+ this.vScrollbar.root.classList.toggle( 'hidden', !needsVerticalScrollbar );
4524
+ this.hScrollbar.root.style.width = `calc(100% - ${ 48 + ( needsVerticalScrollbar ? ScrollBar.SCROLLBAR_VERTICAL_WIDTH : 0 ) }px)`; // 48 is the line gutter
4525
+ this.codeArea.root.style.width = `calc(100% - ${ needsVerticalScrollbar ? ScrollBar.SCROLLBAR_VERTICAL_WIDTH : 0 }px)`;
3903
4526
  }
3904
- else
4527
+
4528
+ if( flag & CodeEditor.RESIZE_SCROLLBAR_H )
3905
4529
  {
3906
- this.codeScroller.classList.add( 'with-hscrollbar' );
3907
- this.hScrollbar.root.classList.remove( 'scrollbar-unused' );
3908
- this.hScrollbar.thumb.size = (numViewportChars / maxLineLength);
3909
- this.hScrollbar.thumb.style.width = (this.hScrollbar.thumb.size * 100.0) + "%";
4530
+ const numViewportChars = Math.floor( ( this.codeScroller.clientWidth - CodeEditor.LINE_GUTTER_WIDTH ) / this.charWidth );
4531
+ const maxLineLength = this._lastMaxLineLength;
4532
+ const needsHorizontalScrollbar = maxLineLength >= numViewportChars;
4533
+
4534
+ if( needsHorizontalScrollbar )
4535
+ {
4536
+ this.hScrollbar.thumb.size = ( numViewportChars / maxLineLength );
4537
+ this.hScrollbar.thumb.style.width = ( this.hScrollbar.thumb.size * 100.0 ) + "%";
4538
+ }
4539
+
4540
+ this.hScrollbar.root.classList.toggle( 'hidden', !needsHorizontalScrollbar );
4541
+ this.codeArea.root.style.height = `calc(100% - ${ this._verticalBottomOffset + ( needsHorizontalScrollbar ? ScrollBar.SCROLLBAR_HORIZONTAL_HEIGHT : 0 ) }px)`;
3910
4542
  }
3911
4543
  }
3912
4544
 
@@ -3980,7 +4612,6 @@ class CodeEditor {
3980
4612
  }
3981
4613
 
3982
4614
  getCharAtPos( cursor, offset = 0 ) {
3983
-
3984
4615
  return this.code.lines[ cursor.line ][ cursor.position + offset ];
3985
4616
  }
3986
4617
 
@@ -4034,7 +4665,7 @@ class CodeEditor {
4034
4665
  return [ word, from, to ];
4035
4666
  }
4036
4667
 
4037
- _measureChar( char = "a", use_floating = false, get_bb = false ) {
4668
+ _measureChar( char = "a", useFloating = false, getBB = false ) {
4038
4669
  const parentContainer = LX.makeContainer( null, "lexcodeeditor", "", document.body );
4039
4670
  const container = LX.makeContainer( null, "code", "", parentContainer );
4040
4671
  const line = document.createElement( "pre" );
@@ -4044,12 +4675,11 @@ class CodeEditor {
4044
4675
  text.innerText = char;
4045
4676
  var rect = text.getBoundingClientRect();
4046
4677
  LX.deleteElement( parentContainer );
4047
- const bb = [ use_floating ? rect.width : Math.floor( rect.width ), use_floating ? rect.height : Math.floor( rect.height ) ];
4048
- return get_bb ? bb : bb[ 0 ];
4678
+ const bb = [ useFloating ? rect.width : Math.floor( rect.width ), useFloating ? rect.height : Math.floor( rect.height ) ];
4679
+ return getBB ? bb : bb[ 0 ];
4049
4680
  }
4050
4681
 
4051
4682
  measureString( str ) {
4052
-
4053
4683
  return str.length * this.charWidth;
4054
4684
  }
4055
4685
 
@@ -4102,46 +4732,64 @@ class CodeEditor {
4102
4732
  showAutoCompleteBox( key, cursor ) {
4103
4733
 
4104
4734
  if( !cursor.isMain )
4735
+ {
4105
4736
  return;
4737
+ }
4106
4738
 
4107
- const [word, start, end] = this.getWordAtPos( cursor, -1 );
4108
- if( key == ' ' || !word.length ) {
4739
+ const [ word, start, end ] = this.getWordAtPos( cursor, -1 );
4740
+ if( key == ' ' || !word.length )
4741
+ {
4109
4742
  this.hideAutoCompleteBox();
4110
4743
  return;
4111
4744
  }
4112
4745
 
4113
4746
  this.autocomplete.innerHTML = ""; // Clear all suggestions
4114
4747
 
4115
- let suggestions = [];
4116
-
4117
4748
  // Add language special keys...
4118
- suggestions = suggestions.concat(
4119
- Object.keys( CodeEditor.builtIn[ this.highlight ] ?? {} ),
4120
- Object.keys( CodeEditor.keywords[ this.highlight ] ?? {} ),
4121
- Object.keys( CodeEditor.statementsAndDeclarations[ this.highlight ] ?? {} ),
4122
- Object.keys( CodeEditor.types[ this.highlight ] ?? {} ),
4123
- Object.keys( CodeEditor.utils[ this.highlight ] ?? {} )
4124
- );
4749
+ let suggestions = [
4750
+ ...Array.from( CodeEditor.keywords[ this.highlight ] ?? [] ),
4751
+ ...Array.from( CodeEditor.builtIn[ this.highlight ] ?? [] ),
4752
+ ...Array.from( CodeEditor.statements[ this.highlight ] ?? [] ),
4753
+ ...Array.from( CodeEditor.types[ this.highlight ] ?? [] ),
4754
+ ...Array.from( CodeEditor.utils[ this.highlight ] ?? [] )
4755
+ ];
4756
+
4757
+ suggestions = suggestions.concat( Object.keys(this.code.tokens).filter( a => a != word ) );
4758
+
4759
+ const scopeStack = [ ...this.code.lineScopes[ cursor.line ] ];
4760
+ const scope = scopeStack.at( -1 );
4761
+ if( scope.type.startsWith( "enum" ) )
4762
+ {
4763
+ const enumValues = Array.from( this.code.symbolsTable ).filter( s => s[ 1 ][ 0 ].kind === "enum_value" && s[ 1 ][ 0 ].scope === scope.name ).map( s => s[ 0 ] );
4764
+ suggestions = suggestions.concat( enumValues.slice( 0, -1 ) );
4765
+ }
4766
+ else
4767
+ {
4768
+ const otherValues = Array.from( this.code.symbolsTable ).map( s => s[ 0 ] );
4769
+ suggestions = suggestions.concat( otherValues.slice( 0, -1 ) );
4770
+ }
4125
4771
 
4126
- // Add words in current tab plus remove current word
4127
- // suggestions = suggestions.concat( Object.keys(this.code.tokens).filter( a => a != word ) );
4772
+ const prefix = word.toLowerCase();
4128
4773
 
4129
4774
  // Remove 1/2 char words and duplicates...
4130
- suggestions = suggestions.filter( (value, index) => value.length > 2 && suggestions.indexOf(value) === index );
4775
+ suggestions = Array.from( new Set( suggestions )).filter( s => s.length > 2 && s.toLowerCase().includes( prefix ) );
4131
4776
 
4132
4777
  // Order...
4133
- suggestions = suggestions.sort( ( a, b ) => a.localeCompare( b ) );
4778
+
4779
+ function scoreSuggestion( s, prefix ) {
4780
+ if( s.startsWith( prefix ) ) return 0; // best option
4781
+ if( s.includes( prefix )) return 1;
4782
+ return 2; // worst
4783
+ }
4784
+
4785
+ suggestions = suggestions.sort( ( a, b ) => scoreSuggestion( a, prefix ) - scoreSuggestion( b, prefix ) || a.localeCompare( b ) );
4134
4786
 
4135
4787
  for( let s of suggestions )
4136
4788
  {
4137
- if( !s.toLowerCase().includes( word.toLowerCase() ) )
4138
- continue;
4139
-
4140
4789
  var pre = document.createElement( 'pre' );
4141
4790
  this.autocomplete.appendChild( pre );
4142
4791
 
4143
4792
  var icon = "Type";
4144
-
4145
4793
  if( this._mustHightlightWord( s, CodeEditor.utils ) )
4146
4794
  icon = "Box";
4147
4795
  else if( this._mustHightlightWord( s, CodeEditor.types ) )
@@ -4177,11 +4825,11 @@ class CodeEditor {
4177
4825
  }
4178
4826
 
4179
4827
  // Select always first option
4180
- this.autocomplete.firstChild.classList.add('selected');
4828
+ this.autocomplete.firstChild.classList.add( 'selected' );
4181
4829
 
4182
4830
  // Show box
4183
- this.autocomplete.classList.toggle('show', true);
4184
- this.autocomplete.classList.toggle('no-scrollbar', !(this.autocomplete.scrollHeight > this.autocomplete.offsetHeight));
4831
+ this.autocomplete.classList.toggle( 'show', true );
4832
+ this.autocomplete.classList.toggle( 'no-scrollbar', !( this.autocomplete.scrollHeight > this.autocomplete.offsetHeight ) );
4185
4833
  this.autocomplete.style.left = (cursor._left + CodeEditor.LINE_GUTTER_WIDTH - this.getScrollLeft()) + "px";
4186
4834
  this.autocomplete.style.top = (cursor._top + 28 + this.lineHeight - this.getScrollTop()) + "px";
4187
4835
 
@@ -4293,7 +4941,7 @@ class CodeEditor {
4293
4941
  }
4294
4942
  else
4295
4943
  {
4296
- const cursor = this._getCurrentCursor();
4944
+ const cursor = this.getCurrentCursor();
4297
4945
 
4298
4946
  if( cursor.selection )
4299
4947
  {
@@ -4333,9 +4981,11 @@ class CodeEditor {
4333
4981
  text = text ?? this._lastTextFound;
4334
4982
 
4335
4983
  if( !text )
4984
+ {
4336
4985
  return;
4986
+ }
4337
4987
 
4338
- let cursor = this._getCurrentCursor();
4988
+ let cursor = this.getCurrentCursor();
4339
4989
  let cursorData = new LX.vec2( cursor.position, cursor.line );
4340
4990
  let line = null;
4341
4991
  let char = -1;
@@ -4344,6 +4994,7 @@ class CodeEditor {
4344
4994
  {
4345
4995
  LX.deleteElement( this._lastResult.dom );
4346
4996
  cursorData = this._lastResult.pos;
4997
+ cursorData.x += text.length * ( reverse ? -1 : 1 );
4347
4998
  delete this._lastResult;
4348
4999
  }
4349
5000
 
@@ -4389,10 +5040,12 @@ class CodeEditor {
4389
5040
  }
4390
5041
  }
4391
5042
 
4392
- if( line == null)
5043
+ if( line == null )
4393
5044
  {
4394
5045
  if( !skipAlert )
5046
+ {
4395
5047
  alert( "No results!" );
5048
+ }
4396
5049
 
4397
5050
  const lastLine = this.code.lines.length - 1;
4398
5051
 
@@ -4400,6 +5053,7 @@ class CodeEditor {
4400
5053
  'dom': this.searchResultSelections.lastChild,
4401
5054
  'pos': reverse ? new LX.vec2( this.code.lines[ lastLine ].length, lastLine ) : new LX.vec2( 0, 0 )
4402
5055
  };
5056
+
4403
5057
  return;
4404
5058
  }
4405
5059
 
@@ -4409,9 +5063,10 @@ class CodeEditor {
4409
5063
  have to add the length of the substring (0, first_ocurrence)
4410
5064
  */
4411
5065
 
4412
-
4413
5066
  if( !reverse )
5067
+ {
4414
5068
  char += ( line == cursorData.y ? cursorData.x : 0 );
5069
+ }
4415
5070
 
4416
5071
 
4417
5072
  // Text found..
@@ -4439,8 +5094,13 @@ class CodeEditor {
4439
5094
 
4440
5095
  this._lastResult = {
4441
5096
  'dom': this.searchResultSelections.lastChild,
4442
- 'pos': new LX.vec2( char + text.length * ( reverse ? -1 : 1 ) , line )
5097
+ 'pos': new LX.vec2( char , line ),
5098
+ reverse
4443
5099
  };
5100
+
5101
+ // Force focus back to search box
5102
+ const input = this.searchbox.querySelector( 'input' );
5103
+ input.focus();
4444
5104
  }
4445
5105
 
4446
5106
  showSearchLineBox() {
@@ -4472,7 +5132,7 @@ class CodeEditor {
4472
5132
  this.codeScroller.scrollTo( 0, Math.max( line - 15 ) * this.lineHeight );
4473
5133
 
4474
5134
  // Select line ?
4475
- var cursor = this._getCurrentCursor( true );
5135
+ var cursor = this.getCurrentCursor( true );
4476
5136
  this.cursorToLine( cursor, line - 1, true );
4477
5137
  }
4478
5138
 
@@ -4527,13 +5187,16 @@ class CodeEditor {
4527
5187
 
4528
5188
  number = number ?? this.state.activeLine;
4529
5189
 
4530
- this._updateDataInfoPanel( "@cursor-line", "Ln " + ( number + 1 ) );
5190
+ const cursor = this.getCurrentCursor();
5191
+ this._updateDataInfoPanel( "@cursor-data", `Ln ${ number + 1 }, Col ${ cursor.position + 1 }` );
4531
5192
 
4532
- const old_local = this.toLocalLine( this.state.activeLine );
4533
- let line = this.code.childNodes[ old_local ];
5193
+ const oldLocal = this.toLocalLine( this.state.activeLine );
5194
+ let line = this.code.childNodes[ oldLocal ];
4534
5195
 
4535
5196
  if( !line )
5197
+ {
4536
5198
  return;
5199
+ }
4537
5200
 
4538
5201
  line.classList.remove( 'active-line' );
4539
5202
 
@@ -4541,71 +5204,54 @@ class CodeEditor {
4541
5204
  {
4542
5205
  this.state.activeLine = number;
4543
5206
 
4544
- const new_local = this.toLocalLine( number );
4545
- line = this.code.childNodes[ new_local ];
5207
+ const newLocal = this.toLocalLine( number );
5208
+ line = this.code.childNodes[ newLocal ];
4546
5209
  if( line ) line.classList.add( 'active-line' );
4547
5210
  }
4548
5211
  }
4549
5212
 
4550
5213
  _hideActiveLine() {
4551
-
4552
5214
  this.code.querySelectorAll( '.active-line' ).forEach( e => e.classList.remove( 'active-line' ) );
4553
5215
  }
4554
5216
 
4555
- _increaseFontSize() {
4556
-
5217
+ _setFontSize( size ) {
4557
5218
  // Change font size
4558
-
4559
- var r = document.querySelector( ':root' );
4560
- var s = getComputedStyle( r );
4561
- var pixels = parseInt( s.getPropertyValue( "--code-editor-font-size" ) );
4562
- pixels = LX.clamp( pixels + 1, CodeEditor.CODE_MIN_FONT_SIZE, CodeEditor.CODE_MAX_FONT_SIZE );
4563
- r.style.setProperty( "--code-editor-font-size", pixels + "px" );
5219
+ this.fontSize = size;
5220
+ const r = document.querySelector( ':root' );
5221
+ r.style.setProperty( "--code-editor-font-size", `${ this.fontSize }px` );
4564
5222
  this.charWidth = this._measureChar( "a", true );
4565
5223
 
4566
- // Change row size
5224
+ window.localStorage.setItem( "lexcodeeditor-font-size", this.fontSize );
4567
5225
 
4568
- var row_pixels = pixels + 6;
4569
- r.style.setProperty( "--code-editor-row-height", row_pixels + "px" );
4570
- this.lineHeight = row_pixels;
5226
+ // Change row size
5227
+ const rowPixels = this.fontSize + 6;
5228
+ r.style.setProperty( "--code-editor-row-height", `${ rowPixels }px` );
5229
+ this.lineHeight = rowPixels;
4571
5230
 
4572
5231
  // Relocate cursors
4573
-
4574
5232
  this.relocateCursors();
4575
5233
 
4576
5234
  // Resize the code area
4577
-
4578
5235
  this.processLines();
4579
- }
4580
-
4581
- _decreaseFontSize() {
4582
-
4583
- // Change font size
4584
-
4585
- var r = document.querySelector( ':root' );
4586
- var s = getComputedStyle( r );
4587
- var pixels = parseInt( s.getPropertyValue( "--code-editor-font-size" ) );
4588
- pixels = LX.clamp( pixels - 1, CodeEditor.CODE_MIN_FONT_SIZE, CodeEditor.CODE_MAX_FONT_SIZE );
4589
- r.style.setProperty( "--code-editor-font-size", pixels + "px" );
4590
- this.charWidth = this._measureChar( "a", true );
4591
-
4592
- // Change row size
4593
-
4594
- var row_pixels = pixels + 6;
4595
- r.style.setProperty( "--code-editor-row-height", row_pixels + "px" );
4596
- this.lineHeight = row_pixels;
4597
5236
 
4598
- // Relocate cursors
5237
+ // Emit event
5238
+ LX.emit( "@font-size", this.fontSize );
5239
+ }
4599
5240
 
4600
- this.relocateCursors();
5241
+ _applyFontSizeOffset( offset = 0 ) {
5242
+ const newFontSize = LX.clamp( this.fontSize + offset, CodeEditor.CODE_MIN_FONT_SIZE, CodeEditor.CODE_MAX_FONT_SIZE );
5243
+ this._setFontSize( newFontSize );
5244
+ }
4601
5245
 
4602
- // Resize the code area
5246
+ _increaseFontSize() {
5247
+ this._applyFontSizeOffset( 1 );
5248
+ }
4603
5249
 
4604
- this.processLines();
5250
+ _decreaseFontSize() {
5251
+ this._applyFontSizeOffset( -1 );
4605
5252
  }
4606
5253
 
4607
5254
  _clearTmpVariables() {
4608
-
4609
5255
  delete this._currentLineString;
4610
5256
  delete this._currentLineNumber;
4611
5257
  delete this._buildingString;
@@ -4613,38 +5259,70 @@ class CodeEditor {
4613
5259
  delete this._buildingBlockComment;
4614
5260
  delete this._markdownHeader;
4615
5261
  delete this._lastResult;
5262
+ delete this._scopeStack;
4616
5263
  }
4617
5264
  }
4618
5265
 
4619
- CodeEditor.keywords = {
5266
+ CodeEditor.languages = {
5267
+ 'Plain Text': { ext: "txt", blockComments: false, singleLineComments: false, numbers: false, icon: "AlignLeft gray" },
5268
+ 'JavaScript': { ext: "js", icon: "Js goldenrod" },
5269
+ 'TypeScript': { ext: "ts", icon: "Ts pipelineblue" },
5270
+ 'C': { ext: [ 'c', 'h' ], usePreprocessor: true, icon: { 'c': "C pictonblue", 'h': "C heliotrope" } },
5271
+ 'C++': { ext: [ "cpp", "hpp" ], usePreprocessor: true, icon: { 'cpp': "CPlusPlus pictonblue", 'hpp': "CPlusPlus heliotrope" } },
5272
+ 'CSS': { ext: "css", icon: "Hash dodgerblue" },
5273
+ 'CMake': { ext: "cmake", singleLineCommentToken: '#', blockComments: false, ignoreCase: true },
5274
+ 'GLSL': { ext: "glsl", usePreprocessor: true },
5275
+ 'WGSL': { ext: "wgsl", usePreprocessor: true },
5276
+ 'JSON': { ext: "json", blockComments: false, singleLineComments: false, icon: "Braces fg-primary" },
5277
+ 'XML': { ext: "xml", tags: true, icon: "Rss orange" },
5278
+ 'Rust': { ext: "rs", icon: "Rust fg-primary" },
5279
+ 'Python': { ext: "py", singleLineCommentToken: '#', icon: "Python munsellblue" },
5280
+ 'HTML': { ext: "html", tags: true, singleLineComments: false, blockCommentsTokens: [ '<!--', '-->' ], numbers: false, icon: "Code orange" },
5281
+ 'Batch': { ext: "bat", blockComments: false, singleLineCommentToken: '::', ignoreCase: true, icon: "Windows lightblue" },
5282
+ 'Markdown': { ext: "md", blockComments: false, singleLineCommentToken: '::', tags: true, numbers: false, icon: "Markdown fg-primary" },
5283
+ 'PHP': { ext: "php", icon: "Php blueviolet" },
5284
+ };
5285
+
5286
+ CodeEditor.nativeTypes = {
5287
+ 'C++': ['int', 'float', 'double', 'bool', 'long', 'short', 'char', 'wchar_t', 'void']
5288
+ };
4620
5289
 
5290
+ CodeEditor.declarationKeywords = {
5291
+ 'JavaScript': ['var', 'let', 'const', 'this', 'static', 'class'],
5292
+ 'C++': [...CodeEditor.nativeTypes["C++"], 'const', 'auto', 'class', 'struct', 'namespace', 'enum', 'extern']
5293
+ };
5294
+
5295
+ CodeEditor.keywords = {
4621
5296
  'JavaScript': ['var', 'let', 'const', 'this', 'in', 'of', 'true', 'false', 'new', 'function', 'NaN', 'static', 'class', 'constructor', 'null', 'typeof', 'debugger', 'abstract',
4622
5297
  'arguments', 'extends', 'instanceof', 'Infinity'],
5298
+ 'TypeScript': ['var', 'let', 'const', 'this', 'in', 'of', 'true', 'false', 'new', 'function', 'class', 'extends', 'instanceof', 'Infinity', 'private', 'public', 'protected', 'interface',
5299
+ 'enum', 'type'],
4623
5300
  'C': ['int', 'float', 'double', 'long', 'short', 'char', 'const', 'void', 'true', 'false', 'auto', 'struct', 'typedef', 'signed', 'volatile', 'unsigned', 'static', 'extern', 'enum', 'register',
4624
5301
  'union'],
4625
- 'C++': ['int', 'float', 'double', 'bool', 'long', 'short', 'char', 'wchar_t', 'const', 'static_cast', 'dynamic_cast', 'new', 'delete', 'void', 'true', 'false', 'auto', 'class', 'struct', 'typedef', 'nullptr',
5302
+ 'C++': [...CodeEditor.nativeTypes["C++"], 'const', 'static_cast', 'dynamic_cast', 'new', 'delete', 'true', 'false', 'auto', 'class', 'struct', 'typedef', 'nullptr',
4626
5303
  'NULL', 'signed', 'unsigned', 'namespace', 'enum', 'extern', 'union', 'sizeof', 'static', 'private', 'public'],
4627
5304
  'CMake': ['cmake_minimum_required', 'set', 'not', 'if', 'endif', 'exists', 'string', 'strequal', 'add_definitions', 'macro', 'endmacro', 'file', 'list', 'source_group', 'add_executable',
4628
5305
  'target_include_directories', 'set_target_properties', 'set_property', 'add_compile_options', 'add_link_options', 'include_directories', 'add_library', 'target_link_libraries',
4629
5306
  'target_link_options', 'add_subdirectory', 'add_compile_definitions', 'project', 'cache'],
4630
5307
  'JSON': ['true', 'false'],
4631
5308
  'GLSL': ['true', 'false', 'function', 'int', 'float', 'vec2', 'vec3', 'vec4', 'mat2x2', 'mat3x3', 'mat4x4', 'struct'],
4632
- 'CSS': ['body', 'html', 'canvas', 'div', 'input', 'span', '.'],
4633
- 'WGSL': ['var', 'let', 'true', 'false', 'fn', 'bool', 'u32', 'i32', 'f16', 'f32', 'vec2f', 'vec3f', 'vec4f', 'mat2x2f', 'mat3x3f', 'mat4x4f', 'array', 'atomic', 'struct',
5309
+ 'CSS': ['body', 'html', 'canvas', 'div', 'input', 'span', '.', 'table', 'tr', 'td', 'th', 'label', 'video', 'img', 'code', 'button', 'select', 'option', 'svg', 'media', 'all',
5310
+ 'i', 'a', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'last-child', 'tbody', 'pre', 'monospace', 'font-face'],
5311
+ 'WGSL': ['var', 'let', 'true', 'false', 'fn', 'bool', 'u32', 'i32', 'f16', 'f32', 'vec2', 'vec3', 'vec4', 'vec2f', 'vec3f', 'vec4f', 'mat2x2f', 'mat3x3f', 'mat4x4f', 'array', 'atomic', 'struct',
4634
5312
  'sampler', 'sampler_comparison', 'texture_depth_2d', 'texture_depth_2d_array', 'texture_depth_cube', 'texture_depth_cube_array', 'texture_depth_multisampled_2d',
4635
5313
  'texture_external', 'texture_1d', 'texture_2d', 'texture_2d_array', 'texture_3d', 'texture_cube', 'texture_cube_array', 'texture_storage_1d', 'texture_storage_2d',
4636
- 'texture_storage_2d_array', 'texture_storage_3d', 'vec2u', 'vec3u', 'vec4u'],
5314
+ 'texture_storage_2d_array', 'texture_storage_3d', 'vec2u', 'vec3u', 'vec4u', 'ptr'],
4637
5315
  'Rust': ['as', 'const', 'crate', 'enum', 'extern', 'false', 'fn', 'impl', 'in', 'let', 'mod', 'move', 'mut', 'pub', 'ref', 'self', 'Self', 'static', 'struct', 'super', 'trait', 'true',
4638
5316
  'type', 'unsafe', 'use', 'where', 'abstract', 'become', 'box', 'final', 'macro', 'override', 'priv', 'typeof', 'unsized', 'virtual'],
4639
5317
  'Python': ['False', 'def', 'None', 'True', 'in', 'is', 'and', 'lambda', 'nonlocal', 'not', 'or'],
4640
- 'Batch': ['set', 'SET', 'echo', 'ECHO', 'off', 'OFF', 'del', 'DEL', 'defined', 'DEFINED', 'setlocal', 'SETLOCAL', 'enabledelayedexpansion', 'ENABLEDELAYEDEXPANSION', 'driverquery',
4641
- 'DRIVERQUERY', 'print', 'PRINT'],
5318
+ 'Batch': ['set', 'echo', 'off', 'del', 'defined', 'setlocal', 'enabledelayedexpansion', 'driverquery', 'print'],
4642
5319
  'HTML': ['html', 'meta', 'title', 'link', 'script', 'body', 'DOCTYPE', 'head', 'br', 'i', 'a', 'li', 'img', 'tr', 'td', 'h1', 'h2', 'h3', 'h4', 'h5'],
4643
5320
  'Markdown': ['br', 'i', 'a', 'li', 'img', 'table', 'title', 'tr', 'td', 'h1', 'h2', 'h3', 'h4', 'h5'],
5321
+ 'PHP': ['const', 'function', 'array', 'new', 'int', 'string', '$this', 'public', 'null', 'private', 'protected', 'implements', 'class', 'use', 'namespace', 'abstract', 'clone', 'final',
5322
+ 'enum'],
4644
5323
  };
4645
5324
 
4646
5325
  CodeEditor.utils = { // These ones don't have hightlight, used as suggestions to autocomplete only...
4647
-
4648
5326
  'JavaScript': ['querySelector', 'body', 'addEventListener', 'removeEventListener', 'remove', 'sort', 'keys', 'filter', 'isNaN', 'parseFloat', 'parseInt', 'EPSILON', 'isFinite',
4649
5327
  'bind', 'prototype', 'length', 'assign', 'entries', 'values', 'concat', 'substring', 'substr', 'splice', 'slice', 'buffer', 'appendChild', 'createElement', 'prompt',
4650
5328
  'alert'],
@@ -4656,43 +5334,47 @@ CodeEditor.utils = { // These ones don't have hightlight, used as suggestions to
4656
5334
  };
4657
5335
 
4658
5336
  CodeEditor.types = {
4659
-
4660
5337
  'JavaScript': ['Object', 'String', 'Function', 'Boolean', 'Symbol', 'Error', 'Number', 'TextEncoder', 'TextDecoder', 'Array', 'ArrayBuffer', 'InputEvent', 'MouseEvent',
4661
5338
  'Int8Array', 'Int16Array', 'Int32Array', 'Float32Array', 'Float64Array', 'Element'],
5339
+ 'TypeScript': ['arguments', 'constructor', 'null', 'typeof', 'debugger', 'abstract', 'Object', 'string', 'String', 'Function', 'Boolean', 'boolean', 'Error', 'Number', 'number', 'TextEncoder',
5340
+ 'TextDecoder', 'Array', 'ArrayBuffer', 'InputEvent', 'MouseEvent', 'Int8Array', 'Int16Array', 'Int32Array', 'Float32Array', 'Float64Array', 'Element'],
4662
5341
  'Rust': ['u128'],
4663
5342
  'Python': ['int', 'type', 'float', 'map', 'list', 'ArithmeticError', 'AssertionError', 'AttributeError', 'Exception', 'EOFError', 'FloatingPointError', 'GeneratorExit',
4664
5343
  'ImportError', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'NotImplementedError', 'OSError',
4665
5344
  'OverflowError', 'ReferenceError', 'RuntimeError', 'StopIteration', 'SyntaxError', 'TabError', 'SystemError', 'SystemExit', 'TypeError', 'UnboundLocalError',
4666
5345
  'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError', 'ValueError', 'ZeroDivisionError'],
4667
- 'C++': ['uint8_t', 'uint16_t', 'uint32_t']
5346
+ 'C++': ['uint8_t', 'uint16_t', 'uint32_t'],
5347
+ 'PHP': ['Exception', 'DateTime', 'JsonSerializable'],
4668
5348
  };
4669
5349
 
4670
5350
  CodeEditor.builtIn = {
4671
-
4672
5351
  'JavaScript': ['document', 'console', 'window', 'navigator', 'performance'],
4673
5352
  'CSS': ['*', '!important'],
4674
5353
  'C++': ['vector', 'list', 'map'],
4675
5354
  'HTML': ['type', 'xmlns', 'PUBLIC', 'http-equiv', 'src', 'style', 'lang', 'href', 'rel', 'content', 'xml', 'alt'], // attributes
4676
5355
  'Markdown': ['type', 'src', 'style', 'lang', 'href', 'rel', 'content', 'valign', 'alt'], // attributes
5356
+ 'PHP': ['echo', 'print'],
4677
5357
  };
4678
5358
 
4679
- CodeEditor.statementsAndDeclarations = {
4680
-
5359
+ CodeEditor.statements = {
4681
5360
  'JavaScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await'],
5361
+ 'TypeScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await', 'as'],
4682
5362
  'CSS': ['@', 'import'],
4683
5363
  'C': ['for', 'if', 'else', 'return', 'continue', 'break', 'case', 'switch', 'while', 'using', 'default', 'goto', 'do'],
4684
5364
  'C++': ['std', 'for', 'if', 'else', 'return', 'continue', 'break', 'case', 'switch', 'while', 'using', 'glm', 'spdlog', 'default'],
4685
5365
  'GLSL': ['for', 'if', 'else', 'return', 'continue', 'break'],
4686
- 'WGSL': ['const','for', 'if', 'else', 'return', 'continue', 'break', 'storage', 'read', 'read_write', 'uniform', 'function', 'workgroup'],
5366
+ 'WGSL': ['const','for', 'if', 'else', 'return', 'continue', 'break', 'storage', 'read', 'read_write', 'uniform', 'function', 'workgroup', 'bitcast'],
4687
5367
  'Rust': ['break', 'else', 'continue', 'for', 'if', 'loop', 'match', 'return', 'while', 'do', 'yield'],
4688
5368
  'Python': ['if', 'raise', 'del', 'import', 'return', 'elif', 'try', 'else', 'while', 'as', 'except', 'with', 'assert', 'finally', 'yield', 'break', 'for', 'class', 'continue',
4689
5369
  'global', 'pass', 'from'],
4690
- 'Batch': ['if', 'IF', 'for', 'FOR', 'in', 'IN', 'do', 'DO', 'call', 'CALL', 'goto', 'GOTO', 'exit', 'EXIT']
5370
+ 'Batch': ['if', 'IF', 'for', 'FOR', 'in', 'IN', 'do', 'DO', 'call', 'CALL', 'goto', 'GOTO', 'exit', 'EXIT'],
5371
+ 'PHP': ['declare', 'enddeclare', 'foreach', 'endforeach', 'if', 'else', 'elseif', 'endif', 'for', 'endfor', 'while', 'endwhile', 'switch', 'case', 'default', 'endswitch', 'return', 'break', 'continue',
5372
+ 'try', 'catch', 'die', 'do', 'exit', 'finally'],
4691
5373
  };
4692
5374
 
4693
5375
  CodeEditor.symbols = {
4694
-
4695
5376
  'JavaScript': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '??'],
5377
+ 'TypeScript': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '??'],
4696
5378
  'C': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '*', '-', '+'],
4697
5379
  'C++': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '::', '*', '-', '+'],
4698
5380
  'CMake': ['{', '}'],
@@ -4704,7 +5386,22 @@ CodeEditor.symbols = {
4704
5386
  'Python': ['<', '>', '[', ']', '(', ')', '='],
4705
5387
  'Batch': ['[', ']', '(', ')', '%'],
4706
5388
  'HTML': ['<', '>', '/'],
4707
- 'XML': ['<', '>', '/']
5389
+ 'XML': ['<', '>', '/'],
5390
+ 'PHP': ['[', ']', '{', '}', '(', ')'],
5391
+ };
5392
+
5393
+ CodeEditor.REGISTER_LANGUAGE = function( name, options = {}, def, rules )
5394
+ {
5395
+ CodeEditor.languages[ name ] = options;
5396
+
5397
+ if( def?.keywords ) CodeEditor.keywords[ name ] = new Set( def.keywords );
5398
+ if( def?.utils ) CodeEditor.utils[ name ] = new Set( def.utils );
5399
+ if( def?.types ) CodeEditor.types[ name ] = new Set( def.types );
5400
+ if( def?.builtIn ) CodeEditor.builtIn[ name ] = new Set( def.builtIn );
5401
+ if( def?.statements ) CodeEditor.statements[ name ] = new Set( def.statements );
5402
+ if( def?.symbols ) CodeEditor.symbols[ name ] = new Set( def.symbols );
5403
+
5404
+ if( rules ) HighlightRules[ name ] = rules;
4708
5405
  };
4709
5406
 
4710
5407
  LX.CodeEditor = CodeEditor;