lexgui 0.1.14 → 0.1.16

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.
@@ -29,6 +29,10 @@ function firstNonspaceIndex(str) {
29
29
  return str.search(/\S|$/);
30
30
  }
31
31
 
32
+ function deleteElement(el) {
33
+ if(el) el.remove();
34
+ }
35
+
32
36
  let ASYNC_ENABLED = true;
33
37
 
34
38
  function doAsync( fn, ms ) {
@@ -102,10 +106,31 @@ class CodeEditor {
102
106
 
103
107
  constructor( area, options = {} ) {
104
108
 
109
+ // var a = [];
110
+
111
+ // var map = {};
112
+
113
+ // for( var i = 0; i < 1000000; ++i )
114
+ // map[i] = 1;
115
+
116
+ // const start = performance.now();
117
+
118
+ // for( var i = 0; i < 3000; ++i ) {
119
+ // const b = map[Math.floor( Math.random() * 1000000 )];
120
+ // }
121
+
122
+ // console.log( performance.now() - start );
123
+
124
+
125
+ // debugger;
126
+
127
+
105
128
  window.editor = this;
106
129
 
107
130
  CodeEditor.__instances.push( this );
108
131
 
132
+ var that = this;
133
+
109
134
  this.base_area = area;
110
135
  this.area = new LX.Area( { className: "lexcodeeditor", height: "auto", no_append: true } );
111
136
 
@@ -142,6 +167,9 @@ class CodeEditor {
142
167
  this.root.addEventListener( 'click', this.processMouse.bind(this) );
143
168
  this.root.addEventListener( 'contextmenu', this.processMouse.bind(this) );
144
169
 
170
+ // Take into account the scrollbar..
171
+ this.tabs.area.root.classList.add( 'codetabsarea' );
172
+
145
173
  // Cursors and selection
146
174
 
147
175
  this.cursors = document.createElement('div');
@@ -170,6 +198,96 @@ class CodeEditor {
170
198
  this.cursors.appendChild(cursor);
171
199
  }
172
200
 
201
+ // Add custom vertical scroll bar
202
+ {
203
+ var scrollbar = document.createElement('div');
204
+ scrollbar.className = "lexcodescrollbar";
205
+ this.scrollbar = scrollbar;
206
+ area.attach(this.scrollbar);
207
+
208
+ var scrollbarThumb = document.createElement('div');
209
+ this.scrollbarThumb = scrollbarThumb;
210
+ this.scrollbarThumb._top = 0;
211
+ scrollbar.appendChild(scrollbarThumb);
212
+
213
+ this.scrollbarThumb.addEventListener("mousedown", inner_mousedown);
214
+
215
+ var last_pos = 0;
216
+
217
+ function inner_mousedown(e)
218
+ {
219
+ var doc = that.root.ownerDocument;
220
+ doc.addEventListener("mousemove",inner_mousemove);
221
+ doc.addEventListener("mouseup",inner_mouseup);
222
+ last_pos = e.y;
223
+ e.stopPropagation();
224
+ e.preventDefault();
225
+ }
226
+
227
+ function inner_mousemove(e)
228
+ {
229
+ var dt = (last_pos - e.y);
230
+
231
+ that.applyVerticalScrollFromScrollBar( that.scrollbarThumb._top - dt )
232
+
233
+ last_pos = e.y;
234
+ e.stopPropagation();
235
+ e.preventDefault();
236
+ }
237
+
238
+ function inner_mouseup(e)
239
+ {
240
+ var doc = that.root.ownerDocument;
241
+ doc.removeEventListener("mousemove", inner_mousemove);
242
+ doc.removeEventListener("mouseup", inner_mouseup);
243
+ }
244
+ }
245
+
246
+ // Add custom horizontal scroll bar
247
+ {
248
+ var hScrollbar = document.createElement('div');
249
+ hScrollbar.className = "lexcodescrollbar horizontal";
250
+ this.hScrollbar = hScrollbar;
251
+ area.attach(this.hScrollbar);
252
+
253
+ var hScrollbarThumb = document.createElement('div');
254
+ this.hScrollbarThumb = hScrollbarThumb;
255
+ this.hScrollbarThumb._left = 0;
256
+ hScrollbar.appendChild(hScrollbarThumb);
257
+
258
+ this.hScrollbarThumb.addEventListener("mousedown", inner_mousedown);
259
+
260
+ var last_pos = 0;
261
+
262
+ function inner_mousedown(e)
263
+ {
264
+ var doc = that.root.ownerDocument;
265
+ doc.addEventListener("mousemove",inner_mousemove);
266
+ doc.addEventListener("mouseup",inner_mouseup);
267
+ last_pos = e.x;
268
+ e.stopPropagation();
269
+ e.preventDefault();
270
+ }
271
+
272
+ function inner_mousemove(e)
273
+ {
274
+ var dt = (last_pos - e.x);
275
+
276
+ that.applyHorizontalScrollFromScrollBar( that.hScrollbarThumb._left - dt )
277
+
278
+ last_pos = e.x;
279
+ e.stopPropagation();
280
+ e.preventDefault();
281
+ }
282
+
283
+ function inner_mouseup(e)
284
+ {
285
+ var doc = that.root.ownerDocument;
286
+ doc.removeEventListener("mousemove", inner_mousemove);
287
+ doc.removeEventListener("mouseup", inner_mouseup);
288
+ }
289
+ }
290
+
173
291
  // Add autocomplete box
174
292
  {
175
293
  var box = document.createElement('div');
@@ -199,6 +317,7 @@ class CodeEditor {
199
317
  this.tabSpaces = 4;
200
318
  this.maxUndoSteps = 16;
201
319
  this.lineHeight = 20;
320
+ this.defaultSingleLineCommentToken = "//";
202
321
  this.charWidth = 8; //this.measureChar();
203
322
  this._lastTime = null;
204
323
 
@@ -210,28 +329,48 @@ class CodeEditor {
210
329
  "[": "]"
211
330
  };
212
331
 
332
+ this.stringKeys = { // adding @ because some words are always true in (e.g. constructor..)
333
+ "@\"": "\"",
334
+ "@'": "'"
335
+ };
336
+
213
337
  // Scan tokens..
214
338
  setInterval( this.scanWordSuggestions.bind(this), 2000 );
215
339
 
216
- this.languages = [
217
- 'Plain Text', 'JavaScript', 'C++', 'CSS', 'GLSL', 'WGSL', 'JSON', 'XML', 'Python'
218
- ];
340
+ this.languages = {
341
+ 'Plain Text': { },
342
+ 'JavaScript': { },
343
+ 'C++': { },
344
+ 'CSS': { },
345
+ 'GLSL': { },
346
+ 'WGSL': { },
347
+ 'JSON': { },
348
+ 'XML': { },
349
+ 'Python': { },
350
+ 'Batch': { blockComments: false, singleLineCommentToken: '::' }
351
+ };
352
+
219
353
  this.specialKeys = [
220
354
  'Backspace', 'Enter', 'ArrowUp', 'ArrowDown',
221
355
  'ArrowRight', 'ArrowLeft', 'Delete', 'Home',
222
356
  'End', 'Tab', 'Escape'
223
357
  ];
358
+
224
359
  this.keywords = {
225
360
  'JavaScript': ['var', 'let', 'const', 'this', 'in', 'of', 'true', 'false', 'new', 'function', 'NaN', 'static', 'class', 'constructor', 'null', 'typeof', 'debugger', 'abstract',
226
361
  'arguments', 'extends', 'instanceof'],
227
- 'C++': ['int', 'float', 'bool', 'const', 'static_cast', 'dynamic_cast', 'new', 'delete', 'void', 'true', 'false', 'auto', 'struct', 'typedef'],
362
+ 'C++': ['int', 'float', 'double', 'bool', 'char', 'wchar_t', 'const', 'static_cast', 'dynamic_cast', 'new', 'delete', 'void', 'true', 'false', 'auto', 'struct', 'typedef', 'nullptr',
363
+ 'NULL', 'unsigned'],
364
+ 'JSON': ['true', 'false'],
228
365
  'GLSL': ['true', 'false', 'function', 'int', 'float', 'vec2', 'vec3', 'vec4', 'mat2x2', 'mat3x3', 'mat4x4', 'struct'],
229
366
  'CSS': ['body', 'html', 'canvas', 'div', 'input', 'span', '.'],
230
367
  'WGSL': ['var', 'let', 'true', 'false', 'fn', 'bool', 'u32', 'i32', 'f16', 'f32', 'vec2f', 'vec3f', 'vec4f', 'mat2x2f', 'mat3x3f', 'mat4x4f', 'array', 'atomic', 'struct',
231
368
  'sampler', 'sampler_comparison', 'texture_depth_2d', 'texture_depth_2d_array', 'texture_depth_cube', 'texture_depth_cube_array', 'texture_depth_multisampled_2d',
232
369
  'texture_external', 'texture_1d', 'texture_2d', 'texture_2d_array', 'texture_3d', 'texture_cube', 'texture_cube_array', 'texture_storage_1d', 'texture_storage_2d',
233
370
  'texture_storage_2d_array', 'texture_storage_3d'],
234
- 'Python': ['False', 'def', 'None', 'True', 'in', 'is', 'and', 'lambda', 'nonlocal', 'not', 'or']
371
+ 'Python': ['False', 'def', 'None', 'True', 'in', 'is', 'and', 'lambda', 'nonlocal', 'not', 'or'],
372
+ 'Batch': ['set', 'SET', 'echo', 'ECHO', 'off', 'OFF', 'del', 'DEL', 'defined', 'DEFINED', 'setlocal', 'SETLOCAL', 'enabledelayedexpansion', 'ENABLEDELAYEDEXPANSION', 'driverquery',
373
+ 'DRIVERQUERY', 'print', 'PRINT']
235
374
  };
236
375
  this.utils = { // These ones don't have hightlight, used as suggestions to autocomplete only...
237
376
  'JavaScript': ['querySelector', 'body', 'addEventListener', 'removeEventListener', 'remove', 'sort', 'keys', 'filter', 'isNaN', 'parseFloat', 'parseInt', 'EPSILON', 'isFinite',
@@ -248,7 +387,8 @@ class CodeEditor {
248
387
  'Python': ['int', 'type', 'float', 'map', 'list', 'ArithmeticError', 'AssertionError', 'AttributeError', 'Exception', 'EOFError', 'FloatingPointError', 'GeneratorExit',
249
388
  'ImportError', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'NotImplementedError', 'OSError',
250
389
  'OverflowError', 'ReferenceError', 'RuntimeError', 'StopIteration', 'SyntaxError', 'TabError', 'SystemError', 'SystemExit', 'TypeError', 'UnboundLocalError',
251
- 'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError', 'ValueError', 'ZeroDivisionError' ]
390
+ 'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError', 'ValueError', 'ZeroDivisionError' ],
391
+ 'C++': ['uint8_t', 'uint16_t', 'uint32_t']
252
392
  };
253
393
  this.builtin = {
254
394
  'JavaScript': ['document', 'console', 'window', 'navigator', 'performance'],
@@ -257,21 +397,33 @@ class CodeEditor {
257
397
  };
258
398
  this.statementsAndDeclarations = {
259
399
  'JavaScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await'],
260
- 'C++': ['std', 'for', 'if', 'else', 'return', 'continue', 'break', 'case', 'switch', 'while', 'glm'],
400
+ 'C++': ['std', 'for', 'if', 'else', 'return', 'continue', 'break', 'case', 'switch', 'while', 'glm', 'spdlog'],
261
401
  'GLSL': ['for', 'if', 'else', 'return', 'continue', 'break'],
262
402
  'WGSL': ['const','for', 'if', 'else', 'return', 'continue', 'break', 'storage', 'read', 'uniform'],
263
- 'Python': ['if', 'raise', 'del', 'import', 'return', 'elif', 'try', 'else', 'while', 'as', 'except', 'with', 'assert', 'finally', 'yield', 'break', 'for', 'class', 'continue', 'global', 'pass']
403
+ 'Python': ['if', 'raise', 'del', 'import', 'return', 'elif', 'try', 'else', 'while', 'as', 'except', 'with', 'assert', 'finally', 'yield', 'break', 'for', 'class', 'continue',
404
+ 'global', 'pass'],
405
+ 'Batch': ['if', 'IF', 'for', 'FOR', 'in', 'IN', 'do', 'DO', 'call', 'CALL', 'goto', 'GOTO', 'exit', 'EXIT']
264
406
  };
265
407
  this.symbols = {
266
408
  'JavaScript': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '??'],
267
- 'C++': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '::'],
409
+ 'C++': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '::', '*', '-', '+'],
268
410
  'JSON': ['[', ']', '{', '}', '(', ')'],
269
411
  'GLSL': ['[', ']', '{', '}', '(', ')'],
270
412
  'WGSL': ['[', ']', '{', '}', '(', ')', '->'],
271
413
  'CSS': ['{', '}', '(', ')', '*'],
272
- 'Python': ['<', '>', '[', ']', '(', ')', '=']
414
+ 'Python': ['<', '>', '[', ']', '(', ')', '='],
415
+ 'Batch': ['[', ']', '(', ')', '%'],
273
416
  };
274
417
 
418
+ // Convert reserved word arrays to maps so we can search tokens faster
419
+
420
+ for( let lang in this.keywords ) this.keywords[lang] = this.keywords[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
421
+ for( let lang in this.utils ) this.utils[lang] = this.utils[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
422
+ for( let lang in this.types ) this.types[lang] = this.types[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
423
+ for( let lang in this.builtin ) this.builtin[lang] = this.builtin[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
424
+ for( let lang in this.statementsAndDeclarations ) this.statementsAndDeclarations[lang] = this.statementsAndDeclarations[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
425
+ for( let lang in this.symbols ) this.symbols[lang] = this.symbols[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
426
+
275
427
  // Action keys
276
428
 
277
429
  this.action('Escape', false, ( ln, cursor, e ) => {
@@ -355,7 +507,7 @@ class CodeEditor {
355
507
  this.resetCursorPos( CodeEditor.CURSOR_LEFT );
356
508
  if(idx > 0) this.cursorToString(cursor, prestring);
357
509
  this._refreshCodeInfo(cursor.line, cursor.position);
358
- this.code.scrollLeft = 0;
510
+ this.setScrollLeft( 0 );
359
511
 
360
512
  if( e.shiftKey && !e.cancelShift )
361
513
  {
@@ -366,7 +518,7 @@ class CodeEditor {
366
518
  this.startSelection(cursor);
367
519
  var string = this.code.lines[ln].substring(idx, last_pos);
368
520
  this.selection.selectInline(idx, cursor.line, this.measureString(string));
369
- } else
521
+ } else if( !e.keepSelection )
370
522
  this.endSelection();
371
523
  });
372
524
 
@@ -384,8 +536,8 @@ class CodeEditor {
384
536
  this.resetCursorPos( CodeEditor.CURSOR_LEFT );
385
537
  this.cursorToString( cursor, this.code.lines[ln] );
386
538
 
387
- const last_char = ((this.code.scrollLeft + this.code.clientWidth) / this.charWidth)|0;
388
- this.code.scrollLeft = cursor.position >= last_char ? (cursor.position - last_char) * this.charWidth : 0;
539
+ const last_char = (this.code.clientWidth / this.charWidth)|0;
540
+ this.setScrollLeft( cursor.position >= last_char ? (cursor.position - last_char) * this.charWidth : 0 );
389
541
  });
390
542
 
391
543
  this.action('Enter', true, ( ln, cursor, e ) => {
@@ -442,7 +594,7 @@ class CodeEditor {
442
594
 
443
595
  var letter = this.getCharAtPos( cursor );
444
596
  if(!letter) {
445
- this.selection.toX = (this.code.lines[cursor.line].length - 1);
597
+ this.selection.toX = this.code.lines[cursor.line].length;
446
598
  this.cursorToPosition(cursor, this.selection.toX);
447
599
  }
448
600
 
@@ -504,7 +656,7 @@ class CodeEditor {
504
656
 
505
657
  if(e.metaKey) { // Apple devices (Command)
506
658
  e.preventDefault();
507
- this.actions[ 'Home' ].callback( ln, cursor );
659
+ this.actions[ 'Home' ].callback( ln, cursor, e );
508
660
  }
509
661
  else if(e.ctrlKey) {
510
662
  // Get next word
@@ -548,13 +700,15 @@ class CodeEditor {
548
700
  }
549
701
  }
550
702
  else if( cursor.line > 0 ) {
551
-
703
+
704
+ if( e.shiftKey ) {
705
+ if(!this.selection) this.startSelection(cursor);
706
+ }
707
+
552
708
  this.lineUp( cursor );
553
- this.resetCursorPos( CodeEditor.CURSOR_LEFT );
554
- this.cursorToPosition( cursor, this.code.lines[cursor.line].length );
709
+ this.actions[ 'End' ].callback( cursor.line, cursor, e );
555
710
 
556
711
  if( e.shiftKey ) {
557
- if(!this.selection) this.startSelection(cursor);
558
712
  this.selection.toX = cursor.position;
559
713
  this.selection.toY--;
560
714
  this.processSelection(null, true);
@@ -613,17 +767,22 @@ class CodeEditor {
613
767
  }
614
768
  else if( this.code.lines[ cursor.line + 1 ] !== undefined ) {
615
769
 
770
+ if( e.shiftKey ) {
771
+ if(!this.selection) this.startSelection(cursor);
772
+ e.cancelShift = true;
773
+ e.keepSelection = true;
774
+ }
775
+
616
776
  this.lineDown( cursor );
617
- e.cancelShift = true;
618
777
  this.actions['Home'].callback(cursor.line, cursor, e);
619
- this.hideAutoCompleteBox();
620
-
778
+
621
779
  if( e.shiftKey ) {
622
- if(!this.selection) this.startSelection(cursor);
623
780
  this.selection.toX = cursor.position;
624
781
  this.selection.toY++;
625
782
  this.processSelection(null, true);
626
783
  }
784
+
785
+ this.hideAutoCompleteBox();
627
786
  }
628
787
  }
629
788
  });
@@ -652,7 +811,7 @@ class CodeEditor {
652
811
  }
653
812
 
654
813
  // This can be used to empty all text...
655
- setText( text = "" ) {
814
+ setText( text = "", lang ) {
656
815
 
657
816
  let new_lines = text.split('\n');
658
817
  this.code.lines = [].concat(new_lines);
@@ -663,6 +822,11 @@ class CodeEditor {
663
822
  this.cursorToLine(cursor, new_lines.length); // Already substracted 1
664
823
  this.cursorToPosition(cursor, lastLine.length);
665
824
  this.processLines();
825
+
826
+ if( lang )
827
+ {
828
+ this._changeLanguage( lang );
829
+ }
666
830
  }
667
831
 
668
832
  appendText( text ) {
@@ -731,11 +895,13 @@ class CodeEditor {
731
895
  loadFile( file ) {
732
896
 
733
897
  const inner_add_tab = ( text, name, title ) => {
734
- this.addTab(name, true, title);
735
- text = text.replaceAll('\r', '');
736
- this.code.lines = text.split('\n');
737
- this.processLines();
738
- this._refreshCodeInfo();
898
+ const existing = this.addTab(name, true, title);
899
+ if( !existing )
900
+ {
901
+ text = text.replaceAll('\r', '');
902
+ this.code.lines = text.split('\n');
903
+ this._changeLanguageFromExtension( LX.getExtension(name) );
904
+ }
739
905
  };
740
906
 
741
907
  if(file.constructor == String)
@@ -744,7 +910,6 @@ class CodeEditor {
744
910
  LX.request({ url: filename, success: text => {
745
911
 
746
912
  const name = filename.substring(filename.lastIndexOf('/') + 1);
747
- this._changeLanguageFromExtension( LX.getExtension(name) );
748
913
  inner_add_tab( text, name, filename );
749
914
  } });
750
915
  }
@@ -753,12 +918,10 @@ class CodeEditor {
753
918
  const fr = new FileReader();
754
919
  fr.readAsText( file );
755
920
  fr.onload = e => {
756
- this._changeLanguageFromExtension( LX.getExtension(file.name) );
757
921
  const text = e.currentTarget.result;
758
922
  inner_add_tab( text, file.name );
759
923
  };
760
924
  }
761
-
762
925
  }
763
926
 
764
927
  _addUndoStep( cursor ) {
@@ -773,6 +936,8 @@ class CodeEditor {
773
936
  }
774
937
 
775
938
  _changeLanguage( lang ) {
939
+
940
+ this.code.lang = lang;
776
941
  this.highlight = lang;
777
942
  this._refreshCodeInfo();
778
943
  this.processLines();
@@ -780,6 +945,9 @@ class CodeEditor {
780
945
 
781
946
  _changeLanguageFromExtension( ext ) {
782
947
 
948
+ if( !ext )
949
+ return this._changeLanguage( this.code.lang );
950
+
783
951
  switch(ext.toLowerCase())
784
952
  {
785
953
  case 'js': return this._changeLanguage('JavaScript');
@@ -791,6 +959,7 @@ class CodeEditor {
791
959
  case 'xml': return this._changeLanguage('XML');
792
960
  case 'wgsl': return this._changeLanguage('WGSL');
793
961
  case 'py': return this._changeLanguage('Python');
962
+ case 'bat': return this._changeLanguage('Batch');
794
963
  case 'txt':
795
964
  default:
796
965
  this._changeLanguage('Plain Text');
@@ -815,7 +984,7 @@ class CodeEditor {
815
984
  panel.addLabel("Col " + panel.col, { width: "64px" });
816
985
  panel.addButton("<b>{ }</b>", this.highlight, (value, event) => {
817
986
  LX.addContextMenu( "Language", event, m => {
818
- for( const lang of this.languages )
987
+ for( const lang of Object.keys(this.languages) )
819
988
  m.add( lang, this._changeLanguage.bind(this) );
820
989
  });
821
990
  }, { width: "25%", nameWidth: "15%" });
@@ -856,48 +1025,48 @@ class CodeEditor {
856
1025
  if(this.openedTabs[name])
857
1026
  {
858
1027
  this.tabs.select( this.code.tabName );
859
- return;
1028
+ return true;
860
1029
  }
861
1030
 
862
1031
  // Create code content
863
1032
  let code = document.createElement('div');
864
1033
  code.className = 'code';
865
1034
  code.lines = [""];
1035
+ code.lang = "Plain Text";
866
1036
  code.cursorState = {};
867
1037
  code.undoSteps = [];
868
1038
  code.tabName = name;
869
1039
  code.title = title ?? name;
870
- code.tokens = {};
1040
+ code.tokens = {};
1041
+ code.customScroll = new LX.vec2;
871
1042
 
872
1043
  code.addEventListener('dragenter', function(e) {
873
1044
  e.preventDefault();
874
- this.classList.add('dragging');
1045
+ this.parentElement.classList.add('dragging');
875
1046
  });
876
1047
  code.addEventListener('dragleave', function(e) {
877
1048
  e.preventDefault();
878
- this.classList.remove('dragging');
1049
+ this.parentElement.remove('dragging');
879
1050
  });
880
1051
  code.addEventListener('drop', (e) => {
881
1052
  e.preventDefault();
882
- code.classList.remove('dragging');
1053
+ code.parentElement.classList.remove('dragging');
883
1054
  for( let i = 0; i < e.dataTransfer.files.length; ++i )
884
1055
  this.loadFile( e.dataTransfer.files[i] );
885
1056
  });
1057
+ code.addEventListener('wheel', (e) => {
886
1058
 
887
- code.addEventListener('scroll', (e) => {
888
- this.gutter.scrollTop = code.scrollTop;
889
- this.gutter.scrollLeft = code.scrollLeft;
1059
+ // Get scroll data
890
1060
 
891
- // Update cursor
892
- var cursor = this.cursors.children[0];
893
- cursor.style.top = (cursor._top - code.scrollTop) + "px";
894
- cursor.style.left = "calc( " + (cursor._left - code.scrollLeft) + "px + " + this.xPadding + ")";
1061
+ const dX = (e.deltaY > 0.0 ? 1.0 : -1.0) * 20.0 * ( e.shiftKey ? 1.0 : 0.0 );
1062
+ const dY = (e.deltaY > 0.0 ? 1.0 : -1.0) * 40.0 * ( e.shiftKey ? 0.0 : 1.0 );
895
1063
 
896
- // Update selection
897
- for( let s of this.selections.childNodes ) {
898
- s.style.top = (s._top - code.scrollTop) + "px";
899
- s.style.left = "calc( " + (s._left - code.scrollLeft) + "px + " + this.xPadding + ")";
900
- }
1064
+ var new_scroll = code.customScroll.add( new LX.vec2( dX, dY ), new LX.vec2() );
1065
+
1066
+ // Update state
1067
+
1068
+ if( new_scroll.x != this.getScrollLeft()) this.setScrollLeft( new_scroll.x );
1069
+ if( new_scroll.y != this.getScrollTop()) this.setScrollTop( new_scroll.y );
901
1070
  });
902
1071
 
903
1072
  this.openedTabs[name] = code;
@@ -919,13 +1088,14 @@ class CodeEditor {
919
1088
  this._refreshCodeInfo(cursor.line, cursor.position);
920
1089
 
921
1090
  // Restore scroll
922
- this.gutter.scrollTop = this.code.scrollTop;
923
- this.gutter.scrollLeft = this.code.scrollLeft;
1091
+ this.gutter.scrollLeft = this.getScrollLeft();
1092
+ this.gutter.scrollTop = this.getScrollTop();
924
1093
  }});
925
1094
 
926
1095
  this.endSelection();
927
1096
 
928
- if(selected){
1097
+ if( selected )
1098
+ {
929
1099
  this.code = code;
930
1100
  this.resetCursorPos(CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP);
931
1101
  this.processLines();
@@ -963,7 +1133,7 @@ class CodeEditor {
963
1133
 
964
1134
  var cursor = this.cursors.children[0];
965
1135
  var code_rect = this.code.getBoundingClientRect();
966
- var mouse_pos = [(e.clientX - code_rect.x) + this.getScrollLeft(), (e.clientY - code_rect.y) + this.getScrollTop()];
1136
+ var mouse_pos = [(e.clientX - code_rect.x), (e.clientY - code_rect.y)];
967
1137
 
968
1138
  // Discard out of lines click...
969
1139
  if( e.type != 'contextmenu' )
@@ -974,9 +1144,6 @@ class CodeEditor {
974
1144
 
975
1145
  if( e.type == 'mousedown' )
976
1146
  {
977
- if( mouse_pos[0] > this.code.scrollWidth || mouse_pos[1] > this.code.scrollHeight )
978
- return; // Scrollbar click
979
-
980
1147
  // Left click only...
981
1148
  if( e.button === 2 )
982
1149
  {
@@ -1067,7 +1234,7 @@ class CodeEditor {
1067
1234
  processClick(e, skip_refresh = false) {
1068
1235
 
1069
1236
  var code_rect = this.code.getBoundingClientRect();
1070
- var position = [(e.clientX - code_rect.x) + this.getScrollLeft(), (e.clientY - code_rect.y) + this.getScrollTop()];
1237
+ var position = [(e.clientX - code_rect.x), (e.clientY - code_rect.y)];
1071
1238
  var ln = (position[1] / this.lineHeight)|0;
1072
1239
 
1073
1240
  if(this.code.lines[ln] == undefined) return;
@@ -1079,7 +1246,6 @@ class CodeEditor {
1079
1246
 
1080
1247
  var ch = (position[0] / this.charWidth)|0;
1081
1248
  var string = this.code.lines[ln].slice(0, ch);
1082
- // this.cursorToString(cursor, string);
1083
1249
  this.cursorToPosition(cursor, string.length);
1084
1250
 
1085
1251
  this.hideAutoCompleteBox();
@@ -1115,7 +1281,7 @@ class CodeEditor {
1115
1281
  if( deltaY >= 0 )
1116
1282
  {
1117
1283
  while( deltaY < (this.selections.childElementCount - 1) )
1118
- this.selections.lastChild.remove();
1284
+ deleteElement( this.selections.lastChild );
1119
1285
 
1120
1286
  for(let i = fromY; i <= toY; i++){
1121
1287
 
@@ -1158,7 +1324,7 @@ class CodeEditor {
1158
1324
  else // Selection goes up...
1159
1325
  {
1160
1326
  while( Math.abs(deltaY) < (this.selections.childElementCount - 1) )
1161
- this.selections.firstChild.remove();
1327
+ deleteElement( this.selections.firstChild );
1162
1328
 
1163
1329
  for(let i = toY; i <= fromY; i++){
1164
1330
 
@@ -1470,216 +1636,299 @@ class CodeEditor {
1470
1636
  }
1471
1637
  }
1472
1638
 
1473
- processLines( from ) {
1639
+ processLines( from__legacy ) {
1474
1640
 
1475
- // const start = performance.now();
1641
+ const start = performance.now();
1642
+
1643
+ var gutter_html = "";
1644
+ var code_html = "";
1645
+
1646
+ this.resizeScrollBars();
1476
1647
 
1477
- this.gutter.innerHTML = "";
1478
1648
  this.code.innerHTML = "";
1649
+ this.gutter.innerHTML = "";
1479
1650
 
1480
- for( let i = 0; i < this.code.lines.length; ++i )
1651
+ // Get info about lines in viewport
1652
+ const margin = 20;
1653
+ const firstLineInViewport = (this.getScrollTop() / this.lineHeight)|0;
1654
+ const totalLinesInViewport = ((this.code.parentElement.offsetHeight - 36) / this.lineHeight)|0;
1655
+ const viewportRange = new LX.vec2(
1656
+ Math.max( firstLineInViewport - margin, 0 ),
1657
+ Math.min( firstLineInViewport + totalLinesInViewport + margin, this.code.lines.length )
1658
+ );
1659
+
1660
+ for( let i = viewportRange.x; i < viewportRange.y; ++i )
1481
1661
  {
1482
- this.processLine( i, true );
1662
+ gutter_html += "<span>" + (i + 1) + "</span>";
1663
+ code_html += this.processLine( i, true );
1483
1664
  }
1484
1665
 
1485
- // console.log( performance.now() - start );
1666
+ this.code.innerHTML = code_html;
1667
+ this.gutter.innerHTML = gutter_html;
1668
+
1669
+ console.log( "Num lines processed: " + (viewportRange.y - viewportRange.x), performance.now() - start );
1486
1670
  }
1487
1671
 
1488
1672
  processLine( linenum, force ) {
1489
1673
 
1490
- // if(!force)
1491
- // {
1492
- // this.processLines();
1493
- // return;
1494
- // }
1495
-
1496
- delete this._building_string; // multi-line strings not supported by now
1674
+ delete this._buildingString; // multi-line strings not supported by now
1497
1675
 
1498
1676
  // It's allowed to process only 1 line to optimize
1499
1677
  let linestring = this.code.lines[ linenum ];
1500
1678
 
1501
- var pre = document.createElement('pre');
1502
- pre.dataset['linenum'] = linenum;
1503
-
1504
- // Line gutter
1505
- var linenumspan = document.createElement('span');
1506
- linenumspan.innerHTML = (linenum + 1);
1507
-
1508
- if( force )
1509
- {
1510
- this.gutter.appendChild(linenumspan);
1511
- this.code.appendChild( pre );
1512
- } else
1679
+ if( !force )
1513
1680
  {
1514
- this.gutter.childNodes[ linenum ].remove();
1515
- this.code.childNodes[ linenum ].remove();
1516
- this.gutter.insertChildAtIndex( linenumspan, linenum );
1681
+ var pre = document.createElement('pre');
1682
+
1683
+ // Single code line
1684
+ deleteElement( this.code.childNodes[ linenum ] );
1517
1685
  this.code.insertChildAtIndex( pre, linenum );
1686
+
1687
+ // Gutter
1688
+ deleteElement( this.gutter.childNodes[ linenum ] );
1689
+ var linenumspan = document.createElement('span');
1690
+ linenumspan.innerHTML = (linenum + 1);
1691
+ this.gutter.insertChildAtIndex( linenumspan, linenum );
1518
1692
  }
1519
1693
 
1520
- var linespan = document.createElement('span');
1521
- pre.appendChild(linespan);
1694
+ const tokensToEvaluate = this._getTokensFromString( linestring );
1522
1695
 
1523
- const to_process = this._getTokensFromString( linestring );
1696
+ if( !tokensToEvaluate.length )
1697
+ return "<pre></pre>";
1524
1698
 
1525
- let line_inner_html = "";
1699
+ var line_inner_html = "";
1526
1700
 
1527
1701
  // Process all tokens
1528
- for( var i = 0; i < to_process.length; ++i )
1702
+ for( var i = 0; i < tokensToEvaluate.length; ++i )
1529
1703
  {
1530
1704
  let it = i - 1;
1531
- let prev = to_process[it];
1705
+ let prev = tokensToEvaluate[it];
1532
1706
  while( prev == ' ' ) {
1533
1707
  it--;
1534
- prev = to_process[it];
1708
+ prev = tokensToEvaluate[it];
1535
1709
  }
1536
1710
 
1537
1711
  it = i + 1;
1538
- let next = to_process[it];
1539
- while( next == ' ' || next == '"') {
1712
+ let next = tokensToEvaluate[it];
1713
+ while( next == ' ' || next == '"' ) {
1540
1714
  it++;
1541
- next = to_process[it];
1715
+ next = tokensToEvaluate[it];
1542
1716
  }
1543
1717
 
1544
- let token = to_process[i];
1545
- if( token.substr(0, 2) == '/*' )
1546
- this._building_block_comment = true;
1547
- if( token.substr(token.length - 2) == '*/' )
1548
- delete this._building_block_comment;
1549
-
1550
- line_inner_html += this.processToken(token, prev, next);
1718
+ const token = tokensToEvaluate[i];
1719
+
1720
+ if( this.languages[ this.highlight ].blockComments ?? true )
1721
+ {
1722
+ if( token.substr(0, 2) == '/*' )
1723
+ this._buildingBlockComment = true;
1724
+ if( token.substr(token.length - 2) == '*/' )
1725
+ delete this._buildingBlockComment;
1726
+ }
1727
+
1728
+ line_inner_html += this.evaluateToken(token, prev, next);
1729
+ }
1730
+
1731
+ // Single line update
1732
+ if( !force )
1733
+ {
1734
+ this.code.childNodes[ linenum ].innerHTML = line_inner_html;
1735
+ }
1736
+ // Update all lines at once
1737
+ else
1738
+ {
1739
+ return "<pre>" + line_inner_html + "</pre>";
1740
+ }
1741
+ }
1742
+
1743
+ _processTokens( tokens, offset = 0 ) {
1744
+
1745
+ if( this.highlight == 'C++' )
1746
+ {
1747
+ var idx = tokens.slice(offset).findIndex( (value, index) => this.isNumber(value) );
1748
+ if( idx > -1 )
1749
+ {
1750
+ idx += offset; // Add offset to compute within the whole array of tokens
1751
+ let data = tokens[idx] + tokens[++idx];
1752
+ while( this.isNumber( data ) )
1753
+ {
1754
+ tokens[ idx - 1 ] += tokens[ idx ];
1755
+ tokens.splice( idx, 1 );
1756
+ data += tokens[idx];
1757
+ }
1758
+ // Scan for numbers again
1759
+ return this._processTokens( tokens, idx );
1760
+ }
1551
1761
  }
1552
1762
 
1553
- linespan.innerHTML = line_inner_html;
1763
+ return tokens;
1554
1764
  }
1555
1765
 
1556
1766
  _getTokensFromString( linestring, skipNonWords ) {
1557
1767
 
1558
1768
  // Check if line comment
1559
- const is_comment = linestring.split('//');
1560
- linestring = ( is_comment.length > 1 ) ? is_comment[0] : linestring;
1769
+ const singleLineCommentToken = this.languages[ this.highlight ].singleLineCommentToken ?? this.defaultSingleLineCommentToken;
1770
+ const usesBlockComments = this.languages[ this.highlight ].blockComments ?? true;
1771
+ const has_comment = linestring.split(singleLineCommentToken);
1772
+ linestring = ( has_comment.length > 1 ) ? has_comment[0] : linestring;
1561
1773
 
1562
- const tokens = linestring.split(' ').join('¬ ¬').split('¬'); // trick to split without losing spaces
1563
- const to_process = []; // store in a temp array so we know prev and next tokens...
1564
-
1565
- for( let t of tokens )
1566
- {
1567
- if( !t.length || (skipNonWords && ( t.includes('"') || t.length < 3 )) )
1568
- continue;
1774
+ // const tokens = linestring.split(' ').join('¬ ¬').split('¬'); // trick to split without losing spaces
1775
+
1776
+ let tokensToEvaluate = []; // store in a temp array so we know prev and next tokens...
1569
1777
 
1570
- if( t == ' ' ) {
1571
- to_process.push( t );
1572
- continue;
1573
- }
1778
+ const pushToken = function( t ) {
1779
+ if( (skipNonWords && ( t.includes('"') || t.length < 3 )) )
1780
+ return;
1781
+ tokensToEvaluate.push( t );
1782
+ };
1574
1783
 
1575
- let iter = t.matchAll(/(::|[\[\](){}<>.,;:"'])/g);
1576
- let subtokens = iter.next();
1577
- if( subtokens.value )
1784
+ let iter = linestring.matchAll(/(::|[\[\](){}<>.,;:*"'%@ ])/g);
1785
+ let subtokens = iter.next();
1786
+ if( subtokens.value )
1787
+ {
1788
+ let idx = 0;
1789
+ while( subtokens.value != undefined )
1578
1790
  {
1579
- let idx = 0;
1580
- while( subtokens.value != undefined )
1581
- {
1582
- const _pt = t.substring(idx, subtokens.value.index);
1583
- if( _pt.length ) to_process.push( _pt );
1584
- to_process.push( subtokens.value[0] );
1585
- idx = subtokens.value.index + subtokens.value[0].length;
1586
- subtokens = iter.next();
1587
- if(!subtokens.value) {
1588
- const _at = t.substring(idx);
1589
- if( _at.length ) to_process.push( _at );
1590
- }
1791
+ const _pt = linestring.substring(idx, subtokens.value.index);
1792
+ if( _pt.length ) pushToken( _pt );
1793
+ pushToken( subtokens.value[0] );
1794
+ idx = subtokens.value.index + subtokens.value[0].length;
1795
+ subtokens = iter.next();
1796
+ if(!subtokens.value) {
1797
+ const _at = linestring.substring(idx);
1798
+ if( _at.length ) pushToken( _at );
1591
1799
  }
1592
1800
  }
1593
- else to_process.push( t );
1594
1801
  }
1802
+ else tokensToEvaluate.push( linestring );
1595
1803
 
1596
- if( is_comment.length > 1 && !skipNonWords )
1597
- to_process.push( "//" + is_comment[1] );
1598
-
1599
- return to_process;
1600
- }
1804
+ // if( usesBlockComments )
1805
+ // {
1806
+ // var block = false;
1807
+
1808
+ // if( t.includes('/*') )
1809
+ // {
1810
+ // const idx = t.indexOf( '/*' );
1811
+ // tokensToEvaluate.push( t.substring( 0, idx ), '/*', t.substring( idx + 2 ) );
1812
+ // block |= true;
1813
+ // }
1814
+ // else if( t.includes('*/') )
1815
+ // {
1816
+ // const idx = t.indexOf( '*/' );
1817
+ // tokensToEvaluate.push( t.substring( 0, idx ), '*/', t.substring( idx + 2 ) );
1818
+ // block |= true;
1819
+ // }
1820
+
1821
+ // if( block ) continue;
1822
+ // }
1601
1823
 
1602
- _mustHightlightWord( token, kindArray ) {
1824
+ if( has_comment.length > 1 && !skipNonWords )
1825
+ pushToken( singleLineCommentToken + has_comment[1] );
1603
1826
 
1604
- return kindArray[this.highlight] && kindArray[this.highlight].indexOf(token) > -1;
1827
+ // console.log( tokensToEvaluate );
1828
+
1829
+ return this._processTokens( tokensToEvaluate );
1605
1830
  }
1606
1831
 
1607
- processToken(token, prev, next) {
1832
+ _mustHightlightWord( token, kindArray ) {
1608
1833
 
1609
- let sString = false;
1610
- let highlight = this.highlight.replace(/\s/g, '').replaceAll("+", "p").toLowerCase();
1611
- let inner_html = "", token_classname = "";
1834
+ return kindArray[this.highlight] && kindArray[this.highlight][token] != undefined;
1835
+ }
1612
1836
 
1613
- if(token == '"' || token == "'")
1614
- {
1615
- sString = (this._building_string == token); // stop string if i was building it
1616
- this._building_string = this._building_string ? this._building_string : token;
1617
- }
1837
+ evaluateToken( token, prev, next ) {
1618
1838
 
1619
1839
  if(token == ' ')
1620
1840
  {
1621
- inner_html += token;
1841
+ return token;
1622
1842
  }
1623
1843
  else
1624
1844
  {
1625
- if( this._building_block_comment )
1626
- token_classname += "cm-com";
1845
+ let stringEnded = false;
1846
+ let highlight = this.highlight.replace(/\s/g, '').replaceAll("+", "p").toLowerCase();
1847
+
1848
+ const singleLineCommentToken = this.languages[ this.highlight ].singleLineCommentToken ?? this.defaultSingleLineCommentToken;
1849
+ const usesBlockComments = this.languages[ this.highlight ].blockComments ?? true;
1850
+ const customStringKeys = Object.assign( {}, this.stringKeys );
1851
+
1852
+ if( highlight == 'cpp' && prev && prev.includes('#') ) // preprocessor code..
1853
+ {
1854
+ customStringKeys['@<'] = '>';
1855
+ }
1856
+
1857
+ // Manage strings
1858
+ if( this._buildingString != undefined )
1859
+ {
1860
+ const idx = Object.values(customStringKeys).indexOf( token );
1861
+ stringEnded = (idx > -1) && (idx == Object.values(customStringKeys).indexOf( customStringKeys[ '@' + this._buildingString ] ));
1862
+ }
1863
+ else if( customStringKeys[ '@' + token ] )
1864
+ {
1865
+ // Start new string
1866
+ this._buildingString = token;
1867
+ }
1868
+
1869
+ let token_classname = "";
1870
+
1871
+ if( this._buildingBlockComment != undefined )
1872
+ token_classname = "cm-com";
1627
1873
 
1628
- else if( this._building_string )
1629
- token_classname += "cm-str";
1874
+ else if( this._buildingString != undefined )
1875
+ token_classname = "cm-str";
1630
1876
 
1631
1877
  else if( this._mustHightlightWord( token, this.keywords ) )
1632
- token_classname += "cm-kwd";
1878
+ token_classname = "cm-kwd";
1633
1879
 
1634
1880
  else if( this._mustHightlightWord( token, this.builtin ) )
1635
- token_classname += "cm-bln";
1881
+ token_classname = "cm-bln";
1636
1882
 
1637
1883
  else if( this._mustHightlightWord( token, this.statementsAndDeclarations ) )
1638
- token_classname += "cm-std";
1884
+ token_classname = "cm-std";
1639
1885
 
1640
1886
  else if( this._mustHightlightWord( token, this.symbols ) )
1641
- token_classname += "cm-sym";
1887
+ token_classname = "cm-sym";
1642
1888
 
1643
- else if( token.substr(0, 2) == '//' )
1644
- token_classname += "cm-com";
1889
+ else if( token.substr(0, 2) == singleLineCommentToken )
1890
+ token_classname = "cm-com";
1645
1891
 
1646
- else if( token.substr(0, 2) == '/*' )
1647
- token_classname += "cm-com";
1892
+ else if( usesBlockComments && token.substr(0, 2) == '/*' )
1893
+ token_classname = "cm-com";
1648
1894
 
1649
- else if( token.substr(token.length - 2) == '*/' )
1650
- token_classname += "cm-com";
1895
+ else if( usesBlockComments && token.substr(token.length - 2) == '*/' )
1896
+ token_classname = "cm-com";
1651
1897
 
1652
- else if( this.isNumber(token) || this.isNumber( token.replace(/[px]|[em]|%/g,'') ) )
1653
- token_classname += "cm-dec";
1898
+ else if( this.isNumber(token) || this.isNumber( token.replace(/[px]|[em]|%/g,'') ) )
1899
+ token_classname = "cm-dec";
1654
1900
 
1655
1901
  else if( this.isCSSClass(token, prev, next) )
1656
- token_classname += "cm-kwd";
1902
+ token_classname = "cm-kwd";
1657
1903
 
1658
1904
  else if ( this.isType(token, prev, next) )
1659
- token_classname += "cm-typ";
1905
+ token_classname = "cm-typ";
1660
1906
 
1661
- else if ( token[0] != '@' && next == '(' )
1662
- token_classname += "cm-mtd";
1907
+ else if ( highlight == 'batch' && (token == '@' || prev == ':' || prev == '@') )
1908
+ token_classname = "cm-kwd";
1663
1909
 
1664
1910
  else if ( highlight == 'cpp' && token.includes('#') ) // C++ preprocessor
1665
- token_classname += "cm-ppc";
1911
+ token_classname = "cm-ppc";
1666
1912
 
1667
- else if ( highlight == 'cpp' && prev != 'WDWD:' && next == '::' ) // C++ Class
1668
- token_classname += "cm-typ";
1913
+ else if ( highlight == 'cpp' && prev == '<' && (next == '>' || next == '*') ) // Defining template type in C++
1914
+ token_classname = "cm-typ";
1915
+
1916
+ else if ( highlight == 'cpp' && (next == '::' || prev == '::' && next != '(' )) // C++ Class
1917
+ token_classname = "cm-typ";
1669
1918
 
1670
1919
  else if ( highlight == 'css' && prev == ':' && (next == ';' || next == '!important') ) // CSS value
1671
- token_classname += "cm-str";
1920
+ token_classname = "cm-str";
1672
1921
 
1673
1922
  else if ( highlight == 'css' && prev == undefined && next == ':' ) // CSS attribute
1674
- token_classname += "cm-typ";
1923
+ token_classname = "cm-typ";
1675
1924
 
1676
- token_classname += " " + highlight;
1677
- inner_html += "<span class=' " + token_classname + "'>" + token + "</span>";
1678
- }
1925
+ else if ( token[0] != '@' && next == '(' )
1926
+ token_classname = "cm-mtd";
1679
1927
 
1680
- if(sString) delete this._building_string;
1928
+ this._buildingString = stringEnded ? undefined : this._buildingString;
1681
1929
 
1682
- return inner_html;
1930
+ return "<span class='" + highlight + " " + token_classname + "'>" + token + "</span>";
1931
+ }
1683
1932
  }
1684
1933
 
1685
1934
  isCSSClass( token, prev, next ) {
@@ -1687,7 +1936,16 @@ class CodeEditor {
1687
1936
  }
1688
1937
 
1689
1938
  isNumber( token ) {
1690
- return token.length && !Number.isNaN(+token);
1939
+
1940
+ if(this.highlight == 'C++')
1941
+ {
1942
+ if( token.lastChar == 'f' )
1943
+ return this.isNumber( token.substring(0, token.length - 1) )
1944
+ else if( token.lastChar == 'u' )
1945
+ return !(token.includes('.')) && this.isNumber( token.substring(0, token.length - 1) );
1946
+ }
1947
+
1948
+ return token.length && token != ' ' && !Number.isNaN(+token);
1691
1949
  }
1692
1950
 
1693
1951
  isType( token, prev, next ) {
@@ -1700,9 +1958,13 @@ class CodeEditor {
1700
1958
  {
1701
1959
  return (prev == 'class' && next == '{') || (prev == 'new' && next == '(');
1702
1960
  }
1961
+ else if( this.highlight == 'C++' )
1962
+ {
1963
+ return (prev == 'class' && next == '{') || (prev == 'struct' && next == '{');
1964
+ }
1703
1965
  else if ( this.highlight == 'WGSL' )
1704
1966
  {
1705
- const is_kwd = (this.keywords[this.highlight] && this.keywords[this.highlight].indexOf(token) == -1);
1967
+ const is_kwd = !this._mustHightlightWord( token, this.keywords );
1706
1968
  return (prev == 'struct' && next == '{') ||
1707
1969
  ( is_kwd &&
1708
1970
  ( prev == ':' && next == ')' || prev == ':' && next == ',' || prev == '>' && next == '{'
@@ -1715,7 +1977,7 @@ class CodeEditor {
1715
1977
  if( !this.selection || (this.selection.fromY != this.selection.toY) )
1716
1978
  return false;
1717
1979
 
1718
- const _lastLeft = cursor._left;
1980
+ this.selection.invertIfNecessary();
1719
1981
 
1720
1982
  // Insert first..
1721
1983
  this.code.lines[lidx] = [
@@ -1851,9 +2113,9 @@ class CodeEditor {
1851
2113
  // Add horizontal scroll
1852
2114
 
1853
2115
  doAsync(() => {
1854
- var last_char = ((this.code.scrollLeft + this.code.clientWidth) / this.charWidth)|0;
2116
+ var last_char = ((this.code.clientWidth) / this.charWidth)|0;
1855
2117
  if( cursor.position >= last_char )
1856
- this.code.scrollLeft += this.charWidth;
2118
+ this.setScrollLeft( this.getScrollLeft() + this.charWidth );
1857
2119
  });
1858
2120
  }
1859
2121
 
@@ -1869,10 +2131,12 @@ class CodeEditor {
1869
2131
  this.restartBlink();
1870
2132
  this._refreshCodeInfo( cursor.line, cursor.position );
1871
2133
 
2134
+ // Add horizontal scroll
2135
+
1872
2136
  doAsync(() => {
1873
- var first_char = (this.code.scrollLeft / this.charWidth)|0;
2137
+ var first_char = (this.getScrollLeft() / this.charWidth)|0;
1874
2138
  if( (cursor.position - 1) < first_char )
1875
- this.code.scrollLeft -= this.charWidth;
2139
+ this.setScrollLeft( this.getScrollLeft() - this.charWidth );
1876
2140
  });
1877
2141
  }
1878
2142
 
@@ -1890,9 +2154,9 @@ class CodeEditor {
1890
2154
  this._refreshCodeInfo( cursor.line, cursor.position );
1891
2155
 
1892
2156
  doAsync(() => {
1893
- var first_line = (this.code.scrollTop / this.lineHeight)|0;
2157
+ var first_line = (this.getScrollTop() / this.lineHeight)|0;
1894
2158
  if( (cursor.line - 1) < first_line )
1895
- this.code.scrollTop -= this.lineHeight;
2159
+ this.setScrollTop( this.getScrollTop() - this.lineHeight );
1896
2160
  });
1897
2161
  }
1898
2162
 
@@ -1909,9 +2173,9 @@ class CodeEditor {
1909
2173
  this._refreshCodeInfo( cursor.line, cursor.position );
1910
2174
 
1911
2175
  doAsync(() => {
1912
- var last_line = ((this.code.scrollTop + this.code.offsetHeight) / this.lineHeight)|0;
2176
+ var last_line = ((this.code.parentElement.offsetHeight - 32) / this.lineHeight)|0;
1913
2177
  if( cursor.line >= last_line )
1914
- this.code.scrollTop += this.lineHeight;
2178
+ this.setScrollTop( this.getScrollTop() + this.lineHeight );
1915
2179
  });
1916
2180
  }
1917
2181
 
@@ -1998,13 +2262,148 @@ class CodeEditor {
1998
2262
  getScrollLeft() {
1999
2263
 
2000
2264
  if(!this.code) return 0;
2001
- return this.code.scrollLeft;
2265
+ return this.code.customScroll.x;
2002
2266
  }
2003
2267
 
2004
2268
  getScrollTop() {
2005
2269
 
2006
2270
  if(!this.code) return 0;
2007
- return this.code.scrollTop;
2271
+ return this.code.customScroll.y;
2272
+ }
2273
+
2274
+ setScrollLeft( value, keepScrollBar ) {
2275
+
2276
+ if(!this.code) return;
2277
+
2278
+ const realClientWidth = (this.code.clientWidth - this.code.customScroll.x);
2279
+ const maxWidth = Math.max( this.code.scrollWidth - realClientWidth, 0 );
2280
+
2281
+ value = LX.UTILS.clamp( value, 0, maxWidth );
2282
+
2283
+ this.code.style.marginLeft = (-value) + "px";
2284
+
2285
+ if( !keepScrollBar )
2286
+ {
2287
+ const scrollWidth = this.hScrollbarThumb.parentElement.offsetWidth;
2288
+ const scrollBarWidth = this.hScrollbarThumb.offsetWidth;
2289
+ this.setScrollBarValue( ( scrollWidth - scrollBarWidth ) * ( value / maxWidth ), 'horizontal' );
2290
+ }
2291
+
2292
+ // Update cursor
2293
+ var cursor = this.cursors.children[0];
2294
+ cursor.style.left = "calc( " + (cursor._left - value) + "px + " + this.xPadding + ")";
2295
+
2296
+ // Update selection
2297
+ for( let s of this.selections.childNodes ) {
2298
+ s.style.left = "calc( " + (s._left - value) + "px + " + this.xPadding + ")";
2299
+ }
2300
+
2301
+ this.code.customScroll.x = value;
2302
+ }
2303
+
2304
+ setScrollTop( value, keepScrollBar ) {
2305
+
2306
+ if(!this.code) return;
2307
+
2308
+ const realClientHeight = this.code.parentElement.offsetHeight - 36;
2309
+ const maxHeight = Math.max( this.code.scrollHeight - realClientHeight, 0 );
2310
+
2311
+ value = LX.UTILS.clamp( value, 0, maxHeight );
2312
+
2313
+ this.gutter.scrollTop = value;
2314
+
2315
+ this.code.style.marginTop = (-value) + "px";
2316
+
2317
+ if( !keepScrollBar )
2318
+ {
2319
+ const scrollHeight = this.scrollbarThumb.parentElement.offsetHeight;
2320
+ const scrollBarHeight = this.scrollbarThumb.offsetHeight;
2321
+ // this.setScrollBarValue( ( scrollHeight - scrollBarHeight ) * ( value / maxHeight ) )
2322
+ const firstLineInViewport = (this.getScrollTop() / this.lineHeight)|0;
2323
+ this.setScrollBarValue( ( scrollHeight - scrollBarHeight ) * ( firstLineInViewport / this.code.lines.length ) )
2324
+ }
2325
+
2326
+ // Update cursor
2327
+ var cursor = this.cursors.children[0];
2328
+ cursor.style.top = (cursor._top - value) + "px";
2329
+
2330
+ // Update selection
2331
+ for( let s of this.selections.childNodes ) {
2332
+ s.style.top = (s._top - value) + "px";
2333
+ }
2334
+
2335
+ this.code.customScroll.y = value;
2336
+ }
2337
+
2338
+ resizeScrollBars() {
2339
+
2340
+ const numViewportLines = Math.floor( (this.code.parentElement.offsetHeight - 36) / this.lineHeight );
2341
+
2342
+ if( numViewportLines > this.code.lines.length )
2343
+ {
2344
+ this.scrollbar.classList.add( 'scrollbar-unused' );
2345
+ this.tabs.area.root.classList.remove( 'with-vscrollbar' );
2346
+ }
2347
+ else
2348
+ {
2349
+ this.scrollbar.classList.remove( 'scrollbar-unused' );
2350
+ this.tabs.area.root.classList.add( 'with-vscrollbar' );
2351
+ this.scrollbarThumb.size = (numViewportLines / this.code.lines.length);
2352
+ this.scrollbarThumb.style.height = (this.scrollbarThumb.size * 100.0) + "%";
2353
+ }
2354
+
2355
+ const numViewportChars = Math.floor( this.code.clientWidth / this.charWidth );
2356
+ const line_lengths = this.code.lines.map( value => value.length );
2357
+ const maxLineLength = Math.max(...line_lengths);
2358
+
2359
+ if( numViewportChars > maxLineLength )
2360
+ {
2361
+ this.hScrollbar.classList.add( 'scrollbar-unused' );
2362
+ this.tabs.area.root.classList.remove( 'with-hscrollbar' );
2363
+ }
2364
+ else
2365
+ {
2366
+ this.hScrollbar.classList.remove( 'scrollbar-unused' );
2367
+ this.tabs.area.root.classList.add( 'with-hscrollbar' );
2368
+ this.hScrollbarThumb.size = (numViewportChars / maxLineLength);
2369
+ this.hScrollbarThumb.style.width = (this.hScrollbarThumb.size * 100.0) + "%";
2370
+ }
2371
+ }
2372
+
2373
+ setScrollBarValue( value, type = 'vertical' ) {
2374
+
2375
+ if( type == 'vertical' )
2376
+ {
2377
+ const scrollHeight = this.scrollbarThumb.parentElement.offsetHeight;
2378
+ const scrollBarHeight = this.scrollbarThumb.offsetHeight;
2379
+
2380
+ value = LX.UTILS.clamp( value, 0, ( scrollHeight - scrollBarHeight ) );
2381
+
2382
+ this.scrollbarThumb._top = value;
2383
+ this.scrollbarThumb.style.top = this.scrollbarThumb._top + "px";
2384
+ }
2385
+ else
2386
+ {
2387
+ const scrollWidth = this.hScrollbarThumb.parentElement.offsetWidth;
2388
+ const scrollBarWidth = this.hScrollbarThumb.offsetWidth;
2389
+
2390
+ value = LX.UTILS.clamp( value, 0, ( scrollWidth - scrollBarWidth ) );
2391
+
2392
+ this.hScrollbarThumb._left = value;
2393
+ this.hScrollbarThumb.style.left = this.hScrollbarThumb._left + "px";
2394
+ }
2395
+ }
2396
+
2397
+ applyHorizontalScrollFromScrollBar( value ) {
2398
+
2399
+ this.setScrollBarValue( value, 'horizontal');
2400
+ this.setScrollLeft( value / this.hScrollbarThumb.size, true );
2401
+ }
2402
+
2403
+ applyVerticalScrollFromScrollBar( value ) {
2404
+
2405
+ this.setScrollBarValue( value );
2406
+ this.setScrollTop( value / this.scrollbarThumb.size, true );
2008
2407
  }
2009
2408
 
2010
2409
  getCharAtPos( cursor, offset = 0 ) {
@@ -2020,7 +2419,7 @@ class CodeEditor {
2020
2419
  const words = this.code.lines[col];
2021
2420
 
2022
2421
  const is_char = (char) => {
2023
- const exceptions = ['_'];
2422
+ const exceptions = ['_', '#', '!'];
2024
2423
  const code = char.charCodeAt(0);
2025
2424
  return (exceptions.indexOf(char) > - 1) || (code > 47 && code < 58) || (code > 64 && code < 91) || (code > 96 && code < 123);
2026
2425
  }
@@ -2048,7 +2447,7 @@ class CodeEditor {
2048
2447
  test.innerHTML = char;
2049
2448
  document.body.appendChild(test);
2050
2449
  var rect = test.getBoundingClientRect();
2051
- test.remove();
2450
+ deleteElement( test );
2052
2451
  const bb = [Math.floor(rect.width), Math.floor(rect.height)];
2053
2452
  return get_bb ? bb : bb[0];
2054
2453
  }
@@ -2059,6 +2458,7 @@ class CodeEditor {
2059
2458
  }
2060
2459
 
2061
2460
  runScript( code ) {
2461
+
2062
2462
  var script = document.createElement('script');
2063
2463
  script.type = 'module';
2064
2464
  script.innerHTML = code;
@@ -2115,15 +2515,15 @@ class CodeEditor {
2115
2515
 
2116
2516
  // Add language special keys...
2117
2517
  suggestions = suggestions.concat(
2118
- this.builtin[ this.highlight ] ?? [],
2119
- this.keywords[ this.highlight ] ?? [],
2120
- this.statementsAndDeclarations[ this.highlight ] ?? [],
2121
- this.types[ this.highlight ] ?? [],
2122
- this.utils[ this.highlight ] ?? []
2518
+ Object.keys( this.builtin[ this.highlight ] ) ?? [],
2519
+ Object.keys( this.keywords[ this.highlight ] ) ?? [],
2520
+ Object.keys( this.statementsAndDeclarations[ this.highlight ] ) ?? [],
2521
+ Object.keys( this.types[ this.highlight ] ) ?? [],
2522
+ Object.keys( this.utils[ this.highlight ] ) ?? []
2123
2523
  );
2124
2524
 
2125
2525
  // Add words in current tab plus remove current word
2126
- suggestions = suggestions.concat( Object.keys(this.code.tokens).filter( a => a != word ) );
2526
+ // suggestions = suggestions.concat( Object.keys(this.code.tokens).filter( a => a != word ) );
2127
2527
 
2128
2528
  // Remove 1/2 char words and duplicates...
2129
2529
  suggestions = suggestions.filter( (value, index) => value.length > 2 && suggestions.indexOf(value) === index );
@@ -2183,9 +2583,8 @@ class CodeEditor {
2183
2583
  // Show box
2184
2584
  this.autocomplete.classList.toggle('show', true);
2185
2585
  this.autocomplete.classList.toggle('no-scrollbar', !(this.autocomplete.scrollHeight > this.autocomplete.offsetHeight));
2186
- this.autocomplete.style.left = (cursor._left + 36) + "px";
2187
- this.autocomplete.style.top = (cursor._top + 48) + "px";
2188
-
2586
+ this.autocomplete.style.left = (cursor._left + 36 - this.getScrollLeft()) + "px";
2587
+ this.autocomplete.style.top = (cursor._top + 48 - this.getScrollTop()) + "px";
2189
2588
 
2190
2589
  this.isAutoCompleteActive = true;
2191
2590
  }