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