lexgui 0.1.20 → 0.1.22

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.
@@ -21,14 +21,33 @@ function swapArrayElements( array, id0, id1 ) {
21
21
  [array[id0], array[id1]] = [array[id1], array[id0]];
22
22
  };
23
23
 
24
- function sliceChar( str, idx ) {
25
- return str.substr(0, idx) + str.substr(idx + 1);
24
+ function sliceChars( str, idx, n = 1 ) {
25
+ return str.substr(0, idx) + str.substr(idx + n);
26
26
  }
27
27
 
28
28
  function firstNonspaceIndex( str ) {
29
29
  return str.search(/\S|$/);
30
30
  }
31
31
 
32
+ function indexOfFrom( str, reg, from, reverse ) {
33
+
34
+ from = from ?? 0;
35
+
36
+ if( reverse )
37
+ {
38
+ str = str.substr( 0, from );
39
+ var k = from - 1;
40
+ while( str[ k ] && str[ k ] != reg )
41
+ k--;
42
+ return str[ k ] ? k : -1;
43
+ }
44
+ else
45
+ {
46
+ str = str.substr( from );
47
+ return from + str.indexOf( reg );
48
+ }
49
+ }
50
+
32
51
  function deleteElement( el ) {
33
52
  if( el ) el.remove();
34
53
  }
@@ -44,10 +63,11 @@ function doAsync( fn, ms ) {
44
63
 
45
64
  class CodeSelection {
46
65
 
47
- constructor( editor, ix, iy ) {
66
+ constructor( editor, ix, iy, className = "lexcodeselection" ) {
48
67
 
49
68
  this.editor = editor;
50
69
  this.chars = 0;
70
+ this.className = className;
51
71
 
52
72
  this.fromX = ix;
53
73
  this.toX = ix;
@@ -88,7 +108,7 @@ class CodeSelection {
88
108
  this.fromY = this.toY = y;
89
109
 
90
110
  var domEl = document.createElement( 'div' );
91
- domEl.className = "lexcodeselection";
111
+ domEl.className = this.className;
92
112
 
93
113
  domEl._top = y * this.editor.lineHeight;
94
114
  domEl.style.top = domEl._top + "px";
@@ -181,6 +201,9 @@ class CodeEditor {
181
201
  static WORD_TYPE_METHOD = 0;
182
202
  static WORD_TYPE_CLASS = 1;
183
203
 
204
+ static CODE_MAX_FONT_SIZE = 20;
205
+ static CODE_MIN_FONT_SIZE = 11;
206
+
184
207
  /**
185
208
  * @param {*} options
186
209
  * skip_info, allow_add_scripts, name
@@ -281,7 +304,7 @@ class CodeEditor {
281
304
 
282
305
  if( !this.disableEdition )
283
306
  {
284
- this.root.addEventListener( 'keydown', this.processKey.bind( this), true );
307
+ this.root.addEventListener( 'keydown', this.processKey.bind( this) );
285
308
  this.root.addEventListener( 'focus', this.processFocus.bind( this, true ) );
286
309
  this.root.addEventListener( 'focusout', this.processFocus.bind( this, false ) );
287
310
  }
@@ -393,8 +416,16 @@ class CodeEditor {
393
416
  });
394
417
 
395
418
  this.codeScroller.addEventListener( 'wheel', e => {
396
- const dX = ( e.deltaY > 0.0 ? 10.0 : -10.0 ) * ( e.shiftKey ? 1.0 : 0.0 );
397
- if( dX != 0.0 ) this.setScrollBarValue( 'horizontal', dX );
419
+ if( e.ctrlKey )
420
+ {
421
+ e.preventDefault();
422
+ ( e.deltaY > 0.0 ? this._decreaseFontSize() : this._increaseFontSize() );
423
+ }
424
+ else
425
+ {
426
+ const dX = ( e.deltaY > 0.0 ? 10.0 : -10.0 ) * ( e.shiftKey ? 1.0 : 0.0 );
427
+ if( dX != 0.0 ) this.setScrollBarValue( 'horizontal', dX );
428
+ }
398
429
  });
399
430
  }
400
431
 
@@ -427,6 +458,51 @@ class CodeEditor {
427
458
  this.isAutoCompleteActive = false;
428
459
  }
429
460
 
461
+ // Add search box
462
+ {
463
+ var box = document.createElement( 'div' );
464
+ box.className = "searchbox";
465
+
466
+ var searchPanel = new LX.Panel();
467
+ box.appendChild( searchPanel.root );
468
+
469
+ searchPanel.sameLine( 4 );
470
+ searchPanel.addText( null, "", null, { placeholder: "Find" } );
471
+ searchPanel.addButton( null, "up", () => this.search( null, true ), { className: 'micro', icon: "fa fa-arrow-up" } );
472
+ searchPanel.addButton( null, "down", () => this.search(), { className: 'micro', icon: "fa fa-arrow-down" } );
473
+ searchPanel.addButton( null, "x", this.hideSearchBox.bind( this ), { className: 'micro', icon: "fa fa-xmark" } );
474
+
475
+ box.querySelector( 'input' ).addEventListener( 'keyup', e => {
476
+ if( e.key == 'Escape' ) this.hideSearchBox();
477
+ else if( e.key == 'Enter' ) this.search( e.target.value, !!e.shiftKey );
478
+ } );
479
+
480
+ this.searchbox = box;
481
+ this.tabs.area.attach( box );
482
+ }
483
+
484
+ // Add search LINE box
485
+ {
486
+ var box = document.createElement( 'div' );
487
+ box.className = "searchbox gotoline";
488
+
489
+ var searchPanel = new LX.Panel();
490
+ box.appendChild( searchPanel.root );
491
+
492
+ searchPanel.addText( null, "", ( value, event ) => {
493
+ input.value = ":" + value.replaceAll( ':', '' );
494
+ this.goToLine( input.value.slice( 1 ) );
495
+ }, { placeholder: "Go to line", trigger: "input" } );
496
+
497
+ let input = box.querySelector( 'input' );
498
+ input.addEventListener( 'keyup', e => {
499
+ if( e.key == 'Escape' ) this.hideSearchLineBox();
500
+ } );
501
+
502
+ this.searchlinebox = box;
503
+ this.tabs.area.attach( box );
504
+ }
505
+
430
506
  // Add code-sizer
431
507
  {
432
508
  this.codeSizer = document.createElement( 'div' );
@@ -458,8 +534,8 @@ class CodeEditor {
458
534
  this.tabSpaces = 4;
459
535
  this.maxUndoSteps = 16;
460
536
  this.lineHeight = 20;
461
- this.defaultSingleLineCommentToken = "//";
462
- this.charWidth = 7; //this._measureChar();
537
+ this.defaultSingleLineCommentToken = '//';
538
+ this.defaultBlockCommentTokens = [ '/*', '*/' ];
463
539
  this._lastTime = null;
464
540
 
465
541
  this.pairKeys = {
@@ -479,18 +555,19 @@ class CodeEditor {
479
555
  // setInterval( this.scanWordSuggestions.bind( this ), 2000 );
480
556
 
481
557
  this.languages = {
482
- 'Plain Text': { ext: 'txt' },
558
+ 'Plain Text': { ext: 'txt', blockComments: false, singleLineComments: false },
483
559
  'JavaScript': { ext: 'js' },
484
560
  'C++': { ext: 'cpp' },
485
561
  'CSS': { ext: 'css' },
486
562
  'GLSL': { ext: 'glsl' },
487
563
  'WGSL': { ext: 'wgsl' },
488
- 'JSON': { ext: 'json' },
489
- 'XML': { ext: 'xml' },
564
+ 'JSON': { ext: 'json', blockComments: false, singleLineComments: false },
565
+ 'XML': { ext: 'xml', tags: true },
490
566
  'Rust': { ext: 'rs' },
491
567
  'Python': { ext: 'py', singleLineCommentToken: '#' },
492
- 'HTML': { ext: 'html' },
493
- 'Batch': { ext: 'bat', blockComments: false, singleLineCommentToken: '::' }
568
+ 'HTML': { ext: 'html', tags: true, singleLineComments: false, blockCommentsTokens: [ '<!--', '-->' ] },
569
+ 'Batch': { ext: 'bat', blockComments: false, singleLineCommentToken: '::' },
570
+ 'Markdown': { ext: 'md', blockComments: false, singleLineCommentToken: '::', tags: true }
494
571
  };
495
572
 
496
573
  this.specialKeys = [
@@ -516,7 +593,8 @@ class CodeEditor {
516
593
  'Python': ['False', 'def', 'None', 'True', 'in', 'is', 'and', 'lambda', 'nonlocal', 'not', 'or'],
517
594
  'Batch': ['set', 'SET', 'echo', 'ECHO', 'off', 'OFF', 'del', 'DEL', 'defined', 'DEFINED', 'setlocal', 'SETLOCAL', 'enabledelayedexpansion', 'ENABLEDELAYEDEXPANSION', 'driverquery',
518
595
  'DRIVERQUERY', 'print', 'PRINT'],
519
- 'HTML': ['html', 'meta', 'title', 'link', 'script', 'body', 'DOCTYPE', 'head'],
596
+ 'HTML': ['html', 'meta', 'title', 'link', 'script', 'body', 'DOCTYPE', 'head', 'br', 'i', 'a', 'li', 'img', 'tr', 'td', 'h1', 'h2', 'h3', 'h4', 'h5'],
597
+ 'Markdown': ['br', 'i', 'a', 'li', 'img', 'table', 'title', 'tr', 'td', 'h1', 'h2', 'h3', 'h4', 'h5'],
520
598
  };
521
599
  this.utils = { // These ones don't have hightlight, used as suggestions to autocomplete only...
522
600
  'JavaScript': ['querySelector', 'body', 'addEventListener', 'removeEventListener', 'remove', 'sort', 'keys', 'filter', 'isNaN', 'parseFloat', 'parseInt', 'EPSILON', 'isFinite',
@@ -541,7 +619,8 @@ class CodeEditor {
541
619
  'JavaScript': ['document', 'console', 'window', 'navigator', 'performance'],
542
620
  'CSS': ['*', '!important'],
543
621
  'C++': ['vector', 'list', 'map'],
544
- 'HTML': ['type', 'xmlns', 'PUBLIC', 'http-equiv', 'src', 'lang', 'href', 'rel', 'content', 'xml'], // attributes
622
+ 'HTML': ['type', 'xmlns', 'PUBLIC', 'http-equiv', 'src', 'style', 'lang', 'href', 'rel', 'content', 'xml', 'alt'], // attributes
623
+ 'Markdown': ['type', 'src', 'style', 'lang', 'href', 'rel', 'content', 'valign', 'alt'], // attributes
545
624
  };
546
625
  this.statementsAndDeclarations = {
547
626
  'JavaScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await'],
@@ -579,6 +658,7 @@ class CodeEditor {
579
658
 
580
659
  this.action( 'Escape', false, ( ln, cursor, e ) => {
581
660
  this.hideAutoCompleteBox();
661
+ this.hideSearchBox();
582
662
  });
583
663
 
584
664
  this.action( 'Backspace', false, ( ln, cursor, e ) => {
@@ -595,16 +675,37 @@ class CodeEditor {
595
675
  }
596
676
  }
597
677
  else {
678
+
598
679
  var letter = this.getCharAtPos( cursor, -1 );
599
680
  if( letter ) {
600
- this.code.lines[ ln ] = sliceChar( this.code.lines[ ln ], cursor.position - 1 );
601
- this.cursorToLeft( letter );
681
+
682
+ var deleteFromPosition = cursor.position - 1;
683
+ var numCharsDeleted = 1;
684
+
685
+ // Delete full word
686
+ if( e.shiftKey )
687
+ {
688
+ const [word, from, to] = this.getWordAtPos( cursor, -1 );
689
+
690
+ if( word.length > 1 )
691
+ {
692
+ deleteFromPosition = from;
693
+ numCharsDeleted = word.length;
694
+ }
695
+ }
696
+
697
+ this.code.lines[ ln ] = sliceChars( this.code.lines[ ln ], deleteFromPosition, numCharsDeleted );
602
698
  this.processLine( ln );
699
+
700
+ this.cursorToPosition( cursor, deleteFromPosition );
701
+
603
702
  if( this.useAutoComplete )
604
703
  this.showAutoCompleteBox( 'foo', cursor );
605
704
  }
606
705
  else if( this.code.lines[ ln - 1 ] != undefined ) {
706
+
607
707
  this.lineUp();
708
+ e.cancelShift = true;
608
709
  this.actions[ 'End' ].callback( cursor.line, cursor, e );
609
710
  // Move line on top
610
711
  this.code.lines[ ln - 1 ] += this.code.lines[ ln ];
@@ -626,7 +727,7 @@ class CodeEditor {
626
727
  {
627
728
  var letter = this.getCharAtPos( cursor );
628
729
  if( letter ) {
629
- this.code.lines[ ln ] = sliceChar( this.code.lines[ ln ], cursor.position );
730
+ this.code.lines[ ln ] = sliceChars( this.code.lines[ ln ], cursor.position );
630
731
  this.processLine( ln );
631
732
  }
632
733
  else if(this.code.lines[ ln + 1 ] != undefined) {
@@ -721,7 +822,7 @@ class CodeEditor {
721
822
  return;
722
823
  }
723
824
 
724
- this._addUndoStep( cursor );
825
+ this._addUndoStep( cursor, true );
725
826
 
726
827
  var _c0 = this.getCharAtPos( cursor, -1 );
727
828
  var _c1 = this.getCharAtPos( cursor );
@@ -949,7 +1050,7 @@ class CodeEditor {
949
1050
  if( options.allow_add_scripts ?? true )
950
1051
  this.addTab("+", false, "New File");
951
1052
 
952
- this.addTab(options.name || "untitled", true, options.title);
1053
+ this.addTab( options.name || "untitled", true, options.title );
953
1054
 
954
1055
  // Create inspector panel
955
1056
  let panel = this._createPanelInfo();
@@ -967,14 +1068,15 @@ class CodeEditor {
967
1068
  );
968
1069
 
969
1070
  // Add to the document.fonts (FontFaceSet)
970
- document.fonts.add(commitMono);
1071
+ document.fonts.add( commitMono );
971
1072
 
972
1073
  // Load the font
973
1074
  commitMono.load();
974
1075
 
975
1076
  // Wait until the fonts are all loaded
976
1077
  document.fonts.ready.then(() => {
977
- console.log("commitMono loaded")
1078
+ // console.log("commitMono loaded")
1079
+ this.charWidth = this._measureChar( "a", true );
978
1080
  });
979
1081
  }
980
1082
 
@@ -1132,23 +1234,32 @@ class CodeEditor {
1132
1234
  }
1133
1235
  }
1134
1236
 
1135
- _addUndoStep( cursor ) {
1237
+ _addUndoStep( cursor, force, deleteRedo = true ) {
1136
1238
 
1137
1239
  const d = new Date();
1138
1240
  const current = d.getTime();
1139
1241
 
1140
- if( !this._lastTime ) {
1141
- this._lastTime = current;
1142
- } else {
1143
- if( ( current - this._lastTime ) > 3000 ){
1144
- this._lastTime = null;
1145
- } else {
1146
- // If time not enough, reset timer
1242
+ if( !force )
1243
+ {
1244
+ if( !this._lastTime ) {
1147
1245
  this._lastTime = current;
1148
- return;
1246
+ } else {
1247
+ if( ( current - this._lastTime ) > 3000 ){
1248
+ this._lastTime = null;
1249
+ } else {
1250
+ // If time not enough, reset timer
1251
+ this._lastTime = current;
1252
+ return;
1253
+ }
1149
1254
  }
1150
1255
  }
1151
1256
 
1257
+ if( deleteRedo )
1258
+ {
1259
+ // Remove all redo steps
1260
+ this.code.redoSteps.length = 0;
1261
+ }
1262
+
1152
1263
  var cursor = cursor ?? this.cursors.children[ 0 ];
1153
1264
 
1154
1265
  this.code.undoSteps.push( {
@@ -1159,6 +1270,18 @@ class CodeEditor {
1159
1270
  } );
1160
1271
  }
1161
1272
 
1273
+ _addRedoStep( cursor ) {
1274
+
1275
+ var cursor = cursor ?? this.cursors.children[ 0 ];
1276
+
1277
+ this.code.redoSteps.push( {
1278
+ lines: LX.deepCopy( this.code.lines ),
1279
+ cursor: this.saveCursor( cursor ),
1280
+ line: cursor.line,
1281
+ position: cursor.position
1282
+ } );
1283
+ }
1284
+
1162
1285
  _changeLanguage( lang ) {
1163
1286
 
1164
1287
  this.code.language = lang;
@@ -1316,6 +1439,7 @@ class CodeEditor {
1316
1439
  code.language = "Plain Text";
1317
1440
  code.cursorState = {};
1318
1441
  code.undoSteps = [];
1442
+ code.redoSteps = [];
1319
1443
  code.tabName = name;
1320
1444
  code.title = title ?? name;
1321
1445
  code.tokens = {};
@@ -1486,6 +1610,7 @@ class CodeEditor {
1486
1610
  this.lastMouseDown = LX.getTime();
1487
1611
  this.state.selectingText = true;
1488
1612
  this.endSelection();
1613
+ this.processClick( e );
1489
1614
  }
1490
1615
 
1491
1616
  else if( e.type == 'mouseup' )
@@ -1549,9 +1674,8 @@ class CodeEditor {
1549
1674
 
1550
1675
  _onMouseUp( e ) {
1551
1676
 
1552
- if( (LX.getTime() - this.lastMouseDown) < 300 ) {
1677
+ if( (LX.getTime() - this.lastMouseDown) < 120 ) {
1553
1678
  this.state.selectingText = false;
1554
- this.processClick( e );
1555
1679
  this.endSelection();
1556
1680
  }
1557
1681
 
@@ -1741,7 +1865,7 @@ class CodeEditor {
1741
1865
 
1742
1866
  async processKey( e ) {
1743
1867
 
1744
- if( !this.code )
1868
+ if( !this.code || e.srcElement.constructor != HTMLDivElement )
1745
1869
  return;
1746
1870
 
1747
1871
  var key = e.key ?? e.detail.key;
@@ -1778,11 +1902,20 @@ class CodeEditor {
1778
1902
  return;
1779
1903
  case 'd': // duplicate line
1780
1904
  e.preventDefault();
1905
+ this.endSelection();
1781
1906
  this.code.lines.splice( lidx, 0, this.code.lines[ lidx ] );
1782
1907
  this.lineDown( cursor );
1783
1908
  this.processLines();
1784
1909
  this.hideAutoCompleteBox();
1785
1910
  return;
1911
+ case 'f': // find/search
1912
+ e.preventDefault();
1913
+ this.showSearchBox();
1914
+ return;
1915
+ case 'g': // find line
1916
+ e.preventDefault();
1917
+ this.showSearchLineBox();
1918
+ return;
1786
1919
  case 's': // save
1787
1920
  e.preventDefault();
1788
1921
  this.onsave( this.getText() );
@@ -1794,13 +1927,31 @@ class CodeEditor {
1794
1927
  this._cutContent();
1795
1928
  this.hideAutoCompleteBox();
1796
1929
  return;
1930
+ case 'y': // redo
1931
+ if(!this.code.redoSteps.length)
1932
+ return;
1933
+ this._addUndoStep( cursor, true, false);
1934
+ const redo_step = this.code.redoSteps.pop();
1935
+ this.code.lines = redo_step.lines;
1936
+ this.processLines();
1937
+ this.restoreCursor( cursor, redo_step.cursor );
1938
+ return;
1797
1939
  case 'z': // undo
1798
1940
  if(!this.code.undoSteps.length)
1799
1941
  return;
1800
- const step = this.code.undoSteps.pop();
1801
- this.code.lines = step.lines;
1942
+ this._addRedoStep( cursor );
1943
+ const undo_step = this.code.undoSteps.pop();
1944
+ this.code.lines = undo_step.lines;
1802
1945
  this.processLines();
1803
- this.restoreCursor( cursor, step.cursor );
1946
+ this.restoreCursor( cursor, undo_step.cursor );
1947
+ return;
1948
+ case '+': // increase size
1949
+ e.preventDefault();
1950
+ this._increaseFontSize();
1951
+ return;
1952
+ case '-': // decrease size
1953
+ e.preventDefault();
1954
+ this._decreaseFontSize();
1804
1955
  return;
1805
1956
  }
1806
1957
  }
@@ -1964,7 +2115,7 @@ class CodeEditor {
1964
2115
  let lidx = cursor.line;
1965
2116
  let text_to_cut = "";
1966
2117
 
1967
- this._addUndoStep( cursor );
2118
+ this._addUndoStep( cursor, true );
1968
2119
 
1969
2120
  if( !this.selection ) {
1970
2121
  text_to_cut = "\n" + this.code.lines[ cursor.line ];
@@ -2080,6 +2231,7 @@ class CodeEditor {
2080
2231
 
2081
2232
  processLine( linenum, force ) {
2082
2233
 
2234
+ const lang = this.languages[ this.highlight ];
2083
2235
  const local_line_num = this.toLocalLine( linenum );
2084
2236
  const gutter_line = "<span class='line-gutter'>" + (linenum + 1) + "</span>";
2085
2237
 
@@ -2096,6 +2248,7 @@ class CodeEditor {
2096
2248
  // multi-line strings not supported by now
2097
2249
  delete this._buildingString;
2098
2250
  delete this._pendingString;
2251
+ delete this._markdownHeader;
2099
2252
 
2100
2253
  let linestring = this.code.lines[ linenum ];
2101
2254
 
@@ -2138,44 +2291,36 @@ class CodeEditor {
2138
2291
 
2139
2292
  const token = tokensToEvaluate[ i ];
2140
2293
 
2141
- if( this.languages[ this.highlight ].blockComments ?? true )
2294
+ if( lang.blockComments ?? true )
2142
2295
  {
2143
- if( token.substr( 0, 2 ) == '/*' )
2296
+ const blockCommentsToken = ( lang.blockCommentsTokens ?? this.defaultBlockCommentTokens )[ 0 ];
2297
+ if( token.substr( 0, blockCommentsToken.length ) == blockCommentsToken )
2144
2298
  this._buildingBlockComment = true;
2145
2299
  }
2146
2300
 
2147
- line_inner_html += this._evaluateToken( token, prev, next, (i == tokensToEvaluate.length - 1) );
2301
+ line_inner_html += this._evaluateToken( {
2302
+ token: token,
2303
+ prev: prev,
2304
+ prevWithSpaces: tokensToEvaluate[ i - 1 ],
2305
+ next: next,
2306
+ nextWithSpaces: tokensToEvaluate[ i + 1 ],
2307
+ tokenIndex: i,
2308
+ isFirstToken: (i == 0),
2309
+ isLastToken: (i == tokensToEvaluate.length - 1)
2310
+ } );
2148
2311
  }
2149
2312
 
2150
2313
  return UPDATE_LINE( line_inner_html );
2151
2314
  }
2152
2315
 
2153
- _processTokens( tokens, offset = 0 ) {
2154
-
2155
- if( this.highlight == 'C++' || this.highlight == 'CSS' )
2156
- {
2157
- var idx = tokens.slice( offset ).findIndex( ( value, index ) => this.isNumber( value ) );
2158
- if( idx > -1 )
2159
- {
2160
- idx += offset; // Add offset to compute within the whole array of tokens
2161
- let data = tokens[ idx ] + tokens[ ++idx ];
2162
- while( this.isNumber( data ) )
2163
- {
2164
- tokens[ idx - 1 ] += tokens[ idx ];
2165
- tokens.splice( idx, 1 );
2166
- data += tokens[ idx ];
2167
- }
2168
- // Scan for numbers again
2169
- return this._processTokens( tokens, idx );
2170
- }
2171
- }
2316
+ _lineHasComment( linestring ) {
2172
2317
 
2173
- return tokens;
2174
- }
2318
+ const lang = this.languages[ this.highlight ];
2175
2319
 
2176
- _lineHasComment( linestring ) {
2320
+ if( !(lang.singleLineComments ?? true) )
2321
+ return;
2177
2322
 
2178
- const singleLineCommentToken = this.languages[ this.highlight ].singleLineCommentToken ?? this.defaultSingleLineCommentToken;
2323
+ const singleLineCommentToken = lang.singleLineCommentToken ?? this.defaultSingleLineCommentToken;
2179
2324
  const idx = linestring.indexOf( singleLineCommentToken );
2180
2325
 
2181
2326
  if( idx > -1 )
@@ -2199,6 +2344,8 @@ class CodeEditor {
2199
2344
 
2200
2345
  _getTokensFromLine( linestring, skipNonWords ) {
2201
2346
 
2347
+ this._currentLineString = linestring;
2348
+
2202
2349
  // Check if line comment
2203
2350
  const ogLine = linestring;
2204
2351
  const hasCommentIdx = this._lineHasComment( linestring );
@@ -2209,14 +2356,19 @@ class CodeEditor {
2209
2356
  }
2210
2357
 
2211
2358
  let tokensToEvaluate = []; // store in a temp array so we know prev and next tokens...
2359
+ let charCounterList = [];
2360
+ let charCounter = 0;
2212
2361
 
2213
2362
  const pushToken = function( t ) {
2214
2363
  if( (skipNonWords && ( t.includes('"') || t.length < 3 )) )
2215
2364
  return;
2216
2365
  tokensToEvaluate.push( t );
2366
+ charCounterList.push( charCounter );
2367
+ // Update positions
2368
+ charCounter += t.length;
2217
2369
  };
2218
2370
 
2219
- let iter = linestring.matchAll(/(\*\/|\/\*|::|[\[\](){}<>.,;:*"'%@!/= ])/g);
2371
+ let iter = linestring.matchAll(/(<!--|-->|\*\/|\/\*|::|[\[\](){}<>.,;:*"'%@!/= ])/g);
2220
2372
  let subtokens = iter.next();
2221
2373
  if( subtokens.value )
2222
2374
  {
@@ -2234,47 +2386,103 @@ class CodeEditor {
2234
2386
  }
2235
2387
  }
2236
2388
  }
2237
- else tokensToEvaluate.push( linestring );
2389
+ else pushToken( linestring );
2238
2390
 
2239
2391
  if( hasCommentIdx != undefined )
2240
2392
  {
2241
2393
  pushToken( ogLine.substring( hasCommentIdx ) );
2242
2394
  }
2243
2395
 
2396
+ this._currentTokenPositions = charCounterList;
2397
+
2244
2398
  return this._processTokens( tokensToEvaluate );
2245
2399
  }
2246
2400
 
2401
+ _processTokens( tokens, offset = 0 ) {
2402
+
2403
+ if( this.highlight == 'C++' || this.highlight == 'CSS' )
2404
+ {
2405
+ var idx = tokens.slice( offset ).findIndex( ( value, index ) => this.isNumber( value ) );
2406
+ if( idx > -1 )
2407
+ {
2408
+ idx += offset; // Add offset to compute within the whole array of tokens
2409
+ let data = tokens[ idx ] + tokens[ ++idx ];
2410
+ while( this.isNumber( data ) )
2411
+ {
2412
+ tokens[ idx - 1 ] += tokens[ idx ];
2413
+ tokens.splice( idx, 1 );
2414
+ data += tokens[ idx ];
2415
+ }
2416
+ // Scan for numbers again
2417
+ return this._processTokens( tokens, idx );
2418
+ }
2419
+ }
2420
+
2421
+ return tokens;
2422
+ }
2423
+
2247
2424
  _mustHightlightWord( token, kindArray ) {
2248
2425
 
2249
2426
  return kindArray[this.highlight] && kindArray[this.highlight][token] != undefined;
2250
2427
  }
2251
2428
 
2252
- _evaluateToken( token, prev, next, isLastToken ) {
2429
+ _evaluateToken( ctxData ) {
2430
+
2431
+ let token = ctxData.token,
2432
+ prev = ctxData.prev,
2433
+ next = ctxData.next,
2434
+ tokenIndex = ctxData.tokenIndex,
2435
+ isFirstToken = ctxData.isFirstToken,
2436
+ isLastToken = ctxData.isLastToken;
2253
2437
 
2254
- const highlight = this.highlight.replace(/\s/g, '').replaceAll("+", "p").toLowerCase();
2255
- const customStringKeys = Object.assign( {}, this.stringKeys );
2438
+ const lang = this.languages[ this.highlight ],
2439
+ highlight = this.highlight.replace( /\s/g, '' ).replaceAll( "+", "p" ).toLowerCase(),
2440
+ customStringKeys = Object.assign( {}, this.stringKeys );
2256
2441
 
2257
- if( highlight == 'cpp' && prev && prev.includes('#') ) // preprocessor code..
2442
+ var usePreviousTokenToCheckString = false;
2443
+
2444
+ if( highlight == 'cpp' && prev && prev.includes( '#' ) ) // preprocessor code..
2258
2445
  {
2259
2446
  customStringKeys['@<'] = '>';
2447
+ }
2448
+ else if( highlight == 'markdown' && ( ctxData.prevWithSpaces == '[' || ctxData.nextWithSpaces == ']' ) )
2449
+ {
2450
+ // console.warn(prev, token, next)
2451
+ usePreviousTokenToCheckString = true;
2452
+ customStringKeys['@['] = ']';
2260
2453
  }
2261
2454
 
2262
2455
  // Manage strings
2263
2456
  this._stringEnded = false;
2264
- if( this._buildingString != undefined )
2457
+
2458
+ if( usePreviousTokenToCheckString || ( !this._buildingBlockComment && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) ) )
2265
2459
  {
2266
- const idx = Object.values(customStringKeys).indexOf( token );
2267
- this._stringEnded = (idx > -1) && (idx == Object.values(customStringKeys).indexOf( customStringKeys[ '@' + this._buildingString ] ));
2268
- }
2269
- else if( customStringKeys[ '@' + token ] )
2270
- {
2271
- // Start new string
2272
- this._buildingString = token;
2273
- }
2460
+ const checkIfStringEnded = t => {
2461
+ const idx = Object.values( customStringKeys ).indexOf( t );
2462
+ this._stringEnded = (idx > -1) && (idx == Object.values(customStringKeys).indexOf( customStringKeys[ '@' + this._buildingString ] ));
2463
+ };
2464
+
2465
+ if( this._buildingString != undefined )
2466
+ {
2467
+ checkIfStringEnded( usePreviousTokenToCheckString ? ctxData.nextWithSpaces : token );
2468
+ }
2469
+ else if( customStringKeys[ '@' + ( usePreviousTokenToCheckString ? ctxData.prevWithSpaces : token ) ] )
2470
+ {
2471
+ // Start new string
2472
+ this._buildingString = ( usePreviousTokenToCheckString ? ctxData.prevWithSpaces : token );
2274
2473
 
2275
- const usesBlockComments = this.languages[ this.highlight ].blockComments ?? true;
2474
+ // Check if string ended in same token using next...
2475
+ if( usePreviousTokenToCheckString )
2476
+ {
2477
+ checkIfStringEnded( ctxData.nextWithSpaces );
2478
+ }
2479
+ }
2480
+ }
2481
+
2482
+ const usesBlockComments = lang.blockComments ?? true;
2483
+ const blockCommentsTokens = lang.blockCommentsTokens ?? this.defaultBlockCommentTokens;
2276
2484
 
2277
- if(token == ' ')
2485
+ if( !usePreviousTokenToCheckString && token == ' ' )
2278
2486
  {
2279
2487
  if( this._buildingString != undefined )
2280
2488
  {
@@ -2285,7 +2493,7 @@ class CodeEditor {
2285
2493
  }
2286
2494
  else
2287
2495
  {
2288
- const singleLineCommentToken = this.languages[ this.highlight ].singleLineCommentToken ?? this.defaultSingleLineCommentToken;
2496
+ const singleLineCommentToken = lang.singleLineCommentToken ?? this.defaultSingleLineCommentToken;
2289
2497
 
2290
2498
  let token_classname = "";
2291
2499
  let discardToken = false;
@@ -2296,10 +2504,10 @@ class CodeEditor {
2296
2504
  else if( this._buildingString != undefined )
2297
2505
  discardToken = this._appendStringToken( token );
2298
2506
 
2299
- else if( this._mustHightlightWord( token, this.keywords ) )
2507
+ else if( this._mustHightlightWord( token, this.keywords ) && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) )
2300
2508
  token_classname = "cm-kwd";
2301
2509
 
2302
- else if( this._mustHightlightWord( token, this.builtin ) )
2510
+ else if( this._mustHightlightWord( token, this.builtin ) && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) )
2303
2511
  token_classname = "cm-bln";
2304
2512
 
2305
2513
  else if( this._mustHightlightWord( token, this.statementsAndDeclarations ) )
@@ -2337,12 +2545,19 @@ class CodeEditor {
2337
2545
 
2338
2546
  else if ( highlight == 'css' && prev == undefined && next == ':' ) // CSS attribute
2339
2547
  token_classname = "cm-typ";
2548
+
2549
+ else if ( this._markdownHeader || ( highlight == 'markdown' && isFirstToken && token.replaceAll('#', '').length != token.length ) ) // Header
2550
+ {
2551
+ token_classname = "cm-kwd";
2552
+ this._markdownHeader = true;
2553
+ }
2340
2554
 
2341
2555
  else if ( token[ 0 ] != '@' && token[ 0 ] != ',' && next == '(' )
2342
2556
  token_classname = "cm-mtd";
2343
2557
 
2344
2558
 
2345
- if( usesBlockComments && this._buildingBlockComment && token.substr( 0, 2 ) == '*/' )
2559
+ if( usesBlockComments && this._buildingBlockComment
2560
+ && token.substr( 0, blockCommentsTokens[ 1 ].length ) == blockCommentsTokens[ 1 ] )
2346
2561
  {
2347
2562
  delete this._buildingBlockComment;
2348
2563
  }
@@ -2389,6 +2604,19 @@ class CodeEditor {
2389
2604
  return chars;
2390
2605
  }
2391
2606
 
2607
+ _enclosedByTokens( token, tokenIndex, tagStart, tagEnd ) {
2608
+
2609
+ const tokenStartIndex = this._currentTokenPositions[ tokenIndex ];
2610
+ const tagStartIndex = indexOfFrom(this._currentLineString, tagStart, tokenStartIndex, true );
2611
+ if( tagStartIndex < 0 ) // Not found..
2612
+ return;
2613
+ const tagEndIndex = indexOfFrom(this._currentLineString, tagEnd, tokenStartIndex );
2614
+ if( tagEndIndex < 0 ) // Not found..
2615
+ return;
2616
+
2617
+ return ( tagStartIndex < tokenStartIndex ) && ( tagEndIndex >= ( tokenStartIndex + token.length ) );
2618
+ }
2619
+
2392
2620
  _isCSSClass( token, prev, next ) {
2393
2621
  return this.highlight == 'CSS' && prev == '.';
2394
2622
  }
@@ -2690,9 +2918,9 @@ class CodeEditor {
2690
2918
  cursor.position = state.position ?? 0;
2691
2919
 
2692
2920
  cursor._left = state.left ?? 0;
2693
- cursor.style.left = "calc(" + ( cursor._left - this.getScrollLeft() ) + "px + " + this.xPadding + ")";
2921
+ cursor.style.left = "calc(" + cursor._left + "px + " + this.xPadding + ")";
2694
2922
  cursor._top = state.top ?? 0;
2695
- cursor.style.top = "calc(" + ( cursor._top - this.getScrollTop() ) + "px)";
2923
+ cursor.style.top = "calc(" + cursor._top + "px)";
2696
2924
  }
2697
2925
 
2698
2926
  resetCursorPos( flag, cursor ) {
@@ -2773,8 +3001,6 @@ class CodeEditor {
2773
3001
 
2774
3002
  this.resizeScrollBars();
2775
3003
 
2776
- // console.warn("Resize editor viewport");
2777
-
2778
3004
  }, 10 );
2779
3005
  }
2780
3006
 
@@ -2939,15 +3165,16 @@ class CodeEditor {
2939
3165
  return [ word, from, to ];
2940
3166
  }
2941
3167
 
2942
- _measureChar( char = "a", get_bb = false ) {
3168
+ _measureChar( char = "a", use_floating = false, get_bb = false ) {
2943
3169
 
2944
- var test = document.createElement( "pre" );
2945
- test.className = "codechar";
2946
- test.innerHTML = char;
2947
- document.body.appendChild( test );
2948
- var rect = test.getBoundingClientRect();
2949
- deleteElement( test );
2950
- const bb = [ Math.floor( rect.width ), Math.floor( rect.height ) ];
3170
+ var line = document.createElement( "pre" );
3171
+ var text = document.createElement( "span" );
3172
+ line.appendChild( text );
3173
+ text.innerText = char;
3174
+ this.code.appendChild( line );
3175
+ var rect = text.getBoundingClientRect();
3176
+ deleteElement( line );
3177
+ const bb = [ use_floating ? rect.width : Math.floor( rect.width ), use_floating ? rect.height : Math.floor( rect.height ) ];
2951
3178
  return get_bb ? bb : bb[ 0 ];
2952
3179
  }
2953
3180
 
@@ -3030,15 +3257,15 @@ class CodeEditor {
3030
3257
  suggestions = suggestions.filter( (value, index) => value.length > 2 && suggestions.indexOf(value) === index );
3031
3258
 
3032
3259
  // Order...
3033
- suggestions = suggestions.sort( (a, b) => a.localeCompare(b) );
3260
+ suggestions = suggestions.sort( ( a, b ) => a.localeCompare( b ) );
3034
3261
 
3035
3262
  for( let s of suggestions )
3036
3263
  {
3037
- if( !s.toLowerCase().includes(word.toLowerCase()) )
3264
+ if( !s.toLowerCase().includes( word.toLowerCase() ) )
3038
3265
  continue;
3039
3266
 
3040
3267
  var pre = document.createElement( 'pre' );
3041
- this.autocomplete.appendChild(pre);
3268
+ this.autocomplete.appendChild( pre );
3042
3269
 
3043
3270
  var icon = document.createElement( 'a' );
3044
3271
 
@@ -3049,27 +3276,27 @@ class CodeEditor {
3049
3276
  else
3050
3277
  icon.className = "fa fa-font";
3051
3278
 
3052
- pre.appendChild(icon);
3279
+ pre.appendChild( icon );
3053
3280
 
3054
3281
  pre.addEventListener( 'click', () => {
3055
3282
  this.autoCompleteWord( cursor, s );
3056
3283
  } );
3057
3284
 
3058
3285
  // Highlight the written part
3059
- const index = s.toLowerCase().indexOf(word.toLowerCase());
3286
+ const index = s.toLowerCase().indexOf( word.toLowerCase() );
3060
3287
 
3061
- var preWord = document.createElement('span');
3062
- preWord.innerHTML = s.substring(0, index);
3063
- pre.appendChild(preWord);
3288
+ var preWord = document.createElement( 'span' );
3289
+ preWord.innerHTML = s.substring( 0, index );
3290
+ pre.appendChild( preWord );
3064
3291
 
3065
3292
  var actualWord = document.createElement('span');
3066
- actualWord.innerHTML = s.substr(index, word.length);
3293
+ actualWord.innerHTML = s.substr( index, word.length );
3067
3294
  actualWord.classList.add( 'word-highlight' );
3068
- pre.appendChild(actualWord);
3295
+ pre.appendChild( actualWord );
3069
3296
 
3070
3297
  var postWord = document.createElement('span');
3071
- postWord.innerHTML = s.substring(index + word.length);
3072
- pre.appendChild(postWord);
3298
+ postWord.innerHTML = s.substring( index + word.length );
3299
+ pre.appendChild( postWord );
3073
3300
  }
3074
3301
 
3075
3302
  if( !this.autocomplete.childElementCount )
@@ -3156,6 +3383,146 @@ class CodeEditor {
3156
3383
  this.autocomplete.childNodes[ idx + offset ].classList.add('selected');
3157
3384
  }
3158
3385
 
3386
+ showSearchBox( clear ) {
3387
+
3388
+ this.searchbox.classList.add( 'opened' );
3389
+ this.searchboxActive = true;
3390
+
3391
+ const input = this.searchbox.querySelector( 'input' );
3392
+
3393
+ if( clear )
3394
+ {
3395
+ input.value = "";
3396
+ }
3397
+
3398
+ input.focus();
3399
+ }
3400
+
3401
+ hideSearchBox() {
3402
+
3403
+ if( this.searchboxActive )
3404
+ {
3405
+ this.searchbox.classList.remove( 'opened' );
3406
+ this.searchboxActive = false;
3407
+ }
3408
+
3409
+ else if( this._lastResult )
3410
+ {
3411
+ this._lastResult.dom.remove();
3412
+ delete this._lastResult;
3413
+ }
3414
+ }
3415
+
3416
+ search( text, reverse ) {
3417
+
3418
+ text = text ?? this._lastTextFound;
3419
+
3420
+ if( !text )
3421
+ return;
3422
+
3423
+ let cursorData = new LX.vec2( this.position, this.line );
3424
+ let line = null;
3425
+ let char = -1;
3426
+
3427
+ if( this._lastResult )
3428
+ {
3429
+ this._lastResult.dom.remove();
3430
+ cursorData = this._lastResult.pos;
3431
+ delete this._lastResult;
3432
+ }
3433
+
3434
+ const getIndex = l => {
3435
+ return this.code.lines[ l ].substr( l == cursorData.y ? cursorData.x : 0 ).indexOf( text );
3436
+ };
3437
+
3438
+ if( reverse )
3439
+ {
3440
+ for( var j = cursorData.y; j >= 0; --j )
3441
+ {
3442
+ char = getIndex( j );
3443
+ if( char > -1 )
3444
+ {
3445
+ line = j;
3446
+ break;
3447
+ }
3448
+ }
3449
+ }
3450
+ else
3451
+ {
3452
+ for( var j = cursorData.y; j < this.code.lines.length; ++j )
3453
+ {
3454
+ char = getIndex( j );
3455
+ if( char > -1 )
3456
+ {
3457
+ line = j;
3458
+ break;
3459
+ }
3460
+ }
3461
+ }
3462
+
3463
+ if( line == null)
3464
+ {
3465
+ alert("No results!")
3466
+ return;
3467
+ }
3468
+
3469
+ /*
3470
+ Position idx is computed from last pos, which could be in same line,
3471
+ so we search in the substring (first_ocurrence, end). That's why we
3472
+ have to add the length of the substring (0, first_ocurrence)
3473
+ */
3474
+
3475
+ char += ( line == cursorData.y ? cursorData.x : 0 );
3476
+
3477
+ // Text found..
3478
+
3479
+ this._lastTextFound = text;
3480
+
3481
+ this.codeScroller.scrollTo(
3482
+ Math.max( char * this.charWidth - this.codeScroller.clientWidth ),
3483
+ Math.max( line - 10 ) * this.lineHeight
3484
+ );
3485
+
3486
+ // Show elements
3487
+ this.selections.classList.add( 'show' );
3488
+
3489
+ // Create new selection instance
3490
+ this.selection = new CodeSelection( this, 0, 0, "lexcodesearchresult" );
3491
+ this.selection.selectInline( char, line, this.measureString( text ) );
3492
+ this._lastResult = {
3493
+ 'dom': this.selections.lastChild,
3494
+ 'pos': new LX.vec2( char + text.length, line )
3495
+ };
3496
+
3497
+ }
3498
+
3499
+ showSearchLineBox() {
3500
+
3501
+ this.searchlinebox.classList.add( 'opened' );
3502
+ this.searchlineboxActive = true;
3503
+
3504
+ const input = this.searchlinebox.querySelector( 'input' );
3505
+ input.value = ":";
3506
+ input.focus();
3507
+ }
3508
+
3509
+ hideSearchLineBox() {
3510
+
3511
+ if( this.searchlineboxActive )
3512
+ {
3513
+ this.searchlinebox.classList.remove( 'opened' );
3514
+ this.searchlineboxActive = false;
3515
+ }
3516
+ }
3517
+
3518
+ goToLine( line ) {
3519
+
3520
+ if( !this.isNumber( line ) )
3521
+ return;
3522
+
3523
+ this.codeScroller.scrollTo( 0, Math.max( line - 15 ) * this.lineHeight );
3524
+ }
3525
+
3159
3526
  _updateDataInfoPanel( signal, value ) {
3160
3527
 
3161
3528
  if( !this.skipCodeInfo )
@@ -3188,11 +3555,53 @@ class CodeEditor {
3188
3555
  }
3189
3556
  }
3190
3557
 
3558
+ _increaseFontSize() {
3559
+
3560
+ // Change font size
3561
+
3562
+ var r = document.querySelector( ':root' );
3563
+ var s = getComputedStyle( r );
3564
+ var pixels = parseInt( s.getPropertyValue( "--code-editor-font-size" ) );
3565
+ pixels = LX.UTILS.clamp( pixels + 1, CodeEditor.CODE_MIN_FONT_SIZE, CodeEditor.CODE_MAX_FONT_SIZE );
3566
+ r.style.setProperty( "--code-editor-font-size", pixels + "px" );
3567
+ this.charWidth = this._measureChar( "a", true );
3568
+
3569
+ // Change row size
3570
+
3571
+ var row_pixels = pixels + 6;
3572
+ r.style.setProperty( "--code-editor-row-height", row_pixels + "px" );
3573
+ this.lineHeight = row_pixels;
3574
+
3575
+ this.processLines(); // ... it's necessary?
3576
+ }
3577
+
3578
+ _decreaseFontSize() {
3579
+
3580
+ // Change font size
3581
+
3582
+ var r = document.querySelector( ':root' );
3583
+ var s = getComputedStyle( r );
3584
+ var pixels = parseInt( s.getPropertyValue( "--code-editor-font-size" ) );
3585
+ pixels = LX.UTILS.clamp( pixels - 1, CodeEditor.CODE_MIN_FONT_SIZE, CodeEditor.CODE_MAX_FONT_SIZE );
3586
+ r.style.setProperty( "--code-editor-font-size", pixels + "px" );
3587
+ this.charWidth = this._measureChar( "a", true );
3588
+
3589
+ // Change row size
3590
+
3591
+ var row_pixels = pixels + 6;
3592
+ r.style.setProperty( "--code-editor-row-height", row_pixels + "px" );
3593
+ this.lineHeight = row_pixels;
3594
+
3595
+ this.processLines(); // ... it's necessary?
3596
+ }
3597
+
3191
3598
  _clearTmpVariables() {
3192
3599
 
3600
+ delete this._currentLineString;
3193
3601
  delete this._buildingString;
3194
3602
  delete this._pendingString;
3195
3603
  delete this._buildingBlockComment;
3604
+ delete this._markdownHeader;
3196
3605
  }
3197
3606
  }
3198
3607