lexgui 0.7.0 → 0.7.2

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,15 +33,6 @@ 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();
56
- }
57
-
58
36
  class CodeSelection {
59
37
 
60
38
  constructor( editor, cursor, className = "lexcodeselection" ) {
@@ -104,7 +82,6 @@ class CodeSelection {
104
82
 
105
83
  var domEl = document.createElement( 'div' );
106
84
  domEl.className = this.className;
107
-
108
85
  domEl._top = y * this.editor.lineHeight;
109
86
  domEl.style.top = domEl._top + "px";
110
87
  domEl._left = x * this.editor.charWidth;
@@ -213,6 +190,76 @@ class ScrollBar {
213
190
 
214
191
  }
215
192
 
193
+ /* Highlight rules
194
+ - test: function that receives a context object and returns true or false
195
+ - className: class to apply if test is true
196
+ - action: optional function to execute if test is true, receives context and editor as parameter
197
+ - discard: optional boolean, if true the token is discarded, action value is returned
198
+ to "ctx.discardToken" and no class is applied
199
+ */
200
+
201
+ const HighlightRules = {
202
+
203
+ common: [
204
+ { test: ctx => ctx.inBlockComment, className: "cm-com" },
205
+ { test: ctx => ctx.inString, action: (ctx, editor) => editor._appendStringToken( ctx.token ), discard: true },
206
+ { test: ctx => ctx.token.substr( 0, ctx.singleLineCommentToken.length ) == ctx.singleLineCommentToken, className: "cm-com" },
207
+ { test: (ctx, editor) => editor._isKeyword( ctx ), className: "cm-kwd" },
208
+ { test: (ctx, editor) => editor._mustHightlightWord( ctx.token, CodeEditor.builtIn, ctx.lang ) && ( ctx.lang.tags ?? false ? ( editor._enclosedByTokens( ctx.token, ctx.tokenIndex, '<', '>' ) ) : true ), className: "cm-bln" },
209
+ { test: (ctx, editor) => editor._mustHightlightWord( ctx.token, CodeEditor.statementsAndDeclarations, ctx.lang ), className: "cm-std" },
210
+ { test: (ctx, editor) => editor._mustHightlightWord( ctx.token, CodeEditor.symbols, ctx.lang ), className: "cm-sym" },
211
+ { test: (ctx, editor) => editor._mustHightlightWord( ctx.token, CodeEditor.types, ctx.lang ), className: "cm-typ" },
212
+ { test: (ctx, editor) => editor._isNumber( ctx.token ) || editor._isNumber( ctx.token.replace(/[px]|[em]|%/g,'') ), className: "cm-dec" },
213
+ { test: ctx => ctx.lang.usePreprocessor && ctx.token.includes( '#' ), className: "cm-ppc" },
214
+ ],
215
+
216
+ javascript: [
217
+ { test: ctx => (ctx.prev === 'class' && ctx.next === '{') || (ctx.prev === 'new' && ctx.next === '('), className: "cm-typ" }
218
+ ],
219
+
220
+ typescript: [
221
+ { test: ctx => ctx.scope && (ctx.token !== ',' && ctx.scope == "enum"), className: "cm-enu" },
222
+ { test: ctx => (ctx.prev === ':' && ctx.next !== undefined && isLetter(ctx.token) ) || (ctx.prev === 'interface' && ctx.next === '{') || (ctx.prev === 'enum' && ctx.next === '{'), className: "cm-typ" },
223
+ { test: ctx => (ctx.prev === 'class' && ctx.next === '{') || (ctx.prev === 'class' && ctx.next === '<') || (ctx.prev === 'new' && ctx.next === '(') || (ctx.prev === 'new' && ctx.next === '<'), className: "cm-typ" },
224
+ { test: (ctx, editor) => ctx.token !== ',' && editor._enclosedByTokens( ctx.token, ctx.tokenIndex, '<', '>' ), className: "cm-typ" },
225
+ ],
226
+
227
+ cpp: [
228
+ { test: ctx => ctx.scope && (ctx.token !== ',' && ctx.scope == "enum"), className: "cm-enu" },
229
+ { test: ctx => (ctx.prev === 'class' && ctx.next === '{') || (ctx.prev === 'struct' && ctx.next === '{'), className: "cm-typ" },
230
+ { test: ctx => ctx.prev === "<" && (ctx.next === ">" || ctx.next === "*"), className: "cm-typ" }, // Defining template type in C++
231
+ { test: ctx => ctx.next === "::" || (ctx.prev === "::" && ctx.next !== "("), className: "cm-typ" } // C++ Class
232
+ ],
233
+
234
+ wgsl: [
235
+ { test: ctx => ctx.prev === '>' && (!ctx.next || ctx.next === '{'), className: "cm-typ" }, // Function return type
236
+ { test: ctx => (ctx.prev === ':' && ctx.next !== undefined) || (ctx.prev === 'struct' && ctx.next === '{'), className: "cm-typ" },
237
+ { test: (ctx, editor) => ctx.token !== ',' && editor._enclosedByTokens( ctx.token, ctx.tokenIndex, '<', '>' ), className: "cm-typ" },
238
+ ],
239
+
240
+ css: [
241
+ { test: ctx => ( ctx.prev == '.' || ctx.prev == '::' || ( ctx.prev == ':' && ctx.next == '{' ) || ( ctx.token[ 0 ] == '#' && ctx.prev != ':' ) ), className: "cm-kwd" },
242
+ { test: ctx => ctx.prev === ':' && (ctx.next === ';' || ctx.next === '!important'), className: "cm-str" }, // CSS value
243
+ { test: ctx => ctx.prev === undefined && ctx.next === ":", className: "cm-typ" }, // CSS attribute
244
+ ],
245
+
246
+ batch: [
247
+ { test: ctx => ctx.token === '@' || ctx.prev === ':' || ctx.prev === '@', className: "cm-kwd" }
248
+ ],
249
+
250
+ markdown: [
251
+ { test: ctx => ctx.isFirstToken && ctx.token.replaceAll('#', '').length != ctx.token.length, action: (ctx, editor) => editor._markdownHeader = true, className: "cm-kwd" }
252
+ ],
253
+
254
+ php: [
255
+ { test: ctx => (ctx.prev === 'class' && ctx.next === '{') || (ctx.prev === 'class' && ctx.next === 'implements'), className: "cm-typ" },
256
+ ],
257
+
258
+ post_common: [
259
+ { test: ctx => isLetter(ctx.token) && (ctx.token[ 0 ] != '@') && (ctx.token[ 0 ] != ',') && (ctx.next === '('), className: "cm-mtd" }
260
+ ],
261
+ };
262
+
216
263
  /**
217
264
  * @class CodeEditor
218
265
  */
@@ -451,9 +498,13 @@ class CodeEditor {
451
498
  if( e.ctrlKey )
452
499
  {
453
500
  e.preventDefault();
501
+ e.stopPropagation();
454
502
  ( e.deltaY > 0.0 ? this._decreaseFontSize() : this._increaseFontSize() );
455
503
  }
456
- else
504
+ } );
505
+
506
+ this.codeScroller.addEventListener( 'wheel', e => {
507
+ if( !e.ctrlKey )
457
508
  {
458
509
  const dX = ( e.deltaY > 0.0 ? 10.0 : -10.0 ) * ( e.shiftKey ? 1.0 : 0.0 );
459
510
  if( dX != 0.0 ) this.setScrollBarValue( 'horizontal', dX );
@@ -495,17 +546,17 @@ class CodeEditor {
495
546
 
496
547
  // Add search box
497
548
  {
498
- var box = document.createElement( 'div' );
549
+ const box = document.createElement( 'div' );
499
550
  box.className = "searchbox";
500
551
 
501
- var searchPanel = new LX.Panel();
552
+ const searchPanel = new LX.Panel();
502
553
  box.appendChild( searchPanel.root );
503
554
 
504
555
  searchPanel.sameLine( 4 );
505
556
  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" } );
557
+ searchPanel.addButton( null, "up", () => this.search( null, true ), { icon: "ArrowUp", title: "Previous Match", tooltip: true } );
558
+ searchPanel.addButton( null, "down", () => this.search(), { icon: "ArrowDown", title: "Next Match", tooltip: true } );
559
+ searchPanel.addButton( null, "x", this.hideSearchBox.bind( this ), { icon: "X", title: "Close", tooltip: true } );
509
560
 
510
561
  box.querySelector( 'input' ).addEventListener( 'keyup', e => {
511
562
  if( e.key == 'Escape' ) this.hideSearchBox();
@@ -574,6 +625,7 @@ class CodeEditor {
574
625
  this.charWidth = 7; // To update later depending on size..
575
626
  this.defaultSingleLineCommentToken = '//';
576
627
  this.defaultBlockCommentTokens = [ '/*', '*/' ];
628
+ this.lineScopes = [];
577
629
  this._lastTime = null;
578
630
 
579
631
  this.pairKeys = {
@@ -592,24 +644,6 @@ class CodeEditor {
592
644
  // Scan tokens..
593
645
  // setInterval( this.scanWordSuggestions.bind( this ), 2000 );
594
646
 
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
647
  this.specialKeys = [
614
648
  'Backspace', 'Enter', 'ArrowUp', 'ArrowDown',
615
649
  'ArrowRight', 'ArrowLeft', 'Delete', 'Home',
@@ -1101,15 +1135,29 @@ class CodeEditor {
1101
1135
  const onLoadAll = () => {
1102
1136
  // Create inspector panel when the initial state is complete
1103
1137
  // and we have at least 1 tab opened
1104
- this.infoPanel = this._createInfoPanel();
1105
- if( this.infoPanel )
1138
+ this.statusPanel = this._createStatusPanel();
1139
+ if( this.statusPanel )
1106
1140
  {
1107
- area.attach( this.infoPanel );
1141
+ area.attach( this.statusPanel );
1108
1142
  }
1109
1143
 
1110
1144
  // Wait until the fonts are all loaded
1111
1145
  document.fonts.ready.then(() => {
1112
- this.charWidth = this._measureChar( "a", true );
1146
+ // Load any font size from local storage
1147
+ const savedFontSize = window.localStorage.getItem( "lexcodeeditor-font-size" );
1148
+ if( savedFontSize )
1149
+ {
1150
+ this._setFontSize( parseInt( savedFontSize ) );
1151
+ }
1152
+ else // Use default size
1153
+ {
1154
+ const r = document.querySelector( ':root' );
1155
+ const s = getComputedStyle( r );
1156
+ this.fontSize = parseInt( s.getPropertyValue( "--code-editor-font-size" ) );
1157
+ this.charWidth = this._measureChar( "a", true );
1158
+ }
1159
+
1160
+ LX.emit( "@font-size", this.fontSize );
1113
1161
  });
1114
1162
 
1115
1163
  window.editor = this;
@@ -1284,7 +1332,7 @@ class CodeEditor {
1284
1332
  options: options
1285
1333
  };
1286
1334
 
1287
- const ext = this.languages[ options.language ] ?. ext;
1335
+ const ext = CodeEditor.languages[ options.language ] ?. ext;
1288
1336
  this.addExplorerItem( { id: name, skipVisibility: true, icon: this._getFileIcon( name, ext ) } );
1289
1337
  this.explorer.innerTree.frefresh( name );
1290
1338
  }
@@ -1363,7 +1411,11 @@ class CodeEditor {
1363
1411
  get: (v) => { return cursor._position },
1364
1412
  set: (v) => {
1365
1413
  cursor._position = v;
1366
- if( cursor.isMain ) this._updateDataInfoPanel( "@cursor-pos", "Col " + ( v + 1 ) );
1414
+ if( cursor.isMain )
1415
+ {
1416
+ const activeLine = this.state.activeLine;
1417
+ this._updateDataInfoPanel( "@cursor-data", `Ln ${ activeLine + 1 }, Col ${ v + 1 }` );
1418
+ }
1367
1419
  }
1368
1420
  } );
1369
1421
 
@@ -1515,7 +1567,7 @@ class CodeEditor {
1515
1567
  this._updateDataInfoPanel( "@highlight", lang );
1516
1568
  this.processLines();
1517
1569
 
1518
- const ext = langExtension ?? this.languages[ lang ].ext;
1570
+ const ext = langExtension ?? CodeEditor.languages[ lang ].ext;
1519
1571
  const icon = this._getFileIcon( null, ext );
1520
1572
 
1521
1573
  // Update tab icon
@@ -1553,9 +1605,9 @@ class CodeEditor {
1553
1605
  return this._changeLanguage( this.code.language );
1554
1606
  }
1555
1607
 
1556
- for( let l in this.languages )
1608
+ for( let l in CodeEditor.languages )
1557
1609
  {
1558
- const langExtension = this.languages[ l ].ext;
1610
+ const langExtension = CodeEditor.languages[ l ].ext;
1559
1611
 
1560
1612
  if( langExtension.constructor == Array )
1561
1613
  {
@@ -1577,17 +1629,25 @@ class CodeEditor {
1577
1629
  this._changeLanguage( 'Plain Text' );
1578
1630
  }
1579
1631
 
1580
- _createInfoPanel() {
1632
+ _createStatusPanel() {
1581
1633
 
1582
1634
  if( !this.skipInfo )
1583
1635
  {
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 ) => {
1636
+ let panel = new LX.Panel({ className: "lexcodetabinfo flex flex-row", height: "auto" });
1637
+
1638
+ let leftStatusPanel = new LX.Panel( { id: "FontSizeZoomStatusComponent", height: "auto" } );
1639
+ leftStatusPanel.sameLine();
1640
+ leftStatusPanel.addButton( null, "ZoomOutButton", this._decreaseFontSize.bind( this ), { icon: "ZoomOut", width: "32px", title: "Zoom Out", tooltip: true } );
1641
+ leftStatusPanel.addLabel( this.fontSize ?? 14, { fit: true, signal: "@font-size" });
1642
+ leftStatusPanel.addButton( null, "ZoomInButton", this._increaseFontSize.bind( this ), { icon: "ZoomIn", width: "32px", title: "Zoom In", tooltip: true } );
1643
+ leftStatusPanel.endLine( "justify-start" );
1644
+ panel.attach( leftStatusPanel.root );
1645
+
1646
+ let rightStatusPanel = new LX.Panel( { height: "auto" } );
1647
+ rightStatusPanel.sameLine();
1648
+ rightStatusPanel.addLabel( this.code.title, { id: "EditorFilenameStatusComponent", fit: true, signal: "@tab-name" });
1649
+ rightStatusPanel.addButton( null, "Ln 1, Col 1", this.showSearchLineBox.bind( this ), { id: "EditorSelectionStatusComponent", fit: true, signal: "@cursor-data" });
1650
+ rightStatusPanel.addButton( null, "Spaces: " + this.tabSpaces, ( value, event ) => {
1591
1651
  LX.addContextMenu( "Spaces", event, m => {
1592
1652
  const options = [ 2, 4, 8 ];
1593
1653
  for( const n of options )
@@ -1597,24 +1657,57 @@ class CodeEditor {
1597
1657
  this._updateDataInfoPanel( "@tab-spaces", "Spaces: " + this.tabSpaces );
1598
1658
  } );
1599
1659
  });
1600
- }, { nameWidth: "15%", signal: "@tab-spaces" });
1601
- panel.addButton( "<b>{ }</b>", this.highlight, ( value, event ) => {
1660
+ }, { id: "EditorIndentationStatusComponent", nameWidth: "15%", signal: "@tab-spaces" });
1661
+ rightStatusPanel.addButton( "<b>{ }</b>", this.highlight, ( value, event ) => {
1602
1662
  LX.addContextMenu( "Language", event, m => {
1603
- for( const lang of Object.keys( this.languages ) )
1663
+ for( const lang of Object.keys( CodeEditor.languages ) )
1604
1664
  {
1605
1665
  m.add( lang, v => {
1606
1666
  this._changeLanguage( v, null, true )
1607
1667
  } );
1608
1668
  }
1609
1669
  });
1610
- }, { nameWidth: "15%", signal: "@highlight" });
1611
- panel.endLine();
1670
+ }, { id: "EditorLanguageStatusComponent", nameWidth: "15%", signal: "@highlight" });
1671
+ rightStatusPanel.endLine( "justify-end" );
1672
+ panel.attach( rightStatusPanel.root );
1673
+
1674
+ const itemVisibilityMap = {
1675
+ "Font Size Zoom": true,
1676
+ "Editor Filename": true,
1677
+ "Editor Selection": true,
1678
+ "Editor Indentation": true,
1679
+ "Editor Language": true,
1680
+ };
1681
+
1682
+ panel.root.addEventListener( "contextmenu", (e) => {
1683
+
1684
+ if( e.target && ( e.target.classList.contains( "lexpanel" ) || e.target.classList.contains( "lexinlinecomponents" ) ) )
1685
+ {
1686
+ return;
1687
+ }
1688
+
1689
+ const menuOptions = Object.keys( itemVisibilityMap ).map( ( itemName, idx ) => {
1690
+ const item = {
1691
+ name: itemName,
1692
+ icon: "Check",
1693
+ callback: () => {
1694
+ itemVisibilityMap[ itemName ] = !itemVisibilityMap[ itemName ];
1695
+ const b = panel.root.querySelector( `#${ itemName.replaceAll( " ", "" ) }StatusComponent` );
1696
+ console.assert( b, `${ itemName } has no status button!` );
1697
+ b.classList.toggle( "hidden", !itemVisibilityMap[ itemName ] );
1698
+ }
1699
+ }
1700
+ if( !itemVisibilityMap[ itemName ] ) delete item.icon;
1701
+ return item;
1702
+ } );
1703
+ new LX.DropdownMenu( e.target, menuOptions, { side: "top", align: "start" });
1704
+ } );
1612
1705
 
1613
1706
  return panel;
1614
1707
  }
1615
1708
  else
1616
1709
  {
1617
- doAsync( () => {
1710
+ LX.doAsync( () => {
1618
1711
 
1619
1712
  // Change css a little bit...
1620
1713
  this.gutter.style.height = "calc(100% - 28px)";
@@ -1623,7 +1716,7 @@ class CodeEditor {
1623
1716
  this.base_area.root.querySelector( '.lexcodescrollbar.vertical' ).style.height = "calc(100% - 27px)";
1624
1717
  this.tabs.area.root.classList.add( 'no-code-info' );
1625
1718
 
1626
- }, 100);
1719
+ }, 100 );
1627
1720
  }
1628
1721
  }
1629
1722
 
@@ -1661,6 +1754,7 @@ class CodeEditor {
1661
1754
  extension == "bat" ? "Windows lightblue" :
1662
1755
  extension == "json" ? "Braces fg-primary" :
1663
1756
  extension == "js" ? "Js goldenrod" :
1757
+ extension == "ts" ? "Ts pipelineblue" :
1664
1758
  extension == "py" ? "Python munsellblue" :
1665
1759
  extension == "rs" ? "Rust fg-primary" :
1666
1760
  extension == "md" ? "Markdown fg-primary" :
@@ -1668,6 +1762,7 @@ class CodeEditor {
1668
1762
  extension == "hpp" ? "CPlusPlus heliotrope" :
1669
1763
  extension == "c" ? "C pictonblue" :
1670
1764
  extension == "h" ? "C heliotrope" :
1765
+ extension == "php" ? "Php blueviolet" :
1671
1766
  !isNewTabButton ? "AlignLeft gray" : undefined;
1672
1767
  }
1673
1768
 
@@ -2709,7 +2804,7 @@ class CodeEditor {
2709
2804
 
2710
2805
  _commentLine( cursor, line, minNonspaceIdx ) {
2711
2806
 
2712
- const lang = this.languages[ this.highlight ];
2807
+ const lang = CodeEditor.languages[ this.highlight ];
2713
2808
 
2714
2809
  if( !( lang.singleLineComments ?? true ))
2715
2810
  return;
@@ -2762,7 +2857,7 @@ class CodeEditor {
2762
2857
 
2763
2858
  _uncommentLine( cursor, line ) {
2764
2859
 
2765
- const lang = this.languages[ this.highlight ];
2860
+ const lang = CodeEditor.languages[ this.highlight ];
2766
2861
 
2767
2862
  if( !( lang.singleLineComments ?? true ))
2768
2863
  return;
@@ -2804,8 +2899,8 @@ class CodeEditor {
2804
2899
 
2805
2900
  for( let i = 0; i < this.code.lines.length; ++i )
2806
2901
  {
2807
- const linestring = this.code.lines[ i ];
2808
- const tokens = this._getTokensFromLine( linestring, true );
2902
+ const lineString = this.code.lines[ i ];
2903
+ const tokens = this._getTokensFromLine( lineString, true );
2809
2904
  tokens.forEach( t => this.code.tokens[ t ] = 1 );
2810
2905
  }
2811
2906
  }
@@ -2823,7 +2918,7 @@ class CodeEditor {
2823
2918
 
2824
2919
  processLines( mode ) {
2825
2920
 
2826
- var code_html = "";
2921
+ var htmlCode = "";
2827
2922
  this._blockCommentCache.length = 0;
2828
2923
 
2829
2924
  // Reset all lines content
@@ -2843,16 +2938,20 @@ class CodeEditor {
2843
2938
  {
2844
2939
  const diff = Math.max( this.code.lines.length - this.visibleLinesViewport.y, 0 );
2845
2940
  if( diff <= this.lineScrollMargin.y )
2941
+ {
2846
2942
  this.visibleLinesViewport.y += diff;
2943
+ }
2847
2944
  }
2848
2945
 
2946
+ this._scopeStack = [];
2947
+
2849
2948
  // Process visible lines
2850
2949
  for( let i = this.visibleLinesViewport.x; i < this.visibleLinesViewport.y; ++i )
2851
2950
  {
2852
- code_html += this.processLine( i, true );
2951
+ htmlCode += this.processLine( i, true );
2853
2952
  }
2854
2953
 
2855
- this.code.innerHTML = code_html;
2954
+ this.code.innerHTML = htmlCode;
2856
2955
 
2857
2956
  // Update scroll data
2858
2957
  this.codeScroller.scrollTop = lastScrollTop;
@@ -2866,28 +2965,53 @@ class CodeEditor {
2866
2965
  this.resize();
2867
2966
  }
2868
2967
 
2869
- processLine( linenum, force ) {
2968
+ processLine( lineNumber, force ) {
2870
2969
 
2871
2970
  // Check if we are in block comment sections..
2872
- if( !force && this._inBlockCommentSection( linenum ) )
2971
+ if( !force && this._inBlockCommentSection( lineNumber ) )
2873
2972
  {
2874
2973
  this.processLines();
2875
2974
  return;
2876
2975
  }
2877
2976
 
2878
- const lang = this.languages[ this.highlight ];
2879
- const localLineNum = this.toLocalLine( linenum );
2880
- const gutterLineHtml = "<span class='line-gutter'>" + (linenum + 1) + "</span>";
2977
+ if( this._scopeStack )
2978
+ {
2979
+ this.lineScopes[ lineNumber ] = [ ...this._scopeStack ];
2980
+ }
2981
+ else
2982
+ {
2983
+ this._scopeStack = [ ...this.lineScopes[ lineNumber ] ];
2984
+ }
2881
2985
 
2882
- const UPDATE_LINE = ( html ) => {
2986
+ const lang = CodeEditor.languages[ this.highlight ];
2987
+ const localLineNum = this.toLocalLine( lineNumber );
2988
+ const gutterLineHtml = `<span class='line-gutter'>${ lineNumber + 1 }</span>`;
2989
+
2990
+ const _updateLine = ( html ) => {
2883
2991
  if( !force ) // Single line update
2884
2992
  {
2993
+ _processNextLineIfNecessary();
2885
2994
  this.code.childNodes[ localLineNum ].innerHTML = gutterLineHtml + html;
2886
- this._setActiveLine( linenum );
2995
+ this._setActiveLine( lineNumber );
2887
2996
  this._clearTmpVariables();
2888
2997
  }
2889
2998
  else // Update all lines at once
2890
- return "<pre>" + ( gutterLineHtml + html ) + "</pre>";
2999
+ {
3000
+ return `<pre>${ gutterLineHtml + html }</pre>`;
3001
+ }
3002
+ }
3003
+
3004
+ const _processNextLineIfNecessary = () => {
3005
+ // Only update scope stack if something changed when editing a single line
3006
+ if( this._scopeStack.toString() == this.lineScopes[ lineNumber + 1 ]?.toString() )
3007
+ return;
3008
+
3009
+ this.lineScopes[ lineNumber + 1 ] = [ ...this._scopeStack ];
3010
+
3011
+ if( this.code.lines.length > lineNumber + 1 )
3012
+ {
3013
+ this.processLine( lineNumber + 1 );
3014
+ }
2891
3015
  }
2892
3016
 
2893
3017
  // multi-line strings not supported by now
@@ -2895,7 +3019,7 @@ class CodeEditor {
2895
3019
  delete this._pendingString;
2896
3020
  delete this._markdownHeader;
2897
3021
 
2898
- let linestring = this.code.lines[ linenum ];
3022
+ let lineString = this.code.lines[ lineNumber ];
2899
3023
 
2900
3024
  // Single line
2901
3025
  if( !force )
@@ -2907,35 +3031,36 @@ class CodeEditor {
2907
3031
  // Early out check for no highlighting languages
2908
3032
  if( this.highlight == 'Plain Text' )
2909
3033
  {
2910
- const plainTextHtml = linestring.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
2911
- return UPDATE_LINE( plainTextHtml );
3034
+ const plainTextHtml = lineString.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
3035
+ return _updateLine( plainTextHtml );
2912
3036
  }
2913
3037
 
2914
- this._currentLineNumber = linenum;
2915
- this._currentLineString = linestring;
2916
-
2917
- const tokensToEvaluate = this._getTokensFromLine( linestring );
3038
+ this._currentLineNumber = lineNumber;
3039
+ this._currentLineString = lineString;
2918
3040
 
3041
+ const tokensToEvaluate = this._getTokensFromLine( lineString );
2919
3042
  if( !tokensToEvaluate.length )
2920
3043
  {
2921
- return "<pre><span class='line-gutter'>" + linenum + "</span></pre>";
3044
+ return _updateLine( "" );
2922
3045
  }
2923
3046
 
2924
- var lineInnerHtml = "";
3047
+ let lineInnerHtml = "";
2925
3048
 
2926
3049
  // Process all tokens
2927
- for( var i = 0; i < tokensToEvaluate.length; ++i )
3050
+ for( let i = 0; i < tokensToEvaluate.length; ++i )
2928
3051
  {
2929
3052
  let it = i - 1;
2930
3053
  let prev = tokensToEvaluate[ it ];
2931
- while( prev == ' ' ) {
3054
+ while( prev == ' ' )
3055
+ {
2932
3056
  it--;
2933
3057
  prev = tokensToEvaluate[ it ];
2934
3058
  }
2935
3059
 
2936
3060
  it = i + 1;
2937
3061
  let next = tokensToEvaluate[ it ];
2938
- while( next == ' ' || next == '"' ) {
3062
+ while( next == ' ' || next == '"' )
3063
+ {
2939
3064
  it++;
2940
3065
  next = tokensToEvaluate[ it ];
2941
3066
  }
@@ -2946,7 +3071,13 @@ class CodeEditor {
2946
3071
  {
2947
3072
  const blockCommentsToken = ( lang.blockCommentsTokens ?? this.defaultBlockCommentTokens )[ 0 ];
2948
3073
  if( token.substr( 0, blockCommentsToken.length ) == blockCommentsToken )
2949
- this._buildingBlockComment = linenum;
3074
+ this._buildingBlockComment = lineNumber;
3075
+ }
3076
+
3077
+ // Pop current scope if necessary
3078
+ if( token === "}" )
3079
+ {
3080
+ this._scopeStack.pop();
2950
3081
  }
2951
3082
 
2952
3083
  lineInnerHtml += this._evaluateToken( {
@@ -2960,20 +3091,49 @@ class CodeEditor {
2960
3091
  isLastToken: (i == tokensToEvaluate.length - 1),
2961
3092
  tokens: tokensToEvaluate
2962
3093
  } );
3094
+
3095
+ // Store current scopes
3096
+ if( token === "{" )
3097
+ {
3098
+ // Get some context about the scope from previous lines
3099
+ const contextTokens = [
3100
+ ...this._getTokensFromLine( this.code.lines[ lineNumber - 2 ] ),
3101
+ ...this._getTokensFromLine( this.code.lines[ lineNumber - 1 ] ),
3102
+ ...this._getTokensFromLine( this.code.lines[ lineNumber ].substring( 0, lineString.indexOf( token ) ) )
3103
+ ].reverse().filter( v => v.length && v != ' ' );
3104
+
3105
+ let scopeType = contextTokens[ 1 ]; // This is the type of scope (function, class, enum, etc)
3106
+
3107
+ // Special cases
3108
+ if( scopeType === ":" ) // enum type specification
3109
+ {
3110
+ scopeType = contextTokens[ 3 ];
3111
+ }
3112
+
3113
+ if( !this._mustHightlightWord( scopeType, CodeEditor.keywords, lang ) )
3114
+ {
3115
+ // If the scope type is not a keyword, use an empty string
3116
+ scopeType = "";
3117
+ }
3118
+
3119
+ this._scopeStack.push( scopeType );
3120
+ }
2963
3121
  }
2964
3122
 
2965
- return UPDATE_LINE( lineInnerHtml );
3123
+ return _updateLine( lineInnerHtml );
2966
3124
  }
2967
3125
 
2968
- _lineHasComment( linestring ) {
3126
+ _lineHasComment( lineString ) {
2969
3127
 
2970
- const lang = this.languages[ this.highlight ];
3128
+ const lang = CodeEditor.languages[ this.highlight ];
2971
3129
 
2972
3130
  if( !(lang.singleLineComments ?? true) )
3131
+ {
2973
3132
  return;
3133
+ }
2974
3134
 
2975
3135
  const singleLineCommentToken = lang.singleLineCommentToken ?? this.defaultSingleLineCommentToken;
2976
- const idx = linestring.indexOf( singleLineCommentToken );
3136
+ const idx = lineString.indexOf( singleLineCommentToken );
2977
3137
 
2978
3138
  if( idx > -1 )
2979
3139
  {
@@ -2982,22 +3142,27 @@ class CodeEditor {
2982
3142
  var err = false;
2983
3143
  err |= stringKeys.some( function(v) {
2984
3144
  var re = new RegExp( v, "g" );
2985
- var matches = (linestring.substring( 0, idx ).match( re ) || []);
3145
+ var matches = (lineString.substring( 0, idx ).match( re ) || []);
2986
3146
  return (matches.length % 2) !== 0;
2987
3147
  } );
2988
3148
  return err ? undefined : idx;
2989
3149
  }
2990
3150
  }
2991
3151
 
2992
- _getTokensFromLine( linestring, skipNonWords ) {
3152
+ _getTokensFromLine( lineString, skipNonWords ) {
3153
+
3154
+ if( !lineString || !lineString.length )
3155
+ {
3156
+ return [];
3157
+ }
2993
3158
 
2994
3159
  // Check if line comment
2995
- const ogLine = linestring;
2996
- const hasCommentIdx = this._lineHasComment( linestring );
3160
+ const ogLine = lineString;
3161
+ const hasCommentIdx = this._lineHasComment( lineString );
2997
3162
 
2998
3163
  if( hasCommentIdx != undefined )
2999
3164
  {
3000
- linestring = ogLine.substring( 0, hasCommentIdx );
3165
+ lineString = ogLine.substring( 0, hasCommentIdx );
3001
3166
  }
3002
3167
 
3003
3168
  let tokensToEvaluate = []; // store in a temp array so we know prev and next tokens...
@@ -3013,25 +3178,25 @@ class CodeEditor {
3013
3178
  charCounter += t.length;
3014
3179
  };
3015
3180
 
3016
- let iter = linestring.matchAll(/(<!--|-->|\*\/|\/\*|::|[\[\](){}<>.,;:*"'%@!/= ])/g);
3181
+ let iter = lineString.matchAll(/(<!--|-->|\*\/|\/\*|::|[\[\](){}<>.,;:*"'%@$!/= ])/g);
3017
3182
  let subtokens = iter.next();
3018
3183
  if( subtokens.value )
3019
3184
  {
3020
3185
  let idx = 0;
3021
3186
  while( subtokens.value != undefined )
3022
3187
  {
3023
- const _pt = linestring.substring(idx, subtokens.value.index);
3188
+ const _pt = lineString.substring(idx, subtokens.value.index);
3024
3189
  if( _pt.length ) pushToken( _pt );
3025
3190
  pushToken( subtokens.value[ 0 ] );
3026
3191
  idx = subtokens.value.index + subtokens.value[ 0 ].length;
3027
3192
  subtokens = iter.next();
3028
3193
  if(!subtokens.value) {
3029
- const _at = linestring.substring(idx);
3194
+ const _at = lineString.substring(idx);
3030
3195
  if( _at.length ) pushToken( _at );
3031
3196
  }
3032
3197
  }
3033
3198
  }
3034
- else pushToken( linestring );
3199
+ else pushToken( lineString );
3035
3200
 
3036
3201
  if( hasCommentIdx != undefined )
3037
3202
  {
@@ -3061,6 +3226,22 @@ class CodeEditor {
3061
3226
  // Scan for numbers again
3062
3227
  return this._processTokens( tokens, idx );
3063
3228
  }
3229
+
3230
+ const importantIdx = tokens.indexOf( 'important' );
3231
+ if( this.highlight == 'CSS' && importantIdx > -1 && tokens[ importantIdx - 1 ] === '!' )
3232
+ {
3233
+ tokens[ importantIdx - 1 ] = "!important";
3234
+ tokens.splice( importantIdx, 1 );
3235
+ }
3236
+ }
3237
+ else if( this.highlight == 'PHP' )
3238
+ {
3239
+ const dollarIdx = tokens.indexOf( '$' );
3240
+ if( dollarIdx > -1 && tokens[ dollarIdx + 1 ] === 'this-' )
3241
+ {
3242
+ tokens[ dollarIdx ] = "$this";
3243
+ tokens[ dollarIdx + 1 ] = "-";
3244
+ }
3064
3245
  }
3065
3246
 
3066
3247
  return tokens;
@@ -3070,7 +3251,7 @@ class CodeEditor {
3070
3251
 
3071
3252
  if( !lang )
3072
3253
  {
3073
- lang = this.languages[ this.highlight ];
3254
+ lang = CodeEditor.languages[ this.highlight ];
3074
3255
  }
3075
3256
 
3076
3257
  let t = token;
@@ -3083,16 +3264,30 @@ class CodeEditor {
3083
3264
  return kindArray[ this.highlight ] && kindArray[ this.highlight ][ t ] != undefined;
3084
3265
  }
3085
3266
 
3267
+ _getTokenHighlighting( ctx, highlight ) {
3268
+
3269
+ const rules = [ ...HighlightRules.common, ...( HighlightRules[highlight] || [] ), ...HighlightRules.post_common ];
3270
+
3271
+ for( const rule of rules )
3272
+ {
3273
+ if( !rule.test( ctx, this ) )
3274
+ {
3275
+ continue;
3276
+ }
3277
+
3278
+ const r = rule.action ? rule.action( ctx, this ) : undefined;
3279
+ if( rule.discard ) ctx.discardToken = r;
3280
+ return rule.className;
3281
+ }
3282
+
3283
+ return null;
3284
+ }
3285
+
3086
3286
  _evaluateToken( ctxData ) {
3087
3287
 
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;
3288
+ let { token, prev, next, tokenIndex, isFirstToken, isLastToken } = ctxData;
3094
3289
 
3095
- const lang = this.languages[ this.highlight ],
3290
+ const lang = CodeEditor.languages[ this.highlight ],
3096
3291
  highlight = this.highlight.replace( /\s/g, '' ).replaceAll( "+", "p" ).toLowerCase(),
3097
3292
  customStringKeys = Object.assign( {}, this.stringKeys );
3098
3293
 
@@ -3114,14 +3309,14 @@ class CodeEditor {
3114
3309
 
3115
3310
  if( usePreviousTokenToCheckString || ( this._buildingBlockComment === undefined && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) ) )
3116
3311
  {
3117
- const checkIfStringEnded = t => {
3312
+ const _checkIfStringEnded = t => {
3118
3313
  const idx = Object.values( customStringKeys ).indexOf( t );
3119
3314
  this._stringEnded = (idx > -1) && (idx == Object.values(customStringKeys).indexOf( customStringKeys[ '@' + this._buildingString ] ));
3120
3315
  };
3121
3316
 
3122
3317
  if( this._buildingString != undefined )
3123
3318
  {
3124
- checkIfStringEnded( usePreviousTokenToCheckString ? ctxData.nextWithSpaces : token );
3319
+ _checkIfStringEnded( usePreviousTokenToCheckString ? ctxData.nextWithSpaces : token );
3125
3320
  }
3126
3321
  else if( customStringKeys[ '@' + ( usePreviousTokenToCheckString ? ctxData.prevWithSpaces : token ) ] )
3127
3322
  {
@@ -3131,77 +3326,25 @@ class CodeEditor {
3131
3326
  // Check if string ended in same token using next...
3132
3327
  if( usePreviousTokenToCheckString )
3133
3328
  {
3134
- checkIfStringEnded( ctxData.nextWithSpaces );
3329
+ _checkIfStringEnded( ctxData.nextWithSpaces );
3135
3330
  }
3136
3331
  }
3137
3332
  }
3138
3333
 
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";
3187
-
3188
- else if ( highlight == 'css' && prev == ':' && (next == ';' || next == '!important') ) // CSS value
3189
- token_classname = "cm-str";
3190
-
3191
- else if ( highlight == 'css' && prev == undefined && next == ':' ) // CSS attribute
3192
- token_classname = "cm-typ";
3193
-
3194
- else if ( this._markdownHeader || ( highlight == 'markdown' && isFirstToken && token.replaceAll('#', '').length != token.length ) ) // Header
3195
- {
3196
- token_classname = "cm-kwd";
3197
- this._markdownHeader = true;
3198
- }
3199
-
3200
- else if ( token[ 0 ] != '@' && token[ 0 ] != ',' && next == '(' )
3201
- token_classname = "cm-mtd";
3334
+ // Update context data for next tests
3335
+ ctxData.discardToken = false;
3336
+ ctxData.inBlockComment = this._buildingBlockComment;
3337
+ ctxData.markdownHeader = this._markdownHeader;
3338
+ ctxData.inString = this._buildingString;
3339
+ ctxData.singleLineCommentToken = lang.singleLineCommentToken ?? this.defaultSingleLineCommentToken;
3340
+ ctxData.lang = lang;
3341
+ ctxData.scope = this._scopeStack[ this._scopeStack.length - 1 ];
3202
3342
 
3343
+ // Get highlighting class based on language common and specific rules
3344
+ let tokenClass = this._getTokenHighlighting( ctxData, highlight );
3203
3345
 
3204
- if( usesBlockComments && this._buildingBlockComment != undefined
3346
+ const blockCommentsTokens = lang.blockCommentsTokens ?? this.defaultBlockCommentTokens;
3347
+ if( ( lang.blockComments ?? true ) && this._buildingBlockComment != undefined
3205
3348
  && token.substr( 0, blockCommentsTokens[ 1 ].length ) == blockCommentsTokens[ 1 ] )
3206
3349
  {
3207
3350
  this._blockCommentCache.push( new LX.vec2( this._buildingBlockComment, this._currentLineNumber ) );
@@ -3212,34 +3355,36 @@ class CodeEditor {
3212
3355
  if( this._buildingString && ( this._stringEnded || isLastToken ) )
3213
3356
  {
3214
3357
  token = this._getCurrentString();
3215
- token_classname = "cm-str";
3216
- discardToken = false;
3358
+ tokenClass = "cm-str";
3359
+ ctxData.discardToken = false;
3217
3360
  }
3218
3361
 
3219
3362
  // Update state
3220
3363
  this._buildingString = this._stringEnded ? undefined : this._buildingString;
3221
3364
 
3222
- if( discardToken )
3365
+ if( ctxData.discardToken )
3223
3366
  {
3224
3367
  return "";
3225
3368
  }
3226
3369
 
3227
- token = token.replace( "<", "&lt;" );
3228
- token = token.replace( ">", "&gt;" );
3370
+ // Replace html chars
3371
+ token = token.replace( "<", "&lt;" ).replace( ">", "&gt;" );
3229
3372
 
3230
3373
  // No highlighting, no need to put it inside another span..
3231
- if( !token_classname.length )
3374
+ if( !tokenClass )
3232
3375
  {
3233
3376
  return token;
3234
3377
  }
3235
3378
 
3236
- return "<span class='" + highlight + " " + token_classname + "'>" + token + "</span>";
3379
+ return `<span class="${ highlight } ${ tokenClass }">${ token }</span>`;
3237
3380
  }
3238
3381
 
3239
3382
  _appendStringToken( token ) {
3240
3383
 
3241
3384
  if( !this._pendingString )
3385
+ {
3242
3386
  this._pendingString = "";
3387
+ }
3243
3388
 
3244
3389
  this._pendingString += token;
3245
3390
 
@@ -3285,11 +3430,9 @@ class CodeEditor {
3285
3430
  return false;
3286
3431
  }
3287
3432
 
3288
- _isKeyword( ctxData, lang ) {
3433
+ _isKeyword( ctxData ) {
3289
3434
 
3290
- const token = ctxData.token;
3291
- const tokenIndex = ctxData.tokenIndex;
3292
- const tokens = ctxData.tokens;
3435
+ const { token, tokenIndex, tokens, lang } = ctxData;
3293
3436
 
3294
3437
  let isKwd = this._mustHightlightWord( token, CodeEditor.keywords ) || this.highlight == 'XML';
3295
3438
 
@@ -3306,6 +3449,10 @@ class CodeEditor {
3306
3449
  isKwd |= ( ctxData.tokens[ tokenIndex - 2 ] == '$' );
3307
3450
  }
3308
3451
  }
3452
+ if( this.highlight == 'Markdown' )
3453
+ {
3454
+ isKwd = ( this._markdownHeader !== undefined );
3455
+ }
3309
3456
  else if( lang.tags )
3310
3457
  {
3311
3458
  isKwd &= ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) != undefined );
@@ -3315,24 +3462,14 @@ class CodeEditor {
3315
3462
  return isKwd;
3316
3463
  }
3317
3464
 
3318
- _isCSSClass( ctxData ) {
3319
-
3320
- const token = ctxData.token;
3321
- const prev = ctxData.prev;
3322
- const next = ctxData.next;
3465
+ _isNumber( token ) {
3323
3466
 
3324
- if( this.highlight != 'CSS' )
3467
+ const lang = CodeEditor.languages[ this.highlight ];
3468
+ if( !( lang.numbers ?? true ) )
3325
3469
  {
3326
3470
  return false;
3327
3471
  }
3328
3472
 
3329
- return ( prev == '.' || prev == '::'
3330
- || ( prev == ':' && next == '{' )
3331
- || ( token[ 0 ] == '#' && prev != ':' ) );
3332
- }
3333
-
3334
- _isNumber( token ) {
3335
-
3336
3473
  const subToken = token.substring( 0, token.length - 1 );
3337
3474
 
3338
3475
  if( this.highlight == 'C++' )
@@ -3364,53 +3501,24 @@ class CodeEditor {
3364
3501
  return token.length && token != ' ' && !Number.isNaN( +token );
3365
3502
  }
3366
3503
 
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
- }
3504
+ _encloseSelectedWordWithKey( key, lidx, cursor ) {
3378
3505
 
3379
- if( this.highlight == 'JavaScript' )
3380
- {
3381
- return (prev == 'class' && next == '{') || (prev == 'new' && next == '(');
3382
- }
3383
- else if( this.highlight == 'C++' )
3506
+ if( !cursor.selection || ( cursor.selection.fromY != cursor.selection.toY ) )
3384
3507
  {
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 ));
3508
+ return false;
3395
3509
  }
3396
- }
3397
-
3398
- _encloseSelectedWordWithKey( key, lidx, cursor ) {
3399
-
3400
- if( !cursor.selection || (cursor.selection.fromY != cursor.selection.toY) )
3401
- return false;
3402
3510
 
3403
3511
  cursor.selection.invertIfNecessary();
3404
3512
 
3405
3513
  // Insert first..
3406
3514
  this.code.lines[ lidx ] = [
3407
- this.code.lines[ lidx ].slice(0, cursor.selection.fromX),
3515
+ this.code.lines[ lidx ].slice( 0, cursor.selection.fromX ),
3408
3516
  key,
3409
- this.code.lines[ lidx ].slice(cursor.selection.fromX)
3517
+ this.code.lines[ lidx ].slice( cursor.selection.fromX )
3410
3518
  ].join('');
3411
3519
 
3412
3520
  // Go to the end of the word
3413
- this.cursorToPosition(cursor, cursor.selection.toX + 1);
3521
+ this.cursorToPosition( cursor, cursor.selection.toX + 1 );
3414
3522
 
3415
3523
  // Change next key?
3416
3524
  switch(key)
@@ -3578,7 +3686,7 @@ class CodeEditor {
3578
3686
  if( !key ) return;
3579
3687
 
3580
3688
  cursor._left += this.charWidth;
3581
- cursor.style.left = "calc( " + cursor._left + "px + " + this.xPadding + " )";
3689
+ cursor.style.left = `calc( ${ cursor._left }px + ${ this.xPadding } )`;
3582
3690
  cursor.position++;
3583
3691
 
3584
3692
  this.restartBlink();
@@ -3598,7 +3706,7 @@ class CodeEditor {
3598
3706
 
3599
3707
  cursor._left -= this.charWidth;
3600
3708
  cursor._left = Math.max( cursor._left, 0 );
3601
- cursor.style.left = "calc( " + cursor._left + "px + " + this.xPadding + " )";
3709
+ cursor.style.left = `calc( ${ cursor._left }px + ${ this.xPadding } )`;
3602
3710
  cursor.position--;
3603
3711
  cursor.position = Math.max( cursor.position, 0 );
3604
3712
  this.restartBlink();
@@ -3616,8 +3724,8 @@ class CodeEditor {
3616
3724
  cursorToTop( cursor, resetLeft = false ) {
3617
3725
 
3618
3726
  cursor._top -= this.lineHeight;
3619
- cursor._top = Math.max(cursor._top, 0);
3620
- cursor.style.top = "calc(" + cursor._top + "px)";
3727
+ cursor._top = Math.max( cursor._top, 0 );
3728
+ cursor.style.top = `calc(${ cursor._top }px)`;
3621
3729
  this.restartBlink();
3622
3730
 
3623
3731
  if( resetLeft )
@@ -3647,10 +3755,10 @@ class CodeEditor {
3647
3755
 
3648
3756
  const currentScrollTop = this.getScrollTop();
3649
3757
  const tabsHeight = this.tabs.root.getBoundingClientRect().height;
3650
- const infoPanelHeight = this.skipInfo ? 0 : this.infoPanel.root.getBoundingClientRect().height;
3758
+ const statusPanelHeight = this.skipInfo ? 0 : this.statusPanel.root.getBoundingClientRect().height;
3651
3759
  const scrollerHeight = this.codeScroller.offsetHeight;
3652
3760
 
3653
- var lastLine = ( ( scrollerHeight - tabsHeight - infoPanelHeight + currentScrollTop ) / this.lineHeight )|0;
3761
+ var lastLine = ( ( scrollerHeight - tabsHeight - statusPanelHeight + currentScrollTop ) / this.lineHeight )|0;
3654
3762
  if( cursor.line >= lastLine )
3655
3763
  {
3656
3764
  this.setScrollTop( currentScrollTop + this.lineHeight );
@@ -3660,7 +3768,9 @@ class CodeEditor {
3660
3768
  cursorToString( cursor, text, reverse ) {
3661
3769
 
3662
3770
  if( !text.length )
3771
+ {
3663
3772
  return;
3773
+ }
3664
3774
 
3665
3775
  for( let char of text )
3666
3776
  {
@@ -3672,7 +3782,7 @@ class CodeEditor {
3672
3782
 
3673
3783
  cursor.position = position;
3674
3784
  cursor._left = position * this.charWidth;
3675
- cursor.style.left = "calc(" + cursor._left + "px + " + this.xPadding + ")";
3785
+ cursor.style.left = `calc( ${ cursor._left }px + ${ this.xPadding } )`;
3676
3786
  }
3677
3787
 
3678
3788
  cursorToLine( cursor, line, resetLeft = false ) {
@@ -3709,9 +3819,9 @@ class CodeEditor {
3709
3819
  for( let cursor of this.cursors.children )
3710
3820
  {
3711
3821
  cursor._left = cursor.position * this.charWidth;
3712
- cursor.style.left = "calc(" + cursor._left + "px + " + this.xPadding + ")";
3822
+ cursor.style.left = `calc( ${ cursor._left }px + ${ this.xPadding } )`;
3713
3823
  cursor._top = cursor.line * this.lineHeight;
3714
- cursor.style.top = "calc(" + cursor._top + "px)";
3824
+ cursor.style.top = `calc(${ cursor._top }px)`;
3715
3825
  }
3716
3826
  }
3717
3827
 
@@ -3730,9 +3840,9 @@ class CodeEditor {
3730
3840
  cursor.line = state.line ?? 0;
3731
3841
 
3732
3842
  cursor._left = cursor.position * this.charWidth;
3733
- cursor.style.left = "calc(" + cursor._left + "px + " + this.xPadding + ")";
3843
+ cursor.style.left = `calc( ${ cursor._left }px + ${ this.xPadding } )`;
3734
3844
  cursor._top = cursor.line * this.lineHeight;
3735
- cursor.style.top = "calc(" + cursor._top + "px)";
3845
+ cursor.style.top = `calc(${ cursor._top }px)`;
3736
3846
 
3737
3847
  if( state.selection )
3738
3848
  {
@@ -3772,7 +3882,8 @@ class CodeEditor {
3772
3882
 
3773
3883
  _addSpaceTabs( cursor, n ) {
3774
3884
 
3775
- for( var i = 0; i < n; ++i ) {
3885
+ for( var i = 0; i < n; ++i )
3886
+ {
3776
3887
  this.actions[ 'Tab' ].callback( cursor.line, cursor, null );
3777
3888
  }
3778
3889
  }
@@ -3789,13 +3900,17 @@ class CodeEditor {
3789
3900
  }
3790
3901
 
3791
3902
  _removeSpaces( cursor ) {
3903
+
3792
3904
  const lidx = cursor.line;
3905
+
3793
3906
  // Remove indentation
3794
3907
  let lineStart = firstNonspaceIndex( this.code.lines[ lidx ] );
3795
3908
 
3796
3909
  // Nothing to remove... we are at the start of the line
3797
3910
  if( lineStart == 0 )
3911
+ {
3798
3912
  return;
3913
+ }
3799
3914
 
3800
3915
  // Only tabs/spaces in the line...
3801
3916
  if( lineStart == -1 ) {
@@ -3835,7 +3950,7 @@ class CodeEditor {
3835
3950
 
3836
3951
  setScrollLeft( value ) {
3837
3952
  if( !this.codeScroller ) return;
3838
- doAsync( () => {
3953
+ LX.doAsync( () => {
3839
3954
  this.codeScroller.scrollLeft = value;
3840
3955
  this.setScrollBarValue( 'horizontal', 0 );
3841
3956
  }, 20 );
@@ -3843,7 +3958,7 @@ class CodeEditor {
3843
3958
 
3844
3959
  setScrollTop( value ) {
3845
3960
  if( !this.codeScroller ) return;
3846
- doAsync( () => {
3961
+ LX.doAsync( () => {
3847
3962
  this.codeScroller.scrollTop = value;
3848
3963
  this.setScrollBarValue( 'vertical' );
3849
3964
  }, 20 );
@@ -3858,13 +3973,13 @@ class CodeEditor {
3858
3973
  const scrollWidth = maxLineLength * this.charWidth + CodeEditor.LINE_GUTTER_WIDTH;
3859
3974
 
3860
3975
  const tabsHeight = this.tabs.root.getBoundingClientRect().height;
3861
- const infoPanelHeight = this.skipInfo ? 0 : this.infoPanel.root.getBoundingClientRect().height;
3976
+ const statusPanelHeight = this.skipInfo ? 0 : this.statusPanel.root.getBoundingClientRect().height;
3862
3977
  const scrollHeight = this.code.lines.length * this.lineHeight;
3863
3978
 
3864
3979
  this._lastMaxLineLength = maxLineLength;
3865
3980
 
3866
3981
  this.codeSizer.style.minWidth = scrollWidth + "px";
3867
- this.codeSizer.style.minHeight = ( scrollHeight + tabsHeight + infoPanelHeight ) + "px";
3982
+ this.codeSizer.style.minHeight = ( scrollHeight + tabsHeight + statusPanelHeight ) + "px";
3868
3983
 
3869
3984
  this.resizeScrollBars();
3870
3985
 
@@ -4034,7 +4149,7 @@ class CodeEditor {
4034
4149
  return [ word, from, to ];
4035
4150
  }
4036
4151
 
4037
- _measureChar( char = "a", use_floating = false, get_bb = false ) {
4152
+ _measureChar( char = "a", useFloating = false, getBB = false ) {
4038
4153
  const parentContainer = LX.makeContainer( null, "lexcodeeditor", "", document.body );
4039
4154
  const container = LX.makeContainer( null, "code", "", parentContainer );
4040
4155
  const line = document.createElement( "pre" );
@@ -4044,8 +4159,8 @@ class CodeEditor {
4044
4159
  text.innerText = char;
4045
4160
  var rect = text.getBoundingClientRect();
4046
4161
  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 ];
4162
+ const bb = [ useFloating ? rect.width : Math.floor( rect.width ), useFloating ? rect.height : Math.floor( rect.height ) ];
4163
+ return getBB ? bb : bb[ 0 ];
4049
4164
  }
4050
4165
 
4051
4166
  measureString( str ) {
@@ -4333,7 +4448,9 @@ class CodeEditor {
4333
4448
  text = text ?? this._lastTextFound;
4334
4449
 
4335
4450
  if( !text )
4451
+ {
4336
4452
  return;
4453
+ }
4337
4454
 
4338
4455
  let cursor = this._getCurrentCursor();
4339
4456
  let cursorData = new LX.vec2( cursor.position, cursor.line );
@@ -4344,6 +4461,7 @@ class CodeEditor {
4344
4461
  {
4345
4462
  LX.deleteElement( this._lastResult.dom );
4346
4463
  cursorData = this._lastResult.pos;
4464
+ cursorData.x += text.length * ( reverse ? -1 : 1 );
4347
4465
  delete this._lastResult;
4348
4466
  }
4349
4467
 
@@ -4389,10 +4507,12 @@ class CodeEditor {
4389
4507
  }
4390
4508
  }
4391
4509
 
4392
- if( line == null)
4510
+ if( line == null )
4393
4511
  {
4394
4512
  if( !skipAlert )
4513
+ {
4395
4514
  alert( "No results!" );
4515
+ }
4396
4516
 
4397
4517
  const lastLine = this.code.lines.length - 1;
4398
4518
 
@@ -4400,6 +4520,7 @@ class CodeEditor {
4400
4520
  'dom': this.searchResultSelections.lastChild,
4401
4521
  'pos': reverse ? new LX.vec2( this.code.lines[ lastLine ].length, lastLine ) : new LX.vec2( 0, 0 )
4402
4522
  };
4523
+
4403
4524
  return;
4404
4525
  }
4405
4526
 
@@ -4409,9 +4530,10 @@ class CodeEditor {
4409
4530
  have to add the length of the substring (0, first_ocurrence)
4410
4531
  */
4411
4532
 
4412
-
4413
4533
  if( !reverse )
4534
+ {
4414
4535
  char += ( line == cursorData.y ? cursorData.x : 0 );
4536
+ }
4415
4537
 
4416
4538
 
4417
4539
  // Text found..
@@ -4439,8 +4561,13 @@ class CodeEditor {
4439
4561
 
4440
4562
  this._lastResult = {
4441
4563
  'dom': this.searchResultSelections.lastChild,
4442
- 'pos': new LX.vec2( char + text.length * ( reverse ? -1 : 1 ) , line )
4564
+ 'pos': new LX.vec2( char , line ),
4565
+ reverse
4443
4566
  };
4567
+
4568
+ // Force focus back to search box
4569
+ const input = this.searchbox.querySelector( 'input' );
4570
+ input.focus();
4444
4571
  }
4445
4572
 
4446
4573
  showSearchLineBox() {
@@ -4527,13 +4654,16 @@ class CodeEditor {
4527
4654
 
4528
4655
  number = number ?? this.state.activeLine;
4529
4656
 
4530
- this._updateDataInfoPanel( "@cursor-line", "Ln " + ( number + 1 ) );
4657
+ const cursor = this._getCurrentCursor();
4658
+ this._updateDataInfoPanel( "@cursor-data", `Ln ${ number + 1 }, Col ${ cursor.position + 1 }` );
4531
4659
 
4532
- const old_local = this.toLocalLine( this.state.activeLine );
4533
- let line = this.code.childNodes[ old_local ];
4660
+ const oldLocal = this.toLocalLine( this.state.activeLine );
4661
+ let line = this.code.childNodes[ oldLocal ];
4534
4662
 
4535
4663
  if( !line )
4664
+ {
4536
4665
  return;
4666
+ }
4537
4667
 
4538
4668
  line.classList.remove( 'active-line' );
4539
4669
 
@@ -4541,71 +4671,54 @@ class CodeEditor {
4541
4671
  {
4542
4672
  this.state.activeLine = number;
4543
4673
 
4544
- const new_local = this.toLocalLine( number );
4545
- line = this.code.childNodes[ new_local ];
4674
+ const newLocal = this.toLocalLine( number );
4675
+ line = this.code.childNodes[ newLocal ];
4546
4676
  if( line ) line.classList.add( 'active-line' );
4547
4677
  }
4548
4678
  }
4549
4679
 
4550
4680
  _hideActiveLine() {
4551
-
4552
4681
  this.code.querySelectorAll( '.active-line' ).forEach( e => e.classList.remove( 'active-line' ) );
4553
4682
  }
4554
4683
 
4555
- _increaseFontSize() {
4556
-
4684
+ _setFontSize( size ) {
4557
4685
  // 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" );
4686
+ this.fontSize = size;
4687
+ const r = document.querySelector( ':root' );
4688
+ r.style.setProperty( "--code-editor-font-size", `${ this.fontSize }px` );
4564
4689
  this.charWidth = this._measureChar( "a", true );
4565
4690
 
4566
- // Change row size
4691
+ window.localStorage.setItem( "lexcodeeditor-font-size", this.fontSize );
4567
4692
 
4568
- var row_pixels = pixels + 6;
4569
- r.style.setProperty( "--code-editor-row-height", row_pixels + "px" );
4570
- this.lineHeight = row_pixels;
4693
+ // Change row size
4694
+ const rowPixels = this.fontSize + 6;
4695
+ r.style.setProperty( "--code-editor-row-height", `${ rowPixels }px` );
4696
+ this.lineHeight = rowPixels;
4571
4697
 
4572
4698
  // Relocate cursors
4573
-
4574
4699
  this.relocateCursors();
4575
4700
 
4576
4701
  // Resize the code area
4577
-
4578
4702
  this.processLines();
4579
- }
4580
-
4581
- _decreaseFontSize() {
4582
-
4583
- // Change font size
4584
4703
 
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
-
4598
- // Relocate cursors
4704
+ // Emit event
4705
+ LX.emit( "@font-size", this.fontSize );
4706
+ }
4599
4707
 
4600
- this.relocateCursors();
4708
+ _applyFontSizeOffset( offset = 0 ) {
4709
+ const newFontSize = LX.clamp( this.fontSize + offset, CodeEditor.CODE_MIN_FONT_SIZE, CodeEditor.CODE_MAX_FONT_SIZE );
4710
+ this._setFontSize( newFontSize );
4711
+ }
4601
4712
 
4602
- // Resize the code area
4713
+ _increaseFontSize() {
4714
+ this._applyFontSizeOffset( 1 );
4715
+ }
4603
4716
 
4604
- this.processLines();
4717
+ _decreaseFontSize() {
4718
+ this._applyFontSizeOffset( -1 );
4605
4719
  }
4606
4720
 
4607
4721
  _clearTmpVariables() {
4608
-
4609
4722
  delete this._currentLineString;
4610
4723
  delete this._currentLineNumber;
4611
4724
  delete this._buildingString;
@@ -4613,13 +4726,35 @@ class CodeEditor {
4613
4726
  delete this._buildingBlockComment;
4614
4727
  delete this._markdownHeader;
4615
4728
  delete this._lastResult;
4729
+ delete this._scopeStack;
4616
4730
  }
4617
4731
  }
4618
4732
 
4619
- CodeEditor.keywords = {
4733
+ CodeEditor.languages = {
4734
+ 'Plain Text': { ext: 'txt', blockComments: false, singleLineComments: false, numbers: false },
4735
+ 'JavaScript': { ext: 'js' },
4736
+ 'TypeScript': { ext: 'ts' },
4737
+ 'C': { ext: [ 'c', 'h' ], usePreprocessor: true },
4738
+ 'C++': { ext: [ 'cpp', 'hpp' ], usePreprocessor: true },
4739
+ 'CSS': { ext: 'css' },
4740
+ 'CMake': { ext: 'cmake', singleLineCommentToken: '#', blockComments: false, ignoreCase: true },
4741
+ 'GLSL': { ext: 'glsl', usePreprocessor: true },
4742
+ 'WGSL': { ext: 'wgsl', usePreprocessor: true },
4743
+ 'JSON': { ext: 'json', blockComments: false, singleLineComments: false },
4744
+ 'XML': { ext: 'xml', tags: true },
4745
+ 'Rust': { ext: 'rs' },
4746
+ 'Python': { ext: 'py', singleLineCommentToken: '#' },
4747
+ 'HTML': { ext: 'html', tags: true, singleLineComments: false, blockCommentsTokens: [ '<!--', '-->' ], numbers: false },
4748
+ 'Batch': { ext: 'bat', blockComments: false, singleLineCommentToken: '::' },
4749
+ 'Markdown': { ext: 'md', blockComments: false, singleLineCommentToken: '::', tags: true, numbers: false },
4750
+ 'PHP': { ext: 'php' },
4751
+ };
4620
4752
 
4753
+ CodeEditor.keywords = {
4621
4754
  'JavaScript': ['var', 'let', 'const', 'this', 'in', 'of', 'true', 'false', 'new', 'function', 'NaN', 'static', 'class', 'constructor', 'null', 'typeof', 'debugger', 'abstract',
4622
4755
  'arguments', 'extends', 'instanceof', 'Infinity'],
4756
+ 'TypeScript': ['var', 'let', 'const', 'this', 'in', 'of', 'true', 'false', 'new', 'function', 'class', 'extends', 'instanceof', 'Infinity', 'private', 'public', 'protected', 'interface',
4757
+ 'enum', 'type'],
4623
4758
  'C': ['int', 'float', 'double', 'long', 'short', 'char', 'const', 'void', 'true', 'false', 'auto', 'struct', 'typedef', 'signed', 'volatile', 'unsigned', 'static', 'extern', 'enum', 'register',
4624
4759
  'union'],
4625
4760
  'C++': ['int', 'float', 'double', 'bool', 'long', 'short', 'char', 'wchar_t', 'const', 'static_cast', 'dynamic_cast', 'new', 'delete', 'void', 'true', 'false', 'auto', 'class', 'struct', 'typedef', 'nullptr',
@@ -4630,10 +4765,10 @@ CodeEditor.keywords = {
4630
4765
  'JSON': ['true', 'false'],
4631
4766
  'GLSL': ['true', 'false', 'function', 'int', 'float', 'vec2', 'vec3', 'vec4', 'mat2x2', 'mat3x3', 'mat4x4', 'struct'],
4632
4767
  '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',
4768
+ 'WGSL': ['var', 'let', 'true', 'false', 'fn', 'bool', 'u32', 'i32', 'f16', 'f32', 'vec2', 'vec3', 'vec4', 'vec2f', 'vec3f', 'vec4f', 'mat2x2f', 'mat3x3f', 'mat4x4f', 'array', 'atomic', 'struct',
4634
4769
  'sampler', 'sampler_comparison', 'texture_depth_2d', 'texture_depth_2d_array', 'texture_depth_cube', 'texture_depth_cube_array', 'texture_depth_multisampled_2d',
4635
4770
  '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'],
4771
+ 'texture_storage_2d_array', 'texture_storage_3d', 'vec2u', 'vec3u', 'vec4u', 'ptr'],
4637
4772
  'Rust': ['as', 'const', 'crate', 'enum', 'extern', 'false', 'fn', 'impl', 'in', 'let', 'mod', 'move', 'mut', 'pub', 'ref', 'self', 'Self', 'static', 'struct', 'super', 'trait', 'true',
4638
4773
  'type', 'unsafe', 'use', 'where', 'abstract', 'become', 'box', 'final', 'macro', 'override', 'priv', 'typeof', 'unsized', 'virtual'],
4639
4774
  'Python': ['False', 'def', 'None', 'True', 'in', 'is', 'and', 'lambda', 'nonlocal', 'not', 'or'],
@@ -4641,10 +4776,10 @@ CodeEditor.keywords = {
4641
4776
  'DRIVERQUERY', 'print', 'PRINT'],
4642
4777
  'HTML': ['html', 'meta', 'title', 'link', 'script', 'body', 'DOCTYPE', 'head', 'br', 'i', 'a', 'li', 'img', 'tr', 'td', 'h1', 'h2', 'h3', 'h4', 'h5'],
4643
4778
  'Markdown': ['br', 'i', 'a', 'li', 'img', 'table', 'title', 'tr', 'td', 'h1', 'h2', 'h3', 'h4', 'h5'],
4779
+ 'PHP': ['const', 'function', 'array', 'new', 'int', 'string', '$this', 'public', 'null', 'private', 'protected', 'implements', 'class', 'use', 'namespace', 'abstract', 'clone', 'final'],
4644
4780
  };
4645
4781
 
4646
4782
  CodeEditor.utils = { // These ones don't have hightlight, used as suggestions to autocomplete only...
4647
-
4648
4783
  'JavaScript': ['querySelector', 'body', 'addEventListener', 'removeEventListener', 'remove', 'sort', 'keys', 'filter', 'isNaN', 'parseFloat', 'parseInt', 'EPSILON', 'isFinite',
4649
4784
  'bind', 'prototype', 'length', 'assign', 'entries', 'values', 'concat', 'substring', 'substr', 'splice', 'slice', 'buffer', 'appendChild', 'createElement', 'prompt',
4650
4785
  'alert'],
@@ -4656,42 +4791,45 @@ CodeEditor.utils = { // These ones don't have hightlight, used as suggestions to
4656
4791
  };
4657
4792
 
4658
4793
  CodeEditor.types = {
4659
-
4660
4794
  'JavaScript': ['Object', 'String', 'Function', 'Boolean', 'Symbol', 'Error', 'Number', 'TextEncoder', 'TextDecoder', 'Array', 'ArrayBuffer', 'InputEvent', 'MouseEvent',
4661
4795
  'Int8Array', 'Int16Array', 'Int32Array', 'Float32Array', 'Float64Array', 'Element'],
4796
+ 'TypeScript': ['arguments', 'constructor', 'null', 'typeof', 'debugger', 'abstract', 'Object', 'string', 'String', 'Function', 'Boolean', 'boolean', 'Error', 'Number', 'number', 'TextEncoder',
4797
+ 'TextDecoder', 'Array', 'ArrayBuffer', 'InputEvent', 'MouseEvent', 'Int8Array', 'Int16Array', 'Int32Array', 'Float32Array', 'Float64Array', 'Element'],
4662
4798
  'Rust': ['u128'],
4663
4799
  'Python': ['int', 'type', 'float', 'map', 'list', 'ArithmeticError', 'AssertionError', 'AttributeError', 'Exception', 'EOFError', 'FloatingPointError', 'GeneratorExit',
4664
4800
  'ImportError', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'NotImplementedError', 'OSError',
4665
4801
  'OverflowError', 'ReferenceError', 'RuntimeError', 'StopIteration', 'SyntaxError', 'TabError', 'SystemError', 'SystemExit', 'TypeError', 'UnboundLocalError',
4666
4802
  'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError', 'ValueError', 'ZeroDivisionError'],
4667
- 'C++': ['uint8_t', 'uint16_t', 'uint32_t']
4803
+ 'C++': ['uint8_t', 'uint16_t', 'uint32_t'],
4804
+ 'PHP': ['Exception', 'DateTime', 'JsonSerializable'],
4668
4805
  };
4669
4806
 
4670
4807
  CodeEditor.builtIn = {
4671
-
4672
4808
  'JavaScript': ['document', 'console', 'window', 'navigator', 'performance'],
4673
4809
  'CSS': ['*', '!important'],
4674
4810
  'C++': ['vector', 'list', 'map'],
4675
4811
  'HTML': ['type', 'xmlns', 'PUBLIC', 'http-equiv', 'src', 'style', 'lang', 'href', 'rel', 'content', 'xml', 'alt'], // attributes
4676
4812
  'Markdown': ['type', 'src', 'style', 'lang', 'href', 'rel', 'content', 'valign', 'alt'], // attributes
4813
+ 'PHP': ['echo', 'print'],
4677
4814
  };
4678
4815
 
4679
4816
  CodeEditor.statementsAndDeclarations = {
4680
-
4681
4817
  'JavaScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await'],
4818
+ 'TypeScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await', 'as'],
4682
4819
  'CSS': ['@', 'import'],
4683
4820
  'C': ['for', 'if', 'else', 'return', 'continue', 'break', 'case', 'switch', 'while', 'using', 'default', 'goto', 'do'],
4684
4821
  'C++': ['std', 'for', 'if', 'else', 'return', 'continue', 'break', 'case', 'switch', 'while', 'using', 'glm', 'spdlog', 'default'],
4685
4822
  'GLSL': ['for', 'if', 'else', 'return', 'continue', 'break'],
4686
- 'WGSL': ['const','for', 'if', 'else', 'return', 'continue', 'break', 'storage', 'read', 'read_write', 'uniform', 'function', 'workgroup'],
4823
+ 'WGSL': ['const','for', 'if', 'else', 'return', 'continue', 'break', 'storage', 'read', 'read_write', 'uniform', 'function', 'workgroup', 'bitcast'],
4687
4824
  'Rust': ['break', 'else', 'continue', 'for', 'if', 'loop', 'match', 'return', 'while', 'do', 'yield'],
4688
4825
  'Python': ['if', 'raise', 'del', 'import', 'return', 'elif', 'try', 'else', 'while', 'as', 'except', 'with', 'assert', 'finally', 'yield', 'break', 'for', 'class', 'continue',
4689
4826
  'global', 'pass', 'from'],
4690
- 'Batch': ['if', 'IF', 'for', 'FOR', 'in', 'IN', 'do', 'DO', 'call', 'CALL', 'goto', 'GOTO', 'exit', 'EXIT']
4827
+ 'Batch': ['if', 'IF', 'for', 'FOR', 'in', 'IN', 'do', 'DO', 'call', 'CALL', 'goto', 'GOTO', 'exit', 'EXIT'],
4828
+ 'PHP': ['declare', 'enddeclare', 'foreach', 'endforeach', 'if', 'else', 'elseif', 'endif', 'for', 'endfor', 'while', 'endwhile', 'switch', 'case', 'default', 'endswitch', 'return', 'break', 'continue',
4829
+ 'try', 'catch', 'die', 'do', 'exit', 'finally'],
4691
4830
  };
4692
4831
 
4693
4832
  CodeEditor.symbols = {
4694
-
4695
4833
  'JavaScript': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '??'],
4696
4834
  'C': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '*', '-', '+'],
4697
4835
  'C++': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '::', '*', '-', '+'],
@@ -4704,7 +4842,22 @@ CodeEditor.symbols = {
4704
4842
  'Python': ['<', '>', '[', ']', '(', ')', '='],
4705
4843
  'Batch': ['[', ']', '(', ')', '%'],
4706
4844
  'HTML': ['<', '>', '/'],
4707
- 'XML': ['<', '>', '/']
4845
+ 'XML': ['<', '>', '/'],
4846
+ 'PHP': ['{', '}', '(', ')'],
4847
+ };
4848
+
4849
+ CodeEditor.REGISTER_LANGUAGE = function( name, options = {}, def, rules )
4850
+ {
4851
+ CodeEditor.languages[ name ] = options;
4852
+
4853
+ if( def?.keywords ) CodeEditor.keywords[ name ] = def.keywords.reduce((a, v) => ({ ...a, [v]: true}), {});
4854
+ if( def?.utils ) CodeEditor.utils[ name ] = def.utils.reduce((a, v) => ({ ...a, [v]: true}), {});
4855
+ if( def?.types ) CodeEditor.types[ name ] = def.types.reduce((a, v) => ({ ...a, [v]: true}), {});
4856
+ if( def?.builtIn ) CodeEditor.builtIn[ name ] = def.builtIn.reduce((a, v) => ({ ...a, [v]: true}), {});
4857
+ if( def?.statementsAndDeclarations ) CodeEditor.statementsAndDeclarations[ name ] = def.statementsAndDeclarations.reduce((a, v) => ({ ...a, [v]: true}), {});
4858
+ if( def?.symbols ) CodeEditor.symbols[ name ] = def.symbols.reduce((a, v) => ({ ...a, [v]: true}), {});
4859
+
4860
+ if( rules ) HighlightRules[ name ] = rules;
4708
4861
  };
4709
4862
 
4710
4863
  LX.CodeEditor = CodeEditor;