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