lexgui 0.1.16 → 0.1.18

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.
@@ -13,23 +13,23 @@ function flushCss(element) {
13
13
  element.offsetHeight;
14
14
  }
15
15
 
16
- function swapElements (obj, a, b) {
16
+ function swapElements( obj, a, b ) {
17
17
  [obj[a], obj[b]] = [obj[b], obj[a]];
18
18
  }
19
19
 
20
- function swapArrayElements (array, id0, id1) {
20
+ function swapArrayElements( array, id0, id1 ) {
21
21
  [array[id0], array[id1]] = [array[id1], array[id0]];
22
22
  };
23
23
 
24
- function sliceChar(str, idx) {
24
+ function sliceChar( str, idx ) {
25
25
  return str.substr(0, idx) + str.substr(idx + 1);
26
26
  }
27
27
 
28
- function firstNonspaceIndex(str) {
28
+ function firstNonspaceIndex( str ) {
29
29
  return str.search(/\S|$/);
30
30
  }
31
31
 
32
- function deleteElement(el) {
32
+ function deleteElement( el ) {
33
33
  if(el) el.remove();
34
34
  }
35
35
 
@@ -42,9 +42,9 @@ function doAsync( fn, ms ) {
42
42
  fn();
43
43
  }
44
44
 
45
- class ISelection {
45
+ class CodeSelection {
46
46
 
47
- constructor(editor, ix, iy) {
47
+ constructor( editor, ix, iy ) {
48
48
 
49
49
  this.editor = editor;
50
50
  this.chars = 0;
@@ -66,25 +66,83 @@ class ISelection {
66
66
  swapElements(this, 'fromY', 'toY');
67
67
  }
68
68
 
69
- selectInline(x, y, width) {
69
+ selectInline( x, y, width ) {
70
70
 
71
71
  this.chars = width / this.editor.charWidth;
72
72
  this.fromX = x;
73
73
  this.toX = x + this.chars;
74
74
  this.fromY = this.toY = y;
75
75
 
76
- var domEl = document.createElement('div');
76
+ var domEl = document.createElement( 'div' );
77
77
  domEl.className = "lexcodeselection";
78
78
 
79
- domEl._top = 4 + y * this.editor.lineHeight;
80
- domEl.style.top = (domEl._top - this.editor.getScrollTop()) + "px";
79
+ domEl._top = y * this.editor.lineHeight;
80
+ domEl.style.top = domEl._top + "px";
81
81
  domEl._left = x * this.editor.charWidth;
82
- domEl.style.left = "calc(" + (domEl._left - this.editor.getScrollLeft()) + "px + " + this.editor.xPadding + ")";
82
+ domEl.style.left = "calc(" + domEl._left + "px + " + this.editor.xPadding + ")";
83
83
  domEl.style.width = width + "px";
84
84
  this.editor.selections.appendChild(domEl);
85
85
  }
86
86
  };
87
87
 
88
+ class ScrollBar {
89
+
90
+ static SCROLLBAR_VERTICAL = 1;
91
+ static SCROLLBAR_HORIZONTAL = 2;
92
+
93
+ constructor( editor, type ) {
94
+
95
+ this.editor = editor;
96
+ this.type = type;
97
+
98
+ this.root = document.createElement( 'div' );
99
+ this.root.className = "lexcodescrollbar";
100
+ if( type & ScrollBar.SCROLLBAR_HORIZONTAL )
101
+ this.root.classList.add( 'horizontal' );
102
+
103
+ this.thumb = document.createElement( 'div' );
104
+ this.thumb._top = 0;
105
+ this.thumb._left = 0;
106
+ this.root.appendChild( this.thumb );
107
+
108
+ this.thumb.addEventListener( "mousedown", inner_mousedown );
109
+
110
+ this.lastPosition = new LX.vec2( 0, 0 );
111
+
112
+ let that = this;
113
+
114
+ function inner_mousedown( e )
115
+ {
116
+ var doc = editor.root.ownerDocument;
117
+ doc.addEventListener( "mousemove",inner_mousemove );
118
+ doc.addEventListener( "mouseup",inner_mouseup );
119
+ that.lastPosition.set( e.x, e.y );
120
+ e.stopPropagation();
121
+ e.preventDefault();
122
+ }
123
+
124
+ function inner_mousemove( e )
125
+ {
126
+ var dt = that.lastPosition.sub( new LX.vec2( e.x, e.y ) );
127
+ if( that.type & ScrollBar.SCROLLBAR_VERTICAL )
128
+ editor.updateVerticalScrollFromScrollBar( dt.y )
129
+ else
130
+ editor.updateHorizontalScrollFromScrollBar( dt.x )
131
+ that.lastPosition.set( e.x, e.y );
132
+ e.stopPropagation();
133
+ e.preventDefault();
134
+ }
135
+
136
+ function inner_mouseup( e )
137
+ {
138
+ var doc = editor.root.ownerDocument;
139
+ doc.removeEventListener( "mousemove", inner_mousemove );
140
+ doc.removeEventListener( "mouseup", inner_mouseup );
141
+ }
142
+ }
143
+
144
+ }
145
+
88
146
  /**
89
147
  * @class CodeEditor
90
148
  */
@@ -96,6 +154,9 @@ class CodeEditor {
96
154
  static CURSOR_LEFT = 1;
97
155
  static CURSOR_TOP = 2;
98
156
 
157
+ static KEEP_VISIBLE_LINES = 1;
158
+ static UPDATE_VISIBLE_LINES = 2;
159
+
99
160
  static WORD_TYPE_METHOD = 0;
100
161
  static WORD_TYPE_CLASS = 1;
101
162
 
@@ -106,35 +167,14 @@ class CodeEditor {
106
167
 
107
168
  constructor( area, options = {} ) {
108
169
 
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
-
128
170
  window.editor = this;
129
171
 
130
172
  CodeEditor.__instances.push( this );
131
173
 
132
- var that = this;
133
-
134
174
  this.base_area = area;
135
175
  this.area = new LX.Area( { className: "lexcodeeditor", height: "auto", no_append: true } );
136
176
 
137
- this.tabs = this.area.addTabs( { onclose: (name) => delete this.openedTabs[name] } );
177
+ this.tabs = this.area.addTabs( { onclose: (name) => delete this.openedTabs[ name ] } );
138
178
  this.tabs.root.addEventListener( 'dblclick', (e) => {
139
179
  if( options.allow_add_scripts ?? true ) {
140
180
  e.preventDefault();
@@ -142,10 +182,11 @@ class CodeEditor {
142
182
  }
143
183
  } );
144
184
 
185
+ // Full editor
145
186
  area.root.classList.add('codebasearea');
146
- this.gutter = document.createElement('div');
147
- this.gutter.className = "lexcodegutter";
148
- area.attach( this.gutter );
187
+
188
+ // Code area
189
+ this.tabs.area.root.classList.add( 'codetabsarea' );
149
190
 
150
191
  this.root = this.area.root;
151
192
  this.root.tabIndex = -1;
@@ -156,9 +197,9 @@ class CodeEditor {
156
197
 
157
198
  if( !this.disableEdition )
158
199
  {
159
- this.root.addEventListener( 'keydown', this.processKey.bind(this), true);
160
- this.root.addEventListener( 'focus', this.processFocus.bind(this, true) );
161
- this.root.addEventListener( 'focusout', this.processFocus.bind(this, false) );
200
+ this.root.addEventListener( 'keydown', this.processKey.bind( this), true );
201
+ this.root.addEventListener( 'focus', this.processFocus.bind( this, true ) );
202
+ this.root.addEventListener( 'focusout', this.processFocus.bind( this, false ) );
162
203
  }
163
204
 
164
205
  this.root.addEventListener( 'mousedown', this.processMouse.bind(this) );
@@ -167,141 +208,134 @@ class CodeEditor {
167
208
  this.root.addEventListener( 'click', this.processMouse.bind(this) );
168
209
  this.root.addEventListener( 'contextmenu', this.processMouse.bind(this) );
169
210
 
170
- // Take into account the scrollbar..
171
- this.tabs.area.root.classList.add( 'codetabsarea' );
172
-
173
211
  // Cursors and selection
174
212
 
175
- this.cursors = document.createElement('div');
213
+ this.cursors = document.createElement( 'div' );
176
214
  this.cursors.className = 'cursors';
177
- this.tabs.area.attach(this.cursors);
215
+ this.tabs.area.attach( this.cursors );
178
216
 
179
- this.selections = document.createElement('div');
217
+ this.selections = document.createElement( 'div' );
180
218
  this.selections.className = 'selections';
181
- this.tabs.area.attach(this.selections);
219
+ this.tabs.area.attach( this.selections );
182
220
 
183
221
  // Css char synchronization
184
- this.xPadding = "0.25em";
222
+ this.xPadding = "48px";
185
223
 
186
224
  // Add main cursor
187
225
  {
188
- var cursor = document.createElement('div');
226
+ var cursor = document.createElement( 'div' );
189
227
  cursor.className = "cursor";
190
228
  cursor.innerHTML = "&nbsp;";
191
229
  cursor._left = 0;
192
230
  cursor.style.left = this.xPadding;
193
- cursor._top = 4;
194
- cursor.style.top = "4px";
231
+ cursor._top = 0;
232
+ cursor.style.top = cursor._top + "px";
195
233
  cursor.position = 0;
196
- cursor.line = 0;
197
- cursor.print = (function() { console.log( this.line, this.position ) }).bind(cursor);
198
- this.cursors.appendChild(cursor);
234
+ cursor._line = 0;
235
+ cursor.print = (function() { console.log( this.line, this.position ) }).bind( cursor );
236
+
237
+ Object.defineProperty( cursor, 'line', {
238
+ get: (v) => { return this._line },
239
+ set: (v) => { this._line = v; }
240
+ } );
241
+
242
+ this.cursors.appendChild( cursor );
199
243
  }
200
244
 
201
- // Add custom vertical scroll bar
245
+ // Scroll stuff
202
246
  {
203
- var scrollbar = document.createElement('div');
204
- scrollbar.className = "lexcodescrollbar";
205
- this.scrollbar = scrollbar;
206
- area.attach(this.scrollbar);
247
+ this.codeScroller = this.tabs.area.root;
248
+ this.firstLineInViewport = 0;
249
+ this.lineScrollMargin = new LX.vec2( 20, 20 ); // [ mUp, mDown ]
250
+ window.scroller = this.codeScroller;
207
251
 
208
- var scrollbarThumb = document.createElement('div');
209
- this.scrollbarThumb = scrollbarThumb;
210
- this.scrollbarThumb._top = 0;
211
- scrollbar.appendChild(scrollbarThumb);
252
+ let lastScrollTopValue = -1;
253
+ this.codeScroller.addEventListener( 'scroll', e => {
212
254
 
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
- }
255
+ if( this._discardScroll )
256
+ {
257
+ this._discardScroll = false;
258
+ return;
259
+ }
226
260
 
227
- function inner_mousemove(e)
228
- {
229
- var dt = (last_pos - e.y);
230
-
231
- that.applyVerticalScrollFromScrollBar( that.scrollbarThumb._top - dt )
261
+ this.setScrollBarValue( 'vertical' );
232
262
 
233
- last_pos = e.y;
234
- e.stopPropagation();
235
- e.preventDefault();
236
- }
263
+ const scrollTop = this.getScrollTop();
237
264
 
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);
265
+ // Scroll down...
266
+ if( scrollTop > lastScrollTopValue )
267
+ {
268
+ if( this.visibleLinesViewport.y < (this.code.lines.length - 1) )
269
+ {
270
+ const totalLinesInViewport = ((this.codeScroller.offsetHeight - 36) / this.lineHeight)|0;
271
+ const scrollDownBoundary =
272
+ ( Math.max( this.visibleLinesViewport.y - totalLinesInViewport, 0 ) - 1 ) * this.lineHeight;
273
+
274
+ if( scrollTop >= scrollDownBoundary )
275
+ this.processLines( CodeEditor.UPDATE_VISIBLE_LINES );
276
+ }
277
+ }
278
+ // Scroll up...
279
+ else
280
+ {
281
+ const scrollUpBoundary = parseInt( this.code.style.top );
282
+ if( scrollTop < scrollUpBoundary )
283
+ this.processLines( CodeEditor.UPDATE_VISIBLE_LINES );
284
+ }
252
285
 
253
- var hScrollbarThumb = document.createElement('div');
254
- this.hScrollbarThumb = hScrollbarThumb;
255
- this.hScrollbarThumb._left = 0;
256
- hScrollbar.appendChild(hScrollbarThumb);
286
+ lastScrollTopValue = scrollTop;
287
+ });
257
288
 
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
- }
289
+ this.codeScroller.addEventListener( 'wheel', e => {
290
+ const dX = ( e.deltaY > 0.0 ? 10.0 : -10.0 ) * ( e.shiftKey ? 1.0 : 0.0 );
291
+ if( dX != 0.0 ) this.setScrollBarValue( 'horizontal', dX );
292
+ });
293
+ }
271
294
 
272
- function inner_mousemove(e)
273
- {
274
- var dt = (last_pos - e.x);
275
-
276
- that.applyHorizontalScrollFromScrollBar( that.hScrollbarThumb._left - dt )
295
+ // This is only the container, line numbers are in the same line div
296
+ {
297
+ this.gutter = document.createElement( 'div' );
298
+ this.gutter.className = "lexcodegutter";
299
+ area.attach( this.gutter );
300
+ }
277
301
 
278
- last_pos = e.x;
279
- e.stopPropagation();
280
- e.preventDefault();
281
- }
302
+ // Add custom vertical scroll bar
303
+ {
304
+ this.vScrollbar = new ScrollBar( this, ScrollBar.SCROLLBAR_VERTICAL );
305
+ area.attach( this.vScrollbar.root );
306
+ }
282
307
 
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
- }
308
+ // Add custom horizontal scroll bar
309
+ {
310
+ this.hScrollbar = new ScrollBar( this, ScrollBar.SCROLLBAR_HORIZONTAL );
311
+ area.attach( this.hScrollbar.root );
289
312
  }
290
313
 
291
314
  // Add autocomplete box
292
315
  {
293
- var box = document.createElement('div');
316
+ var box = document.createElement( 'div' );
294
317
  box.className = "autocomplete";
295
318
  this.autocomplete = box;
296
- this.tabs.area.attach(box);
319
+ this.tabs.area.attach( box );
297
320
 
298
321
  this.isAutoCompleteActive = false;
299
322
  }
300
323
 
324
+ // Add code-sizer
325
+ {
326
+ this.codeSizer = document.createElement( 'div' );
327
+ this.codeSizer.className = "code-sizer";
328
+
329
+ // Append all childs
330
+ while( this.codeScroller.firstChild )
331
+ this.codeSizer.appendChild( this.codeScroller.firstChild );
332
+
333
+ this.codeScroller.appendChild( this.codeSizer );
334
+ }
335
+
301
336
  // State
302
337
 
303
338
  this.state = {
304
- overwrite: false,
305
339
  focused: false,
306
340
  selectingText: false
307
341
  }
@@ -335,7 +369,7 @@ class CodeEditor {
335
369
  };
336
370
 
337
371
  // Scan tokens..
338
- setInterval( this.scanWordSuggestions.bind(this), 2000 );
372
+ // setInterval( this.scanWordSuggestions.bind( this ), 2000 );
339
373
 
340
374
  this.languages = {
341
375
  'Plain Text': { },
@@ -346,7 +380,8 @@ class CodeEditor {
346
380
  'WGSL': { },
347
381
  'JSON': { },
348
382
  'XML': { },
349
- 'Python': { },
383
+ 'Python': { singleLineCommentToken: '#' },
384
+ 'HTML': { },
350
385
  'Batch': { blockComments: false, singleLineCommentToken: '::' }
351
386
  };
352
387
 
@@ -370,7 +405,8 @@ class CodeEditor {
370
405
  'texture_storage_2d_array', 'texture_storage_3d'],
371
406
  'Python': ['False', 'def', 'None', 'True', 'in', 'is', 'and', 'lambda', 'nonlocal', 'not', 'or'],
372
407
  'Batch': ['set', 'SET', 'echo', 'ECHO', 'off', 'OFF', 'del', 'DEL', 'defined', 'DEFINED', 'setlocal', 'SETLOCAL', 'enabledelayedexpansion', 'ENABLEDELAYEDEXPANSION', 'driverquery',
373
- 'DRIVERQUERY', 'print', 'PRINT']
408
+ 'DRIVERQUERY', 'print', 'PRINT'],
409
+ 'HTML': ['html', 'meta', 'title', 'link', 'script', 'body', 'DOCTYPE', 'head'],
374
410
  };
375
411
  this.utils = { // These ones don't have hightlight, used as suggestions to autocomplete only...
376
412
  'JavaScript': ['querySelector', 'body', 'addEventListener', 'removeEventListener', 'remove', 'sort', 'keys', 'filter', 'isNaN', 'parseFloat', 'parseInt', 'EPSILON', 'isFinite',
@@ -387,13 +423,14 @@ class CodeEditor {
387
423
  'Python': ['int', 'type', 'float', 'map', 'list', 'ArithmeticError', 'AssertionError', 'AttributeError', 'Exception', 'EOFError', 'FloatingPointError', 'GeneratorExit',
388
424
  'ImportError', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'NotImplementedError', 'OSError',
389
425
  'OverflowError', 'ReferenceError', 'RuntimeError', 'StopIteration', 'SyntaxError', 'TabError', 'SystemError', 'SystemExit', 'TypeError', 'UnboundLocalError',
390
- 'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError', 'ValueError', 'ZeroDivisionError' ],
426
+ 'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError', 'ValueError', 'ZeroDivisionError'],
391
427
  'C++': ['uint8_t', 'uint16_t', 'uint32_t']
392
428
  };
393
429
  this.builtin = {
394
430
  'JavaScript': ['document', 'console', 'window', 'navigator', 'performance'],
395
431
  'CSS': ['*', '!important'],
396
- 'C++': ['vector', 'list', 'map']
432
+ 'C++': ['vector', 'list', 'map'],
433
+ 'HTML': ['type', 'xmlns', 'PUBLIC', 'http-equiv', 'src', 'lang', 'href', 'rel', 'content', 'xml'], // attributes
397
434
  };
398
435
  this.statementsAndDeclarations = {
399
436
  'JavaScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await'],
@@ -413,6 +450,7 @@ class CodeEditor {
413
450
  'CSS': ['{', '}', '(', ')', '*'],
414
451
  'Python': ['<', '>', '[', ']', '(', ')', '='],
415
452
  'Batch': ['[', ']', '(', ')', '%'],
453
+ 'HTML': ['<', '>', '/']
416
454
  };
417
455
 
418
456
  // Convert reserved word arrays to maps so we can search tokens faster
@@ -426,64 +464,67 @@ class CodeEditor {
426
464
 
427
465
  // Action keys
428
466
 
429
- this.action('Escape', false, ( ln, cursor, e ) => {
467
+ this.action( 'Escape', false, ( ln, cursor, e ) => {
430
468
  this.hideAutoCompleteBox();
431
469
  });
432
470
 
433
- this.action('Backspace', false, ( ln, cursor, e ) => {
471
+ this.action( 'Backspace', false, ( ln, cursor, e ) => {
434
472
 
435
- this._addUndoStep(cursor);
473
+ this._addUndoStep( cursor );
436
474
 
437
- if(this.selection) {
438
- this.deleteSelection(cursor);
475
+ if( this.selection ) {
476
+ this.deleteSelection( cursor );
439
477
  // Remove entire line when selecting with triple click
440
- if(this.code.lines[ln] && !this.code.lines[ln].length)
441
- this.actions['Backspace'].callback(ln, cursor, e);
478
+ if(this.code.lines[ ln ] != undefined && !this.code.lines[ ln ].length)
479
+ {
480
+ this.actions['Backspace'].callback( ln, cursor, e );
481
+ this.lineDown( cursor, true );
482
+ }
442
483
  }
443
484
  else {
444
485
  var letter = this.getCharAtPos( cursor, -1 );
445
- if(letter) {
446
- this.code.lines[ln] = sliceChar( this.code.lines[ln], cursor.position - 1 );
486
+ if( letter ) {
487
+ this.code.lines[ ln ] = sliceChar( this.code.lines[ ln ], cursor.position - 1 );
447
488
  this.cursorToLeft( letter );
448
- this.processLine(ln);
489
+ this.processLine( ln );
449
490
  if( this.useAutoComplete )
450
491
  this.showAutoCompleteBox( 'foo', cursor );
451
492
  }
452
- else if(this.code.lines[ln - 1] != undefined) {
493
+ else if( this.code.lines[ ln - 1 ] != undefined ) {
453
494
  this.lineUp();
454
- this.actions['End'].callback(cursor.line, cursor, e);
495
+ this.actions[ 'End' ].callback( cursor.line, cursor, e );
455
496
  // Move line on top
456
- this.code.lines[ln - 1] += this.code.lines[ln];
457
- this.code.lines.splice(ln, 1);
458
- this.processLines(ln - 1);
497
+ this.code.lines[ ln - 1 ] += this.code.lines[ ln ];
498
+ this.code.lines.splice( ln, 1 );
499
+ this.processLines();
459
500
  }
460
501
  }
461
502
  });
462
503
 
463
- this.action('Delete', false, ( ln, cursor, e ) => {
504
+ this.action( 'Delete', false, ( ln, cursor, e ) => {
464
505
 
465
506
  this._addUndoStep( cursor );
466
507
 
467
508
  if(this.selection) {
468
509
  // Use 'Backspace' as it's the same callback...
469
- this.actions['Backspace'].callback(ln, cursor, e);
510
+ this.actions['Backspace'].callback( ln, cursor, e );
470
511
  }
471
512
  else
472
513
  {
473
514
  var letter = this.getCharAtPos( cursor );
474
- if(letter) {
475
- this.code.lines[ln] = sliceChar( this.code.lines[ln], cursor.position );
476
- this.processLine(ln);
515
+ if( letter ) {
516
+ this.code.lines[ ln ] = sliceChar( this.code.lines[ ln ], cursor.position );
517
+ this.processLine( ln );
477
518
  }
478
- else if(this.code.lines[ln + 1] != undefined) {
479
- this.code.lines[ln] += this.code.lines[ln + 1];
480
- this.code.lines.splice(ln + 1, 1);
481
- this.processLines(ln);
519
+ else if(this.code.lines[ ln + 1 ] != undefined) {
520
+ this.code.lines[ ln ] += this.code.lines[ ln + 1 ];
521
+ this.code.lines.splice( ln + 1, 1 );
522
+ this.processLines();
482
523
  }
483
524
  }
484
525
  });
485
526
 
486
- this.action('Tab', true, ( ln, cursor, e ) => {
527
+ this.action( 'Tab', true, ( ln, cursor, e ) => {
487
528
 
488
529
  if( this.isAutoCompleteActive )
489
530
  {
@@ -494,53 +535,66 @@ class CodeEditor {
494
535
  }
495
536
  });
496
537
 
497
- this.action('Home', false, ( ln, cursor, e ) => {
538
+ this.action( 'Home', false, ( ln, cursor, e ) => {
498
539
 
499
- let idx = firstNonspaceIndex(this.code.lines[ln]);
540
+ let idx = firstNonspaceIndex( this.code.lines[ ln ] );
500
541
 
501
542
  // We already are in the first non space index...
502
543
  if(idx == cursor.position) idx = 0;
503
544
 
504
- const prestring = this.code.lines[ln].substring(0, idx);
505
- let last_pos = cursor.position;
545
+ const prestring = this.code.lines[ ln ].substring( 0, idx );
546
+ let lastX = cursor.position;
506
547
 
507
548
  this.resetCursorPos( CodeEditor.CURSOR_LEFT );
508
- if(idx > 0) this.cursorToString(cursor, prestring);
509
- this._refreshCodeInfo(cursor.line, cursor.position);
549
+ if(idx > 0) this.cursorToString( cursor, prestring );
550
+ this._refreshCodeInfo( cursor.line, cursor.position );
510
551
  this.setScrollLeft( 0 );
511
552
 
512
553
  if( e.shiftKey && !e.cancelShift )
513
554
  {
514
555
  // Get last selection range
515
- if(this.selection)
516
- last_pos += this.selection.chars;
556
+ if( this.selection )
557
+ lastX += this.selection.chars;
517
558
 
518
- this.startSelection(cursor);
519
- var string = this.code.lines[ln].substring(idx, last_pos);
520
- this.selection.selectInline(idx, cursor.line, this.measureString(string));
559
+ if( !this.selection )
560
+ this.startSelection( cursor );
561
+ var string = this.code.lines[ ln ].substring( idx, lastX );
562
+ if( this.selection.sameLine() )
563
+ this.selection.selectInline( idx, cursor.line, this.measureString( string ) );
564
+ else
565
+ {
566
+ this.processSelection();
567
+ }
521
568
  } else if( !e.keepSelection )
522
569
  this.endSelection();
523
570
  });
524
571
 
525
- this.action('End', false, ( ln, cursor, e ) => {
572
+ this.action( 'End', false, ( ln, cursor, e ) => {
526
573
 
527
574
  if( e.shiftKey || e._shiftKey ) {
528
575
 
529
- var string = this.code.lines[ln].substring(cursor.position);
530
- if(!this.selection)
531
- this.startSelection(cursor);
532
- this.selection.selectInline(cursor.position, cursor.line, this.measureString(string));
576
+ var string = this.code.lines[ ln ].substring( cursor.position );
577
+ if( !this.selection )
578
+ this.startSelection( cursor );
579
+ if( this.selection.sameLine() )
580
+ this.selection.selectInline(cursor.position, cursor.line, this.measureString( string ));
581
+ else
582
+ {
583
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT );
584
+ this.cursorToString( cursor, this.code.lines[ ln ] );
585
+ this.processSelection();
586
+ }
533
587
  } else
534
588
  this.endSelection();
535
589
 
536
590
  this.resetCursorPos( CodeEditor.CURSOR_LEFT );
537
- this.cursorToString( cursor, this.code.lines[ln] );
591
+ this.cursorToString( cursor, this.code.lines[ ln ] );
538
592
 
539
- const last_char = (this.code.clientWidth / this.charWidth)|0;
540
- this.setScrollLeft( cursor.position >= last_char ? (cursor.position - last_char) * this.charWidth : 0 );
593
+ const last_char = ( this.code.clientWidth / this.charWidth )|0;
594
+ this.setScrollLeft( cursor.position >= last_char ? ( cursor.position - last_char ) * this.charWidth : 0 );
541
595
  });
542
596
 
543
- this.action('Enter', true, ( ln, cursor, e ) => {
597
+ this.action( 'Enter', true, ( ln, cursor, e ) => {
544
598
 
545
599
  // Add word
546
600
  if( this.isAutoCompleteActive )
@@ -549,63 +603,63 @@ class CodeEditor {
549
603
  return;
550
604
  }
551
605
 
552
- if(e.ctrlKey)
606
+ if( e.ctrlKey )
553
607
  {
554
608
  this.onrun( this.getText() );
555
609
  return;
556
610
  }
557
611
 
558
- this._addUndoStep(cursor);
612
+ this._addUndoStep( cursor );
559
613
 
560
614
  var _c0 = this.getCharAtPos( cursor, -1 );
561
615
  var _c1 = this.getCharAtPos( cursor );
562
616
 
563
- this.code.lines.splice(cursor.line + 1, 0, "");
564
- this.code.lines[cursor.line + 1] = this.code.lines[ln].substr( cursor.position ); // new line (below)
565
- this.code.lines[ln] = this.code.lines[ln].substr( 0, cursor.position ); // line above
566
- this.lineDown(cursor, true);
617
+ this.code.lines.splice( cursor.line + 1, 0, "" );
618
+ this.code.lines[cursor.line + 1] = this.code.lines[ ln ].substr( cursor.position ); // new line (below)
619
+ this.code.lines[ ln ] = this.code.lines[ ln ].substr( 0, cursor.position ); // line above
620
+ this.lineDown( cursor, true );
567
621
 
568
622
  // Check indentation
569
- var spaces = firstNonspaceIndex(this.code.lines[ln]);
623
+ var spaces = firstNonspaceIndex( this.code.lines[ ln ]);
570
624
  var tabs = Math.floor( spaces / this.tabSpaces );
571
625
 
572
626
  if( _c0 == '{' && _c1 == '}' ) {
573
- this.code.lines.splice(cursor.line, 0, "");
574
- this.addSpaceTabs(tabs + 1);
575
- this.code.lines[cursor.line + 1] = " ".repeat(spaces) + this.code.lines[cursor.line + 1];
627
+ this.code.lines.splice( cursor.line, 0, "" );
628
+ this.addSpaceTabs( tabs + 1 );
629
+ this.code.lines[ cursor.line + 1 ] = " ".repeat(spaces) + this.code.lines[ cursor.line + 1 ];
576
630
  } else {
577
- this.addSpaceTabs(tabs);
631
+ this.addSpaceTabs( tabs );
578
632
  }
579
633
 
580
- this.processLines( ln );
634
+ this.processLines();
581
635
  });
582
636
 
583
- this.action('ArrowUp', false, ( ln, cursor, e ) => {
637
+ this.action( 'ArrowUp', false, ( ln, cursor, e ) => {
584
638
 
585
639
  // Move cursor..
586
640
  if( !this.isAutoCompleteActive )
587
641
  {
588
642
  if( e.shiftKey ) {
589
- if(!this.selection)
590
- this.startSelection(cursor);
643
+ if( !this.selection )
644
+ this.startSelection( cursor );
591
645
 
592
- this.selection.toY = (this.selection.toY > 0) ? (this.selection.toY - 1) : 0;
593
- this.cursorToLine(cursor, this.selection.toY);
646
+ this.selection.toY = ( this.selection.toY > 0 ) ? ( this.selection.toY - 1 ) : 0;
647
+ this.cursorToLine( cursor, this.selection.toY );
594
648
 
595
649
  var letter = this.getCharAtPos( cursor );
596
- if(!letter) {
597
- this.selection.toX = this.code.lines[cursor.line].length;
598
- this.cursorToPosition(cursor, this.selection.toX);
650
+ if( !letter ) {
651
+ this.selection.toX = this.code.lines[ cursor.line ].length;
652
+ this.cursorToPosition( cursor, this.selection.toX );
599
653
  }
600
654
 
601
- this.processSelection(null, true);
655
+ this.processSelection( null, true );
602
656
 
603
657
  } else {
604
658
  this.endSelection();
605
659
  this.lineUp();
606
660
  // Go to end of line if out of line
607
661
  var letter = this.getCharAtPos( cursor );
608
- if(!letter) this.actions['End'].callback(cursor.line, cursor, e);
662
+ if( !letter ) this.actions['End'].callback( cursor.line, cursor, e );
609
663
  }
610
664
  }
611
665
  // Move up autocomplete selection
@@ -615,34 +669,34 @@ class CodeEditor {
615
669
  }
616
670
  });
617
671
 
618
- this.action('ArrowDown', false, ( ln, cursor, e ) => {
672
+ this.action( 'ArrowDown', false, ( ln, cursor, e ) => {
619
673
 
620
674
  // Move cursor..
621
675
  if( !this.isAutoCompleteActive )
622
676
  {
623
677
  if( e.shiftKey ) {
624
- if(!this.selection)
625
- this.startSelection(cursor);
678
+ if( !this.selection )
679
+ this.startSelection( cursor );
626
680
 
627
681
  this.selection.toY = this.selection.toY < this.code.lines.length - 1 ? this.selection.toY + 1 : this.code.lines.length - 1;
628
- this.cursorToLine(cursor, this.selection.toY);
682
+ this.cursorToLine( cursor, this.selection.toY );
629
683
 
630
684
  var letter = this.getCharAtPos( cursor );
631
- if(!letter) {
632
- this.selection.toX = Math.max(this.code.lines[cursor.line].length - 1, 0);
685
+ if( !letter ) {
686
+ this.selection.toX = Math.max(this.code.lines[ cursor.line ].length - 1, 0);
633
687
  this.cursorToPosition(cursor, this.selection.toX);
634
688
  }
635
689
 
636
- this.processSelection(null, true);
690
+ this.processSelection( null, true );
637
691
  } else {
638
692
 
639
693
  if( this.code.lines[ ln + 1 ] == undefined )
640
694
  return;
641
695
  this.endSelection();
642
- this.lineDown();
696
+ this.lineDown( cursor );
643
697
  // Go to end of line if out of line
644
698
  var letter = this.getCharAtPos( cursor );
645
- if(!letter) this.actions['End'].callback(cursor.line, cursor, e);
699
+ if( !letter ) this.actions['End'].callback(cursor.line, cursor, e);
646
700
  }
647
701
  }
648
702
  // Move down autocomplete selection
@@ -652,40 +706,41 @@ class CodeEditor {
652
706
  }
653
707
  });
654
708
 
655
- this.action('ArrowLeft', false, ( ln, cursor, e ) => {
709
+ this.action( 'ArrowLeft', false, ( ln, cursor, e ) => {
656
710
 
657
- if(e.metaKey) { // Apple devices (Command)
711
+ if( e.metaKey ) { // Apple devices (Command)
658
712
  e.preventDefault();
659
713
  this.actions[ 'Home' ].callback( ln, cursor, e );
660
714
  }
661
- else if(e.ctrlKey) {
715
+ else if( e.ctrlKey ) {
662
716
  // Get next word
663
717
  const [word, from, to] = this.getWordAtPos( cursor, -1 );
664
718
  var diff = Math.max(cursor.position - from, 1);
665
719
  var substr = word.substr(0, diff);
666
720
  // Selections...
667
- if( e.shiftKey ) if(!this.selection) this.startSelection(cursor);
721
+ if( e.shiftKey ) { if( !this.selection ) this.startSelection( cursor ); }
722
+ else this.endSelection();
668
723
  this.cursorToString(cursor, substr, true);
669
724
  if( e.shiftKey ) this.processSelection();
670
725
  }
671
726
  else {
672
727
  var letter = this.getCharAtPos( cursor, -1 );
673
- if(letter) {
728
+ if( letter ) {
674
729
  if( e.shiftKey ) {
675
- if(!this.selection) this.startSelection(cursor);
676
- if( ((cursor.position - 1) < this.selection.fromX) && this.selection.sameLine() )
730
+ if( !this.selection ) this.startSelection( cursor );
731
+ if( ( ( cursor.position - 1 ) < this.selection.fromX ) && this.selection.sameLine() )
677
732
  this.selection.fromX--;
678
- else if( (cursor.position - 1) == this.selection.fromX && this.selection.sameLine() ) {
733
+ else if( ( cursor.position - 1 ) == this.selection.fromX && this.selection.sameLine() ) {
679
734
  this.cursorToLeft( letter, cursor );
680
735
  this.endSelection();
681
736
  return;
682
737
  }
683
738
  else this.selection.toX--;
684
739
  this.cursorToLeft( letter, cursor );
685
- this.processSelection(null, true);
740
+ this.processSelection( null, true );
686
741
  }
687
742
  else {
688
- if(!this.selection) {
743
+ if( !this.selection ) {
689
744
  this.cursorToLeft( letter, cursor );
690
745
  if( this.useAutoComplete && this.isAutoCompleteActive )
691
746
  this.showAutoCompleteBox( 'foo', cursor );
@@ -693,8 +748,8 @@ class CodeEditor {
693
748
  else {
694
749
  this.selection.invertIfNecessary();
695
750
  this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP );
696
- this.cursorToLine(cursor, this.selection.fromY, true);
697
- this.cursorToPosition(cursor, this.selection.fromX);
751
+ this.cursorToLine( cursor, this.selection.fromY, true );
752
+ this.cursorToPosition( cursor, this.selection.fromX );
698
753
  this.endSelection();
699
754
  }
700
755
  }
@@ -702,7 +757,7 @@ class CodeEditor {
702
757
  else if( cursor.line > 0 ) {
703
758
 
704
759
  if( e.shiftKey ) {
705
- if(!this.selection) this.startSelection(cursor);
760
+ if( !this.selection ) this.startSelection( cursor );
706
761
  }
707
762
 
708
763
  this.lineUp( cursor );
@@ -711,34 +766,35 @@ class CodeEditor {
711
766
  if( e.shiftKey ) {
712
767
  this.selection.toX = cursor.position;
713
768
  this.selection.toY--;
714
- this.processSelection(null, true);
769
+ this.processSelection( null, true );
715
770
  }
716
771
  }
717
772
  }
718
773
  });
719
774
 
720
- this.action('ArrowRight', false, ( ln, cursor, e ) => {
775
+ this.action( 'ArrowRight', false, ( ln, cursor, e ) => {
721
776
 
722
- if(e.metaKey) { // Apple devices (Command)
777
+ if( e.metaKey ) { // Apple devices (Command)
723
778
  e.preventDefault();
724
779
  this.actions[ 'End' ].callback( ln, cursor );
725
- } else if(e.ctrlKey) {
780
+ } else if( e.ctrlKey ) {
726
781
  // Get next word
727
- const [word, from, to] = this.getWordAtPos( cursor );
782
+ const [ word, from, to ] = this.getWordAtPos( cursor );
728
783
  var diff = cursor.position - from;
729
- var substr = word.substr(diff);
784
+ var substr = word.substr( diff );
730
785
  // Selections...
731
- if( e.shiftKey ) if(!this.selection) this.startSelection(cursor);
732
- this.cursorToString(cursor, substr);
786
+ if( e.shiftKey ) { if( !this.selection ) this.startSelection( cursor ); }
787
+ else this.endSelection();
788
+ this.cursorToString( cursor, substr);
733
789
  if( e.shiftKey ) this.processSelection();
734
790
  } else {
735
791
  var letter = this.getCharAtPos( cursor );
736
- if(letter) {
792
+ if( letter ) {
737
793
  if( e.shiftKey ) {
738
- if(!this.selection) this.startSelection(cursor);
794
+ if( !this.selection ) this.startSelection( cursor );
739
795
  var keep_range = false;
740
796
  if( cursor.position == this.selection.fromX ) {
741
- if( (cursor.position + 1) == this.selection.toX && this.selection.sameLine() ) {
797
+ if( ( cursor.position + 1 ) == this.selection.toX && this.selection.sameLine() ) {
742
798
  this.cursorToRight( letter, cursor );
743
799
  this.endSelection();
744
800
  return;
@@ -748,9 +804,9 @@ class CodeEditor {
748
804
  } else this.selection.toX++;
749
805
  }
750
806
  this.cursorToRight( letter, cursor );
751
- this.processSelection(null, keep_range);
807
+ this.processSelection( null, keep_range );
752
808
  }else{
753
- if(!this.selection) {
809
+ if( !this.selection ) {
754
810
  this.cursorToRight( letter, cursor );
755
811
  if( this.useAutoComplete && this.isAutoCompleteActive )
756
812
  this.showAutoCompleteBox( 'foo', cursor );
@@ -759,8 +815,8 @@ class CodeEditor {
759
815
  {
760
816
  this.selection.invertIfNecessary();
761
817
  this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP );
762
- this.cursorToLine(cursor, this.selection.toY);
763
- this.cursorToPosition(cursor, this.selection.toX);
818
+ this.cursorToLine( cursor, this.selection.toY );
819
+ this.cursorToPosition( cursor, this.selection.toX );
764
820
  this.endSelection();
765
821
  }
766
822
  }
@@ -768,18 +824,18 @@ class CodeEditor {
768
824
  else if( this.code.lines[ cursor.line + 1 ] !== undefined ) {
769
825
 
770
826
  if( e.shiftKey ) {
771
- if(!this.selection) this.startSelection(cursor);
827
+ if( !this.selection ) this.startSelection( cursor );
772
828
  e.cancelShift = true;
773
829
  e.keepSelection = true;
774
830
  }
775
831
 
776
832
  this.lineDown( cursor );
777
- this.actions['Home'].callback(cursor.line, cursor, e);
833
+ this.actions['Home'].callback( cursor.line, cursor, e );
778
834
 
779
835
  if( e.shiftKey ) {
780
836
  this.selection.toX = cursor.position;
781
837
  this.selection.toY++;
782
- this.processSelection(null, true);
838
+ this.processSelection( null, true );
783
839
  }
784
840
 
785
841
  this.hideAutoCompleteBox();
@@ -813,14 +869,14 @@ class CodeEditor {
813
869
  // This can be used to empty all text...
814
870
  setText( text = "", lang ) {
815
871
 
816
- let new_lines = text.split('\n');
817
- this.code.lines = [].concat(new_lines);
872
+ let new_lines = text.split( '\n' );
873
+ this.code.lines = [].concat( new_lines );
818
874
 
819
- let cursor = this.cursors.children[0];
875
+ let cursor = this.cursors.children[ 0 ];
820
876
  let lastLine = new_lines.pop();
821
877
 
822
- this.cursorToLine(cursor, new_lines.length); // Already substracted 1
823
- this.cursorToPosition(cursor, lastLine.length);
878
+ this.cursorToLine( cursor, new_lines.length ); // Already substracted 1
879
+ this.cursorToPosition( cursor, lastLine.length );
824
880
  this.processLines();
825
881
 
826
882
  if( lang )
@@ -831,64 +887,64 @@ class CodeEditor {
831
887
 
832
888
  appendText( text ) {
833
889
 
834
- let cursor = this.cursors.children[0];
890
+ let cursor = this.cursors.children[ 0 ];
835
891
  let lidx = cursor.line;
836
892
 
837
893
  if( this.selection ) {
838
- this.deleteSelection(cursor);
894
+ this.deleteSelection( cursor );
839
895
  lidx = cursor.line;
840
896
  }
841
897
 
842
898
  this.endSelection();
843
899
 
844
- const new_lines = text.split('\n');
900
+ const new_lines = text.split( '\n' );
845
901
 
846
902
  // Pasting Multiline...
847
- if(new_lines.length != 1)
903
+ if( new_lines.length != 1 )
848
904
  {
849
905
  let num_lines = new_lines.length;
850
- console.assert(num_lines > 0);
906
+ console.assert( num_lines > 0 );
851
907
  const first_line = new_lines.shift();
852
908
  num_lines--;
853
909
 
854
- const remaining = this.code.lines[lidx].slice(cursor.position);
910
+ const remaining = this.code.lines[ lidx ].slice( cursor.position );
855
911
 
856
912
  // Add first line
857
- this.code.lines[lidx] = [
858
- this.code.lines[lidx].slice(0, cursor.position),
913
+ this.code.lines[ lidx ] = [
914
+ this.code.lines[ lidx ].slice( 0, cursor.position ),
859
915
  first_line
860
916
  ].join('');
861
917
 
862
- this.cursorToPosition(cursor, (cursor.position + first_line.length));
918
+ this.cursorToPosition( cursor, ( cursor.position + first_line.length ) );
863
919
 
864
920
  // Enter next lines...
865
921
 
866
922
  let _text = null;
867
923
 
868
924
  for( var i = 0; i < new_lines.length; ++i ) {
869
- _text = new_lines[i];
870
- this.cursorToLine(cursor, cursor.line++, true);
925
+ _text = new_lines[ i ];
926
+ this.cursorToLine( cursor, cursor.line++, true );
871
927
  // Add remaining...
872
928
  if( i == (new_lines.length - 1) )
873
929
  _text += remaining;
874
- this.code.lines.splice( 1 + lidx + i, 0, _text);
930
+ this.code.lines.splice( 1 + lidx + i, 0, _text );
875
931
  }
876
932
 
877
- if(_text) this.cursorToPosition(cursor, _text.length);
878
- this.cursorToLine(cursor, cursor.line + num_lines);
879
- this.processLines(lidx);
933
+ if( _text ) this.cursorToPosition( cursor, _text.length );
934
+ this.cursorToLine( cursor, cursor.line + num_lines );
935
+ this.processLines();
880
936
  }
881
937
  // Pasting one line...
882
938
  else
883
939
  {
884
- this.code.lines[lidx] = [
885
- this.code.lines[lidx].slice(0, cursor.position),
886
- new_lines[0],
887
- this.code.lines[lidx].slice(cursor.position)
940
+ this.code.lines[ lidx ] = [
941
+ this.code.lines[ lidx ].slice( 0, cursor.position ),
942
+ new_lines[ 0 ],
943
+ this.code.lines[ lidx ].slice( cursor.position )
888
944
  ].join('');
889
945
 
890
- this.cursorToPosition(cursor, (cursor.position + new_lines[0].length));
891
- this.processLine(lidx);
946
+ this.cursorToPosition( cursor, ( cursor.position + new_lines[ 0 ].length ) );
947
+ this.processLine( lidx );
892
948
  }
893
949
  }
894
950
 
@@ -898,18 +954,18 @@ class CodeEditor {
898
954
  const existing = this.addTab(name, true, title);
899
955
  if( !existing )
900
956
  {
901
- text = text.replaceAll('\r', '');
902
- this.code.lines = text.split('\n');
903
- this._changeLanguageFromExtension( LX.getExtension(name) );
957
+ text = text.replaceAll( '\r', '' );
958
+ this.code.lines = text.split( '\n' );
959
+ this._changeLanguageFromExtension( LX.getExtension( name ) );
904
960
  }
905
961
  };
906
962
 
907
- if(file.constructor == String)
963
+ if( file.constructor == String )
908
964
  {
909
965
  let filename = file;
910
966
  LX.request({ url: filename, success: text => {
911
967
 
912
- const name = filename.substring(filename.lastIndexOf('/') + 1);
968
+ const name = filename.substring(filename.lastIndexOf( '/' ) + 1);
913
969
  inner_add_tab( text, name, filename );
914
970
  } });
915
971
  }
@@ -926,18 +982,34 @@ class CodeEditor {
926
982
 
927
983
  _addUndoStep( cursor ) {
928
984
 
929
- var cursor = cursor ?? this.cursors.children[0];
985
+ const d = new Date();
986
+ const current = d.getTime();
987
+
988
+ if( !this._lastTime ) {
989
+ this._lastTime = current;
990
+ } else {
991
+ if( ( current - this._lastTime ) > 3000 ){
992
+ this._lastTime = null;
993
+ } else {
994
+ // If time not enough, reset timer
995
+ this._lastTime = current;
996
+ return;
997
+ }
998
+ }
999
+
1000
+ var cursor = cursor ?? this.cursors.children[ 0 ];
930
1001
 
931
1002
  this.code.undoSteps.push( {
932
- lines: LX.deepCopy(this.code.lines),
933
- cursor: this.saveCursor(cursor),
934
- line: cursor.line
1003
+ lines: LX.deepCopy( this.code.lines ),
1004
+ cursor: this.saveCursor( cursor ),
1005
+ line: cursor.line,
1006
+ position: cursor.position
935
1007
  } );
936
1008
  }
937
1009
 
938
1010
  _changeLanguage( lang ) {
939
1011
 
940
- this.code.lang = lang;
1012
+ this.code.language = lang;
941
1013
  this.highlight = lang;
942
1014
  this._refreshCodeInfo();
943
1015
  this.processLines();
@@ -946,23 +1018,24 @@ class CodeEditor {
946
1018
  _changeLanguageFromExtension( ext ) {
947
1019
 
948
1020
  if( !ext )
949
- return this._changeLanguage( this.code.lang );
950
-
951
- switch(ext.toLowerCase())
952
- {
953
- case 'js': return this._changeLanguage('JavaScript');
954
- case 'cpp': return this._changeLanguage('C++');
955
- case 'h': return this._changeLanguage('C++');
956
- case 'glsl': return this._changeLanguage('GLSL');
957
- case 'css': return this._changeLanguage('CSS');
958
- case 'json': return this._changeLanguage('JSON');
959
- case 'xml': return this._changeLanguage('XML');
960
- case 'wgsl': return this._changeLanguage('WGSL');
961
- case 'py': return this._changeLanguage('Python');
962
- case 'bat': return this._changeLanguage('Batch');
1021
+ return this._changeLanguage( this.code.language );
1022
+
1023
+ switch( ext.toLowerCase() )
1024
+ {
1025
+ case 'js': return this._changeLanguage( 'JavaScript' );
1026
+ case 'cpp': return this._changeLanguage( 'C++' );
1027
+ case 'h': return this._changeLanguage( 'C++' );
1028
+ case 'glsl': return this._changeLanguage( 'GLSL' );
1029
+ case 'css': return this._changeLanguage( 'CSS' );
1030
+ case 'json': return this._changeLanguage( 'JSON' );
1031
+ case 'xml': return this._changeLanguage( 'XML' );
1032
+ case 'wgsl': return this._changeLanguage( 'WGSL' );
1033
+ case 'py': return this._changeLanguage( 'Python' );
1034
+ case 'bat': return this._changeLanguage( 'Batch' );
1035
+ case 'html': return this._changeLanguage( 'HTML' );
963
1036
  case 'txt':
964
1037
  default:
965
- this._changeLanguage('Plain Text');
1038
+ this._changeLanguage( 'Plain Text' );
966
1039
  }
967
1040
  }
968
1041
 
@@ -974,15 +1047,15 @@ class CodeEditor {
974
1047
  panel.ln = 0;
975
1048
  panel.col = 0;
976
1049
 
977
- this._refreshCodeInfo = (ln = panel.ln, col = panel.col) => {
1050
+ this._refreshCodeInfo = ( ln = panel.ln, col = panel.col ) => {
978
1051
  panel.ln = ln + 1;
979
1052
  panel.col = col + 1;
980
1053
  panel.clear();
981
1054
  panel.sameLine();
982
- panel.addLabel(this.code.title, { float: 'right' });
983
- panel.addLabel("Ln " + panel.ln, { width: "64px" });
984
- panel.addLabel("Col " + panel.col, { width: "64px" });
985
- panel.addButton("<b>{ }</b>", this.highlight, (value, event) => {
1055
+ panel.addLabel( this.code.title, { float: 'right' });
1056
+ panel.addLabel( "Ln " + panel.ln, { width: "64px" });
1057
+ panel.addLabel( "Col " + panel.col, { width: "64px" });
1058
+ panel.addButton( "<b>{ }</b>", this.highlight, ( value, event ) => {
986
1059
  LX.addContextMenu( "Language", event, m => {
987
1060
  for( const lang of Object.keys(this.languages) )
988
1061
  m.add( lang, this._changeLanguage.bind(this) );
@@ -1003,8 +1076,8 @@ class CodeEditor {
1003
1076
 
1004
1077
  // Change css a little bit...
1005
1078
  this.gutter.style.height = "calc(100% - 38px)";
1006
- this.root.querySelectorAll('.code').forEach( e => e.style.height = "calc(100% - 6px)" );
1007
- this.root.querySelector('.lexareatabscontent').style.height = "calc(100% - 23px)";
1079
+ this.root.querySelectorAll( '.code' ).forEach( e => e.style.height = "calc(100% - 6px)" );
1080
+ this.root.querySelector( '.lexareatabscontent' ).style.height = "calc(100% - 23px)";
1008
1081
 
1009
1082
  }, 100);
1010
1083
  }
@@ -1015,102 +1088,98 @@ class CodeEditor {
1015
1088
  this.processFocus(false);
1016
1089
 
1017
1090
  LX.addContextMenu( null, e, m => {
1018
- m.add( "Create", this.addTab.bind(this, "unnamed.js", true) );
1019
- m.add( "Load", this.loadTab.bind(this, "unnamed.js", true) );
1091
+ m.add( "Create", this.addTab.bind( this, "unnamed.js", true ) );
1092
+ m.add( "Load", this.loadTab.bind( this, "unnamed.js", true ) );
1020
1093
  });
1021
1094
  }
1022
1095
 
1023
- addTab(name, selected, title) {
1096
+ addTab( name, selected, title ) {
1024
1097
 
1025
- if(this.openedTabs[name])
1098
+ if(this.openedTabs[ name ])
1026
1099
  {
1027
1100
  this.tabs.select( this.code.tabName );
1028
1101
  return true;
1029
1102
  }
1030
1103
 
1031
1104
  // Create code content
1032
- let code = document.createElement('div');
1105
+ let code = document.createElement( 'div' );
1033
1106
  code.className = 'code';
1034
- code.lines = [""];
1035
- code.lang = "Plain Text";
1107
+ code.lines = [ "" ];
1108
+ code.language = "Plain Text";
1036
1109
  code.cursorState = {};
1037
1110
  code.undoSteps = [];
1038
1111
  code.tabName = name;
1039
1112
  code.title = title ?? name;
1040
1113
  code.tokens = {};
1041
- code.customScroll = new LX.vec2;
1114
+ code.style.left = "0px";
1115
+ code.style.top = "0px";
1042
1116
 
1043
- code.addEventListener('dragenter', function(e) {
1117
+ code.addEventListener( 'dragenter', function(e) {
1044
1118
  e.preventDefault();
1045
- this.parentElement.classList.add('dragging');
1119
+ this.parentElement.classList.add( 'dragging' );
1046
1120
  });
1047
- code.addEventListener('dragleave', function(e) {
1121
+ code.addEventListener( 'dragleave', function(e) {
1048
1122
  e.preventDefault();
1049
- this.parentElement.remove('dragging');
1123
+ this.parentElement.remove( 'dragging' );
1050
1124
  });
1051
- code.addEventListener('drop', (e) => {
1125
+ code.addEventListener( 'drop', e => {
1052
1126
  e.preventDefault();
1053
- code.parentElement.classList.remove('dragging');
1127
+ code.parentElement.classList.remove( 'dragging' );
1054
1128
  for( let i = 0; i < e.dataTransfer.files.length; ++i )
1055
- this.loadFile( e.dataTransfer.files[i] );
1129
+ this.loadFile( e.dataTransfer.files[ i ] );
1056
1130
  });
1057
- code.addEventListener('wheel', (e) => {
1058
1131
 
1059
- // Get scroll data
1132
+ this.openedTabs[ name ] = code;
1060
1133
 
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 );
1063
-
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 );
1070
- });
1134
+ const ext = LX.getExtension( name );
1071
1135
 
1072
- this.openedTabs[name] = code;
1136
+ this.tabs.add(name, code, {
1137
+ selected: selected,
1138
+ fixed: (name === '+') ,
1139
+ title: code.title,
1140
+ icon: ext == 'html' ? "fa-solid fa-code orange" :
1141
+ ext == 'js' ? "images/js.png" :
1142
+ ext == 'py' ? "images/py.png" : undefined,
1143
+ onSelect: (e, tabname) => {
1073
1144
 
1074
- this.tabs.add(name, code, { 'selected': selected, 'fixed': (name === '+') , 'title': code.title, 'onSelect': (e, tabname) => {
1145
+ if(tabname == '+')
1146
+ {
1147
+ this._onNewTab( e );
1148
+ return;
1149
+ }
1075
1150
 
1076
- if(tabname == '+')
1077
- {
1078
- this._onNewTab( e );
1079
- return;
1151
+ var cursor = cursor ?? this.cursors.children[ 0 ];
1152
+ this.saveCursor( cursor, this.code.cursorState );
1153
+ this.code = this.openedTabs[ tabname ];
1154
+ this.restoreCursor( cursor, this.code.cursorState );
1155
+ this.endSelection();
1156
+ this._changeLanguageFromExtension( LX.getExtension( tabname ) );
1157
+ this._refreshCodeInfo( cursor.line, cursor.position );
1080
1158
  }
1159
+ });
1081
1160
 
1082
- var cursor = cursor ?? this.cursors.children[0];
1083
- this.saveCursor(cursor, this.code.cursorState);
1084
- this.code = this.openedTabs[tabname];
1085
- this.restoreCursor(cursor, this.code.cursorState);
1086
- this.endSelection();
1087
- this._changeLanguageFromExtension( LX.getExtension(tabname) );
1088
- this._refreshCodeInfo(cursor.line, cursor.position);
1089
-
1090
- // Restore scroll
1091
- this.gutter.scrollLeft = this.getScrollLeft();
1092
- this.gutter.scrollTop = this.getScrollTop();
1093
- }});
1161
+ // Move into the sizer..
1162
+ this.codeSizer.appendChild( code );
1094
1163
 
1095
1164
  this.endSelection();
1096
1165
 
1097
1166
  if( selected )
1098
1167
  {
1099
1168
  this.code = code;
1100
- this.resetCursorPos(CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP);
1169
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP );
1101
1170
  this.processLines();
1102
- doAsync( () => this._refreshCodeInfo(0, 0), 50 );
1171
+ doAsync( () => this._refreshCodeInfo( 0, 0 ), 50 );
1103
1172
  }
1104
1173
  }
1105
1174
 
1106
1175
  loadTab() {
1107
- const input = document.createElement('input');
1176
+ const input = document.createElement( 'input' );
1108
1177
  input.type = 'file';
1109
- document.body.appendChild(input);
1178
+ document.body.appendChild( input );
1110
1179
  input.click();
1111
- input.addEventListener('change', (e) => {
1112
- if (e.target.files[0]) {
1113
- this.loadFile( e.target.files[0] );
1180
+ input.addEventListener('change', e => {
1181
+ if (e.target.files[ 0 ]) {
1182
+ this.loadFile( e.target.files[ 0 ] );
1114
1183
  }
1115
1184
  input.remove();
1116
1185
  });
@@ -1131,7 +1200,7 @@ class CodeEditor {
1131
1200
  if( !e.target.classList.contains('code') ) return;
1132
1201
  if( !this.code ) return;
1133
1202
 
1134
- var cursor = this.cursors.children[0];
1203
+ var cursor = this.cursors.children[ 0 ];
1135
1204
  var code_rect = this.code.getBoundingClientRect();
1136
1205
  var mouse_pos = [(e.clientX - code_rect.x), (e.clientY - code_rect.y)];
1137
1206
 
@@ -1139,7 +1208,7 @@ class CodeEditor {
1139
1208
  if( e.type != 'contextmenu' )
1140
1209
  {
1141
1210
  var ln = (mouse_pos[1] / this.lineHeight)|0;
1142
- if(this.code.lines[ln] == undefined) return;
1211
+ if(this.code.lines[ ln ] == undefined) return;
1143
1212
  }
1144
1213
 
1145
1214
  if( e.type == 'mousedown' )
@@ -1193,7 +1262,7 @@ class CodeEditor {
1193
1262
  this.resetCursorPos( CodeEditor.CURSOR_LEFT );
1194
1263
  this.cursorToPosition( cursor, from );
1195
1264
  this.startSelection( cursor );
1196
- this.selection.selectInline(from, cursor.line, this.measureString(word));
1265
+ this.selection.selectInline( from, cursor.line, this.measureString( word ) );
1197
1266
  this.cursorToString( cursor, word ); // Go to the end of the word
1198
1267
  break;
1199
1268
  // Select entire line
@@ -1220,8 +1289,8 @@ class CodeEditor {
1220
1289
  m.add( "Paste", () => { this._pasteContent(); } );
1221
1290
  m.add( "" );
1222
1291
  m.add( "Format/JSON", () => {
1223
- let json = this.toJSONFormat(this.getText());
1224
- this.code.lines = json.split("\n");
1292
+ let json = this.toJSONFormat( this.getText() );
1293
+ this.code.lines = json.split( "\n" );
1225
1294
  this.processLines();
1226
1295
  } );
1227
1296
  }
@@ -1231,36 +1300,35 @@ class CodeEditor {
1231
1300
  }
1232
1301
  }
1233
1302
 
1234
- processClick(e, skip_refresh = false) {
1303
+ processClick( e, skip_refresh = false ) {
1235
1304
 
1236
- var code_rect = this.code.getBoundingClientRect();
1237
- var position = [(e.clientX - code_rect.x), (e.clientY - code_rect.y)];
1238
- var ln = (position[1] / this.lineHeight)|0;
1305
+ var cursor = this.cursors.children[ 0 ];
1306
+ var code_rect = this.codeScroller.getBoundingClientRect();
1307
+ var position = [( e.clientX - code_rect.x ) + this.getScrollLeft(), (e.clientY - code_rect.y) + this.getScrollTop()];
1308
+ var ln = (position[ 1 ] / this.lineHeight)|0;
1239
1309
 
1240
- if(this.code.lines[ln] == undefined) return;
1310
+ if( this.code.lines[ ln ] == undefined )
1311
+ return;
1241
1312
 
1242
- var cursor = this.cursors.children[0];
1243
- cursor.line = ln;
1244
-
1245
- this.cursorToLine(cursor, ln, true);
1313
+ this.cursorToLine( cursor, ln, true );
1246
1314
 
1247
- var ch = (position[0] / this.charWidth)|0;
1248
- var string = this.code.lines[ln].slice(0, ch);
1249
- this.cursorToPosition(cursor, string.length);
1315
+ var ch = ( ( position[ 0 ] - parseInt( this.xPadding ) + 3) / this.charWidth )|0;
1316
+ var string = this.code.lines[ ln ].slice( 0, ch );
1317
+ this.cursorToPosition( cursor, string.length );
1250
1318
 
1251
1319
  this.hideAutoCompleteBox();
1252
1320
 
1253
- if(!skip_refresh)
1321
+ if( !skip_refresh )
1254
1322
  this._refreshCodeInfo( ln, cursor.position );
1255
1323
  }
1256
1324
 
1257
1325
  processSelection( e, keep_range ) {
1258
1326
 
1259
- var cursor = this.cursors.children[0];
1327
+ var cursor = this.cursors.children[ 0 ];
1260
1328
 
1261
- if(e) this.processClick(e, true);
1329
+ if( e ) this.processClick( e, true );
1262
1330
  if( !this.selection )
1263
- this.startSelection(cursor);
1331
+ this.startSelection( cursor );
1264
1332
 
1265
1333
  // Update selection
1266
1334
  if(!keep_range)
@@ -1280,20 +1348,25 @@ class CodeEditor {
1280
1348
  // Selection goes down...
1281
1349
  if( deltaY >= 0 )
1282
1350
  {
1283
- while( deltaY < (this.selections.childElementCount - 1) )
1351
+ while( deltaY < ( this.selections.childElementCount - 1 ) )
1284
1352
  deleteElement( this.selections.lastChild );
1285
1353
 
1286
1354
  for(let i = fromY; i <= toY; i++){
1287
1355
 
1288
1356
  const sId = i - fromY;
1357
+ const isVisible = i >= this.visibleLinesViewport.x && i <= this.visibleLinesViewport.y;
1358
+ let domEl = null;
1289
1359
 
1290
- // Make sure that the line selection is generated...
1291
- let domEl = this.selections.childNodes[sId];
1292
- if(!domEl)
1360
+ if( isVisible )
1293
1361
  {
1294
- domEl = document.createElement('div');
1295
- domEl.className = "lexcodeselection";
1296
- this.selections.appendChild( domEl );
1362
+ // Make sure that the line selection is generated...
1363
+ domEl = this.selections.childNodes[ sId ];
1364
+ if(!domEl)
1365
+ {
1366
+ domEl = document.createElement( 'div' );
1367
+ domEl.className = "lexcodeselection";
1368
+ this.selections.appendChild( domEl );
1369
+ }
1297
1370
  }
1298
1371
 
1299
1372
  // Compute new width and selection margins
@@ -1302,68 +1375,80 @@ class CodeEditor {
1302
1375
  if(sId == 0) // First line 2 cases (single line, multiline)
1303
1376
  {
1304
1377
  const reverse = fromX > toX;
1305
- if(deltaY == 0) string = !reverse ? this.code.lines[i].substring(fromX, toX) : this.code.lines[i].substring(toX, fromX);
1306
- else string = this.code.lines[i].substr(fromX);
1307
- const pixels = ((reverse && deltaY == 0 ? toX : fromX) * this.charWidth) - this.getScrollLeft();
1308
- domEl.style.left = "calc(" + pixels + "px + " + this.xPadding + ")";
1378
+ if(deltaY == 0) string = !reverse ? this.code.lines[ i ].substring( fromX, toX ) : this.code.lines[ i ].substring(toX, fromX);
1379
+ else string = this.code.lines[ i ].substr( fromX );
1380
+ const pixels = (reverse && deltaY == 0 ? toX : fromX) * this.charWidth;
1381
+ if( isVisible ) domEl.style.left = "calc(" + pixels + "px + " + this.xPadding + ")";
1309
1382
  }
1310
1383
  else
1311
1384
  {
1312
- string = (i == toY) ? this.code.lines[i].substring(0, toX) : this.code.lines[i]; // Last line, any multiple line...
1313
- const pixels = -this.getScrollLeft();
1314
- domEl.style.left = "calc(" + pixels + "px + " + this.xPadding + ")";
1385
+ string = (i == toY) ? this.code.lines[ i ].substring( 0, toX ) : this.code.lines[ i ]; // Last line, any multiple line...
1386
+ if( isVisible ) domEl.style.left = this.xPadding;
1315
1387
  }
1316
1388
 
1317
- const stringWidth = this.measureString(string);
1318
- domEl.style.width = (stringWidth || 8) + "px";
1319
- domEl._top = 4 + i * this.lineHeight;
1320
- domEl.style.top = (domEl._top - this.getScrollTop()) + "px";
1389
+ const stringWidth = this.measureString( string );
1321
1390
  this.selection.chars += stringWidth / this.charWidth;
1391
+
1392
+ if( isVisible )
1393
+ {
1394
+ domEl.style.width = (stringWidth || 8) + "px";
1395
+ domEl._top = i * this.lineHeight;
1396
+ domEl.style.top = domEl._top + "px";
1397
+ }
1322
1398
  }
1323
1399
  }
1324
1400
  else // Selection goes up...
1325
1401
  {
1326
- while( Math.abs(deltaY) < (this.selections.childElementCount - 1) )
1402
+ while( Math.abs( deltaY ) < ( this.selections.childElementCount - 1 ) )
1327
1403
  deleteElement( this.selections.firstChild );
1328
1404
 
1329
- for(let i = toY; i <= fromY; i++){
1405
+ for( let i = toY; i <= fromY; i++ ){
1330
1406
 
1331
1407
  const sId = i - toY;
1408
+ const isVisible = i >= this.visibleLinesViewport.x && i <= this.visibleLinesViewport.y;
1409
+ let domEl = null;
1332
1410
 
1333
- // Make sure that the line selection is generated...
1334
- let domEl = this.selections.childNodes[sId];
1335
- if(!domEl)
1411
+ if( isVisible )
1336
1412
  {
1337
- domEl = document.createElement('div');
1338
- domEl.className = "lexcodeselection";
1339
- this.selections.appendChild( domEl );
1413
+ // Make sure that the line selection is generated...
1414
+ domEl = this.selections.childNodes[ sId ];
1415
+ if(!domEl)
1416
+ {
1417
+ domEl = document.createElement( 'div' );
1418
+ domEl.className = "lexcodeselection";
1419
+ this.selections.appendChild( domEl );
1420
+ }
1340
1421
  }
1341
1422
 
1342
1423
  // Compute new width and selection margins
1343
1424
  let string;
1344
1425
 
1345
- if(sId == 0)
1426
+ if( sId == 0 )
1346
1427
  {
1347
- string = this.code.lines[i].substr(toX);
1348
- const pixels = (toX * this.charWidth) - this.getScrollLeft();
1349
- domEl.style.left = "calc(" + pixels + "px + " + this.xPadding + ")";
1428
+ string = this.code.lines[ i ].substr(toX);
1429
+ const pixels = toX * this.charWidth;
1430
+ if( isVisible ) domEl.style.left = "calc(" + pixels + "px + " + this.xPadding + ")";
1350
1431
  }
1351
1432
  else
1352
1433
  {
1353
- string = (i == fromY) ? this.code.lines[i].substring(0, fromX) : this.code.lines[i]; // Last line, any multiple line...
1354
- domEl.style.left = "calc(" + (-this.getScrollLeft()) + "px + " + this.xPadding + ")";
1434
+ string = (i == fromY) ? this.code.lines[ i ].substring(0, fromX) : this.code.lines[ i ]; // Last line, any multiple line...
1435
+ if( isVisible ) domEl.style.left = this.xPadding;
1355
1436
  }
1356
1437
 
1357
- const stringWidth = this.measureString(string);
1358
- domEl.style.width = (stringWidth || 8) + "px";
1359
- domEl._top = 4 + i * this.lineHeight;
1360
- domEl.style.top = (domEl._top - this.getScrollTop()) + "px";
1438
+ const stringWidth = this.measureString( string );
1361
1439
  this.selection.chars += stringWidth / this.charWidth;
1440
+
1441
+ if( isVisible )
1442
+ {
1443
+ domEl.style.width = (stringWidth || 8) + "px";
1444
+ domEl._top = i * this.lineHeight;
1445
+ domEl.style.top = domEl._top + "px";
1446
+ }
1362
1447
  }
1363
1448
  }
1364
1449
  }
1365
1450
 
1366
- async processKey(e) {
1451
+ async processKey( e ) {
1367
1452
 
1368
1453
  if( !this.code )
1369
1454
  return;
@@ -1373,12 +1458,12 @@ class CodeEditor {
1373
1458
  const skip_undo = e.detail.skip_undo ?? false;
1374
1459
 
1375
1460
  // keys with length > 1 are probably special keys
1376
- if( key.length > 1 && this.specialKeys.indexOf(key) == -1 )
1461
+ if( key.length > 1 && this.specialKeys.indexOf( key ) == -1 )
1377
1462
  return;
1378
1463
 
1379
- let cursor = this.cursors.children[0];
1464
+ let cursor = this.cursors.children[ 0 ];
1380
1465
  let lidx = cursor.line;
1381
- this.code.lines[lidx] = this.code.lines[lidx] ?? "";
1466
+ this.code.lines[ lidx ] = this.code.lines[ lidx ] ?? "";
1382
1467
 
1383
1468
  // Check combinations
1384
1469
 
@@ -1388,22 +1473,22 @@ class CodeEditor {
1388
1473
  case 'a': // select all
1389
1474
  e.preventDefault();
1390
1475
  this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP );
1391
- this.startSelection(cursor);
1476
+ this.startSelection( cursor );
1392
1477
  const nlines = this.code.lines.length - 1;
1393
- this.selection.toX = this.code.lines[nlines].length;
1478
+ this.selection.toX = this.code.lines[ nlines ].length;
1394
1479
  this.selection.toY = nlines;
1395
- this.processSelection(null, true);
1396
- this.cursorToPosition(cursor, this.selection.toX);
1397
- this.cursorToLine(cursor, this.selection.toY);
1480
+ this.processSelection( null, true );
1481
+ this.cursorToPosition( cursor, this.selection.toX );
1482
+ this.cursorToLine( cursor, this.selection.toY );
1398
1483
  break;
1399
1484
  case 'c': // copy
1400
1485
  this._copyContent();
1401
1486
  return;
1402
1487
  case 'd': // duplicate line
1403
1488
  e.preventDefault();
1404
- this.code.lines.splice(lidx, 0, this.code.lines[lidx]);
1489
+ this.code.lines.splice( lidx, 0, this.code.lines[ lidx ] );
1405
1490
  this.lineDown( cursor );
1406
- this.processLines(lidx);
1491
+ this.processLines();
1407
1492
  return;
1408
1493
  case 's': // save
1409
1494
  e.preventDefault();
@@ -1420,12 +1505,9 @@ class CodeEditor {
1420
1505
  return;
1421
1506
  const step = this.code.undoSteps.pop();
1422
1507
  this.code.lines = step.lines;
1423
- cursor.line = step.line;
1424
1508
  this.restoreCursor( cursor, step.cursor );
1425
1509
  this.processLines();
1426
1510
  return;
1427
-
1428
-
1429
1511
  }
1430
1512
  }
1431
1513
 
@@ -1469,23 +1551,9 @@ class CodeEditor {
1469
1551
 
1470
1552
  // Add undo steps
1471
1553
 
1472
- const d = new Date();
1473
- const current = d.getTime();
1474
-
1475
- if( !skip_undo )
1554
+ if( !skip_undo && this.code.lines.length )
1476
1555
  {
1477
- if( !this._lastTime ) {
1478
- this._lastTime = current;
1479
- this._addUndoStep( cursor );
1480
- } else {
1481
- if( (current - this._lastTime) > 3000 && this.code.lines.length){
1482
- this._lastTime = null;
1483
- this._addUndoStep( cursor );
1484
- }else{
1485
- // If time not enough, reset timer
1486
- this._lastTime = current;
1487
- }
1488
- }
1556
+ this._addUndoStep( cursor );
1489
1557
  }
1490
1558
 
1491
1559
  // Some custom cases for word enclosing (), {}, "", '', ...
@@ -1493,7 +1561,7 @@ class CodeEditor {
1493
1561
  const enclosableKeys = ["\"", "'", "(", "{"];
1494
1562
  if( enclosableKeys.indexOf( key ) > -1 )
1495
1563
  {
1496
- if( this.encloseSelectedWordWithKey(key, lidx, cursor) )
1564
+ if( this._encloseSelectedWordWithKey(key, lidx, cursor) )
1497
1565
  return;
1498
1566
  }
1499
1567
 
@@ -1509,14 +1577,14 @@ class CodeEditor {
1509
1577
  // Append key
1510
1578
 
1511
1579
  const isPairKey = (Object.values( this.pairKeys ).indexOf( key ) > -1) && !this.wasKeyPaired;
1512
- const sameKeyNext = isPairKey && (this.code.lines[lidx][cursor.position] === key);
1580
+ const sameKeyNext = isPairKey && (this.code.lines[ lidx ][cursor.position] === key);
1513
1581
 
1514
1582
  if( !sameKeyNext )
1515
1583
  {
1516
- this.code.lines[lidx] = [
1517
- this.code.lines[lidx].slice(0, cursor.position),
1584
+ this.code.lines[ lidx ] = [
1585
+ this.code.lines[ lidx ].slice(0, cursor.position),
1518
1586
  key,
1519
- this.code.lines[lidx].slice(cursor.position)
1587
+ this.code.lines[ lidx ].slice(cursor.position)
1520
1588
  ].join('');
1521
1589
  }
1522
1590
 
@@ -1543,6 +1611,14 @@ class CodeEditor {
1543
1611
  // Update only the current line, since it's only an appended key
1544
1612
  this.processLine( lidx );
1545
1613
 
1614
+ // We are out of the viewport and max length is different? Resize scrollbars...
1615
+ const maxLineLength = this.getMaxLineLength();
1616
+ const numViewportChars = Math.floor( this.codeScroller.clientWidth / this.charWidth );
1617
+ if( maxLineLength >= numViewportChars && maxLineLength != this._lastMaxLineLength )
1618
+ {
1619
+ this.resize( maxLineLength );
1620
+ }
1621
+
1546
1622
  // Manage autocomplete
1547
1623
 
1548
1624
  if( this.useAutoComplete )
@@ -1556,13 +1632,17 @@ class CodeEditor {
1556
1632
 
1557
1633
  async _copyContent() {
1558
1634
 
1559
- let cursor = this.cursors.children[0];
1635
+ let cursor = this.cursors.children[ 0 ];
1560
1636
  let text_to_copy = "";
1561
1637
 
1562
1638
  if( !this.selection ) {
1563
- text_to_copy = "\n" + this.code.lines[cursor.line];
1639
+ text_to_copy = "\n" + this.code.lines[ cursor.line ];
1564
1640
  }
1565
1641
  else {
1642
+
1643
+ // Some selections don't depend on mouse up..
1644
+ if( this.selection ) this.selection.invertIfNecessary();
1645
+
1566
1646
  const separator = "_NEWLINE_";
1567
1647
  let code = this.code.lines.join(separator);
1568
1648
 
@@ -1570,7 +1650,7 @@ class CodeEditor {
1570
1650
  let index = 0;
1571
1651
 
1572
1652
  for(let i = 0; i <= this.selection.fromY; i++)
1573
- index += (i == this.selection.fromY ? this.selection.fromX : this.code.lines[i].length);
1653
+ index += (i == this.selection.fromY ? this.selection.fromX : this.code.lines[ i ].length);
1574
1654
 
1575
1655
  index += this.selection.fromY * separator.length;
1576
1656
  const num_chars = this.selection.chars + (this.selection.toY - this.selection.fromY) * separator.length;
@@ -1579,22 +1659,26 @@ class CodeEditor {
1579
1659
  text_to_copy = lines.join('\n');
1580
1660
  }
1581
1661
 
1582
- navigator.clipboard.writeText(text_to_copy).then(() => console.log("Successfully copied"), (err) => console.error("Error"));
1662
+ navigator.clipboard.writeText( text_to_copy ).then(() => console.log("Successfully copied"), (err) => console.error("Error"));
1583
1663
  }
1584
1664
 
1585
1665
  async _cutContent() {
1586
1666
 
1587
- let cursor = this.cursors.children[0];
1667
+ let cursor = this.cursors.children[ 0 ];
1588
1668
  let lidx = cursor.line;
1589
1669
  let text_to_cut = "";
1590
1670
 
1591
1671
  if( !this.selection ) {
1592
- text_to_cut = "\n" + this.code.lines[cursor.line];
1593
- this.code.lines.splice(lidx, 1);
1594
- this.processLines(lidx);
1672
+ text_to_cut = "\n" + this.code.lines[ cursor.line ];
1673
+ this.code.lines.splice( lidx, 1 );
1674
+ this.processLines();
1595
1675
  this.resetCursorPos( CodeEditor.CURSOR_LEFT );
1596
1676
  }
1597
1677
  else {
1678
+
1679
+ // Some selections don't depend on mouse up..
1680
+ if( this.selection ) this.selection.invertIfNecessary();
1681
+
1598
1682
  const separator = "_NEWLINE_";
1599
1683
  let code = this.code.lines.join(separator);
1600
1684
 
@@ -1602,7 +1686,7 @@ class CodeEditor {
1602
1686
  let index = 0;
1603
1687
 
1604
1688
  for(let i = 0; i <= this.selection.fromY; i++)
1605
- index += (i == this.selection.fromY ? this.selection.fromX : this.code.lines[i].length);
1689
+ index += (i == this.selection.fromY ? this.selection.fromX : this.code.lines[ i ].length);
1606
1690
 
1607
1691
  index += this.selection.fromY * separator.length;
1608
1692
  const num_chars = this.selection.chars + (this.selection.toY - this.selection.fromY) * separator.length;
@@ -1613,7 +1697,7 @@ class CodeEditor {
1613
1697
  this.deleteSelection( cursor );
1614
1698
  }
1615
1699
 
1616
- navigator.clipboard.writeText(text_to_cut).then(() => console.log("Successfully cut"), (err) => console.error("Error"));
1700
+ navigator.clipboard.writeText( text_to_cut ).then(() => console.log("Successfully cut"), (err) => console.error("Error"));
1617
1701
  }
1618
1702
 
1619
1703
  action( key, deleteSelection, fn ) {
@@ -1631,70 +1715,112 @@ class CodeEditor {
1631
1715
  for( let i = 0; i < this.code.lines.length; ++i )
1632
1716
  {
1633
1717
  const linestring = this.code.lines[ i ];
1634
- const tokens = this._getTokensFromString( linestring, true );
1718
+ const tokens = this._getTokensFromLine( linestring, true );
1635
1719
  tokens.forEach( t => this.code.tokens[ t ] = 1 );
1636
1720
  }
1637
1721
  }
1638
1722
 
1639
- processLines( from__legacy ) {
1723
+ toLocalLine( line ) {
1724
+
1725
+ const d = Math.max( this.firstLineInViewport - this.lineScrollMargin.x, 0 );
1726
+ return Math.min( Math.max( line - d, 0 ), this.code.lines.length - 1 );
1727
+ }
1728
+
1729
+ getMaxLineLength() {
1730
+
1731
+ return Math.max(...this.code.lines.map( v => v.length ));
1732
+ }
1640
1733
 
1734
+ processLines( mode ) {
1735
+
1641
1736
  const start = performance.now();
1642
1737
 
1643
1738
  var gutter_html = "";
1644
1739
  var code_html = "";
1645
-
1646
- this.resizeScrollBars();
1647
-
1740
+
1741
+ // Reset all lines content
1648
1742
  this.code.innerHTML = "";
1649
- this.gutter.innerHTML = "";
1650
-
1743
+
1651
1744
  // 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 )
1745
+ const lastScrollTop = this.getScrollTop();
1746
+ this.firstLineInViewport = ( mode ?? CodeEditor.KEEP_VISIBLE_LINES ) & CodeEditor.UPDATE_VISIBLE_LINES ?
1747
+ ( (lastScrollTop / this.lineHeight)|0 ) : this.firstLineInViewport;
1748
+ const totalLinesInViewport = ((this.codeScroller.offsetHeight - 36) / this.lineHeight)|0;
1749
+ this.visibleLinesViewport = new LX.vec2(
1750
+ Math.max( this.firstLineInViewport - this.lineScrollMargin.x, 0 ),
1751
+ Math.min( this.firstLineInViewport + totalLinesInViewport + this.lineScrollMargin.y, this.code.lines.length )
1658
1752
  );
1659
1753
 
1660
- for( let i = viewportRange.x; i < viewportRange.y; ++i )
1754
+ // Add remaining lines if we are near the end of the scroll
1755
+ {
1756
+ const diff = Math.max( this.code.lines.length - this.visibleLinesViewport.y, 0 );
1757
+ if( diff <= this.lineScrollMargin.y )
1758
+ this.visibleLinesViewport.y += diff;
1759
+ }
1760
+
1761
+ // Process visible lines
1762
+ for( let i = this.visibleLinesViewport.x; i < this.visibleLinesViewport.y; ++i )
1661
1763
  {
1662
1764
  gutter_html += "<span>" + (i + 1) + "</span>";
1663
1765
  code_html += this.processLine( i, true );
1664
1766
  }
1665
-
1666
- this.code.innerHTML = code_html;
1667
- this.gutter.innerHTML = gutter_html;
1668
1767
 
1669
- console.log( "Num lines processed: " + (viewportRange.y - viewportRange.x), performance.now() - start );
1768
+ this.code.innerHTML = code_html;
1769
+
1770
+ // console.log("RANGE:", this.visibleLinesViewport);
1771
+ // console.log( "Num lines processed:", (this.visibleLinesViewport.y - this.visibleLinesViewport.x), performance.now() - start );
1772
+ // console.log("--------------------------------------------");
1773
+
1774
+ // Update scroll data
1775
+ this.codeScroller.scrollTop = lastScrollTop;
1776
+ this.code.style.top = ( this.visibleLinesViewport.x * this.lineHeight ) + "px";
1777
+
1778
+ // Update selections
1779
+ if( this.selection )
1780
+ this.processSelection( null, true );
1781
+
1782
+ // Clear tmp vars
1783
+ delete this._buildingString;
1784
+ delete this._pendingString;
1785
+
1786
+ this.resize();
1670
1787
  }
1671
1788
 
1672
1789
  processLine( linenum, force ) {
1673
1790
 
1674
- delete this._buildingString; // multi-line strings not supported by now
1791
+ const local_line_num = this.toLocalLine( linenum );
1792
+ const gutter_line = "<span class='line-gutter'>" + (linenum + 1) + "</span>";
1793
+
1794
+ const UPDATE_LINE = ( html ) => {
1795
+ if( !force ) // Single line update
1796
+ this.code.childNodes[ local_line_num ].innerHTML = gutter_line + html;
1797
+ else // Update all lines at once
1798
+ return "<pre>" + ( gutter_line + html ) + "</pre>";
1799
+ }
1800
+
1801
+ // multi-line strings not supported by now
1802
+ delete this._buildingString;
1803
+ delete this._pendingString;
1675
1804
 
1676
- // It's allowed to process only 1 line to optimize
1677
1805
  let linestring = this.code.lines[ linenum ];
1678
1806
 
1807
+ // Single line
1679
1808
  if( !force )
1680
1809
  {
1681
- var pre = document.createElement('pre');
1810
+ deleteElement( this.code.childNodes[ local_line_num ] );
1811
+ this.code.insertChildAtIndex( document.createElement( 'pre' ), local_line_num );
1812
+ }
1682
1813
 
1683
- // Single code line
1684
- deleteElement( this.code.childNodes[ linenum ] );
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 );
1814
+ // Early out check for no highlighting languages
1815
+ if( this.highlight == 'Plain Text' )
1816
+ {
1817
+ return UPDATE_LINE( linestring );
1692
1818
  }
1693
1819
 
1694
- const tokensToEvaluate = this._getTokensFromString( linestring );
1820
+ const tokensToEvaluate = this._getTokensFromLine( linestring );
1695
1821
 
1696
1822
  if( !tokensToEvaluate.length )
1697
- return "<pre></pre>";
1823
+ return "<pre><span class='line-gutter'>" + linenum + "</span></pre>";
1698
1824
 
1699
1825
  var line_inner_html = "";
1700
1826
 
@@ -1702,58 +1828,49 @@ class CodeEditor {
1702
1828
  for( var i = 0; i < tokensToEvaluate.length; ++i )
1703
1829
  {
1704
1830
  let it = i - 1;
1705
- let prev = tokensToEvaluate[it];
1831
+ let prev = tokensToEvaluate[ it ];
1706
1832
  while( prev == ' ' ) {
1707
1833
  it--;
1708
- prev = tokensToEvaluate[it];
1834
+ prev = tokensToEvaluate[ it ];
1709
1835
  }
1710
1836
 
1711
1837
  it = i + 1;
1712
- let next = tokensToEvaluate[it];
1838
+ let next = tokensToEvaluate[ it ];
1713
1839
  while( next == ' ' || next == '"' ) {
1714
1840
  it++;
1715
- next = tokensToEvaluate[it];
1841
+ next = tokensToEvaluate[ it ];
1716
1842
  }
1717
1843
 
1718
- const token = tokensToEvaluate[i];
1844
+ const token = tokensToEvaluate[ i ];
1719
1845
 
1720
1846
  if( this.languages[ this.highlight ].blockComments ?? true )
1721
1847
  {
1722
- if( token.substr(0, 2) == '/*' )
1848
+ if( token.substr( 0, 2 ) == '/*' )
1723
1849
  this._buildingBlockComment = true;
1724
- if( token.substr(token.length - 2) == '*/' )
1850
+ if( token.substr( token.length - 2 ) == '*/' )
1725
1851
  delete this._buildingBlockComment;
1726
1852
  }
1727
1853
 
1728
- line_inner_html += this.evaluateToken(token, prev, next);
1854
+ line_inner_html += this._evaluateToken( token, prev, next, (i == tokensToEvaluate.length - 1) );
1729
1855
  }
1730
1856
 
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
- }
1857
+ return UPDATE_LINE( line_inner_html );
1741
1858
  }
1742
1859
 
1743
1860
  _processTokens( tokens, offset = 0 ) {
1744
1861
 
1745
1862
  if( this.highlight == 'C++' )
1746
1863
  {
1747
- var idx = tokens.slice(offset).findIndex( (value, index) => this.isNumber(value) );
1864
+ var idx = tokens.slice( offset ).findIndex( ( value, index ) => this.isNumber( value ) );
1748
1865
  if( idx > -1 )
1749
1866
  {
1750
1867
  idx += offset; // Add offset to compute within the whole array of tokens
1751
- let data = tokens[idx] + tokens[++idx];
1868
+ let data = tokens[ idx ] + tokens[ ++idx ];
1752
1869
  while( this.isNumber( data ) )
1753
1870
  {
1754
1871
  tokens[ idx - 1 ] += tokens[ idx ];
1755
1872
  tokens.splice( idx, 1 );
1756
- data += tokens[idx];
1873
+ data += tokens[ idx ];
1757
1874
  }
1758
1875
  // Scan for numbers again
1759
1876
  return this._processTokens( tokens, idx );
@@ -1763,16 +1880,43 @@ class CodeEditor {
1763
1880
  return tokens;
1764
1881
  }
1765
1882
 
1766
- _getTokensFromString( linestring, skipNonWords ) {
1883
+ _lineHasComment( linestring ) {
1767
1884
 
1768
- // Check if line comment
1769
1885
  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;
1886
+ const idx = linestring.indexOf( singleLineCommentToken );
1887
+
1888
+ if( idx > -1 )
1889
+ {
1890
+ const stringKeys = Object.values( this.stringKeys );
1891
+ // Count times we started a string BEFORE the comment
1892
+ var err = false;
1893
+ err |= stringKeys.some( function(v) {
1894
+ var re = new RegExp( v, "g" );
1895
+ var matches = (linestring.substring( 0, idx ).match( re ) || []);
1896
+ return (matches.length % 2) !== 0;
1897
+ } );
1898
+ err |= stringKeys.some( function(v) {
1899
+ var re = new RegExp( v, "g" );
1900
+ var matches = (linestring.substring( idx ).match( re ) || []);
1901
+ return (matches.length % 2) !== 0;
1902
+ } );
1903
+ return err ? undefined : idx;
1904
+ }
1905
+ }
1906
+
1907
+ _getTokensFromLine( linestring, skipNonWords ) {
1908
+
1909
+ // Check if line comment
1910
+ const ogLine = linestring;
1911
+ const hasCommentIdx = this._lineHasComment( linestring );
1912
+
1913
+ if( hasCommentIdx != undefined )
1914
+ {
1915
+ linestring = ogLine.substring( 0, hasCommentIdx );
1916
+ }
1917
+
1918
+ // const usesBlockComments = this.languages[ this.highlight ].blockComments ?? true;
1773
1919
 
1774
- // const tokens = linestring.split(' ').join('¬ ¬').split('¬'); // trick to split without losing spaces
1775
-
1776
1920
  let tokensToEvaluate = []; // store in a temp array so we know prev and next tokens...
1777
1921
 
1778
1922
  const pushToken = function( t ) {
@@ -1781,7 +1925,7 @@ class CodeEditor {
1781
1925
  tokensToEvaluate.push( t );
1782
1926
  };
1783
1927
 
1784
- let iter = linestring.matchAll(/(::|[\[\](){}<>.,;:*"'%@ ])/g);
1928
+ let iter = linestring.matchAll(/(::|[\[\](){}<>.,;:*"'%@!/= ])/g);
1785
1929
  let subtokens = iter.next();
1786
1930
  if( subtokens.value )
1787
1931
  {
@@ -1790,8 +1934,8 @@ class CodeEditor {
1790
1934
  {
1791
1935
  const _pt = linestring.substring(idx, subtokens.value.index);
1792
1936
  if( _pt.length ) pushToken( _pt );
1793
- pushToken( subtokens.value[0] );
1794
- idx = subtokens.value.index + subtokens.value[0].length;
1937
+ pushToken( subtokens.value[ 0 ] );
1938
+ idx = subtokens.value.index + subtokens.value[ 0 ].length;
1795
1939
  subtokens = iter.next();
1796
1940
  if(!subtokens.value) {
1797
1941
  const _at = linestring.substring(idx);
@@ -1821,10 +1965,10 @@ class CodeEditor {
1821
1965
  // if( block ) continue;
1822
1966
  // }
1823
1967
 
1824
- if( has_comment.length > 1 && !skipNonWords )
1825
- pushToken( singleLineCommentToken + has_comment[1] );
1826
-
1827
- // console.log( tokensToEvaluate );
1968
+ if( hasCommentIdx != undefined )
1969
+ {
1970
+ pushToken( ogLine.substring( hasCommentIdx ) );
1971
+ }
1828
1972
 
1829
1973
  return this._processTokens( tokensToEvaluate );
1830
1974
  }
@@ -1834,45 +1978,51 @@ class CodeEditor {
1834
1978
  return kindArray[this.highlight] && kindArray[this.highlight][token] != undefined;
1835
1979
  }
1836
1980
 
1837
- evaluateToken( token, prev, next ) {
1981
+ _evaluateToken( token, prev, next, isLastToken ) {
1982
+
1983
+ const highlight = this.highlight.replace(/\s/g, '').replaceAll("+", "p").toLowerCase();
1984
+ const customStringKeys = Object.assign( {}, this.stringKeys );
1985
+
1986
+ if( highlight == 'cpp' && prev && prev.includes('#') ) // preprocessor code..
1987
+ {
1988
+ customStringKeys['@<'] = '>';
1989
+ }
1990
+
1991
+ // Manage strings
1992
+ this._stringEnded = false;
1993
+ if( this._buildingString != undefined )
1994
+ {
1995
+ const idx = Object.values(customStringKeys).indexOf( token );
1996
+ this._stringEnded = (idx > -1) && (idx == Object.values(customStringKeys).indexOf( customStringKeys[ '@' + this._buildingString ] ));
1997
+ }
1998
+ else if( customStringKeys[ '@' + token ] )
1999
+ {
2000
+ // Start new string
2001
+ this._buildingString = token;
2002
+ }
1838
2003
 
1839
2004
  if(token == ' ')
1840
2005
  {
2006
+ if( this._buildingString != undefined )
2007
+ {
2008
+ this._appendStringToken( token );
2009
+ return "";
2010
+ }
1841
2011
  return token;
1842
2012
  }
1843
2013
  else
1844
2014
  {
1845
- let stringEnded = false;
1846
- let highlight = this.highlight.replace(/\s/g, '').replaceAll("+", "p").toLowerCase();
1847
-
1848
2015
  const singleLineCommentToken = this.languages[ this.highlight ].singleLineCommentToken ?? this.defaultSingleLineCommentToken;
1849
2016
  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
-
2017
+
1869
2018
  let token_classname = "";
2019
+ let discardToken = false;
1870
2020
 
1871
2021
  if( this._buildingBlockComment != undefined )
1872
2022
  token_classname = "cm-com";
1873
2023
 
1874
2024
  else if( this._buildingString != undefined )
1875
- token_classname = "cm-str";
2025
+ discardToken = this._appendStringToken( token );
1876
2026
 
1877
2027
  else if( this._mustHightlightWord( token, this.keywords ) )
1878
2028
  token_classname = "cm-kwd";
@@ -1886,28 +2036,28 @@ class CodeEditor {
1886
2036
  else if( this._mustHightlightWord( token, this.symbols ) )
1887
2037
  token_classname = "cm-sym";
1888
2038
 
1889
- else if( token.substr(0, 2) == singleLineCommentToken )
2039
+ else if( token.substr( 0, singleLineCommentToken.length ) == singleLineCommentToken )
1890
2040
  token_classname = "cm-com";
1891
2041
 
1892
- else if( usesBlockComments && token.substr(0, 2) == '/*' )
2042
+ else if( usesBlockComments && token.substr( 0, 2 ) == '/*' )
1893
2043
  token_classname = "cm-com";
1894
2044
 
1895
- else if( usesBlockComments && token.substr(token.length - 2) == '*/' )
2045
+ else if( usesBlockComments && token.substr( token.length - 2 ) == '*/' )
1896
2046
  token_classname = "cm-com";
1897
2047
 
1898
- else if( this.isNumber(token) || this.isNumber( token.replace(/[px]|[em]|%/g,'') ) )
2048
+ else if( this.isNumber( token ) || this.isNumber( token.replace(/[px]|[em]|%/g,'') ) )
1899
2049
  token_classname = "cm-dec";
1900
2050
 
1901
- else if( this.isCSSClass(token, prev, next) )
2051
+ else if( this._isCSSClass( token, prev, next ) )
1902
2052
  token_classname = "cm-kwd";
1903
2053
 
1904
- else if ( this.isType(token, prev, next) )
2054
+ else if ( this._isType( token, prev, next ) )
1905
2055
  token_classname = "cm-typ";
1906
2056
 
1907
- else if ( highlight == 'batch' && (token == '@' || prev == ':' || prev == '@') )
2057
+ else if ( highlight == 'batch' && ( token == '@' || prev == ':' || prev == '@' ) )
1908
2058
  token_classname = "cm-kwd";
1909
2059
 
1910
- else if ( highlight == 'cpp' && token.includes('#') ) // C++ preprocessor
2060
+ else if ( highlight == 'cpp' && token.includes( '#' ) ) // C++ preprocessor
1911
2061
  token_classname = "cm-ppc";
1912
2062
 
1913
2063
  else if ( highlight == 'cpp' && prev == '<' && (next == '>' || next == '*') ) // Defining template type in C++
@@ -1922,16 +2072,53 @@ class CodeEditor {
1922
2072
  else if ( highlight == 'css' && prev == undefined && next == ':' ) // CSS attribute
1923
2073
  token_classname = "cm-typ";
1924
2074
 
1925
- else if ( token[0] != '@' && next == '(' )
2075
+ else if ( token[ 0 ] != '@' && token[ 0 ] != ',' && next == '(' )
1926
2076
  token_classname = "cm-mtd";
1927
2077
 
1928
- this._buildingString = stringEnded ? undefined : this._buildingString;
2078
+
2079
+ // We finished constructing a string
2080
+ if( this._buildingString && ( this._stringEnded || isLastToken ) )
2081
+ {
2082
+ token = this._getCurrentString();
2083
+ token_classname = "cm-str";
2084
+ discardToken = false;
2085
+ }
2086
+
2087
+ // Update state
2088
+ this._buildingString = this._stringEnded ? undefined : this._buildingString;
2089
+
2090
+ if( discardToken )
2091
+ return "";
2092
+
2093
+ token = token.replace( "<", "&lt;" );
2094
+ token = token.replace( ">", "&gt;" );
2095
+
2096
+ // No highlighting, no need to put it inside another span..
2097
+ if( !token_classname.length )
2098
+ return token;
1929
2099
 
1930
2100
  return "<span class='" + highlight + " " + token_classname + "'>" + token + "</span>";
1931
2101
  }
1932
2102
  }
1933
2103
 
1934
- isCSSClass( token, prev, next ) {
2104
+ _appendStringToken( token ) {
2105
+
2106
+ if( !this._pendingString )
2107
+ this._pendingString = "";
2108
+
2109
+ this._pendingString += token;
2110
+
2111
+ return true;
2112
+ }
2113
+
2114
+ _getCurrentString() {
2115
+
2116
+ const chars = this._pendingString;
2117
+ delete this._pendingString;
2118
+ return chars;
2119
+ }
2120
+
2121
+ _isCSSClass( token, prev, next ) {
1935
2122
  return this.highlight == 'CSS' && prev == '.';
1936
2123
  }
1937
2124
 
@@ -1948,7 +2135,7 @@ class CodeEditor {
1948
2135
  return token.length && token != ' ' && !Number.isNaN(+token);
1949
2136
  }
1950
2137
 
1951
- isType( token, prev, next ) {
2138
+ _isType( token, prev, next ) {
1952
2139
 
1953
2140
  // Common case
1954
2141
  if( this._mustHightlightWord( token, this.types ) )
@@ -1972,7 +2159,7 @@ class CodeEditor {
1972
2159
  }
1973
2160
  }
1974
2161
 
1975
- encloseSelectedWordWithKey( key, lidx, cursor ) {
2162
+ _encloseSelectedWordWithKey( key, lidx, cursor ) {
1976
2163
 
1977
2164
  if( !this.selection || (this.selection.fromY != this.selection.toY) )
1978
2165
  return false;
@@ -1980,10 +2167,10 @@ class CodeEditor {
1980
2167
  this.selection.invertIfNecessary();
1981
2168
 
1982
2169
  // Insert first..
1983
- this.code.lines[lidx] = [
1984
- this.code.lines[lidx].slice(0, this.selection.fromX),
2170
+ this.code.lines[ lidx ] = [
2171
+ this.code.lines[ lidx ].slice(0, this.selection.fromX),
1985
2172
  key,
1986
- this.code.lines[lidx].slice(this.selection.fromX)
2173
+ this.code.lines[ lidx ].slice(this.selection.fromX)
1987
2174
  ].join('');
1988
2175
 
1989
2176
  // Go to the end of the word
@@ -2000,10 +2187,10 @@ class CodeEditor {
2000
2187
  }
2001
2188
 
2002
2189
  // Insert the other
2003
- this.code.lines[lidx] = [
2004
- this.code.lines[lidx].slice(0, cursor.position),
2190
+ this.code.lines[ lidx ] = [
2191
+ this.code.lines[ lidx ].slice(0, cursor.position),
2005
2192
  key,
2006
- this.code.lines[lidx].slice(cursor.position)
2193
+ this.code.lines[ lidx ].slice(cursor.position)
2007
2194
  ].join('');
2008
2195
 
2009
2196
  // Recompute and reposition current selection
@@ -2018,19 +2205,19 @@ class CodeEditor {
2018
2205
  return true;
2019
2206
  }
2020
2207
 
2021
- lineUp(cursor, resetLeft) {
2208
+ lineUp( cursor, resetLeft ) {
2022
2209
 
2023
- cursor = cursor ?? this.cursors.children[0];
2210
+ cursor = cursor ?? this.cursors.children[ 0 ];
2024
2211
  cursor.line--;
2025
- cursor.line = Math.max(0, cursor.line);
2026
- this.cursorToTop(cursor, resetLeft);
2212
+ cursor.line = Math.max( 0, cursor.line );
2213
+ this.cursorToTop( cursor, resetLeft );
2027
2214
  }
2028
2215
 
2029
- lineDown(cursor, resetLeft) {
2216
+ lineDown( cursor, resetLeft ) {
2030
2217
 
2031
- cursor = cursor ?? this.cursors.children[0];
2218
+ cursor = cursor ?? this.cursors.children[ 0 ];
2032
2219
  cursor.line++;
2033
- this.cursorToBottom(cursor, resetLeft);
2220
+ this.cursorToBottom( cursor, resetLeft );
2034
2221
  }
2035
2222
 
2036
2223
  restartBlink() {
@@ -2057,40 +2244,41 @@ class CodeEditor {
2057
2244
  this.selections.classList.add('show');
2058
2245
 
2059
2246
  // Create new selection instance
2060
- this.selection = new ISelection(this, cursor.position, cursor.line);
2247
+ this.selection = new CodeSelection(this, cursor.position, cursor.line);
2061
2248
  }
2062
2249
 
2063
2250
  deleteSelection( cursor ) {
2064
2251
 
2065
2252
  // I think it's not necessary but...
2066
- if(this.disableEdition)
2253
+ if( this.disableEdition )
2067
2254
  return;
2068
2255
 
2069
2256
  // Some selections don't depend on mouse up..
2070
- if(this.selection) this.selection.invertIfNecessary();
2257
+ if( this.selection ) this.selection.invertIfNecessary();
2071
2258
 
2072
2259
  const separator = "_NEWLINE_";
2073
- let code = this.code.lines.join(separator);
2260
+ let code = this.code.lines.join( separator );
2074
2261
 
2075
2262
  // Get linear start index
2076
2263
  let index = 0;
2077
2264
  for(let i = 0; i <= this.selection.fromY; i++)
2078
- index += (i == this.selection.fromY ? this.selection.fromX : this.code.lines[i].length);
2265
+ index += (i == this.selection.fromY ? this.selection.fromX : this.code.lines[ i ].length);
2079
2266
 
2080
2267
  index += this.selection.fromY * separator.length;
2081
2268
 
2082
2269
  const num_chars = this.selection.chars + (this.selection.toY - this.selection.fromY) * separator.length;
2083
- const pre = code.slice(0, index);
2084
- const post = code.slice(index + num_chars);
2085
-
2086
- this.code.lines = (pre + post).split(separator);
2087
- this.processLines(this.selection.fromY);
2270
+ const pre = code.slice( 0, index );
2271
+ const post = code.slice( index + num_chars );
2088
2272
 
2089
- this.cursorToLine(cursor, this.selection.fromY, true);
2090
- this.cursorToPosition(cursor, this.selection.fromX);
2273
+ this.code.lines = ( pre + post ).split( separator );
2274
+
2275
+ this.cursorToLine( cursor, this.selection.fromY, true );
2276
+ this.cursorToPosition( cursor, this.selection.fromX );
2277
+
2091
2278
  this.endSelection();
2092
2279
 
2093
- this._refreshCodeInfo(cursor.line, cursor.position);
2280
+ this.processLines();
2281
+ this._refreshCodeInfo( cursor.line, cursor.position );
2094
2282
  }
2095
2283
 
2096
2284
  endSelection() {
@@ -2102,19 +2290,20 @@ class CodeEditor {
2102
2290
 
2103
2291
  cursorToRight( key, cursor ) {
2104
2292
 
2105
- if(!key) return;
2106
- cursor = cursor ?? this.cursors.children[0];
2293
+ if( !key ) return;
2294
+ cursor = cursor ?? this.cursors.children[ 0 ];
2107
2295
  cursor._left += this.charWidth;
2108
- cursor.style.left = "calc(" + (cursor._left - this.getScrollLeft()) + "px + " + this.xPadding + ")";
2296
+ cursor.style.left = "calc( " + cursor._left + "px + " + this.xPadding + " )";
2109
2297
  cursor.position++;
2298
+
2110
2299
  this.restartBlink();
2111
2300
  this._refreshCodeInfo( cursor.line, cursor.position );
2112
2301
 
2113
2302
  // Add horizontal scroll
2114
2303
 
2115
2304
  doAsync(() => {
2116
- var last_char = ((this.code.clientWidth) / this.charWidth)|0;
2117
- if( cursor.position >= last_char )
2305
+ var viewportSizeX = ( this.codeScroller.clientWidth + this.getScrollLeft() ) - 48; // Gutter offset
2306
+ if( (cursor.position * this.charWidth) >= viewportSizeX )
2118
2307
  this.setScrollLeft( this.getScrollLeft() + this.charWidth );
2119
2308
  });
2120
2309
  }
@@ -2122,30 +2311,30 @@ class CodeEditor {
2122
2311
  cursorToLeft( key, cursor ) {
2123
2312
 
2124
2313
  if(!key) return;
2125
- cursor = cursor ?? this.cursors.children[0];
2314
+ cursor = cursor ?? this.cursors.children[ 0 ];
2126
2315
  cursor._left -= this.charWidth;
2127
- cursor._left = Math.max(cursor._left, 0);
2128
- cursor.style.left = "calc(" + (cursor._left - this.getScrollLeft()) + "px + " + this.xPadding + ")";
2316
+ cursor._left = Math.max( cursor._left, 0 );
2317
+ cursor.style.left = "calc( " + cursor._left + "px + " + this.xPadding + " )";
2129
2318
  cursor.position--;
2130
- cursor.position = Math.max(cursor.position, 0);
2319
+ cursor.position = Math.max( cursor.position, 0 );
2131
2320
  this.restartBlink();
2132
2321
  this._refreshCodeInfo( cursor.line, cursor.position );
2133
2322
 
2134
2323
  // Add horizontal scroll
2135
2324
 
2136
2325
  doAsync(() => {
2137
- var first_char = (this.getScrollLeft() / this.charWidth)|0;
2138
- if( (cursor.position - 1) < first_char )
2326
+ var viewportSizeX = this.getScrollLeft(); // Gutter offset
2327
+ if( ( ( cursor.position - 1 ) * this.charWidth ) < viewportSizeX )
2139
2328
  this.setScrollLeft( this.getScrollLeft() - this.charWidth );
2140
2329
  });
2141
2330
  }
2142
2331
 
2143
2332
  cursorToTop( cursor, resetLeft = false ) {
2144
2333
 
2145
- cursor = cursor ?? this.cursors.children[0];
2334
+ cursor = cursor ?? this.cursors.children[ 0 ];
2146
2335
  cursor._top -= this.lineHeight;
2147
- cursor._top = Math.max(cursor._top, 4);
2148
- cursor.style.top = "calc(" + (cursor._top - this.getScrollTop()) + "px)";
2336
+ cursor._top = Math.max(cursor._top, 0);
2337
+ cursor.style.top = "calc(" + cursor._top + "px)";
2149
2338
  this.restartBlink();
2150
2339
 
2151
2340
  if(resetLeft)
@@ -2154,7 +2343,7 @@ class CodeEditor {
2154
2343
  this._refreshCodeInfo( cursor.line, cursor.position );
2155
2344
 
2156
2345
  doAsync(() => {
2157
- var first_line = (this.getScrollTop() / this.lineHeight)|0;
2346
+ var first_line = ( this.getScrollTop() / this.lineHeight )|0;
2158
2347
  if( (cursor.line - 1) < first_line )
2159
2348
  this.setScrollTop( this.getScrollTop() - this.lineHeight );
2160
2349
  });
@@ -2162,9 +2351,9 @@ class CodeEditor {
2162
2351
 
2163
2352
  cursorToBottom( cursor, resetLeft = false ) {
2164
2353
 
2165
- cursor = cursor ?? this.cursors.children[0];
2354
+ cursor = cursor ?? this.cursors.children[ 0 ];
2166
2355
  cursor._top += this.lineHeight;
2167
- cursor.style.top = "calc(" + (cursor._top - this.getScrollTop()) + "px)";
2356
+ cursor.style.top = "calc(" + cursor._top + "px)";
2168
2357
  this.restartBlink();
2169
2358
 
2170
2359
  if(resetLeft)
@@ -2173,7 +2362,7 @@ class CodeEditor {
2173
2362
  this._refreshCodeInfo( cursor.line, cursor.position );
2174
2363
 
2175
2364
  doAsync(() => {
2176
- var last_line = ((this.code.parentElement.offsetHeight - 32) / this.lineHeight)|0;
2365
+ var last_line = ( ( this.codeScroller.offsetHeight + this.getScrollTop() ) / this.lineHeight )|0;
2177
2366
  if( cursor.line >= last_line )
2178
2367
  this.setScrollTop( this.getScrollTop() + this.lineHeight );
2179
2368
  });
@@ -2181,7 +2370,7 @@ class CodeEditor {
2181
2370
 
2182
2371
  cursorToString( cursor, text, reverse ) {
2183
2372
 
2184
- cursor = cursor ?? this.cursors.children[0];
2373
+ cursor = cursor ?? this.cursors.children[ 0 ];
2185
2374
  for( let char of text )
2186
2375
  reverse ? this.cursorToLeft(char) : this.cursorToRight(char);
2187
2376
  }
@@ -2190,20 +2379,20 @@ class CodeEditor {
2190
2379
 
2191
2380
  cursor.position = position;
2192
2381
  cursor._left = position * this.charWidth;
2193
- cursor.style.left = "calc(" + (cursor._left - this.getScrollLeft()) + "px + " + this.xPadding + ")";
2382
+ cursor.style.left = "calc(" + cursor._left + "px + " + this.xPadding + ")";
2194
2383
  }
2195
2384
 
2196
2385
  cursorToLine( cursor, line, resetLeft = false ) {
2197
2386
 
2198
2387
  cursor.line = line;
2199
- cursor._top = 4 + this.lineHeight * line;
2200
- cursor.style.top = (cursor._top - this.getScrollTop()) + "px";
2388
+ cursor._top = this.lineHeight * line;
2389
+ cursor.style.top = cursor._top + "px";
2201
2390
  if(resetLeft) this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
2202
2391
  }
2203
2392
 
2204
2393
  saveCursor( cursor, state = {} ) {
2205
2394
 
2206
- var cursor = cursor ?? this.cursors.children[0];
2395
+ var cursor = cursor ?? this.cursors.children[ 0 ];
2207
2396
  state.top = cursor._top;
2208
2397
  state.left = cursor._left;
2209
2398
  state.line = cursor.line;
@@ -2213,19 +2402,19 @@ class CodeEditor {
2213
2402
 
2214
2403
  restoreCursor( cursor, state ) {
2215
2404
 
2216
- cursor = cursor ?? this.cursors.children[0];
2405
+ cursor = cursor ?? this.cursors.children[ 0 ];
2217
2406
  cursor.line = state.line ?? 0;
2218
- cursor.position = state.charPos ?? 0;
2407
+ cursor.position = state.position ?? 0;
2219
2408
 
2220
2409
  cursor._left = state.left ?? 0;
2221
2410
  cursor.style.left = "calc(" + (cursor._left - this.getScrollLeft()) + "px + " + this.xPadding + ")";
2222
- cursor._top = state.top ?? 4;
2411
+ cursor._top = state.top ?? 0;
2223
2412
  cursor.style.top = "calc(" + (cursor._top - this.getScrollTop()) + "px)";
2224
2413
  }
2225
2414
 
2226
2415
  resetCursorPos( flag, cursor ) {
2227
2416
 
2228
- cursor = cursor ?? this.cursors.children[0];
2417
+ cursor = cursor ?? this.cursors.children[ 0 ];
2229
2418
 
2230
2419
  if( flag & CodeEditor.CURSOR_LEFT )
2231
2420
  {
@@ -2236,7 +2425,7 @@ class CodeEditor {
2236
2425
 
2237
2426
  if( flag & CodeEditor.CURSOR_TOP )
2238
2427
  {
2239
- cursor._top = 4;
2428
+ cursor._top = 0;
2240
2429
  cursor.style.top = (cursor._top - this.getScrollTop()) + "px";
2241
2430
  cursor.line = 0;
2242
2431
  }
@@ -2261,160 +2450,163 @@ class CodeEditor {
2261
2450
 
2262
2451
  getScrollLeft() {
2263
2452
 
2264
- if(!this.code) return 0;
2265
- return this.code.customScroll.x;
2453
+ if(!this.codeScroller) return 0;
2454
+ return this.codeScroller.scrollLeft;
2266
2455
  }
2267
2456
 
2268
2457
  getScrollTop() {
2269
2458
 
2270
- if(!this.code) return 0;
2271
- return this.code.customScroll.y;
2459
+ if(!this.codeScroller) return 0;
2460
+ return this.codeScroller.scrollTop;
2272
2461
  }
2273
2462
 
2274
- setScrollLeft( value, keepScrollBar ) {
2463
+ setScrollLeft( value ) {
2275
2464
 
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";
2465
+ if(!this.codeScroller) return;
2466
+ this.codeScroller.scrollLeft = value;
2467
+ this.setScrollBarValue( 'horizontal', 0 );
2468
+ }
2284
2469
 
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
- }
2470
+ setScrollTop( value ) {
2471
+
2472
+ if(!this.codeScroller) return;
2473
+ this.codeScroller.scrollTop = value;
2474
+ this.setScrollBarValue( 'vertical' );
2475
+ }
2291
2476
 
2292
- // Update cursor
2293
- var cursor = this.cursors.children[0];
2294
- cursor.style.left = "calc( " + (cursor._left - value) + "px + " + this.xPadding + ")";
2477
+ resize( pMaxLength ) {
2478
+
2479
+ setTimeout( () => {
2295
2480
 
2296
- // Update selection
2297
- for( let s of this.selections.childNodes ) {
2298
- s.style.left = "calc( " + (s._left - value) + "px + " + this.xPadding + ")";
2299
- }
2481
+ // Update max viewport
2482
+ const maxLineLength = pMaxLength ?? this.getMaxLineLength();
2483
+ const scrollWidth = maxLineLength * this.charWidth;
2484
+ const scrollHeight = this.code.lines.length * this.lineHeight;
2300
2485
 
2301
- this.code.customScroll.x = value;
2302
- }
2486
+ this._lastMaxLineLength = maxLineLength;
2303
2487
 
2304
- setScrollTop( value, keepScrollBar ) {
2305
-
2306
- if(!this.code) return;
2488
+ this.codeSizer.style.minWidth = scrollWidth + "px";
2489
+ this.codeSizer.style.minHeight = scrollHeight + "px";
2307
2490
 
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";
2491
+ this.resizeScrollBars();
2316
2492
 
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
- }
2493
+ // console.warn("Resize editor viewport");
2325
2494
 
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;
2495
+ }, 10 );
2336
2496
  }
2337
2497
 
2338
2498
  resizeScrollBars() {
2339
2499
 
2340
- const numViewportLines = Math.floor( (this.code.parentElement.offsetHeight - 36) / this.lineHeight );
2500
+ const totalLinesInViewport = ((this.codeScroller.offsetHeight - 36) / this.lineHeight)|0;
2341
2501
 
2342
- if( numViewportLines > this.code.lines.length )
2502
+ if( totalLinesInViewport > this.code.lines.length )
2343
2503
  {
2344
- this.scrollbar.classList.add( 'scrollbar-unused' );
2345
- this.tabs.area.root.classList.remove( 'with-vscrollbar' );
2504
+ this.codeScroller.classList.remove( 'with-vscrollbar' );
2505
+ this.vScrollbar.root.classList.add( 'scrollbar-unused' );
2346
2506
  }
2347
2507
  else
2348
2508
  {
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) + "%";
2509
+ this.codeScroller.classList.add( 'with-vscrollbar' );
2510
+ this.vScrollbar.root.classList.remove( 'scrollbar-unused' );
2511
+ this.vScrollbar.thumb.size = (totalLinesInViewport / this.code.lines.length);
2512
+ this.vScrollbar.thumb.style.height = (this.vScrollbar.thumb.size * 100.0) + "%";
2353
2513
  }
2354
2514
 
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);
2515
+ const numViewportChars = Math.floor( this.codeScroller.clientWidth / this.charWidth );
2516
+ const maxLineLength = this._lastMaxLineLength;
2358
2517
 
2359
2518
  if( numViewportChars > maxLineLength )
2360
2519
  {
2361
- this.hScrollbar.classList.add( 'scrollbar-unused' );
2362
- this.tabs.area.root.classList.remove( 'with-hscrollbar' );
2520
+ this.codeScroller.classList.remove( 'with-hscrollbar' );
2521
+ this.hScrollbar.root.classList.add( 'scrollbar-unused' );
2363
2522
  }
2364
2523
  else
2365
2524
  {
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) + "%";
2525
+ this.codeScroller.classList.add( 'with-hscrollbar' );
2526
+ this.hScrollbar.root.classList.remove( 'scrollbar-unused' );
2527
+ this.hScrollbar.thumb.size = (numViewportChars / maxLineLength);
2528
+ this.hScrollbar.thumb.style.width = (this.hScrollbar.thumb.size * 100.0) + "%";
2370
2529
  }
2371
2530
  }
2372
2531
 
2373
- setScrollBarValue( value, type = 'vertical' ) {
2532
+ setScrollBarValue( type = 'vertical', value ) {
2374
2533
 
2375
2534
  if( type == 'vertical' )
2376
2535
  {
2377
- const scrollHeight = this.scrollbarThumb.parentElement.offsetHeight;
2378
- const scrollBarHeight = this.scrollbarThumb.offsetHeight;
2536
+ const scrollBarHeight = this.vScrollbar.thumb.parentElement.offsetHeight;
2537
+ const scrollThumbHeight = this.vScrollbar.thumb.offsetHeight;
2379
2538
 
2380
- value = LX.UTILS.clamp( value, 0, ( scrollHeight - scrollBarHeight ) );
2381
-
2382
- this.scrollbarThumb._top = value;
2383
- this.scrollbarThumb.style.top = this.scrollbarThumb._top + "px";
2539
+ const scrollHeight = this.codeScroller.scrollHeight - this.codeScroller.clientHeight;
2540
+ const currentScroll = this.codeScroller.scrollTop;
2541
+
2542
+ this.vScrollbar.thumb._top = ( currentScroll / scrollHeight ) * ( scrollBarHeight - scrollThumbHeight );
2543
+ this.vScrollbar.thumb.style.top = this.vScrollbar.thumb._top + "px";
2384
2544
  }
2385
2545
  else
2386
2546
  {
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";
2547
+ this.codeScroller.scrollLeft += value;
2548
+
2549
+ const scrollBarWidth = this.hScrollbar.thumb.parentElement.offsetWidth;
2550
+ const scrollThumbWidth = this.hScrollbar.thumb.offsetWidth;
2551
+
2552
+ const scrollWidth = this.codeScroller.scrollWidth - this.codeScroller.clientWidth;
2553
+ const currentScroll = this.codeScroller.scrollLeft;
2554
+
2555
+ this.hScrollbar.thumb._left = ( currentScroll / scrollWidth ) * ( scrollBarWidth - scrollThumbWidth );
2556
+ this.hScrollbar.thumb.style.left = this.hScrollbar.thumb._left + "px";
2394
2557
  }
2395
2558
  }
2396
2559
 
2397
- applyHorizontalScrollFromScrollBar( value ) {
2560
+ updateHorizontalScrollFromScrollBar( value ) {
2561
+
2562
+ value = this.hScrollbar.thumb._left - value;
2563
+
2564
+ // Move scrollbar thumb
2565
+
2566
+ const scrollBarWidth = this.hScrollbar.thumb.parentElement.offsetWidth;
2567
+ const scrollThumbWidth = this.hScrollbar.thumb.offsetWidth;
2398
2568
 
2399
- this.setScrollBarValue( value, 'horizontal');
2400
- this.setScrollLeft( value / this.hScrollbarThumb.size, true );
2569
+ this.hScrollbar.thumb._left = LX.UTILS.clamp( value, 0, ( scrollBarWidth - scrollThumbWidth ) );
2570
+ this.hScrollbar.thumb.style.left = this.hScrollbar.thumb._left + "px";
2571
+
2572
+ // Scroll code
2573
+
2574
+ const scrollWidth = this.codeScroller.scrollWidth - this.codeScroller.clientWidth;
2575
+ const currentScroll = (this.hScrollbar.thumb._left * scrollWidth) / ( scrollBarWidth - scrollThumbWidth );
2576
+ this.codeScroller.scrollLeft = currentScroll;
2577
+
2578
+
2579
+ this._discardScroll = true;
2401
2580
  }
2402
2581
 
2403
- applyVerticalScrollFromScrollBar( value ) {
2582
+ updateVerticalScrollFromScrollBar( value ) {
2583
+
2584
+ value = this.vScrollbar.thumb._top - value;
2585
+
2586
+ // Move scrollbar thumb
2587
+
2588
+ const scrollBarHeight = this.vScrollbar.thumb.parentElement.offsetHeight;
2589
+ const scrollThumbHeight = this.vScrollbar.thumb.offsetHeight;
2590
+
2591
+ this.vScrollbar.thumb._top = LX.UTILS.clamp( value, 0, ( scrollBarHeight - scrollThumbHeight ) );
2592
+ this.vScrollbar.thumb.style.top = this.vScrollbar.thumb._top + "px";
2593
+
2594
+ // Scroll code
2404
2595
 
2405
- this.setScrollBarValue( value );
2406
- this.setScrollTop( value / this.scrollbarThumb.size, true );
2596
+ const scrollHeight = this.codeScroller.scrollHeight - this.codeScroller.clientHeight;
2597
+ const currentScroll = (this.vScrollbar.thumb._top * scrollHeight) / ( scrollBarHeight - scrollThumbHeight );
2598
+ this.codeScroller.scrollTop = currentScroll;
2407
2599
  }
2408
2600
 
2409
2601
  getCharAtPos( cursor, offset = 0 ) {
2410
2602
 
2411
- cursor = cursor ?? this.cursors.children[0];
2412
- return this.code.lines[cursor.line][cursor.position + offset];
2603
+ cursor = cursor ?? this.cursors.children[ 0 ];
2604
+ return this.code.lines[ cursor.line ][cursor.position + offset];
2413
2605
  }
2414
2606
 
2415
2607
  getWordAtPos( cursor, offset = 0 ) {
2416
2608
 
2417
- cursor = cursor ?? this.cursors.children[0];
2609
+ cursor = cursor ?? this.cursors.children[ 0 ];
2418
2610
  const col = cursor.line;
2419
2611
  const words = this.code.lines[col];
2420
2612
 
@@ -2449,7 +2641,7 @@ class CodeEditor {
2449
2641
  var rect = test.getBoundingClientRect();
2450
2642
  deleteElement( test );
2451
2643
  const bb = [Math.floor(rect.width), Math.floor(rect.height)];
2452
- return get_bb ? bb : bb[0];
2644
+ return get_bb ? bb : bb[ 0 ];
2453
2645
  }
2454
2646
 
2455
2647
  measureString( str ) {
@@ -2462,10 +2654,10 @@ class CodeEditor {
2462
2654
  var script = document.createElement('script');
2463
2655
  script.type = 'module';
2464
2656
  script.innerHTML = code;
2465
- // script.src = url[i] + ( version ? "?version=" + version : "" );
2657
+ // script.src = url[ i ] + ( version ? "?version=" + version : "" );
2466
2658
  script.async = false;
2467
2659
  // script.onload = function(e) { };
2468
- document.getElementsByTagName('head')[0].appendChild(script);
2660
+ document.getElementsByTagName('head')[ 0 ].appendChild(script);
2469
2661
  }
2470
2662
 
2471
2663
  toJSONFormat( text ) {
@@ -2473,19 +2665,19 @@ class CodeEditor {
2473
2665
  let params = text.split(":");
2474
2666
 
2475
2667
  for(let i = 0; i < params.length; i++) {
2476
- let key = params[i].split(',');
2668
+ let key = params[ i ].split(',');
2477
2669
  if(key.length > 1) {
2478
2670
  if(key[key.length-1].includes("]"))
2479
2671
  continue;
2480
2672
  key = key[key.length-1];
2481
2673
  }
2482
- else if(key[0].includes("}"))
2674
+ else if(key[ 0 ].includes("}"))
2483
2675
  continue;
2484
2676
  else
2485
- key = key[0];
2677
+ key = key[ 0 ];
2486
2678
  key = key.replaceAll(/[{}\n\r]/g,"").replaceAll(" ","")
2487
- if(key[0] != '"' && key[key.length - 1] != '"') {
2488
- params[i] = params[i].replace(key, '"' + key + '"');
2679
+ if(key[ 0 ] != '"' && key[key.length - 1] != '"') {
2680
+ params[ i ] = params[ i ].replace(key, '"' + key + '"');
2489
2681
  }
2490
2682
  }
2491
2683
 
@@ -2515,11 +2707,11 @@ class CodeEditor {
2515
2707
 
2516
2708
  // Add language special keys...
2517
2709
  suggestions = suggestions.concat(
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 ] ) ?? []
2710
+ Object.keys( this.builtin[ this.highlight ] ?? {} ),
2711
+ Object.keys( this.keywords[ this.highlight ] ?? {} ),
2712
+ Object.keys( this.statementsAndDeclarations[ this.highlight ] ?? {} ),
2713
+ Object.keys( this.types[ this.highlight ] ?? {} ),
2714
+ Object.keys( this.utils[ this.highlight ] ?? {} )
2523
2715
  );
2524
2716
 
2525
2717
  // Add words in current tab plus remove current word
@@ -2622,7 +2814,7 @@ class CodeEditor {
2622
2814
 
2623
2815
  for( let i = 0; i < this.autocomplete.childElementCount; ++i )
2624
2816
  {
2625
- const child = this.autocomplete.childNodes[i];
2817
+ const child = this.autocomplete.childNodes[ i ];
2626
2818
  if( child.classList.contains('selected') )
2627
2819
  {
2628
2820
  var word = "";