lexgui 8.2.5 → 8.3.0

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.
Files changed (35) hide show
  1. package/build/components/NodeTree.d.ts +51 -51
  2. package/build/components/Tabs.d.ts +1 -0
  3. package/build/core/Namespace.js +1 -1
  4. package/build/core/Namespace.js.map +1 -1
  5. package/build/extensions/AssetView.d.ts +138 -138
  6. package/build/extensions/AssetView.js +1433 -1433
  7. package/build/extensions/CodeEditor.d.ts +466 -363
  8. package/build/extensions/CodeEditor.js +3768 -4638
  9. package/build/extensions/CodeEditor.js.map +1 -1
  10. package/build/extensions/DocMaker.d.ts +28 -28
  11. package/build/extensions/DocMaker.js +363 -363
  12. package/build/extensions/Timeline.d.ts +2 -2
  13. package/build/extensions/Timeline.js +28 -15
  14. package/build/extensions/Timeline.js.map +1 -1
  15. package/build/extensions/VideoEditor.d.ts +1 -1
  16. package/build/extensions/VideoEditor.js +15 -7
  17. package/build/extensions/VideoEditor.js.map +1 -1
  18. package/build/extensions/index.js +1 -1
  19. package/build/lexgui.all.js +6169 -6960
  20. package/build/lexgui.all.js.map +1 -1
  21. package/build/lexgui.all.min.js +1 -1
  22. package/build/lexgui.all.module.js +6169 -6961
  23. package/build/lexgui.all.module.js.map +1 -1
  24. package/build/lexgui.all.module.min.js +1 -1
  25. package/build/lexgui.css +7534 -7459
  26. package/build/lexgui.js +4475 -205
  27. package/build/lexgui.js.map +1 -1
  28. package/build/lexgui.min.css +1 -1
  29. package/build/lexgui.min.js +1 -1
  30. package/build/lexgui.module.js +4475 -205
  31. package/build/lexgui.module.js.map +1 -1
  32. package/build/lexgui.module.min.js +1 -1
  33. package/changelog.md +31 -1
  34. package/examples/code-editor.html +88 -16
  35. package/package.json +1 -1
package/build/lexgui.js CHANGED
@@ -10,13 +10,13 @@
10
10
  * Main namespace
11
11
  * @namespace LX
12
12
  */
13
- const g = globalThis;
13
+ const g$1 = globalThis;
14
14
  // Update global namespace if not present (Loading module)
15
15
  // Extension scripts rely on LX being globally available
16
- exports.LX = g.LX;
16
+ exports.LX = g$1.LX;
17
17
  if (!exports.LX) {
18
18
  exports.LX = {
19
- version: '8.2.5',
19
+ version: '8.3.0',
20
20
  ready: false,
21
21
  extensions: [], // Store extensions used
22
22
  extraCommandbarEntries: [], // User specific entries for command bar
@@ -33,7 +33,7 @@
33
33
  CURVE_MOVEOUT_DELETE: 1,
34
34
  DRAGGABLE_Z_INDEX: 101
35
35
  };
36
- g.LX = exports.LX;
36
+ g$1.LX = exports.LX;
37
37
  }
38
38
 
39
39
  // Icons.ts @jxarco
@@ -1408,6 +1408,15 @@
1408
1408
  }
1409
1409
  this.tabDOMs[name].click();
1410
1410
  }
1411
+ setIcon(name, icon) {
1412
+ const tabEl = this.tabDOMs[name];
1413
+ if (!tabEl) {
1414
+ return;
1415
+ }
1416
+ const classes = icon.split(' ');
1417
+ const iconEl = exports.LX.makeIcon(classes[0], { svgClass: 'sm ' + classes.slice(0).join(' ') });
1418
+ tabEl.innerHTML = iconEl.innerHTML + name;
1419
+ }
1411
1420
  delete(name) {
1412
1421
  if (this.selected == name) {
1413
1422
  this.selected = null;
@@ -5097,80 +5106,82 @@
5097
5106
  };
5098
5107
  const r = await onContextMenu(event);
5099
5108
  const multiple = this.selected.length > 1;
5100
- exports.LX.addContextMenu(multiple ? 'Selected Nodes' : node.id, e, (m) => {
5109
+ const useDefaultContextMenuItems = this.options.useDefaultContextMenuItems ?? true;
5110
+ exports.LX.addContextMenu(this.options.contextMenuTitle ?? (multiple ? 'Selected Nodes' : node.id), e, (m) => {
5101
5111
  if (r?.length) {
5102
5112
  for (const i of r) {
5103
5113
  m.add(i.name, { callback: i.callback });
5104
5114
  }
5105
- m.add('');
5106
- }
5107
- m.add('Select Children', () => {
5108
- const selectChildren = (n) => {
5109
- if (n.closed) {
5110
- return;
5111
- }
5112
- for (let child of n.children ?? []) {
5113
- if (!child) {
5114
- continue;
5115
- }
5116
- let nodeItem = this.domEl.querySelector('#' + child.id);
5117
- nodeItem.classList.add('selected');
5118
- this.selected.push(child);
5119
- selectChildren(child);
5120
- }
5121
- };
5122
- this.domEl.querySelectorAll('.selected').forEach((i) => i.classList.remove('selected'));
5123
- this.selected.length = 0;
5124
- // Add childs of the clicked node
5125
- selectChildren(node);
5126
- const onSelect = this._callbacks['select'];
5127
- if (onSelect !== undefined) {
5128
- const event = {
5129
- type: 'select',
5130
- items: [node],
5131
- result: this.selected,
5132
- domEvent: e,
5133
- userInitiated: true
5134
- };
5135
- onSelect(event);
5115
+ if (useDefaultContextMenuItems) {
5116
+ m.add('');
5136
5117
  }
5137
- });
5138
- m.add('Delete', { callback: () => {
5139
- const onBeforeDelete = this._callbacks['beforeDelete'];
5140
- const onDelete = this._callbacks['delete'];
5141
- const resolve = (...args) => {
5142
- let deletedNodes = [];
5143
- if (this.selected.length) {
5144
- deletedNodes.push(...that.deleteNodes(this.selected));
5118
+ }
5119
+ if (useDefaultContextMenuItems) {
5120
+ m.add('Select Children', () => {
5121
+ const selectChildren = (n) => {
5122
+ if (n.closed) {
5123
+ return;
5145
5124
  }
5146
- else if (that.deleteNode(node)) {
5147
- deletedNodes.push(node);
5125
+ for (let child of n.children ?? []) {
5126
+ if (!child) {
5127
+ continue;
5128
+ }
5129
+ let nodeItem = this.domEl.querySelector('#' + child.id);
5130
+ nodeItem.classList.add('selected');
5131
+ this.selected.push(child);
5132
+ selectChildren(child);
5148
5133
  }
5149
- this.refresh();
5150
- const event = {
5151
- type: 'delete',
5152
- items: deletedNodes,
5153
- userInitiated: true
5154
- };
5155
- if (onDelete)
5156
- onDelete(event, ...args);
5157
5134
  };
5158
- if (onBeforeDelete) {
5135
+ this.domEl.querySelectorAll('.selected').forEach((i) => i.classList.remove('selected'));
5136
+ this.selected.length = 0;
5137
+ // Add childs of the clicked node
5138
+ selectChildren(node);
5139
+ const onSelect = this._callbacks['select'];
5140
+ if (onSelect !== undefined) {
5159
5141
  const event = {
5160
- type: 'delete',
5161
- items: this.selected.length ? this.selected : [node],
5142
+ type: 'select',
5143
+ items: [node],
5144
+ result: this.selected,
5145
+ domEvent: e,
5162
5146
  userInitiated: true
5163
5147
  };
5164
- onBeforeDelete(event, resolve);
5165
- }
5166
- else {
5167
- resolve();
5148
+ onSelect(event);
5168
5149
  }
5169
- } });
5150
+ });
5151
+ m.add('Delete', { callback: () => {
5152
+ const onBeforeDelete = this._callbacks['beforeDelete'];
5153
+ const onDelete = this._callbacks['delete'];
5154
+ const resolve = (...args) => {
5155
+ let deletedNodes = [];
5156
+ if (this.selected.length) {
5157
+ deletedNodes.push(...that.deleteNodes(this.selected));
5158
+ }
5159
+ else if (that.deleteNode(node)) {
5160
+ deletedNodes.push(node);
5161
+ }
5162
+ this.refresh();
5163
+ const event = {
5164
+ type: 'delete',
5165
+ items: deletedNodes,
5166
+ userInitiated: true
5167
+ };
5168
+ if (onDelete)
5169
+ onDelete(event, ...args);
5170
+ };
5171
+ if (onBeforeDelete) {
5172
+ const event = {
5173
+ type: 'delete',
5174
+ items: this.selected.length ? this.selected : [node],
5175
+ userInitiated: true
5176
+ };
5177
+ onBeforeDelete(event, resolve);
5178
+ }
5179
+ else {
5180
+ resolve();
5181
+ }
5182
+ } });
5183
+ }
5170
5184
  });
5171
- if (!(this.options.addDefault ?? false)) {
5172
- return;
5173
- }
5174
5185
  });
5175
5186
  item.addEventListener('keydown', (e) => {
5176
5187
  if (node.rename) {
@@ -5852,7 +5863,7 @@
5852
5863
  progress.value = value;
5853
5864
  container.appendChild(progress);
5854
5865
  const _updateColor = () => {
5855
- let backgroundColor = exports.LX.getCSSVariable('blue-500');
5866
+ let backgroundColor = exports.LX.getCSSVariable('color-blue-500');
5856
5867
  if (progress.low != undefined && progress.value < progress.low) {
5857
5868
  backgroundColor = exports.LX.getCSSVariable('destructive');
5858
5869
  }
@@ -11336,6 +11347,42 @@
11336
11347
  xhr.send(data);
11337
11348
  return xhr;
11338
11349
  },
11350
+ /**
11351
+ * Request file with a promise from url (it could be a binary, text, etc.). If you want a simplied version use
11352
+ * @method requestFileAsync
11353
+ * @param {String} url
11354
+ * @param {String} dataType
11355
+ * @param {Boolean} nocache
11356
+ */
11357
+ async requestFileAsync(url, dataType, nocache = false) {
11358
+ return new Promise((resolve, reject) => {
11359
+ dataType = dataType ?? 'arraybuffer';
11360
+ const mimeType = dataType === 'arraybuffer' ? 'application/octet-stream' : undefined;
11361
+ var xhr = new XMLHttpRequest();
11362
+ xhr.open('GET', url, true);
11363
+ xhr.responseType = dataType;
11364
+ if (mimeType) {
11365
+ xhr.overrideMimeType(mimeType);
11366
+ }
11367
+ if (nocache) {
11368
+ xhr.setRequestHeader('Cache-Control', 'no-cache');
11369
+ }
11370
+ xhr.onload = function () {
11371
+ var response = this.response;
11372
+ if (this.status != 200) {
11373
+ var err = 'Error ' + this.status;
11374
+ reject(err);
11375
+ return;
11376
+ }
11377
+ resolve(response);
11378
+ };
11379
+ xhr.onerror = function (err) {
11380
+ reject(err);
11381
+ };
11382
+ xhr.send();
11383
+ return xhr;
11384
+ });
11385
+ },
11339
11386
  /**
11340
11387
  * Request file from url
11341
11388
  * @method requestText
@@ -11759,7 +11806,12 @@
11759
11806
  exports.LX.makeContainer(['100%', '100%'], 'text-lg font-medium text-foreground px-2', title, p);
11760
11807
  p.addTextArea(null, message, null, { disabled: true, fitHeight: true, inputClass: 'bg-none text-sm text-muted-foreground' });
11761
11808
  p.sameLine(2, 'justify-end');
11762
- p.addButton(null, options.cancelText ?? 'Cancel', () => this.destroy(), {
11809
+ p.addButton(null, options.cancelText ?? 'Cancel', () => {
11810
+ if (options.cancelCallback) {
11811
+ options.cancelCallback();
11812
+ }
11813
+ this.destroy();
11814
+ }, {
11763
11815
  buttonClass: 'outline'
11764
11816
  });
11765
11817
  p.addButton(null, options.continueText ?? 'Continue', () => {
@@ -12164,7 +12216,7 @@
12164
12216
  }
12165
12217
  else if (item.kbd) {
12166
12218
  item.kbd = [].concat(item.kbd);
12167
- const kbd = exports.LX.makeKbd(item.kbd);
12219
+ const kbd = exports.LX.makeKbd(item.kbd, item.useKbdSpecialKeys ?? true);
12168
12220
  menuItem.appendChild(kbd);
12169
12221
  document.addEventListener('keydown', (e) => {
12170
12222
  if (!this._trigger.ddm)
@@ -13407,152 +13459,4365 @@
13407
13459
  }
13408
13460
  exports.LX.Tour = Tour;
13409
13461
 
13410
- // Core.ts @jxarco
13411
- /**
13412
- * @method init
13413
- * @param {Object} options
13414
- * autoTheme: Use theme depending on browser-system default theme [true]
13415
- * container: Root location for the gui (default is the document body)
13416
- * id: Id of the main area
13417
- * rootClass: Extra class to the root container
13418
- * skipRoot: Skip adding LX root container
13419
- * skipDefaultArea: Skip creation of main area
13420
- * layoutMode: Sets page layout mode (document | app)
13421
- * spacingMode: Sets page layout spacing mode (default | compact)
13422
- */
13423
- exports.LX.init = async function (options = {}) {
13424
- if (this.ready) {
13425
- return this.mainArea;
13462
+ // CodeEditor.ts @jxarco
13463
+ if (!exports.LX) {
13464
+ throw ('Missing LX namespace!');
13465
+ }
13466
+ exports.LX.extensions.push('CodeEditor');
13467
+ // _____ _ _ _ _ _ _
13468
+ // | | | |_|_| |_| |_|_|___ ___
13469
+ // | | | _| | | | _| | -_|_ -|
13470
+ // |_____|_| |_|_|_|_| |_|___|___|
13471
+ function firstNonspaceIndex(str) {
13472
+ const index = str.search(/\S|$/);
13473
+ return index < str.length ? index : -1;
13474
+ }
13475
+ function isSymbol(str) {
13476
+ return /[^\w\s]/.test(str);
13477
+ }
13478
+ function isWord(str) {
13479
+ return /\w/.test(str);
13480
+ }
13481
+ function getLanguageIcon(langDef, extension) {
13482
+ if (!langDef?.icon) {
13483
+ return 'FileCode text-neutral-500'; // default icon
13426
13484
  }
13427
- await exports.LX.loadScriptSync('https://unpkg.com/lucide@latest');
13428
- // LexGUI root
13429
- console.log(`LexGUI v${this.version}`);
13430
- const root = exports.LX.makeElement('div', exports.LX.mergeClass('lexcontainer', options.rootClass));
13431
- root.id = 'lexroot';
13432
- root.tabIndex = -1;
13433
- this.modal = exports.LX.makeElement('div', 'inset-0 hidden-opacity bg-black/50 fixed z-100 transition-opacity duration-100 ease-in');
13434
- this.modal.id = 'modal';
13435
- this.modal.toggle = function (force) {
13436
- this.classList.toggle('hidden-opacity', force);
13437
- };
13438
- function blockScroll(e) {
13439
- e.preventDefault();
13440
- e.stopPropagation();
13485
+ if (typeof langDef.icon === 'string') {
13486
+ return langDef.icon;
13441
13487
  }
13442
- this.modal.addEventListener('wheel', blockScroll, { passive: false });
13443
- this.modal.addEventListener('touchmove', blockScroll, { passive: false });
13444
- this.root = root;
13445
- this.container = document.body;
13446
- if (options.container) {
13447
- this.container = options.container.constructor === String
13448
- ? document.getElementById(options.container)
13449
- : options.container;
13488
+ if (extension && langDef.icon[extension]) {
13489
+ return langDef.icon[extension];
13450
13490
  }
13451
- this.layoutMode = options.layoutMode ?? 'app';
13452
- document.documentElement.setAttribute('data-layout', this.layoutMode);
13453
- if (this.layoutMode == 'document') ;
13454
- this.spacingMode = options.spacingMode ?? 'default';
13455
- document.documentElement.setAttribute('data-spacing', this.spacingMode);
13456
- this.container.appendChild(this.modal);
13457
- if (!options.skipRoot) {
13458
- this.container.appendChild(root);
13491
+ const firstIcon = Object.values(langDef.icon)[0];
13492
+ return firstIcon || 'FileCode text-neutral-500';
13493
+ }
13494
+ // _____ _ _
13495
+ // |_ _|___| |_ ___ ___|_|___ ___ ___
13496
+ // | | | . | '_| -_| | |- _| -_| _|
13497
+ // |_| |___|_,_|___|_|_|_|___|___|_|
13498
+ class Tokenizer {
13499
+ static languages = new Map();
13500
+ static extensionMap = new Map(); // ext -> language name
13501
+ static registerLanguage(def) {
13502
+ Tokenizer.languages.set(def.name, def);
13503
+ for (const ext of def.extensions) {
13504
+ Tokenizer.extensionMap.set(ext, def.name);
13505
+ }
13459
13506
  }
13460
- else {
13461
- this.root = document.body;
13507
+ static getLanguage(name) {
13508
+ return Tokenizer.languages.get(name);
13462
13509
  }
13463
- // Notifications
13464
- {
13465
- const notifSection = document.createElement('section');
13466
- notifSection.className = 'notifications';
13467
- this.notifications = document.createElement('ol');
13468
- this.notifications.className = 'fixed flex flex-col-reverse m-0 p-0 gap-1 z-1000';
13469
- this.notifications.iWidth = 0;
13470
- notifSection.appendChild(this.notifications);
13471
- document.body.appendChild(notifSection);
13472
- this.notifications.addEventListener('mouseenter', () => {
13473
- this.notifications.classList.add('list');
13474
- });
13475
- this.notifications.addEventListener('mouseleave', () => {
13476
- this.notifications.classList.remove('list');
13477
- });
13510
+ static getLanguageByExtension(ext) {
13511
+ const name = Tokenizer.extensionMap.get(ext);
13512
+ return name ? Tokenizer.languages.get(name) : undefined;
13478
13513
  }
13479
- // Disable drag icon
13480
- root.addEventListener('dragover', function (e) {
13481
- e.preventDefault();
13482
- }, false);
13483
- document.addEventListener('contextmenu', function (e) {
13484
- e.preventDefault();
13485
- }, false);
13486
- // Global vars
13487
- this.DEFAULT_NAME_WIDTH = '30%';
13488
- this.DEFAULT_SPLITBAR_SIZE = 4;
13489
- this.OPEN_CONTEXTMENU_ENTRY = 'click';
13490
- this.componentResizeObserver = new ResizeObserver((entries) => {
13491
- for (const entry of entries) {
13492
- const c = entry.target?.jsInstance;
13493
- if (c && c.onResize) {
13494
- c.onResize(entry.contentRect);
13514
+ static getRegisteredLanguages() {
13515
+ return Array.from(Tokenizer.languages.keys());
13516
+ }
13517
+ static initialState() {
13518
+ return { stack: ['root'] };
13519
+ }
13520
+ /**
13521
+ * Tokenize a single line given a language and the state from the previous line.
13522
+ * Returns the tokens and the updated state for the next line.
13523
+ */
13524
+ static tokenizeLine(line, language, state) {
13525
+ const tokens = [];
13526
+ const stack = [...state.stack]; // clone
13527
+ let pos = 0;
13528
+ while (pos < line.length) {
13529
+ const currentState = stack[stack.length - 1];
13530
+ const rules = language.states[currentState];
13531
+ if (!rules) {
13532
+ // No rules for this state, so emit rest as text
13533
+ tokens.push({ type: 'text', value: line.slice(pos) });
13534
+ pos = line.length;
13535
+ break;
13536
+ }
13537
+ let matched = false;
13538
+ for (const rule of rules) {
13539
+ // Anchor regex at current position
13540
+ const regex = new RegExp(rule.match.source, 'y' + (rule.match.flags.replace(/[gy]/g, '')));
13541
+ regex.lastIndex = pos;
13542
+ const m = regex.exec(line);
13543
+ if (m) {
13544
+ if (m[0].length === 0) {
13545
+ // Zero-length match, skipping to avoid infinite loop...
13546
+ continue;
13547
+ }
13548
+ tokens.push({ type: rule.type, value: m[0] });
13549
+ pos += m[0].length;
13550
+ // State transitions
13551
+ if (rule.next) {
13552
+ stack.push(rule.next);
13553
+ }
13554
+ else if (rule.pop) {
13555
+ const popCount = typeof rule.pop === 'number' ? rule.pop : 1;
13556
+ for (let i = 0; i < popCount && stack.length > 1; i++) {
13557
+ stack.pop();
13558
+ }
13559
+ }
13560
+ matched = true;
13561
+ break;
13562
+ }
13563
+ }
13564
+ if (!matched) {
13565
+ // No rule matched, consume one character as text either merged or as a new token
13566
+ const lastToken = tokens[tokens.length - 1];
13567
+ if (lastToken && lastToken.type === 'text') {
13568
+ lastToken.value += line[pos];
13569
+ }
13570
+ else {
13571
+ tokens.push({ type: 'text', value: line[pos] });
13572
+ }
13573
+ pos++;
13495
13574
  }
13496
13575
  }
13497
- });
13498
- this.ready = true;
13499
- this.menubars = [];
13500
- this.sidebars = [];
13501
- this.commandbar = this._createCommandbar(this.container);
13502
- if (!options.skipRoot && !options.skipDefaultArea) {
13503
- this.mainArea = new Area({ id: options.id ?? 'mainarea' });
13504
- }
13505
- // Initial or automatic changes don't force color scheme
13506
- // to be stored in localStorage
13507
- this._onChangeSystemTheme = function (event) {
13508
- const storedcolorScheme = localStorage.getItem('lxColorScheme');
13509
- if (storedcolorScheme)
13510
- return;
13511
- exports.LX.setMode(event.matches ? 'dark' : 'light', false);
13512
- };
13513
- this._mqlPrefersDarkScheme = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
13514
- const storedcolorScheme = localStorage.getItem('lxColorScheme');
13515
- if (storedcolorScheme) {
13516
- exports.LX.setMode(storedcolorScheme);
13576
+ const merged = Tokenizer._mergeTokens(tokens);
13577
+ return { tokens: merged, state: { stack } };
13517
13578
  }
13518
- else if (this._mqlPrefersDarkScheme && (options.autoTheme ?? true)) {
13519
- if (window.matchMedia('(prefers-color-scheme: light)').matches) {
13520
- exports.LX.setMode('light', false);
13579
+ /**
13580
+ * Merge consecutive tokens with the same type into one.
13581
+ */
13582
+ static _mergeTokens(tokens) {
13583
+ if (tokens.length === 0)
13584
+ return tokens;
13585
+ const result = [tokens[0]];
13586
+ for (let i = 1; i < tokens.length; i++) {
13587
+ const prev = result[result.length - 1];
13588
+ if (tokens[i].type === prev.type) {
13589
+ prev.value += tokens[i].value;
13590
+ }
13591
+ else {
13592
+ result.push(tokens[i]);
13593
+ }
13521
13594
  }
13522
- this._mqlPrefersDarkScheme.addEventListener('change', this._onChangeSystemTheme);
13595
+ return result;
13523
13596
  }
13524
- // LX.setThemeColor( 'rose' );
13525
- return this.mainArea;
13526
- };
13597
+ }
13598
+ exports.LX.Tokenizer = Tokenizer;
13599
+ // __ _____ _
13600
+ // | | ___ ___ ___ _ _ ___ ___ ___ | | |___| |___ ___ ___ ___
13601
+ // | |__| .'| | . | | | .'| . | -_| | | -_| | . | -_| _|_ -|
13602
+ // |_____|__,|_|_|_ |___|__,|_ |___| |__|__|___|_| _|___|_| |___|
13603
+ // |___| |___| |_|
13527
13604
  /**
13528
- * @method setSpacingMode
13529
- * @param {String} mode: "default" | "compact"
13605
+ * Build a word-boundary regex from a list of words to use as a language rule "match".
13606
+ * e.g. words(['const', 'let', 'var']) → /\b(?:const|let|var)\b/
13530
13607
  */
13531
- exports.LX.setSpacingMode = function (mode) {
13532
- this.spacingMode = mode;
13533
- document.documentElement.setAttribute('data-spacing', this.spacingMode);
13534
- };
13608
+ function words(list) {
13609
+ return new RegExp('\\b(?:' + list.join('|') + ')\\b');
13610
+ }
13535
13611
  /**
13536
- * @method setLayoutMode
13537
- * @param {String} mode: "app" | "document"
13612
+ * Common state rules reusable across C-like languages.
13538
13613
  */
13539
- exports.LX.setLayoutMode = function (mode) {
13540
- this.layoutMode = mode;
13541
- document.documentElement.setAttribute('data-layout', this.layoutMode);
13614
+ const CommonStates = {
13615
+ blockComment: [
13616
+ { match: /\*\//, type: 'comment', pop: true },
13617
+ { match: /[^*]+/, type: 'comment' },
13618
+ { match: /\*/, type: 'comment' },
13619
+ ],
13620
+ doubleString: [
13621
+ { match: /\\./, type: 'string' },
13622
+ { match: /"/, type: 'string', pop: true },
13623
+ { match: /[^"\\]+/, type: 'string' },
13624
+ ],
13625
+ singleString: [
13626
+ { match: /\\./, type: 'string' },
13627
+ { match: /'/, type: 'string', pop: true },
13628
+ { match: /[^'\\]+/, type: 'string' },
13629
+ ],
13542
13630
  };
13543
- /**
13544
- * @method addSignal
13545
- * @param {String} name
13546
- * @param {Object} obj
13547
- * @param {Function} callback
13548
- */
13549
- exports.LX.addSignal = function (name, obj, callback) {
13550
- obj[name] = callback;
13551
- if (!exports.LX.signals[name]) {
13552
- exports.LX.signals[name] = [];
13553
- }
13554
- if (exports.LX.signals[name].indexOf(obj) > -1) {
13555
- return;
13631
+ /** Common number rules for C-like languages. */
13632
+ const NumberRules = [
13633
+ // Binary: 0b1010, 0B1010
13634
+ { match: /0[bB][01]+(?:[uU][lL]{0,2}|[lL]{1,2}[uU]?)?\b/, type: 'number' },
13635
+ // Hex: 0xFF, 0xDEADBEEF, 0xFFu, 0xFFul, 0xFFull
13636
+ { match: /0[xX][0-9a-fA-F]+(?:[uU][lL]{0,2}|[lL]{1,2}[uU]?)?\b/, type: 'number' },
13637
+ // Octal: 0o77, 0O77
13638
+ { match: /0[oO][0-7]+(?:[uU][lL]{0,2}|[lL]{1,2}[uU]?)?\b/, type: 'number' },
13639
+ // Decimal with optional suffix: 123, 123.456, 1.23e10, 0.5f, 16u, 100L, 42ul, 3.14d, 100m
13640
+ { match: /\d+\.?\d*(?:[eE][+-]?\d+)?(?:[fFdDmMlLuUiI]|[uU][lL]{0,2}|[lL]{1,2}[uU]?)?\b/, type: 'number' },
13641
+ // Decimal starting with dot: .123, .5f
13642
+ { match: /\.\d+(?:[eE][+-]?\d+)?(?:[fFdDmM])?\b/, type: 'number' },
13643
+ ];
13644
+ /** Common tail rules: method detection, identifiers, symbols, whitespace. */
13645
+ const TailRules = [
13646
+ { match: /[a-zA-Z_$]\w*(?=\s*[<(])/, type: 'method' }, // function/method names (followed by < or ()
13647
+ { match: /[a-zA-Z_$]\w*/, type: 'text' },
13648
+ { match: /[{}()\[\];,.:?!&|<>=+\-*/%^~@#]/, type: 'symbol' },
13649
+ { match: /\s+/, type: 'text' },
13650
+ ];
13651
+ /** Template string states for JS/TS. */
13652
+ function templateStringStates(exprKeywords) {
13653
+ return {
13654
+ templateString: [
13655
+ { match: /\\./, type: 'string' },
13656
+ { match: /`/, type: 'string', pop: true },
13657
+ { match: /\$\{/, type: 'symbol', next: 'templateExpr' },
13658
+ { match: /[^`\\$]+/, type: 'string' },
13659
+ { match: /\$/, type: 'string' },
13660
+ ],
13661
+ templateExpr: [
13662
+ { match: /\}/, type: 'symbol', pop: true },
13663
+ { match: /\/\*/, type: 'comment', next: 'blockComment' },
13664
+ { match: /\/\/.*/, type: 'comment' },
13665
+ { match: /"/, type: 'string', next: 'doubleString' },
13666
+ { match: /'/, type: 'string', next: 'singleString' },
13667
+ { match: /`/, type: 'string', next: 'templateString' },
13668
+ ...NumberRules,
13669
+ { match: words(exprKeywords), type: 'keyword' },
13670
+ ...TailRules,
13671
+ ],
13672
+ };
13673
+ }
13674
+ // __ ____ ___ _ _ _ _
13675
+ // | | ___ ___ ___ _ _ ___ ___ ___ | \ ___| _|_|___|_| |_|_|___ ___ ___
13676
+ // | |__| .'| | . | | | .'| . | -_| | | | -_| _| | | | _| | . | |_ -|
13677
+ // |_____|__,|_|_|_ |___|__,|_ |___| |____/|___|_| |_|_|_|_|_| |_|___|_|_|___|
13678
+ // |___| |___|
13679
+ Tokenizer.registerLanguage({
13680
+ name: 'Plain Text',
13681
+ extensions: ['txt'],
13682
+ states: {
13683
+ root: [
13684
+ { match: /.+/, type: 'text' }
13685
+ ]
13686
+ },
13687
+ reservedWords: [],
13688
+ icon: 'FileText text-neutral-500'
13689
+ });
13690
+ // JavaScript
13691
+ const jsKeywords = [
13692
+ 'var', 'let', 'const', 'this', 'in', 'of', 'true', 'false', 'null', 'undefined',
13693
+ 'new', 'function', 'class', 'extends', 'super', 'import', 'export', 'from',
13694
+ 'default', 'async', 'typeof', 'instanceof', 'void', 'delete', 'debugger', 'NaN',
13695
+ 'static', 'constructor', 'Infinity', 'abstract'
13696
+ ];
13697
+ const jsStatements = [
13698
+ 'for', 'if', 'else', 'switch', 'case', 'return', 'while', 'do', 'continue', 'break',
13699
+ 'await', 'yield', 'throw', 'try', 'catch', 'finally', 'with'
13700
+ ];
13701
+ const jsBuiltins = [
13702
+ 'document', 'console', 'window', 'navigator', 'performance',
13703
+ 'Math', 'JSON', 'Promise', 'Array', 'Object', 'String', 'Number', 'Boolean',
13704
+ 'RegExp', 'Error', 'Map', 'Set', 'WeakMap', 'WeakSet', 'Symbol', 'Proxy', 'Reflect'
13705
+ ];
13706
+ Tokenizer.registerLanguage({
13707
+ name: 'JavaScript',
13708
+ extensions: ['js', 'mjs', 'cjs'],
13709
+ lineComment: '//',
13710
+ states: {
13711
+ root: [
13712
+ { match: /\/\*/, type: 'comment', next: 'blockComment' },
13713
+ { match: /\/\/.*/, type: 'comment' },
13714
+ { match: /`/, type: 'string', next: 'templateString' },
13715
+ { match: /"/, type: 'string', next: 'doubleString' },
13716
+ { match: /'/, type: 'string', next: 'singleString' },
13717
+ ...NumberRules,
13718
+ { match: words(jsKeywords), type: 'keyword' },
13719
+ { match: words(jsBuiltins), type: 'builtin' },
13720
+ { match: words(jsStatements), type: 'statement' },
13721
+ { match: /(?<=\b(?:class|enum)\s+)[A-Z][a-zA-Z0-9_]*/, type: 'type' }, // class/enum names
13722
+ ...TailRules,
13723
+ ],
13724
+ ...CommonStates,
13725
+ ...templateStringStates(['var', 'let', 'const', 'this', 'true', 'false', 'null', 'undefined', 'new', 'typeof', 'instanceof', 'void']),
13726
+ },
13727
+ reservedWords: [...jsKeywords, ...jsStatements, ...jsBuiltins],
13728
+ icon: 'Js text-yellow-500'
13729
+ });
13730
+ // TypeScript
13731
+ const tsKeywords = [
13732
+ ...jsKeywords,
13733
+ 'as', 'interface', 'type', 'enum', 'namespace', 'declare', 'private', 'protected',
13734
+ 'implements', 'readonly', 'keyof', 'infer', 'is', 'asserts', 'override', 'satisfies'
13735
+ ];
13736
+ const tsTypes = [
13737
+ 'string', 'number', 'boolean', 'any', 'unknown', 'never', 'void', 'null',
13738
+ 'undefined', 'object', 'symbol', 'bigint', 'Promise',
13739
+ 'Record', 'Partial', 'Required', 'Readonly', 'Pick', 'Omit', 'Exclude',
13740
+ 'Extract', 'NonNullable', 'ReturnType', 'Parameters', 'ConstructorParameters',
13741
+ 'InstanceType', 'Awaited'
13742
+ ];
13743
+ Tokenizer.registerLanguage({
13744
+ name: 'TypeScript',
13745
+ extensions: ['ts', 'tsx'],
13746
+ lineComment: '//',
13747
+ states: {
13748
+ root: [
13749
+ { match: /\/\*/, type: 'comment', next: 'blockComment' },
13750
+ { match: /\/\/.*/, type: 'comment' },
13751
+ { match: /`/, type: 'string', next: 'templateString' },
13752
+ { match: /"/, type: 'string', next: 'doubleString' },
13753
+ { match: /'/, type: 'string', next: 'singleString' },
13754
+ ...NumberRules,
13755
+ { match: words(tsKeywords), type: 'keyword' },
13756
+ { match: words(tsTypes), type: 'type' },
13757
+ { match: words(jsBuiltins), type: 'builtin' },
13758
+ { match: words(jsStatements), type: 'statement' },
13759
+ { match: /(?<=\b(?:class|enum|interface|type|extends|implements)\s+)[A-Z][a-zA-Z0-9_]*/, type: 'type' }, // class/enum/interface/type names
13760
+ { match: /(?<=<\s*)[A-Z][a-zA-Z0-9_]*(?=\s*(?:[,>]|extends|=))/, type: 'type' }, // type parameters in generics <T>
13761
+ { match: /(?<=,\s*)[A-Z][a-zA-Z0-9_]*(?=\s*(?:[,>]|extends|=))/, type: 'type' }, // type parameters after comma <T, K>
13762
+ { match: /(?<=:\s*)[A-Z][a-zA-Z0-9_]*/, type: 'type' }, // type annotations after colon
13763
+ ...TailRules,
13764
+ ],
13765
+ ...CommonStates,
13766
+ ...templateStringStates(['var', 'let', 'const', 'this', 'true', 'false', 'null', 'undefined', 'new', 'typeof', 'instanceof', 'void']),
13767
+ },
13768
+ reservedWords: [...tsKeywords, ...tsTypes, ...jsBuiltins, ...jsStatements],
13769
+ icon: 'Ts text-blue-600'
13770
+ });
13771
+ // WGSL (WebGPU Shading Language)
13772
+ const wgslKeywords = [
13773
+ 'bool', 'i32', 'u32', 'f16', 'f32', 'vec2', 'vec3', 'vec4', 'vec2i', 'vec3i', 'vec4i',
13774
+ 'vec2u', 'vec3u', 'vec4u', 'vec2f', 'vec3f', 'vec4f', 'mat2x2f', 'mat2x3f', 'mat2x4f', 'mat3x2f', 'mat3x3f',
13775
+ 'mat3x4f', 'mat4x2f', 'mat4x3f', 'mat4x4f', 'array', 'struct', 'ptr', 'atomic', 'sampler', 'sampler_comparison',
13776
+ 'texture_1d', 'texture_2d', 'texture_2d_array', 'texture_3d', 'texture_cube', 'texture_cube_array', 'texture_multisampled_2d',
13777
+ 'texture_depth_2d', 'texture_depth_2d_array', 'texture_depth_cube', 'texture_depth_cube_array',
13778
+ 'texture_depth_multisampled_2d', 'texture_storage_1d', 'texture_storage_2d', 'texture_storage_2d_array',
13779
+ 'texture_storage_3d', 'texture_external', 'var', 'let', 'const', 'override', 'fn', 'type', 'alias',
13780
+ 'true', 'false'
13781
+ ];
13782
+ const wgslStatements = [
13783
+ 'if', 'else', 'switch', 'case', 'default', 'for', 'loop', 'while', 'break', 'continue', 'discard',
13784
+ 'return', 'function', 'private', 'workgroup', 'uniform', 'storage', 'read', 'write', 'read_write', 'bitcast'
13785
+ ];
13786
+ const wgslBuiltins = [
13787
+ 'position', 'vertex_index', 'instance_index', 'front_facing', 'frag_depth',
13788
+ 'local_invocation_id', 'local_invocation_index', 'global_invocation_id', 'workgroup_id', 'num_workgroups',
13789
+ 'abs', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atanh', 'atan2', 'ceil', 'clamp', 'cos', 'cosh',
13790
+ 'cross', 'degrees', 'determinant', 'distance', 'dot', 'exp', 'exp2', 'floor', 'fma', 'fract', 'inverseSqrt',
13791
+ 'length', 'log', 'log2', 'max', 'min', 'mix', 'normalize', 'pow', 'radians', 'reflect', 'refract', 'round',
13792
+ 'saturate', 'sign', 'sin', 'sinh', 'smoothstep', 'sqrt', 'step', 'tan', 'tanh', 'transpose', 'trunc',
13793
+ 'textureSample', 'textureSampleBias', 'textureSampleLevel', 'textureSampleGrad',
13794
+ 'textureSampleCompare', 'textureSampleCompareLevel', 'textureSampleBaseClampToEdge',
13795
+ 'textureLoad', 'textureStore', 'textureGather', 'textureGatherCompare',
13796
+ 'textureDimensions', 'textureNumLayers', 'textureNumLevels', 'textureNumSamples',
13797
+ 'pack4x8snorm', 'pack4x8unorm', 'pack2x16snorm', 'pack2x16unorm', 'pack2x16float',
13798
+ 'unpack4x8snorm', 'unpack4x8unorm', 'unpack2x16snorm', 'unpack2x16unorm', 'unpack2x16float',
13799
+ 'atomicLoad', 'atomicStore', 'atomicAdd', 'atomicSub', 'atomicMax', 'atomicMin',
13800
+ 'atomicAnd', 'atomicOr', 'atomicXor', 'atomicExchange', 'atomicCompareExchangeWeak',
13801
+ 'dpdx', 'dpdxCoarse', 'dpdxFine', 'dpdy', 'dpdyCoarse', 'dpdyFine', 'fwidth', 'fwidthCoarse', 'fwidthFine',
13802
+ 'select', 'arrayLength', 'countLeadingZeros', 'countOneBits', 'countTrailingZeros',
13803
+ 'extractBits', 'firstLeadingBit', 'firstTrailingBit', 'insertBits', 'reverseBits',
13804
+ 'storageBarrier', 'workgroupBarrier', 'workgroupUniformLoad'
13805
+ ];
13806
+ Tokenizer.registerLanguage({
13807
+ name: 'WGSL',
13808
+ extensions: ['wgsl'],
13809
+ lineComment: '//',
13810
+ states: {
13811
+ root: [
13812
+ { match: /\/\*/, type: 'comment', next: 'blockComment' },
13813
+ { match: /\/\/.*/, type: 'comment' },
13814
+ { match: /#\w+/, type: 'preprocessor' },
13815
+ ...NumberRules,
13816
+ { match: words(wgslKeywords), type: 'keyword' },
13817
+ { match: words(wgslBuiltins), type: 'builtin' },
13818
+ { match: words(wgslStatements), type: 'statement' },
13819
+ { match: /@\w+/, type: 'text' },
13820
+ ...TailRules,
13821
+ ],
13822
+ ...CommonStates
13823
+ },
13824
+ reservedWords: [...wgslKeywords, ...wgslBuiltins, ...wgslStatements],
13825
+ icon: 'AlignLeft text-orange-500'
13826
+ });
13827
+ // GLSL (OpenGL/WebGL Shading Language)
13828
+ const glslKeywords = [
13829
+ 'true', 'false', 'int', 'float', 'double', 'bool', 'void', 'uint', 'struct', 'mat2', 'mat3',
13830
+ 'mat4', 'mat2x2', 'mat2x3', 'mat2x4', 'mat3x2', 'mat3x3', 'mat3x4', 'mat4x2', 'mat4x3', 'mat4x4',
13831
+ 'vec2', 'vec3', 'vec4', 'ivec2', 'ivec3', 'ivec4', 'uvec2', 'uvec3', 'uvec4', 'dvec2', 'dvec3',
13832
+ 'dvec4', 'bvec2', 'bvec3', 'bvec4', 'sampler1D', 'sampler2D', 'sampler3D', 'samplerCube',
13833
+ 'sampler2DShadow', 'samplerCubeShadow', 'sampler2DArray', 'sampler2DArrayShadow',
13834
+ 'samplerCubeArray', 'samplerCubeArrayShadow', 'isampler2D', 'usampler2D', 'isampler3D',
13835
+ 'usampler3D', 'lowp', 'mediump', 'highp', 'precision', 'in', 'out', 'inout', 'uniform',
13836
+ 'varying', 'attribute', 'const', 'layout', 'centroid', 'flat', 'smooth', 'noperspective',
13837
+ 'patch', 'sample', 'buffer', 'shared', 'coherent', 'volatile', 'restrict', 'readonly', 'writeonly'
13838
+ ];
13839
+ const glslStatements = [
13840
+ 'if', 'else', 'switch', 'case', 'default', 'for', 'while', 'do', 'break', 'continue',
13841
+ 'return', 'discard'
13842
+ ];
13843
+ const glslBuiltins = [
13844
+ 'radians', 'degrees', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'pow', 'exp', 'log',
13845
+ 'exp2', 'log2', 'sqrt', 'inversesqrt', 'abs', 'sign', 'floor', 'ceil', 'fract',
13846
+ 'mod', 'min', 'max', 'clamp', 'mix', 'step', 'smoothstep', 'length', 'distance',
13847
+ 'dot', 'cross', 'normalize', 'reflect', 'refract', 'matrixCompMult',
13848
+ 'lessThan', 'lessThanEqual', 'greaterThan', 'greaterThanEqual',
13849
+ 'equal', 'notEqual', 'any', 'all', 'not', 'texture', 'textureProj',
13850
+ 'textureLod', 'textureGrad', 'texelFetch'
13851
+ ];
13852
+ Tokenizer.registerLanguage({
13853
+ name: 'GLSL',
13854
+ extensions: ['glsl'],
13855
+ lineComment: '//',
13856
+ states: {
13857
+ root: [
13858
+ { match: /\/\*/, type: 'comment', next: 'blockComment' },
13859
+ { match: /\/\/.*/, type: 'comment' },
13860
+ { match: /#\w+/, type: 'preprocessor' },
13861
+ ...NumberRules,
13862
+ { match: words(glslKeywords), type: 'keyword' },
13863
+ { match: words(glslBuiltins), type: 'builtin' },
13864
+ { match: words(glslStatements), type: 'statement' },
13865
+ ...TailRules,
13866
+ ],
13867
+ ...CommonStates
13868
+ },
13869
+ reservedWords: [...glslKeywords, ...glslBuiltins, ...glslStatements],
13870
+ icon: 'AlignLeft text-neutral-500'
13871
+ });
13872
+ // HLSL (DirectX Shader Language)
13873
+ const hlslKeywords = [
13874
+ 'bool', 'int', 'uint', 'dword', 'half', 'float', 'double', 'min16float', 'min10float', 'min16int', 'min12int', 'min16uint',
13875
+ 'float1', 'float2', 'float3', 'float4', 'int1', 'int2', 'int3', 'int4', 'uint1', 'uint2', 'uint3', 'uint4',
13876
+ 'bool1', 'bool2', 'bool3', 'bool4', 'half1', 'half2', 'half3', 'half4',
13877
+ 'float1x1', 'float1x2', 'float1x3', 'float1x4', 'float2x1', 'float2x2', 'float2x3', 'float2x4',
13878
+ 'float3x1', 'float3x2', 'float3x3', 'float3x4', 'float4x1', 'float4x2', 'float4x3', 'float4x4',
13879
+ 'vector', 'matrix', 'string', 'void', 'struct', 'class', 'interface', 'true', 'false',
13880
+ 'sampler', 'sampler1D', 'sampler2D', 'sampler3D', 'samplerCUBE', 'sampler_state',
13881
+ 'Texture1D', 'Texture2D', 'Texture3D', 'TextureCube', 'Texture1DArray', 'Texture2DArray', 'TextureCubeArray',
13882
+ 'Buffer', 'AppendStructuredBuffer', 'ConsumeStructuredBuffer', 'StructuredBuffer', 'RWStructuredBuffer',
13883
+ 'ByteAddressBuffer', 'RWByteAddressBuffer', 'RWTexture1D', 'RWTexture2D', 'RWTexture3D', 'RWTexture1DArray', 'RWTexture2DArray',
13884
+ 'cbuffer', 'tbuffer', 'in', 'out', 'inout', 'uniform', 'extern', 'static', 'volatile', 'precise', 'shared', 'groupshared',
13885
+ 'linear', 'centroid', 'nointerpolation', 'noperspective', 'sample', 'const', 'row_major', 'column_major'
13886
+ ];
13887
+ const hlslStatements = [
13888
+ 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'default', 'break', 'continue', 'discard', 'return',
13889
+ 'typedef', 'register', 'packoffset'
13890
+ ];
13891
+ const hlslBuiltins = [
13892
+ 'SV_Position', 'SV_Target', 'SV_Depth', 'SV_VertexID', 'SV_InstanceID', 'SV_PrimitiveID', 'SV_DispatchThreadID',
13893
+ 'SV_GroupID', 'SV_GroupThreadID', 'SV_GroupIndex', 'SV_Coverage', 'SV_IsFrontFace', 'SV_RenderTargetArrayIndex',
13894
+ 'POSITION', 'NORMAL', 'TEXCOORD', 'COLOR', 'TANGENT', 'BINORMAL'
13895
+ ];
13896
+ Tokenizer.registerLanguage({
13897
+ name: 'HLSL',
13898
+ extensions: ['hlsl', 'fx', 'fxh', 'vsh', 'psh'],
13899
+ lineComment: '//',
13900
+ states: {
13901
+ root: [
13902
+ { match: /\/\*/, type: 'comment', next: 'blockComment' },
13903
+ { match: /\/\/.*/, type: 'comment' },
13904
+ { match: /#\w+/, type: 'preprocessor' },
13905
+ ...NumberRules,
13906
+ { match: words(hlslKeywords), type: 'keyword' },
13907
+ { match: words(hlslBuiltins), type: 'builtin' },
13908
+ { match: words(hlslStatements), type: 'statement' },
13909
+ ...TailRules,
13910
+ ],
13911
+ ...CommonStates
13912
+ },
13913
+ reservedWords: [...hlslKeywords, ...hlslBuiltins, ...hlslStatements],
13914
+ icon: 'AlignLeft text-purple-500'
13915
+ });
13916
+ // Python
13917
+ const pyKeywords = [
13918
+ 'False', 'def', 'None', 'True', 'in', 'is', 'and', 'lambda', 'nonlocal', 'not', 'or'
13919
+ ];
13920
+ const pyStatements = [
13921
+ 'if', 'elif', 'else', 'for', 'while', 'try', 'except', 'finally', 'with', 'match', 'case',
13922
+ 'break', 'continue', 'return', 'raise', 'pass', 'import', 'from', 'as', 'global', 'nonlocal',
13923
+ 'assert', 'del', 'yield'
13924
+ ];
13925
+ const pyBuiltins = [
13926
+ 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod',
13927
+ 'compile', 'complex', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'filter',
13928
+ 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id',
13929
+ 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'list', 'locals', 'map', 'max',
13930
+ 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property',
13931
+ 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod',
13932
+ 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip'
13933
+ ];
13934
+ const pyTypes = [
13935
+ 'int', 'float', 'complex', 'bool', 'str', 'bytes', 'bytearray', 'list', 'tuple', 'set', 'frozenset',
13936
+ 'dict', 'object', 'type', 'ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException',
13937
+ 'BufferError', 'EOFError', 'Exception', 'FloatingPointError', 'GeneratorExit', 'ImportError',
13938
+ 'ModuleNotFoundError', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt',
13939
+ 'LookupError', 'MemoryError', 'NameError', 'NotImplementedError', 'OSError', 'OverflowError',
13940
+ 'RecursionError', 'ReferenceError', 'RuntimeError', 'StopAsyncIteration', 'StopIteration',
13941
+ 'SyntaxError', 'TabError', 'SystemError', 'SystemExit', 'TypeError', 'UnboundLocalError',
13942
+ 'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError',
13943
+ 'ValueError', 'ZeroDivisionError'
13944
+ ];
13945
+ Tokenizer.registerLanguage({
13946
+ name: 'Python',
13947
+ extensions: ['py'],
13948
+ lineComment: '#',
13949
+ states: {
13950
+ root: [
13951
+ { match: /\/\*/, type: 'comment', next: 'blockComment' },
13952
+ { match: /#.*/, type: 'comment' },
13953
+ { match: /"/, type: 'string', next: 'doubleString' },
13954
+ { match: /'/, type: 'string', next: 'singleString' },
13955
+ ...NumberRules,
13956
+ { match: words(pyKeywords), type: 'keyword' },
13957
+ { match: words(pyTypes), type: 'type' },
13958
+ { match: words(pyBuiltins), type: 'builtin' },
13959
+ { match: words(pyStatements), type: 'statement' },
13960
+ ...TailRules,
13961
+ ],
13962
+ ...CommonStates
13963
+ },
13964
+ reservedWords: [...pyKeywords, ...pyTypes, ...pyBuiltins, ...pyStatements],
13965
+ icon: 'Python text-cyan-600'
13966
+ });
13967
+ // PHP
13968
+ const phpKeywords = [
13969
+ 'abstract', 'and', 'array', 'as', 'callable', 'class', 'clone', 'const',
13970
+ 'enum', 'extends', 'final', 'fn', 'function', 'global',
13971
+ 'implements', 'include', 'include_once', 'instanceof',
13972
+ 'insteadof', 'interface', 'namespace', 'new', 'null', 'or',
13973
+ 'private', 'protected', 'public', 'readonly', 'require',
13974
+ 'require_once', 'static', 'trait', 'use', 'var', 'xor',
13975
+ 'from', '$this'
13976
+ ];
13977
+ const phpStatements = [
13978
+ 'if', 'else', 'elseif', 'endif', 'switch', 'case', 'default', 'endswitch',
13979
+ 'for', 'endfor', 'foreach', 'endforeach', 'while', 'endwhile', 'do',
13980
+ 'break', 'continue', 'return', 'try', 'catch', 'finally', 'throw',
13981
+ 'declare', 'enddeclare', 'goto', 'yield', 'match'
13982
+ ];
13983
+ const phpTypes = [
13984
+ 'int', 'float', 'string', 'bool', 'array', 'object', 'callable', 'iterable',
13985
+ 'void', 'never', 'mixed', 'static', 'self', 'parent',
13986
+ 'Exception', 'Error', 'Throwable', 'DateTime', 'DateTimeImmutable',
13987
+ 'Closure', 'Generator', 'JsonSerializable'
13988
+ ];
13989
+ const phpBuiltins = [
13990
+ 'echo', 'print', 'isset', 'empty', 'unset', 'eval', 'die', 'exit',
13991
+ 'count', 'sizeof', 'in_array', 'array_merge', 'array_push', 'array_pop',
13992
+ 'strlen', 'strpos', 'substr', 'str_replace', 'explode', 'implode',
13993
+ 'json_encode', 'json_decode', 'var_dump', 'print_r'
13994
+ ];
13995
+ Tokenizer.registerLanguage({
13996
+ name: 'PHP',
13997
+ extensions: ['php'],
13998
+ lineComment: '//',
13999
+ states: {
14000
+ root: [
14001
+ { match: /\/\*/, type: 'comment', next: 'blockComment' },
14002
+ { match: /\/\/.*/, type: 'comment' },
14003
+ { match: /"/, type: 'string', next: 'doubleString' },
14004
+ { match: /'/, type: 'string', next: 'singleString' },
14005
+ ...NumberRules,
14006
+ { match: words(phpKeywords), type: 'keyword' },
14007
+ { match: words(phpTypes), type: 'type' },
14008
+ { match: words(phpBuiltins), type: 'builtin' },
14009
+ { match: words(phpStatements), type: 'statement' },
14010
+ ...TailRules,
14011
+ ],
14012
+ ...CommonStates
14013
+ },
14014
+ reservedWords: [...phpKeywords, ...phpTypes, ...phpBuiltins, ...phpStatements],
14015
+ icon: 'Php text-purple-700'
14016
+ });
14017
+ // C
14018
+ const cKeywords = [
14019
+ 'int', 'float', 'double', 'bool', 'long', 'short', 'char', 'void', 'const', 'enum', 'extern', 'register', 'sizeof', 'static',
14020
+ 'struct', 'typedef', 'union', 'volatile', 'true', 'false'
14021
+ ];
14022
+ const cStatements = [
14023
+ 'break', 'continue', 'do', 'else', 'for', 'goto', 'if', 'return', 'switch', 'while'
14024
+ ];
14025
+ Tokenizer.registerLanguage({
14026
+ name: 'C',
14027
+ extensions: ['c', 'h'],
14028
+ lineComment: '//',
14029
+ states: {
14030
+ root: [
14031
+ { match: /\/\*/, type: 'comment', next: 'blockComment' },
14032
+ { match: /\/\/.*/, type: 'comment' },
14033
+ { match: /#\w+/, type: 'preprocessor' },
14034
+ { match: /"/, type: 'string', next: 'doubleString' },
14035
+ { match: /'/, type: 'string', next: 'singleString' },
14036
+ ...NumberRules,
14037
+ { match: words(cKeywords), type: 'keyword' },
14038
+ { match: words(cStatements), type: 'statement' },
14039
+ ...TailRules,
14040
+ ],
14041
+ ...CommonStates
14042
+ },
14043
+ reservedWords: [...cKeywords, ...cStatements],
14044
+ icon: { 'c': 'C text-sky-400', 'h': 'C text-fuchsia-500' }
14045
+ });
14046
+ // C++
14047
+ const cppKeywords = [
14048
+ ...cKeywords, 'wchar_t', 'static_cast', 'dynamic_cast', 'new', 'delete', 'auto', 'class', 'nullptr', 'NULL', 'signed',
14049
+ 'unsigned', 'namespace', 'static', 'private', 'public'
14050
+ ];
14051
+ const cppStatements = [
14052
+ ...cStatements, 'case', 'using', 'glm', 'spdlog', 'default'
14053
+ ];
14054
+ const cppTypes = [
14055
+ 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', 'int8_t', 'int16_t', 'int32_t', 'int64_t', 'size_t', 'ptrdiff_t'
14056
+ ];
14057
+ const cppBuiltins = [
14058
+ 'std', 'string', 'vector', 'map', 'set', 'unordered_map', 'unordered_set', 'array', 'tuple', 'optional', 'variant',
14059
+ 'cout', 'cin', 'cerr', 'clog'
14060
+ ];
14061
+ Tokenizer.registerLanguage({
14062
+ name: 'C++',
14063
+ extensions: ['cpp', 'hpp'],
14064
+ lineComment: '//',
14065
+ states: {
14066
+ root: [
14067
+ { match: /\/\*/, type: 'comment', next: 'blockComment' },
14068
+ { match: /\/\/.*/, type: 'comment' },
14069
+ { match: /#\w+/, type: 'preprocessor' },
14070
+ { match: /"/, type: 'string', next: 'doubleString' },
14071
+ { match: /'/, type: 'string', next: 'singleString' },
14072
+ ...NumberRules,
14073
+ { match: words(cppKeywords), type: 'keyword' },
14074
+ { match: words(cppTypes), type: 'type' },
14075
+ { match: words(cppBuiltins), type: 'builtin' },
14076
+ { match: words(cppStatements), type: 'statement' },
14077
+ ...TailRules,
14078
+ ],
14079
+ ...CommonStates
14080
+ },
14081
+ reservedWords: [...cppKeywords, ...cppTypes, ...cppBuiltins, ...cppStatements],
14082
+ icon: { 'cpp': 'CPlusPlus text-sky-400', 'hpp': 'CPlusPlus text-fuchsia-500' }
14083
+ });
14084
+ // JSON
14085
+ Tokenizer.registerLanguage({
14086
+ name: 'JSON',
14087
+ extensions: ['json', 'jsonc', 'bml'],
14088
+ states: {
14089
+ root: [
14090
+ { match: /"/, type: 'string', next: 'doubleString' },
14091
+ { match: /\btrue\b|\bfalse\b|\bnull\b/, type: 'keyword' },
14092
+ { match: /-?\d+\.?\d*(?:[eE][+-]?\d+)?/, type: 'number' },
14093
+ { match: /[{}[\]:,]/, type: 'symbol' },
14094
+ { match: /\s+/, type: 'text' },
14095
+ ],
14096
+ ...CommonStates
14097
+ },
14098
+ reservedWords: [],
14099
+ icon: 'Json text-yellow-600'
14100
+ });
14101
+ // XML
14102
+ Tokenizer.registerLanguage({
14103
+ name: 'XML',
14104
+ extensions: ['xml', 'xaml', 'xsd', 'xsl'],
14105
+ lineComment: '<!--',
14106
+ states: {
14107
+ root: [
14108
+ { match: /<!--/, type: 'comment', next: 'xmlComment' },
14109
+ { match: /<\?/, type: 'preprocessor', next: 'processingInstruction' },
14110
+ { match: /<\/[a-zA-Z][\w:-]*>/, type: 'keyword' },
14111
+ { match: /<[a-zA-Z][\w:-]*/, type: 'keyword', next: 'tag' },
14112
+ { match: /[^<]+/, type: 'text' },
14113
+ ],
14114
+ tag: [
14115
+ { match: /\/?>/, type: 'keyword', pop: true },
14116
+ { match: /[a-zA-Z][\w:-]*(?==)/, type: 'type' },
14117
+ { match: /"/, type: 'string', next: 'doubleString' },
14118
+ { match: /'/, type: 'string', next: 'singleString' },
14119
+ { match: /[^"'>/]+/, type: 'text' },
14120
+ ],
14121
+ xmlComment: [
14122
+ { match: /-->/, type: 'comment', pop: true },
14123
+ { match: /[^-]+/, type: 'comment' },
14124
+ { match: /-/, type: 'comment' },
14125
+ ],
14126
+ processingInstruction: [
14127
+ { match: /\?>/, type: 'preprocessor', pop: true },
14128
+ { match: /[^?]+/, type: 'preprocessor' },
14129
+ { match: /\?/, type: 'preprocessor' },
14130
+ ],
14131
+ ...CommonStates
14132
+ },
14133
+ reservedWords: [],
14134
+ icon: 'Rss text-orange-600'
14135
+ });
14136
+ // HTML
14137
+ const htmlTags = [
14138
+ 'html', 'head', 'body', 'title', 'meta', 'link', 'script', 'style',
14139
+ 'div', 'span', 'p', 'a', 'img', 'ul', 'ol', 'li', 'table', 'tr', 'td', 'th',
14140
+ 'form', 'input', 'button', 'select', 'option', 'textarea', 'label',
14141
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'footer', 'nav', 'section', 'article',
14142
+ 'aside', 'main', 'figure', 'figcaption', 'video', 'audio', 'source', 'canvas', 'svg'
14143
+ ];
14144
+ Tokenizer.registerLanguage({
14145
+ name: 'HTML',
14146
+ extensions: ['html'],
14147
+ lineComment: '<!--',
14148
+ states: {
14149
+ root: [
14150
+ { match: /<!--/, type: 'comment', next: 'xmlComment' },
14151
+ { match: /<!DOCTYPE/i, type: 'preprocessor' },
14152
+ { match: /<\/[a-zA-Z][\w-]*>/, type: 'keyword' },
14153
+ { match: /<script\b/i, type: 'keyword', next: 'scriptTag' },
14154
+ { match: /<style\b/i, type: 'keyword', next: 'styleTag' },
14155
+ { match: /<[a-zA-Z][\w-]*/, type: 'keyword', next: 'tag' },
14156
+ { match: /[^<]+/, type: 'text' },
14157
+ ],
14158
+ tag: [
14159
+ { match: /\/?>/, type: 'keyword', pop: true },
14160
+ { match: /[a-zA-Z][\w-]*(?==)/, type: 'type' },
14161
+ { match: /"/, type: 'string', next: 'doubleString' },
14162
+ { match: /'/, type: 'string', next: 'singleString' },
14163
+ { match: /[^"'>/]+/, type: 'text' },
14164
+ ],
14165
+ scriptTag: [
14166
+ { match: /\/>/, type: 'keyword', pop: true },
14167
+ { match: />/, type: 'keyword', next: 'scriptContent' },
14168
+ { match: /[a-zA-Z][\w-]*(?==)/, type: 'type' },
14169
+ { match: /"/, type: 'string', next: 'doubleString' },
14170
+ { match: /'/, type: 'string', next: 'singleString' },
14171
+ { match: /[^"'>/]+/, type: 'text' },
14172
+ ],
14173
+ scriptContent: [
14174
+ { match: /<\/script>/i, type: 'keyword', pop: 2 },
14175
+ { match: /[^<]+/, type: 'text' },
14176
+ { match: /</, type: 'text' },
14177
+ ],
14178
+ styleTag: [
14179
+ { match: /\/>/, type: 'keyword', pop: true },
14180
+ { match: />/, type: 'keyword', next: 'styleContent' },
14181
+ { match: /[a-zA-Z][\w-]*(?==)/, type: 'type' },
14182
+ { match: /"/, type: 'string', next: 'doubleString' },
14183
+ { match: /'/, type: 'string', next: 'singleString' },
14184
+ { match: /[^"'>/]+/, type: 'text' },
14185
+ ],
14186
+ styleContent: [
14187
+ { match: /<\/style>/i, type: 'keyword', pop: 2 },
14188
+ { match: /[^<]+/, type: 'text' },
14189
+ { match: /</, type: 'text' },
14190
+ ],
14191
+ xmlComment: [
14192
+ { match: /-->/, type: 'comment', pop: true },
14193
+ { match: /[^-]+/, type: 'comment' },
14194
+ { match: /-/, type: 'comment' },
14195
+ ],
14196
+ ...CommonStates
14197
+ },
14198
+ reservedWords: [...htmlTags],
14199
+ icon: 'Code text-orange-500'
14200
+ });
14201
+ // CSS
14202
+ const cssProperties = [
14203
+ 'color', 'background', 'border', 'margin', 'padding', 'font', 'display', 'position',
14204
+ 'width', 'height', 'top', 'left', 'right', 'bottom', 'flex', 'grid', 'z-index',
14205
+ 'opacity', 'transform', 'transition', 'animation', 'content', 'visibility'
14206
+ ];
14207
+ const cssPropertyValues = [
14208
+ 'inherit', 'initial', 'unset', 'revert', 'revert-layer', 'auto', 'none', 'hidden', 'visible', 'collapse',
14209
+ 'block', 'inline', 'inline-block', 'flex', 'inline-flex', 'grid', 'inline-grid', 'contents', 'list-item',
14210
+ 'static', 'relative', 'absolute', 'fixed', 'sticky', 'solid', 'dashed', 'dotted', 'double', 'groove', 'ridge',
14211
+ 'inset', 'outset', 'bold', 'bolder', 'lighter', 'normal', 'italic', 'oblique', 'uppercase', 'lowercase', 'capitalize',
14212
+ 'left', 'right', 'center', 'top', 'bottom', 'start', 'end', 'stretch', 'space-between', 'space-around', 'space-evenly',
14213
+ 'repeat', 'no-repeat', 'repeat-x', 'repeat-y', 'cover', 'contain', 'pointer', 'default', 'move', 'text', 'not-allowed',
14214
+ 'transparent', 'currentColor'
14215
+ ];
14216
+ const cssPseudos = [
14217
+ 'hover', 'active', 'focus', 'visited', 'link', 'before', 'after', 'first-child',
14218
+ 'last-child', 'nth-child', 'not', 'root', 'disabled', 'checked'
14219
+ ];
14220
+ Tokenizer.registerLanguage({
14221
+ name: 'CSS',
14222
+ extensions: ['css', 'scss', 'sass', 'less'],
14223
+ lineComment: '//',
14224
+ states: {
14225
+ root: [
14226
+ { match: /\/\*/, type: 'comment', next: 'blockComment' },
14227
+ { match: /\/\/.*/, type: 'comment' }, // SCSS/Less
14228
+ { match: /@[\w-]+/, type: 'statement' }, // @media, @import, @keyframes, etc.
14229
+ { match: /#[\w-]+/, type: 'keyword' }, // ID selectors
14230
+ { match: /\.[\w-]+/, type: 'keyword' }, // class selectors
14231
+ { match: /::?[\w-]+(?:\([^)]*\))?/, type: 'keyword' }, // pseudo-classes/elements
14232
+ { match: /\[[\w-]+(?:[~|^$*]?=(?:"[^"]*"|'[^']*'|[\w-]+))?\]/, type: 'type' }, // attribute selectors
14233
+ { match: /{/, type: 'symbol', next: 'properties' },
14234
+ { match: /}/, type: 'symbol' },
14235
+ { match: /[,>+~*]/, type: 'symbol' }, // combinators
14236
+ { match: /[\w-]+/, type: 'keyword' }, // element selectors
14237
+ { match: /\s+/, type: 'text' },
14238
+ ],
14239
+ properties: [
14240
+ { match: /\/\*/, type: 'comment', next: 'blockComment' },
14241
+ { match: /\/\/.*/, type: 'comment' }, // SCSS/Less
14242
+ { match: /}/, type: 'symbol', pop: true },
14243
+ { match: /[\w-]+(?=\s*:)/, type: 'type' }, // property names
14244
+ { match: /:/, type: 'symbol' },
14245
+ { match: words(cssPropertyValues), type: 'string' },
14246
+ { match: /;/, type: 'symbol' },
14247
+ { match: /"/, type: 'string', next: 'doubleString' },
14248
+ { match: /'/, type: 'string', next: 'singleString' },
14249
+ { match: /#[a-fA-F0-9]{3,8}\b/, type: 'string' }, // hex colors
14250
+ { match: /-?\d+\.?\d*(?:px|em|rem|%|vh|vw|vmin|vmax|pt|cm|mm|in|pc|ex|ch|fr|deg|rad|grad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?/, type: 'number' },
14251
+ { match: /!important\b/, type: 'builtin' },
14252
+ { match: /[\w-]+\(/, type: 'method' }, // functions: rgb(), calc(), var(), url()
14253
+ { match: /--[\w-]+/, type: 'type' }, // vars --
14254
+ { match: /[(),]/, type: 'symbol' },
14255
+ { match: /{/, type: 'symbol', next: 'properties' }, // nested rules (SCSS/Less)
14256
+ { match: /[\w-]+/, type: 'text' },
14257
+ { match: /\s+/, type: 'text' },
14258
+ ],
14259
+ ...CommonStates
14260
+ },
14261
+ reservedWords: [...cssProperties, ...cssPropertyValues, ...cssPseudos],
14262
+ icon: 'Hash text-blue-500'
14263
+ });
14264
+ // Markdown
14265
+ Tokenizer.registerLanguage({
14266
+ name: 'Markdown',
14267
+ extensions: ['md', 'markdown'],
14268
+ states: {
14269
+ root: [
14270
+ { match: /^#{1,6}\s+.+/, type: 'keyword' },
14271
+ { match: /^\*\*\*.+$/, type: 'symbol' },
14272
+ { match: /\*\*[^*]+\*\*/, type: 'keyword' },
14273
+ { match: /\*[^*]+\*/, type: 'type' },
14274
+ { match: /__[^_]+__/, type: 'keyword' },
14275
+ { match: /_[^_]+_/, type: 'type' },
14276
+ { match: /`[^`]+`/, type: 'string' },
14277
+ { match: /```/, type: 'comment', next: 'codeBlock' },
14278
+ { match: /^\s*[-*+]\s+/, type: 'symbol' },
14279
+ { match: /^\s*\d+\.\s+/, type: 'symbol' },
14280
+ { match: /\[([^\]]+)\]\(([^)]+)\)/, type: 'builtin' },
14281
+ { match: /^>\s+/, type: 'comment' },
14282
+ { match: /.+/, type: 'text' },
14283
+ ],
14284
+ codeBlock: [
14285
+ { match: /```/, type: 'comment', pop: true },
14286
+ { match: /.+/, type: 'string' },
14287
+ ]
14288
+ },
14289
+ reservedWords: [],
14290
+ icon: 'Markdown text-red-500'
14291
+ });
14292
+ // Batch
14293
+ const batchKeywords = [
14294
+ 'if', 'else', 'for', 'in', 'do', 'goto', 'call', 'exit', 'setlocal', 'endlocal',
14295
+ 'set', 'echo', 'rem', 'pause', 'cd', 'pushd', 'popd', 'shift', 'start'
14296
+ ];
14297
+ const batchBuiltins = [
14298
+ 'dir', 'copy', 'move', 'del', 'ren', 'md', 'rd', 'type', 'find', 'findstr',
14299
+ 'tasklist', 'taskkill', 'ping', 'ipconfig', 'netstat', 'cls', 'title', 'color'
14300
+ ];
14301
+ Tokenizer.registerLanguage({
14302
+ name: 'Batch',
14303
+ extensions: ['bat', 'cmd'],
14304
+ lineComment: 'rem',
14305
+ states: {
14306
+ root: [
14307
+ { match: /^rem\s+.*/i, type: 'comment' },
14308
+ { match: /^::.*/, type: 'comment' },
14309
+ { match: /"/, type: 'string', next: 'doubleString' },
14310
+ { match: /%[\w]+%/, type: 'type' },
14311
+ { match: /\b\d+\b/, type: 'number' },
14312
+ { match: words([...batchKeywords, ...batchKeywords.map(w => w.toUpperCase())]), type: 'keyword' },
14313
+ { match: words(batchBuiltins), type: 'builtin' },
14314
+ { match: /@echo/, type: 'statement' },
14315
+ { match: /[a-zA-Z_]\w*/, type: 'text' },
14316
+ { match: /[<>|&()@]/, type: 'symbol' },
14317
+ { match: /\s+/, type: 'text' },
14318
+ ],
14319
+ ...CommonStates
14320
+ },
14321
+ reservedWords: [...batchKeywords, ...batchBuiltins],
14322
+ icon: 'Terminal text-gray-300'
14323
+ });
14324
+ // CMake
14325
+ const cmakeCommands = [
14326
+ 'project', 'cmake_minimum_required', 'add_executable', 'add_library', 'target_link_libraries',
14327
+ 'target_include_directories', 'set', 'option', 'if', 'else', 'elseif', 'endif', 'foreach',
14328
+ 'endforeach', 'while', 'endwhile', 'function', 'endfunction', 'macro', 'endmacro',
14329
+ 'find_package', 'include', 'message', 'install', 'add_subdirectory', 'configure_file'
14330
+ ];
14331
+ Tokenizer.registerLanguage({
14332
+ name: 'CMake',
14333
+ extensions: ['cmake', 'txt', 'cmake-cache'],
14334
+ lineComment: '#',
14335
+ states: {
14336
+ root: [
14337
+ { match: /#.*/, type: 'comment' },
14338
+ { match: /"/, type: 'string', next: 'doubleString' },
14339
+ { match: /\$\{[^}]+\}/, type: 'type' },
14340
+ { match: /\b\d+\.?\d*\b/, type: 'number' },
14341
+ { match: words([...cmakeCommands, ...cmakeCommands.map(w => w.toUpperCase())]), type: 'keyword' },
14342
+ { match: /\b[A-Z_][A-Z0-9_]*\b/, type: 'builtin' },
14343
+ { match: /[a-zA-Z_]\w*/, type: 'text' },
14344
+ { match: /[(){}]/, type: 'symbol' },
14345
+ { match: /\s+/, type: 'text' },
14346
+ ],
14347
+ ...CommonStates
14348
+ },
14349
+ reservedWords: [...cmakeCommands],
14350
+ icon: 'AlignLeft text-neutral-500'
14351
+ });
14352
+ // Rust
14353
+ const rustKeywords = [
14354
+ 'as', 'break', 'const', 'continue', 'crate', 'else', 'enum', 'extern', 'false', 'fn',
14355
+ 'for', 'if', 'impl', 'in', 'let', 'loop', 'match', 'mod', 'move', 'mut', 'pub', 'ref',
14356
+ 'return', 'self', 'Self', 'static', 'struct', 'super', 'trait', 'true', 'type', 'unsafe',
14357
+ 'use', 'where', 'while', 'async', 'await', 'dyn', 'abstract', 'become', 'box', 'do',
14358
+ 'final', 'macro', 'override', 'priv', 'typeof', 'unsized', 'virtual', 'yield'
14359
+ ];
14360
+ const rustTypes = [
14361
+ 'i8', 'i16', 'i32', 'i64', 'i128', 'isize', 'u8', 'u16', 'u32', 'u64', 'u128', 'usize',
14362
+ 'f32', 'f64', 'bool', 'char', 'str', 'String', 'Vec', 'Option', 'Result', 'Box'
14363
+ ];
14364
+ const rustBuiltins = [
14365
+ 'println', 'print', 'eprintln', 'eprint', 'format', 'panic', 'assert', 'assert_eq',
14366
+ 'assert_ne', 'debug_assert', 'vec', 'Some', 'None', 'Ok', 'Err'
14367
+ ];
14368
+ Tokenizer.registerLanguage({
14369
+ name: 'Rust',
14370
+ extensions: ['rs'],
14371
+ lineComment: '//',
14372
+ states: {
14373
+ root: [
14374
+ { match: /\/\*/, type: 'comment', next: 'blockComment' },
14375
+ { match: /\/\/.*/, type: 'comment' },
14376
+ { match: /"/, type: 'string', next: 'doubleString' },
14377
+ { match: /'(?:\\.|[^'])+'/, type: 'string' },
14378
+ { match: /r#*"/, type: 'string', next: 'rawString' },
14379
+ ...NumberRules,
14380
+ { match: /#\[[\w:]+\]/, type: 'preprocessor' },
14381
+ { match: words(rustKeywords), type: 'keyword' },
14382
+ { match: words(rustTypes), type: 'type' },
14383
+ { match: words(rustBuiltins), type: 'builtin' },
14384
+ { match: /![a-zA-Z_]\w*/, type: 'preprocessor' },
14385
+ ...TailRules,
14386
+ ],
14387
+ rawString: [
14388
+ { match: /"#*/, type: 'string', pop: true },
14389
+ { match: /[^"]+/, type: 'string' },
14390
+ { match: /"/, type: 'string' },
14391
+ ],
14392
+ ...CommonStates
14393
+ },
14394
+ reservedWords: [...rustKeywords, ...rustTypes, ...rustBuiltins],
14395
+ icon: 'Rust text-orange-400'
14396
+ });
14397
+ // ____ _
14398
+ // | \ ___ ___ _ _ _____ ___ ___| |_
14399
+ // | | | . | _| | | | -_| | _|
14400
+ // |____/|___|___|___|_|_|_|___|_|_|_|
14401
+ class CodeDocument {
14402
+ onChange = undefined;
14403
+ _lines = [''];
14404
+ get lineCount() {
14405
+ return this._lines.length;
14406
+ }
14407
+ constructor(onChange) {
14408
+ this.onChange = onChange;
14409
+ }
14410
+ getLine(n) {
14411
+ return this._lines[n] ?? '';
14412
+ }
14413
+ getText(separator = '\n') {
14414
+ return this._lines.join(separator);
14415
+ }
14416
+ setText(text) {
14417
+ this._lines = text.split(/\r?\n/);
14418
+ if (this._lines.length === 0) {
14419
+ this._lines = [''];
14420
+ }
14421
+ if (this.onChange)
14422
+ this.onChange(this);
14423
+ }
14424
+ getCharAt(line, col) {
14425
+ const l = this._lines[line];
14426
+ if (!l || col < 0 || col >= l.length)
14427
+ return undefined;
14428
+ return l[col];
14429
+ }
14430
+ /**
14431
+ * Get the word at a given position. Returns [word, startCol, endCol].
14432
+ */
14433
+ getWordAt(line, col) {
14434
+ const l = this._lines[line];
14435
+ if (!l || col < 0 || col > l.length)
14436
+ return ['', col, col];
14437
+ // Expand left
14438
+ let start = col;
14439
+ while (start > 0 && isWord(l[start - 1])) {
14440
+ start--;
14441
+ }
14442
+ // Expand right
14443
+ let end = col;
14444
+ while (end < l.length && isWord(l[end])) {
14445
+ end++;
14446
+ }
14447
+ return [l.substring(start, end), start, end];
14448
+ }
14449
+ /**
14450
+ * Get the indentation level (number of leading spaces) of a line.
14451
+ */
14452
+ getIndent(line) {
14453
+ const idx = firstNonspaceIndex(this._lines[line] ?? '');
14454
+ return idx === -1 ? (this._lines[line]?.length ?? 0) : idx;
14455
+ }
14456
+ /**
14457
+ * Find the next occurrence of 'text' starting after (line, col). Returns null if no match.
14458
+ */
14459
+ findNext(text, startLine, startCol) {
14460
+ if (!text)
14461
+ return null;
14462
+ // Search from startLine:startCol forward
14463
+ for (let i = startLine; i < this._lines.length; i++) {
14464
+ const searchFrom = i === startLine ? startCol : 0;
14465
+ const idx = this._lines[i].indexOf(text, searchFrom);
14466
+ if (idx !== -1)
14467
+ return { line: i, col: idx };
14468
+ }
14469
+ // Wrap around from beginning
14470
+ for (let i = 0; i <= startLine; i++) {
14471
+ const searchUntil = i === startLine ? startCol : this._lines[i].length;
14472
+ const idx = this._lines[i].indexOf(text);
14473
+ if (idx !== -1 && idx < searchUntil)
14474
+ return { line: i, col: idx };
14475
+ }
14476
+ return null;
14477
+ }
14478
+ // Mutations (return EditOperations for undo):
14479
+ /**
14480
+ * Insert text at a position (handles newlines in the inserted text).
14481
+ */
14482
+ insert(line, col, text) {
14483
+ const parts = text.split(/\r?\n/);
14484
+ const currentLine = this._lines[line] ?? '';
14485
+ const before = currentLine.substring(0, col);
14486
+ const after = currentLine.substring(col);
14487
+ if (parts.length === 1) {
14488
+ this._lines[line] = before + parts[0] + after;
14489
+ }
14490
+ else {
14491
+ this._lines[line] = before + parts[0];
14492
+ for (let i = 1; i < parts.length - 1; i++) {
14493
+ this._lines.splice(line + i, 0, parts[i]);
14494
+ }
14495
+ this._lines.splice(line + parts.length - 1, 0, parts[parts.length - 1] + after);
14496
+ }
14497
+ if (this.onChange)
14498
+ this.onChange(this);
14499
+ return { type: 'insert', line, col, text };
14500
+ }
14501
+ /**
14502
+ * Delete `length` characters forward from a position (handles crossing line boundaries).
14503
+ */
14504
+ delete(line, col, length) {
14505
+ let remaining = length;
14506
+ let deletedText = '';
14507
+ let currentLine = line;
14508
+ let currentCol = col;
14509
+ while (remaining > 0 && currentLine < this._lines.length) {
14510
+ const l = this._lines[currentLine];
14511
+ const available = l.length - currentCol;
14512
+ if (remaining <= available) {
14513
+ deletedText += l.substring(currentCol, currentCol + remaining);
14514
+ this._lines[currentLine] = l.substring(0, currentCol) + l.substring(currentCol + remaining);
14515
+ remaining = 0;
14516
+ }
14517
+ else {
14518
+ // Delete rest of this line + the newline
14519
+ deletedText += l.substring(currentCol) + '\n';
14520
+ remaining -= (available + 1); // +1 for the newline
14521
+ // Merge next line into current
14522
+ const nextContent = this._lines[currentLine + 1] ?? '';
14523
+ this._lines[currentLine] = l.substring(0, currentCol) + nextContent;
14524
+ this._lines.splice(currentLine + 1, 1);
14525
+ }
14526
+ }
14527
+ if (this.onChange)
14528
+ this.onChange(this);
14529
+ return { type: 'delete', line, col, text: deletedText };
14530
+ }
14531
+ /**
14532
+ * Insert a new line after 'afterLine'. If afterLine is -1, inserts at the beginning.
14533
+ */
14534
+ insertLine(afterLine, text = '') {
14535
+ const insertAt = afterLine + 1;
14536
+ this._lines.splice(insertAt, 0, text);
14537
+ if (this.onChange)
14538
+ this.onChange(this);
14539
+ return { type: 'insert', line: Math.max(afterLine, 0), col: afterLine >= 0 ? this._lines[afterLine]?.length ?? 0 : 0, text: '\n' + text };
14540
+ }
14541
+ /**
14542
+ * Remove an entire line and its trailing newline.
14543
+ */
14544
+ removeLine(line) {
14545
+ const text = this._lines[line];
14546
+ this._lines.splice(line, 1);
14547
+ if (this._lines.length === 0) {
14548
+ this._lines = [''];
14549
+ }
14550
+ if (this.onChange)
14551
+ this.onChange(this);
14552
+ return { type: 'delete', line: Math.max(line - 1, 0), col: line > 0 ? (this._lines[line - 1]?.length ?? 0) : 0, text: '\n' + text };
14553
+ }
14554
+ replaceLine(line, newText) {
14555
+ const oldText = this._lines[line];
14556
+ this._lines[line] = newText;
14557
+ if (this.onChange)
14558
+ this.onChange(this);
14559
+ return { type: 'replaceLine', line, col: 0, text: newText, oldText };
14560
+ }
14561
+ /**
14562
+ * Apply an edit operation (used by undo/redo).
14563
+ */
14564
+ applyInverse(op) {
14565
+ if (op.type === 'insert') {
14566
+ return this.delete(op.line, op.col, op.text.length);
14567
+ }
14568
+ else if (op.type === 'replaceLine') {
14569
+ return this.replaceLine(op.line, op.oldText);
14570
+ }
14571
+ else {
14572
+ return this.insert(op.line, op.col, op.text);
14573
+ }
14574
+ }
14575
+ }
14576
+ class UndoManager {
14577
+ _undoStack = [];
14578
+ _redoStack = [];
14579
+ _pendingOps = [];
14580
+ _pendingCursorsBefore = [];
14581
+ _pendingCursorsAfter = [];
14582
+ _lastPushTime = 0;
14583
+ _groupThresholdMs;
14584
+ _maxSteps;
14585
+ constructor(groupThresholdMs = 2000, maxSteps = 200) {
14586
+ this._groupThresholdMs = groupThresholdMs;
14587
+ this._maxSteps = maxSteps;
14588
+ }
14589
+ /**
14590
+ * Record an edit operation. Consecutive operations within the time threshold
14591
+ * are grouped into a single undo step.
14592
+ */
14593
+ record(op, cursors) {
14594
+ const now = Date.now();
14595
+ const elapsed = now - this._lastPushTime;
14596
+ if (elapsed > this._groupThresholdMs && this._pendingOps.length > 0) {
14597
+ this._flush();
14598
+ }
14599
+ if (this._pendingOps.length === 0) {
14600
+ this._pendingCursorsBefore = cursors.map(c => ({ ...c }));
14601
+ }
14602
+ this._pendingOps.push(op);
14603
+ this._pendingCursorsAfter = cursors.map(c => ({ ...c }));
14604
+ this._lastPushTime = now;
14605
+ // New edits clear the redo stack
14606
+ this._redoStack.length = 0;
14607
+ }
14608
+ /**
14609
+ * Force-flush pending operations into a final undo step.
14610
+ */
14611
+ flush(cursorsAfter) {
14612
+ if (cursorsAfter && this._pendingOps.length > 0) {
14613
+ this._pendingCursorsAfter = cursorsAfter.map(c => ({ ...c }));
14614
+ }
14615
+ this._flush();
14616
+ }
14617
+ /**
14618
+ * Undo the last step. Stores the operations to apply inversely, and returns the cursor state to restore.
14619
+ */
14620
+ undo(doc, currentCursors) {
14621
+ this.flush(currentCursors);
14622
+ const entry = this._undoStack.pop();
14623
+ if (!entry)
14624
+ return null;
14625
+ // Apply in reverse order
14626
+ const redoOps = [];
14627
+ for (let i = entry.operations.length - 1; i >= 0; i--) {
14628
+ const inverseOp = doc.applyInverse(entry.operations[i]);
14629
+ redoOps.unshift(inverseOp);
14630
+ }
14631
+ this._redoStack.push({ operations: redoOps, cursorsBefore: entry.cursorsBefore, cursorsAfter: entry.cursorsAfter });
14632
+ return { cursors: entry.cursorsBefore };
14633
+ }
14634
+ /**
14635
+ * Redo the last undone step. Returns the cursor state to restore.
14636
+ */
14637
+ redo(doc) {
14638
+ const entry = this._redoStack.pop();
14639
+ if (!entry)
14640
+ return null;
14641
+ // Apply in forward order (redo entries are already in correct order)
14642
+ const undoOps = [];
14643
+ for (let i = 0; i < entry.operations.length; i++) {
14644
+ const inverseOp = doc.applyInverse(entry.operations[i]);
14645
+ undoOps.push(inverseOp);
14646
+ }
14647
+ this._undoStack.push({ operations: undoOps, cursorsBefore: entry.cursorsBefore, cursorsAfter: entry.cursorsAfter });
14648
+ return { cursors: entry.cursorsAfter };
14649
+ }
14650
+ canUndo() {
14651
+ return this._undoStack.length > 0 || this._pendingOps.length > 0;
14652
+ }
14653
+ canRedo() {
14654
+ return this._redoStack.length > 0;
14655
+ }
14656
+ clear() {
14657
+ this._undoStack.length = 0;
14658
+ this._redoStack.length = 0;
14659
+ this._pendingOps.length = 0;
14660
+ this._lastPushTime = 0;
14661
+ }
14662
+ _flush() {
14663
+ if (this._pendingOps.length === 0)
14664
+ return;
14665
+ this._undoStack.push({
14666
+ operations: [...this._pendingOps],
14667
+ cursorsBefore: [...this._pendingCursorsBefore],
14668
+ cursorsAfter: [...this._pendingCursorsAfter]
14669
+ });
14670
+ this._pendingOps.length = 0;
14671
+ this._pendingCursorsBefore = [];
14672
+ this._pendingCursorsAfter = [];
14673
+ // Limit stack size
14674
+ while (this._undoStack.length > this._maxSteps) {
14675
+ this._undoStack.shift();
14676
+ }
14677
+ }
14678
+ }
14679
+ function posEqual(a, b) {
14680
+ return a.line === b.line && a.col === b.col;
14681
+ }
14682
+ function posBefore(a, b) {
14683
+ return a.line < b.line || (a.line === b.line && a.col < b.col);
14684
+ }
14685
+ function selectionStart(sel) {
14686
+ return posBefore(sel.anchor, sel.head) ? sel.anchor : sel.head;
14687
+ }
14688
+ function selectionEnd(sel) {
14689
+ return posBefore(sel.anchor, sel.head) ? sel.head : sel.anchor;
14690
+ }
14691
+ function selectionIsEmpty(sel) {
14692
+ return posEqual(sel.anchor, sel.head);
14693
+ }
14694
+ class CursorSet {
14695
+ cursors = [];
14696
+ constructor() {
14697
+ // Start with one cursor at 0,0
14698
+ this.cursors = [{ anchor: { line: 0, col: 0 }, head: { line: 0, col: 0 } }];
14699
+ }
14700
+ getPrimary() {
14701
+ return this.cursors[0];
14702
+ }
14703
+ /**
14704
+ * Set a single cursor position. Clears all secondary cursors and selections.
14705
+ */
14706
+ set(line, col) {
14707
+ this.cursors = [{ anchor: { line, col }, head: { line, col } }];
14708
+ }
14709
+ // Movement:
14710
+ moveLeft(doc, selecting = false) {
14711
+ for (const sel of this.cursors) {
14712
+ this._moveHead(sel, doc, -1, 0, selecting);
14713
+ }
14714
+ this._merge();
14715
+ }
14716
+ moveRight(doc, selecting = false) {
14717
+ for (const sel of this.cursors) {
14718
+ this._moveHead(sel, doc, 1, 0, selecting);
14719
+ }
14720
+ this._merge();
14721
+ }
14722
+ moveUp(doc, selecting = false) {
14723
+ for (const sel of this.cursors) {
14724
+ this._moveVertical(sel, doc, -1, selecting);
14725
+ }
14726
+ this._merge();
14727
+ }
14728
+ moveDown(doc, selecting = false) {
14729
+ for (const sel of this.cursors) {
14730
+ this._moveVertical(sel, doc, 1, selecting);
14731
+ }
14732
+ this._merge();
14733
+ }
14734
+ moveToLineStart(doc, selecting = false, noIndent = false) {
14735
+ for (const sel of this.cursors) {
14736
+ const line = sel.head.line;
14737
+ const indent = doc.getIndent(line);
14738
+ // Toggle between indent and column 0
14739
+ const targetCol = (sel.head.col === indent || noIndent) ? 0 : indent;
14740
+ sel.head = { line, col: targetCol };
14741
+ if (!selecting)
14742
+ sel.anchor = { ...sel.head };
14743
+ }
14744
+ this._merge();
14745
+ }
14746
+ moveToLineEnd(doc, selecting = false) {
14747
+ for (const sel of this.cursors) {
14748
+ const line = sel.head.line;
14749
+ sel.head = { line, col: doc.getLine(line).length };
14750
+ if (!selecting)
14751
+ sel.anchor = { ...sel.head };
14752
+ }
14753
+ this._merge();
14754
+ }
14755
+ moveWordLeft(doc, selecting = false) {
14756
+ for (const sel of this.cursors) {
14757
+ const { line, col } = sel.head;
14758
+ if (col === 0 && line > 0) {
14759
+ // Jump to end of previous line
14760
+ sel.head = { line: line - 1, col: doc.getLine(line - 1).length };
14761
+ }
14762
+ else {
14763
+ const l = doc.getLine(line);
14764
+ let c = col;
14765
+ // Skip whitespace
14766
+ while (c > 0 && /\s/.test(l[c - 1]))
14767
+ c--;
14768
+ // Skip word or symbols
14769
+ if (c > 0 && isWord(l[c - 1])) {
14770
+ while (c > 0 && isWord(l[c - 1]))
14771
+ c--;
14772
+ }
14773
+ else if (c > 0) {
14774
+ while (c > 0 && isSymbol(l[c - 1]))
14775
+ c--;
14776
+ }
14777
+ sel.head = { line, col: c };
14778
+ }
14779
+ if (!selecting)
14780
+ sel.anchor = { ...sel.head };
14781
+ }
14782
+ this._merge();
14783
+ }
14784
+ moveWordRight(doc, selecting = false) {
14785
+ for (const sel of this.cursors) {
14786
+ const { line, col } = sel.head;
14787
+ const l = doc.getLine(line);
14788
+ if (col >= l.length && line < doc.lineCount - 1) {
14789
+ // Jump to start of next line
14790
+ sel.head = { line: line + 1, col: 0 };
14791
+ }
14792
+ else {
14793
+ let c = col;
14794
+ // Skip leading whitespace first
14795
+ while (c < l.length && /\s/.test(l[c]))
14796
+ c++;
14797
+ // Then skip word or symbols to end of group
14798
+ if (c < l.length && isWord(l[c])) {
14799
+ while (c < l.length && isWord(l[c]))
14800
+ c++;
14801
+ }
14802
+ else if (c < l.length && isSymbol(l[c])) {
14803
+ while (c < l.length && isSymbol(l[c]))
14804
+ c++;
14805
+ }
14806
+ sel.head = { line, col: c };
14807
+ }
14808
+ if (!selecting)
14809
+ sel.anchor = { ...sel.head };
14810
+ }
14811
+ this._merge();
14812
+ }
14813
+ selectAll(doc) {
14814
+ const lastLine = doc.lineCount - 1;
14815
+ this.cursors = [{
14816
+ anchor: { line: 0, col: 0 },
14817
+ head: { line: lastLine, col: doc.getLine(lastLine).length }
14818
+ }];
14819
+ }
14820
+ // Multi-cursor:
14821
+ addCursor(line, col) {
14822
+ this.cursors.push({ anchor: { line, col }, head: { line, col } });
14823
+ this._merge();
14824
+ }
14825
+ removeSecondaryCursors() {
14826
+ this.cursors = [this.cursors[0]];
14827
+ }
14828
+ /**
14829
+ * Returns cursor indices sorted by head position, bottom-to-top (last in doc first)
14830
+ * to ensure earlier cursors' positions stay valid.
14831
+ */
14832
+ sortedIndicesBottomUp() {
14833
+ return this.cursors
14834
+ .map((_, i) => i)
14835
+ .sort((a, b) => {
14836
+ const ah = this.cursors[a].head;
14837
+ const bh = this.cursors[b].head;
14838
+ return bh.line !== ah.line ? bh.line - ah.line : bh.col - ah.col;
14839
+ });
14840
+ }
14841
+ /**
14842
+ * After editing at (line, col), shift all other cursors on the same line
14843
+ * that are at or after `afterCol` by `colDelta`. Also handles line shifts
14844
+ * for multi-line inserts/deletes via `lineDelta`.
14845
+ */
14846
+ adjustOthers(skipIdx, line, afterCol, colDelta, lineDelta = 0) {
14847
+ for (let i = 0; i < this.cursors.length; i++) {
14848
+ if (i === skipIdx)
14849
+ continue;
14850
+ const c = this.cursors[i];
14851
+ // Adjust head
14852
+ if (lineDelta !== 0 && c.head.line > line) {
14853
+ c.head = { line: c.head.line + lineDelta, col: c.head.col };
14854
+ }
14855
+ else if (c.head.line === line && c.head.col >= afterCol) {
14856
+ c.head = { line: c.head.line + lineDelta, col: c.head.col + colDelta };
14857
+ }
14858
+ // Adjust anchor
14859
+ if (lineDelta !== 0 && c.anchor.line > line) {
14860
+ c.anchor = { line: c.anchor.line + lineDelta, col: c.anchor.col };
14861
+ }
14862
+ else if (c.anchor.line === line && c.anchor.col >= afterCol) {
14863
+ c.anchor = { line: c.anchor.line + lineDelta, col: c.anchor.col + colDelta };
14864
+ }
14865
+ }
14866
+ }
14867
+ // Queries:
14868
+ hasSelection(index = 0) {
14869
+ const sel = this.cursors[index];
14870
+ return sel ? !posEqual(sel.anchor, sel.head) : false;
14871
+ }
14872
+ getSelectedText(doc, index = 0) {
14873
+ const sel = this.cursors[index];
14874
+ if (!sel || selectionIsEmpty(sel))
14875
+ return '';
14876
+ const start = selectionStart(sel);
14877
+ const end = selectionEnd(sel);
14878
+ if (start.line === end.line) {
14879
+ return doc.getLine(start.line).substring(start.col, end.col);
14880
+ }
14881
+ const lines = [];
14882
+ lines.push(doc.getLine(start.line).substring(start.col));
14883
+ for (let i = start.line + 1; i < end.line; i++) {
14884
+ lines.push(doc.getLine(i));
14885
+ }
14886
+ lines.push(doc.getLine(end.line).substring(0, end.col));
14887
+ return lines.join('\n');
14888
+ }
14889
+ getCursorPositions() {
14890
+ return this.cursors.map(s => ({ ...s.head }));
14891
+ }
14892
+ _moveHead(sel, doc, dx, _dy, selecting) {
14893
+ const { line, col } = sel.head;
14894
+ // If there's a selection and we're not extending it, collapse to the edge
14895
+ if (!selecting && !selectionIsEmpty(sel)) {
14896
+ const target = dx < 0 ? selectionStart(sel) : selectionEnd(sel);
14897
+ sel.head = { ...target };
14898
+ sel.anchor = { ...target };
14899
+ return;
14900
+ }
14901
+ if (dx < 0) {
14902
+ if (col > 0) {
14903
+ sel.head = { line, col: col - 1 };
14904
+ }
14905
+ else if (line > 0) {
14906
+ sel.head = { line: line - 1, col: doc.getLine(line - 1).length };
14907
+ }
14908
+ }
14909
+ else if (dx > 0) {
14910
+ const lineLen = doc.getLine(line).length;
14911
+ if (col < lineLen) {
14912
+ sel.head = { line, col: col + 1 };
14913
+ }
14914
+ else if (line < doc.lineCount - 1) {
14915
+ sel.head = { line: line + 1, col: 0 };
14916
+ }
14917
+ }
14918
+ if (!selecting)
14919
+ sel.anchor = { ...sel.head };
14920
+ }
14921
+ _moveVertical(sel, doc, direction, selecting) {
14922
+ const { line, col } = sel.head;
14923
+ const newLine = line + direction;
14924
+ if (newLine < 0 || newLine >= doc.lineCount)
14925
+ return;
14926
+ const newLineLen = doc.getLine(newLine).length;
14927
+ sel.head = { line: newLine, col: Math.min(col, newLineLen) };
14928
+ if (!selecting)
14929
+ sel.anchor = { ...sel.head };
14930
+ }
14931
+ /**
14932
+ * Merge overlapping cursors/selections. Keeps the first one when duplicates exist.
14933
+ */
14934
+ _merge() {
14935
+ if (this.cursors.length <= 1)
14936
+ return;
14937
+ // Sort by head position
14938
+ this.cursors.sort((a, b) => {
14939
+ if (a.head.line !== b.head.line)
14940
+ return a.head.line - b.head.line;
14941
+ return a.head.col - b.head.col;
14942
+ });
14943
+ const merged = [this.cursors[0]];
14944
+ for (let i = 1; i < this.cursors.length; i++) {
14945
+ const prev = merged[merged.length - 1];
14946
+ const curr = this.cursors[i];
14947
+ if (posEqual(prev.head, curr.head)) {
14948
+ // Same position — skip duplicate
14949
+ continue;
14950
+ }
14951
+ merged.push(curr);
14952
+ }
14953
+ this.cursors = merged;
14954
+ }
14955
+ }
14956
+ /**
14957
+ * Manages code symbols for autocomplete, navigation, and outlining.
14958
+ * Incrementally updates as lines change.
14959
+ */
14960
+ class SymbolTable {
14961
+ _symbols = new Map(); // name -> symbols[]
14962
+ _lineSymbols = []; // [lineNum] -> symbols declared on that line
14963
+ _scopeStack = [{ name: 'global', type: 'global', line: 0 }];
14964
+ _lineScopes = []; // [lineNum] -> scope stack at that line
14965
+ get currentScope() {
14966
+ return this._scopeStack[this._scopeStack.length - 1]?.name ?? 'global';
14967
+ }
14968
+ get currentScopeType() {
14969
+ return this._scopeStack[this._scopeStack.length - 1]?.type ?? 'global';
14970
+ }
14971
+ getScopeAtLine(line) {
14972
+ return this._lineScopes[line] ?? [{ name: 'global', type: 'global', line: 0 }];
14973
+ }
14974
+ getSymbols(name) {
14975
+ return this._symbols.get(name) ?? [];
14976
+ }
14977
+ getAllSymbolNames() {
14978
+ return Array.from(this._symbols.keys());
14979
+ }
14980
+ getAllSymbols() {
14981
+ const all = [];
14982
+ for (const symbols of this._symbols.values()) {
14983
+ all.push(...symbols);
14984
+ }
14985
+ return all;
14986
+ }
14987
+ getLineSymbols(line) {
14988
+ return this._lineSymbols[line] ?? [];
14989
+ }
14990
+ /** Update scope stack for a line (call before parsing symbols) */
14991
+ updateScopeForLine(line, lineText) {
14992
+ // Track braces to maintain scope stack
14993
+ const openBraces = (lineText.match(/\{/g) || []).length;
14994
+ const closeBraces = (lineText.match(/\}/g) || []).length;
14995
+ // Save current scope for this line
14996
+ this._lineScopes[line] = [...this._scopeStack];
14997
+ // Pop scopes for closing braces
14998
+ for (let i = 0; i < closeBraces; i++) {
14999
+ if (this._scopeStack.length > 1)
15000
+ this._scopeStack.pop();
15001
+ }
15002
+ // Push scopes for opening braces (will be named by symbol detection)
15003
+ for (let i = 0; i < openBraces; i++) {
15004
+ this._scopeStack.push({ name: 'anonymous', type: 'anonymous', line });
15005
+ }
15006
+ }
15007
+ /** Name the most recent anonymous scope (called when detecting class/function) */
15008
+ nameCurrentScope(name, type) {
15009
+ if (this._scopeStack.length > 0) {
15010
+ const current = this._scopeStack[this._scopeStack.length - 1];
15011
+ if (current.name === 'anonymous' || current.type === 'anonymous') {
15012
+ current.name = name;
15013
+ current.type = type;
15014
+ }
15015
+ }
15016
+ }
15017
+ /** Remove symbols from a line (before reparsing) */
15018
+ removeLineSymbols(line) {
15019
+ const oldSymbols = this._lineSymbols[line];
15020
+ if (!oldSymbols)
15021
+ return;
15022
+ for (const symbol of oldSymbols) {
15023
+ const list = this._symbols.get(symbol.name);
15024
+ if (!list)
15025
+ continue;
15026
+ // Remove this specific symbol from the name's list
15027
+ const index = list.findIndex(s => s.line === line && s.kind === symbol.kind && s.scope === symbol.scope);
15028
+ if (index !== -1)
15029
+ list.splice(index, 1);
15030
+ if (list.length === 0)
15031
+ this._symbols.delete(symbol.name);
15032
+ }
15033
+ this._lineSymbols[line] = [];
15034
+ }
15035
+ addSymbol(symbol) {
15036
+ if (!this._symbols.has(symbol.name)) {
15037
+ this._symbols.set(symbol.name, []);
15038
+ }
15039
+ this._symbols.get(symbol.name).push(symbol);
15040
+ if (!this._lineSymbols[symbol.line]) {
15041
+ this._lineSymbols[symbol.line] = [];
15042
+ }
15043
+ this._lineSymbols[symbol.line].push(symbol);
15044
+ }
15045
+ /** Reset scope stack (e.g. when document structure changes significantly) */
15046
+ resetScopes() {
15047
+ this._scopeStack = [{ name: 'global', type: 'global', line: 0 }];
15048
+ this._lineScopes = [];
15049
+ }
15050
+ clear() {
15051
+ this._symbols.clear();
15052
+ this._lineSymbols = [];
15053
+ this.resetScopes();
15054
+ }
15055
+ }
15056
+ /**
15057
+ * Parse symbols from a line of code using token types and text patterns to detect declarations.
15058
+ */
15059
+ function parseSymbolsFromLine(lineText, tokens, line, symbolTable) {
15060
+ const symbols = [];
15061
+ const scope = symbolTable.currentScope;
15062
+ const scopeType = symbolTable.currentScopeType;
15063
+ // Build set of reserved words from tokens (keywords, statements, builtins) to skip when detecting symbols
15064
+ const reservedWords = new Set();
15065
+ for (const token of tokens) {
15066
+ if (['keyword', 'statement', 'builtin', 'preprocessor'].includes(token.type)) {
15067
+ reservedWords.add(token.value);
15068
+ }
15069
+ }
15070
+ // Track added symbols by name and approximate position to avoid duplicates
15071
+ const addedSymbols = new Set();
15072
+ const addSymbol = (name, kind, col = 0) => {
15073
+ if (!name || !name.match(/^[a-zA-Z_$][\w$]*$/))
15074
+ return; // Valid identifier check
15075
+ if (reservedWords.has(name))
15076
+ return;
15077
+ // Unique key using 5 chars tolerance
15078
+ const posKey = `${name}@${Math.floor(col / 5) * 5}`;
15079
+ if (addedSymbols.has(posKey))
15080
+ return; // Already added
15081
+ symbols.push({ name, kind, scope, line, col });
15082
+ addedSymbols.add(posKey);
15083
+ };
15084
+ // Top-level declaration patterns (only one per line, checked with anchors)
15085
+ const topLevelPatterns = [
15086
+ { regex: /^\s*class\s+([A-Z_]\w*)/i, kind: 'class' },
15087
+ { regex: /^\s*struct\s+([A-Z_]\w*)/i, kind: 'struct' },
15088
+ { regex: /^\s*interface\s+([A-Z_]\w*)/i, kind: 'interface' },
15089
+ { regex: /^\s*enum\s+([A-Z_]\w*)/i, kind: 'enum' },
15090
+ { regex: /^\s*type\s+([A-Z_]\w*)\s*=/i, kind: 'type' },
15091
+ { regex: /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)/i, kind: 'function' },
15092
+ { regex: /^\s*(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>/i, kind: 'function' },
15093
+ { regex: /^\s*(?:static\s+|const\s+|virtual\s+|inline\s+|extern\s+|pub\s+|async\s+)*(\w+[\w\s\*&:<>,]*?)\s+(\w+)\s*\([^)]*\)\s*[{;]/i, kind: 'typed-function' },
15094
+ { regex: /^\s*(?:public|private|protected|static|readonly)*\s*(\w+)\s*\([^)]*\)\s*[:{]/i, kind: scopeType === 'class' ? 'method' : 'function' }
15095
+ ];
15096
+ const multiPatterns = [
15097
+ { regex: /(?:const|let|var)\s+(\w+)/gi, kind: 'variable' },
15098
+ { regex: /(\w+)\s*:\s*(?:function|[A-Z]\w*)/gi, kind: 'variable' },
15099
+ { regex: /this\.(\w+)\s*=/gi, kind: 'property' },
15100
+ { regex: /new\s+([A-Z]\w+)/gi, kind: 'constructor-call' },
15101
+ { regex: /(\w+)\s*\(/gi, kind: 'method-call' }
15102
+ ];
15103
+ let foundTopLevel = false;
15104
+ for (const pattern of topLevelPatterns) {
15105
+ const match = lineText.match(pattern.regex);
15106
+ if (match) {
15107
+ let name;
15108
+ let kind = pattern.kind;
15109
+ if (pattern.kind === 'typed-function' && match[2]) {
15110
+ name = match[2];
15111
+ kind = scopeType === 'class' ? 'method' : 'function';
15112
+ }
15113
+ else if (match[1]) {
15114
+ name = match[1];
15115
+ }
15116
+ else {
15117
+ continue;
15118
+ }
15119
+ const col = lineText.indexOf(name);
15120
+ addSymbol(name, kind, col);
15121
+ if (['class', 'function', 'method', 'enum', 'struct', 'interface'].includes(kind)) {
15122
+ symbolTable.nameCurrentScope(name, kind);
15123
+ }
15124
+ foundTopLevel = true;
15125
+ break;
15126
+ }
15127
+ }
15128
+ // If no top-level declaration, check for multiple occurrences
15129
+ if (!foundTopLevel) {
15130
+ for (const pattern of multiPatterns) {
15131
+ const matches = lineText.matchAll(pattern.regex);
15132
+ for (const match of matches) {
15133
+ if (match[1]) {
15134
+ const name = match[1];
15135
+ const col = match.index ?? 0;
15136
+ addSymbol(name, pattern.kind, col);
15137
+ }
15138
+ }
15139
+ }
15140
+ }
15141
+ if (scopeType === 'enum') {
15142
+ const enumValueMatch = lineText.match(/^\s*(\w+)\s*[,=]?/);
15143
+ if (enumValueMatch && enumValueMatch[1] && !lineText.includes('enum')) {
15144
+ addSymbol(enumValueMatch[1], 'enum-value', lineText.indexOf(enumValueMatch[1]));
15145
+ }
15146
+ }
15147
+ return symbols;
15148
+ }
15149
+ // _____ _ _____ _ _ _ ____ _____ _____
15150
+ // | |___ _| |___| __|_| |_| |_ ___ ___ | \| | |
15151
+ // | --| . | . | -_| __| . | | _| . | _| | | | | | | | |
15152
+ // |_____|___|___|___|_____|___|_|_| |___|_| |____/|_____|_|_|_|
15153
+ const g = globalThis;
15154
+ const TOKEN_CLASS_MAP = {
15155
+ 'keyword': 'cm-kwd',
15156
+ 'statement': 'cm-std',
15157
+ 'type': 'cm-typ',
15158
+ 'builtin': 'cm-bln',
15159
+ 'string': 'cm-str',
15160
+ 'comment': 'cm-com',
15161
+ 'number': 'cm-dec',
15162
+ 'method': 'cm-mtd',
15163
+ 'symbol': 'cm-sym',
15164
+ 'enum': 'cm-enu',
15165
+ 'preprocessor': 'cm-ppc',
15166
+ 'variable': 'cm-var',
15167
+ };
15168
+ exports.LX.Area;
15169
+ exports.LX.Panel;
15170
+ exports.LX.Tabs;
15171
+ exports.LX.NodeTree;
15172
+ // _____ _ _ _____
15173
+ // | __|___ ___ ___| | | __ |___ ___
15174
+ // |__ | _| _| . | | | __ -| .'| _|
15175
+ // |_____|___|_| |___|_|_|_____|__,|_|
15176
+ class ScrollBar {
15177
+ static SIZE = 10;
15178
+ root;
15179
+ thumb;
15180
+ _vertical;
15181
+ _thumbPos = 0; // current thumb offset in px
15182
+ _thumbRatio = 0; // thumb size as fraction of track (0..1)
15183
+ _lastMouse = 0;
15184
+ _onDrag = null;
15185
+ get visible() { return !this.root.classList.contains('hidden'); }
15186
+ get isVertical() { return this._vertical; }
15187
+ constructor(vertical, onDrag) {
15188
+ this._vertical = vertical;
15189
+ this._onDrag = onDrag;
15190
+ this.root = exports.LX.makeElement('div', `lexcodescrollbar hidden ${vertical ? 'vertical' : 'horizontal'}`);
15191
+ this.thumb = exports.LX.makeElement('div');
15192
+ this.thumb.addEventListener('mousedown', (e) => this._onMouseDown(e));
15193
+ this.root.appendChild(this.thumb);
15194
+ }
15195
+ setThumbRatio(ratio) {
15196
+ this._thumbRatio = exports.LX.clamp(ratio, 0, 1);
15197
+ const needed = this._thumbRatio < 1;
15198
+ this.root.classList.toggle('hidden', !needed);
15199
+ if (needed) {
15200
+ if (this._vertical)
15201
+ this.thumb.style.height = (this._thumbRatio * 100) + '%';
15202
+ else
15203
+ this.thumb.style.width = (this._thumbRatio * 100) + '%';
15204
+ }
15205
+ }
15206
+ syncToScroll(scrollPos, scrollMax) {
15207
+ if (scrollMax <= 0)
15208
+ return;
15209
+ const trackSize = this._vertical ? this.root.offsetHeight : this.root.offsetWidth;
15210
+ const thumbSize = this._vertical ? this.thumb.offsetHeight : this.thumb.offsetWidth;
15211
+ const available = trackSize - thumbSize;
15212
+ if (available <= 0)
15213
+ return;
15214
+ this._thumbPos = (scrollPos / scrollMax) * available;
15215
+ this._applyPosition();
15216
+ }
15217
+ applyDragDelta(delta, scrollMax) {
15218
+ const trackSize = this._vertical ? this.root.offsetHeight : this.root.offsetWidth;
15219
+ const thumbSize = this._vertical ? this.thumb.offsetHeight : this.thumb.offsetWidth;
15220
+ const available = trackSize - thumbSize;
15221
+ if (available <= 0)
15222
+ return 0;
15223
+ this._thumbPos = exports.LX.clamp(this._thumbPos + delta, 0, available);
15224
+ this._applyPosition();
15225
+ return (this._thumbPos / available) * scrollMax;
15226
+ }
15227
+ _applyPosition() {
15228
+ if (this._vertical)
15229
+ this.thumb.style.top = this._thumbPos + 'px';
15230
+ else
15231
+ this.thumb.style.left = this._thumbPos + 'px';
15232
+ }
15233
+ _onMouseDown(e) {
15234
+ const doc = document;
15235
+ this._lastMouse = this._vertical ? e.clientY : e.clientX;
15236
+ const onMouseMove = (e) => {
15237
+ const current = this._vertical ? e.clientY : e.clientX;
15238
+ const delta = current - this._lastMouse;
15239
+ this._lastMouse = current;
15240
+ this._onDrag?.(delta);
15241
+ e.stopPropagation();
15242
+ e.preventDefault();
15243
+ };
15244
+ const onMouseUp = () => {
15245
+ doc.removeEventListener('mousemove', onMouseMove);
15246
+ doc.removeEventListener('mouseup', onMouseUp);
15247
+ };
15248
+ doc.addEventListener('mousemove', onMouseMove);
15249
+ doc.addEventListener('mouseup', onMouseUp);
15250
+ e.stopPropagation();
15251
+ e.preventDefault();
15252
+ }
15253
+ }
15254
+ /**
15255
+ * @class CodeEditor
15256
+ * The main editor class. Wires Document, Tokenizer, CursorSet, UndoManager
15257
+ * together with the DOM.
15258
+ */
15259
+ class CodeEditor {
15260
+ static __instances = [];
15261
+ language;
15262
+ symbolTable;
15263
+ // DOM:
15264
+ area;
15265
+ baseArea;
15266
+ codeArea;
15267
+ explorerArea;
15268
+ tabs;
15269
+ root;
15270
+ codeScroller;
15271
+ codeSizer;
15272
+ cursorsLayer;
15273
+ selectionsLayer;
15274
+ lineGutter;
15275
+ vScrollbar;
15276
+ hScrollbar;
15277
+ searchBox = null;
15278
+ searchLineBox = null;
15279
+ autocomplete = null;
15280
+ currentTab = null;
15281
+ statusPanel;
15282
+ leftStatusPanel;
15283
+ rightStatusPanel;
15284
+ explorer = null;
15285
+ // Measurements:
15286
+ charWidth = 0;
15287
+ lineHeight = 0;
15288
+ fontSize = 0;
15289
+ xPadding = 64; // 4rem left padding in pixels
15290
+ _cachedTabsHeight = 0;
15291
+ _cachedStatusPanelHeight = 0;
15292
+ // Editor options:
15293
+ skipInfo = false;
15294
+ disableEdition = false;
15295
+ skipTabs = false;
15296
+ useFileExplorer = false;
15297
+ useAutoComplete = true;
15298
+ allowAddScripts = true;
15299
+ allowClosingTabs = true;
15300
+ allowLoadingFiles = true;
15301
+ tabSize = 4;
15302
+ highlight = 'Plain Text';
15303
+ newTabOptions = null;
15304
+ customSuggestions = [];
15305
+ explorerName = 'EXPLORER';
15306
+ // Editor callbacks:
15307
+ onSave;
15308
+ onRun;
15309
+ onCtrlSpace;
15310
+ onCreateStatusPanel;
15311
+ onContextMenu;
15312
+ onNewTab;
15313
+ onSelectTab;
15314
+ onReady;
15315
+ onCreateFile;
15316
+ onCodeChange;
15317
+ _inputArea;
15318
+ // State:
15319
+ _lineStates = []; // tokenizer state at end of each line
15320
+ _lineElements = []; // <pre> element per line
15321
+ _openedTabs = {};
15322
+ _loadedTabs = {};
15323
+ _storedTabs = {};
15324
+ _focused = false;
15325
+ _composing = false;
15326
+ _keyChain = null;
15327
+ _wasPaired = false;
15328
+ _lastAction = '';
15329
+ _blinkerInterval = null;
15330
+ _cursorVisible = true;
15331
+ _cursorBlinkRate = 550;
15332
+ _clickCount = 0;
15333
+ _lastClickTime = 0;
15334
+ _lastClickLine = -1;
15335
+ _isSearchBoxActive = false;
15336
+ _isSearchLineBoxActive = false;
15337
+ _searchMatchCase = false;
15338
+ _lastTextFound = '';
15339
+ _lastSearchPos = null;
15340
+ _discardScroll = false;
15341
+ _isReady = false;
15342
+ _lastMaxLineLength = 0;
15343
+ _lastLineCount = 0;
15344
+ _isAutoCompleteActive = false;
15345
+ _selectedAutocompleteIndex = 0;
15346
+ _displayObservers;
15347
+ static CODE_MIN_FONT_SIZE = 9;
15348
+ static CODE_MAX_FONT_SIZE = 22;
15349
+ static PAIR_KEYS = { '"': '"', "'": "'", '`': '`', '(': ')', '{': '}', '[': ']' };
15350
+ get doc() {
15351
+ return this.currentTab.doc;
15352
+ }
15353
+ get undoManager() {
15354
+ return this.currentTab.undoManager;
15355
+ }
15356
+ get cursorSet() {
15357
+ return this.currentTab.cursorSet;
15358
+ }
15359
+ get codeContainer() {
15360
+ return this.currentTab.dom;
15361
+ }
15362
+ static getInstances() {
15363
+ return CodeEditor.__instances;
15364
+ }
15365
+ constructor(area, options = {}) {
15366
+ g.editor = this;
15367
+ CodeEditor.__instances.push(this);
15368
+ this.skipInfo = options.skipInfo ?? this.skipInfo;
15369
+ this.disableEdition = options.disableEdition ?? this.disableEdition;
15370
+ this.skipTabs = options.skipTabs ?? this.skipTabs;
15371
+ this.useFileExplorer = (options.fileExplorer ?? this.useFileExplorer) && !this.skipTabs;
15372
+ this.useAutoComplete = options.autocomplete ?? this.useAutoComplete;
15373
+ this.allowAddScripts = options.allowAddScripts ?? this.allowAddScripts;
15374
+ this.allowClosingTabs = options.allowClosingTabs ?? this.allowClosingTabs;
15375
+ this.allowLoadingFiles = options.allowLoadingFiles ?? this.allowLoadingFiles;
15376
+ this.highlight = options.highlight ?? this.highlight;
15377
+ this.newTabOptions = options.newTabOptions;
15378
+ this.customSuggestions = options.customSuggestions ?? [];
15379
+ this.explorerName = options.explorerName ?? this.explorerName;
15380
+ // Editor callbacks
15381
+ this.onSave = options.onSave;
15382
+ this.onRun = options.onRun;
15383
+ this.onCtrlSpace = options.onCtrlSpace;
15384
+ this.onCreateStatusPanel = options.onCreateStatusPanel;
15385
+ this.onContextMenu = options.onContextMenu;
15386
+ this.onNewTab = options.onNewTab;
15387
+ this.onSelectTab = options.onSelectTab;
15388
+ this.onReady = options.onReady;
15389
+ this.onCodeChange = options.onCodeChange;
15390
+ this.language = Tokenizer.getLanguage(this.highlight) ?? Tokenizer.getLanguage('Plain Text');
15391
+ this.symbolTable = new SymbolTable();
15392
+ // File explorer
15393
+ if (this.useFileExplorer) {
15394
+ let [explorerArea, editorArea] = area.split({ sizes: ['15%', '85%'] });
15395
+ // explorerArea.setLimitBox( 180, 20, 512 );
15396
+ this.explorerArea = explorerArea;
15397
+ let panel = new exports.LX.Panel();
15398
+ panel.addTitle(this.explorerName);
15399
+ let sceneData = [];
15400
+ this.explorer = panel.addTree(null, sceneData, {
15401
+ filter: false,
15402
+ rename: false,
15403
+ skipDefaultIcon: true
15404
+ });
15405
+ this.explorer.on('dblClick', (event) => {
15406
+ const node = event.items[0];
15407
+ const name = node.id;
15408
+ this.loadTab(name);
15409
+ });
15410
+ this.explorer.on('beforeDelete', (event, resolve) => {
15411
+ return;
15412
+ });
15413
+ explorerArea.attach(panel);
15414
+ // Update area
15415
+ area = editorArea;
15416
+ }
15417
+ // Full editor
15418
+ area.root.className = exports.LX.mergeClass(area.root.className, 'codebasearea overflow-hidden flex relative');
15419
+ this.baseArea = area;
15420
+ this.area = new exports.LX.Area({
15421
+ className: 'lexcodeeditor flex flex-col outline-none overflow-hidden size-full select-none bg-inherit relative',
15422
+ skipAppend: true
15423
+ });
15424
+ const codeAreaClassName = 'lexcodearea scrollbar-hidden flex flex-row flex-auto-fill';
15425
+ if (!this.skipTabs) {
15426
+ this.tabs = this.area.addTabs({ contentClass: codeAreaClassName, onclose: (name) => {
15427
+ // this.closeTab triggers this onclose!
15428
+ delete this._openedTabs[name];
15429
+ } });
15430
+ exports.LX.addClass(this.tabs.root.parentElement, 'rounded-t-lg');
15431
+ if (!this.disableEdition) {
15432
+ this.tabs.root.parentElement.addEventListener('dblclick', (e) => {
15433
+ if (!this.allowAddScripts)
15434
+ return;
15435
+ e.preventDefault();
15436
+ this._onCreateNewFile();
15437
+ });
15438
+ }
15439
+ this.codeArea = this.tabs.area;
15440
+ }
15441
+ else {
15442
+ this.codeArea = new exports.LX.Area({ className: codeAreaClassName, skipAppend: true });
15443
+ this.area.attach(this.codeArea);
15444
+ }
15445
+ this.root = this.area.root;
15446
+ area.attach(this.root);
15447
+ this.codeScroller = this.codeArea.root;
15448
+ // Add Line numbers gutter, only the container, line numbers are in the same line div
15449
+ this.lineGutter = exports.LX.makeElement('div', `w-16 overflow-hidden absolute top-0 bg-inherit z-10 ${this.skipTabs ? '' : 'mt-8'}`, null, this.codeScroller);
15450
+ // Add code sizer, which will have the code elements
15451
+ this.codeSizer = exports.LX.makeElement('div', 'pseudoparent-tabs w-full', null, this.codeScroller);
15452
+ // Cursors and selections
15453
+ this.cursorsLayer = exports.LX.makeElement('div', 'cursors', null, this.codeSizer);
15454
+ this.selectionsLayer = exports.LX.makeElement('div', 'selections', null, this.codeSizer);
15455
+ // Custom scrollbars
15456
+ if (!this.disableEdition) {
15457
+ this.vScrollbar = new ScrollBar(true, (delta) => {
15458
+ const scrollMax = this.codeScroller.scrollHeight - this.codeScroller.clientHeight;
15459
+ this.codeScroller.scrollTop = this.vScrollbar.applyDragDelta(delta, scrollMax);
15460
+ this._discardScroll = true;
15461
+ });
15462
+ this.root.appendChild(this.vScrollbar.root);
15463
+ this.hScrollbar = new ScrollBar(false, (delta) => {
15464
+ const scrollMax = this.codeScroller.scrollWidth - this.codeScroller.clientWidth;
15465
+ this.codeScroller.scrollLeft = this.hScrollbar.applyDragDelta(delta, scrollMax);
15466
+ this._discardScroll = true;
15467
+ });
15468
+ this.root.appendChild(this.hScrollbar.root);
15469
+ // Sync scrollbar thumbs on native scroll
15470
+ this.codeScroller.addEventListener('scroll', () => {
15471
+ if (this._discardScroll) {
15472
+ this._discardScroll = false;
15473
+ return;
15474
+ }
15475
+ this._syncScrollBars();
15476
+ });
15477
+ // Touch events for native scrolling on mobile
15478
+ let touchStartX = 0;
15479
+ let touchStartY = 0;
15480
+ this.codeScroller.addEventListener('touchstart', (e) => {
15481
+ touchStartX = e.touches[0].clientX;
15482
+ touchStartY = e.touches[0].clientY;
15483
+ }, { passive: true });
15484
+ this.codeScroller.addEventListener('touchmove', (e) => {
15485
+ const touchX = e.touches[0].clientX;
15486
+ const touchY = e.touches[0].clientY;
15487
+ const deltaX = touchStartX - touchX;
15488
+ const deltaY = touchStartY - touchY;
15489
+ this.codeScroller.scrollLeft += deltaX;
15490
+ this.codeScroller.scrollTop += deltaY;
15491
+ touchStartX = touchX;
15492
+ touchStartY = touchY;
15493
+ }, { passive: true });
15494
+ // Wheel: Ctrl+Wheel for font zoom, Shift+Wheel for horizontal scroll
15495
+ this.codeScroller.addEventListener('wheel', (e) => {
15496
+ if (e.ctrlKey) {
15497
+ e.preventDefault();
15498
+ this._applyFontSizeOffset(e.deltaY < 0 ? 1 : -1);
15499
+ return;
15500
+ }
15501
+ if (e.shiftKey) {
15502
+ this.codeScroller.scrollLeft += e.deltaY > 0 ? 10 : -10;
15503
+ }
15504
+ }, { passive: false });
15505
+ }
15506
+ // Resize observer
15507
+ const codeResizeObserver = new ResizeObserver(() => {
15508
+ if (!this.currentTab)
15509
+ return;
15510
+ this.resize(true);
15511
+ });
15512
+ codeResizeObserver.observe(this.codeArea.root);
15513
+ if (!this.disableEdition) {
15514
+ // Add autocomplete box
15515
+ if (this.useAutoComplete) {
15516
+ this.autocomplete = exports.LX.makeElement('div', 'autocomplete');
15517
+ this.codeArea.attach(this.autocomplete);
15518
+ }
15519
+ const searchBoxClass = 'searchbox bg-card min-w-96 absolute z-100 top-8 right-2 rounded-lg border-color overflow-y-scroll opacity-0';
15520
+ // Add search box
15521
+ {
15522
+ const box = exports.LX.makeElement('div', searchBoxClass, null, this.codeArea);
15523
+ const searchPanel = new exports.LX.Panel();
15524
+ box.appendChild(searchPanel.root);
15525
+ searchPanel.sameLine();
15526
+ const textComponent = searchPanel.addText(null, '', null, { placeholder: 'Find', inputClass: 'bg-secondary' });
15527
+ searchPanel.addButton(null, 'MatchCaseButton', (v) => {
15528
+ this._searchMatchCase = v;
15529
+ this._doSearch();
15530
+ }, { icon: 'CaseSensitive', selectable: true, buttonClass: 'link', title: 'Match Case',
15531
+ tooltip: true });
15532
+ searchPanel.addButton(null, 'up', () => this._doSearch(null, true), { icon: 'ArrowUp', buttonClass: 'ghost', title: 'Previous Match',
15533
+ tooltip: true });
15534
+ searchPanel.addButton(null, 'down', () => this._doSearch(), { icon: 'ArrowDown', buttonClass: 'ghost', title: 'Next Match',
15535
+ tooltip: true });
15536
+ searchPanel.addButton(null, 'x', this._doHideSearch.bind(this), { icon: 'X', buttonClass: 'ghost', title: 'Close',
15537
+ tooltip: true });
15538
+ searchPanel.endLine();
15539
+ const searchInput = textComponent.root.querySelector('input');
15540
+ searchInput?.addEventListener('keyup', (e) => {
15541
+ if (e.key == 'Escape')
15542
+ this._doHideSearch();
15543
+ else if (e.key == 'Enter')
15544
+ this._doSearch(e.target.value, !!e.shiftKey);
15545
+ });
15546
+ this.searchBox = box;
15547
+ }
15548
+ // Add search LINE box
15549
+ {
15550
+ const box = exports.LX.makeElement('div', searchBoxClass, null, this.codeArea);
15551
+ const searchPanel = new exports.LX.Panel();
15552
+ box.appendChild(searchPanel.root);
15553
+ searchPanel.sameLine();
15554
+ const textComponent = searchPanel.addText(null, '', (value) => {
15555
+ searchInput.value = ':' + value.replaceAll(':', '');
15556
+ this._doGotoLine(parseInt(searchInput.value.slice(1)));
15557
+ }, { className: 'flex-auto-fill', placeholder: 'Go to line', trigger: 'input' });
15558
+ searchPanel.addButton(null, 'x', this._doHideSearch.bind(this), { icon: 'X', title: 'Close', buttonClass: 'ghost',
15559
+ tooltip: true });
15560
+ searchPanel.endLine();
15561
+ const searchInput = textComponent.root.querySelector('input');
15562
+ searchInput.addEventListener('keyup', (e) => {
15563
+ if (e.key == 'Escape')
15564
+ this._doHideSearch();
15565
+ });
15566
+ searchPanel.addText(null, 'Type a line number to go to (from 0 to 0).', null, { disabled: true, inputClass: 'text-xs bg-none', signal: '@line-number-range' });
15567
+ this.searchLineBox = box;
15568
+ }
15569
+ }
15570
+ // Hidden textarea for capturing keyboard input (dead keys, IME, etc.)
15571
+ this._inputArea = exports.LX.makeElement('textarea', 'absolute opacity-0 w-[1px] h-[1px] top-0 left-0 overflow-hidden resize-none', '', this.root);
15572
+ this._inputArea.setAttribute('autocorrect', 'off');
15573
+ this._inputArea.setAttribute('autocapitalize', 'off');
15574
+ this._inputArea.setAttribute('spellcheck', 'false');
15575
+ this._inputArea.tabIndex = 0;
15576
+ // Events:
15577
+ this._inputArea.addEventListener('keydown', this._onKeyDown.bind(this));
15578
+ this._inputArea.addEventListener('compositionstart', () => this._composing = true);
15579
+ this._inputArea.addEventListener('compositionend', (e) => {
15580
+ this._composing = false;
15581
+ this._inputArea.value = '';
15582
+ if (e.data)
15583
+ this._doInsertChar(e.data);
15584
+ });
15585
+ this._inputArea.addEventListener('input', () => {
15586
+ if (this._composing)
15587
+ return;
15588
+ const val = this._inputArea.value;
15589
+ if (val) {
15590
+ this._inputArea.value = '';
15591
+ this._doInsertChar(val);
15592
+ }
15593
+ });
15594
+ this._inputArea.addEventListener('focus', () => this._setFocused(true));
15595
+ this._inputArea.addEventListener('blur', () => this._setFocused(false));
15596
+ this.codeArea.root.addEventListener('mousedown', this._onMouseDown.bind(this));
15597
+ this.codeArea.root.addEventListener('contextmenu', this._onMouseDown.bind(this));
15598
+ // Bottom status panel
15599
+ this.statusPanel = this._createStatusPanel(options);
15600
+ if (this.statusPanel) {
15601
+ // Don't do this.area.attach here, since it will append to the tabs area.
15602
+ this.area.root.appendChild(this.statusPanel.root);
15603
+ }
15604
+ if (this.allowAddScripts) {
15605
+ this.onCreateFile = options.onCreateFile;
15606
+ this.addTab('+', {
15607
+ selected: false,
15608
+ title: 'Create file'
15609
+ });
15610
+ }
15611
+ // Starter code tab container
15612
+ if (options.defaultTab ?? true) {
15613
+ this.addTab(options.name || 'untitled', {
15614
+ language: this.highlight,
15615
+ title: options.title
15616
+ });
15617
+ // Initial render
15618
+ this._renderAllLines();
15619
+ this._renderCursors();
15620
+ }
15621
+ this._init();
15622
+ }
15623
+ _init() {
15624
+ if (this._displayObservers)
15625
+ return;
15626
+ this._isReady = false;
15627
+ const root = this.root;
15628
+ const _isVisible = () => {
15629
+ return (root.offsetParent !== null
15630
+ && root.clientWidth > 0
15631
+ && root.clientHeight > 0);
15632
+ };
15633
+ const _tryPrepare = async () => {
15634
+ if (this._isReady)
15635
+ return;
15636
+ if (!_isVisible())
15637
+ return;
15638
+ // Stop observing once prepared
15639
+ intersectionObserver.disconnect();
15640
+ resizeObserver.disconnect();
15641
+ await this._setupEditorWhenVisible();
15642
+ };
15643
+ // IntersectionObserver (for viewport)
15644
+ const intersectionObserver = new IntersectionObserver((entries) => {
15645
+ for (const entry of entries) {
15646
+ if (entry.isIntersecting) {
15647
+ _tryPrepare();
15648
+ }
15649
+ }
15650
+ });
15651
+ intersectionObserver.observe(root);
15652
+ // ResizeObserver (for display property changes)
15653
+ const resizeObserver = new ResizeObserver(() => {
15654
+ _tryPrepare();
15655
+ });
15656
+ resizeObserver.observe(root);
15657
+ // Fallback polling (don't use it for now)
15658
+ // const interval = setInterval( () => {
15659
+ // if ( this._isReady ) {
15660
+ // clearInterval( interval );
15661
+ // return;
15662
+ // }
15663
+ // _tryPrepare();
15664
+ // }, 250 );
15665
+ this._displayObservers = {
15666
+ intersectionObserver,
15667
+ resizeObserver
15668
+ // interval,
15669
+ };
15670
+ }
15671
+ clear() {
15672
+ console.assert(this.rightStatusPanel && this.leftStatusPanel, 'No panels to clear.');
15673
+ this.rightStatusPanel.clear();
15674
+ this.leftStatusPanel.clear();
15675
+ }
15676
+ addExplorerItem(item) {
15677
+ if (!this.explorer) {
15678
+ return;
15679
+ }
15680
+ if (!this.explorer.innerTree.data.find((value, index) => value.id === item.id)) {
15681
+ this.explorer.innerTree.data.push(item);
15682
+ }
15683
+ }
15684
+ ;
15685
+ setText(text) {
15686
+ if (!this.currentTab)
15687
+ return;
15688
+ this.doc.setText(text);
15689
+ this.cursorSet.set(0, 0);
15690
+ this.undoManager.clear();
15691
+ this._lineStates = [];
15692
+ this._renderAllLines();
15693
+ this._renderCursors();
15694
+ this._renderSelections();
15695
+ this.resize(true);
15696
+ }
15697
+ appendText(text) {
15698
+ const cursor = this.cursorSet.getPrimary();
15699
+ const { line, col } = cursor.head;
15700
+ const op = this.doc.insert(line, col, text);
15701
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
15702
+ // Move cursor to end of inserted text
15703
+ const lines = text.split(/\r?\n/);
15704
+ if (lines.length === 1) {
15705
+ cursor.head = { line, col: col + text.length };
15706
+ }
15707
+ else {
15708
+ cursor.head = { line: line + lines.length - 1, col: lines[lines.length - 1].length };
15709
+ }
15710
+ cursor.anchor = { ...cursor.head };
15711
+ this._rebuildLines();
15712
+ this._renderCursors();
15713
+ this._renderSelections();
15714
+ this.resize();
15715
+ this._scrollCursorIntoView();
15716
+ }
15717
+ getText() {
15718
+ return this.doc.getText();
15719
+ }
15720
+ setLanguage(name, extension) {
15721
+ const lang = Tokenizer.getLanguage(name);
15722
+ if (!lang)
15723
+ return;
15724
+ this.language = lang;
15725
+ if (this.currentTab) {
15726
+ this.currentTab.language = name;
15727
+ if (!this.skipTabs) {
15728
+ this.tabs.setIcon(this.currentTab.name, getLanguageIcon(lang, extension));
15729
+ }
15730
+ }
15731
+ this._lineStates = [];
15732
+ this._renderAllLines();
15733
+ exports.LX.emitSignal('@highlight', name);
15734
+ }
15735
+ focus() {
15736
+ this._inputArea.focus();
15737
+ }
15738
+ addTab(name, options = {}) {
15739
+ const isNewTabButton = name === '+';
15740
+ const dom = exports.LX.makeElement('div', 'code');
15741
+ const langName = options.language ?? 'Plain Text';
15742
+ const langDef = Tokenizer.getLanguage(langName);
15743
+ const extension = exports.LX.getExtension(name);
15744
+ const icon = isNewTabButton ? null : getLanguageIcon(langDef, extension);
15745
+ const selected = options.selected ?? true;
15746
+ const codeTab = {
15747
+ name,
15748
+ dom,
15749
+ doc: new CodeDocument(this.onCodeChange),
15750
+ cursorSet: new CursorSet(),
15751
+ undoManager: new UndoManager(),
15752
+ language: langName,
15753
+ title: options.title ?? name
15754
+ };
15755
+ this._openedTabs[name] = codeTab;
15756
+ this._loadedTabs[name] = codeTab;
15757
+ if (this.useFileExplorer && !isNewTabButton) {
15758
+ this.addExplorerItem({ id: name, skipVisibility: true, icon });
15759
+ this.explorer.innerTree.frefresh(name);
15760
+ }
15761
+ if (!this.skipTabs) {
15762
+ this.tabs.add(name, dom, {
15763
+ selected,
15764
+ icon,
15765
+ fixed: isNewTabButton,
15766
+ title: codeTab.title,
15767
+ onSelect: this._onSelectTab.bind(this, isNewTabButton),
15768
+ onContextMenu: this._onContextMenuTab.bind(this, isNewTabButton),
15769
+ allowDelete: this.allowClosingTabs,
15770
+ indexOffset: options.indexOffset
15771
+ });
15772
+ }
15773
+ // Move into the sizer..
15774
+ this.codeSizer.appendChild(dom);
15775
+ if (options.text) {
15776
+ codeTab.doc.setText(options.text);
15777
+ codeTab.cursorSet.set(0, 0);
15778
+ codeTab.undoManager.clear();
15779
+ this._renderAllLines();
15780
+ this._renderCursors();
15781
+ this._renderSelections();
15782
+ this._resetGutter();
15783
+ }
15784
+ if (selected) {
15785
+ this.currentTab = codeTab;
15786
+ this._updateDataInfoPanel('@tab-name', name);
15787
+ this.setLanguage(langName, extension);
15788
+ }
15789
+ return codeTab;
15790
+ }
15791
+ loadTab(name) {
15792
+ if (this._openedTabs[name]) {
15793
+ this.tabs.select(name);
15794
+ return;
15795
+ }
15796
+ const tab = this._loadedTabs[name];
15797
+ if (tab) {
15798
+ this._openedTabs[name] = tab;
15799
+ this.tabs.add(name, tab.dom, {
15800
+ selected: true,
15801
+ // icon,
15802
+ title: tab.title,
15803
+ onSelect: this._onSelectTab.bind(this),
15804
+ onContextMenu: this._onContextMenuTab.bind(this),
15805
+ allowDelete: this.allowClosingTabs
15806
+ });
15807
+ // Move into the sizer..
15808
+ this.codeSizer.appendChild(tab.dom);
15809
+ this.currentTab = tab;
15810
+ this._updateDataInfoPanel('@tab-name', name);
15811
+ return;
15812
+ }
15813
+ this.addTab(name, this._storedTabs[name] ?? null);
15814
+ }
15815
+ closeTab(name) {
15816
+ if (!this.allowClosingTabs)
15817
+ return;
15818
+ this.tabs.delete(name);
15819
+ const tab = this._openedTabs[name];
15820
+ if (tab) {
15821
+ tab.dom.remove();
15822
+ delete this._openedTabs[name];
15823
+ }
15824
+ // If we closed the current tab, switch to the first available
15825
+ if (this.currentTab?.name === name) {
15826
+ const remaining = Object.keys(this._openedTabs).filter(k => k !== '+');
15827
+ if (remaining.length > 0) {
15828
+ this.tabs.select(remaining[0]);
15829
+ }
15830
+ else {
15831
+ this.currentTab = null;
15832
+ }
15833
+ }
15834
+ }
15835
+ setCustomSuggestions(suggestions) {
15836
+ if (!suggestions || suggestions.constructor !== Array) {
15837
+ console.warn('suggestions should be a string array!');
15838
+ return;
15839
+ }
15840
+ this.customSuggestions = suggestions;
15841
+ }
15842
+ loadFile(file, options = {}) {
15843
+ const onLoad = (text, name) => {
15844
+ // Remove Carriage Return in some cases and sub tabs using spaces
15845
+ text = text.replaceAll('\r', '').replaceAll(/\t|\\t/g, ' '.repeat(this.tabSize));
15846
+ const ext = exports.LX.getExtension(name);
15847
+ const lang = options.language ?? (Tokenizer.getLanguage(options.language)
15848
+ ?? (Tokenizer.getLanguageByExtension(ext) ?? Tokenizer.getLanguage('Plain Text')));
15849
+ const langName = lang.name;
15850
+ if (this.useFileExplorer || this.skipTabs) {
15851
+ this._storedTabs[name] = {
15852
+ text,
15853
+ title: options.title ?? name,
15854
+ language: langName,
15855
+ ...options
15856
+ };
15857
+ if (this.useFileExplorer) {
15858
+ this.addExplorerItem({ id: name, skipVisibility: true, icon: getLanguageIcon(lang, ext) });
15859
+ this.explorer.innerTree.frefresh(name);
15860
+ }
15861
+ }
15862
+ else {
15863
+ this.addTab(name, {
15864
+ selected: true,
15865
+ title: options.title ?? name,
15866
+ language: langName
15867
+ });
15868
+ this.doc.setText(text);
15869
+ this.setLanguage(langName, ext);
15870
+ this.cursorSet.set(0, 0);
15871
+ this.undoManager.clear();
15872
+ this._renderCursors();
15873
+ this._renderSelections();
15874
+ this._resetGutter();
15875
+ }
15876
+ if (options.callback) {
15877
+ options.callback(name, text);
15878
+ }
15879
+ };
15880
+ if (typeof file === 'string') {
15881
+ const url = file;
15882
+ const name = options.filename ?? url.substring(url.lastIndexOf('/') + 1);
15883
+ exports.LX.request({ url, success: (text) => {
15884
+ onLoad(text, name);
15885
+ } });
15886
+ }
15887
+ else {
15888
+ const fr = new FileReader();
15889
+ fr.readAsText(file);
15890
+ fr.onload = (e) => {
15891
+ const text = e.currentTarget.result;
15892
+ onLoad(text, file.name);
15893
+ };
15894
+ }
15895
+ }
15896
+ async loadFiles(files, onComplete, async = false) {
15897
+ if (!files || files.length === 0) {
15898
+ onComplete?.(this, [], 0);
15899
+ return;
15900
+ }
15901
+ const results = [];
15902
+ for (const filePath of files) {
15903
+ try {
15904
+ const text = await exports.LX.requestFileAsync(filePath, 'text');
15905
+ // Process the loaded file
15906
+ const name = filePath.substring(filePath.lastIndexOf('/') + 1);
15907
+ const processedText = text.replaceAll('\r', '').replaceAll(/\t|\\t/g, ' '.repeat(this.tabSize));
15908
+ const ext = exports.LX.getExtension(name);
15909
+ const lang = Tokenizer.getLanguageByExtension(ext) ?? Tokenizer.getLanguage('Plain Text');
15910
+ const langName = lang.name;
15911
+ if (this.useFileExplorer || this.skipTabs) {
15912
+ this._storedTabs[name] = {
15913
+ text: processedText,
15914
+ title: name,
15915
+ language: langName
15916
+ };
15917
+ if (this.useFileExplorer) {
15918
+ this.addExplorerItem({ id: name, skipVisibility: true, icon: getLanguageIcon(lang, ext) });
15919
+ this.explorer.innerTree.frefresh(name);
15920
+ }
15921
+ }
15922
+ else {
15923
+ this.addTab(name, {
15924
+ selected: results.length === 0, // Select first tab only
15925
+ title: name,
15926
+ language: langName
15927
+ });
15928
+ if (results.length === 0) {
15929
+ this.doc.setText(processedText);
15930
+ this.setLanguage(langName, ext);
15931
+ this.cursorSet.set(0, 0);
15932
+ this.undoManager.clear();
15933
+ this._renderCursors();
15934
+ this._renderSelections();
15935
+ this._resetGutter();
15936
+ }
15937
+ }
15938
+ results.push({ filePath, name, success: true });
15939
+ }
15940
+ catch (error) {
15941
+ console.error(`[LX.CodeEditor] Failed to load file: ${filePath}`, error);
15942
+ results.push({ filePath, success: false, error });
15943
+ }
15944
+ }
15945
+ onComplete?.(this, results, results.length);
15946
+ }
15947
+ async _setupEditorWhenVisible() {
15948
+ // Load any font size from local storage
15949
+ // If not, use default size and make sure it's sync by not hardcoding a number by default here
15950
+ const savedFontSize = window.localStorage.getItem('lexcodeeditor-font-size');
15951
+ if (savedFontSize) {
15952
+ await this._setFontSize(parseInt(savedFontSize), false);
15953
+ }
15954
+ else {
15955
+ const r = document.querySelector(':root');
15956
+ const s = getComputedStyle(r);
15957
+ this.fontSize = parseInt(s.getPropertyValue('--code-editor-font-size'));
15958
+ await this._measureChar();
15959
+ }
15960
+ exports.LX.emitSignal('@font-size', this.fontSize);
15961
+ exports.LX.doAsync(() => {
15962
+ if (!this._isReady) {
15963
+ this._isReady = true;
15964
+ if (this.onReady) {
15965
+ this.onReady(this);
15966
+ }
15967
+ console.log(`[LX.CodeEditor] Ready! (font size: ${this.fontSize}px, char size: ${this.charWidth}px)`);
15968
+ }
15969
+ }, 20);
15970
+ }
15971
+ _onSelectTab(isNewTabButton, event, name) {
15972
+ if (this.disableEdition) {
15973
+ return;
15974
+ }
15975
+ if (isNewTabButton) {
15976
+ this._onNewTab(event);
15977
+ return;
15978
+ }
15979
+ this.currentTab = this._openedTabs[name];
15980
+ this._updateDataInfoPanel('@tab-name', name);
15981
+ this.language = Tokenizer.getLanguage(this.currentTab.language) ?? Tokenizer.getLanguage('Plain Text');
15982
+ exports.LX.emitSignal('@highlight', this.currentTab.language);
15983
+ this._renderAllLines();
15984
+ this._afterCursorMove();
15985
+ if (!isNewTabButton && this.onSelectTab) {
15986
+ this.onSelectTab(name, this);
15987
+ }
15988
+ }
15989
+ _onNewTab(event) {
15990
+ if (this.onNewTab) {
15991
+ this.onNewTab(event);
15992
+ return;
15993
+ }
15994
+ const dmOptions = this.newTabOptions ?? [
15995
+ { name: 'Create file', icon: 'FilePlus', callback: this._onCreateNewFile.bind(this) },
15996
+ { name: 'Load file', icon: 'FileUp', disabled: !this.allowLoadingFiles, callback: this._doLoadFromFile.bind(this) }
15997
+ ];
15998
+ exports.LX.addDropdownMenu(event.target, dmOptions, { side: 'bottom', align: 'start' });
15999
+ }
16000
+ _onCreateNewFile() {
16001
+ let options = {};
16002
+ if (this.onCreateFile) {
16003
+ options = this.onCreateFile(this);
16004
+ // Skip adding new file
16005
+ if (!options) {
16006
+ return;
16007
+ }
16008
+ }
16009
+ const name = options.name ?? 'unnamed.js';
16010
+ this.addTab(name, {
16011
+ selected: true,
16012
+ title: name,
16013
+ indexOffset: options.indexOffset,
16014
+ language: options.language ?? 'JavaScript'
16015
+ });
16016
+ this._renderAllLines();
16017
+ this._renderCursors();
16018
+ this._renderSelections();
16019
+ this._resetGutter();
16020
+ }
16021
+ _onContextMenuTab(isNewTabButton = false, event, name) {
16022
+ if (isNewTabButton) {
16023
+ return;
16024
+ }
16025
+ exports.LX.addDropdownMenu(event.target, [
16026
+ { name: 'Close', kbd: 'MWB', disabled: !this.allowClosingTabs, callback: () => {
16027
+ this.closeTab(name);
16028
+ } },
16029
+ { name: 'Close Others', disabled: !this.allowClosingTabs, callback: () => {
16030
+ for (const key of Object.keys(this.tabs.tabs)) {
16031
+ if (key === '+' || key === name)
16032
+ continue;
16033
+ this.closeTab(key);
16034
+ }
16035
+ } },
16036
+ { name: 'Close All', disabled: !this.allowClosingTabs, callback: () => {
16037
+ for (const key of Object.keys(this.tabs.tabs)) {
16038
+ if (key === '+')
16039
+ continue;
16040
+ this.closeTab(key);
16041
+ }
16042
+ } },
16043
+ null,
16044
+ { name: 'Copy Path', icon: 'Copy', callback: () => {
16045
+ navigator.clipboard.writeText(this._openedTabs[name].path ?? '');
16046
+ } }
16047
+ ], { side: 'bottom', align: 'start', event });
16048
+ }
16049
+ async _measureChar() {
16050
+ const parentContainer = exports.LX.makeContainer(null, 'lexcodeeditor', '', document.body);
16051
+ const container = exports.LX.makeContainer(null, 'code', '', parentContainer);
16052
+ const line = document.createElement('pre');
16053
+ container.appendChild(line);
16054
+ const measurer = document.createElement('span');
16055
+ measurer.className = 'codechar';
16056
+ measurer.style.visibility = 'hidden';
16057
+ measurer.textContent = 'M';
16058
+ line.appendChild(measurer);
16059
+ // Force load the font before measuring
16060
+ const computedStyle = getComputedStyle(measurer);
16061
+ const fontFamily = computedStyle.fontFamily;
16062
+ const fontSize = computedStyle.fontSize;
16063
+ const fontWeight = computedStyle.fontWeight || 'normal';
16064
+ const fontStyle = computedStyle.fontStyle || 'normal';
16065
+ const fontString = `${fontStyle} ${fontWeight} ${fontSize} ${fontFamily}`;
16066
+ try {
16067
+ await document.fonts.load(fontString);
16068
+ }
16069
+ catch (e) {
16070
+ console.warn('[LX.CodeEditor] Failed to load font:', fontString, e);
16071
+ }
16072
+ // Use requestAnimationFrame to ensure the element is rendered
16073
+ requestAnimationFrame(() => {
16074
+ const rect = measurer.getBoundingClientRect();
16075
+ this.charWidth = rect.width || 7;
16076
+ this.lineHeight = parseFloat(getComputedStyle(this.root).getPropertyValue('--code-editor-row-height')) || 20;
16077
+ exports.LX.deleteElement(parentContainer);
16078
+ // Re-render cursors with correct measurements
16079
+ this._renderCursors();
16080
+ this._renderSelections();
16081
+ this.resize(true);
16082
+ });
16083
+ }
16084
+ _createStatusPanel(options = {}) {
16085
+ if (this.skipInfo) {
16086
+ return;
16087
+ }
16088
+ let panel = new exports.LX.Panel({ className: 'lexcodetabinfo bg-inherit flex flex-row flex-auto-keep', height: 'auto' });
16089
+ if (this.onCreateStatusPanel) {
16090
+ this.onCreateStatusPanel(panel, this);
16091
+ }
16092
+ let leftStatusPanel = this.leftStatusPanel = new exports.LX.Panel({ id: 'FontSizeZoomStatusComponent',
16093
+ className: 'pad-xs content-center items-center flex-auto-keep', width: 'auto', height: 'auto' });
16094
+ leftStatusPanel.sameLine();
16095
+ // Zoom Component
16096
+ leftStatusPanel.addButton(null, 'ZoomOutButton', this._decreaseFontSize.bind(this), { icon: 'ZoomOut', buttonClass: 'ghost sm',
16097
+ title: 'Zoom Out', tooltip: true });
16098
+ leftStatusPanel.addLabel(this.fontSize, { fit: true, signal: '@font-size' });
16099
+ leftStatusPanel.addButton(null, 'ZoomInButton', this._increaseFontSize.bind(this), { icon: 'ZoomIn', buttonClass: 'ghost sm',
16100
+ title: 'Zoom In', tooltip: true });
16101
+ leftStatusPanel.endLine('justify-start');
16102
+ panel.attach(leftStatusPanel.root);
16103
+ // Filename cursor data
16104
+ let rightStatusPanel = this.rightStatusPanel = new exports.LX.Panel({ className: 'pad-xs content-center items-center', height: 'auto' });
16105
+ rightStatusPanel.sameLine();
16106
+ rightStatusPanel.addLabel(this.currentTab?.title ?? '', { id: 'EditorFilenameStatusComponent', fit: true, inputClass: 'text-xs',
16107
+ signal: '@tab-name' });
16108
+ rightStatusPanel.addButton(null, 'Ln 1, Col 1', this._doOpenLineSearch.bind(this), {
16109
+ id: 'EditorSelectionStatusComponent',
16110
+ buttonClass: 'outline xs',
16111
+ fit: true,
16112
+ signal: '@cursor-data'
16113
+ });
16114
+ const tabSizeButton = rightStatusPanel.addButton(null, 'Spaces: ' + this.tabSize, (value, event) => {
16115
+ const _onNewTabSize = (v) => {
16116
+ this.tabSize = parseInt(v);
16117
+ this._rebuildLines();
16118
+ this._updateDataInfoPanel('@tab-spaces', `Spaces: ${this.tabSize}`);
16119
+ };
16120
+ const dd = exports.LX.addDropdownMenu(tabSizeButton.root, ['2', '4', '8'].map((v) => { return { name: v, className: 'w-full place-content-center', callback: _onNewTabSize }; }), { side: 'top', align: 'end' });
16121
+ exports.LX.addClass(dd.root, 'min-w-16! items-center');
16122
+ }, { id: 'EditorIndentationStatusComponent', buttonClass: 'outline xs', signal: '@tab-spaces' });
16123
+ const langButton = rightStatusPanel.addButton('<b>{ }</b>', this.highlight, (value, event) => {
16124
+ const dd = exports.LX.addDropdownMenu(langButton.root, Tokenizer.getRegisteredLanguages().map((v) => {
16125
+ const lang = Tokenizer.getLanguage(v);
16126
+ const icon = getLanguageIcon(lang);
16127
+ const iconData = icon ? icon.split(' ') : [];
16128
+ return {
16129
+ name: v,
16130
+ icon: iconData[0],
16131
+ className: 'w-full text-xs px-3',
16132
+ svgClass: iconData.slice(1).join(' '),
16133
+ callback: (v) => this.setLanguage(v)
16134
+ };
16135
+ }), { side: 'top', align: 'end' });
16136
+ exports.LX.addClass(dd.root, 'min-w-min! items-center');
16137
+ }, { id: 'EditorLanguageStatusComponent', nameWidth: 'auto', buttonClass: 'outline xs', signal: '@highlight', title: '' });
16138
+ rightStatusPanel.endLine('justify-end');
16139
+ panel.attach(rightStatusPanel.root);
16140
+ const itemVisibilityMap = {
16141
+ 'Font Size Zoom': options.statusShowFontSizeZoom ?? true,
16142
+ 'Editor Filename': options.statusShowEditorFilename ?? true,
16143
+ 'Editor Selection': options.statusShowEditorSelection ?? true,
16144
+ 'Editor Indentation': options.statusShowEditorIndentation ?? true,
16145
+ 'Editor Language': options.statusShowEditorLanguage ?? true
16146
+ };
16147
+ const _setVisibility = (itemName) => {
16148
+ const b = panel.root.querySelector(`#${itemName.replaceAll(' ', '')}StatusComponent`);
16149
+ console.assert(b, `${itemName} has no status button!`);
16150
+ b.classList.toggle('hidden', !itemVisibilityMap[itemName]);
16151
+ };
16152
+ for (const [itemName, v] of Object.entries(itemVisibilityMap)) {
16153
+ _setVisibility(itemName);
16154
+ }
16155
+ panel.root.addEventListener('contextmenu', (e) => {
16156
+ if (e.target
16157
+ && (e.target.classList.contains('lexpanel')
16158
+ || e.target.classList.contains('lexinlinecomponents'))) {
16159
+ return;
16160
+ }
16161
+ const menuOptions = Object.keys(itemVisibilityMap).map((itemName, idx) => {
16162
+ const item = {
16163
+ name: itemName,
16164
+ icon: 'Check',
16165
+ callback: () => {
16166
+ itemVisibilityMap[itemName] = !itemVisibilityMap[itemName];
16167
+ _setVisibility(itemName);
16168
+ }
16169
+ };
16170
+ if (!itemVisibilityMap[itemName])
16171
+ delete item.icon;
16172
+ return item;
16173
+ });
16174
+ exports.LX.addDropdownMenu(e.target, menuOptions, { side: 'top', align: 'start' });
16175
+ });
16176
+ return panel;
16177
+ }
16178
+ _updateDataInfoPanel(signal, value) {
16179
+ if (this.skipInfo)
16180
+ return;
16181
+ if (this.cursorSet.cursors.length > 1) {
16182
+ value = '';
16183
+ }
16184
+ exports.LX.emitSignal(signal, value);
16185
+ }
16186
+ /**
16187
+ * Tokenize a line and return its innerHTML with syntax highlighting spans.
16188
+ */
16189
+ _tokenizeLine(lineIndex) {
16190
+ const prevState = lineIndex > 0
16191
+ ? (this._lineStates[lineIndex - 1] ?? Tokenizer.initialState())
16192
+ : Tokenizer.initialState();
16193
+ const lineText = this.doc.getLine(lineIndex);
16194
+ const result = Tokenizer.tokenizeLine(lineText, this.language, prevState);
16195
+ // Build HTML
16196
+ const langClass = this.language.name.toLowerCase().replace(/[^a-z]/g, '');
16197
+ let html = '';
16198
+ for (const token of result.tokens) {
16199
+ const cls = TOKEN_CLASS_MAP[token.type];
16200
+ const escaped = token.value
16201
+ .replace(/&/g, '&amp;')
16202
+ .replace(/</g, '&lt;')
16203
+ .replace(/>/g, '&gt;');
16204
+ if (cls) {
16205
+ html += `<span class="${cls} ${langClass}">${escaped}</span>`;
16206
+ }
16207
+ else {
16208
+ html += escaped;
16209
+ }
16210
+ }
16211
+ return { html: html || '&nbsp;', endState: result.state, tokens: result.tokens };
16212
+ }
16213
+ /**
16214
+ * Update symbol table for a line.
16215
+ */
16216
+ _updateSymbolsForLine(lineIndex) {
16217
+ const lineText = this.doc.getLine(lineIndex);
16218
+ this.symbolTable.updateScopeForLine(lineIndex, lineText);
16219
+ this.symbolTable.removeLineSymbols(lineIndex);
16220
+ const { tokens } = this._tokenizeLine(lineIndex);
16221
+ const symbols = parseSymbolsFromLine(lineText, tokens, lineIndex, this.symbolTable);
16222
+ for (const symbol of symbols) {
16223
+ this.symbolTable.addSymbol(symbol);
16224
+ }
16225
+ }
16226
+ /**
16227
+ * Render all lines from scratch.
16228
+ */
16229
+ _renderAllLines() {
16230
+ if (!this.currentTab)
16231
+ return;
16232
+ this.codeContainer.innerHTML = '';
16233
+ this._lineElements = [];
16234
+ this._lineStates = [];
16235
+ this.symbolTable.clear(); // Clear all symbols on full rebuild
16236
+ for (let i = 0; i < this.doc.lineCount; i++) {
16237
+ this._appendLineElement(i);
16238
+ }
16239
+ }
16240
+ /**
16241
+ * Gets the html for the line gutter.
16242
+ */
16243
+ _getGutterHtml(lineIndex) {
16244
+ return `<span class="line-gutter">${lineIndex + 1}</span>`;
16245
+ }
16246
+ /**
16247
+ * Create and append a <pre> element for a line.
16248
+ */
16249
+ _appendLineElement(lineIndex) {
16250
+ const { html, endState } = this._tokenizeLine(lineIndex);
16251
+ this._lineStates[lineIndex] = endState;
16252
+ const pre = document.createElement('pre');
16253
+ pre.innerHTML = this._getGutterHtml(lineIndex) + html;
16254
+ this.codeContainer.appendChild(pre);
16255
+ this._lineElements[lineIndex] = pre;
16256
+ // Update symbols for this line
16257
+ this._updateSymbolsForLine(lineIndex);
16258
+ }
16259
+ /**
16260
+ * Re-render a single line's content (after editing).
16261
+ */
16262
+ _updateLine(lineIndex) {
16263
+ const { html, endState } = this._tokenizeLine(lineIndex);
16264
+ const oldState = this._lineStates[lineIndex];
16265
+ this._lineStates[lineIndex] = endState;
16266
+ if (this._lineElements[lineIndex]) {
16267
+ this._lineElements[lineIndex].innerHTML = this._getGutterHtml(lineIndex) + html;
16268
+ }
16269
+ // Update symbols for this line
16270
+ this._updateSymbolsForLine(lineIndex);
16271
+ // If the tokenizer state changed (e.g. opened/closed a block comment),
16272
+ // re-render subsequent lines until states stabilize
16273
+ if (!this._statesEqual(oldState, endState)) {
16274
+ for (let i = lineIndex + 1; i < this.doc.lineCount; i++) {
16275
+ const { html: nextHtml, endState: nextEnd } = this._tokenizeLine(i);
16276
+ const nextOld = this._lineStates[i];
16277
+ this._lineStates[i] = nextEnd;
16278
+ if (this._lineElements[i]) {
16279
+ this._lineElements[i].innerHTML = this._getGutterHtml(i) + nextHtml;
16280
+ }
16281
+ this._updateSymbolsForLine(i); // Update symbols for cascaded lines too
16282
+ if (this._statesEqual(nextOld, nextEnd))
16283
+ break;
16284
+ }
16285
+ }
16286
+ }
16287
+ /**
16288
+ * Rebuild line elements after structural changes (insert/delete lines).
16289
+ */
16290
+ _rebuildLines() {
16291
+ // Diff: if count matches, just update content; otherwise full rebuild
16292
+ if (this._lineElements.length === this.doc.lineCount) {
16293
+ for (let i = 0; i < this.doc.lineCount; i++) {
16294
+ this._updateLine(i);
16295
+ }
16296
+ }
16297
+ else {
16298
+ this._renderAllLines();
16299
+ }
16300
+ this.resize();
16301
+ }
16302
+ _statesEqual(a, b) {
16303
+ if (!a)
16304
+ return false;
16305
+ if (a.stack.length !== b.stack.length)
16306
+ return false;
16307
+ for (let i = 0; i < a.stack.length; i++) {
16308
+ if (a.stack[i] !== b.stack[i])
16309
+ return false;
16310
+ }
16311
+ return true;
16312
+ }
16313
+ _updateActiveLine() {
16314
+ const hasSelection = this.cursorSet.hasSelection();
16315
+ const activeLine = this.cursorSet.getPrimary().head.line;
16316
+ const activeCol = this.cursorSet.getPrimary().head.col;
16317
+ for (let i = 0; i < this._lineElements.length; i++) {
16318
+ this._lineElements[i].classList.toggle('active-line', !hasSelection && i === activeLine);
16319
+ }
16320
+ this._updateDataInfoPanel('@cursor-data', `Ln ${activeLine + 1}, Col ${activeCol + 1}`);
16321
+ }
16322
+ _renderCursors() {
16323
+ if (!this.currentTab)
16324
+ return;
16325
+ this.cursorsLayer.innerHTML = '';
16326
+ for (const sel of this.cursorSet.cursors) {
16327
+ const el = document.createElement('div');
16328
+ el.className = 'cursor';
16329
+ el.innerHTML = '&nbsp;';
16330
+ el.style.left = (sel.head.col * this.charWidth + this.xPadding) + 'px';
16331
+ el.style.top = (sel.head.line * this.lineHeight) + 'px';
16332
+ this.cursorsLayer.appendChild(el);
16333
+ }
16334
+ this._updateActiveLine();
16335
+ }
16336
+ _renderSelections() {
16337
+ if (!this.currentTab)
16338
+ return;
16339
+ this.selectionsLayer.innerHTML = '';
16340
+ for (const sel of this.cursorSet.cursors) {
16341
+ if (selectionIsEmpty(sel))
16342
+ continue;
16343
+ const start = selectionStart(sel);
16344
+ const end = selectionEnd(sel);
16345
+ for (let line = start.line; line <= end.line; line++) {
16346
+ const lineText = this.doc.getLine(line);
16347
+ const fromCol = line === start.line ? start.col : 0;
16348
+ const toCol = line === end.line ? end.col : lineText.length;
16349
+ if (fromCol === toCol)
16350
+ continue;
16351
+ const div = document.createElement('div');
16352
+ div.className = 'lexcodeselection';
16353
+ div.style.top = (line * this.lineHeight) + 'px';
16354
+ div.style.left = (fromCol * this.charWidth + this.xPadding) + 'px';
16355
+ div.style.width = ((toCol - fromCol) * this.charWidth) + 'px';
16356
+ this.selectionsLayer.appendChild(div);
16357
+ }
16358
+ }
16359
+ }
16360
+ _setFocused(focused) {
16361
+ this._focused = focused;
16362
+ if (focused) {
16363
+ this.cursorsLayer.classList.add('show');
16364
+ this.selectionsLayer.classList.add('show');
16365
+ this.selectionsLayer.classList.remove('unfocused');
16366
+ this._startBlinker();
16367
+ }
16368
+ else {
16369
+ this.cursorsLayer.classList.remove('show');
16370
+ if (!this._isSearchBoxActive) {
16371
+ this.selectionsLayer.classList.add('unfocused');
16372
+ }
16373
+ else {
16374
+ this.selectionsLayer.classList.add('show');
16375
+ }
16376
+ this._stopBlinker();
16377
+ }
16378
+ }
16379
+ _startBlinker() {
16380
+ this._stopBlinker();
16381
+ this._cursorVisible = true;
16382
+ this._setCursorVisibility(true);
16383
+ this._blinkerInterval = setInterval(() => {
16384
+ this._cursorVisible = !this._cursorVisible;
16385
+ this._setCursorVisibility(this._cursorVisible);
16386
+ }, this._cursorBlinkRate);
16387
+ }
16388
+ _stopBlinker() {
16389
+ if (this._blinkerInterval !== null) {
16390
+ clearInterval(this._blinkerInterval);
16391
+ this._blinkerInterval = null;
16392
+ }
16393
+ }
16394
+ _resetBlinker() {
16395
+ if (this._focused) {
16396
+ this._startBlinker();
16397
+ }
16398
+ }
16399
+ _setCursorVisibility(visible) {
16400
+ const cursors = this.cursorsLayer.querySelectorAll('.cursor');
16401
+ for (const c of cursors) {
16402
+ c.style.opacity = visible ? '0.6' : '0';
16403
+ }
16404
+ }
16405
+ // Keyboard input events:
16406
+ _onKeyDown(e) {
16407
+ if (!this.currentTab)
16408
+ return;
16409
+ // Ignore events during IME / dead key composition
16410
+ if (this._composing || e.key === 'Dead')
16411
+ return;
16412
+ // Ignore modifier-only presses
16413
+ if (['Control', 'Shift', 'Alt', 'Meta'].includes(e.key))
16414
+ return;
16415
+ const ctrl = e.ctrlKey || e.metaKey;
16416
+ const shift = e.shiftKey;
16417
+ const alt = e.altKey;
16418
+ if (this._keyChain) {
16419
+ const chain = this._keyChain;
16420
+ this._keyChain = null;
16421
+ e.preventDefault();
16422
+ if (ctrl && chain === 'k') {
16423
+ switch (e.key.toLowerCase()) {
16424
+ case 'c':
16425
+ this._commentLines();
16426
+ return;
16427
+ case 'u':
16428
+ this._uncommentLines();
16429
+ return;
16430
+ }
16431
+ }
16432
+ return; // Unknown chord, just consume it
16433
+ }
16434
+ if (ctrl) {
16435
+ switch (e.key.toLowerCase()) {
16436
+ case 'a':
16437
+ e.preventDefault();
16438
+ this.cursorSet.selectAll(this.doc);
16439
+ this._afterCursorMove();
16440
+ return;
16441
+ case 'd':
16442
+ e.preventDefault();
16443
+ this._doFindNextOcurrence();
16444
+ return;
16445
+ case 'f':
16446
+ e.preventDefault();
16447
+ this._doOpenSearch();
16448
+ return;
16449
+ case 'g':
16450
+ e.preventDefault();
16451
+ this._doOpenLineSearch();
16452
+ return;
16453
+ case 'k':
16454
+ e.preventDefault();
16455
+ this._keyChain = 'k';
16456
+ return;
16457
+ case 's': // save
16458
+ e.preventDefault();
16459
+ if (this.onSave) {
16460
+ this.onSave(this.getText(), this);
16461
+ }
16462
+ return;
16463
+ case 'z':
16464
+ e.preventDefault();
16465
+ shift ? this._doRedo() : this._doUndo();
16466
+ return;
16467
+ case 'y':
16468
+ e.preventDefault();
16469
+ this._doRedo();
16470
+ return;
16471
+ case 'c':
16472
+ e.preventDefault();
16473
+ this._doCopy();
16474
+ return;
16475
+ case 'x':
16476
+ e.preventDefault();
16477
+ this._doCut();
16478
+ return;
16479
+ case 'v':
16480
+ e.preventDefault();
16481
+ this._doPaste();
16482
+ return;
16483
+ case ' ':
16484
+ e.preventDefault();
16485
+ // Also call user callback if provided
16486
+ if (this.onCtrlSpace) {
16487
+ this.onCtrlSpace(this.getText(), this);
16488
+ }
16489
+ return;
16490
+ }
16491
+ }
16492
+ switch (e.key) {
16493
+ case 'ArrowLeft':
16494
+ e.preventDefault();
16495
+ this._wasPaired = false;
16496
+ if (ctrl)
16497
+ this.cursorSet.moveWordLeft(this.doc, shift);
16498
+ else
16499
+ this.cursorSet.moveLeft(this.doc, shift);
16500
+ this._afterCursorMove();
16501
+ return;
16502
+ case 'ArrowRight':
16503
+ e.preventDefault();
16504
+ this._wasPaired = false;
16505
+ if (ctrl)
16506
+ this.cursorSet.moveWordRight(this.doc, shift);
16507
+ else
16508
+ this.cursorSet.moveRight(this.doc, shift);
16509
+ this._afterCursorMove();
16510
+ return;
16511
+ case 'ArrowUp':
16512
+ e.preventDefault();
16513
+ if (this._isAutoCompleteActive) {
16514
+ const items = this.autocomplete.childNodes;
16515
+ items[this._selectedAutocompleteIndex]?.classList.remove('selected');
16516
+ this._selectedAutocompleteIndex = (this._selectedAutocompleteIndex - 1 + items.length) % items.length;
16517
+ items[this._selectedAutocompleteIndex]?.classList.add('selected');
16518
+ items[this._selectedAutocompleteIndex]?.scrollIntoView({ block: 'nearest' });
16519
+ return;
16520
+ }
16521
+ this._wasPaired = false;
16522
+ if (alt && shift) {
16523
+ this._duplicateLine(-1);
16524
+ return;
16525
+ }
16526
+ if (alt) {
16527
+ this._swapLine(-1);
16528
+ return;
16529
+ }
16530
+ this.cursorSet.moveUp(this.doc, shift);
16531
+ this._afterCursorMove();
16532
+ return;
16533
+ case 'ArrowDown':
16534
+ e.preventDefault();
16535
+ if (this._isAutoCompleteActive) {
16536
+ const items = this.autocomplete.childNodes;
16537
+ items[this._selectedAutocompleteIndex]?.classList.remove('selected');
16538
+ this._selectedAutocompleteIndex = (this._selectedAutocompleteIndex + 1) % items.length;
16539
+ items[this._selectedAutocompleteIndex]?.classList.add('selected');
16540
+ items[this._selectedAutocompleteIndex]?.scrollIntoView({ block: 'nearest' });
16541
+ return;
16542
+ }
16543
+ this._wasPaired = false;
16544
+ if (alt && shift) {
16545
+ this._duplicateLine(1);
16546
+ return;
16547
+ }
16548
+ if (alt) {
16549
+ this._swapLine(1);
16550
+ return;
16551
+ }
16552
+ this.cursorSet.moveDown(this.doc, shift);
16553
+ this._afterCursorMove();
16554
+ return;
16555
+ case 'Home':
16556
+ e.preventDefault();
16557
+ this._wasPaired = false;
16558
+ this.cursorSet.moveToLineStart(this.doc, shift);
16559
+ this._afterCursorMove();
16560
+ return;
16561
+ case 'End':
16562
+ e.preventDefault();
16563
+ this._wasPaired = false;
16564
+ this.cursorSet.moveToLineEnd(this.doc, shift);
16565
+ this._afterCursorMove();
16566
+ return;
16567
+ case 'Escape':
16568
+ e.preventDefault();
16569
+ if (this._isAutoCompleteActive) {
16570
+ this._doHideAutocomplete();
16571
+ return;
16572
+ }
16573
+ if (this._doHideSearch())
16574
+ return;
16575
+ this.cursorSet.removeSecondaryCursors();
16576
+ // Collapse selection
16577
+ const h = this.cursorSet.getPrimary().head;
16578
+ this.cursorSet.set(h.line, h.col);
16579
+ this._afterCursorMove();
16580
+ return;
16581
+ }
16582
+ switch (e.key) {
16583
+ case 'Backspace':
16584
+ e.preventDefault();
16585
+ this._doBackspace(ctrl);
16586
+ return;
16587
+ case 'Delete':
16588
+ e.preventDefault();
16589
+ this._doDelete(ctrl);
16590
+ return;
16591
+ case 'Enter':
16592
+ e.preventDefault();
16593
+ this._doEnter(ctrl);
16594
+ return;
16595
+ case 'Tab':
16596
+ e.preventDefault();
16597
+ this._doTab(shift);
16598
+ return;
16599
+ }
16600
+ if (e.key.length === 1 && !ctrl) {
16601
+ e.preventDefault();
16602
+ this._doInsertChar(e.key);
16603
+ }
16604
+ }
16605
+ _flushIfActionChanged(action) {
16606
+ if (this._lastAction !== action) {
16607
+ this.undoManager.flush(this.cursorSet.getCursorPositions());
16608
+ this._lastAction = action;
16609
+ }
16610
+ }
16611
+ _flushAction() {
16612
+ this.undoManager.flush(this.cursorSet.getCursorPositions());
16613
+ this._lastAction = '';
16614
+ }
16615
+ _doInsertChar(char) {
16616
+ // Enclose selection if applicable
16617
+ if (char in CodeEditor.PAIR_KEYS && this.cursorSet.hasSelection()) {
16618
+ this._encloseSelection(char, CodeEditor.PAIR_KEYS[char]);
16619
+ return;
16620
+ }
16621
+ this._flushIfActionChanged('insert');
16622
+ this._deleteSelectionIfAny();
16623
+ const changedLines = new Set();
16624
+ let paired = false;
16625
+ for (const idx of this.cursorSet.sortedIndicesBottomUp()) {
16626
+ const cursor = this.cursorSet.cursors[idx];
16627
+ const { line, col } = cursor.head;
16628
+ const nextChar = this.doc.getCharAt(line, col);
16629
+ // If we just auto-paired and the next char is the same closing char, skip over it
16630
+ if (this._wasPaired && nextChar === char) {
16631
+ cursor.head = { line, col: col + 1 };
16632
+ cursor.anchor = { ...cursor.head };
16633
+ continue;
16634
+ }
16635
+ const op = this.doc.insert(line, col, char);
16636
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
16637
+ const pairInserted = char in CodeEditor.PAIR_KEYS && (!nextChar || /\s/.test(nextChar));
16638
+ const charsInserted = pairInserted ? 2 : 1;
16639
+ // Auto-pair: insert closing char if next char is whitespace or end of line
16640
+ if (pairInserted) {
16641
+ const closeOp = this.doc.insert(line, col + 1, CodeEditor.PAIR_KEYS[char]);
16642
+ this.undoManager.record(closeOp, this.cursorSet.getCursorPositions());
16643
+ paired = true;
16644
+ }
16645
+ cursor.head = { line, col: col + 1 };
16646
+ cursor.anchor = { ...cursor.head };
16647
+ this.cursorSet.adjustOthers(idx, line, col, charsInserted);
16648
+ changedLines.add(line);
16649
+ }
16650
+ this._wasPaired = paired;
16651
+ for (const line of changedLines)
16652
+ this._updateLine(line);
16653
+ this._afterCursorMove();
16654
+ // Close autocomplete when typing most chars (except word chars which update it)
16655
+ if (/[\w$]/.test(char)) {
16656
+ // Update autocomplete with new partial word
16657
+ this._doOpenAutocomplete();
16658
+ }
16659
+ else {
16660
+ // Non-word char, close autocomplete
16661
+ this._doHideAutocomplete();
16662
+ }
16663
+ }
16664
+ _getAffectedLines() {
16665
+ const cursor = this.cursorSet.getPrimary();
16666
+ const a = cursor.anchor.line;
16667
+ const h = cursor.head.line;
16668
+ return a <= h ? [a, h] : [h, a];
16669
+ }
16670
+ _commentLines() {
16671
+ const token = this.language.lineComment;
16672
+ if (!token)
16673
+ return;
16674
+ const [fromLine, toLine] = this._getAffectedLines();
16675
+ const comment = token + ' ';
16676
+ // Find minimum indentation across affected lines (skip empty lines)
16677
+ let minIndent = Infinity;
16678
+ for (let i = fromLine; i <= toLine; i++) {
16679
+ const line = this.doc.getLine(i);
16680
+ if (line.trim().length === 0)
16681
+ continue;
16682
+ minIndent = Math.min(minIndent, this.doc.getIndent(i));
16683
+ }
16684
+ if (minIndent === Infinity)
16685
+ minIndent = 0;
16686
+ this._flushAction();
16687
+ for (let i = fromLine; i <= toLine; i++) {
16688
+ const line = this.doc.getLine(i);
16689
+ if (line.trim().length === 0)
16690
+ continue;
16691
+ const op = this.doc.insert(i, minIndent, comment);
16692
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
16693
+ this._updateLine(i);
16694
+ }
16695
+ this._afterCursorMove();
16696
+ }
16697
+ _uncommentLines() {
16698
+ const token = this.language.lineComment;
16699
+ if (!token)
16700
+ return;
16701
+ const [fromLine, toLine] = this._getAffectedLines();
16702
+ this._flushAction();
16703
+ for (let i = fromLine; i <= toLine; i++) {
16704
+ const line = this.doc.getLine(i);
16705
+ const indent = this.doc.getIndent(i);
16706
+ const rest = line.substring(indent);
16707
+ // Check for "// " or "//"
16708
+ if (rest.startsWith(token + ' ')) {
16709
+ const op = this.doc.delete(i, indent, token.length + 1);
16710
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
16711
+ }
16712
+ else if (rest.startsWith(token)) {
16713
+ const op = this.doc.delete(i, indent, token.length);
16714
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
16715
+ }
16716
+ }
16717
+ this._rebuildLines();
16718
+ this._afterCursorMove();
16719
+ }
16720
+ _doFindNextOcurrence() {
16721
+ const primary = this.cursorSet.getPrimary();
16722
+ // Get the search text: selection or word under cursor
16723
+ let searchText = this.cursorSet.getSelectedText(this.doc);
16724
+ if (!searchText) {
16725
+ const { line, col } = primary.head;
16726
+ const [word, start, end] = this.doc.getWordAt(line, col);
16727
+ if (!word)
16728
+ return;
16729
+ // Select the word under cursor first (first Ctrl+D)
16730
+ primary.anchor = { line, col: start };
16731
+ primary.head = { line, col: end };
16732
+ this._renderCursors();
16733
+ this._renderSelections();
16734
+ return;
16735
+ }
16736
+ const lastCursor = this.cursorSet.cursors[this.cursorSet.cursors.length - 1];
16737
+ const lastEnd = posBefore(lastCursor.anchor, lastCursor.head) ? lastCursor.head : lastCursor.anchor;
16738
+ const match = this.doc.findNext(searchText, lastEnd.line, lastEnd.col);
16739
+ if (!match)
16740
+ return;
16741
+ // Check if this occurrence is already selected by any cursor
16742
+ const alreadySelected = this.cursorSet.cursors.some(sel => {
16743
+ const start = posBefore(sel.anchor, sel.head) ? sel.anchor : sel.head;
16744
+ return start.line === match.line && start.col === match.col;
16745
+ });
16746
+ if (alreadySelected)
16747
+ return;
16748
+ this.cursorSet.cursors.push({
16749
+ anchor: { line: match.line, col: match.col },
16750
+ head: { line: match.line, col: match.col + searchText.length }
16751
+ });
16752
+ this._renderCursors();
16753
+ this._renderSelections();
16754
+ this._scrollCursorIntoView();
16755
+ }
16756
+ _doOpenSearch(clear = false) {
16757
+ if (!this.searchBox)
16758
+ return;
16759
+ this._doHideSearch();
16760
+ exports.LX.addClass(this.searchBox, 'opened');
16761
+ this._isSearchBoxActive = true;
16762
+ const input = this.searchBox.querySelector('input');
16763
+ if (!input)
16764
+ return;
16765
+ if (clear) {
16766
+ input.value = '';
16767
+ }
16768
+ else if (this.cursorSet.hasSelection()) {
16769
+ input.value = this.cursorSet.getSelectedText(this.doc);
16770
+ }
16771
+ input.selectionStart = 0;
16772
+ input.selectionEnd = input.value.length;
16773
+ input.focus();
16774
+ }
16775
+ _doOpenLineSearch() {
16776
+ if (!this.searchLineBox)
16777
+ return;
16778
+ this._doHideSearch();
16779
+ exports.LX.emitSignal('@line-number-range', `Type a line number to go to (from 1 to ${this.doc.lineCount}).`);
16780
+ exports.LX.addClass(this.searchLineBox, 'opened');
16781
+ this._isSearchLineBoxActive = true;
16782
+ const input = this.searchLineBox.querySelector('input');
16783
+ if (!input)
16784
+ return;
16785
+ input.value = ':';
16786
+ input.focus();
16787
+ }
16788
+ /**
16789
+ * Returns true if visibility changed.
16790
+ */
16791
+ _doHideSearch() {
16792
+ if (!this.searchBox || !this.searchLineBox)
16793
+ return false;
16794
+ const active = this._isSearchBoxActive;
16795
+ const activeLine = this._isSearchLineBoxActive;
16796
+ if (active) {
16797
+ this.searchBox.classList.remove('opened');
16798
+ this._isSearchBoxActive = false;
16799
+ this._lastSearchPos = null;
16800
+ }
16801
+ else if (activeLine) {
16802
+ this.searchLineBox.classList.remove('opened');
16803
+ this._isSearchLineBoxActive = false;
16804
+ }
16805
+ return (active != this._isSearchBoxActive) || (activeLine != this._isSearchLineBoxActive);
16806
+ }
16807
+ _doSearch(text, reverse = false, callback, skipAlert = true, forceFocus = true) {
16808
+ text = text ?? this._lastTextFound;
16809
+ if (!text)
16810
+ return;
16811
+ const doc = this.doc;
16812
+ let startLine;
16813
+ let startCol;
16814
+ if (this._lastSearchPos) {
16815
+ startLine = this._lastSearchPos.line;
16816
+ startCol = this._lastSearchPos.col + (reverse ? -text.length : text.length);
16817
+ }
16818
+ else {
16819
+ const cursor = this.cursorSet.getPrimary();
16820
+ startLine = cursor.head.line;
16821
+ startCol = cursor.head.col;
16822
+ }
16823
+ const findInLine = (lineIdx, fromCol) => {
16824
+ let lineText = doc.getLine(lineIdx);
16825
+ let needle = text;
16826
+ if (!this._searchMatchCase) {
16827
+ lineText = lineText.toLowerCase();
16828
+ needle = needle.toLowerCase();
16829
+ }
16830
+ if (reverse) {
16831
+ const sub = lineText.substring(0, fromCol);
16832
+ return sub.lastIndexOf(needle);
16833
+ }
16834
+ else {
16835
+ return lineText.indexOf(needle, fromCol);
16836
+ }
16837
+ };
16838
+ let foundLine = -1;
16839
+ let foundCol = -1;
16840
+ if (reverse) {
16841
+ for (let j = startLine; j >= 0; j--) {
16842
+ const col = findInLine(j, j === startLine ? startCol : doc.getLine(j).length);
16843
+ if (col > -1) {
16844
+ foundLine = j;
16845
+ foundCol = col;
16846
+ break;
16847
+ }
16848
+ }
16849
+ // Wrap around from bottom
16850
+ if (foundLine === -1) {
16851
+ for (let j = doc.lineCount - 1; j > startLine; j--) {
16852
+ const col = findInLine(j, doc.getLine(j).length);
16853
+ if (col > -1) {
16854
+ foundLine = j;
16855
+ foundCol = col;
16856
+ break;
16857
+ }
16858
+ }
16859
+ }
16860
+ }
16861
+ else {
16862
+ for (let j = startLine; j < doc.lineCount; j++) {
16863
+ const col = findInLine(j, j === startLine ? startCol : 0);
16864
+ if (col > -1) {
16865
+ foundLine = j;
16866
+ foundCol = col;
16867
+ break;
16868
+ }
16869
+ }
16870
+ // Wrap around from top
16871
+ if (foundLine === -1) {
16872
+ for (let j = 0; j < startLine; j++) {
16873
+ const col = findInLine(j, 0);
16874
+ if (col > -1) {
16875
+ foundLine = j;
16876
+ foundCol = col;
16877
+ break;
16878
+ }
16879
+ }
16880
+ }
16881
+ }
16882
+ if (foundLine === -1) {
16883
+ if (!skipAlert)
16884
+ alert('No results!');
16885
+ this._lastSearchPos = null;
16886
+ return;
16887
+ }
16888
+ this._lastTextFound = text;
16889
+ this._lastSearchPos = { line: foundLine, col: foundCol };
16890
+ if (callback) {
16891
+ callback(foundCol, foundLine);
16892
+ }
16893
+ else {
16894
+ // Select the found text
16895
+ const primary = this.cursorSet.getPrimary();
16896
+ primary.anchor = { line: foundLine, col: foundCol };
16897
+ primary.head = { line: foundLine, col: foundCol + text.length };
16898
+ this._renderCursors();
16899
+ this._renderSelections();
16900
+ }
16901
+ // Scroll to the match
16902
+ this.codeScroller.scrollTop = Math.max((foundLine - 10) * this.lineHeight, 0);
16903
+ this.codeScroller.scrollLeft = Math.max(foundCol * this.charWidth - this.codeScroller.clientWidth / 2, 0);
16904
+ if (forceFocus) {
16905
+ const input = this.searchBox?.querySelector('input');
16906
+ input?.focus();
16907
+ }
16908
+ }
16909
+ _doGotoLine(lineNumber) {
16910
+ if (Number.isNaN(lineNumber) || lineNumber < 1 || lineNumber > this.doc.lineCount)
16911
+ return;
16912
+ this.cursorSet.set(lineNumber - 1, 0);
16913
+ this._afterCursorMove();
16914
+ }
16915
+ _encloseSelection(open, close) {
16916
+ const cursor = this.cursorSet.getPrimary();
16917
+ const sel = cursor.anchor;
16918
+ const head = cursor.head;
16919
+ // Normalize selection (old invertIfNecessary)
16920
+ const fromLine = sel.line < head.line || (sel.line === head.line && sel.col < head.col) ? sel : head;
16921
+ const toLine = fromLine === sel ? head : sel;
16922
+ // Only single-line selections for now
16923
+ if (fromLine.line !== toLine.line)
16924
+ return;
16925
+ const line = fromLine.line;
16926
+ const from = fromLine.col;
16927
+ const to = toLine.col;
16928
+ this._flushAction();
16929
+ const op1 = this.doc.insert(line, from, open);
16930
+ this.undoManager.record(op1, this.cursorSet.getCursorPositions());
16931
+ const op2 = this.doc.insert(line, to + 1, close);
16932
+ this.undoManager.record(op2, this.cursorSet.getCursorPositions());
16933
+ // Keep selection on the enclosed word (shifted by 1)
16934
+ cursor.anchor = { line, col: from + 1 };
16935
+ cursor.head = { line, col: to + 1 };
16936
+ this._updateLine(line);
16937
+ this._afterCursorMove();
16938
+ }
16939
+ _doBackspace(ctrlKey) {
16940
+ this._flushIfActionChanged('backspace');
16941
+ // If any cursor has a selection, delete selections
16942
+ if (this.cursorSet.hasSelection()) {
16943
+ this._deleteSelectionIfAny();
16944
+ this._rebuildLines();
16945
+ this._afterCursorMove();
16946
+ return;
16947
+ }
16948
+ for (const idx of this.cursorSet.sortedIndicesBottomUp()) {
16949
+ const cursor = this.cursorSet.cursors[idx];
16950
+ const { line, col } = cursor.head;
16951
+ if (line === 0 && col === 0)
16952
+ continue;
16953
+ if (col === 0) {
16954
+ // Merge with previous line
16955
+ const prevLineLen = this.doc.getLine(line - 1).length;
16956
+ const op = this.doc.delete(line - 1, prevLineLen, 1);
16957
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
16958
+ cursor.head = { line: line - 1, col: prevLineLen };
16959
+ cursor.anchor = { ...cursor.head };
16960
+ this.cursorSet.adjustOthers(idx, line, 0, prevLineLen, -1);
16961
+ }
16962
+ else if (ctrlKey) {
16963
+ // Delete word left
16964
+ const [word, from] = this.doc.getWordAt(line, col - 1);
16965
+ const deleteFrom = word.length > 0 ? from : col - 1;
16966
+ const deleteLen = col - deleteFrom;
16967
+ const op = this.doc.delete(line, deleteFrom, deleteLen);
16968
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
16969
+ cursor.head = { line, col: deleteFrom };
16970
+ cursor.anchor = { ...cursor.head };
16971
+ this.cursorSet.adjustOthers(idx, line, col, -deleteLen);
16972
+ }
16973
+ else {
16974
+ const op = this.doc.delete(line, col - 1, 1);
16975
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
16976
+ cursor.head = { line, col: col - 1 };
16977
+ cursor.anchor = { ...cursor.head };
16978
+ this.cursorSet.adjustOthers(idx, line, col, -1);
16979
+ }
16980
+ }
16981
+ this._rebuildLines();
16982
+ this._afterCursorMove();
16983
+ this._doOpenAutocomplete();
16984
+ }
16985
+ _doDelete(ctrlKey) {
16986
+ if (this.cursorSet.hasSelection()) {
16987
+ this._deleteSelectionIfAny();
16988
+ this._rebuildLines();
16989
+ this._afterCursorMove();
16990
+ return;
16991
+ }
16992
+ this._flushIfActionChanged('delete');
16993
+ for (const idx of this.cursorSet.sortedIndicesBottomUp()) {
16994
+ const cursor = this.cursorSet.cursors[idx];
16995
+ const { line, col } = cursor.head;
16996
+ const lineText = this.doc.getLine(line);
16997
+ if (col >= lineText.length && line >= this.doc.lineCount - 1)
16998
+ continue;
16999
+ if (col >= lineText.length) {
17000
+ // Merge with next line
17001
+ const op = this.doc.delete(line, col, 1);
17002
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17003
+ this.cursorSet.adjustOthers(idx, line, col, 0, -1);
17004
+ }
17005
+ else if (ctrlKey) {
17006
+ // Delete word right
17007
+ const [word, , end] = this.doc.getWordAt(line, col);
17008
+ const deleteLen = word.length > 0 ? end - col : 1;
17009
+ const op = this.doc.delete(line, col, deleteLen);
17010
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17011
+ this.cursorSet.adjustOthers(idx, line, col, -deleteLen);
17012
+ }
17013
+ else {
17014
+ const op = this.doc.delete(line, col, 1);
17015
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17016
+ this.cursorSet.adjustOthers(idx, line, col, -1);
17017
+ }
17018
+ }
17019
+ this._rebuildLines();
17020
+ this._afterCursorMove();
17021
+ }
17022
+ _doEnter(shift) {
17023
+ if (this._isAutoCompleteActive) {
17024
+ this._doAutocompleteWord();
17025
+ return;
17026
+ }
17027
+ if (shift && this.onRun) {
17028
+ this.onRun(this.getText(), this);
17029
+ return;
17030
+ }
17031
+ this._deleteSelectionIfAny();
17032
+ this._flushAction();
17033
+ for (const idx of this.cursorSet.sortedIndicesBottomUp()) {
17034
+ const cursor = this.cursorSet.cursors[idx];
17035
+ const { line, col } = cursor.head;
17036
+ const indent = this.doc.getIndent(line);
17037
+ const spaces = ' '.repeat(indent);
17038
+ const op = this.doc.insert(line, col, '\n' + spaces);
17039
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17040
+ cursor.head = { line: line + 1, col: indent };
17041
+ cursor.anchor = { ...cursor.head };
17042
+ this.cursorSet.adjustOthers(idx, line, col, 0, 1);
17043
+ }
17044
+ this._rebuildLines();
17045
+ this._afterCursorMove();
17046
+ }
17047
+ _doTab(shift) {
17048
+ if (this._isAutoCompleteActive) {
17049
+ this._doAutocompleteWord();
17050
+ return;
17051
+ }
17052
+ this._flushAction();
17053
+ for (const idx of this.cursorSet.sortedIndicesBottomUp()) {
17054
+ const cursor = this.cursorSet.cursors[idx];
17055
+ const { line, col } = cursor.head;
17056
+ if (shift) {
17057
+ // Dedent: remove up to tabSize spaces from start
17058
+ const lineText = this.doc.getLine(line);
17059
+ let spacesToRemove = 0;
17060
+ while (spacesToRemove < this.tabSize && spacesToRemove < lineText.length && lineText[spacesToRemove] === ' ') {
17061
+ spacesToRemove++;
17062
+ }
17063
+ if (spacesToRemove > 0) {
17064
+ const op = this.doc.delete(line, 0, spacesToRemove);
17065
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17066
+ cursor.head = { line, col: Math.max(0, col - spacesToRemove) };
17067
+ cursor.anchor = { ...cursor.head };
17068
+ this.cursorSet.adjustOthers(idx, line, 0, -spacesToRemove);
17069
+ }
17070
+ }
17071
+ else {
17072
+ const spacesToAdd = this.tabSize - (col % this.tabSize);
17073
+ const spaces = ' '.repeat(spacesToAdd);
17074
+ const op = this.doc.insert(line, col, spaces);
17075
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17076
+ cursor.head = { line, col: col + spacesToAdd };
17077
+ cursor.anchor = { ...cursor.head };
17078
+ this.cursorSet.adjustOthers(idx, line, col, spacesToAdd);
17079
+ }
17080
+ }
17081
+ this._rebuildLines();
17082
+ this._afterCursorMove();
17083
+ }
17084
+ _deleteSelectionIfAny() {
17085
+ let anyDeleted = false;
17086
+ for (const idx of this.cursorSet.sortedIndicesBottomUp()) {
17087
+ const sel = this.cursorSet.cursors[idx];
17088
+ if (selectionIsEmpty(sel))
17089
+ continue;
17090
+ const start = selectionStart(sel);
17091
+ const end = selectionEnd(sel);
17092
+ const selectedText = this.cursorSet.getSelectedText(this.doc, idx);
17093
+ if (!selectedText)
17094
+ continue;
17095
+ const linesRemoved = end.line - start.line;
17096
+ // not exact for multiline, but start.col is where cursor lands
17097
+ const colDelta = linesRemoved === 0 ? (end.col - start.col) : start.col;
17098
+ const op = this.doc.delete(start.line, start.col, selectedText.length);
17099
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17100
+ sel.head = { ...start };
17101
+ sel.anchor = { ...start };
17102
+ this.cursorSet.adjustOthers(idx, start.line, start.col, -colDelta, -linesRemoved);
17103
+ anyDeleted = true;
17104
+ }
17105
+ if (anyDeleted)
17106
+ this._rebuildLines();
17107
+ }
17108
+ // Clipboard helpers:
17109
+ _doCopy() {
17110
+ const text = this.cursorSet.getSelectedText(this.doc);
17111
+ if (text) {
17112
+ navigator.clipboard.writeText(text);
17113
+ }
17114
+ }
17115
+ _doCut() {
17116
+ this._flushAction();
17117
+ const text = this.cursorSet.getSelectedText(this.doc);
17118
+ if (text) {
17119
+ navigator.clipboard.writeText(text);
17120
+ this._deleteSelectionIfAny();
17121
+ }
17122
+ else {
17123
+ const cursor = this.cursorSet.getPrimary();
17124
+ const line = cursor.head.line;
17125
+ const lineText = this.doc.getLine(line);
17126
+ const isLastLine = line === this.doc.lineCount - 1;
17127
+ navigator.clipboard.writeText(lineText + (isLastLine ? '' : '\n'));
17128
+ const op = this.doc.removeLine(line);
17129
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17130
+ // Place cursor at col 0 of the resulting line
17131
+ const newLine = Math.min(line, this.doc.lineCount - 1);
17132
+ cursor.head = { line: newLine, col: 0 };
17133
+ cursor.anchor = { ...cursor.head };
17134
+ }
17135
+ this._rebuildLines();
17136
+ this._afterCursorMove();
17137
+ }
17138
+ _swapLine(dir) {
17139
+ const cursor = this.cursorSet.getPrimary();
17140
+ const line = cursor.head.line;
17141
+ const targetLine = line + dir;
17142
+ if (targetLine < 0 || targetLine >= this.doc.lineCount)
17143
+ return;
17144
+ const currentText = this.doc.getLine(line);
17145
+ const targetText = this.doc.getLine(targetLine);
17146
+ this._flushAction();
17147
+ const op1 = this.doc.replaceLine(line, targetText);
17148
+ this.undoManager.record(op1, this.cursorSet.getCursorPositions());
17149
+ const op2 = this.doc.replaceLine(targetLine, currentText);
17150
+ this.undoManager.record(op2, this.cursorSet.getCursorPositions());
17151
+ cursor.head = { line: targetLine, col: cursor.head.col };
17152
+ cursor.anchor = { ...cursor.head };
17153
+ this._rebuildLines();
17154
+ this._afterCursorMove();
17155
+ }
17156
+ _duplicateLine(dir) {
17157
+ const cursor = this.cursorSet.getPrimary();
17158
+ const line = cursor.head.line;
17159
+ const text = this.doc.getLine(line);
17160
+ this._flushAction();
17161
+ const op = this.doc.insertLine(line, text);
17162
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17163
+ const newLine = dir === 1 ? line + 1 : line;
17164
+ cursor.head = { line: newLine, col: cursor.head.col };
17165
+ cursor.anchor = { ...cursor.head };
17166
+ this._rebuildLines();
17167
+ this._afterCursorMove();
17168
+ }
17169
+ async _doPaste() {
17170
+ const text = await navigator.clipboard.readText();
17171
+ if (!text)
17172
+ return;
17173
+ this._flushAction();
17174
+ this._deleteSelectionIfAny();
17175
+ const cursor = this.cursorSet.getPrimary();
17176
+ const op = this.doc.insert(cursor.head.line, cursor.head.col, text);
17177
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17178
+ // Calculate new cursor position after paste
17179
+ const lines = text.split('\n');
17180
+ if (lines.length === 1) {
17181
+ this.cursorSet.set(cursor.head.line, cursor.head.col + text.length);
17182
+ }
17183
+ else {
17184
+ this.cursorSet.set(cursor.head.line + lines.length - 1, lines[lines.length - 1].length);
17185
+ }
17186
+ this._rebuildLines();
17187
+ this._afterCursorMove();
17188
+ }
17189
+ // Undo/Redo:
17190
+ _doUndo() {
17191
+ const result = this.undoManager.undo(this.doc, this.cursorSet.getCursorPositions());
17192
+ if (result) {
17193
+ if (result.cursors.length > 0) {
17194
+ const c = result.cursors[0];
17195
+ this.cursorSet.set(c.line, c.col);
17196
+ }
17197
+ this._rebuildLines();
17198
+ this._afterCursorMove();
17199
+ }
17200
+ }
17201
+ _doRedo() {
17202
+ const result = this.undoManager.redo(this.doc);
17203
+ if (result) {
17204
+ if (result.cursors.length > 0) {
17205
+ const c = result.cursors[0];
17206
+ this.cursorSet.set(c.line, c.col);
17207
+ }
17208
+ this._rebuildLines();
17209
+ this._afterCursorMove();
17210
+ }
17211
+ }
17212
+ // Mouse input events:
17213
+ _onMouseDown(e) {
17214
+ if (!this.currentTab)
17215
+ return;
17216
+ if (this.searchBox && this.searchBox.contains(e.target))
17217
+ return;
17218
+ if (this.autocomplete && this.autocomplete.contains(e.target))
17219
+ return;
17220
+ e.preventDefault(); // Prevent browser from stealing focus from _inputArea
17221
+ this._wasPaired = false;
17222
+ // Calculate line and column from click position
17223
+ const rect = this.codeContainer.getBoundingClientRect();
17224
+ const x = e.clientX - rect.left - this.xPadding;
17225
+ const y = e.clientY - rect.top;
17226
+ const line = exports.LX.clamp(Math.floor(y / this.lineHeight), 0, this.doc.lineCount - 1);
17227
+ const col = exports.LX.clamp(Math.round(x / this.charWidth), 0, this.doc.getLine(line).length);
17228
+ if (e.type === 'contextmenu') {
17229
+ this._onContextMenu(e, line, col);
17230
+ return;
17231
+ }
17232
+ if (e.button !== 0)
17233
+ return;
17234
+ const now = Date.now();
17235
+ if (now - this._lastClickTime < 400 && line === this._lastClickLine) {
17236
+ this._clickCount = Math.min(this._clickCount + 1, 3);
17237
+ }
17238
+ else {
17239
+ this._clickCount = 1;
17240
+ }
17241
+ this._lastClickTime = now;
17242
+ this._lastClickLine = line;
17243
+ // Triple click: select entire line
17244
+ if (this._clickCount === 3) {
17245
+ const sel = this.cursorSet.getPrimary();
17246
+ sel.anchor = { line, col: 0 };
17247
+ sel.head = { line, col: this.doc.getLine(line).length };
17248
+ }
17249
+ // Double click: select word
17250
+ else if (this._clickCount === 2) {
17251
+ const [, start, end] = this.doc.getWordAt(line, col);
17252
+ const sel = this.cursorSet.getPrimary();
17253
+ sel.anchor = { line, col: start };
17254
+ sel.head = { line, col: end };
17255
+ }
17256
+ else if (e.shiftKey) {
17257
+ // Extend selection
17258
+ const sel = this.cursorSet.getPrimary();
17259
+ sel.head = { line, col };
17260
+ }
17261
+ else {
17262
+ this.cursorSet.set(line, col);
17263
+ }
17264
+ this._afterCursorMove();
17265
+ this._inputArea.focus();
17266
+ // Track mouse for drag selection
17267
+ const onMouseMove = (me) => {
17268
+ const mx = me.clientX - rect.left - this.xPadding;
17269
+ const my = me.clientY - rect.top;
17270
+ const ml = Math.max(0, Math.min(Math.floor(my / this.lineHeight), this.doc.lineCount - 1));
17271
+ const mc = Math.max(0, Math.min(Math.round(mx / this.charWidth), this.doc.getLine(ml).length));
17272
+ const sel = this.cursorSet.getPrimary();
17273
+ sel.head = { line: ml, col: mc };
17274
+ this._renderCursors();
17275
+ this._renderSelections();
17276
+ };
17277
+ const onMouseUp = () => {
17278
+ document.removeEventListener('mousemove', onMouseMove);
17279
+ document.removeEventListener('mouseup', onMouseUp);
17280
+ };
17281
+ document.addEventListener('mousemove', onMouseMove);
17282
+ document.addEventListener('mouseup', onMouseUp);
17283
+ }
17284
+ _onContextMenu(e, line, col) {
17285
+ e.preventDefault();
17286
+ // if ( !this.canOpenContextMenu )
17287
+ // {
17288
+ // return;
17289
+ // }
17290
+ const dmOptions = [{ name: 'Copy', icon: 'Copy', callback: () => this._doCopy(), kbd: ['Ctrl', 'C'], useKbdSpecialKeys: false }];
17291
+ if (!this.disableEdition) {
17292
+ dmOptions.push({ name: 'Cut', icon: 'Scissors', callback: () => this._doCut(), kbd: ['Ctrl', 'X'], useKbdSpecialKeys: false });
17293
+ dmOptions.push({ name: 'Paste', icon: 'Paste', callback: () => this._doPaste(), kbd: ['Ctrl', 'V'], useKbdSpecialKeys: false });
17294
+ }
17295
+ if (this.onContextMenu) {
17296
+ const content = this.cursorSet.getSelectedText(this.doc);
17297
+ const options = this.onContextMenu(this, content, e);
17298
+ if (options?.length) {
17299
+ dmOptions.push(null); // Separator
17300
+ for (const o of options) {
17301
+ dmOptions.push({ name: o.path, disabled: o.disabled, callback: o.callback });
17302
+ }
17303
+ }
17304
+ }
17305
+ exports.LX.addDropdownMenu(e.target, dmOptions, { event: e, side: 'bottom', align: 'start' });
17306
+ }
17307
+ // Autocomplete:
17308
+ /**
17309
+ * Get word at cursor position for autocomplete.
17310
+ */
17311
+ _getWordAtCursor() {
17312
+ const cursor = this.cursorSet.getPrimary().head;
17313
+ const line = this.doc.getLine(cursor.line);
17314
+ let start = cursor.col;
17315
+ let end = cursor.col;
17316
+ // Find word boundaries
17317
+ while (start > 0 && /[\w$]/.test(line[start - 1]))
17318
+ start--;
17319
+ while (end < line.length && /[\w$]/.test(line[end]))
17320
+ end++;
17321
+ return { word: line.slice(start, end), start, end };
17322
+ }
17323
+ /**
17324
+ * Open autocomplete box with suggestions from symbols and custom suggestions.
17325
+ */
17326
+ _doOpenAutocomplete() {
17327
+ if (!this.autocomplete || !this.useAutoComplete)
17328
+ return;
17329
+ this.autocomplete.innerHTML = ''; // Clear all suggestions
17330
+ const { word } = this._getWordAtCursor();
17331
+ if (!word || word.length === 0) {
17332
+ this._doHideAutocomplete();
17333
+ return;
17334
+ }
17335
+ const suggestions = [];
17336
+ const added = new Set();
17337
+ const addSuggestion = (label, kind, scope, detail) => {
17338
+ if (!added.has(label)) {
17339
+ suggestions.push({ label, kind, scope, detail });
17340
+ added.add(label);
17341
+ }
17342
+ };
17343
+ // Get first suggestions from symbol table
17344
+ const allSymbols = this.symbolTable.getAllSymbols();
17345
+ for (const symbol of allSymbols) {
17346
+ if (symbol.name.toLowerCase().startsWith(word.toLowerCase())) {
17347
+ addSuggestion(symbol.name, symbol.kind, symbol.scope, `${symbol.kind} in ${symbol.scope}`);
17348
+ }
17349
+ }
17350
+ // Add language reserved keys
17351
+ for (const reservedWord of this.language.reservedWords) {
17352
+ if (reservedWord.toLowerCase().startsWith(word.toLowerCase())) {
17353
+ addSuggestion(reservedWord);
17354
+ }
17355
+ }
17356
+ // Add custom suggestions
17357
+ for (const suggestion of this.customSuggestions) {
17358
+ const label = typeof suggestion === 'string' ? suggestion : suggestion.label;
17359
+ const kind = typeof suggestion === 'object' ? suggestion.kind : undefined;
17360
+ const detail = typeof suggestion === 'object' ? suggestion.detail : undefined;
17361
+ if (label.toLowerCase().startsWith(word.toLowerCase())) {
17362
+ addSuggestion(label, kind, undefined, detail);
17363
+ }
17364
+ }
17365
+ // Close autocomplete if no suggestions
17366
+ if (suggestions.length === 0) {
17367
+ this._doHideAutocomplete();
17368
+ return;
17369
+ }
17370
+ // Sort suggestions: exact matches first, then alphabetically
17371
+ suggestions.sort((a, b) => {
17372
+ const aExact = a.label.toLowerCase() === word.toLowerCase() ? 0 : 1;
17373
+ const bExact = b.label.toLowerCase() === word.toLowerCase() ? 0 : 1;
17374
+ if (aExact !== bExact)
17375
+ return aExact - bExact;
17376
+ return a.label.localeCompare(b.label);
17377
+ });
17378
+ this._selectedAutocompleteIndex = 0;
17379
+ // Render suggestions
17380
+ suggestions.forEach((suggestion, index) => {
17381
+ const item = document.createElement('pre');
17382
+ if (index === this._selectedAutocompleteIndex)
17383
+ item.classList.add('selected');
17384
+ const currSuggestion = suggestion.label;
17385
+ let iconName = 'CaseLower';
17386
+ let iconClass = 'foo';
17387
+ switch (suggestion.kind) {
17388
+ case 'class':
17389
+ iconName = 'CircleNodes';
17390
+ iconClass = 'text-orange-500';
17391
+ break;
17392
+ case 'struct':
17393
+ iconName = 'Form';
17394
+ iconClass = 'text-orange-400';
17395
+ break;
17396
+ case 'interface':
17397
+ iconName = 'FileType';
17398
+ iconClass = 'text-cyan-500';
17399
+ break;
17400
+ case 'enum':
17401
+ iconName = 'ListTree';
17402
+ iconClass = 'text-yellow-500';
17403
+ break;
17404
+ case 'enum-value':
17405
+ iconName = 'Dot';
17406
+ iconClass = 'text-yellow-400';
17407
+ break;
17408
+ case 'type':
17409
+ iconName = 'Type';
17410
+ iconClass = 'text-teal-500';
17411
+ break;
17412
+ case 'function':
17413
+ iconName = 'Function';
17414
+ iconClass = 'text-purple-500';
17415
+ break;
17416
+ case 'method':
17417
+ iconName = 'Box';
17418
+ iconClass = 'text-fuchsia-500';
17419
+ break;
17420
+ case 'variable':
17421
+ iconName = 'Cuboid';
17422
+ iconClass = 'text-blue-400';
17423
+ break;
17424
+ case 'property':
17425
+ iconName = 'Layers';
17426
+ iconClass = 'text-blue-300';
17427
+ break;
17428
+ case 'constructor-call':
17429
+ iconName = 'Hammer';
17430
+ iconClass = 'text-green-500';
17431
+ break;
17432
+ case 'method-call':
17433
+ iconName = 'PlayCircle';
17434
+ iconClass = 'text-gray-400';
17435
+ break;
17436
+ default:
17437
+ iconName = 'CaseLower';
17438
+ iconClass = 'text-gray-500';
17439
+ break;
17440
+ }
17441
+ item.appendChild(exports.LX.makeIcon(iconName, { iconClass: 'ml-1 mr-2', svgClass: 'sm ' + iconClass }));
17442
+ // Highlight the written part
17443
+ const hIndex = currSuggestion.toLowerCase().indexOf(word.toLowerCase());
17444
+ var preWord = document.createElement('span');
17445
+ preWord.textContent = currSuggestion.substring(0, hIndex);
17446
+ item.appendChild(preWord);
17447
+ var actualWord = document.createElement('span');
17448
+ actualWord.textContent = currSuggestion.substring(hIndex, hIndex + word.length);
17449
+ actualWord.classList.add('word-highlight');
17450
+ item.appendChild(actualWord);
17451
+ var postWord = document.createElement('span');
17452
+ postWord.textContent = currSuggestion.substring(hIndex + word.length);
17453
+ item.appendChild(postWord);
17454
+ if (suggestion.kind) {
17455
+ const kind = document.createElement('span');
17456
+ kind.textContent = ` (${suggestion.kind})`;
17457
+ kind.className = 'kind text-muted-foreground text-xs! ml-2';
17458
+ item.appendChild(kind);
17459
+ }
17460
+ item.addEventListener('click', () => {
17461
+ this._doAutocompleteWord();
17462
+ });
17463
+ this.autocomplete.appendChild(item);
17464
+ });
17465
+ this._isAutoCompleteActive = true;
17466
+ const handleClick = (e) => {
17467
+ if (!this.autocomplete?.contains(e.target)) {
17468
+ this._doHideAutocomplete();
17469
+ }
17470
+ };
17471
+ setTimeout(() => document.addEventListener('click', handleClick, { once: true }), 0);
17472
+ // Store cleanup function
17473
+ this.autocomplete._cleanup = () => {
17474
+ document.removeEventListener('click', handleClick);
17475
+ };
17476
+ // Prepare autocomplete ui
17477
+ this.autocomplete.classList.toggle('show', true);
17478
+ this.autocomplete.classList.toggle('no-scrollbar', !(this.autocomplete.scrollHeight > this.autocomplete.offsetHeight));
17479
+ const cursor = this.cursorSet.getPrimary().head;
17480
+ const left = cursor.col * this.charWidth + this.xPadding;
17481
+ const top = (cursor.line + 1) * this.lineHeight + this._cachedTabsHeight - this.codeScroller.scrollTop;
17482
+ this.autocomplete.style.left = left + 'px';
17483
+ this.autocomplete.style.top = top + 'px';
17484
+ }
17485
+ _doHideAutocomplete() {
17486
+ if (!this.autocomplete || !this._isAutoCompleteActive)
17487
+ return;
17488
+ this.autocomplete.innerHTML = ''; // Clear all suggestions
17489
+ this.autocomplete.classList.remove('show');
17490
+ this._isAutoCompleteActive = false;
17491
+ if (this.autocomplete._cleanup) {
17492
+ this.autocomplete._cleanup();
17493
+ delete this.autocomplete._cleanup;
17494
+ }
17495
+ }
17496
+ /**
17497
+ * Insert the selected autocomplete word at cursor.
17498
+ */
17499
+ _doAutocompleteWord() {
17500
+ const word = this._getSelectedAutoCompleteWord();
17501
+ if (!word)
17502
+ return;
17503
+ const cursor = this.cursorSet.getPrimary().head;
17504
+ const { start, end } = this._getWordAtCursor();
17505
+ const line = cursor.line;
17506
+ const cursorsBefore = this.cursorSet.getCursorPositions();
17507
+ if (end > start) {
17508
+ const deleteOp = this.doc.delete(line, start, end - start);
17509
+ this.undoManager.record(deleteOp, cursorsBefore);
17510
+ }
17511
+ const insertOp = this.doc.insert(line, start, word);
17512
+ this.cursorSet.set(line, start + word.length);
17513
+ const cursorsAfter = this.cursorSet.getCursorPositions();
17514
+ this.undoManager.record(insertOp, cursorsAfter);
17515
+ this._rebuildLines();
17516
+ this._afterCursorMove();
17517
+ this._doHideAutocomplete();
17518
+ }
17519
+ _getSelectedAutoCompleteWord() {
17520
+ if (!this.autocomplete || !this._isAutoCompleteActive)
17521
+ return null;
17522
+ const pre = this.autocomplete.childNodes[this._selectedAutocompleteIndex];
17523
+ var word = '';
17524
+ for (let childSpan of pre.childNodes) {
17525
+ const span = childSpan;
17526
+ if (span.constructor != HTMLSpanElement || span.classList.contains('kind')) {
17527
+ continue;
17528
+ }
17529
+ word += span.textContent;
17530
+ }
17531
+ return word;
17532
+ }
17533
+ _afterCursorMove() {
17534
+ this._renderCursors();
17535
+ this._renderSelections();
17536
+ this._resetBlinker();
17537
+ this.resize();
17538
+ this._scrollCursorIntoView();
17539
+ }
17540
+ // Scrollbar & Resize:
17541
+ _scrollCursorIntoView() {
17542
+ const cursor = this.cursorSet.getPrimary().head;
17543
+ const top = cursor.line * this.lineHeight;
17544
+ const left = cursor.col * this.charWidth;
17545
+ // Vertical scroll
17546
+ if (top < this.codeScroller.scrollTop) {
17547
+ this.codeScroller.scrollTop = top;
17548
+ }
17549
+ else if (top + this.lineHeight > this.codeScroller.scrollTop + this.codeScroller.clientHeight) {
17550
+ this.codeScroller.scrollTop = top + this.lineHeight - this.codeScroller.clientHeight;
17551
+ }
17552
+ // Horizontal scroll
17553
+ const sbOffset = ScrollBar.SIZE * 2;
17554
+ if (left < this.codeScroller.scrollLeft) {
17555
+ this.codeScroller.scrollLeft = left;
17556
+ }
17557
+ else if (left + sbOffset > this.codeScroller.scrollLeft + this.codeScroller.clientWidth - this.xPadding) {
17558
+ this.codeScroller.scrollLeft = left + sbOffset - this.codeScroller.clientWidth + this.xPadding;
17559
+ }
17560
+ }
17561
+ _resetGutter() {
17562
+ // Use cached value or compute if not available (e.g., on initial load)
17563
+ const tabsHeight = this._cachedTabsHeight || (this.tabs?.root.getBoundingClientRect().height ?? 0);
17564
+ this.lineGutter.style.height = `calc(100% - ${tabsHeight}px)`;
17565
+ }
17566
+ getMaxLineLength() {
17567
+ if (!this.currentTab)
17568
+ return 0;
17569
+ let max = 0;
17570
+ for (let i = 0; i < this.doc.lineCount; i++) {
17571
+ const len = this.doc.getLine(i).length;
17572
+ if (len > max)
17573
+ max = len;
17574
+ }
17575
+ return max;
17576
+ }
17577
+ resize(force = false) {
17578
+ if (!this.charWidth)
17579
+ return;
17580
+ // Cache layout measurements to avoid reflows
17581
+ this._cachedTabsHeight = this.tabs?.root.getBoundingClientRect().height ?? 0;
17582
+ this._cachedStatusPanelHeight = this.statusPanel?.root.getBoundingClientRect().height ?? 0;
17583
+ const maxLineLength = this.getMaxLineLength();
17584
+ const lineCount = this.currentTab ? this.doc.lineCount : 0;
17585
+ const viewportChars = Math.floor((this.codeScroller.clientWidth - this.xPadding) / this.charWidth);
17586
+ const viewportLines = Math.floor(this.codeScroller.clientHeight / this.lineHeight);
17587
+ let needsHResize = maxLineLength !== this._lastMaxLineLength
17588
+ && (maxLineLength >= viewportChars || this._lastMaxLineLength >= viewportChars);
17589
+ let needsVResize = lineCount !== this._lastLineCount
17590
+ && (lineCount >= viewportLines || this._lastLineCount >= viewportLines);
17591
+ // If doesn't need resize due to not reaching min length, maybe we need to resize if the content shrinks and we have extra space now
17592
+ needsHResize = needsHResize || (maxLineLength < viewportChars && this.hScrollbar?.visible);
17593
+ needsVResize = needsVResize || (lineCount < viewportLines && this.vScrollbar?.visible);
17594
+ if (!force && !needsHResize && !needsVResize)
17595
+ return;
17596
+ this._lastMaxLineLength = maxLineLength;
17597
+ this._lastLineCount = lineCount;
17598
+ if (force || needsHResize) {
17599
+ this.codeSizer.style.minWidth = (maxLineLength * this.charWidth + this.xPadding + ScrollBar.SIZE * 2) + 'px';
17600
+ }
17601
+ if (force || needsVResize) {
17602
+ this.codeSizer.style.minHeight = (lineCount * this.lineHeight + ScrollBar.SIZE * 2) + 'px';
17603
+ }
17604
+ this._resetGutter();
17605
+ setTimeout(() => this._resizeScrollBars(), 10);
17606
+ }
17607
+ _resizeScrollBars() {
17608
+ if (!this.vScrollbar)
17609
+ return;
17610
+ // Use cached offsets to avoid reflows
17611
+ const topOffset = this._cachedTabsHeight;
17612
+ const bottomOffset = this._cachedStatusPanelHeight;
17613
+ // Vertical scrollbar: right edge, between tabs and status bar
17614
+ const scrollHeight = this.codeScroller.scrollHeight;
17615
+ this.vScrollbar.setThumbRatio(scrollHeight > 0 ? this.codeScroller.clientHeight / scrollHeight : 1);
17616
+ this.vScrollbar.root.style.top = topOffset + 'px';
17617
+ this.vScrollbar.root.style.height = `calc(100% - ${topOffset + bottomOffset}px)`;
17618
+ // Horizontal scrollbar: bottom of code area, offset by gutter and vertical scrollbar
17619
+ const scrollWidth = this.codeScroller.scrollWidth;
17620
+ this.hScrollbar.setThumbRatio(scrollWidth > 0 ? this.codeScroller.clientWidth / scrollWidth : 1);
17621
+ this.hScrollbar.root.style.bottom = bottomOffset + 'px';
17622
+ this.hScrollbar.root.style.width = `calc(100% - ${this.xPadding + (this.vScrollbar.visible ? ScrollBar.SIZE : 0)}px)`;
17623
+ }
17624
+ _syncScrollBars() {
17625
+ if (!this.vScrollbar)
17626
+ return;
17627
+ this.vScrollbar.syncToScroll(this.codeScroller.scrollTop, this.codeScroller.scrollHeight - this.codeScroller.clientHeight);
17628
+ this.hScrollbar.syncToScroll(this.codeScroller.scrollLeft, this.codeScroller.scrollWidth - this.codeScroller.clientWidth);
17629
+ }
17630
+ // Files:
17631
+ _doLoadFromFile() {
17632
+ const input = exports.LX.makeElement('input', '', '', document.body);
17633
+ input.type = 'file';
17634
+ input.click();
17635
+ input.addEventListener('change', (e) => {
17636
+ const target = e.target;
17637
+ if (target.files && target.files[0]) {
17638
+ this.loadFile(target.files[0]);
17639
+ }
17640
+ input.remove();
17641
+ });
17642
+ }
17643
+ // Font Size utils:
17644
+ async _setFontSize(size, updateDOM = true) {
17645
+ // Change font size
17646
+ this.fontSize = size;
17647
+ const r = document.querySelector(':root');
17648
+ r.style.setProperty('--code-editor-font-size', `${this.fontSize}px`);
17649
+ window.localStorage.setItem('lexcodeeditor-font-size', `${this.fontSize}`);
17650
+ await this._measureChar();
17651
+ // Change row size
17652
+ const rowPixels = this.fontSize + 6;
17653
+ r.style.setProperty('--code-editor-row-height', `${rowPixels}px`);
17654
+ this.lineHeight = rowPixels;
17655
+ if (updateDOM) {
17656
+ this._rebuildLines();
17657
+ this._afterCursorMove();
17658
+ }
17659
+ // Emit event
17660
+ exports.LX.emitSignal('@font-size', this.fontSize);
17661
+ }
17662
+ _applyFontSizeOffset(offset = 0) {
17663
+ const newFontSize = exports.LX.clamp(this.fontSize + offset, CodeEditor.CODE_MIN_FONT_SIZE, CodeEditor.CODE_MAX_FONT_SIZE);
17664
+ this._setFontSize(newFontSize);
17665
+ }
17666
+ _increaseFontSize() {
17667
+ this._applyFontSizeOffset(1);
17668
+ }
17669
+ _decreaseFontSize() {
17670
+ this._applyFontSizeOffset(-1);
17671
+ }
17672
+ }
17673
+ exports.LX.CodeEditor = CodeEditor;
17674
+
17675
+ // Core.ts @jxarco
17676
+ /**
17677
+ * @method init
17678
+ * @param {Object} options
17679
+ * autoTheme: Use theme depending on browser-system default theme [true]
17680
+ * container: Root location for the gui (default is the document body)
17681
+ * id: Id of the main area
17682
+ * rootClass: Extra class to the root container
17683
+ * skipRoot: Skip adding LX root container
17684
+ * skipDefaultArea: Skip creation of main area
17685
+ * layoutMode: Sets page layout mode (document | app)
17686
+ * spacingMode: Sets page layout spacing mode (default | compact)
17687
+ */
17688
+ exports.LX.init = async function (options = {}) {
17689
+ if (this.ready) {
17690
+ return this.mainArea;
17691
+ }
17692
+ await exports.LX.loadScriptSync('https://unpkg.com/lucide@latest');
17693
+ // LexGUI root
17694
+ console.log(`LexGUI v${this.version}`);
17695
+ const root = exports.LX.makeElement('div', exports.LX.mergeClass('lexcontainer', options.rootClass));
17696
+ root.id = 'lexroot';
17697
+ root.tabIndex = -1;
17698
+ this.modal = exports.LX.makeElement('div', 'inset-0 hidden-opacity bg-black/50 fixed z-100 transition-opacity duration-100 ease-in');
17699
+ this.modal.id = 'modal';
17700
+ this.modal.toggle = function (force) {
17701
+ this.classList.toggle('hidden-opacity', force);
17702
+ };
17703
+ function blockScroll(e) {
17704
+ e.preventDefault();
17705
+ e.stopPropagation();
17706
+ }
17707
+ this.modal.addEventListener('wheel', blockScroll, { passive: false });
17708
+ this.modal.addEventListener('touchmove', blockScroll, { passive: false });
17709
+ this.root = root;
17710
+ this.container = document.body;
17711
+ if (options.container) {
17712
+ this.container = options.container.constructor === String
17713
+ ? document.getElementById(options.container)
17714
+ : options.container;
17715
+ }
17716
+ this.layoutMode = options.layoutMode ?? 'app';
17717
+ document.documentElement.setAttribute('data-layout', this.layoutMode);
17718
+ if (this.layoutMode == 'document') ;
17719
+ this.spacingMode = options.spacingMode ?? 'default';
17720
+ document.documentElement.setAttribute('data-spacing', this.spacingMode);
17721
+ this.container.appendChild(this.modal);
17722
+ if (!options.skipRoot) {
17723
+ this.container.appendChild(root);
17724
+ }
17725
+ else {
17726
+ this.root = document.body;
17727
+ }
17728
+ // Notifications
17729
+ {
17730
+ const notifSection = document.createElement('section');
17731
+ notifSection.className = 'notifications';
17732
+ this.notifications = document.createElement('ol');
17733
+ this.notifications.className = 'fixed flex flex-col-reverse m-0 p-0 gap-1 z-1000';
17734
+ this.notifications.iWidth = 0;
17735
+ notifSection.appendChild(this.notifications);
17736
+ document.body.appendChild(notifSection);
17737
+ this.notifications.addEventListener('mouseenter', () => {
17738
+ this.notifications.classList.add('list');
17739
+ });
17740
+ this.notifications.addEventListener('mouseleave', () => {
17741
+ this.notifications.classList.remove('list');
17742
+ });
17743
+ }
17744
+ // Disable drag icon
17745
+ root.addEventListener('dragover', function (e) {
17746
+ e.preventDefault();
17747
+ }, false);
17748
+ document.addEventListener('contextmenu', function (e) {
17749
+ e.preventDefault();
17750
+ }, false);
17751
+ // Global vars
17752
+ this.DEFAULT_NAME_WIDTH = '30%';
17753
+ this.DEFAULT_SPLITBAR_SIZE = 4;
17754
+ this.OPEN_CONTEXTMENU_ENTRY = 'click';
17755
+ this.componentResizeObserver = new ResizeObserver((entries) => {
17756
+ for (const entry of entries) {
17757
+ const c = entry.target?.jsInstance;
17758
+ if (c && c.onResize) {
17759
+ c.onResize(entry.contentRect);
17760
+ }
17761
+ }
17762
+ });
17763
+ this.ready = true;
17764
+ this.menubars = [];
17765
+ this.sidebars = [];
17766
+ this.commandbar = this._createCommandbar(this.container);
17767
+ if (!options.skipRoot && !options.skipDefaultArea) {
17768
+ this.mainArea = new Area({ id: options.id ?? 'mainarea' });
17769
+ }
17770
+ // Initial or automatic changes don't force color scheme
17771
+ // to be stored in localStorage
17772
+ this._onChangeSystemTheme = function (event) {
17773
+ const storedcolorScheme = localStorage.getItem('lxColorScheme');
17774
+ if (storedcolorScheme)
17775
+ return;
17776
+ exports.LX.setMode(event.matches ? 'dark' : 'light', false);
17777
+ };
17778
+ this._mqlPrefersDarkScheme = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
17779
+ const storedcolorScheme = localStorage.getItem('lxColorScheme');
17780
+ if (storedcolorScheme) {
17781
+ exports.LX.setMode(storedcolorScheme);
17782
+ }
17783
+ else if (this._mqlPrefersDarkScheme && (options.autoTheme ?? true)) {
17784
+ if (window.matchMedia('(prefers-color-scheme: light)').matches) {
17785
+ exports.LX.setMode('light', false);
17786
+ }
17787
+ this._mqlPrefersDarkScheme.addEventListener('change', this._onChangeSystemTheme);
17788
+ }
17789
+ // LX.setThemeColor( 'rose' );
17790
+ return this.mainArea;
17791
+ };
17792
+ /**
17793
+ * @method setSpacingMode
17794
+ * @param {String} mode: "default" | "compact"
17795
+ */
17796
+ exports.LX.setSpacingMode = function (mode) {
17797
+ this.spacingMode = mode;
17798
+ document.documentElement.setAttribute('data-spacing', this.spacingMode);
17799
+ };
17800
+ /**
17801
+ * @method setLayoutMode
17802
+ * @param {String} mode: "app" | "document"
17803
+ */
17804
+ exports.LX.setLayoutMode = function (mode) {
17805
+ this.layoutMode = mode;
17806
+ document.documentElement.setAttribute('data-layout', this.layoutMode);
17807
+ };
17808
+ /**
17809
+ * @method addSignal
17810
+ * @param {String} name
17811
+ * @param {Object} obj
17812
+ * @param {Function} callback
17813
+ */
17814
+ exports.LX.addSignal = function (name, obj, callback) {
17815
+ obj[name] = callback;
17816
+ if (!exports.LX.signals[name]) {
17817
+ exports.LX.signals[name] = [];
17818
+ }
17819
+ if (exports.LX.signals[name].indexOf(obj) > -1) {
17820
+ return;
13556
17821
  }
13557
17822
  exports.LX.signals[name].push(obj);
13558
17823
  };
@@ -13741,19 +18006,24 @@
13741
18006
  const instances = exports.LX.CodeEditor.getInstances();
13742
18007
  if (!instances.length || !instances[0].area.root.offsetHeight)
13743
18008
  return entries;
13744
- const languages = exports.LX.CodeEditor.languages;
13745
- for (let l of Object.keys(languages)) {
18009
+ for (let l of exports.LX.Tokenizer.getRegisteredLanguages()) {
18010
+ const langDef = Tokenizer.getLanguage(l);
18011
+ if (!langDef)
18012
+ continue;
13746
18013
  const key = 'Language: ' + l;
13747
- const icon = instances[0]._getFileIcon(null, languages[l].ext);
13748
- const classes = icon.split(' ');
13749
- let value = exports.LX.makeIcon(classes[0], { svgClass: `${classes.slice(0).join(' ')}` }).innerHTML;
13750
- value += key + " <span class='lang-ext'>(" + languages[l].ext + ')</span>';
18014
+ const icon = langDef?.icon;
18015
+ const iconData = ((icon) => {
18016
+ const data = icon.constructor === String ? icon : Object.values(icon)[0];
18017
+ return icon ? data.split(' ') : [];
18018
+ })(icon);
18019
+ let value = exports.LX.makeIcon(iconData[0], { svgClass: `${iconData.slice(1).join(' ')}` }).innerHTML;
18020
+ value += key + " <span class='lang-ext'>(" + langDef.extensions + ')</span>';
13751
18021
  if (!_filterEntry(key, filter)) {
13752
18022
  continue;
13753
18023
  }
13754
18024
  entries.push({ name: value, callback: () => {
13755
18025
  for (let i of instances) {
13756
- i._changeLanguage(l);
18026
+ i.setLanguage(l);
13757
18027
  }
13758
18028
  } });
13759
18029
  }