lexgui 0.1.17 → 0.1.19

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.
@@ -170,11 +170,74 @@ class CodeEditor {
170
170
  window.editor = this;
171
171
 
172
172
  CodeEditor.__instances.push( this );
173
+
174
+ // File explorer
175
+ if( options.file_explorer ?? false )
176
+ {
177
+ var [explorerArea, codeArea] = area.split({ sizes:["15%","85%"] });
178
+ explorerArea.setLimitBox( 180, 20, 512 );
179
+ this.explorerArea = explorerArea;
180
+
181
+ let panel = new LX.Panel();
182
+
183
+ panel.addTitle( "EXPLORER" );
184
+
185
+ let sceneData = {
186
+ 'id': 'WORKSPACE',
187
+ 'skipVisibility': true,
188
+ 'children': []
189
+ };
190
+
191
+ this.explorer = panel.addTree( null, sceneData, {
192
+ filter: false,
193
+ rename: false,
194
+ skip_default_icon: true,
195
+ onevent: (event) => {
196
+ switch(event.type) {
197
+ // case LX.TreeEvent.NODE_SELECTED:
198
+ // if( !this.tabs.tabDOMs[ event.node.id ] ) break;
199
+ case LX.TreeEvent.NODE_DBLCLICKED:
200
+ this.loadTab( event.node.id );
201
+ break;
202
+ case LX.TreeEvent.NODE_DELETED:
203
+ this.tabs.delete( event.node.id );
204
+ delete this.loadedTabs[ event.node.id ];
205
+ break;
206
+ // case LX.TreeEvent.NODE_CONTEXTMENU:
207
+ // LX.addContextMenu( event.multiple ? "Selected Nodes" : event.node.id, event.value, m => {
208
+ //
209
+ // });
210
+ // break;
211
+ // case LX.TreeEvent.NODE_DRAGGED:
212
+ // console.log(event.node.id + " is now child of " + event.value.id);
213
+ // break;
214
+ }
215
+ }
216
+ });
217
+
218
+ this.addExplorerItem = function( item )
219
+ {
220
+ if( !this.explorer.data.children.find( (value, index) => value.id === item.id ) )
221
+ this.explorer.data.children.push( item );
222
+ };
223
+
224
+ explorerArea.attach( panel );
225
+
226
+ // Update area
227
+ area = codeArea;
228
+ }
173
229
 
174
230
  this.base_area = area;
175
231
  this.area = new LX.Area( { className: "lexcodeeditor", height: "auto", no_append: true } );
176
232
 
177
- this.tabs = this.area.addTabs( { onclose: (name) => delete this.openedTabs[ name ] } );
233
+ this.tabs = this.area.addTabs( { onclose: (name) => {
234
+ delete this.openedTabs[ name ];
235
+ if( Object.keys( this.openedTabs ).length < 2 )
236
+ {
237
+ clearInterval( this.blinker );
238
+ this.cursors.classList.remove('show');
239
+ }
240
+ } } );
178
241
  this.tabs.root.addEventListener( 'dblclick', (e) => {
179
242
  if( options.allow_add_scripts ?? true ) {
180
243
  e.preventDefault();
@@ -245,12 +308,12 @@ class CodeEditor {
245
308
  // Scroll stuff
246
309
  {
247
310
  this.codeScroller = this.tabs.area.root;
248
- this.viewportRangeStart = 0;
311
+ this.firstLineInViewport = 0;
249
312
  this.lineScrollMargin = new LX.vec2( 20, 20 ); // [ mUp, mDown ]
250
313
  window.scroller = this.codeScroller;
251
314
 
252
315
  let lastScrollTopValue = -1;
253
- this.codeScroller.addEventListener( 'scroll', (e) => {
316
+ this.codeScroller.addEventListener( 'scroll', e => {
254
317
 
255
318
  if( this._discardScroll )
256
319
  {
@@ -265,36 +328,29 @@ class CodeEditor {
265
328
  // Scroll down...
266
329
  if( scrollTop > lastScrollTopValue )
267
330
  {
268
- const scrollDownBoundary = (this.viewportRangeStart + this.lineScrollMargin.y) * this.lineHeight;
269
-
270
- if( scrollTop > scrollDownBoundary )
331
+ if( this.visibleLinesViewport.y < (this.code.lines.length - 1) )
271
332
  {
272
- this.code.style.top = (scrollDownBoundary - this.lineScrollMargin.x * this.lineHeight) + "px";
273
- this.processLines( CodeEditor.UPDATE_VISIBLE_LINES );
333
+ const totalLinesInViewport = ((this.codeScroller.offsetHeight - 36) / this.lineHeight)|0;
334
+ const scrollDownBoundary =
335
+ ( Math.max( this.visibleLinesViewport.y - totalLinesInViewport, 0 ) - 1 ) * this.lineHeight;
336
+
337
+ if( scrollTop >= scrollDownBoundary )
338
+ this.processLines( CodeEditor.UPDATE_VISIBLE_LINES );
274
339
  }
275
340
  }
276
341
  // Scroll up...
277
342
  else
278
343
  {
279
- const scrollUpBoundary = (this.viewportRangeStart + this.lineScrollMargin.x) * this.lineHeight;
280
- // console.log(scrollTop, scrollUpBoundary)
281
-
282
- if( scrollTop <= scrollUpBoundary )
283
- {
284
- this.viewportRangeStart -= this.lineScrollMargin.x;
285
- this.viewportRangeStart = Math.max( this.viewportRangeStart, 0 );
286
-
287
- this.code.style.top = this.viewportRangeStart == 0 ? "0px" : (scrollUpBoundary - this.lineScrollMargin.x * this.lineHeight) + "px";
288
-
289
- this.processLines( CodeEditor.KEEP_VISIBLE_LINES );
290
- }
344
+ const scrollUpBoundary = parseInt( this.code.style.top );
345
+ if( scrollTop < scrollUpBoundary )
346
+ this.processLines( CodeEditor.UPDATE_VISIBLE_LINES );
291
347
  }
292
348
 
293
349
  lastScrollTopValue = scrollTop;
294
350
  });
295
351
 
296
- this.codeScroller.addEventListener( 'wheel', (e) => {
297
- const dX = (e.deltaY > 0.0 ? 10.0 : -10.0) * ( e.shiftKey ? 1.0 : 0.0 );
352
+ this.codeScroller.addEventListener( 'wheel', e => {
353
+ const dX = ( e.deltaY > 0.0 ? 10.0 : -10.0 ) * ( e.shiftKey ? 1.0 : 0.0 );
298
354
  if( dX != 0.0 ) this.setScrollBarValue( 'horizontal', dX );
299
355
  });
300
356
  }
@@ -309,13 +365,13 @@ class CodeEditor {
309
365
  // Add custom vertical scroll bar
310
366
  {
311
367
  this.vScrollbar = new ScrollBar( this, ScrollBar.SCROLLBAR_VERTICAL );
312
- area.attach(this.vScrollbar.root);
368
+ area.attach( this.vScrollbar.root );
313
369
  }
314
370
 
315
371
  // Add custom horizontal scroll bar
316
372
  {
317
373
  this.hScrollbar = new ScrollBar( this, ScrollBar.SCROLLBAR_HORIZONTAL );
318
- area.attach(this.hScrollbar.root);
374
+ area.attach( this.hScrollbar.root );
319
375
  }
320
376
 
321
377
  // Add autocomplete box
@@ -323,7 +379,7 @@ class CodeEditor {
323
379
  var box = document.createElement( 'div' );
324
380
  box.className = "autocomplete";
325
381
  this.autocomplete = box;
326
- this.tabs.area.attach(box);
382
+ this.tabs.area.attach( box );
327
383
 
328
384
  this.isAutoCompleteActive = false;
329
385
  }
@@ -351,7 +407,7 @@ class CodeEditor {
351
407
 
352
408
  this.useAutoComplete = options.autocomplete ?? true;
353
409
  this.highlight = options.highlight ?? 'Plain Text';
354
- this.onsave = options.onsave ?? ((code) => { });
410
+ this.onsave = options.onsave ?? ((code) => { console.log( code, "save" ) });
355
411
  this.onrun = options.onrun ?? ((code) => { this.runScript(code) });
356
412
  this.actions = {};
357
413
  this.cursorBlinkRate = 550;
@@ -379,16 +435,17 @@ class CodeEditor {
379
435
  // setInterval( this.scanWordSuggestions.bind( this ), 2000 );
380
436
 
381
437
  this.languages = {
382
- 'Plain Text': { },
383
- 'JavaScript': { },
384
- 'C++': { },
385
- 'CSS': { },
386
- 'GLSL': { },
387
- 'WGSL': { },
388
- 'JSON': { },
389
- 'XML': { },
390
- 'Python': { },
391
- 'Batch': { blockComments: false, singleLineCommentToken: '::' }
438
+ 'Plain Text': { ext: 'txt' },
439
+ 'JavaScript': { ext: 'js' },
440
+ 'C++': { ext: 'cpp' },
441
+ 'CSS': { ext: 'css' },
442
+ 'GLSL': { ext: 'glsl' },
443
+ 'WGSL': { ext: 'wgsl' },
444
+ 'JSON': { ext: 'json' },
445
+ 'XML': { ext: 'xml' },
446
+ 'Python': { ext: 'py', singleLineCommentToken: '#' },
447
+ 'HTML': { ext: 'html' },
448
+ 'Batch': { ext: 'bat', blockComments: false, singleLineCommentToken: '::' }
392
449
  };
393
450
 
394
451
  this.specialKeys = [
@@ -411,7 +468,8 @@ class CodeEditor {
411
468
  'texture_storage_2d_array', 'texture_storage_3d'],
412
469
  'Python': ['False', 'def', 'None', 'True', 'in', 'is', 'and', 'lambda', 'nonlocal', 'not', 'or'],
413
470
  'Batch': ['set', 'SET', 'echo', 'ECHO', 'off', 'OFF', 'del', 'DEL', 'defined', 'DEFINED', 'setlocal', 'SETLOCAL', 'enabledelayedexpansion', 'ENABLEDELAYEDEXPANSION', 'driverquery',
414
- 'DRIVERQUERY', 'print', 'PRINT']
471
+ 'DRIVERQUERY', 'print', 'PRINT'],
472
+ 'HTML': ['html', 'meta', 'title', 'link', 'script', 'body', 'DOCTYPE', 'head'],
415
473
  };
416
474
  this.utils = { // These ones don't have hightlight, used as suggestions to autocomplete only...
417
475
  'JavaScript': ['querySelector', 'body', 'addEventListener', 'removeEventListener', 'remove', 'sort', 'keys', 'filter', 'isNaN', 'parseFloat', 'parseInt', 'EPSILON', 'isFinite',
@@ -428,13 +486,14 @@ class CodeEditor {
428
486
  'Python': ['int', 'type', 'float', 'map', 'list', 'ArithmeticError', 'AssertionError', 'AttributeError', 'Exception', 'EOFError', 'FloatingPointError', 'GeneratorExit',
429
487
  'ImportError', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'NotImplementedError', 'OSError',
430
488
  'OverflowError', 'ReferenceError', 'RuntimeError', 'StopIteration', 'SyntaxError', 'TabError', 'SystemError', 'SystemExit', 'TypeError', 'UnboundLocalError',
431
- 'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError', 'ValueError', 'ZeroDivisionError' ],
489
+ 'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError', 'ValueError', 'ZeroDivisionError'],
432
490
  'C++': ['uint8_t', 'uint16_t', 'uint32_t']
433
491
  };
434
492
  this.builtin = {
435
493
  'JavaScript': ['document', 'console', 'window', 'navigator', 'performance'],
436
494
  'CSS': ['*', '!important'],
437
- 'C++': ['vector', 'list', 'map']
495
+ 'C++': ['vector', 'list', 'map'],
496
+ 'HTML': ['type', 'xmlns', 'PUBLIC', 'http-equiv', 'src', 'lang', 'href', 'rel', 'content', 'xml'], // attributes
438
497
  };
439
498
  this.statementsAndDeclarations = {
440
499
  'JavaScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await'],
@@ -454,6 +513,7 @@ class CodeEditor {
454
513
  'CSS': ['{', '}', '(', ')', '*'],
455
514
  'Python': ['<', '>', '[', ']', '(', ')', '='],
456
515
  'Batch': ['[', ']', '(', ')', '%'],
516
+ 'HTML': ['<', '>', '/']
457
517
  };
458
518
 
459
519
  // Convert reserved word arrays to maps so we can search tokens faster
@@ -478,7 +538,7 @@ class CodeEditor {
478
538
  if( this.selection ) {
479
539
  this.deleteSelection( cursor );
480
540
  // Remove entire line when selecting with triple click
481
- if(this.code.lines[ ln ] != undefined && !this.code.lines[ ln ].length)
541
+ if( this._tripleClickSelection )
482
542
  {
483
543
  this.actions['Backspace'].callback( ln, cursor, e );
484
544
  this.lineDown( cursor, true );
@@ -559,9 +619,15 @@ class CodeEditor {
559
619
  if( this.selection )
560
620
  lastX += this.selection.chars;
561
621
 
562
- this.startSelection( cursor );
622
+ if( !this.selection )
623
+ this.startSelection( cursor );
563
624
  var string = this.code.lines[ ln ].substring( idx, lastX );
564
- this.selection.selectInline( idx, cursor.line, this.measureString( string ) );
625
+ if( this.selection.sameLine() )
626
+ this.selection.selectInline( idx, cursor.line, this.measureString( string ) );
627
+ else
628
+ {
629
+ this.processSelection();
630
+ }
565
631
  } else if( !e.keepSelection )
566
632
  this.endSelection();
567
633
  });
@@ -570,18 +636,25 @@ class CodeEditor {
570
636
 
571
637
  if( e.shiftKey || e._shiftKey ) {
572
638
 
573
- var string = this.code.lines[ ln ].substring(cursor.position);
639
+ var string = this.code.lines[ ln ].substring( cursor.position );
574
640
  if( !this.selection )
575
641
  this.startSelection( cursor );
576
- this.selection.selectInline(cursor.position, cursor.line, this.measureString(string));
642
+ if( this.selection.sameLine() )
643
+ this.selection.selectInline(cursor.position, cursor.line, this.measureString( string ));
644
+ else
645
+ {
646
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT );
647
+ this.cursorToString( cursor, this.code.lines[ ln ] );
648
+ this.processSelection();
649
+ }
577
650
  } else
578
651
  this.endSelection();
579
652
 
580
653
  this.resetCursorPos( CodeEditor.CURSOR_LEFT );
581
654
  this.cursorToString( cursor, this.code.lines[ ln ] );
582
655
 
583
- const last_char = (this.code.clientWidth / this.charWidth)|0;
584
- this.setScrollLeft( cursor.position >= last_char ? (cursor.position - last_char) * this.charWidth : 0 );
656
+ const last_char = ( this.code.clientWidth / this.charWidth )|0;
657
+ this.setScrollLeft( cursor.position >= last_char ? ( cursor.position - last_char ) * this.charWidth : 0 );
585
658
  });
586
659
 
587
660
  this.action( 'Enter', true, ( ln, cursor, e ) => {
@@ -708,7 +781,8 @@ class CodeEditor {
708
781
  var diff = Math.max(cursor.position - from, 1);
709
782
  var substr = word.substr(0, diff);
710
783
  // Selections...
711
- if( e.shiftKey ) if( !this.selection ) this.startSelection( cursor );
784
+ if( e.shiftKey ) { if( !this.selection ) this.startSelection( cursor ); }
785
+ else this.endSelection();
712
786
  this.cursorToString(cursor, substr, true);
713
787
  if( e.shiftKey ) this.processSelection();
714
788
  }
@@ -772,7 +846,8 @@ class CodeEditor {
772
846
  var diff = cursor.position - from;
773
847
  var substr = word.substr( diff );
774
848
  // Selections...
775
- if( e.shiftKey ) if( !this.selection ) this.startSelection( cursor );
849
+ if( e.shiftKey ) { if( !this.selection ) this.startSelection( cursor ); }
850
+ else this.endSelection();
776
851
  this.cursorToString( cursor, substr);
777
852
  if( e.shiftKey ) this.processSelection();
778
853
  } else {
@@ -833,6 +908,7 @@ class CodeEditor {
833
908
 
834
909
  // Default code tab
835
910
 
911
+ this.loadedTabs = { };
836
912
  this.openedTabs = { };
837
913
 
838
914
  if( options.allow_add_scripts ?? true )
@@ -850,6 +926,26 @@ class CodeEditor {
850
926
  return CodeEditor.__instances;
851
927
  }
852
928
 
929
+ // This received key inputs from the entire document...
930
+ onKeyPressed( e ) {
931
+
932
+ // Toggle visibility of the file explorer
933
+ if( e.key == 'b' && e.ctrlKey && this.explorer )
934
+ {
935
+ this.explorerArea.root.classList.toggle( "hidden" );
936
+ if( this._lastBaseareaWidth )
937
+ {
938
+ this.base_area.root.style.width = this._lastBaseareaWidth;
939
+ delete this._lastBaseareaWidth;
940
+
941
+ } else
942
+ {
943
+ this._lastBaseareaWidth = this.base_area.root.style.width;
944
+ this.base_area.root.style.width = "100%";
945
+ }
946
+ }
947
+ }
948
+
853
949
  getText( min ) {
854
950
  return this.code.lines.join(min ? ' ' : '\n');
855
951
  }
@@ -939,11 +1035,22 @@ class CodeEditor {
939
1035
  loadFile( file ) {
940
1036
 
941
1037
  const inner_add_tab = ( text, name, title ) => {
942
- const existing = this.addTab(name, true, title);
943
- if( !existing )
1038
+
1039
+ // Set current text and language
1040
+ const lines = text.replaceAll( '\r', '' ).split( '\n' );
1041
+
1042
+ // Add item in the explorer if used
1043
+ if( this.explorer )
1044
+ {
1045
+ this._storedLines = this._storedLines ?? {};
1046
+ this._storedLines[ name ] = lines;
1047
+ this.addExplorerItem( { 'id': name, 'skipVisibility': true, 'icon': this._getFileIcon( name ) } );
1048
+ this.explorer.frefresh( name );
1049
+ }
1050
+ else
944
1051
  {
945
- text = text.replaceAll( '\r', '' );
946
- this.code.lines = text.split( '\n' );
1052
+ this.addTab(name, true, title);
1053
+ this.code.lines = lines;
947
1054
  this._changeLanguageFromExtension( LX.getExtension( name ) );
948
1055
  }
949
1056
  };
@@ -970,12 +1077,28 @@ class CodeEditor {
970
1077
 
971
1078
  _addUndoStep( cursor ) {
972
1079
 
1080
+ const d = new Date();
1081
+ const current = d.getTime();
1082
+
1083
+ if( !this._lastTime ) {
1084
+ this._lastTime = current;
1085
+ } else {
1086
+ if( ( current - this._lastTime ) > 3000 ){
1087
+ this._lastTime = null;
1088
+ } else {
1089
+ // If time not enough, reset timer
1090
+ this._lastTime = current;
1091
+ return;
1092
+ }
1093
+ }
1094
+
973
1095
  var cursor = cursor ?? this.cursors.children[ 0 ];
974
1096
 
975
1097
  this.code.undoSteps.push( {
976
1098
  lines: LX.deepCopy( this.code.lines ),
977
1099
  cursor: this.saveCursor( cursor ),
978
- line: cursor.line
1100
+ line: cursor.line,
1101
+ position: cursor.position
979
1102
  } );
980
1103
  }
981
1104
 
@@ -985,6 +1108,35 @@ class CodeEditor {
985
1108
  this.highlight = lang;
986
1109
  this._refreshCodeInfo();
987
1110
  this.processLines();
1111
+
1112
+ const ext = this.languages[ lang ].ext;
1113
+ const icon = this._getFileIcon( null, ext );
1114
+
1115
+ // Update tab icon
1116
+ {
1117
+ const tab = this.tabs.tabDOMs[ this.code.tabName ];
1118
+ tab.firstChild.remove();
1119
+ console.assert( tab != undefined );
1120
+ var iconEl;
1121
+ if( icon.includes( 'fa-' ) )
1122
+ {
1123
+ iconEl = document.createElement( 'i' );
1124
+ iconEl.className = icon;
1125
+ } else {
1126
+ iconEl = document.createElement( 'img' );
1127
+ iconEl.src = "https://raw.githubusercontent.com/jxarco/lexgui.js/master/" + icon;
1128
+ }
1129
+ tab.prepend( iconEl );
1130
+ }
1131
+
1132
+ // Update explorer icon
1133
+ if( this.explorer )
1134
+ {
1135
+ const item = this.explorer.data.children.filter( (v) => v.id === this.code.tabName )[ 0 ];
1136
+ console.assert( item != undefined );
1137
+ item.icon = icon;
1138
+ this.explorer.frefresh( this.code.tabName );
1139
+ }
988
1140
  }
989
1141
 
990
1142
  _changeLanguageFromExtension( ext ) {
@@ -1004,6 +1156,7 @@ class CodeEditor {
1004
1156
  case 'wgsl': return this._changeLanguage( 'WGSL' );
1005
1157
  case 'py': return this._changeLanguage( 'Python' );
1006
1158
  case 'bat': return this._changeLanguage( 'Batch' );
1159
+ case 'html': return this._changeLanguage( 'HTML' );
1007
1160
  case 'txt':
1008
1161
  default:
1009
1162
  this._changeLanguage( 'Plain Text' );
@@ -1054,23 +1207,40 @@ class CodeEditor {
1054
1207
  }
1055
1208
  }
1056
1209
 
1210
+ _getFileIcon( name, extension ) {
1211
+
1212
+ const isNewTabButton = name ? ( name === '+' ) : false;
1213
+ const ext = extension ?? LX.getExtension( name );
1214
+ return ext == 'html' ? "fa-solid fa-code orange" :
1215
+ ext == "css" ? "fa-solid fa-hashtag dodgerblue" :
1216
+ ext == "xml" ? "fa-solid fa-rss orange" :
1217
+ ext == "bat" ? "fa-brands fa-windows lightblue" :
1218
+ [ "js", "py", "json", "cpp" ].indexOf( ext ) > -1 ? "images/" + ext + ".png" :
1219
+ !isNewTabButton ? "fa-solid fa-align-left gray" : undefined;
1220
+ }
1221
+
1057
1222
  _onNewTab( e ) {
1058
1223
 
1059
1224
  this.processFocus(false);
1060
1225
 
1061
1226
  LX.addContextMenu( null, e, m => {
1062
1227
  m.add( "Create", this.addTab.bind( this, "unnamed.js", true ) );
1063
- m.add( "Load", this.loadTab.bind( this, "unnamed.js", true ) );
1228
+ m.add( "Load", this.loadTabFromFile.bind( this, "unnamed.js", true ) );
1064
1229
  });
1065
1230
  }
1066
1231
 
1067
- addTab(name, selected, title) {
1232
+ addTab( name, selected, title ) {
1068
1233
 
1069
- if(this.openedTabs[ name ])
1070
- {
1071
- this.tabs.select( this.code.tabName );
1072
- return true;
1073
- }
1234
+ // If already loaded, set new name...
1235
+ const repeats = Object.keys( editor.loadedTabs ).slice( 1 ).reduce( ( v, key ) => {
1236
+ const noRepeatName = key.replace( /[_\d+]/g, '');
1237
+ return v + ( noRepeatName == name );
1238
+ }, 0 );
1239
+
1240
+ if( repeats > 0 )
1241
+ name = name.split( '.' ).join( '_' + repeats + '.' );
1242
+
1243
+ const isNewTabButton = ( name === '+' );
1074
1244
 
1075
1245
  // Create code content
1076
1246
  let code = document.createElement( 'div' );
@@ -1082,6 +1252,8 @@ class CodeEditor {
1082
1252
  code.tabName = name;
1083
1253
  code.title = title ?? name;
1084
1254
  code.tokens = {};
1255
+ code.style.left = "0px";
1256
+ code.style.top = "0px";
1085
1257
 
1086
1258
  code.addEventListener( 'dragenter', function(e) {
1087
1259
  e.preventDefault();
@@ -1098,24 +1270,39 @@ class CodeEditor {
1098
1270
  this.loadFile( e.dataTransfer.files[ i ] );
1099
1271
  });
1100
1272
 
1273
+ this.loadedTabs[ name ] = code;
1101
1274
  this.openedTabs[ name ] = code;
1275
+
1276
+ const tabIcon = this._getFileIcon( name );
1102
1277
 
1103
- this.tabs.add(name, code, { 'selected': selected, 'fixed': (name === '+') , 'title': code.title, 'onSelect': (e, tabname) => {
1278
+ if( this.explorer && !isNewTabButton )
1279
+ {
1280
+ this.addExplorerItem( { 'id': name, 'skipVisibility': true, 'icon': tabIcon } );
1281
+ this.explorer.frefresh( name );
1282
+ }
1104
1283
 
1105
- if(tabname == '+')
1106
- {
1107
- this._onNewTab( e );
1108
- return;
1109
- }
1284
+ this.tabs.add(name, code, {
1285
+ selected: selected,
1286
+ fixed: isNewTabButton,
1287
+ title: code.title,
1288
+ icon: tabIcon,
1289
+ onSelect: (e, tabname) => {
1110
1290
 
1111
- var cursor = cursor ?? this.cursors.children[ 0 ];
1112
- this.saveCursor( cursor, this.code.cursorState );
1113
- this.code = this.openedTabs[ tabname ];
1114
- this.restoreCursor( cursor, this.code.cursorState );
1115
- this.endSelection();
1116
- this._changeLanguageFromExtension( LX.getExtension( tabname ) );
1117
- this._refreshCodeInfo( cursor.line, cursor.position );
1118
- }});
1291
+ if( isNewTabButton )
1292
+ {
1293
+ this._onNewTab( e );
1294
+ return;
1295
+ }
1296
+
1297
+ var cursor = cursor ?? this.cursors.children[ 0 ];
1298
+ this.saveCursor( cursor, this.code.cursorState );
1299
+ this.code = this.loadedTabs[ tabname ];
1300
+ this.restoreCursor( cursor, this.code.cursorState );
1301
+ this.endSelection();
1302
+ this._changeLanguageFromExtension( LX.getExtension( tabname ) );
1303
+ this._refreshCodeInfo( cursor.line, cursor.position );
1304
+ }
1305
+ });
1119
1306
 
1120
1307
  // Move into the sizer..
1121
1308
  this.codeSizer.appendChild( code );
@@ -1129,9 +1316,77 @@ class CodeEditor {
1129
1316
  this.processLines();
1130
1317
  doAsync( () => this._refreshCodeInfo( 0, 0 ), 50 );
1131
1318
  }
1319
+
1320
+ // Bc it could be overrided..
1321
+ return name;
1132
1322
  }
1133
1323
 
1134
- loadTab() {
1324
+ loadTab( name ) {
1325
+
1326
+ // Already open...
1327
+ if( this.openedTabs[ name ] )
1328
+ {
1329
+ this.tabs.select( name );
1330
+ return;
1331
+ }
1332
+
1333
+ let code = this.loadedTabs[ name ]
1334
+
1335
+ if( !code )
1336
+ {
1337
+ this.addTab( name, true );
1338
+ // Unload lines from file...
1339
+ if( this._storedLines[ name ] )
1340
+ {
1341
+ this.code.lines = this._storedLines[ name ];
1342
+ delete this._storedLines[ name ];
1343
+ }
1344
+ this._changeLanguageFromExtension( LX.getExtension( name ) );
1345
+ return;
1346
+ }
1347
+
1348
+ this.openedTabs[ name ] = code;
1349
+
1350
+ const isNewTabButton = ( name === '+' );
1351
+ const tabIcon = this._getFileIcon( name );
1352
+
1353
+ this.tabs.add(name, code, {
1354
+ selected: true,
1355
+ fixed: isNewTabButton,
1356
+ title: code.title,
1357
+ icon: tabIcon,
1358
+ onSelect: (e, tabname) => {
1359
+
1360
+ if( isNewTabButton )
1361
+ {
1362
+ this._onNewTab( e );
1363
+ return;
1364
+ }
1365
+
1366
+ var cursor = cursor ?? this.cursors.children[ 0 ];
1367
+ this.saveCursor( cursor, this.code.cursorState );
1368
+ this.code = this.loadedTabs[ tabname ];
1369
+ this.restoreCursor( cursor, this.code.cursorState );
1370
+ this.endSelection();
1371
+ this._changeLanguageFromExtension( LX.getExtension( tabname ) );
1372
+ this._refreshCodeInfo( cursor.line, cursor.position );
1373
+ }
1374
+ });
1375
+
1376
+ // Move into the sizer..
1377
+ this.codeSizer.appendChild( code );
1378
+
1379
+ this.endSelection();
1380
+
1381
+ // Select as current...
1382
+ this.code = code;
1383
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP );
1384
+ this.processLines();
1385
+ this._changeLanguageFromExtension( LX.getExtension( name ) );
1386
+ doAsync( () => this._refreshCodeInfo( 0, 0 ), 50 );
1387
+ }
1388
+
1389
+ loadTabFromFile() {
1135
1390
  const input = document.createElement( 'input' );
1136
1391
  input.type = 'file';
1137
1392
  document.body.appendChild( input );
@@ -1221,7 +1476,7 @@ class CodeEditor {
1221
1476
  this.resetCursorPos( CodeEditor.CURSOR_LEFT );
1222
1477
  this.cursorToPosition( cursor, from );
1223
1478
  this.startSelection( cursor );
1224
- this.selection.selectInline(from, cursor.line, this.measureString(word));
1479
+ this.selection.selectInline( from, cursor.line, this.measureString( word ) );
1225
1480
  this.cursorToString( cursor, word ); // Go to the end of the word
1226
1481
  break;
1227
1482
  // Select entire line
@@ -1229,6 +1484,7 @@ class CodeEditor {
1229
1484
  this.resetCursorPos( CodeEditor.CURSOR_LEFT );
1230
1485
  e._shiftKey = true;
1231
1486
  this.actions['End'].callback(cursor.line, cursor, e);
1487
+ this._tripleClickSelection = true;
1232
1488
  break;
1233
1489
  }
1234
1490
  }
@@ -1248,8 +1504,8 @@ class CodeEditor {
1248
1504
  m.add( "Paste", () => { this._pasteContent(); } );
1249
1505
  m.add( "" );
1250
1506
  m.add( "Format/JSON", () => {
1251
- let json = this.toJSONFormat(this.getText());
1252
- this.code.lines = json.split("\n");
1507
+ let json = this.toJSONFormat( this.getText() );
1508
+ this.code.lines = json.split( "\n" );
1253
1509
  this.processLines();
1254
1510
  } );
1255
1511
  }
@@ -1285,7 +1541,7 @@ class CodeEditor {
1285
1541
 
1286
1542
  var cursor = this.cursors.children[ 0 ];
1287
1543
 
1288
- if(e) this.processClick( e, true );
1544
+ if( e ) this.processClick( e, true );
1289
1545
  if( !this.selection )
1290
1546
  this.startSelection( cursor );
1291
1547
 
@@ -1307,20 +1563,25 @@ class CodeEditor {
1307
1563
  // Selection goes down...
1308
1564
  if( deltaY >= 0 )
1309
1565
  {
1310
- while( deltaY < (this.selections.childElementCount - 1) )
1566
+ while( deltaY < ( this.selections.childElementCount - 1 ) )
1311
1567
  deleteElement( this.selections.lastChild );
1312
1568
 
1313
1569
  for(let i = fromY; i <= toY; i++){
1314
1570
 
1315
1571
  const sId = i - fromY;
1572
+ const isVisible = i >= this.visibleLinesViewport.x && i <= this.visibleLinesViewport.y;
1573
+ let domEl = null;
1316
1574
 
1317
- // Make sure that the line selection is generated...
1318
- let domEl = this.selections.childNodes[sId];
1319
- if(!domEl)
1575
+ if( isVisible )
1320
1576
  {
1321
- domEl = document.createElement( 'div' );
1322
- domEl.className = "lexcodeselection";
1323
- this.selections.appendChild( domEl );
1577
+ // Make sure that the line selection is generated...
1578
+ domEl = this.selections.childNodes[ sId ];
1579
+ if(!domEl)
1580
+ {
1581
+ domEl = document.createElement( 'div' );
1582
+ domEl.className = "lexcodeselection";
1583
+ this.selections.appendChild( domEl );
1584
+ }
1324
1585
  }
1325
1586
 
1326
1587
  // Compute new width and selection margins
@@ -1329,67 +1590,80 @@ class CodeEditor {
1329
1590
  if(sId == 0) // First line 2 cases (single line, multiline)
1330
1591
  {
1331
1592
  const reverse = fromX > toX;
1332
- if(deltaY == 0) string = !reverse ? this.code.lines[ i ].substring(fromX, toX) : this.code.lines[ i ].substring(toX, fromX);
1333
- else string = this.code.lines[ i ].substr(fromX);
1593
+ if(deltaY == 0) string = !reverse ? this.code.lines[ i ].substring( fromX, toX ) : this.code.lines[ i ].substring(toX, fromX);
1594
+ else string = this.code.lines[ i ].substr( fromX );
1334
1595
  const pixels = (reverse && deltaY == 0 ? toX : fromX) * this.charWidth;
1335
- domEl.style.left = "calc(" + pixels + "px + " + this.xPadding + ")";
1596
+ if( isVisible ) domEl.style.left = "calc(" + pixels + "px + " + this.xPadding + ")";
1336
1597
  }
1337
1598
  else
1338
1599
  {
1339
- string = (i == toY) ? this.code.lines[ i ].substring(0, toX) : this.code.lines[ i ]; // Last line, any multiple line...
1340
- domEl.style.left = this.xPadding;
1600
+ string = (i == toY) ? this.code.lines[ i ].substring( 0, toX ) : this.code.lines[ i ]; // Last line, any multiple line...
1601
+ if( isVisible ) domEl.style.left = this.xPadding;
1341
1602
  }
1342
1603
 
1343
- const stringWidth = this.measureString(string);
1344
- domEl.style.width = (stringWidth || 8) + "px";
1345
- domEl._top = i * this.lineHeight;
1346
- domEl.style.top = domEl._top + "px";
1604
+ const stringWidth = this.measureString( string );
1347
1605
  this.selection.chars += stringWidth / this.charWidth;
1606
+
1607
+ if( isVisible )
1608
+ {
1609
+ domEl.style.width = (stringWidth || 8) + "px";
1610
+ domEl._top = i * this.lineHeight;
1611
+ domEl.style.top = domEl._top + "px";
1612
+ }
1348
1613
  }
1349
1614
  }
1350
1615
  else // Selection goes up...
1351
1616
  {
1352
- while( Math.abs(deltaY) < (this.selections.childElementCount - 1) )
1617
+ while( Math.abs( deltaY ) < ( this.selections.childElementCount - 1 ) )
1353
1618
  deleteElement( this.selections.firstChild );
1354
1619
 
1355
- for(let i = toY; i <= fromY; i++){
1620
+ for( let i = toY; i <= fromY; i++ ){
1356
1621
 
1357
1622
  const sId = i - toY;
1623
+ const isVisible = i >= this.visibleLinesViewport.x && i <= this.visibleLinesViewport.y;
1624
+ let domEl = null;
1358
1625
 
1359
- // Make sure that the line selection is generated...
1360
- let domEl = this.selections.childNodes[sId];
1361
- if(!domEl)
1626
+ if( isVisible )
1362
1627
  {
1363
- domEl = document.createElement( 'div' );
1364
- domEl.className = "lexcodeselection";
1365
- this.selections.appendChild( domEl );
1628
+ // Make sure that the line selection is generated...
1629
+ domEl = this.selections.childNodes[ sId ];
1630
+ if(!domEl)
1631
+ {
1632
+ domEl = document.createElement( 'div' );
1633
+ domEl.className = "lexcodeselection";
1634
+ this.selections.appendChild( domEl );
1635
+ }
1366
1636
  }
1367
1637
 
1368
1638
  // Compute new width and selection margins
1369
1639
  let string;
1370
1640
 
1371
- if(sId == 0)
1641
+ if( sId == 0 )
1372
1642
  {
1373
1643
  string = this.code.lines[ i ].substr(toX);
1374
1644
  const pixels = toX * this.charWidth;
1375
- domEl.style.left = "calc(" + pixels + "px + " + this.xPadding + ")";
1645
+ if( isVisible ) domEl.style.left = "calc(" + pixels + "px + " + this.xPadding + ")";
1376
1646
  }
1377
1647
  else
1378
1648
  {
1379
1649
  string = (i == fromY) ? this.code.lines[ i ].substring(0, fromX) : this.code.lines[ i ]; // Last line, any multiple line...
1380
- domEl.style.left = this.xPadding;
1650
+ if( isVisible ) domEl.style.left = this.xPadding;
1381
1651
  }
1382
1652
 
1383
- const stringWidth = this.measureString(string);
1384
- domEl.style.width = (stringWidth || 8) + "px";
1385
- domEl._top = i * this.lineHeight;
1386
- domEl.style.top = domEl._top + "px";
1653
+ const stringWidth = this.measureString( string );
1387
1654
  this.selection.chars += stringWidth / this.charWidth;
1655
+
1656
+ if( isVisible )
1657
+ {
1658
+ domEl.style.width = (stringWidth || 8) + "px";
1659
+ domEl._top = i * this.lineHeight;
1660
+ domEl.style.top = domEl._top + "px";
1661
+ }
1388
1662
  }
1389
1663
  }
1390
1664
  }
1391
1665
 
1392
- async processKey(e) {
1666
+ async processKey( e ) {
1393
1667
 
1394
1668
  if( !this.code )
1395
1669
  return;
@@ -1399,7 +1673,7 @@ class CodeEditor {
1399
1673
  const skip_undo = e.detail.skip_undo ?? false;
1400
1674
 
1401
1675
  // keys with length > 1 are probably special keys
1402
- if( key.length > 1 && this.specialKeys.indexOf(key) == -1 )
1676
+ if( key.length > 1 && this.specialKeys.indexOf( key ) == -1 )
1403
1677
  return;
1404
1678
 
1405
1679
  let cursor = this.cursors.children[ 0 ];
@@ -1446,7 +1720,6 @@ class CodeEditor {
1446
1720
  return;
1447
1721
  const step = this.code.undoSteps.pop();
1448
1722
  this.code.lines = step.lines;
1449
- cursor.line = step.line;
1450
1723
  this.restoreCursor( cursor, step.cursor );
1451
1724
  this.processLines();
1452
1725
  return;
@@ -1493,23 +1766,9 @@ class CodeEditor {
1493
1766
 
1494
1767
  // Add undo steps
1495
1768
 
1496
- const d = new Date();
1497
- const current = d.getTime();
1498
-
1499
- if( !skip_undo )
1769
+ if( !skip_undo && this.code.lines.length )
1500
1770
  {
1501
- if( !this._lastTime ) {
1502
- this._lastTime = current;
1503
- this._addUndoStep( cursor );
1504
- } else {
1505
- if( (current - this._lastTime) > 3000 && this.code.lines.length){
1506
- this._lastTime = null;
1507
- this._addUndoStep( cursor );
1508
- }else{
1509
- // If time not enough, reset timer
1510
- this._lastTime = current;
1511
- }
1512
- }
1771
+ this._addUndoStep( cursor );
1513
1772
  }
1514
1773
 
1515
1774
  // Some custom cases for word enclosing (), {}, "", '', ...
@@ -1517,7 +1776,7 @@ class CodeEditor {
1517
1776
  const enclosableKeys = ["\"", "'", "(", "{"];
1518
1777
  if( enclosableKeys.indexOf( key ) > -1 )
1519
1778
  {
1520
- if( this.encloseSelectedWordWithKey(key, lidx, cursor) )
1779
+ if( this._encloseSelectedWordWithKey(key, lidx, cursor) )
1521
1780
  return;
1522
1781
  }
1523
1782
 
@@ -1527,7 +1786,6 @@ class CodeEditor {
1527
1786
  if( this.selection )
1528
1787
  {
1529
1788
  this.actions['Backspace'].callback(lidx, cursor, e);
1530
- lidx = cursor.line; // Update this, since it's from the old code
1531
1789
  }
1532
1790
 
1533
1791
  // Append key
@@ -1567,6 +1825,14 @@ class CodeEditor {
1567
1825
  // Update only the current line, since it's only an appended key
1568
1826
  this.processLine( lidx );
1569
1827
 
1828
+ // We are out of the viewport and max length is different? Resize scrollbars...
1829
+ const maxLineLength = this.getMaxLineLength();
1830
+ const numViewportChars = Math.floor( this.codeScroller.clientWidth / this.charWidth );
1831
+ if( maxLineLength >= numViewportChars && maxLineLength != this._lastMaxLineLength )
1832
+ {
1833
+ this.resize( maxLineLength );
1834
+ }
1835
+
1570
1836
  // Manage autocomplete
1571
1837
 
1572
1838
  if( this.useAutoComplete )
@@ -1587,6 +1853,10 @@ class CodeEditor {
1587
1853
  text_to_copy = "\n" + this.code.lines[ cursor.line ];
1588
1854
  }
1589
1855
  else {
1856
+
1857
+ // Some selections don't depend on mouse up..
1858
+ if( this.selection ) this.selection.invertIfNecessary();
1859
+
1590
1860
  const separator = "_NEWLINE_";
1591
1861
  let code = this.code.lines.join(separator);
1592
1862
 
@@ -1603,7 +1873,7 @@ class CodeEditor {
1603
1873
  text_to_copy = lines.join('\n');
1604
1874
  }
1605
1875
 
1606
- navigator.clipboard.writeText(text_to_copy).then(() => console.log("Successfully copied"), (err) => console.error("Error"));
1876
+ navigator.clipboard.writeText( text_to_copy ).then(() => console.log("Successfully copied"), (err) => console.error("Error"));
1607
1877
  }
1608
1878
 
1609
1879
  async _cutContent() {
@@ -1619,6 +1889,10 @@ class CodeEditor {
1619
1889
  this.resetCursorPos( CodeEditor.CURSOR_LEFT );
1620
1890
  }
1621
1891
  else {
1892
+
1893
+ // Some selections don't depend on mouse up..
1894
+ if( this.selection ) this.selection.invertIfNecessary();
1895
+
1622
1896
  const separator = "_NEWLINE_";
1623
1897
  let code = this.code.lines.join(separator);
1624
1898
 
@@ -1637,7 +1911,7 @@ class CodeEditor {
1637
1911
  this.deleteSelection( cursor );
1638
1912
  }
1639
1913
 
1640
- navigator.clipboard.writeText(text_to_cut).then(() => console.log("Successfully cut"), (err) => console.error("Error"));
1914
+ navigator.clipboard.writeText( text_to_cut ).then(() => console.log("Successfully cut"), (err) => console.error("Error"));
1641
1915
  }
1642
1916
 
1643
1917
  action( key, deleteSelection, fn ) {
@@ -1662,7 +1936,7 @@ class CodeEditor {
1662
1936
 
1663
1937
  toLocalLine( line ) {
1664
1938
 
1665
- const d = Math.max( this.viewportRangeStart - this.lineScrollMargin.x, 0 );
1939
+ const d = Math.max( this.firstLineInViewport - this.lineScrollMargin.x, 0 );
1666
1940
  return Math.min( Math.max( line - d, 0 ), this.code.lines.length - 1 );
1667
1941
  }
1668
1942
 
@@ -1672,38 +1946,34 @@ class CodeEditor {
1672
1946
  }
1673
1947
 
1674
1948
  processLines( mode ) {
1675
-
1676
- mode = mode ?? CodeEditor.KEEP_VISIBLE_LINES;
1677
-
1678
- // console.clear();
1679
- console.log("--------------------------------------------");
1680
-
1681
- const lastScrollTop = this.getScrollTop();
1949
+
1682
1950
  const start = performance.now();
1683
1951
 
1684
- var gutter_html = "", code_html = "";
1685
-
1952
+ var gutter_html = "";
1953
+ var code_html = "";
1954
+
1955
+ // Reset all lines content
1686
1956
  this.code.innerHTML = "";
1687
-
1957
+
1688
1958
  // Get info about lines in viewport
1689
- const firstLineInViewport = mode & CodeEditor.UPDATE_VISIBLE_LINES ?
1690
- ( (lastScrollTop / this.lineHeight)|0 ) : this.viewportRangeStart;
1959
+ const lastScrollTop = this.getScrollTop();
1960
+ this.firstLineInViewport = ( mode ?? CodeEditor.KEEP_VISIBLE_LINES ) & CodeEditor.UPDATE_VISIBLE_LINES ?
1961
+ ( (lastScrollTop / this.lineHeight)|0 ) : this.firstLineInViewport;
1691
1962
  const totalLinesInViewport = ((this.codeScroller.offsetHeight - 36) / this.lineHeight)|0;
1692
- this.viewportRangeStart = firstLineInViewport;
1693
-
1694
- const viewportRange = new LX.vec2(
1695
- Math.max( firstLineInViewport - this.lineScrollMargin.x, 0 ),
1696
- Math.min( firstLineInViewport + totalLinesInViewport + this.lineScrollMargin.y, this.code.lines.length )
1963
+ this.visibleLinesViewport = new LX.vec2(
1964
+ Math.max( this.firstLineInViewport - this.lineScrollMargin.x, 0 ),
1965
+ Math.min( this.firstLineInViewport + totalLinesInViewport + this.lineScrollMargin.y, this.code.lines.length )
1697
1966
  );
1698
1967
 
1699
1968
  // Add remaining lines if we are near the end of the scroll
1700
1969
  {
1701
- const diff = Math.max( this.code.lines.length - viewportRange.y, 0 );
1702
- if( diff < ( totalLinesInViewport + this.lineScrollMargin.y ) )
1703
- viewportRange.y += diff;
1970
+ const diff = Math.max( this.code.lines.length - this.visibleLinesViewport.y, 0 );
1971
+ if( diff <= this.lineScrollMargin.y )
1972
+ this.visibleLinesViewport.y += diff;
1704
1973
  }
1705
-
1706
- for( let i = viewportRange.x; i < viewportRange.y; ++i )
1974
+
1975
+ // Process visible lines
1976
+ for( let i = this.visibleLinesViewport.x; i < this.visibleLinesViewport.y; ++i )
1707
1977
  {
1708
1978
  gutter_html += "<span>" + (i + 1) + "</span>";
1709
1979
  code_html += this.processLine( i, true );
@@ -1711,24 +1981,23 @@ class CodeEditor {
1711
1981
 
1712
1982
  this.code.innerHTML = code_html;
1713
1983
 
1714
- console.log("RANGE:", viewportRange);
1715
- console.log( "Num lines processed:", (viewportRange.y - viewportRange.x), performance.now() - start );
1716
- console.log("--------------------------------------------");
1984
+ // console.log("RANGE:", this.visibleLinesViewport);
1985
+ // console.log( "Num lines processed:", (this.visibleLinesViewport.y - this.visibleLinesViewport.x), performance.now() - start );
1986
+ // console.log("--------------------------------------------");
1717
1987
 
1988
+ // Update scroll data
1718
1989
  this.codeScroller.scrollTop = lastScrollTop;
1990
+ this.code.style.top = ( this.visibleLinesViewport.x * this.lineHeight ) + "px";
1719
1991
 
1720
- setTimeout( () => {
1721
-
1722
- // Update max viewport
1723
- const scrollWidth = this.getMaxLineLength() * this.charWidth;
1724
- const scrollHeight = this.code.lines.length * this.lineHeight + 10; // scrollbar offset
1725
-
1726
- this.codeSizer.style.minWidth = scrollWidth + "px";
1727
- this.codeSizer.style.minHeight = scrollHeight + "px";
1992
+ // Update selections
1993
+ if( this.selection )
1994
+ this.processSelection( null, true );
1728
1995
 
1729
- this.resizeScrollBars( totalLinesInViewport );
1996
+ // Clear tmp vars
1997
+ delete this._buildingString;
1998
+ delete this._pendingString;
1730
1999
 
1731
- }, 10 );
2000
+ this.resize();
1732
2001
  }
1733
2002
 
1734
2003
  processLine( linenum, force ) {
@@ -1796,7 +2065,7 @@ class CodeEditor {
1796
2065
  delete this._buildingBlockComment;
1797
2066
  }
1798
2067
 
1799
- line_inner_html += this.evaluateToken( token, prev, next, (i == tokensToEvaluate.length - 1) );
2068
+ line_inner_html += this._evaluateToken( token, prev, next, (i == tokensToEvaluate.length - 1) );
1800
2069
  }
1801
2070
 
1802
2071
  return UPDATE_LINE( line_inner_html );
@@ -1829,7 +2098,7 @@ class CodeEditor {
1829
2098
 
1830
2099
  const singleLineCommentToken = this.languages[ this.highlight ].singleLineCommentToken ?? this.defaultSingleLineCommentToken;
1831
2100
  const idx = linestring.indexOf( singleLineCommentToken );
1832
-
2101
+
1833
2102
  if( idx > -1 )
1834
2103
  {
1835
2104
  const stringKeys = Object.values( this.stringKeys );
@@ -1870,7 +2139,7 @@ class CodeEditor {
1870
2139
  tokensToEvaluate.push( t );
1871
2140
  };
1872
2141
 
1873
- let iter = linestring.matchAll(/(::|[\[\](){}<>.,;:*"'%@ ])/g);
2142
+ let iter = linestring.matchAll(/(::|[\[\](){}<>.,;:*"'%@!/= ])/g);
1874
2143
  let subtokens = iter.next();
1875
2144
  if( subtokens.value )
1876
2145
  {
@@ -1923,7 +2192,7 @@ class CodeEditor {
1923
2192
  return kindArray[this.highlight] && kindArray[this.highlight][token] != undefined;
1924
2193
  }
1925
2194
 
1926
- evaluateToken( token, prev, next, isLastToken ) {
2195
+ _evaluateToken( token, prev, next, isLastToken ) {
1927
2196
 
1928
2197
  const highlight = this.highlight.replace(/\s/g, '').replaceAll("+", "p").toLowerCase();
1929
2198
  const customStringKeys = Object.assign( {}, this.stringKeys );
@@ -1950,7 +2219,7 @@ class CodeEditor {
1950
2219
  {
1951
2220
  if( this._buildingString != undefined )
1952
2221
  {
1953
- this.appendStringToken( token );
2222
+ this._appendStringToken( token );
1954
2223
  return "";
1955
2224
  }
1956
2225
  return token;
@@ -1967,7 +2236,7 @@ class CodeEditor {
1967
2236
  token_classname = "cm-com";
1968
2237
 
1969
2238
  else if( this._buildingString != undefined )
1970
- discardToken = this.appendStringToken( token );
2239
+ discardToken = this._appendStringToken( token );
1971
2240
 
1972
2241
  else if( this._mustHightlightWord( token, this.keywords ) )
1973
2242
  token_classname = "cm-kwd";
@@ -1981,28 +2250,28 @@ class CodeEditor {
1981
2250
  else if( this._mustHightlightWord( token, this.symbols ) )
1982
2251
  token_classname = "cm-sym";
1983
2252
 
1984
- else if( token.substr(0, 2) == singleLineCommentToken )
2253
+ else if( token.substr( 0, singleLineCommentToken.length ) == singleLineCommentToken )
1985
2254
  token_classname = "cm-com";
1986
2255
 
1987
- else if( usesBlockComments && token.substr(0, 2) == '/*' )
2256
+ else if( usesBlockComments && token.substr( 0, 2 ) == '/*' )
1988
2257
  token_classname = "cm-com";
1989
2258
 
1990
- else if( usesBlockComments && token.substr(token.length - 2) == '*/' )
2259
+ else if( usesBlockComments && token.substr( token.length - 2 ) == '*/' )
1991
2260
  token_classname = "cm-com";
1992
2261
 
1993
- else if( this.isNumber(token) || this.isNumber( token.replace(/[px]|[em]|%/g,'') ) )
2262
+ else if( this.isNumber( token ) || this.isNumber( token.replace(/[px]|[em]|%/g,'') ) )
1994
2263
  token_classname = "cm-dec";
1995
2264
 
1996
- else if( this.isCSSClass(token, prev, next) )
2265
+ else if( this._isCSSClass( token, prev, next ) )
1997
2266
  token_classname = "cm-kwd";
1998
2267
 
1999
- else if ( this.isType(token, prev, next) )
2268
+ else if ( this._isType( token, prev, next ) )
2000
2269
  token_classname = "cm-typ";
2001
2270
 
2002
- else if ( highlight == 'batch' && (token == '@' || prev == ':' || prev == '@') )
2271
+ else if ( highlight == 'batch' && ( token == '@' || prev == ':' || prev == '@' ) )
2003
2272
  token_classname = "cm-kwd";
2004
2273
 
2005
- else if ( highlight == 'cpp' && token.includes('#') ) // C++ preprocessor
2274
+ else if ( highlight == 'cpp' && token.includes( '#' ) ) // C++ preprocessor
2006
2275
  token_classname = "cm-ppc";
2007
2276
 
2008
2277
  else if ( highlight == 'cpp' && prev == '<' && (next == '>' || next == '*') ) // Defining template type in C++
@@ -2024,7 +2293,7 @@ class CodeEditor {
2024
2293
  // We finished constructing a string
2025
2294
  if( this._buildingString && ( this._stringEnded || isLastToken ) )
2026
2295
  {
2027
- token = this.getCurrentString();
2296
+ token = this._getCurrentString();
2028
2297
  token_classname = "cm-str";
2029
2298
  discardToken = false;
2030
2299
  }
@@ -2035,6 +2304,9 @@ class CodeEditor {
2035
2304
  if( discardToken )
2036
2305
  return "";
2037
2306
 
2307
+ token = token.replace( "<", "&lt;" );
2308
+ token = token.replace( ">", "&gt;" );
2309
+
2038
2310
  // No highlighting, no need to put it inside another span..
2039
2311
  if( !token_classname.length )
2040
2312
  return token;
@@ -2043,7 +2315,7 @@ class CodeEditor {
2043
2315
  }
2044
2316
  }
2045
2317
 
2046
- appendStringToken( token ) {
2318
+ _appendStringToken( token ) {
2047
2319
 
2048
2320
  if( !this._pendingString )
2049
2321
  this._pendingString = "";
@@ -2053,14 +2325,14 @@ class CodeEditor {
2053
2325
  return true;
2054
2326
  }
2055
2327
 
2056
- getCurrentString() {
2328
+ _getCurrentString() {
2057
2329
 
2058
2330
  const chars = this._pendingString;
2059
2331
  delete this._pendingString;
2060
2332
  return chars;
2061
2333
  }
2062
2334
 
2063
- isCSSClass( token, prev, next ) {
2335
+ _isCSSClass( token, prev, next ) {
2064
2336
  return this.highlight == 'CSS' && prev == '.';
2065
2337
  }
2066
2338
 
@@ -2077,7 +2349,7 @@ class CodeEditor {
2077
2349
  return token.length && token != ' ' && !Number.isNaN(+token);
2078
2350
  }
2079
2351
 
2080
- isType( token, prev, next ) {
2352
+ _isType( token, prev, next ) {
2081
2353
 
2082
2354
  // Common case
2083
2355
  if( this._mustHightlightWord( token, this.types ) )
@@ -2101,7 +2373,7 @@ class CodeEditor {
2101
2373
  }
2102
2374
  }
2103
2375
 
2104
- encloseSelectedWordWithKey( key, lidx, cursor ) {
2376
+ _encloseSelectedWordWithKey( key, lidx, cursor ) {
2105
2377
 
2106
2378
  if( !this.selection || (this.selection.fromY != this.selection.toY) )
2107
2379
  return false;
@@ -2213,12 +2485,13 @@ class CodeEditor {
2213
2485
  const post = code.slice( index + num_chars );
2214
2486
 
2215
2487
  this.code.lines = ( pre + post ).split( separator );
2216
- this.processLines();
2217
-
2488
+
2218
2489
  this.cursorToLine( cursor, this.selection.fromY, true );
2219
2490
  this.cursorToPosition( cursor, this.selection.fromX );
2220
2491
 
2221
2492
  this.endSelection();
2493
+
2494
+ this.processLines();
2222
2495
  this._refreshCodeInfo( cursor.line, cursor.position );
2223
2496
  }
2224
2497
 
@@ -2227,6 +2500,7 @@ class CodeEditor {
2227
2500
  this.selections.classList.remove('show');
2228
2501
  this.selections.innerHTML = "";
2229
2502
  delete this.selection;
2503
+ delete this._tripleClickSelection;
2230
2504
  }
2231
2505
 
2232
2506
  cursorToRight( key, cursor ) {
@@ -2236,14 +2510,15 @@ class CodeEditor {
2236
2510
  cursor._left += this.charWidth;
2237
2511
  cursor.style.left = "calc( " + cursor._left + "px + " + this.xPadding + " )";
2238
2512
  cursor.position++;
2513
+
2239
2514
  this.restartBlink();
2240
2515
  this._refreshCodeInfo( cursor.line, cursor.position );
2241
2516
 
2242
2517
  // Add horizontal scroll
2243
2518
 
2244
2519
  doAsync(() => {
2245
- var last_char = ((this.codeScroller.clientWidth + this.getScrollLeft()) / this.charWidth)|0;
2246
- if( cursor.position >= last_char )
2520
+ var viewportSizeX = ( this.codeScroller.clientWidth + this.getScrollLeft() ) - 48; // Gutter offset
2521
+ if( (cursor.position * this.charWidth) >= viewportSizeX )
2247
2522
  this.setScrollLeft( this.getScrollLeft() + this.charWidth );
2248
2523
  });
2249
2524
  }
@@ -2253,18 +2528,18 @@ class CodeEditor {
2253
2528
  if(!key) return;
2254
2529
  cursor = cursor ?? this.cursors.children[ 0 ];
2255
2530
  cursor._left -= this.charWidth;
2256
- cursor._left = Math.max(cursor._left, 0);
2531
+ cursor._left = Math.max( cursor._left, 0 );
2257
2532
  cursor.style.left = "calc( " + cursor._left + "px + " + this.xPadding + " )";
2258
2533
  cursor.position--;
2259
- cursor.position = Math.max(cursor.position, 0);
2534
+ cursor.position = Math.max( cursor.position, 0 );
2260
2535
  this.restartBlink();
2261
2536
  this._refreshCodeInfo( cursor.line, cursor.position );
2262
2537
 
2263
2538
  // Add horizontal scroll
2264
2539
 
2265
2540
  doAsync(() => {
2266
- var first_char = (this.getScrollLeft() / this.charWidth)|0;
2267
- if( (cursor.position - 1) < first_char )
2541
+ var viewportSizeX = this.getScrollLeft(); // Gutter offset
2542
+ if( ( ( cursor.position - 1 ) * this.charWidth ) < viewportSizeX )
2268
2543
  this.setScrollLeft( this.getScrollLeft() - this.charWidth );
2269
2544
  });
2270
2545
  }
@@ -2283,7 +2558,7 @@ class CodeEditor {
2283
2558
  this._refreshCodeInfo( cursor.line, cursor.position );
2284
2559
 
2285
2560
  doAsync(() => {
2286
- var first_line = (this.getScrollTop() / this.lineHeight)|0;
2561
+ var first_line = ( this.getScrollTop() / this.lineHeight )|0;
2287
2562
  if( (cursor.line - 1) < first_line )
2288
2563
  this.setScrollTop( this.getScrollTop() - this.lineHeight );
2289
2564
  });
@@ -2302,7 +2577,7 @@ class CodeEditor {
2302
2577
  this._refreshCodeInfo( cursor.line, cursor.position );
2303
2578
 
2304
2579
  doAsync(() => {
2305
- var last_line = ((this.codeScroller.offsetHeight + this.getScrollTop()) / this.lineHeight)|0;
2580
+ var last_line = ( ( this.codeScroller.offsetHeight + this.getScrollTop() ) / this.lineHeight )|0;
2306
2581
  if( cursor.line >= last_line )
2307
2582
  this.setScrollTop( this.getScrollTop() + this.lineHeight );
2308
2583
  });
@@ -2344,7 +2619,7 @@ class CodeEditor {
2344
2619
 
2345
2620
  cursor = cursor ?? this.cursors.children[ 0 ];
2346
2621
  cursor.line = state.line ?? 0;
2347
- cursor.position = state.charPos ?? 0;
2622
+ cursor.position = state.position ?? 0;
2348
2623
 
2349
2624
  cursor._left = state.left ?? 0;
2350
2625
  cursor.style.left = "calc(" + (cursor._left - this.getScrollLeft()) + "px + " + this.xPadding + ")";
@@ -2414,9 +2689,32 @@ class CodeEditor {
2414
2689
  this.setScrollBarValue( 'vertical' );
2415
2690
  }
2416
2691
 
2417
- resizeScrollBars( numViewportLines ) {
2692
+ resize( pMaxLength ) {
2693
+
2694
+ setTimeout( () => {
2695
+
2696
+ // Update max viewport
2697
+ const maxLineLength = pMaxLength ?? this.getMaxLineLength();
2698
+ const scrollWidth = maxLineLength * this.charWidth;
2699
+ const scrollHeight = this.code.lines.length * this.lineHeight;
2700
+
2701
+ this._lastMaxLineLength = maxLineLength;
2702
+
2703
+ this.codeSizer.style.minWidth = scrollWidth + "px";
2704
+ this.codeSizer.style.minHeight = scrollHeight + "px";
2705
+
2706
+ this.resizeScrollBars();
2707
+
2708
+ // console.warn("Resize editor viewport");
2709
+
2710
+ }, 10 );
2711
+ }
2712
+
2713
+ resizeScrollBars() {
2714
+
2715
+ const totalLinesInViewport = ((this.codeScroller.offsetHeight - 36) / this.lineHeight)|0;
2418
2716
 
2419
- if( numViewportLines > this.code.lines.length )
2717
+ if( totalLinesInViewport > this.code.lines.length )
2420
2718
  {
2421
2719
  this.codeScroller.classList.remove( 'with-vscrollbar' );
2422
2720
  this.vScrollbar.root.classList.add( 'scrollbar-unused' );
@@ -2425,13 +2723,12 @@ class CodeEditor {
2425
2723
  {
2426
2724
  this.codeScroller.classList.add( 'with-vscrollbar' );
2427
2725
  this.vScrollbar.root.classList.remove( 'scrollbar-unused' );
2428
- this.vScrollbar.thumb.size = (numViewportLines / this.code.lines.length);
2726
+ this.vScrollbar.thumb.size = (totalLinesInViewport / this.code.lines.length);
2429
2727
  this.vScrollbar.thumb.style.height = (this.vScrollbar.thumb.size * 100.0) + "%";
2430
2728
  }
2431
2729
 
2432
2730
  const numViewportChars = Math.floor( this.codeScroller.clientWidth / this.charWidth );
2433
- const line_lengths = this.code.lines.map( value => value.length );
2434
- const maxLineLength = Math.max(...line_lengths);
2731
+ const maxLineLength = this._lastMaxLineLength;
2435
2732
 
2436
2733
  if( numViewportChars > maxLineLength )
2437
2734
  {
@@ -2625,11 +2922,11 @@ class CodeEditor {
2625
2922
 
2626
2923
  // Add language special keys...
2627
2924
  suggestions = suggestions.concat(
2628
- Object.keys( this.builtin[ this.highlight ] ) ?? [],
2629
- Object.keys( this.keywords[ this.highlight ] ) ?? [],
2630
- Object.keys( this.statementsAndDeclarations[ this.highlight ] ) ?? [],
2631
- Object.keys( this.types[ this.highlight ] ) ?? [],
2632
- Object.keys( this.utils[ this.highlight ] ) ?? []
2925
+ Object.keys( this.builtin[ this.highlight ] ?? {} ),
2926
+ Object.keys( this.keywords[ this.highlight ] ?? {} ),
2927
+ Object.keys( this.statementsAndDeclarations[ this.highlight ] ?? {} ),
2928
+ Object.keys( this.types[ this.highlight ] ?? {} ),
2929
+ Object.keys( this.utils[ this.highlight ] ?? {} )
2633
2930
  );
2634
2931
 
2635
2932
  // Add words in current tab plus remove current word