lexgui 0.1.12 → 0.1.14

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.
@@ -102,11 +102,10 @@ class CodeEditor {
102
102
 
103
103
  constructor( area, options = {} ) {
104
104
 
105
- CodeEditor.__instances.push( this );
105
+ window.editor = this;
106
106
 
107
- this.disable_edition = options.disable_edition ?? false;
107
+ CodeEditor.__instances.push( this );
108
108
 
109
- this.skip_info = options.skip_info;
110
109
  this.base_area = area;
111
110
  this.area = new LX.Area( { className: "lexcodeeditor", height: "auto", no_append: true } );
112
111
 
@@ -127,14 +126,21 @@ class CodeEditor {
127
126
  this.root.tabIndex = -1;
128
127
  area.attach( this.root );
129
128
 
130
- this.root.addEventListener( 'keydown', this.processKey.bind(this), true);
129
+ this.skipCodeInfo = options.skip_info ?? false;
130
+ this.disableEdition = options.disable_edition ?? false;
131
+
132
+ if( !this.disableEdition )
133
+ {
134
+ this.root.addEventListener( 'keydown', this.processKey.bind(this), true);
135
+ this.root.addEventListener( 'focus', this.processFocus.bind(this, true) );
136
+ this.root.addEventListener( 'focusout', this.processFocus.bind(this, false) );
137
+ }
138
+
131
139
  this.root.addEventListener( 'mousedown', this.processMouse.bind(this) );
132
140
  this.root.addEventListener( 'mouseup', this.processMouse.bind(this) );
133
141
  this.root.addEventListener( 'mousemove', this.processMouse.bind(this) );
134
142
  this.root.addEventListener( 'click', this.processMouse.bind(this) );
135
143
  this.root.addEventListener( 'contextmenu', this.processMouse.bind(this) );
136
- this.root.addEventListener( 'focus', this.processFocus.bind(this, true) );
137
- this.root.addEventListener( 'focusout', this.processFocus.bind(this, false) );
138
144
 
139
145
  // Cursors and selection
140
146
 
@@ -160,6 +166,7 @@ class CodeEditor {
160
166
  cursor.style.top = "4px";
161
167
  cursor.position = 0;
162
168
  cursor.line = 0;
169
+ cursor.print = (function() { console.log( this.line, this.position ) }).bind(cursor);
163
170
  this.cursors.appendChild(cursor);
164
171
  }
165
172
 
@@ -192,11 +199,22 @@ class CodeEditor {
192
199
  this.tabSpaces = 4;
193
200
  this.maxUndoSteps = 16;
194
201
  this.lineHeight = 20;
195
- this.charWidth = 8;//this.measureChar();
202
+ this.charWidth = 8; //this.measureChar();
196
203
  this._lastTime = null;
197
204
 
205
+ this.pairKeys = {
206
+ "\"": "\"",
207
+ "'": "'",
208
+ "(": ")",
209
+ "{": "}",
210
+ "[": "]"
211
+ };
212
+
213
+ // Scan tokens..
214
+ setInterval( this.scanWordSuggestions.bind(this), 2000 );
215
+
198
216
  this.languages = [
199
- 'Plain Text', 'JavaScript', 'CSS', 'GLSL', 'WGSL', 'JSON', 'XML'
217
+ 'Plain Text', 'JavaScript', 'C++', 'CSS', 'GLSL', 'WGSL', 'JSON', 'XML', 'Python'
200
218
  ];
201
219
  this.specialKeys = [
202
220
  'Backspace', 'Enter', 'ArrowUp', 'ArrowDown',
@@ -204,43 +222,54 @@ class CodeEditor {
204
222
  'End', 'Tab', 'Escape'
205
223
  ];
206
224
  this.keywords = {
207
- 'JavaScript': ['var', 'let', 'const', 'this', 'in', 'of', 'true', 'false', 'new', 'function', 'NaN', 'static', 'class', 'constructor', 'null', 'typeof', 'debugger'],
225
+ 'JavaScript': ['var', 'let', 'const', 'this', 'in', 'of', 'true', 'false', 'new', 'function', 'NaN', 'static', 'class', 'constructor', 'null', 'typeof', 'debugger', 'abstract',
226
+ 'arguments', 'extends', 'instanceof'],
227
+ 'C++': ['int', 'float', 'bool', 'const', 'static_cast', 'dynamic_cast', 'new', 'delete', 'void', 'true', 'false', 'auto', 'struct', 'typedef'],
208
228
  'GLSL': ['true', 'false', 'function', 'int', 'float', 'vec2', 'vec3', 'vec4', 'mat2x2', 'mat3x3', 'mat4x4', 'struct'],
209
229
  'CSS': ['body', 'html', 'canvas', 'div', 'input', 'span', '.'],
210
230
  'WGSL': ['var', 'let', 'true', 'false', 'fn', 'bool', 'u32', 'i32', 'f16', 'f32', 'vec2f', 'vec3f', 'vec4f', 'mat2x2f', 'mat3x3f', 'mat4x4f', 'array', 'atomic', 'struct',
211
231
  'sampler', 'sampler_comparison', 'texture_depth_2d', 'texture_depth_2d_array', 'texture_depth_cube', 'texture_depth_cube_array', 'texture_depth_multisampled_2d',
212
232
  'texture_external', 'texture_1d', 'texture_2d', 'texture_2d_array', 'texture_3d', 'texture_cube', 'texture_cube_array', 'texture_storage_1d', 'texture_storage_2d',
213
233
  'texture_storage_2d_array', 'texture_storage_3d'],
214
- 'JSON': []
234
+ 'Python': ['False', 'def', 'None', 'True', 'in', 'is', 'and', 'lambda', 'nonlocal', 'not', 'or']
235
+ };
236
+ this.utils = { // These ones don't have hightlight, used as suggestions to autocomplete only...
237
+ 'JavaScript': ['querySelector', 'body', 'addEventListener', 'removeEventListener', 'remove', 'sort', 'keys', 'filter', 'isNaN', 'parseFloat', 'parseInt', 'EPSILON', 'isFinite',
238
+ 'bind', 'prototype', 'length', 'assign', 'entries', 'values', 'concat', 'substring', 'substr', 'splice', 'slice', 'buffer', 'appendChild', 'createElement', 'prompt',
239
+ 'alert'],
240
+ 'WGSL': ['textureSample'],
241
+ 'Python': ['abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'delattr', 'dict', 'dir', 'divmod',
242
+ 'enumerate', 'eval', 'exec', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance',
243
+ 'issubclass', 'iter', 'len', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'range', 'repr',
244
+ 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
215
245
  };
216
- this.utils = {
217
- 'JavaScript': ['querySelector', 'body', 'addEventListener', 'removeEventListener', 'remove', 'sort', 'filter', 'isNaN', 'parseFloat', 'parseInt'],
218
- 'GLSL': [],
219
- 'CSS': [],
220
- 'WGSL': [],
221
- 'JSON': []
246
+ this.types = {
247
+ 'JavaScript': ['Object', 'String', 'Function', 'Boolean', 'Symbol', 'Error', 'Number', 'TextEncoder', 'TextDecoder'],
248
+ 'Python': ['int', 'type', 'float', 'map', 'list', 'ArithmeticError', 'AssertionError', 'AttributeError', 'Exception', 'EOFError', 'FloatingPointError', 'GeneratorExit',
249
+ 'ImportError', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'NotImplementedError', 'OSError',
250
+ 'OverflowError', 'ReferenceError', 'RuntimeError', 'StopIteration', 'SyntaxError', 'TabError', 'SystemError', 'SystemExit', 'TypeError', 'UnboundLocalError',
251
+ 'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError', 'ValueError', 'ZeroDivisionError' ]
222
252
  };
223
253
  this.builtin = {
224
- 'JavaScript': ['document', 'console', 'window', 'navigator', 'Object', 'Function', 'Boolean', 'Symbol', 'Error'],
254
+ 'JavaScript': ['document', 'console', 'window', 'navigator', 'performance'],
225
255
  'CSS': ['*', '!important'],
226
- 'GLSL': [],
227
- 'WGSL': [],
228
- 'JSON': []
256
+ 'C++': ['vector', 'list', 'map']
229
257
  };
230
258
  this.statementsAndDeclarations = {
231
- 'JavaScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import',
232
- 'from', 'throw', 'async', 'try', 'catch'],
259
+ 'JavaScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await'],
260
+ 'C++': ['std', 'for', 'if', 'else', 'return', 'continue', 'break', 'case', 'switch', 'while', 'glm'],
233
261
  'GLSL': ['for', 'if', 'else', 'return', 'continue', 'break'],
234
262
  'WGSL': ['const','for', 'if', 'else', 'return', 'continue', 'break', 'storage', 'read', 'uniform'],
235
- 'JSON': [],
236
- 'CSS': []
263
+ 'Python': ['if', 'raise', 'del', 'import', 'return', 'elif', 'try', 'else', 'while', 'as', 'except', 'with', 'assert', 'finally', 'yield', 'break', 'for', 'class', 'continue', 'global', 'pass']
237
264
  };
238
265
  this.symbols = {
239
266
  'JavaScript': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '??'],
267
+ 'C++': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '::'],
240
268
  'JSON': ['[', ']', '{', '}', '(', ')'],
241
269
  'GLSL': ['[', ']', '{', '}', '(', ')'],
242
270
  'WGSL': ['[', ']', '{', '}', '(', ')', '->'],
243
- 'CSS': ['{', '}', '(', ')', '*']
271
+ 'CSS': ['{', '}', '(', ')', '*'],
272
+ 'Python': ['<', '>', '[', ']', '(', ')', '=']
244
273
  };
245
274
 
246
275
  // Action keys
@@ -265,6 +294,8 @@ class CodeEditor {
265
294
  this.code.lines[ln] = sliceChar( this.code.lines[ln], cursor.position - 1 );
266
295
  this.cursorToLeft( letter );
267
296
  this.processLine(ln);
297
+ if( this.useAutoComplete )
298
+ this.showAutoCompleteBox( 'foo', cursor );
268
299
  }
269
300
  else if(this.code.lines[ln - 1] != undefined) {
270
301
  this.lineUp();
@@ -323,19 +354,20 @@ class CodeEditor {
323
354
 
324
355
  this.resetCursorPos( CodeEditor.CURSOR_LEFT );
325
356
  if(idx > 0) this.cursorToString(cursor, prestring);
326
- this._refreshCodeInfo(cursor.line + 1, cursor.position);
357
+ this._refreshCodeInfo(cursor.line, cursor.position);
327
358
  this.code.scrollLeft = 0;
328
359
 
329
- if( !e.shiftKey || e.cancelShift )
330
- return;
331
-
332
- // Get last selection range
333
- if(this.selection)
360
+ if( e.shiftKey && !e.cancelShift )
361
+ {
362
+ // Get last selection range
363
+ if(this.selection)
334
364
  last_pos += this.selection.chars;
335
365
 
336
- this.startSelection(cursor);
337
- var string = this.code.lines[ln].substring(idx, last_pos);
338
- this.selection.selectInline(idx, cursor.line, this.measureString(string));
366
+ this.startSelection(cursor);
367
+ var string = this.code.lines[ln].substring(idx, last_pos);
368
+ this.selection.selectInline(idx, cursor.line, this.measureString(string));
369
+ } else
370
+ this.endSelection();
339
371
  });
340
372
 
341
373
  this.action('End', false, ( ln, cursor, e ) => {
@@ -346,7 +378,8 @@ class CodeEditor {
346
378
  if(!this.selection)
347
379
  this.startSelection(cursor);
348
380
  this.selection.selectInline(cursor.position, cursor.line, this.measureString(string));
349
- }
381
+ } else
382
+ this.endSelection();
350
383
 
351
384
  this.resetCursorPos( CodeEditor.CURSOR_LEFT );
352
385
  this.cursorToString( cursor, this.code.lines[ln] );
@@ -403,11 +436,18 @@ class CodeEditor {
403
436
  if( e.shiftKey ) {
404
437
  if(!this.selection)
405
438
  this.startSelection(cursor);
406
- this.selection.fromX = cursor.position;
407
- this.selection.toY = this.selection.toY > 0 ? this.selection.toY - 1 : 0;
408
- this.processSelection(null, true);
409
- this.cursorToPosition(cursor, this.selection.fromX);
439
+
440
+ this.selection.toY = (this.selection.toY > 0) ? (this.selection.toY - 1) : 0;
410
441
  this.cursorToLine(cursor, this.selection.toY);
442
+
443
+ var letter = this.getCharAtPos( cursor );
444
+ if(!letter) {
445
+ this.selection.toX = (this.code.lines[cursor.line].length - 1);
446
+ this.cursorToPosition(cursor, this.selection.toX);
447
+ }
448
+
449
+ this.processSelection(null, true);
450
+
411
451
  } else {
412
452
  this.endSelection();
413
453
  this.lineUp();
@@ -431,11 +471,17 @@ class CodeEditor {
431
471
  if( e.shiftKey ) {
432
472
  if(!this.selection)
433
473
  this.startSelection(cursor);
434
- this.selection.toX = cursor.position;
474
+
435
475
  this.selection.toY = this.selection.toY < this.code.lines.length - 1 ? this.selection.toY + 1 : this.code.lines.length - 1;
436
- this.processSelection(null, true);
437
- this.cursorToPosition(cursor, this.selection.toX);
438
476
  this.cursorToLine(cursor, this.selection.toY);
477
+
478
+ var letter = this.getCharAtPos( cursor );
479
+ if(!letter) {
480
+ this.selection.toX = Math.max(this.code.lines[cursor.line].length - 1, 0);
481
+ this.cursorToPosition(cursor, this.selection.toX);
482
+ }
483
+
484
+ this.processSelection(null, true);
439
485
  } else {
440
486
 
441
487
  if( this.code.lines[ ln + 1 ] == undefined )
@@ -487,7 +533,11 @@ class CodeEditor {
487
533
  this.processSelection(null, true);
488
534
  }
489
535
  else {
490
- if(!this.selection) this.cursorToLeft( letter, cursor );
536
+ if(!this.selection) {
537
+ this.cursorToLeft( letter, cursor );
538
+ if( this.useAutoComplete && this.isAutoCompleteActive )
539
+ this.showAutoCompleteBox( 'foo', cursor );
540
+ }
491
541
  else {
492
542
  this.selection.invertIfNecessary();
493
543
  this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP );
@@ -546,7 +596,11 @@ class CodeEditor {
546
596
  this.cursorToRight( letter, cursor );
547
597
  this.processSelection(null, keep_range);
548
598
  }else{
549
- if(!this.selection) this.cursorToRight( letter, cursor );
599
+ if(!this.selection) {
600
+ this.cursorToRight( letter, cursor );
601
+ if( this.useAutoComplete && this.isAutoCompleteActive )
602
+ this.showAutoCompleteBox( 'foo', cursor );
603
+ }
550
604
  else
551
605
  {
552
606
  this.selection.invertIfNecessary();
@@ -562,6 +616,7 @@ class CodeEditor {
562
616
  this.lineDown( cursor );
563
617
  e.cancelShift = true;
564
618
  this.actions['Home'].callback(cursor.line, cursor, e);
619
+ this.hideAutoCompleteBox();
565
620
 
566
621
  if( e.shiftKey ) {
567
622
  if(!this.selection) this.startSelection(cursor);
@@ -728,11 +783,14 @@ class CodeEditor {
728
783
  switch(ext.toLowerCase())
729
784
  {
730
785
  case 'js': return this._changeLanguage('JavaScript');
786
+ case 'cpp': return this._changeLanguage('C++');
787
+ case 'h': return this._changeLanguage('C++');
731
788
  case 'glsl': return this._changeLanguage('GLSL');
732
789
  case 'css': return this._changeLanguage('CSS');
733
790
  case 'json': return this._changeLanguage('JSON');
734
791
  case 'xml': return this._changeLanguage('XML');
735
792
  case 'wgsl': return this._changeLanguage('WGSL');
793
+ case 'py': return this._changeLanguage('Python');
736
794
  case 'txt':
737
795
  default:
738
796
  this._changeLanguage('Plain Text');
@@ -741,20 +799,20 @@ class CodeEditor {
741
799
 
742
800
  _createPanelInfo() {
743
801
 
744
- if( !this.skip_info )
802
+ if( !this.skipCodeInfo )
745
803
  {
746
804
  let panel = new LX.Panel({ className: "lexcodetabinfo", width: "calc(100%)", height: "auto" });
747
805
  panel.ln = 0;
748
806
  panel.col = 0;
749
807
 
750
808
  this._refreshCodeInfo = (ln = panel.ln, col = panel.col) => {
751
- panel.ln = ln;
752
- panel.col = col;
809
+ panel.ln = ln + 1;
810
+ panel.col = col + 1;
753
811
  panel.clear();
754
812
  panel.sameLine();
755
813
  panel.addLabel(this.code.title, { float: 'right' });
756
- panel.addLabel("Ln " + ln, { width: "64px" });
757
- panel.addLabel("Col " + col, { width: "64px" });
814
+ panel.addLabel("Ln " + panel.ln, { width: "64px" });
815
+ panel.addLabel("Col " + panel.col, { width: "64px" });
758
816
  panel.addButton("<b>{ }</b>", this.highlight, (value, event) => {
759
817
  LX.addContextMenu( "Language", event, m => {
760
818
  for( const lang of this.languages )
@@ -858,7 +916,7 @@ class CodeEditor {
858
916
  this.restoreCursor(cursor, this.code.cursorState);
859
917
  this.endSelection();
860
918
  this._changeLanguageFromExtension( LX.getExtension(tabname) );
861
- this._refreshCodeInfo(cursor.line + 1, cursor.position);
919
+ this._refreshCodeInfo(cursor.line, cursor.position);
862
920
 
863
921
  // Restore scroll
864
922
  this.gutter.scrollTop = this.code.scrollTop;
@@ -918,10 +976,26 @@ class CodeEditor {
918
976
  {
919
977
  if( mouse_pos[0] > this.code.scrollWidth || mouse_pos[1] > this.code.scrollHeight )
920
978
  return; // Scrollbar click
979
+
980
+ // Left click only...
981
+ if( e.button === 2 )
982
+ {
983
+ this.processClick(e);
984
+
985
+ this.canOpenContextMenu = !this.selection;
986
+
987
+ if( this.selection )
988
+ {
989
+ this.canOpenContextMenu |= (cursor.line >= this.selection.fromY && cursor.line <= this.selection.toY
990
+ && cursor.position >= this.selection.fromX && cursor.position <= this.selection.toX);
991
+ if( this.canOpenContextMenu )
992
+ return;
993
+ }
994
+ }
995
+
921
996
  this.lastMouseDown = LX.getTime();
922
997
  this.state.selectingText = true;
923
998
  this.endSelection();
924
- return;
925
999
  }
926
1000
 
927
1001
  else if( e.type == 'mouseup' )
@@ -964,15 +1038,29 @@ class CodeEditor {
964
1038
  }
965
1039
  }
966
1040
 
967
- else if ( e.type == 'contextmenu' ) {
968
- e.preventDefault()
969
- LX.addContextMenu( "Format code", e, m => {
970
- m.add( "JSON", () => {
971
- let json = this.toJSONFormat(this.getText());
972
- this.code.lines = json.split("\n");
973
- this.processLines();
974
- } );
1041
+ else if ( e.type == 'contextmenu' )
1042
+ {
1043
+ e.preventDefault();
1044
+
1045
+ if( !this.canOpenContextMenu )
1046
+ return;
1047
+
1048
+ LX.addContextMenu( null, e, m => {
1049
+ m.add( "Copy", () => { this._copyContent(); } );
1050
+ if( !this.disableEdition )
1051
+ {
1052
+ m.add( "Cut", () => { this._cutContent(); } );
1053
+ m.add( "Paste", () => { this._pasteContent(); } );
1054
+ m.add( "" );
1055
+ m.add( "Format/JSON", () => {
1056
+ let json = this.toJSONFormat(this.getText());
1057
+ this.code.lines = json.split("\n");
1058
+ this.processLines();
1059
+ } );
1060
+ }
975
1061
  });
1062
+
1063
+ this.canOpenContextMenu = false;
976
1064
  }
977
1065
  }
978
1066
 
@@ -997,7 +1085,7 @@ class CodeEditor {
997
1085
  this.hideAutoCompleteBox();
998
1086
 
999
1087
  if(!skip_refresh)
1000
- this._refreshCodeInfo( ln + 1, cursor.position );
1088
+ this._refreshCodeInfo( ln, cursor.position );
1001
1089
  }
1002
1090
 
1003
1091
  processSelection( e, keep_range ) {
@@ -1143,27 +1231,7 @@ class CodeEditor {
1143
1231
  this.cursorToLine(cursor, this.selection.toY);
1144
1232
  break;
1145
1233
  case 'c': // copy
1146
- let text_to_copy = "";
1147
- if( !this.selection ) {
1148
- text_to_copy = "\n" + this.code.lines[cursor.line];
1149
- }
1150
- else {
1151
- const separator = "_NEWLINE_";
1152
- let code = this.code.lines.join(separator);
1153
-
1154
- // Get linear start index
1155
- let index = 0;
1156
-
1157
- for(let i = 0; i <= this.selection.fromY; i++)
1158
- index += (i == this.selection.fromY ? this.selection.fromX : this.code.lines[i].length);
1159
-
1160
- index += this.selection.fromY * separator.length;
1161
- const num_chars = this.selection.chars + (this.selection.toY - this.selection.fromY) * separator.length;
1162
- const text = code.substr(index, num_chars);
1163
- const lines = text.split(separator);
1164
- text_to_copy = lines.join('\n');
1165
- }
1166
- navigator.clipboard.writeText(text_to_copy).then(() => console.log("Successfully copied"), (err) => console.error("Error"));
1234
+ this._copyContent();
1167
1235
  return;
1168
1236
  case 'd': // duplicate line
1169
1237
  e.preventDefault();
@@ -1176,13 +1244,10 @@ class CodeEditor {
1176
1244
  this.onsave( this.getText() );
1177
1245
  return;
1178
1246
  case 'v': // paste
1179
- let text = await navigator.clipboard.readText();
1180
- this.appendText(text);
1247
+ this._pasteContent();
1181
1248
  return;
1182
1249
  case 'x': // cut line
1183
- const to_copy = this.code.lines.splice(lidx, 1)[0];
1184
- this.processLines(lidx);
1185
- navigator.clipboard.writeText(to_copy + '\n');
1250
+ this._cutContent();
1186
1251
  return;
1187
1252
  case 'z': // undo
1188
1253
  if(!this.code.undoSteps.length)
@@ -1272,27 +1337,43 @@ class CodeEditor {
1272
1337
  if( this.selection )
1273
1338
  {
1274
1339
  this.actions['Backspace'].callback(lidx, cursor, e);
1340
+ lidx = cursor.line; // Update this, since it's from the old code
1275
1341
  }
1276
1342
 
1277
- // Append key
1343
+ // Append key
1278
1344
 
1279
- this.code.lines[lidx] = [
1280
- this.code.lines[lidx].slice(0, cursor.position),
1281
- key,
1282
- this.code.lines[lidx].slice(cursor.position)
1283
- ].join('');
1345
+ const isPairKey = (Object.values( this.pairKeys ).indexOf( key ) > -1) && !this.wasKeyPaired;
1346
+ const sameKeyNext = isPairKey && (this.code.lines[lidx][cursor.position] === key);
1284
1347
 
1285
- this.cursorToRight( key );
1348
+ if( !sameKeyNext )
1349
+ {
1350
+ this.code.lines[lidx] = [
1351
+ this.code.lines[lidx].slice(0, cursor.position),
1352
+ key,
1353
+ this.code.lines[lidx].slice(cursor.position)
1354
+ ].join('');
1355
+ }
1286
1356
 
1287
- // Other special char cases...
1357
+ this.cursorToRight( key );
1288
1358
 
1289
- if( key == '{' )
1359
+ // Some custom cases for auto key pair (), {}, "", '', ...
1360
+
1361
+ const keyMustPair = (this.pairKeys[ key ] !== undefined);
1362
+ if( keyMustPair && !this.wasKeyPaired )
1290
1363
  {
1291
- this.root.dispatchEvent(new KeyboardEvent('keydown', {'key': '}'}));
1364
+ // Make sure to detect later that the key is paired automatically to avoid loops...
1365
+ this.wasKeyPaired = true;
1366
+
1367
+ if(sameKeyNext) return;
1368
+
1369
+ this.root.dispatchEvent(new KeyboardEvent('keydown', { 'key': this.pairKeys[ key ] }));
1292
1370
  this.cursorToLeft( key, cursor );
1293
- return; // It will be processed with the above event
1371
+ return;
1294
1372
  }
1295
1373
 
1374
+ // Once here we can pair keys again
1375
+ delete this.wasKeyPaired;
1376
+
1296
1377
  // Update only the current line, since it's only an appended key
1297
1378
  this.processLine( lidx );
1298
1379
 
@@ -1302,6 +1383,73 @@ class CodeEditor {
1302
1383
  this.showAutoCompleteBox( key, cursor );
1303
1384
  }
1304
1385
 
1386
+ async _pasteContent() {
1387
+ let text = await navigator.clipboard.readText();
1388
+ this.appendText(text);
1389
+ }
1390
+
1391
+ async _copyContent() {
1392
+
1393
+ let cursor = this.cursors.children[0];
1394
+ let text_to_copy = "";
1395
+
1396
+ if( !this.selection ) {
1397
+ text_to_copy = "\n" + this.code.lines[cursor.line];
1398
+ }
1399
+ else {
1400
+ const separator = "_NEWLINE_";
1401
+ let code = this.code.lines.join(separator);
1402
+
1403
+ // Get linear start index
1404
+ let index = 0;
1405
+
1406
+ for(let i = 0; i <= this.selection.fromY; i++)
1407
+ index += (i == this.selection.fromY ? this.selection.fromX : this.code.lines[i].length);
1408
+
1409
+ index += this.selection.fromY * separator.length;
1410
+ const num_chars = this.selection.chars + (this.selection.toY - this.selection.fromY) * separator.length;
1411
+ const text = code.substr(index, num_chars);
1412
+ const lines = text.split(separator);
1413
+ text_to_copy = lines.join('\n');
1414
+ }
1415
+
1416
+ navigator.clipboard.writeText(text_to_copy).then(() => console.log("Successfully copied"), (err) => console.error("Error"));
1417
+ }
1418
+
1419
+ async _cutContent() {
1420
+
1421
+ let cursor = this.cursors.children[0];
1422
+ let lidx = cursor.line;
1423
+ let text_to_cut = "";
1424
+
1425
+ if( !this.selection ) {
1426
+ text_to_cut = "\n" + this.code.lines[cursor.line];
1427
+ this.code.lines.splice(lidx, 1);
1428
+ this.processLines(lidx);
1429
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT );
1430
+ }
1431
+ else {
1432
+ const separator = "_NEWLINE_";
1433
+ let code = this.code.lines.join(separator);
1434
+
1435
+ // Get linear start index
1436
+ let index = 0;
1437
+
1438
+ for(let i = 0; i <= this.selection.fromY; i++)
1439
+ index += (i == this.selection.fromY ? this.selection.fromX : this.code.lines[i].length);
1440
+
1441
+ index += this.selection.fromY * separator.length;
1442
+ const num_chars = this.selection.chars + (this.selection.toY - this.selection.fromY) * separator.length;
1443
+ const text = code.substr(index, num_chars);
1444
+ const lines = text.split(separator);
1445
+ text_to_cut = lines.join('\n');
1446
+
1447
+ this.deleteSelection( cursor );
1448
+ }
1449
+
1450
+ navigator.clipboard.writeText(text_to_cut).then(() => console.log("Successfully cut"), (err) => console.error("Error"));
1451
+ }
1452
+
1305
1453
  action( key, deleteSelection, fn ) {
1306
1454
 
1307
1455
  this.actions[ key ] = {
@@ -1310,58 +1458,103 @@ class CodeEditor {
1310
1458
  };
1311
1459
  }
1312
1460
 
1313
- processLines( from ) {
1461
+ scanWordSuggestions() {
1314
1462
 
1315
- if( !from )
1316
- {
1317
- this.gutter.innerHTML = "";
1318
- this.code.innerHTML = "";
1319
- this.code.tokens = {};
1320
- }
1463
+ this.code.tokens = {};
1321
1464
 
1322
- for( let i = from ?? 0; i < this.code.lines.length; ++i )
1465
+ for( let i = 0; i < this.code.lines.length; ++i )
1323
1466
  {
1324
- this.processLine( i );
1467
+ const linestring = this.code.lines[ i ];
1468
+ const tokens = this._getTokensFromString( linestring, true );
1469
+ tokens.forEach( t => this.code.tokens[ t ] = 1 );
1325
1470
  }
1471
+ }
1326
1472
 
1327
- // Clean up...
1328
- if( from )
1473
+ processLines( from ) {
1474
+
1475
+ // const start = performance.now();
1476
+
1477
+ this.gutter.innerHTML = "";
1478
+ this.code.innerHTML = "";
1479
+
1480
+ for( let i = 0; i < this.code.lines.length; ++i )
1329
1481
  {
1330
- while( this.code.lines.length != this.gutter.children.length )
1331
- this.gutter.lastChild.remove();
1332
- while( this.code.lines.length != this.code.children.length )
1333
- this.code.lastChild.remove();
1482
+ this.processLine( i, true );
1334
1483
  }
1335
1484
 
1485
+ // console.log( performance.now() - start );
1336
1486
  }
1337
1487
 
1338
- processLine( linenum ) {
1488
+ processLine( linenum, force ) {
1489
+
1490
+ // if(!force)
1491
+ // {
1492
+ // this.processLines();
1493
+ // return;
1494
+ // }
1339
1495
 
1340
1496
  delete this._building_string; // multi-line strings not supported by now
1341
1497
 
1342
1498
  // It's allowed to process only 1 line to optimize
1343
1499
  let linestring = this.code.lines[ linenum ];
1344
- var _lines = this.code.querySelectorAll('pre');
1345
- var pre = null, single_update = false;
1346
- if( _lines[linenum] ) {
1347
- pre = _lines[linenum];
1348
- single_update = true;
1349
- }
1350
-
1351
- if(!pre)
1500
+
1501
+ var pre = document.createElement('pre');
1502
+ pre.dataset['linenum'] = linenum;
1503
+
1504
+ // Line gutter
1505
+ var linenumspan = document.createElement('span');
1506
+ linenumspan.innerHTML = (linenum + 1);
1507
+
1508
+ if( force )
1352
1509
  {
1353
- var pre = document.createElement('pre');
1354
- pre.dataset['linenum'] = linenum;
1355
- this.code.appendChild(pre);
1356
- }
1357
- else
1510
+ this.gutter.appendChild(linenumspan);
1511
+ this.code.appendChild( pre );
1512
+ } else
1358
1513
  {
1359
- pre.children[0].remove(); // Remove token list
1514
+ this.gutter.childNodes[ linenum ].remove();
1515
+ this.code.childNodes[ linenum ].remove();
1516
+ this.gutter.insertChildAtIndex( linenumspan, linenum );
1517
+ this.code.insertChildAtIndex( pre, linenum );
1360
1518
  }
1361
1519
 
1362
1520
  var linespan = document.createElement('span');
1363
1521
  pre.appendChild(linespan);
1364
1522
 
1523
+ const to_process = this._getTokensFromString( linestring );
1524
+
1525
+ let line_inner_html = "";
1526
+
1527
+ // Process all tokens
1528
+ for( var i = 0; i < to_process.length; ++i )
1529
+ {
1530
+ let it = i - 1;
1531
+ let prev = to_process[it];
1532
+ while( prev == ' ' ) {
1533
+ it--;
1534
+ prev = to_process[it];
1535
+ }
1536
+
1537
+ it = i + 1;
1538
+ let next = to_process[it];
1539
+ while( next == ' ' || next == '"') {
1540
+ it++;
1541
+ next = to_process[it];
1542
+ }
1543
+
1544
+ let token = to_process[i];
1545
+ if( token.substr(0, 2) == '/*' )
1546
+ this._building_block_comment = true;
1547
+ if( token.substr(token.length - 2) == '*/' )
1548
+ delete this._building_block_comment;
1549
+
1550
+ line_inner_html += this.processToken(token, prev, next);
1551
+ }
1552
+
1553
+ linespan.innerHTML = line_inner_html;
1554
+ }
1555
+
1556
+ _getTokensFromString( linestring, skipNonWords ) {
1557
+
1365
1558
  // Check if line comment
1366
1559
  const is_comment = linestring.split('//');
1367
1560
  linestring = ( is_comment.length > 1 ) ? is_comment[0] : linestring;
@@ -1371,7 +1564,15 @@ class CodeEditor {
1371
1564
 
1372
1565
  for( let t of tokens )
1373
1566
  {
1374
- let iter = t.matchAll(/[\[\](){}<>.,;:"']/g);
1567
+ if( !t.length || (skipNonWords && ( t.includes('"') || t.length < 3 )) )
1568
+ continue;
1569
+
1570
+ if( t == ' ' ) {
1571
+ to_process.push( t );
1572
+ continue;
1573
+ }
1574
+
1575
+ let iter = t.matchAll(/(::|[\[\](){}<>.,;:"'])/g);
1375
1576
  let subtokens = iter.next();
1376
1577
  if( subtokens.value )
1377
1578
  {
@@ -1379,62 +1580,35 @@ class CodeEditor {
1379
1580
  while( subtokens.value != undefined )
1380
1581
  {
1381
1582
  const _pt = t.substring(idx, subtokens.value.index);
1382
- to_process.push( _pt );
1583
+ if( _pt.length ) to_process.push( _pt );
1383
1584
  to_process.push( subtokens.value[0] );
1384
- idx = subtokens.value.index + 1;
1585
+ idx = subtokens.value.index + subtokens.value[0].length;
1385
1586
  subtokens = iter.next();
1386
1587
  if(!subtokens.value) {
1387
1588
  const _at = t.substring(idx);
1388
- to_process.push( _at );
1589
+ if( _at.length ) to_process.push( _at );
1389
1590
  }
1390
1591
  }
1391
1592
  }
1392
- else
1393
- to_process.push( t );
1593
+ else to_process.push( t );
1394
1594
  }
1395
1595
 
1396
- if( is_comment.length > 1 )
1596
+ if( is_comment.length > 1 && !skipNonWords )
1397
1597
  to_process.push( "//" + is_comment[1] );
1398
-
1399
- // Process all tokens
1400
- for( var i = 0; i < to_process.length; ++i )
1401
- {
1402
- let it = i - 1;
1403
- let prev = to_process[it];
1404
- while( prev == '' || prev == ' ' ) {
1405
- it--;
1406
- prev = to_process[it];
1407
- }
1408
-
1409
- it = i + 1;
1410
- let next = to_process[it];
1411
- while( next == '' || next == ' ' || next == '"') {
1412
- it++;
1413
- next = to_process[it];
1414
- }
1415
-
1416
- let token = to_process[i];
1417
- if( token.substr(0, 2) == '/*' )
1418
- this._building_block_comment = true;
1419
- if( token.substr(token.length - 2) == '*/' )
1420
- delete this._building_block_comment;
1421
1598
 
1422
- this.processToken(token, linespan, prev, next);
1423
- }
1599
+ return to_process;
1600
+ }
1424
1601
 
1425
- // add line gutter
1426
- if(!single_update)
1427
- {
1428
- var linenumspan = document.createElement('span');
1429
- linenumspan.innerHTML = (linenum + 1);
1430
- this.gutter.appendChild(linenumspan);
1431
- }
1602
+ _mustHightlightWord( token, kindArray ) {
1603
+
1604
+ return kindArray[this.highlight] && kindArray[this.highlight].indexOf(token) > -1;
1432
1605
  }
1433
1606
 
1434
- processToken(token, linespan, prev, next) {
1607
+ processToken(token, prev, next) {
1435
1608
 
1436
1609
  let sString = false;
1437
- let highlight = this.highlight.replace(/\s/g, '').toLowerCase();
1610
+ let highlight = this.highlight.replace(/\s/g, '').replaceAll("+", "p").toLowerCase();
1611
+ let inner_html = "", token_classname = "";
1438
1612
 
1439
1613
  if(token == '"' || token == "'")
1440
1614
  {
@@ -1444,75 +1618,68 @@ class CodeEditor {
1444
1618
 
1445
1619
  if(token == ' ')
1446
1620
  {
1447
- linespan.innerHTML += token;
1621
+ inner_html += token;
1448
1622
  }
1449
1623
  else
1450
1624
  {
1451
- var span = document.createElement('span');
1452
- span.innerHTML = token;
1453
-
1454
1625
  if( this._building_block_comment )
1455
- span.classList.add("cm-com");
1626
+ token_classname += "cm-com";
1456
1627
 
1457
1628
  else if( this._building_string )
1458
- span.classList.add("cm-str");
1629
+ token_classname += "cm-str";
1459
1630
 
1460
- else if( this.keywords[this.highlight] && this.keywords[this.highlight].indexOf(token) > -1 )
1461
- span.classList.add("cm-kwd");
1631
+ else if( this._mustHightlightWord( token, this.keywords ) )
1632
+ token_classname += "cm-kwd";
1462
1633
 
1463
- else if( this.builtin[this.highlight] && this.builtin[this.highlight].indexOf(token) > -1 )
1464
- span.classList.add("cm-bln");
1634
+ else if( this._mustHightlightWord( token, this.builtin ) )
1635
+ token_classname += "cm-bln";
1465
1636
 
1466
- else if( this.statementsAndDeclarations[this.highlight] && this.statementsAndDeclarations[this.highlight].indexOf(token) > -1 )
1467
- span.classList.add("cm-std");
1637
+ else if( this._mustHightlightWord( token, this.statementsAndDeclarations ) )
1638
+ token_classname += "cm-std";
1468
1639
 
1469
- else if( this.symbols[this.highlight] && this.symbols[this.highlight].indexOf(token) > -1 )
1470
- span.classList.add("cm-sym");
1640
+ else if( this._mustHightlightWord( token, this.symbols ) )
1641
+ token_classname += "cm-sym";
1471
1642
 
1472
1643
  else if( token.substr(0, 2) == '//' )
1473
- span.classList.add("cm-com");
1644
+ token_classname += "cm-com";
1474
1645
 
1475
1646
  else if( token.substr(0, 2) == '/*' )
1476
- span.classList.add("cm-com");
1647
+ token_classname += "cm-com";
1477
1648
 
1478
1649
  else if( token.substr(token.length - 2) == '*/' )
1479
- span.classList.add("cm-com");
1650
+ token_classname += "cm-com";
1480
1651
 
1481
1652
  else if( this.isNumber(token) || this.isNumber( token.replace(/[px]|[em]|%/g,'') ) )
1482
- span.classList.add("cm-dec");
1653
+ token_classname += "cm-dec";
1483
1654
 
1484
1655
  else if( this.isCSSClass(token, prev, next) )
1485
- span.classList.add("cm-kwd");
1656
+ token_classname += "cm-kwd";
1486
1657
 
1487
- else if ( this.isType(token, prev, next) ) {
1488
- span.classList.add("cm-typ");
1489
- this.code.tokens[ token ] = CodeEditor.WORD_TYPE_CLASS;
1490
- }
1658
+ else if ( this.isType(token, prev, next) )
1659
+ token_classname += "cm-typ";
1491
1660
 
1492
- else if ( token[0] != '@' && next == '(' ) {
1493
- span.classList.add("cm-mtd");
1494
- this.code.tokens[ token ] = CodeEditor.WORD_TYPE_METHOD;
1495
- }
1661
+ else if ( token[0] != '@' && next == '(' )
1662
+ token_classname += "cm-mtd";
1663
+
1664
+ else if ( highlight == 'cpp' && token.includes('#') ) // C++ preprocessor
1665
+ token_classname += "cm-ppc";
1666
+
1667
+ else if ( highlight == 'cpp' && prev != 'WDWD:' && next == '::' ) // C++ Class
1668
+ token_classname += "cm-typ";
1496
1669
 
1497
1670
  else if ( highlight == 'css' && prev == ':' && (next == ';' || next == '!important') ) // CSS value
1498
- span.classList.add("cm-str");
1671
+ token_classname += "cm-str";
1499
1672
 
1500
1673
  else if ( highlight == 'css' && prev == undefined && next == ':' ) // CSS attribute
1501
- span.classList.add("cm-typ");
1502
- else {
1503
-
1504
- if( token.length > 1 )
1505
- {
1506
- // Store in token map to show later as autocomplete suggestions
1507
- this.code.tokens[ token ] = -1;
1508
- }
1509
- }
1674
+ token_classname += "cm-typ";
1510
1675
 
1511
- span.classList.add(highlight);
1512
- linespan.appendChild(span);
1676
+ token_classname += " " + highlight;
1677
+ inner_html += "<span class=' " + token_classname + "'>" + token + "</span>";
1513
1678
  }
1514
1679
 
1515
1680
  if(sString) delete this._building_string;
1681
+
1682
+ return inner_html;
1516
1683
  }
1517
1684
 
1518
1685
  isCSSClass( token, prev, next ) {
@@ -1525,6 +1692,10 @@ class CodeEditor {
1525
1692
 
1526
1693
  isType( token, prev, next ) {
1527
1694
 
1695
+ // Common case
1696
+ if( this._mustHightlightWord( token, this.types ) )
1697
+ return true;
1698
+
1528
1699
  if( this.highlight == 'JavaScript' )
1529
1700
  {
1530
1701
  return (prev == 'class' && next == '{') || (prev == 'new' && next == '(');
@@ -1543,7 +1714,7 @@ class CodeEditor {
1543
1714
 
1544
1715
  if( !this.selection || (this.selection.fromY != this.selection.toY) )
1545
1716
  return false;
1546
-
1717
+
1547
1718
  const _lastLeft = cursor._left;
1548
1719
 
1549
1720
  // Insert first..
@@ -1554,8 +1725,7 @@ class CodeEditor {
1554
1725
  ].join('');
1555
1726
 
1556
1727
  // Go to the end of the word
1557
- this.cursorToString(cursor,
1558
- this.code.lines[lidx].slice(this.selection.fromX, this.selection.toX + 1));
1728
+ this.cursorToPosition(cursor, this.selection.toX + 1);
1559
1729
 
1560
1730
  // Change next key?
1561
1731
  switch(key)
@@ -1630,8 +1800,10 @@ class CodeEditor {
1630
1800
 
1631
1801
  deleteSelection( cursor ) {
1632
1802
 
1633
- if(this.disable_edition)
1803
+ // I think it's not necessary but...
1804
+ if(this.disableEdition)
1634
1805
  return;
1806
+
1635
1807
  // Some selections don't depend on mouse up..
1636
1808
  if(this.selection) this.selection.invertIfNecessary();
1637
1809
 
@@ -1674,7 +1846,7 @@ class CodeEditor {
1674
1846
  cursor.style.left = "calc(" + (cursor._left - this.getScrollLeft()) + "px + " + this.xPadding + ")";
1675
1847
  cursor.position++;
1676
1848
  this.restartBlink();
1677
- this._refreshCodeInfo( cursor.line + 1, cursor.position );
1849
+ this._refreshCodeInfo( cursor.line, cursor.position );
1678
1850
 
1679
1851
  // Add horizontal scroll
1680
1852
 
@@ -1695,7 +1867,7 @@ class CodeEditor {
1695
1867
  cursor.position--;
1696
1868
  cursor.position = Math.max(cursor.position, 0);
1697
1869
  this.restartBlink();
1698
- this._refreshCodeInfo( cursor.line + 1, cursor.position );
1870
+ this._refreshCodeInfo( cursor.line, cursor.position );
1699
1871
 
1700
1872
  doAsync(() => {
1701
1873
  var first_char = (this.code.scrollLeft / this.charWidth)|0;
@@ -1715,7 +1887,7 @@ class CodeEditor {
1715
1887
  if(resetLeft)
1716
1888
  this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
1717
1889
 
1718
- this._refreshCodeInfo( cursor.line + 1, cursor.position );
1890
+ this._refreshCodeInfo( cursor.line, cursor.position );
1719
1891
 
1720
1892
  doAsync(() => {
1721
1893
  var first_line = (this.code.scrollTop / this.lineHeight)|0;
@@ -1734,7 +1906,7 @@ class CodeEditor {
1734
1906
  if(resetLeft)
1735
1907
  this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
1736
1908
 
1737
- this._refreshCodeInfo( cursor.line + 1, cursor.position );
1909
+ this._refreshCodeInfo( cursor.line, cursor.position );
1738
1910
 
1739
1911
  doAsync(() => {
1740
1912
  var last_line = ((this.code.scrollTop + this.code.offsetHeight) / this.lineHeight)|0;
@@ -1942,14 +2114,19 @@ class CodeEditor {
1942
2114
  let suggestions = [];
1943
2115
 
1944
2116
  // Add language special keys...
1945
- suggestions = suggestions.concat( this.keywords[ this.highlight ] );
1946
- suggestions = suggestions.concat( this.statementsAndDeclarations[ this.highlight ] );
1947
- suggestions = suggestions.concat( this.builtin[ this.highlight ] );
1948
- suggestions = suggestions.concat( this.utils[ this.highlight ] );
2117
+ suggestions = suggestions.concat(
2118
+ this.builtin[ this.highlight ] ?? [],
2119
+ this.keywords[ this.highlight ] ?? [],
2120
+ this.statementsAndDeclarations[ this.highlight ] ?? [],
2121
+ this.types[ this.highlight ] ?? [],
2122
+ this.utils[ this.highlight ] ?? []
2123
+ );
2124
+
2125
+ // Add words in current tab plus remove current word
1949
2126
  suggestions = suggestions.concat( Object.keys(this.code.tokens).filter( a => a != word ) );
1950
2127
 
1951
- // Remove single chars and duplicates...
1952
- suggestions = suggestions.filter( (value, index) => value.length > 1 && suggestions.indexOf(value) === index );
2128
+ // Remove 1/2 char words and duplicates...
2129
+ suggestions = suggestions.filter( (value, index) => value.length > 2 && suggestions.indexOf(value) === index );
1953
2130
 
1954
2131
  // Order...
1955
2132
  suggestions = suggestions.sort( (a, b) => a.localeCompare(b) );
@@ -1962,6 +2139,17 @@ class CodeEditor {
1962
2139
  var pre = document.createElement('pre');
1963
2140
  this.autocomplete.appendChild(pre);
1964
2141
 
2142
+ var icon = document.createElement('a');
2143
+
2144
+ if( this._mustHightlightWord( s, this.utils ) )
2145
+ icon.className = "fa fa-cube";
2146
+ else if( this._mustHightlightWord( s, this.types ) )
2147
+ icon.className = "fa fa-code";
2148
+ else
2149
+ icon.className = "fa fa-font";
2150
+
2151
+ pre.appendChild(icon);
2152
+
1965
2153
  pre.addEventListener( 'click', () => {
1966
2154
  this.autoCompleteWord( cursor, s );
1967
2155
  } );
@@ -2042,13 +2230,13 @@ class CodeEditor {
2042
2230
  for( let childSpan of child.childNodes )
2043
2231
  word += childSpan.innerHTML;
2044
2232
 
2045
- return [word, i ]; // Get text of the span inside the 'pre' element
2233
+ return [ word, i ]; // Get text of the span inside the 'pre' element
2046
2234
  }
2047
2235
  }
2048
2236
  }
2049
2237
 
2050
2238
  moveArrowSelectedAutoComplete( dir ) {
2051
-
2239
+
2052
2240
  if( !this.isAutoCompleteActive )
2053
2241
  return;
2054
2242
 
@@ -2061,7 +2249,7 @@ class CodeEditor {
2061
2249
  if( (idx + offset) < 0 ) return;
2062
2250
  }
2063
2251
 
2064
- this.autocomplete.scrollTop += offset * 20;
2252
+ this.autocomplete.scrollTop += offset * 18;
2065
2253
 
2066
2254
  // Remove selected from the current word and add it to the next one
2067
2255
  this.autocomplete.childNodes[ idx ].classList.remove('selected');