lexgui 8.3.1 → 8.4.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/BaseComponent.d.ts +77 -75
- package/build/components/Empty.d.ts +8 -0
- package/build/core/Namespace.js +1 -1
- package/build/core/Namespace.js.map +1 -1
- package/build/core/Panel.d.ts +545 -538
- package/build/extensions/AssetView.d.ts +139 -138
- package/build/extensions/AssetView.js +1451 -1433
- package/build/extensions/AssetView.js.map +1 -1
- package/build/extensions/CodeEditor.d.ts +64 -3
- package/build/extensions/CodeEditor.js +720 -90
- package/build/extensions/CodeEditor.js.map +1 -1
- package/build/extensions/VideoEditor.js +1021 -1021
- package/build/lexgui.all.js +962 -145
- package/build/lexgui.all.js.map +1 -1
- package/build/lexgui.all.min.js +1 -1
- package/build/lexgui.all.module.js +962 -145
- package/build/lexgui.all.module.js.map +1 -1
- package/build/lexgui.all.module.min.js +1 -1
- package/build/lexgui.css +87 -7
- package/build/lexgui.js +925 -126
- 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 +925 -126
- package/build/lexgui.module.js.map +1 -1
- package/build/lexgui.module.min.js +1 -1
- package/changelog.md +36 -1
- package/demo.js +2 -0
- package/examples/all-components.html +1 -0
- package/examples/asset-view.html +7 -33
- package/package.json +1 -1
|
@@ -12,7 +12,7 @@ const g$2 = globalThis;
|
|
|
12
12
|
let LX = g$2.LX;
|
|
13
13
|
if (!LX) {
|
|
14
14
|
LX = {
|
|
15
|
-
version: '8.
|
|
15
|
+
version: '8.4.0',
|
|
16
16
|
ready: false,
|
|
17
17
|
extensions: [], // Store extensions used
|
|
18
18
|
extraCommandbarEntries: [], // User specific entries for command bar
|
|
@@ -432,6 +432,8 @@ var ComponentType$1;
|
|
|
432
432
|
ComponentType[ComponentType["LABEL"] = 39] = "LABEL";
|
|
433
433
|
ComponentType[ComponentType["BLANK"] = 40] = "BLANK";
|
|
434
434
|
ComponentType[ComponentType["RATE"] = 41] = "RATE";
|
|
435
|
+
ComponentType[ComponentType["EMPTY"] = 42] = "EMPTY";
|
|
436
|
+
ComponentType[ComponentType["DESCRIPTION"] = 43] = "DESCRIPTION";
|
|
435
437
|
})(ComponentType$1 || (ComponentType$1 = {}));
|
|
436
438
|
LX.ComponentType = ComponentType$1;
|
|
437
439
|
/**
|
|
@@ -733,6 +735,9 @@ class Button extends BaseComponent$1 {
|
|
|
733
735
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
734
736
|
wValue.style.width = `calc( 100% - ${realNameWidth})`;
|
|
735
737
|
};
|
|
738
|
+
this.onSetDisabled = (disabled) => {
|
|
739
|
+
wValue.disabled = disabled;
|
|
740
|
+
};
|
|
736
741
|
// In case of swap, set if a change has to be performed
|
|
737
742
|
this.setState = function (v, skipCallback) {
|
|
738
743
|
const swapInput = wValue.querySelector('input');
|
|
@@ -1470,6 +1475,12 @@ class NumberInput extends BaseComponent$1 {
|
|
|
1470
1475
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
1471
1476
|
container.style.width = options.inputWidth ?? `calc( 100% - ${realNameWidth})`;
|
|
1472
1477
|
};
|
|
1478
|
+
this.onSetDisabled = (disabled) => {
|
|
1479
|
+
vecinput.disabled = disabled;
|
|
1480
|
+
const slider = this.root.querySelector('input[type="range"]');
|
|
1481
|
+
if (slider)
|
|
1482
|
+
slider.disabled = disabled;
|
|
1483
|
+
};
|
|
1473
1484
|
this.setLimits = (newMin, newMax, newStep) => { };
|
|
1474
1485
|
var container = document.createElement('div');
|
|
1475
1486
|
container.className = 'lexnumber';
|
|
@@ -1500,7 +1511,7 @@ class NumberInput extends BaseComponent$1 {
|
|
|
1500
1511
|
if (!options.skipSlider && options.min !== undefined && options.max !== undefined) {
|
|
1501
1512
|
let sliderBox = LX.makeContainer(['100%', 'auto'], 'z-1 input-box', '', box);
|
|
1502
1513
|
let slider = document.createElement('input');
|
|
1503
|
-
slider.className = 'lexinputslider';
|
|
1514
|
+
slider.className = 'lexinputslider disabled:pointer-events-none disabled:opacity-50';
|
|
1504
1515
|
slider.min = options.min;
|
|
1505
1516
|
slider.max = options.max;
|
|
1506
1517
|
slider.step = options.step ?? 1;
|
|
@@ -1637,6 +1648,11 @@ class TextInput extends BaseComponent$1 {
|
|
|
1637
1648
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
1638
1649
|
container.style.width = options.inputWidth ?? `calc( 100% - ${realNameWidth})`;
|
|
1639
1650
|
};
|
|
1651
|
+
this.onSetDisabled = (disabled) => {
|
|
1652
|
+
const input = this.root.querySelector('input');
|
|
1653
|
+
if (input)
|
|
1654
|
+
input.disabled = disabled;
|
|
1655
|
+
};
|
|
1640
1656
|
this.valid = (v, matchField) => {
|
|
1641
1657
|
v = v ?? this.value();
|
|
1642
1658
|
if (!options.pattern)
|
|
@@ -1796,6 +1812,9 @@ class Select extends BaseComponent$1 {
|
|
|
1796
1812
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
1797
1813
|
container.style.width = options.inputWidth ?? `calc( 100% - ${realNameWidth})`;
|
|
1798
1814
|
};
|
|
1815
|
+
this.onSetDisabled = (disabled) => {
|
|
1816
|
+
selectedOption?.setDisabled(disabled);
|
|
1817
|
+
};
|
|
1799
1818
|
let container = document.createElement('div');
|
|
1800
1819
|
container.className = 'lexselect';
|
|
1801
1820
|
this.root.appendChild(container);
|
|
@@ -2070,6 +2089,13 @@ class ArrayInput extends BaseComponent$1 {
|
|
|
2070
2089
|
this._trigger(new IEvent$1(name, values, event), callback);
|
|
2071
2090
|
}
|
|
2072
2091
|
};
|
|
2092
|
+
this.onSetDisabled = (disabled) => {
|
|
2093
|
+
if (this.root.dataset['opened'] == 'true' && disabled) {
|
|
2094
|
+
this.root.dataset['opened'] = false;
|
|
2095
|
+
this.root.querySelector('.lexarrayitems').toggleAttribute('hidden', true);
|
|
2096
|
+
}
|
|
2097
|
+
toggleButton.setDisabled(disabled);
|
|
2098
|
+
};
|
|
2073
2099
|
// Add open array button
|
|
2074
2100
|
let container = document.createElement('div');
|
|
2075
2101
|
container.className = 'lexarray shrink-1 grow-1 ml-4';
|
|
@@ -2161,9 +2187,10 @@ class Card extends BaseComponent$1 {
|
|
|
2161
2187
|
const container = LX.makeContainer(['100%', 'auto'], 'lexcard max-w-sm flex flex-col gap-4 bg-card border-color rounded-xl py-6', '', this.root);
|
|
2162
2188
|
if (options.header) {
|
|
2163
2189
|
const hasAction = options.header.action !== undefined;
|
|
2190
|
+
const actionButtonOptions = options.header.action.options ?? {};
|
|
2164
2191
|
let header = LX.makeContainer(['100%', 'auto'], `flex ${hasAction ? 'flex-row gap-4' : 'flex-col gap-1'} px-6`, '', container);
|
|
2165
2192
|
if (hasAction) {
|
|
2166
|
-
const actionBtn = new Button(null, options.header.action.name, options.header.action.callback, { buttonClass: 'secondary' });
|
|
2193
|
+
const actionBtn = new Button(null, options.header.action.name, options.header.action.callback, { buttonClass: 'secondary', ...actionButtonOptions });
|
|
2167
2194
|
header.appendChild(actionBtn.root);
|
|
2168
2195
|
const titleDescBox = LX.makeContainer(['75%', 'auto'], `flex flex-col gap-1`, '');
|
|
2169
2196
|
header.prepend(titleDescBox);
|
|
@@ -2230,6 +2257,9 @@ class Checkbox extends BaseComponent$1 {
|
|
|
2230
2257
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
2231
2258
|
container.style.width = options.inputWidth ?? `calc( 100% - ${realNameWidth})`;
|
|
2232
2259
|
};
|
|
2260
|
+
this.onSetDisabled = (disabled) => {
|
|
2261
|
+
checkbox.disabled = disabled;
|
|
2262
|
+
};
|
|
2233
2263
|
let container = document.createElement('div');
|
|
2234
2264
|
container.className = 'flex items-center gap-2 my-0 mx-auto [&_span]:truncate [&_span]:flex-auto-fill';
|
|
2235
2265
|
this.root.appendChild(container);
|
|
@@ -2858,7 +2888,12 @@ class ColorInput extends BaseComponent$1 {
|
|
|
2858
2888
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
2859
2889
|
container.style.width = `calc( 100% - ${realNameWidth})`;
|
|
2860
2890
|
};
|
|
2861
|
-
|
|
2891
|
+
this.onSetDisabled = (disabled) => {
|
|
2892
|
+
textComponent.setDisabled(disabled);
|
|
2893
|
+
sampleContainer.classList.toggle('pointer-events-none', disabled);
|
|
2894
|
+
sampleContainer.classList.toggle('opacity-50', disabled);
|
|
2895
|
+
};
|
|
2896
|
+
let container = document.createElement('span');
|
|
2862
2897
|
container.className = 'lexcolor';
|
|
2863
2898
|
this.root.appendChild(container);
|
|
2864
2899
|
this.picker = new ColorPicker(value, {
|
|
@@ -3022,6 +3057,11 @@ class Counter extends BaseComponent$1 {
|
|
|
3022
3057
|
this._trigger(new IEvent$1(name, newValue, event), callback);
|
|
3023
3058
|
}
|
|
3024
3059
|
};
|
|
3060
|
+
this.onSetDisabled = (disabled) => {
|
|
3061
|
+
substrButton.setDisabled(disabled);
|
|
3062
|
+
addButton.setDisabled(disabled);
|
|
3063
|
+
input.disabled = disabled;
|
|
3064
|
+
};
|
|
3025
3065
|
this.count = value;
|
|
3026
3066
|
const min = options.min ?? 0;
|
|
3027
3067
|
const max = options.max ?? 100;
|
|
@@ -3767,6 +3807,12 @@ class DatePicker extends BaseComponent$1 {
|
|
|
3767
3807
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
3768
3808
|
container.style.width = `calc( 100% - ${realNameWidth})`;
|
|
3769
3809
|
};
|
|
3810
|
+
this.onSetDisabled = (disabled) => {
|
|
3811
|
+
const buttons = this.root.querySelectorAll('button');
|
|
3812
|
+
buttons.forEach((b) => {
|
|
3813
|
+
b.disabled = disabled;
|
|
3814
|
+
});
|
|
3815
|
+
};
|
|
3770
3816
|
const container = LX.makeContainer(['auto', 'auto'], 'lexdate flex flex-row');
|
|
3771
3817
|
this.root.appendChild(container);
|
|
3772
3818
|
if (!dateAsRange) {
|
|
@@ -4159,6 +4205,52 @@ class Dial extends BaseComponent$1 {
|
|
|
4159
4205
|
LX.CanvasDial = CanvasDial;
|
|
4160
4206
|
LX.Dial = Dial;
|
|
4161
4207
|
|
|
4208
|
+
// Empty.ts @jxarco
|
|
4209
|
+
/**
|
|
4210
|
+
* @class Empty
|
|
4211
|
+
* @description Empty Component
|
|
4212
|
+
*/
|
|
4213
|
+
class Empty extends BaseComponent$1 {
|
|
4214
|
+
constructor(name, options = {}) {
|
|
4215
|
+
options.hideName = true;
|
|
4216
|
+
super(ComponentType$1.EMPTY, name, null, options);
|
|
4217
|
+
this.root.classList.add('place-content-center');
|
|
4218
|
+
const container = LX.makeContainer(['100%', 'auto'], 'lexcard max-w-sm flex flex-col gap-4 bg-card border-color rounded-xl py-6', '', this.root);
|
|
4219
|
+
if (options.header) {
|
|
4220
|
+
let header = LX.makeContainer(['100%', 'auto'], `flex flex-col gap-4 px-6 items-center`, '', container);
|
|
4221
|
+
if (options.header.icon) {
|
|
4222
|
+
const icon = LX.makeIcon(options.header.icon, { iconClass: 'bg-secondary p-2 rounded-lg!', svgClass: 'lg' });
|
|
4223
|
+
header.appendChild(icon);
|
|
4224
|
+
}
|
|
4225
|
+
else if (options.header.avatar) {
|
|
4226
|
+
const avatar = new LX.Avatar(options.header.avatar);
|
|
4227
|
+
header.appendChild(avatar.root);
|
|
4228
|
+
}
|
|
4229
|
+
if (options.header.title) {
|
|
4230
|
+
LX.makeElement('div', 'text-center text-foreground leading-none font-medium', options.header.title, header);
|
|
4231
|
+
}
|
|
4232
|
+
if (options.header.description) {
|
|
4233
|
+
LX.makeElement('div', 'text-sm text-center text-balance text-muted-foreground', options.header.description, header);
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
if (options.actions) {
|
|
4237
|
+
const content = LX.makeContainer(['100%', 'auto'], 'flex flex-row gap-1 px-6 justify-center', '', container);
|
|
4238
|
+
for (let a of options.actions) {
|
|
4239
|
+
const action = new LX.Button(null, a.name, a.callback, { buttonClass: "sm outline", ...a.options });
|
|
4240
|
+
content.appendChild(action.root);
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
if (options.footer) {
|
|
4244
|
+
const footer = LX.makeContainer(['100%', 'auto'], 'flex flex-col gap-1 px-6', '', container);
|
|
4245
|
+
const elements = [].concat(options.footer);
|
|
4246
|
+
for (let e of elements) {
|
|
4247
|
+
footer.appendChild(e.root ? e.root : e);
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
LX.Empty = Empty;
|
|
4253
|
+
|
|
4162
4254
|
// FileInput.ts @jxarco
|
|
4163
4255
|
/**
|
|
4164
4256
|
* @class FileInput
|
|
@@ -4174,6 +4266,9 @@ class FileInput extends BaseComponent$1 {
|
|
|
4174
4266
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
4175
4267
|
input.style.width = `calc( 100% - ${realNameWidth})`;
|
|
4176
4268
|
};
|
|
4269
|
+
this.onSetDisabled = (disabled) => {
|
|
4270
|
+
input.disabled = disabled;
|
|
4271
|
+
};
|
|
4177
4272
|
// Create hidden input
|
|
4178
4273
|
let input = document.createElement('input');
|
|
4179
4274
|
input.className = 'lexfileinput';
|
|
@@ -4381,6 +4476,9 @@ class Layers extends BaseComponent$1 {
|
|
|
4381
4476
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
4382
4477
|
container.style.width = `calc( 100% - ${realNameWidth})`;
|
|
4383
4478
|
};
|
|
4479
|
+
this.onSetDisabled = (disabled) => {
|
|
4480
|
+
this.setLayers(value);
|
|
4481
|
+
};
|
|
4384
4482
|
const container = LX.makeElement('div', 'lexlayers grid', '', this.root);
|
|
4385
4483
|
const maxBits = options.maxBits ?? 16;
|
|
4386
4484
|
this.setLayers = (val) => {
|
|
@@ -4457,6 +4555,9 @@ class List extends BaseComponent$1 {
|
|
|
4457
4555
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
4458
4556
|
listContainer.style.width = `calc( 100% - ${realNameWidth})`;
|
|
4459
4557
|
};
|
|
4558
|
+
this.onSetDisabled = (disabled) => {
|
|
4559
|
+
this._updateValues(values);
|
|
4560
|
+
};
|
|
4460
4561
|
this._updateValues = (newValues) => {
|
|
4461
4562
|
values = newValues;
|
|
4462
4563
|
listContainer.innerHTML = '';
|
|
@@ -4924,16 +5025,19 @@ class Map2D extends BaseComponent$1 {
|
|
|
4924
5025
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
4925
5026
|
container.style.width = `calc( 100% - ${realNameWidth})`;
|
|
4926
5027
|
};
|
|
5028
|
+
this.onSetDisabled = (disabled) => {
|
|
5029
|
+
openerButton.setDisabled(disabled);
|
|
5030
|
+
};
|
|
4927
5031
|
var container = document.createElement('div');
|
|
4928
5032
|
container.className = 'lexmap2d';
|
|
4929
5033
|
this.root.appendChild(container);
|
|
4930
5034
|
this.map2d = new CanvasMap2D(points, callback, options);
|
|
4931
|
-
const
|
|
4932
|
-
const
|
|
4933
|
-
this._popover = new Popover(
|
|
5035
|
+
const icon = LX.makeIcon(options.mapIcon ?? 'SquareMousePointer');
|
|
5036
|
+
const openerButton = new Button(null, 'Open Map', () => {
|
|
5037
|
+
this._popover = new Popover(openerButton.root, [this.map2d]);
|
|
4934
5038
|
}, { buttonClass: `outline justify-between`, disabled: this.disabled });
|
|
4935
|
-
|
|
4936
|
-
container.appendChild(
|
|
5039
|
+
openerButton.root.querySelector('button').appendChild(icon);
|
|
5040
|
+
container.appendChild(openerButton.root);
|
|
4937
5041
|
LX.doAsync(this.onResize.bind(this));
|
|
4938
5042
|
}
|
|
4939
5043
|
}
|
|
@@ -5640,6 +5744,9 @@ class OTPInput extends BaseComponent$1 {
|
|
|
5640
5744
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
5641
5745
|
container.style.width = `calc( 100% - ${realNameWidth})`;
|
|
5642
5746
|
};
|
|
5747
|
+
this.onSetDisabled = (disabled) => {
|
|
5748
|
+
_refreshInput(value);
|
|
5749
|
+
};
|
|
5643
5750
|
const container = document.createElement('div');
|
|
5644
5751
|
container.className = 'lexotp flex flex-row items-center';
|
|
5645
5752
|
this.root.appendChild(container);
|
|
@@ -5653,7 +5760,7 @@ class OTPInput extends BaseComponent$1 {
|
|
|
5653
5760
|
for (let j = 0; j < g.length; ++j) {
|
|
5654
5761
|
let number = valueString[itemsCount++];
|
|
5655
5762
|
number = number == 'x' ? '' : number;
|
|
5656
|
-
const slotDom = LX.makeContainer(['36px', '30px'], 'lexotpslot border-t-color border-b-color border-l-color px-3 cursor-text select-none font-medium outline-none', number, container);
|
|
5763
|
+
const slotDom = LX.makeContainer(['36px', '30px'], 'lexotpslot content-center border-t-color border-b-color border-l-color px-3 cursor-text select-none font-medium outline-none', number, container);
|
|
5657
5764
|
slotDom.tabIndex = '1';
|
|
5658
5765
|
if (this.disabled) {
|
|
5659
5766
|
slotDom.classList.add('disabled');
|
|
@@ -6057,6 +6164,9 @@ class RangeInput extends BaseComponent$1 {
|
|
|
6057
6164
|
slider.style.setProperty('--range-fix-max-offset', `${diffMaxOffset}rem`);
|
|
6058
6165
|
}
|
|
6059
6166
|
};
|
|
6167
|
+
this.onSetDisabled = (disabled) => {
|
|
6168
|
+
slider.disabled = disabled;
|
|
6169
|
+
};
|
|
6060
6170
|
const container = document.createElement('div');
|
|
6061
6171
|
container.className = 'lexrange relative py-3';
|
|
6062
6172
|
this.root.appendChild(container);
|
|
@@ -6165,6 +6275,9 @@ class Rate extends BaseComponent$1 {
|
|
|
6165
6275
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
6166
6276
|
container.style.width = `calc( 100% - ${realNameWidth})`;
|
|
6167
6277
|
};
|
|
6278
|
+
this.onSetDisabled = (disabled) => {
|
|
6279
|
+
container.dataset['disabled'] = disabled.toString();
|
|
6280
|
+
};
|
|
6168
6281
|
const container = document.createElement('div');
|
|
6169
6282
|
container.className = 'lexrate relative data-[disabled=true]:pointer-events-none';
|
|
6170
6283
|
container.dataset['disabled'] = this.disabled.toString();
|
|
@@ -7308,6 +7421,9 @@ class Tags extends BaseComponent$1 {
|
|
|
7308
7421
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
7309
7422
|
tagsContainer.style.width = `calc( 100% - ${realNameWidth})`;
|
|
7310
7423
|
};
|
|
7424
|
+
this.onSetDisabled = (disabled) => {
|
|
7425
|
+
this.generateTags(arrayValue);
|
|
7426
|
+
};
|
|
7311
7427
|
// Show tags
|
|
7312
7428
|
const tagsContainer = document.createElement('div');
|
|
7313
7429
|
tagsContainer.className = 'inline-flex flex-wrap gap-1 bg-card/50 rounded-lg pad-xs [&_input]:w-2/3';
|
|
@@ -7376,6 +7492,11 @@ class TextArea extends BaseComponent$1 {
|
|
|
7376
7492
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
7377
7493
|
container.style.width = options.inputWidth ?? `calc( 100% - ${realNameWidth})`;
|
|
7378
7494
|
};
|
|
7495
|
+
this.onSetDisabled = (disabled) => {
|
|
7496
|
+
const textarea = this.root.querySelector('textarea');
|
|
7497
|
+
if (textarea)
|
|
7498
|
+
textarea.disabled = disabled;
|
|
7499
|
+
};
|
|
7379
7500
|
let container = document.createElement('div');
|
|
7380
7501
|
container.className = 'lextextarea';
|
|
7381
7502
|
container.style.display = 'flex';
|
|
@@ -7493,6 +7614,9 @@ class Toggle extends BaseComponent$1 {
|
|
|
7493
7614
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
7494
7615
|
container.style.width = options.inputWidth ?? `calc( 100% - ${realNameWidth})`;
|
|
7495
7616
|
};
|
|
7617
|
+
this.onSetDisabled = (disabled) => {
|
|
7618
|
+
toggle.disabled = disabled;
|
|
7619
|
+
};
|
|
7496
7620
|
var container = document.createElement('div');
|
|
7497
7621
|
container.className = 'flex flex-row gap-2 items-center';
|
|
7498
7622
|
this.root.appendChild(container);
|
|
@@ -7562,6 +7686,12 @@ class Vector extends BaseComponent$1 {
|
|
|
7562
7686
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
7563
7687
|
container.style.width = `calc( 100% - ${realNameWidth})`;
|
|
7564
7688
|
};
|
|
7689
|
+
this.onSetDisabled = (disabled) => {
|
|
7690
|
+
const inputs = this.root.querySelectorAll('input');
|
|
7691
|
+
inputs.forEach((i) => {
|
|
7692
|
+
i.disabled = disabled;
|
|
7693
|
+
});
|
|
7694
|
+
};
|
|
7565
7695
|
this.setLimits = (newMin, newMax, newStep) => { };
|
|
7566
7696
|
const vectorInputs = [];
|
|
7567
7697
|
var container = document.createElement('div');
|
|
@@ -8306,6 +8436,19 @@ let Panel$2 = class Panel {
|
|
|
8306
8436
|
component.type = ComponentType$1.LABEL;
|
|
8307
8437
|
return component;
|
|
8308
8438
|
}
|
|
8439
|
+
/**
|
|
8440
|
+
* @method addDescription
|
|
8441
|
+
* @param {String} value Information string
|
|
8442
|
+
* @param {Object} options Text options
|
|
8443
|
+
*/
|
|
8444
|
+
addDescription(value, options = {}) {
|
|
8445
|
+
options.disabled = true;
|
|
8446
|
+
options.fitHeight = true;
|
|
8447
|
+
options.inputClass = LX.mergeClass('bg-none', options.inputClass);
|
|
8448
|
+
const component = this.addTextArea(null, value, null, options);
|
|
8449
|
+
component.type = ComponentType$1.DESCRIPTION;
|
|
8450
|
+
return component;
|
|
8451
|
+
}
|
|
8309
8452
|
/**
|
|
8310
8453
|
* @method addButton
|
|
8311
8454
|
* @param {String} name Component name
|
|
@@ -8344,18 +8487,22 @@ let Panel$2 = class Panel {
|
|
|
8344
8487
|
}
|
|
8345
8488
|
/**
|
|
8346
8489
|
* @method addCard
|
|
8347
|
-
* @param {String} name
|
|
8348
|
-
* @param {Object} options
|
|
8349
|
-
* text: Card text
|
|
8350
|
-
* link: Card link
|
|
8351
|
-
* title: Card dom title
|
|
8352
|
-
* src: url of the image
|
|
8353
|
-
* callback (Function): function to call on click
|
|
8490
|
+
* @param {String} name
|
|
8491
|
+
* @param {Object} options
|
|
8354
8492
|
*/
|
|
8355
8493
|
addCard(name, options = {}) {
|
|
8356
8494
|
const component = new Card(name, options);
|
|
8357
8495
|
return this._attachComponent(component);
|
|
8358
8496
|
}
|
|
8497
|
+
/**
|
|
8498
|
+
* @method addEmpty
|
|
8499
|
+
* @param {String} name
|
|
8500
|
+
* @param {Object} options
|
|
8501
|
+
*/
|
|
8502
|
+
addEmpty(name, options = {}) {
|
|
8503
|
+
const component = new Empty(name, options);
|
|
8504
|
+
return this._attachComponent(component);
|
|
8505
|
+
}
|
|
8359
8506
|
/**
|
|
8360
8507
|
* @method addForm
|
|
8361
8508
|
* @param {String} name Component name
|
|
@@ -11129,47 +11276,67 @@ LX.makeContainer = makeContainer;
|
|
|
11129
11276
|
* offsetY: Tooltip margin vertical offset
|
|
11130
11277
|
* active: Tooltip active by default [true]
|
|
11131
11278
|
* callback: Callback function to execute when the tooltip is shown
|
|
11279
|
+
* delay: Interest delay in ms until showing the tooltip [100]
|
|
11132
11280
|
*/
|
|
11133
11281
|
function asTooltip(trigger, content, options = {}) {
|
|
11134
11282
|
console.assert(trigger, 'You need a trigger to generate a tooltip!');
|
|
11135
11283
|
trigger.dataset['disableTooltip'] = !(options.active ?? true);
|
|
11136
11284
|
let tooltipDom = null;
|
|
11285
|
+
let delayTimer = null;
|
|
11286
|
+
let rafId = null;
|
|
11137
11287
|
const _offset = options.offset;
|
|
11138
11288
|
const _offsetX = options.offsetX ?? (_offset ?? 0);
|
|
11139
11289
|
const _offsetY = options.offsetY ?? (_offset ?? 6);
|
|
11140
|
-
|
|
11141
|
-
|
|
11290
|
+
const _delay = options.delay ?? 100;
|
|
11291
|
+
const _cleanup = () => {
|
|
11292
|
+
clearTimeout(delayTimer);
|
|
11293
|
+
if (rafId !== null) {
|
|
11294
|
+
cancelAnimationFrame(rafId);
|
|
11295
|
+
rafId = null;
|
|
11296
|
+
}
|
|
11297
|
+
if (tooltipDom) {
|
|
11298
|
+
tooltipDom.remove();
|
|
11299
|
+
tooltipDom = null;
|
|
11300
|
+
}
|
|
11301
|
+
};
|
|
11302
|
+
const _watchConnection = () => {
|
|
11303
|
+
if (!trigger.isConnected) {
|
|
11304
|
+
_cleanup();
|
|
11142
11305
|
return;
|
|
11143
11306
|
}
|
|
11307
|
+
if (tooltipDom)
|
|
11308
|
+
rafId = requestAnimationFrame(_watchConnection);
|
|
11309
|
+
};
|
|
11310
|
+
const _showTooltip = () => {
|
|
11144
11311
|
tooltipDom = LX.makeElement('div', 'lextooltip fixed bg-secondary-foreground text-secondary text-xs px-2 py-1 rounded-lg pointer-events-none data-closed:opacity-0', trigger.dataset['tooltipContent'] ?? content);
|
|
11145
11312
|
const nestedDialog = trigger.closest('dialog');
|
|
11146
11313
|
const tooltipParent = nestedDialog ?? LX.root;
|
|
11147
|
-
// Remove
|
|
11314
|
+
// Remove others first
|
|
11148
11315
|
LX.root.querySelectorAll('.lextooltip').forEach((e) => e.remove());
|
|
11149
|
-
// Append new tooltip
|
|
11150
11316
|
tooltipParent.appendChild(tooltipDom);
|
|
11317
|
+
// Watch for trigger being removed from the DOM before mouseleave fires
|
|
11318
|
+
rafId = requestAnimationFrame(_watchConnection);
|
|
11151
11319
|
LX.doAsync(() => {
|
|
11320
|
+
if (!tooltipDom)
|
|
11321
|
+
return;
|
|
11152
11322
|
const position = [0, 0];
|
|
11153
11323
|
const offsetX = parseFloat(trigger.dataset['tooltipOffsetX'] ?? _offsetX);
|
|
11154
11324
|
const offsetY = parseFloat(trigger.dataset['tooltipOffsetY'] ?? _offsetY);
|
|
11155
11325
|
const rect = trigger.getBoundingClientRect();
|
|
11156
|
-
|
|
11157
|
-
|
|
11326
|
+
const side = options.side ?? 'top';
|
|
11327
|
+
const alignWidth = side === 'top' || side === 'bottom';
|
|
11328
|
+
switch (side) {
|
|
11158
11329
|
case 'left':
|
|
11159
11330
|
position[0] += rect.x - tooltipDom.offsetWidth - offsetX;
|
|
11160
|
-
alignWidth = false;
|
|
11161
11331
|
break;
|
|
11162
11332
|
case 'right':
|
|
11163
11333
|
position[0] += rect.x + rect.width + offsetX;
|
|
11164
|
-
alignWidth = false;
|
|
11165
11334
|
break;
|
|
11166
11335
|
case 'top':
|
|
11167
11336
|
position[1] += rect.y - tooltipDom.offsetHeight - offsetY;
|
|
11168
|
-
alignWidth = true;
|
|
11169
11337
|
break;
|
|
11170
11338
|
case 'bottom':
|
|
11171
11339
|
position[1] += rect.y + rect.height + offsetY;
|
|
11172
|
-
alignWidth = true;
|
|
11173
11340
|
break;
|
|
11174
11341
|
}
|
|
11175
11342
|
if (alignWidth)
|
|
@@ -11180,22 +11347,22 @@ function asTooltip(trigger, content, options = {}) {
|
|
|
11180
11347
|
position[0] = LX.clamp(position[0], 0, window.innerWidth - tooltipDom.offsetWidth - 4);
|
|
11181
11348
|
position[1] = LX.clamp(position[1], 0, window.innerHeight - tooltipDom.offsetHeight - 4);
|
|
11182
11349
|
if (nestedDialog) {
|
|
11183
|
-
|
|
11350
|
+
const parentRect = tooltipParent.getBoundingClientRect();
|
|
11184
11351
|
position[0] -= parentRect.x;
|
|
11185
11352
|
position[1] -= parentRect.y;
|
|
11186
11353
|
}
|
|
11187
11354
|
tooltipDom.style.left = `${position[0]}px`;
|
|
11188
11355
|
tooltipDom.style.top = `${position[1]}px`;
|
|
11189
|
-
|
|
11190
|
-
options.callback(tooltipDom, trigger);
|
|
11191
|
-
}
|
|
11356
|
+
options.callback?.(tooltipDom, trigger);
|
|
11192
11357
|
});
|
|
11193
|
-
}
|
|
11194
|
-
trigger.addEventListener('
|
|
11195
|
-
if (
|
|
11196
|
-
|
|
11358
|
+
};
|
|
11359
|
+
trigger.addEventListener('mouseenter', function () {
|
|
11360
|
+
if (trigger.dataset['disableTooltip'] == 'true') {
|
|
11361
|
+
return;
|
|
11197
11362
|
}
|
|
11363
|
+
delayTimer = setTimeout(_showTooltip, _delay);
|
|
11198
11364
|
});
|
|
11365
|
+
trigger.addEventListener('mouseleave', _cleanup);
|
|
11199
11366
|
}
|
|
11200
11367
|
LX.asTooltip = asTooltip;
|
|
11201
11368
|
function insertChildAtIndex(parent, child, index = Infinity) {
|
|
@@ -13317,7 +13484,9 @@ class Tour {
|
|
|
13317
13484
|
// using a fullscreen SVG with "rect" elements
|
|
13318
13485
|
_generateMask(reference) {
|
|
13319
13486
|
this.tourContainer.innerHTML = ''; // Clear previous content
|
|
13487
|
+
const scrollTop = document.scrollingElement?.scrollTop ?? 0;
|
|
13320
13488
|
this.tourMask = LX.makeContainer(['100%', '100%'], 'tour-mask absolute inset-0');
|
|
13489
|
+
this.tourMask.style.top = `${scrollTop}px`;
|
|
13321
13490
|
this.tourContainer.appendChild(this.tourMask);
|
|
13322
13491
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
13323
13492
|
svg.style.width = '100%';
|
|
@@ -13380,7 +13549,7 @@ class Tour {
|
|
|
13380
13549
|
// Reference Highlight
|
|
13381
13550
|
const refContainer = LX.makeContainer(['0', '0'], 'tour-ref-mask absolute');
|
|
13382
13551
|
refContainer.style.left = `${boundingX - hOffset - 1}px`;
|
|
13383
|
-
refContainer.style.top = `${boundingY - vOffset - 1}px`;
|
|
13552
|
+
refContainer.style.top = `${boundingY - vOffset - 1 + scrollTop}px`;
|
|
13384
13553
|
refContainer.style.width = `${boundingWidth + hOffset * 2 + 2}px`;
|
|
13385
13554
|
refContainer.style.height = `${boundingHeight + vOffset * 2 + 2}px`;
|
|
13386
13555
|
this.tourContainer.appendChild(refContainer);
|
|
@@ -14425,12 +14594,12 @@ class CodeDocument {
|
|
|
14425
14594
|
getText(separator = '\n') {
|
|
14426
14595
|
return this._lines.join(separator);
|
|
14427
14596
|
}
|
|
14428
|
-
setText(text) {
|
|
14597
|
+
setText(text, silent = false) {
|
|
14429
14598
|
this._lines = text.split(/\r?\n/);
|
|
14430
14599
|
if (this._lines.length === 0) {
|
|
14431
14600
|
this._lines = [''];
|
|
14432
14601
|
}
|
|
14433
|
-
if (this.onChange)
|
|
14602
|
+
if (!silent && this.onChange)
|
|
14434
14603
|
this.onChange(this);
|
|
14435
14604
|
}
|
|
14436
14605
|
getCharAt(line, col) {
|
|
@@ -14594,6 +14763,7 @@ class UndoManager {
|
|
|
14594
14763
|
_lastPushTime = 0;
|
|
14595
14764
|
_groupThresholdMs;
|
|
14596
14765
|
_maxSteps;
|
|
14766
|
+
_savedDepth = 0;
|
|
14597
14767
|
constructor(groupThresholdMs = 2000, maxSteps = 200) {
|
|
14598
14768
|
this._groupThresholdMs = groupThresholdMs;
|
|
14599
14769
|
this._maxSteps = maxSteps;
|
|
@@ -14665,11 +14835,19 @@ class UndoManager {
|
|
|
14665
14835
|
canRedo() {
|
|
14666
14836
|
return this._redoStack.length > 0;
|
|
14667
14837
|
}
|
|
14838
|
+
markSaved() {
|
|
14839
|
+
this._flush();
|
|
14840
|
+
this._savedDepth = this._undoStack.length;
|
|
14841
|
+
}
|
|
14842
|
+
isModified() {
|
|
14843
|
+
return this._undoStack.length !== this._savedDepth || this._pendingOps.length > 0;
|
|
14844
|
+
}
|
|
14668
14845
|
clear() {
|
|
14669
14846
|
this._undoStack.length = 0;
|
|
14670
14847
|
this._redoStack.length = 0;
|
|
14671
14848
|
this._pendingOps.length = 0;
|
|
14672
14849
|
this._lastPushTime = 0;
|
|
14850
|
+
this._savedDepth = 0;
|
|
14673
14851
|
}
|
|
14674
14852
|
_flush() {
|
|
14675
14853
|
if (this._pendingOps.length === 0)
|
|
@@ -14965,6 +15143,49 @@ class CursorSet {
|
|
|
14965
15143
|
this.cursors = merged;
|
|
14966
15144
|
}
|
|
14967
15145
|
}
|
|
15146
|
+
/**
|
|
15147
|
+
* Strips string literals and single-line comments from a line of code,
|
|
15148
|
+
* leaving only the structural characters (braces, operators, keywords etc).
|
|
15149
|
+
*/
|
|
15150
|
+
function stripLiteralsAndComments(line) {
|
|
15151
|
+
let result = '';
|
|
15152
|
+
let i = 0;
|
|
15153
|
+
while (i < line.length) {
|
|
15154
|
+
const ch = line[i];
|
|
15155
|
+
// Remove single-line comment
|
|
15156
|
+
if (ch === '/' && line[i + 1] === '/') {
|
|
15157
|
+
break;
|
|
15158
|
+
}
|
|
15159
|
+
// Block comment (same line): skip to closing */
|
|
15160
|
+
if (ch === '/' && line[i + 1] === '*') {
|
|
15161
|
+
i += 2;
|
|
15162
|
+
while (i < line.length && !(line[i] === '*' && line[i + 1] === '/'))
|
|
15163
|
+
i++;
|
|
15164
|
+
i += 2;
|
|
15165
|
+
continue;
|
|
15166
|
+
}
|
|
15167
|
+
// Remove strings (single, double, template)
|
|
15168
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
15169
|
+
const quote = ch;
|
|
15170
|
+
i++;
|
|
15171
|
+
while (i < line.length) {
|
|
15172
|
+
if (line[i] === '\\') {
|
|
15173
|
+
i += 2;
|
|
15174
|
+
continue;
|
|
15175
|
+
} // escaped char
|
|
15176
|
+
if (line[i] === quote) {
|
|
15177
|
+
i++;
|
|
15178
|
+
break;
|
|
15179
|
+
} // closing quote
|
|
15180
|
+
i++;
|
|
15181
|
+
}
|
|
15182
|
+
continue;
|
|
15183
|
+
}
|
|
15184
|
+
result += ch;
|
|
15185
|
+
i++;
|
|
15186
|
+
}
|
|
15187
|
+
return result;
|
|
15188
|
+
}
|
|
14968
15189
|
/**
|
|
14969
15190
|
* Manages code symbols for autocomplete, navigation, and outlining.
|
|
14970
15191
|
* Incrementally updates as lines change.
|
|
@@ -14973,7 +15194,8 @@ class SymbolTable {
|
|
|
14973
15194
|
_symbols = new Map(); // name -> symbols[]
|
|
14974
15195
|
_lineSymbols = []; // [lineNum] -> symbols declared on that line
|
|
14975
15196
|
_scopeStack = [{ name: 'global', type: 'global', line: 0 }];
|
|
14976
|
-
_lineScopes = []; // [lineNum] -> scope stack at that line
|
|
15197
|
+
_lineScopes = []; // [lineNum] -> scope stack at start of that line
|
|
15198
|
+
_lineScopesEnd = []; // [lineNum] -> scope stack at end of that line
|
|
14977
15199
|
get currentScope() {
|
|
14978
15200
|
return this._scopeStack[this._scopeStack.length - 1]?.name ?? 'global';
|
|
14979
15201
|
}
|
|
@@ -14983,6 +15205,9 @@ class SymbolTable {
|
|
|
14983
15205
|
getScopeAtLine(line) {
|
|
14984
15206
|
return this._lineScopes[line] ?? [{ name: 'global', type: 'global', line: 0 }];
|
|
14985
15207
|
}
|
|
15208
|
+
getLineScopeEnd(line) {
|
|
15209
|
+
return this._lineScopesEnd[line] ?? [{ name: 'global', type: 'global', line: 0 }];
|
|
15210
|
+
}
|
|
14986
15211
|
getSymbols(name) {
|
|
14987
15212
|
return this._symbols.get(name) ?? [];
|
|
14988
15213
|
}
|
|
@@ -15001,20 +15226,26 @@ class SymbolTable {
|
|
|
15001
15226
|
}
|
|
15002
15227
|
/** Update scope stack for a line (call before parsing symbols) */
|
|
15003
15228
|
updateScopeForLine(line, lineText) {
|
|
15004
|
-
|
|
15005
|
-
|
|
15006
|
-
|
|
15007
|
-
|
|
15229
|
+
if (line === 0) {
|
|
15230
|
+
this._scopeStack = [{ name: 'global', type: 'global', line: 0 }];
|
|
15231
|
+
}
|
|
15232
|
+
else if (this._lineScopesEnd[line - 1]) {
|
|
15233
|
+
this._scopeStack = [...this._lineScopesEnd[line - 1]];
|
|
15234
|
+
}
|
|
15235
|
+
const stripped = stripLiteralsAndComments(lineText);
|
|
15236
|
+
const openBraces = (stripped.match(/\{/g) || []).length;
|
|
15237
|
+
const closeBraces = (stripped.match(/\}/g) || []).length;
|
|
15008
15238
|
this._lineScopes[line] = [...this._scopeStack];
|
|
15009
15239
|
// Pop scopes for closing braces
|
|
15010
15240
|
for (let i = 0; i < closeBraces; i++) {
|
|
15011
15241
|
if (this._scopeStack.length > 1)
|
|
15012
15242
|
this._scopeStack.pop();
|
|
15013
15243
|
}
|
|
15014
|
-
// Push scopes for opening braces (
|
|
15244
|
+
// Push scopes for opening braces (symbol detection will name them later)
|
|
15015
15245
|
for (let i = 0; i < openBraces; i++) {
|
|
15016
15246
|
this._scopeStack.push({ name: 'anonymous', type: 'anonymous', line });
|
|
15017
15247
|
}
|
|
15248
|
+
this._lineScopesEnd[line] = [...this._scopeStack];
|
|
15018
15249
|
}
|
|
15019
15250
|
/** Name the most recent anonymous scope (called when detecting class/function) */
|
|
15020
15251
|
nameCurrentScope(name, type) {
|
|
@@ -15058,6 +15289,7 @@ class SymbolTable {
|
|
|
15058
15289
|
resetScopes() {
|
|
15059
15290
|
this._scopeStack = [{ name: 'global', type: 'global', line: 0 }];
|
|
15060
15291
|
this._lineScopes = [];
|
|
15292
|
+
this._lineScopesEnd = [];
|
|
15061
15293
|
}
|
|
15062
15294
|
clear() {
|
|
15063
15295
|
this._symbols.clear();
|
|
@@ -15070,23 +15302,25 @@ class SymbolTable {
|
|
|
15070
15302
|
*/
|
|
15071
15303
|
function parseSymbolsFromLine(lineText, tokens, line, symbolTable) {
|
|
15072
15304
|
const symbols = [];
|
|
15073
|
-
|
|
15074
|
-
|
|
15075
|
-
|
|
15305
|
+
// Use the scope snapshot from the START of this line, not currentScope/currentScopeType
|
|
15306
|
+
// which will reflect state AFTER updateScopeForLine already pushed/popped braces on this line...
|
|
15307
|
+
const lineScopes = symbolTable.getScopeAtLine(line);
|
|
15308
|
+
const lineScope = lineScopes[lineScopes.length - 1];
|
|
15309
|
+
const scope = lineScope?.name ?? 'global';
|
|
15310
|
+
const scopeType = lineScope?.type ?? 'global';
|
|
15076
15311
|
const reservedWords = new Set();
|
|
15077
15312
|
for (const token of tokens) {
|
|
15078
15313
|
if (['keyword', 'statement', 'builtin', 'preprocessor'].includes(token.type)) {
|
|
15079
15314
|
reservedWords.add(token.value);
|
|
15080
15315
|
}
|
|
15081
15316
|
}
|
|
15082
|
-
// Track added symbols by name and approximate position to avoid duplicates
|
|
15317
|
+
// Track added symbols by name and approximate position using 5 chars tolerance to avoid duplicates
|
|
15083
15318
|
const addedSymbols = new Set();
|
|
15084
15319
|
const addSymbol = (name, kind, col = 0) => {
|
|
15085
15320
|
if (!name || !name.match(/^[a-zA-Z_$][\w$]*$/))
|
|
15086
15321
|
return; // Valid identifier check
|
|
15087
15322
|
if (reservedWords.has(name))
|
|
15088
15323
|
return;
|
|
15089
|
-
// Unique key using 5 chars tolerance
|
|
15090
15324
|
const posKey = `${name}@${Math.floor(col / 5) * 5}`;
|
|
15091
15325
|
if (addedSymbols.has(posKey))
|
|
15092
15326
|
return; // Already added
|
|
@@ -15102,13 +15336,14 @@ function parseSymbolsFromLine(lineText, tokens, line, symbolTable) {
|
|
|
15102
15336
|
{ regex: /^\s*type\s+([A-Z_]\w*)\s*=/i, kind: 'type' },
|
|
15103
15337
|
{ regex: /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)/i, kind: 'function' },
|
|
15104
15338
|
{ regex: /^\s*(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>/i, kind: 'function' },
|
|
15105
|
-
{ 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' },
|
|
15106
|
-
{ regex: /^\s*(?:public|private|protected|static|readonly)
|
|
15339
|
+
{ regex: /^\s*(?:static\s+|const\s+|virtual\s+|inline\s+|extern\s+|pub\s+|async\s+)*(?!\b(?:await|return|if|else|while|for|switch|case|throw|new|delete|typeof|yield)\b)(\w+[\w\s\*&:<>,]*?)\s+(\w+)\s*\([^)]*\)\s*[{;]/i, kind: 'typed-function' },
|
|
15340
|
+
{ regex: /^\s*(?:(?:public|private|protected|static|readonly|async|override)\s+)*(\w+)\s*\([^)]*\)\s*[:{]/i, kind: scopeType === 'class' ? 'method' : 'function' }
|
|
15107
15341
|
];
|
|
15108
15342
|
const multiPatterns = [
|
|
15109
15343
|
{ regex: /(?:const|let|var)\s+(\w+)/gi, kind: 'variable' },
|
|
15110
15344
|
{ regex: /(\w+)\s*:\s*(?:function|[A-Z]\w*)/gi, kind: 'variable' },
|
|
15111
15345
|
{ regex: /this\.(\w+)\s*=/gi, kind: 'property' },
|
|
15346
|
+
{ regex: /\b(?:private|protected|public)\s+(?:(?:static|readonly)\s+)*(\w+)\s*[=:;]/gi, kind: 'property' },
|
|
15112
15347
|
{ regex: /new\s+([A-Z]\w+)/gi, kind: 'constructor-call' },
|
|
15113
15348
|
{ regex: /(\w+)\s*\(/gi, kind: 'method-call' }
|
|
15114
15349
|
];
|
|
@@ -15181,6 +15416,56 @@ LX.Area;
|
|
|
15181
15416
|
LX.Panel;
|
|
15182
15417
|
LX.Tabs;
|
|
15183
15418
|
LX.NodeTree;
|
|
15419
|
+
const HEX_COLOR_RE = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})\b/g;
|
|
15420
|
+
const URL_REGEX = /(https?:\/\/[^\s"'<>)\]]+)/g;
|
|
15421
|
+
/**
|
|
15422
|
+
* Returns true if the string token at `idx` in the token list is a module import path.
|
|
15423
|
+
*/
|
|
15424
|
+
function isImportPath(tokens, idx) {
|
|
15425
|
+
const isWs = (t) => /^\s+$/.test(t.value);
|
|
15426
|
+
const isImportWord = (t) => t.value === 'require' || t.value === 'import';
|
|
15427
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
15428
|
+
const t = tokens[i];
|
|
15429
|
+
if (isWs(t))
|
|
15430
|
+
continue;
|
|
15431
|
+
if (t.type === 'keyword' && t.value === 'from')
|
|
15432
|
+
return true;
|
|
15433
|
+
if (isImportWord(t))
|
|
15434
|
+
return true;
|
|
15435
|
+
if (t.type === 'symbol' && t.value === '(') {
|
|
15436
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
15437
|
+
const t2 = tokens[j];
|
|
15438
|
+
if (isWs(t2))
|
|
15439
|
+
continue;
|
|
15440
|
+
if (isImportWord(t2))
|
|
15441
|
+
return true;
|
|
15442
|
+
break;
|
|
15443
|
+
}
|
|
15444
|
+
}
|
|
15445
|
+
break;
|
|
15446
|
+
}
|
|
15447
|
+
return false;
|
|
15448
|
+
}
|
|
15449
|
+
/**
|
|
15450
|
+
* Scans a raw token value for hex color literals and returns HTML with each
|
|
15451
|
+
* color wrapped in a swatch span. Non-color text is HTML-escaped.
|
|
15452
|
+
*/
|
|
15453
|
+
function injectColorSpans(raw, lineIndex, colOffset) {
|
|
15454
|
+
HEX_COLOR_RE.lastIndex = 0;
|
|
15455
|
+
let result = '';
|
|
15456
|
+
let lastIndex = 0;
|
|
15457
|
+
let match;
|
|
15458
|
+
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
15459
|
+
while ((match = HEX_COLOR_RE.exec(raw)) !== null) {
|
|
15460
|
+
result += esc(raw.slice(lastIndex, match.index));
|
|
15461
|
+
const color = match[0];
|
|
15462
|
+
const col = colOffset + match.index;
|
|
15463
|
+
result += `<span class="code-color" data-color="${color}" data-line="${lineIndex}" data-col="${col}" style="--code-color:${color}"><span class="code-color-swatch"></span>${esc(color)}</span>`;
|
|
15464
|
+
lastIndex = match.index + color.length;
|
|
15465
|
+
}
|
|
15466
|
+
result += esc(raw.slice(lastIndex));
|
|
15467
|
+
return result;
|
|
15468
|
+
}
|
|
15184
15469
|
// _____ _ _ _____
|
|
15185
15470
|
// | __|___ ___ ___| | | __ |___ ___
|
|
15186
15471
|
// |__ | _| _| . | | | __ -| .'| _|
|
|
@@ -15203,6 +15488,15 @@ class ScrollBar {
|
|
|
15203
15488
|
this.thumb = LX.makeElement('div');
|
|
15204
15489
|
this.thumb.addEventListener('mousedown', (e) => this._onMouseDown(e));
|
|
15205
15490
|
this.root.appendChild(this.thumb);
|
|
15491
|
+
this.root.addEventListener('mousedown', (e) => {
|
|
15492
|
+
if (e.target === this.thumb)
|
|
15493
|
+
return;
|
|
15494
|
+
const clickPos = this._vertical ? e.offsetY : e.offsetX;
|
|
15495
|
+
const thumbSize = this._vertical ? this.thumb.offsetHeight : this.thumb.offsetWidth;
|
|
15496
|
+
const delta = (clickPos - thumbSize / 2) - this._thumbPos;
|
|
15497
|
+
this._onDrag?.(delta);
|
|
15498
|
+
this._onMouseDown(e); // continue as drag from new position
|
|
15499
|
+
});
|
|
15206
15500
|
}
|
|
15207
15501
|
setThumbRatio(ratio) {
|
|
15208
15502
|
this._thumbRatio = LX.clamp(ratio, 0, 1);
|
|
@@ -15326,10 +15620,18 @@ class CodeEditor {
|
|
|
15326
15620
|
onReady;
|
|
15327
15621
|
onCreateFile;
|
|
15328
15622
|
onCodeChange;
|
|
15623
|
+
onOpenPath;
|
|
15624
|
+
onHoverSymbol;
|
|
15329
15625
|
_inputArea;
|
|
15330
15626
|
// State:
|
|
15331
15627
|
_lineStates = []; // tokenizer state at end of each line
|
|
15332
15628
|
_lineElements = []; // <pre> element per line
|
|
15629
|
+
_bracketOpenLine = -1; // line of the { opening current scope
|
|
15630
|
+
_bracketCloseLine = -1; // line of the } closing current scope
|
|
15631
|
+
_hoverTimer = null;
|
|
15632
|
+
_hoverPopup = null;
|
|
15633
|
+
_hoverWord = '';
|
|
15634
|
+
_colorPopover = null; // active color picker popover
|
|
15333
15635
|
_openedTabs = {};
|
|
15334
15636
|
_loadedTabs = {};
|
|
15335
15637
|
_storedTabs = {};
|
|
@@ -15399,6 +15701,8 @@ class CodeEditor {
|
|
|
15399
15701
|
this.onSelectTab = options.onSelectTab;
|
|
15400
15702
|
this.onReady = options.onReady;
|
|
15401
15703
|
this.onCodeChange = options.onCodeChange;
|
|
15704
|
+
this.onOpenPath = options.onOpenPath;
|
|
15705
|
+
this.onHoverSymbol = options.onHoverSymbol;
|
|
15402
15706
|
this.language = Tokenizer.getLanguage(this.highlight) ?? Tokenizer.getLanguage('Plain Text');
|
|
15403
15707
|
this.symbolTable = new SymbolTable();
|
|
15404
15708
|
// File explorer
|
|
@@ -15608,19 +15912,38 @@ class CodeEditor {
|
|
|
15608
15912
|
this.codeArea.root.addEventListener('mousedown', this._onMouseDown.bind(this));
|
|
15609
15913
|
this.codeArea.root.addEventListener('contextmenu', this._onMouseDown.bind(this));
|
|
15610
15914
|
this.codeArea.root.addEventListener('mouseover', (e) => {
|
|
15611
|
-
const
|
|
15915
|
+
const target = e.target;
|
|
15916
|
+
const link = target.closest('.code-link');
|
|
15612
15917
|
if (link && e.ctrlKey)
|
|
15613
15918
|
link.classList.add('hovered');
|
|
15919
|
+
const path = target.closest('.code-path');
|
|
15920
|
+
if (path && e.ctrlKey)
|
|
15921
|
+
path.classList.add('hovered');
|
|
15614
15922
|
});
|
|
15615
15923
|
this.codeArea.root.addEventListener('mouseout', (e) => {
|
|
15616
|
-
const
|
|
15924
|
+
const target = e.target;
|
|
15925
|
+
const link = target.closest('.code-link');
|
|
15617
15926
|
if (link)
|
|
15618
15927
|
link.classList.remove('hovered');
|
|
15928
|
+
const path = target.closest('.code-path');
|
|
15929
|
+
if (path)
|
|
15930
|
+
path.classList.remove('hovered');
|
|
15619
15931
|
});
|
|
15620
15932
|
this.codeArea.root.addEventListener('mousemove', (e) => {
|
|
15621
|
-
const
|
|
15933
|
+
const target = e.target;
|
|
15934
|
+
const link = target.closest('.code-link');
|
|
15622
15935
|
if (link)
|
|
15623
15936
|
link.classList.toggle('hovered', e.ctrlKey);
|
|
15937
|
+
const path = target.closest('.code-path');
|
|
15938
|
+
if (path)
|
|
15939
|
+
path.classList.toggle('hovered', e.ctrlKey);
|
|
15940
|
+
this._onCodeAreaMouseMove(e);
|
|
15941
|
+
});
|
|
15942
|
+
this.codeArea.root.addEventListener('mouseleave', () => {
|
|
15943
|
+
this._clearHoverPopup();
|
|
15944
|
+
});
|
|
15945
|
+
this.codeArea.root.addEventListener('click', (e) => {
|
|
15946
|
+
this._onColorSwatchClick(e);
|
|
15624
15947
|
});
|
|
15625
15948
|
// Bottom status panel
|
|
15626
15949
|
this.statusPanel = this._createStatusPanel(options);
|
|
@@ -15712,7 +16035,7 @@ class CodeEditor {
|
|
|
15712
16035
|
setText(text, language, detectLang = false) {
|
|
15713
16036
|
if (!this.currentTab)
|
|
15714
16037
|
return;
|
|
15715
|
-
this.doc.setText(text);
|
|
16038
|
+
this.doc.setText(this._normalizeText(text), true);
|
|
15716
16039
|
this.cursorSet.set(0, 0);
|
|
15717
16040
|
this.undoManager.clear();
|
|
15718
16041
|
this._lineStates = [];
|
|
@@ -15903,10 +16226,14 @@ class CodeEditor {
|
|
|
15903
16226
|
const codeTab = {
|
|
15904
16227
|
name,
|
|
15905
16228
|
dom,
|
|
15906
|
-
doc: new CodeDocument(
|
|
16229
|
+
doc: new CodeDocument((doc) => {
|
|
16230
|
+
this._setTabModified(name, true);
|
|
16231
|
+
this.onCodeChange?.(doc);
|
|
16232
|
+
}),
|
|
15907
16233
|
cursorSet: new CursorSet(),
|
|
15908
16234
|
undoManager: new UndoManager(),
|
|
15909
16235
|
language: langName,
|
|
16236
|
+
modified: false,
|
|
15910
16237
|
title: options.title ?? name
|
|
15911
16238
|
};
|
|
15912
16239
|
this._openedTabs[name] = codeTab;
|
|
@@ -15930,7 +16257,7 @@ class CodeEditor {
|
|
|
15930
16257
|
// Move into the sizer..
|
|
15931
16258
|
this.codeSizer.appendChild(dom);
|
|
15932
16259
|
if (options.text) {
|
|
15933
|
-
codeTab.doc.setText(options.text);
|
|
16260
|
+
codeTab.doc.setText(options.text, true);
|
|
15934
16261
|
codeTab.cursorSet.set(0, 0);
|
|
15935
16262
|
codeTab.undoManager.clear();
|
|
15936
16263
|
this._renderAllLines();
|
|
@@ -15991,15 +16318,14 @@ class CodeEditor {
|
|
|
15991
16318
|
}
|
|
15992
16319
|
setCustomSuggestions(suggestions) {
|
|
15993
16320
|
if (!suggestions || suggestions.constructor !== Array) {
|
|
15994
|
-
console.warn('suggestions should be
|
|
16321
|
+
console.warn('suggestions should be an array!');
|
|
15995
16322
|
return;
|
|
15996
16323
|
}
|
|
15997
|
-
this.customSuggestions = suggestions;
|
|
16324
|
+
this.customSuggestions = suggestions.map(s => typeof s === 'string' ? { label: s } : s);
|
|
15998
16325
|
}
|
|
15999
16326
|
loadFile(file, options = {}) {
|
|
16000
16327
|
const onLoad = (text, name) => {
|
|
16001
|
-
|
|
16002
|
-
text = text.replaceAll('\r', '').replaceAll(/\t|\\t/g, ' '.repeat(this.tabSize));
|
|
16328
|
+
text = this._normalizeText(text);
|
|
16003
16329
|
const ext = LX.getExtension(name);
|
|
16004
16330
|
const lang = options.language ?? (Tokenizer.getLanguage(options.language)
|
|
16005
16331
|
?? (Tokenizer.getLanguageByExtension(ext) ?? Tokenizer.getLanguage('Plain Text')));
|
|
@@ -16022,7 +16348,7 @@ class CodeEditor {
|
|
|
16022
16348
|
title: options.title ?? name,
|
|
16023
16349
|
language: langName
|
|
16024
16350
|
});
|
|
16025
|
-
this.doc.setText(text);
|
|
16351
|
+
this.doc.setText(text, true);
|
|
16026
16352
|
this.setLanguage(langName, ext);
|
|
16027
16353
|
this.cursorSet.set(0, 0);
|
|
16028
16354
|
this.undoManager.clear();
|
|
@@ -16083,7 +16409,7 @@ class CodeEditor {
|
|
|
16083
16409
|
language: langName
|
|
16084
16410
|
});
|
|
16085
16411
|
if (results.length === 0) {
|
|
16086
|
-
this.doc.setText(processedText);
|
|
16412
|
+
this.doc.setText(processedText, true);
|
|
16087
16413
|
this.setLanguage(langName, ext);
|
|
16088
16414
|
this.cursorSet.set(0, 0);
|
|
16089
16415
|
this.undoManager.clear();
|
|
@@ -16125,6 +16451,30 @@ class CodeEditor {
|
|
|
16125
16451
|
}
|
|
16126
16452
|
}, 20);
|
|
16127
16453
|
}
|
|
16454
|
+
_findTabByPath(importPath) {
|
|
16455
|
+
// By now only uses base name
|
|
16456
|
+
const importBase = importPath.split('/').pop().replace(/\.\w+$/, '').toLowerCase();
|
|
16457
|
+
const allNames = new Set([
|
|
16458
|
+
...Object.keys(this._openedTabs),
|
|
16459
|
+
...Object.keys(this._loadedTabs),
|
|
16460
|
+
...Object.keys(this._storedTabs),
|
|
16461
|
+
]);
|
|
16462
|
+
for (const name of allNames) {
|
|
16463
|
+
const tabBase = name.split('/').pop().replace(/\.\w+$/, '').toLowerCase();
|
|
16464
|
+
if (tabBase === importBase)
|
|
16465
|
+
return name;
|
|
16466
|
+
}
|
|
16467
|
+
return null;
|
|
16468
|
+
}
|
|
16469
|
+
_setTabModified(name, modified) {
|
|
16470
|
+
const tab = this._openedTabs[name];
|
|
16471
|
+
if (!tab || tab.modified === modified)
|
|
16472
|
+
return;
|
|
16473
|
+
tab.modified = modified;
|
|
16474
|
+
const tabEl = this.tabs?.tabDOMs?.[name];
|
|
16475
|
+
if (tabEl)
|
|
16476
|
+
tabEl.toggleAttribute('data-modified', modified);
|
|
16477
|
+
}
|
|
16128
16478
|
_onSelectTab(isNewTabButton, event, name) {
|
|
16129
16479
|
if (this.disableEdition) {
|
|
16130
16480
|
return;
|
|
@@ -16350,20 +16700,55 @@ class CodeEditor {
|
|
|
16350
16700
|
const lineText = this.doc.getLine(lineIndex);
|
|
16351
16701
|
const result = Tokenizer.tokenizeLine(lineText, this.language, prevState);
|
|
16352
16702
|
const langClass = this.language.name.toLowerCase().replace(/[^a-z]/g, '');
|
|
16353
|
-
|
|
16703
|
+
// Pre-compute which token index gets the bracket-highlight class
|
|
16704
|
+
let bracketTokenIdx = -1;
|
|
16705
|
+
if (lineIndex === this._bracketOpenLine) {
|
|
16706
|
+
// Last '{' symbol token on this line
|
|
16707
|
+
for (let i = result.tokens.length - 1; i >= 0; i--) {
|
|
16708
|
+
if (result.tokens[i].type === 'symbol' && result.tokens[i].value === '{') {
|
|
16709
|
+
bracketTokenIdx = i;
|
|
16710
|
+
break;
|
|
16711
|
+
}
|
|
16712
|
+
}
|
|
16713
|
+
}
|
|
16714
|
+
else if (lineIndex === this._bracketCloseLine) {
|
|
16715
|
+
// First '}' symbol token on this line
|
|
16716
|
+
for (let i = 0; i < result.tokens.length; i++) {
|
|
16717
|
+
if (result.tokens[i].type === 'symbol' && result.tokens[i].value === '}') {
|
|
16718
|
+
bracketTokenIdx = i;
|
|
16719
|
+
break;
|
|
16720
|
+
}
|
|
16721
|
+
}
|
|
16722
|
+
}
|
|
16354
16723
|
let html = '';
|
|
16355
|
-
|
|
16724
|
+
let colOffset = 0;
|
|
16725
|
+
for (let ti = 0; ti < result.tokens.length; ti++) {
|
|
16726
|
+
const token = result.tokens[ti];
|
|
16356
16727
|
const cls = TOKEN_CLASS_MAP[token.type];
|
|
16357
|
-
const
|
|
16358
|
-
|
|
16359
|
-
|
|
16360
|
-
|
|
16361
|
-
|
|
16362
|
-
|
|
16363
|
-
|
|
16364
|
-
|
|
16728
|
+
const tokenCol = colOffset;
|
|
16729
|
+
colOffset += token.value.length;
|
|
16730
|
+
// Inject content depending on type of token: color, url, path?
|
|
16731
|
+
let content;
|
|
16732
|
+
if (token.type === 'comment') {
|
|
16733
|
+
const escaped = token.value
|
|
16734
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
16735
|
+
content = escaped.replace(URL_REGEX, `<span class="code-link" data-url="$1">$1</span>`);
|
|
16736
|
+
}
|
|
16737
|
+
else if (token.type === 'string' && isImportPath(result.tokens, ti)) {
|
|
16738
|
+
const inner = token.value.slice(1, -1); // strip surrounding quotes
|
|
16739
|
+
const q = token.value[0];
|
|
16740
|
+
const escapedInner = inner.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
16741
|
+
content = `${q}<span class="code-path" data-path="${inner}">${escapedInner}</span>${q}`;
|
|
16742
|
+
}
|
|
16743
|
+
else {
|
|
16744
|
+
content = injectColorSpans(token.value, lineIndex, tokenCol);
|
|
16745
|
+
}
|
|
16746
|
+
const bracketClass = ti === bracketTokenIdx ? ' code-bracket-active' : '';
|
|
16365
16747
|
if (cls) {
|
|
16366
|
-
html += `<span class="${cls} ${langClass}">${content}</span>`;
|
|
16748
|
+
html += `<span class="${cls} ${langClass}${bracketClass}">${content}</span>`;
|
|
16749
|
+
}
|
|
16750
|
+
else if (bracketClass) {
|
|
16751
|
+
html += `<span class="${bracketClass.trim()}">${content}</span>`;
|
|
16367
16752
|
}
|
|
16368
16753
|
else {
|
|
16369
16754
|
html += content;
|
|
@@ -16444,6 +16829,13 @@ class CodeEditor {
|
|
|
16444
16829
|
break;
|
|
16445
16830
|
}
|
|
16446
16831
|
}
|
|
16832
|
+
// Propagate/cascade scope updates to subsequent lines until the start-of-line scope stabilizes
|
|
16833
|
+
for (let i = lineIndex + 1; i < this.doc.lineCount; i++) {
|
|
16834
|
+
const oldStartDepth = this.symbolTable.getScopeAtLine(i).length;
|
|
16835
|
+
this.symbolTable.updateScopeForLine(i, this.doc.getLine(i));
|
|
16836
|
+
if (this.symbolTable.getScopeAtLine(i).length === oldStartDepth)
|
|
16837
|
+
break;
|
|
16838
|
+
}
|
|
16447
16839
|
}
|
|
16448
16840
|
/**
|
|
16449
16841
|
* Rebuild line elements after structural changes (insert/delete lines).
|
|
@@ -16485,12 +16877,9 @@ class CodeEditor {
|
|
|
16485
16877
|
return;
|
|
16486
16878
|
this.cursorsLayer.innerHTML = '';
|
|
16487
16879
|
for (const sel of this.cursorSet.cursors) {
|
|
16488
|
-
const el =
|
|
16489
|
-
el.className = 'cursor';
|
|
16490
|
-
el.innerHTML = ' ';
|
|
16880
|
+
const el = LX.makeElement('div', 'cursor', ' ', this.cursorsLayer);
|
|
16491
16881
|
el.style.left = (sel.head.col * this.charWidth + this.xPadding) + 'px';
|
|
16492
16882
|
el.style.top = (sel.head.line * this.lineHeight) + 'px';
|
|
16493
|
-
this.cursorsLayer.appendChild(el);
|
|
16494
16883
|
}
|
|
16495
16884
|
this._updateActiveLine();
|
|
16496
16885
|
}
|
|
@@ -16507,14 +16896,16 @@ class CodeEditor {
|
|
|
16507
16896
|
const lineText = this.doc.getLine(line);
|
|
16508
16897
|
const fromCol = line === start.line ? start.col : 0;
|
|
16509
16898
|
const toCol = line === end.line ? end.col : lineText.length;
|
|
16510
|
-
|
|
16899
|
+
// Skip only when the selection ends exactly at col 0 of this line
|
|
16900
|
+
if (fromCol === toCol && line === end.line)
|
|
16511
16901
|
continue;
|
|
16512
|
-
const
|
|
16513
|
-
|
|
16902
|
+
const width = fromCol === toCol
|
|
16903
|
+
? Math.ceil(this.charWidth * 0.5) // minimum width for empty lines
|
|
16904
|
+
: (toCol - fromCol) * this.charWidth;
|
|
16905
|
+
const div = LX.makeElement('div', 'lexcodeselection', '', this.selectionsLayer);
|
|
16514
16906
|
div.style.top = (line * this.lineHeight) + 'px';
|
|
16515
16907
|
div.style.left = (fromCol * this.charWidth + this.xPadding) + 'px';
|
|
16516
|
-
div.style.width =
|
|
16517
|
-
this.selectionsLayer.appendChild(div);
|
|
16908
|
+
div.style.width = width + 'px';
|
|
16518
16909
|
}
|
|
16519
16910
|
}
|
|
16520
16911
|
}
|
|
@@ -16619,6 +17010,8 @@ class CodeEditor {
|
|
|
16619
17010
|
e.preventDefault();
|
|
16620
17011
|
if (this.onSave) {
|
|
16621
17012
|
this.onSave(this.getText(), this);
|
|
17013
|
+
this.undoManager.markSaved();
|
|
17014
|
+
this._setTabModified(this.currentTab.name, false);
|
|
16622
17015
|
}
|
|
16623
17016
|
return;
|
|
16624
17017
|
case 'z':
|
|
@@ -16641,6 +17034,17 @@ class CodeEditor {
|
|
|
16641
17034
|
e.preventDefault();
|
|
16642
17035
|
this._doPaste();
|
|
16643
17036
|
return;
|
|
17037
|
+
case 'home':
|
|
17038
|
+
e.preventDefault();
|
|
17039
|
+
this.cursorSet.set(0, 0);
|
|
17040
|
+
this._afterCursorMove();
|
|
17041
|
+
return;
|
|
17042
|
+
case 'end':
|
|
17043
|
+
e.preventDefault();
|
|
17044
|
+
const lastLine = this.doc.lineCount - 1;
|
|
17045
|
+
this.cursorSet.set(lastLine, this.doc.getLine(lastLine).length);
|
|
17046
|
+
this._afterCursorMove();
|
|
17047
|
+
return;
|
|
16644
17048
|
case ' ':
|
|
16645
17049
|
e.preventDefault();
|
|
16646
17050
|
// Also call user callback if provided
|
|
@@ -17372,10 +17776,21 @@ class CodeEditor {
|
|
|
17372
17776
|
this._rebuildLines();
|
|
17373
17777
|
this._afterCursorMove();
|
|
17374
17778
|
}
|
|
17779
|
+
/**
|
|
17780
|
+
* Normalize external text before inserting into the document:
|
|
17781
|
+
* - Unify line endings to \n
|
|
17782
|
+
* - Replace tab characters with the configured number of spaces
|
|
17783
|
+
*/
|
|
17784
|
+
_normalizeText(text) {
|
|
17785
|
+
return text
|
|
17786
|
+
.replace(/\r\n?/g, '\n')
|
|
17787
|
+
.replace(/\t/g, ' '.repeat(this.tabSize));
|
|
17788
|
+
}
|
|
17375
17789
|
async _doPaste() {
|
|
17376
|
-
const
|
|
17377
|
-
if (!
|
|
17790
|
+
const raw = await navigator.clipboard.readText();
|
|
17791
|
+
if (!raw)
|
|
17378
17792
|
return;
|
|
17793
|
+
const text = this._normalizeText(raw);
|
|
17379
17794
|
this._flushAction();
|
|
17380
17795
|
this._deleteSelectionIfAny();
|
|
17381
17796
|
const cursor = this.cursorSet.getPrimary();
|
|
@@ -17402,6 +17817,8 @@ class CodeEditor {
|
|
|
17402
17817
|
}
|
|
17403
17818
|
this._rebuildLines();
|
|
17404
17819
|
this._afterCursorMove();
|
|
17820
|
+
if (this.currentTab)
|
|
17821
|
+
this._setTabModified(this.currentTab.name, this.undoManager.isModified());
|
|
17405
17822
|
}
|
|
17406
17823
|
}
|
|
17407
17824
|
_doRedo() {
|
|
@@ -17413,6 +17830,8 @@ class CodeEditor {
|
|
|
17413
17830
|
}
|
|
17414
17831
|
this._rebuildLines();
|
|
17415
17832
|
this._afterCursorMove();
|
|
17833
|
+
if (this.currentTab)
|
|
17834
|
+
this._setTabModified(this.currentTab.name, this.undoManager.isModified());
|
|
17416
17835
|
}
|
|
17417
17836
|
}
|
|
17418
17837
|
// Mouse input events:
|
|
@@ -17423,7 +17842,7 @@ class CodeEditor {
|
|
|
17423
17842
|
return;
|
|
17424
17843
|
if (this.autocomplete && this.autocomplete.contains(e.target))
|
|
17425
17844
|
return;
|
|
17426
|
-
// Ctrl+click: open link
|
|
17845
|
+
// Ctrl+click: open link or import path
|
|
17427
17846
|
if (e.ctrlKey && e.button === 0) {
|
|
17428
17847
|
const target = e.target;
|
|
17429
17848
|
const link = target.closest('.code-link');
|
|
@@ -17431,6 +17850,15 @@ class CodeEditor {
|
|
|
17431
17850
|
window.open(link.dataset.url, '_blank');
|
|
17432
17851
|
return;
|
|
17433
17852
|
}
|
|
17853
|
+
const pathEl = target.closest('.code-path');
|
|
17854
|
+
if (pathEl?.dataset.path) {
|
|
17855
|
+
const rawPath = pathEl.dataset.path;
|
|
17856
|
+
const tabName = this._findTabByPath(rawPath);
|
|
17857
|
+
if (tabName)
|
|
17858
|
+
this.loadTab(tabName);
|
|
17859
|
+
this.onOpenPath?.(rawPath, this);
|
|
17860
|
+
return;
|
|
17861
|
+
}
|
|
17434
17862
|
}
|
|
17435
17863
|
e.preventDefault(); // Prevent browser from stealing focus from _inputArea
|
|
17436
17864
|
this._wasPaired = false;
|
|
@@ -17478,18 +17906,55 @@ class CodeEditor {
|
|
|
17478
17906
|
}
|
|
17479
17907
|
this._afterCursorMove();
|
|
17480
17908
|
this._inputArea.focus();
|
|
17481
|
-
// Track mouse for drag selection
|
|
17482
|
-
|
|
17483
|
-
|
|
17484
|
-
|
|
17485
|
-
|
|
17486
|
-
const
|
|
17909
|
+
// Track mouse for drag selection (with auto-scroll when outside editor window/area)
|
|
17910
|
+
let lastMouseX = 0;
|
|
17911
|
+
let lastMouseY = 0;
|
|
17912
|
+
let rafId = null;
|
|
17913
|
+
const updateSelection = () => {
|
|
17914
|
+
const currentRect = this.codeContainer.getBoundingClientRect();
|
|
17915
|
+
const mx = lastMouseX - currentRect.left - this.xPadding;
|
|
17916
|
+
const my = lastMouseY - currentRect.top;
|
|
17917
|
+
const ml = LX.clamp(Math.floor(my / this.lineHeight), 0, this.doc.lineCount - 1);
|
|
17918
|
+
const mc = LX.clamp(Math.round(mx / this.charWidth), 0, this.doc.getLine(ml).length);
|
|
17487
17919
|
const sel = this.cursorSet.getPrimary();
|
|
17488
17920
|
sel.head = { line: ml, col: mc };
|
|
17489
17921
|
this._renderCursors();
|
|
17490
17922
|
this._renderSelections();
|
|
17491
17923
|
};
|
|
17924
|
+
const autoScroll = () => {
|
|
17925
|
+
const scrollerRect = this.codeScroller.getBoundingClientRect();
|
|
17926
|
+
const overshootY = lastMouseY < scrollerRect.top ? lastMouseY - scrollerRect.top
|
|
17927
|
+
: lastMouseY > scrollerRect.bottom ? lastMouseY - scrollerRect.bottom : 0;
|
|
17928
|
+
const overshootX = lastMouseX < scrollerRect.left ? lastMouseX - scrollerRect.left
|
|
17929
|
+
: lastMouseX > scrollerRect.right ? lastMouseX - scrollerRect.right : 0;
|
|
17930
|
+
if (overshootY === 0 && overshootX === 0) {
|
|
17931
|
+
rafId = null;
|
|
17932
|
+
return;
|
|
17933
|
+
}
|
|
17934
|
+
const speedY = Math.sign(overshootY) * Math.min(Math.abs(overshootY) * 0.3, 15);
|
|
17935
|
+
const speedX = Math.sign(overshootX) * Math.min(Math.abs(overshootX) * 0.3, 15);
|
|
17936
|
+
this.codeScroller.scrollTop += speedY;
|
|
17937
|
+
this.codeScroller.scrollLeft += speedX;
|
|
17938
|
+
this._syncScrollBars();
|
|
17939
|
+
updateSelection();
|
|
17940
|
+
rafId = requestAnimationFrame(autoScroll);
|
|
17941
|
+
};
|
|
17942
|
+
const onMouseMove = (me) => {
|
|
17943
|
+
lastMouseX = me.clientX;
|
|
17944
|
+
lastMouseY = me.clientY;
|
|
17945
|
+
updateSelection();
|
|
17946
|
+
const scrollerRect = this.codeScroller.getBoundingClientRect();
|
|
17947
|
+
const isOutside = me.clientY < scrollerRect.top || me.clientY > scrollerRect.bottom
|
|
17948
|
+
|| me.clientX < scrollerRect.left || me.clientX > scrollerRect.right;
|
|
17949
|
+
if (isOutside && rafId === null) {
|
|
17950
|
+
rafId = requestAnimationFrame(autoScroll);
|
|
17951
|
+
}
|
|
17952
|
+
};
|
|
17492
17953
|
const onMouseUp = () => {
|
|
17954
|
+
if (rafId !== null) {
|
|
17955
|
+
cancelAnimationFrame(rafId);
|
|
17956
|
+
rafId = null;
|
|
17957
|
+
}
|
|
17493
17958
|
document.removeEventListener('mousemove', onMouseMove);
|
|
17494
17959
|
document.removeEventListener('mouseup', onMouseUp);
|
|
17495
17960
|
};
|
|
@@ -17549,47 +18014,51 @@ class CodeEditor {
|
|
|
17549
18014
|
}
|
|
17550
18015
|
const suggestions = [];
|
|
17551
18016
|
const added = new Set();
|
|
17552
|
-
const addSuggestion = (
|
|
17553
|
-
if (!added.has(label)) {
|
|
17554
|
-
suggestions.push(
|
|
17555
|
-
added.add(label);
|
|
18017
|
+
const addSuggestion = (s) => {
|
|
18018
|
+
if (!added.has(s.label)) {
|
|
18019
|
+
suggestions.push(s);
|
|
18020
|
+
added.add(s.label);
|
|
17556
18021
|
}
|
|
17557
18022
|
};
|
|
18023
|
+
const filterSuggestion = (suggestion, word) => {
|
|
18024
|
+
const w = word.toLowerCase();
|
|
18025
|
+
if (suggestion.filterText) {
|
|
18026
|
+
return suggestion.filterText.split(' ').some(token => token.toLowerCase().trim().startsWith(w));
|
|
18027
|
+
}
|
|
18028
|
+
return suggestion.label.toLowerCase().startsWith(w);
|
|
18029
|
+
};
|
|
17558
18030
|
// Get first suggestions from symbol table
|
|
17559
18031
|
const allSymbols = this.symbolTable.getAllSymbols();
|
|
17560
18032
|
for (const symbol of allSymbols) {
|
|
17561
|
-
|
|
17562
|
-
|
|
17563
|
-
|
|
18033
|
+
const s = { label: symbol.name, kind: symbol.kind, scope: symbol.scope, detail: `${symbol.kind} in ${symbol.scope}` };
|
|
18034
|
+
if (filterSuggestion(s, word))
|
|
18035
|
+
addSuggestion(s);
|
|
17564
18036
|
}
|
|
17565
18037
|
// Add language reserved keys
|
|
17566
18038
|
for (const reservedWord of this.language.reservedWords) {
|
|
17567
|
-
|
|
17568
|
-
|
|
17569
|
-
|
|
18039
|
+
const s = { label: reservedWord };
|
|
18040
|
+
if (filterSuggestion(s, word))
|
|
18041
|
+
addSuggestion(s);
|
|
17570
18042
|
}
|
|
17571
18043
|
// Add custom suggestions
|
|
17572
18044
|
for (const suggestion of this.customSuggestions) {
|
|
17573
|
-
|
|
17574
|
-
|
|
17575
|
-
const detail = typeof suggestion === 'object' ? suggestion.detail : undefined;
|
|
17576
|
-
const insertText = typeof suggestion === 'object' ? suggestion.insertText : suggestion;
|
|
17577
|
-
if (label.toLowerCase().startsWith(word.toLowerCase())) {
|
|
17578
|
-
addSuggestion(label, kind, undefined, detail, insertText);
|
|
17579
|
-
}
|
|
18045
|
+
if (filterSuggestion(suggestion, word))
|
|
18046
|
+
addSuggestion(suggestion);
|
|
17580
18047
|
}
|
|
17581
|
-
// Close autocomplete if no suggestions
|
|
17582
18048
|
if (suggestions.length === 0) {
|
|
17583
18049
|
this._doHideAutocomplete();
|
|
17584
18050
|
return;
|
|
17585
18051
|
}
|
|
17586
|
-
// Sort suggestions: exact matches first, then
|
|
18052
|
+
// Sort suggestions: exact matches first, then by sortText (or label if absent)
|
|
18053
|
+
const w = word.toLowerCase();
|
|
17587
18054
|
suggestions.sort((a, b) => {
|
|
17588
|
-
const
|
|
17589
|
-
const
|
|
18055
|
+
const aKey = (a.sortText ?? a.label).toLowerCase();
|
|
18056
|
+
const bKey = (b.sortText ?? b.label).toLowerCase();
|
|
18057
|
+
const aExact = aKey === w ? 0 : 1;
|
|
18058
|
+
const bExact = bKey === w ? 0 : 1;
|
|
17590
18059
|
if (aExact !== bExact)
|
|
17591
18060
|
return aExact - bExact;
|
|
17592
|
-
return
|
|
18061
|
+
return aKey.localeCompare(bKey);
|
|
17593
18062
|
});
|
|
17594
18063
|
this._selectedAutocompleteIndex = 0;
|
|
17595
18064
|
// Render suggestions
|
|
@@ -17598,9 +18067,9 @@ class CodeEditor {
|
|
|
17598
18067
|
item.insertText = suggestion.insertText ?? suggestion.label;
|
|
17599
18068
|
if (index === this._selectedAutocompleteIndex)
|
|
17600
18069
|
item.classList.add('selected');
|
|
17601
|
-
const
|
|
17602
|
-
let iconName = 'CaseLower';
|
|
17603
|
-
let iconClass = '
|
|
18070
|
+
const currSuggestionLabel = suggestion.label;
|
|
18071
|
+
let iconName = suggestion.icon ?? 'CaseLower';
|
|
18072
|
+
let iconClass = suggestion.iconClass ?? 'text-gray-500';
|
|
17604
18073
|
switch (suggestion.kind) {
|
|
17605
18074
|
case 'class':
|
|
17606
18075
|
iconName = 'CircleNodes';
|
|
@@ -17647,26 +18116,22 @@ class CodeEditor {
|
|
|
17647
18116
|
iconClass = 'text-green-500';
|
|
17648
18117
|
break;
|
|
17649
18118
|
case 'method-call':
|
|
17650
|
-
iconName = '
|
|
18119
|
+
iconName = 'Parentheses';
|
|
17651
18120
|
iconClass = 'text-gray-400';
|
|
17652
18121
|
break;
|
|
17653
|
-
default:
|
|
17654
|
-
iconName = 'CaseLower';
|
|
17655
|
-
iconClass = 'text-gray-500';
|
|
17656
|
-
break;
|
|
17657
18122
|
}
|
|
17658
18123
|
item.appendChild(LX.makeIcon(iconName, { iconClass: 'ml-1 mr-2', svgClass: 'sm ' + iconClass }));
|
|
17659
18124
|
// Highlight the written part
|
|
17660
|
-
const hIndex =
|
|
18125
|
+
const hIndex = currSuggestionLabel.toLowerCase().indexOf(word.toLowerCase());
|
|
17661
18126
|
var preWord = document.createElement('span');
|
|
17662
|
-
preWord.textContent =
|
|
18127
|
+
preWord.textContent = currSuggestionLabel.substring(0, hIndex);
|
|
17663
18128
|
item.appendChild(preWord);
|
|
17664
18129
|
var actualWord = document.createElement('span');
|
|
17665
|
-
actualWord.textContent =
|
|
18130
|
+
actualWord.textContent = currSuggestionLabel.substring(hIndex, hIndex + word.length);
|
|
17666
18131
|
actualWord.classList.add('word-highlight');
|
|
17667
18132
|
item.appendChild(actualWord);
|
|
17668
18133
|
var postWord = document.createElement('span');
|
|
17669
|
-
postWord.textContent =
|
|
18134
|
+
postWord.textContent = currSuggestionLabel.substring(hIndex + word.length);
|
|
17670
18135
|
item.appendChild(postWord);
|
|
17671
18136
|
if (suggestion.kind) {
|
|
17672
18137
|
const kind = document.createElement('span');
|
|
@@ -17751,6 +18216,340 @@ class CodeEditor {
|
|
|
17751
18216
|
this._resetBlinker();
|
|
17752
18217
|
this.resize();
|
|
17753
18218
|
this._scrollCursorIntoView();
|
|
18219
|
+
this._updateBracketHighlight();
|
|
18220
|
+
}
|
|
18221
|
+
/**
|
|
18222
|
+
* Returns the scope stack at the exact cursor position (line + column).
|
|
18223
|
+
* Basically starts from getScopeAtLine and then counts real braces up to the cursor column.
|
|
18224
|
+
*/
|
|
18225
|
+
_getScopeAtCursor() {
|
|
18226
|
+
const cursor = this.cursorSet.getPrimary().head;
|
|
18227
|
+
const line = cursor.line;
|
|
18228
|
+
const col = cursor.col;
|
|
18229
|
+
const lineText = this.doc.getLine(line);
|
|
18230
|
+
const scopeStack = [...this.symbolTable.getScopeAtLine(line)];
|
|
18231
|
+
let i = 0;
|
|
18232
|
+
let inString = false;
|
|
18233
|
+
let stringCh = '';
|
|
18234
|
+
while (i < col && i < lineText.length) {
|
|
18235
|
+
const ch = lineText[i];
|
|
18236
|
+
if (inString) {
|
|
18237
|
+
if (ch === '\\') {
|
|
18238
|
+
i += 2;
|
|
18239
|
+
continue;
|
|
18240
|
+
}
|
|
18241
|
+
if (ch === stringCh)
|
|
18242
|
+
inString = false;
|
|
18243
|
+
i++;
|
|
18244
|
+
continue;
|
|
18245
|
+
}
|
|
18246
|
+
if (ch === '/' && lineText[i + 1] === '/')
|
|
18247
|
+
break;
|
|
18248
|
+
if (ch === '/' && lineText[i + 1] === '*') {
|
|
18249
|
+
i += 2;
|
|
18250
|
+
while (i < col && !(lineText[i] === '*' && lineText[i + 1] === '/'))
|
|
18251
|
+
i++;
|
|
18252
|
+
i += 2;
|
|
18253
|
+
continue;
|
|
18254
|
+
}
|
|
18255
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
18256
|
+
inString = true;
|
|
18257
|
+
stringCh = ch;
|
|
18258
|
+
i++;
|
|
18259
|
+
continue;
|
|
18260
|
+
}
|
|
18261
|
+
if (ch === '{') {
|
|
18262
|
+
scopeStack.push({ name: 'anonymous', type: 'anonymous', line });
|
|
18263
|
+
}
|
|
18264
|
+
else if (ch === '}' && scopeStack.length > 1) {
|
|
18265
|
+
scopeStack.pop();
|
|
18266
|
+
}
|
|
18267
|
+
i++;
|
|
18268
|
+
}
|
|
18269
|
+
return scopeStack;
|
|
18270
|
+
}
|
|
18271
|
+
_updateBracketHighlight() {
|
|
18272
|
+
const scopes = this._getScopeAtCursor();
|
|
18273
|
+
// Find innermost non-global scope
|
|
18274
|
+
let innermost = null;
|
|
18275
|
+
for (let i = scopes.length - 1; i >= 0; i--) {
|
|
18276
|
+
if (scopes[i].type !== 'global') {
|
|
18277
|
+
innermost = scopes[i];
|
|
18278
|
+
break;
|
|
18279
|
+
}
|
|
18280
|
+
}
|
|
18281
|
+
const prevOpen = this._bracketOpenLine;
|
|
18282
|
+
const prevClose = this._bracketCloseLine;
|
|
18283
|
+
if (!innermost) {
|
|
18284
|
+
this._bracketOpenLine = -1;
|
|
18285
|
+
this._bracketCloseLine = -1;
|
|
18286
|
+
}
|
|
18287
|
+
else {
|
|
18288
|
+
const openLine = innermost.line;
|
|
18289
|
+
const targetDepth = scopes.length; // depth including the innermost scope
|
|
18290
|
+
// Closing line: last line where scope depth >= targetDepth
|
|
18291
|
+
let closeLine = openLine;
|
|
18292
|
+
for (let i = openLine + 1; i < this.doc.lineCount; i++) {
|
|
18293
|
+
if (this.symbolTable.getScopeAtLine(i).length >= targetDepth)
|
|
18294
|
+
closeLine = i;
|
|
18295
|
+
else
|
|
18296
|
+
break;
|
|
18297
|
+
}
|
|
18298
|
+
this._bracketOpenLine = openLine;
|
|
18299
|
+
this._bracketCloseLine = closeLine;
|
|
18300
|
+
}
|
|
18301
|
+
// Re-render only the lines that changed
|
|
18302
|
+
const linesToUpdate = new Set();
|
|
18303
|
+
if (prevOpen !== this._bracketOpenLine) {
|
|
18304
|
+
linesToUpdate.add(prevOpen);
|
|
18305
|
+
linesToUpdate.add(this._bracketOpenLine);
|
|
18306
|
+
}
|
|
18307
|
+
if (prevClose !== this._bracketCloseLine) {
|
|
18308
|
+
linesToUpdate.add(prevClose);
|
|
18309
|
+
linesToUpdate.add(this._bracketCloseLine);
|
|
18310
|
+
}
|
|
18311
|
+
for (const line of linesToUpdate) {
|
|
18312
|
+
if (line >= 0 && line < this.doc.lineCount)
|
|
18313
|
+
this._updateLine(line);
|
|
18314
|
+
}
|
|
18315
|
+
}
|
|
18316
|
+
// Color picker:
|
|
18317
|
+
_onColorSwatchClick(e) {
|
|
18318
|
+
const span = e.target.closest('.code-color');
|
|
18319
|
+
if (!span)
|
|
18320
|
+
return;
|
|
18321
|
+
e.stopPropagation();
|
|
18322
|
+
e.preventDefault();
|
|
18323
|
+
const colorValue = span.dataset.color;
|
|
18324
|
+
const lineIndex = parseInt(span.dataset.line);
|
|
18325
|
+
const colStart = parseInt(span.dataset.col);
|
|
18326
|
+
let currentLen = colorValue.length;
|
|
18327
|
+
if (this._colorPopover) {
|
|
18328
|
+
this._colorPopover.destroy();
|
|
18329
|
+
this._colorPopover = null;
|
|
18330
|
+
}
|
|
18331
|
+
const picker = new LX.ColorPicker(colorValue, {
|
|
18332
|
+
colorModel: 'Hex',
|
|
18333
|
+
onChange: (color) => {
|
|
18334
|
+
// Generate a hex string matching the original length (# + 3/4/6/8 hex chars)
|
|
18335
|
+
const raw = color.hex.replace(/^#/, '');
|
|
18336
|
+
const digits = currentLen - 1;
|
|
18337
|
+
const newHex = '#' + (digits <= 4
|
|
18338
|
+
? raw.slice(0, digits).padEnd(digits, '0')
|
|
18339
|
+
: raw.slice(0, Math.min(digits, 8)).padEnd(digits, '0'));
|
|
18340
|
+
const lineText = this.doc.getLine(lineIndex);
|
|
18341
|
+
if (lineText.slice(colStart, colStart + currentLen) !== span.dataset.color)
|
|
18342
|
+
return;
|
|
18343
|
+
const delOp = this.doc.delete(lineIndex, colStart, currentLen);
|
|
18344
|
+
this.undoManager.record(delOp, this.cursorSet.getCursorPositions());
|
|
18345
|
+
const insOp = this.doc.insert(lineIndex, colStart, newHex);
|
|
18346
|
+
this.undoManager.record(insOp, this.cursorSet.getCursorPositions());
|
|
18347
|
+
this._updateLine(lineIndex);
|
|
18348
|
+
currentLen = newHex.length;
|
|
18349
|
+
span.dataset.color = newHex;
|
|
18350
|
+
span.dataset.col = String(colStart);
|
|
18351
|
+
span.style.setProperty('--code-color', newHex);
|
|
18352
|
+
}
|
|
18353
|
+
});
|
|
18354
|
+
this._colorPopover = new LX.Popover(span, [picker], { side: 'bottom', align: 'start', sideOffset: 4 });
|
|
18355
|
+
}
|
|
18356
|
+
// Symbol hover:
|
|
18357
|
+
/**
|
|
18358
|
+
* Extracts the parameter list from a function/method declaration line.
|
|
18359
|
+
*/
|
|
18360
|
+
_getSymbolParams(symLine) {
|
|
18361
|
+
if (symLine < 0 || symLine >= this.doc.lineCount)
|
|
18362
|
+
return '()';
|
|
18363
|
+
const m = this.doc.getLine(symLine).match(/\(([^)]*)\)/);
|
|
18364
|
+
return m ? `(${m[1].trim()})` : '()';
|
|
18365
|
+
}
|
|
18366
|
+
/**
|
|
18367
|
+
* Starting from the line where a class is defined, scans forward to find
|
|
18368
|
+
* the constructor signature and returns its parameter list.
|
|
18369
|
+
*/
|
|
18370
|
+
_findConstructorParams(classLine) {
|
|
18371
|
+
const classDepth = this.symbolTable.getScopeAtLine(classLine).length;
|
|
18372
|
+
const maxScan = Math.min(classLine + 50, this.doc.lineCount);
|
|
18373
|
+
for (let i = classLine + 1; i < maxScan; i++) {
|
|
18374
|
+
if (this.symbolTable.getScopeAtLine(i).length < classDepth + 1)
|
|
18375
|
+
break;
|
|
18376
|
+
const m = this.doc.getLine(i).match(/\bconstructor\s*\(([^)]*)\)/);
|
|
18377
|
+
if (m)
|
|
18378
|
+
return `(${m[1].trim()})`;
|
|
18379
|
+
}
|
|
18380
|
+
return null;
|
|
18381
|
+
}
|
|
18382
|
+
/**
|
|
18383
|
+
* Given multiple symbols with the same name, pick the most likely one for
|
|
18384
|
+
* the hovered position using the available context data.
|
|
18385
|
+
*/
|
|
18386
|
+
_pickBestSymbol(symbols, word, tokenType, lineText, hoveredLine) {
|
|
18387
|
+
if (symbols.length === 1)
|
|
18388
|
+
return symbols[0];
|
|
18389
|
+
const curScopes = this.symbolTable.getScopeAtLine(hoveredLine);
|
|
18390
|
+
const curScope = [...curScopes].reverse().find(s => s.type !== 'global')?.name ?? 'global';
|
|
18391
|
+
const wordEsc = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
18392
|
+
const isNewCall = new RegExp(`\\bnew\\s+${wordEsc}\\b`).test(lineText);
|
|
18393
|
+
const isFuncCall = new RegExp(`\\b${wordEsc}\\s*[(<]`).test(lineText);
|
|
18394
|
+
const isTypeAnno = new RegExp(`(?::\\s*|<\\s*|[,>]\\s*)${wordEsc}\\b`).test(lineText);
|
|
18395
|
+
const typeKinds = new Set(['class', 'interface', 'type', 'enum', 'struct']);
|
|
18396
|
+
const funcKinds = new Set(['function', 'method']);
|
|
18397
|
+
const scored = symbols.map(sym => {
|
|
18398
|
+
let score = 0;
|
|
18399
|
+
// Defined in the current innermost scope
|
|
18400
|
+
if (sym.scope === curScope)
|
|
18401
|
+
score += 5;
|
|
18402
|
+
// Check token type
|
|
18403
|
+
if (tokenType === 'method' && funcKinds.has(sym.kind))
|
|
18404
|
+
score += 3;
|
|
18405
|
+
if (tokenType === 'type' && typeKinds.has(sym.kind))
|
|
18406
|
+
score += 3;
|
|
18407
|
+
// Hovered-line context patterns
|
|
18408
|
+
if (isNewCall && sym.kind === 'constructor-call')
|
|
18409
|
+
score += 20;
|
|
18410
|
+
if (isNewCall && sym.kind === 'class')
|
|
18411
|
+
score += 3;
|
|
18412
|
+
if (isFuncCall && funcKinds.has(sym.kind))
|
|
18413
|
+
score += 3;
|
|
18414
|
+
if (isTypeAnno && typeKinds.has(sym.kind))
|
|
18415
|
+
score += 2;
|
|
18416
|
+
// Validate kind based on symbol declaration line
|
|
18417
|
+
if (sym.line >= 0 && sym.line < this.doc.lineCount) {
|
|
18418
|
+
const declLine = this.doc.getLine(sym.line);
|
|
18419
|
+
if (/\bfunction\b/.test(declLine) && funcKinds.has(sym.kind))
|
|
18420
|
+
score += 4;
|
|
18421
|
+
if (/\bclass\b/.test(declLine) && sym.kind === 'class')
|
|
18422
|
+
score += 4;
|
|
18423
|
+
if (/\binterface\b/.test(declLine) && sym.kind === 'interface')
|
|
18424
|
+
score += 4;
|
|
18425
|
+
if (/\btype\b/.test(declLine) && sym.kind === 'type')
|
|
18426
|
+
score += 4;
|
|
18427
|
+
if (/\benum\b/.test(declLine) && sym.kind === 'enum')
|
|
18428
|
+
score += 4;
|
|
18429
|
+
if (/\b(?:const|let|var)\b/.test(declLine) && sym.kind === 'variable')
|
|
18430
|
+
score += 3;
|
|
18431
|
+
}
|
|
18432
|
+
// Order also by line proximity
|
|
18433
|
+
const dist = Math.abs(sym.line - hoveredLine);
|
|
18434
|
+
score += Math.max(0, 4 - Math.floor(dist / 20));
|
|
18435
|
+
return { sym, score };
|
|
18436
|
+
});
|
|
18437
|
+
scored.sort((a, b) => b.score - a.score);
|
|
18438
|
+
return scored[0].sym;
|
|
18439
|
+
}
|
|
18440
|
+
_clearHoverPopup() {
|
|
18441
|
+
if (this._hoverTimer) {
|
|
18442
|
+
clearTimeout(this._hoverTimer);
|
|
18443
|
+
this._hoverTimer = null;
|
|
18444
|
+
}
|
|
18445
|
+
if (this._hoverPopup) {
|
|
18446
|
+
this._hoverPopup.remove();
|
|
18447
|
+
this._hoverPopup = null;
|
|
18448
|
+
}
|
|
18449
|
+
this._hoverWord = '';
|
|
18450
|
+
}
|
|
18451
|
+
_onCodeAreaMouseMove(e) {
|
|
18452
|
+
if (!this.currentTab)
|
|
18453
|
+
return;
|
|
18454
|
+
// Only show hover when no button is pressed (no dragging)
|
|
18455
|
+
if (e.buttons !== 0) {
|
|
18456
|
+
this._clearHoverPopup();
|
|
18457
|
+
return;
|
|
18458
|
+
}
|
|
18459
|
+
const rect = this.codeContainer.getBoundingClientRect();
|
|
18460
|
+
const x = e.clientX - rect.left - this.xPadding;
|
|
18461
|
+
const y = e.clientY - rect.top;
|
|
18462
|
+
const line = LX.clamp(Math.floor(y / this.lineHeight), 0, this.doc.lineCount - 1);
|
|
18463
|
+
const col = LX.clamp(Math.round(x / this.charWidth), 0, this.doc.getLine(line).length);
|
|
18464
|
+
const [word, wordStart] = this.doc.getWordAt(line, col);
|
|
18465
|
+
if (!word || word === this._hoverWord)
|
|
18466
|
+
return;
|
|
18467
|
+
this._clearHoverPopup();
|
|
18468
|
+
if (!word.trim())
|
|
18469
|
+
return;
|
|
18470
|
+
this._hoverWord = word;
|
|
18471
|
+
this._hoverTimer = setTimeout(() => {
|
|
18472
|
+
this._hoverTimer = null;
|
|
18473
|
+
const prevState = line > 0 ? (this._lineStates[line - 1] ?? Tokenizer.initialState()) : Tokenizer.initialState();
|
|
18474
|
+
const { tokens } = Tokenizer.tokenizeLine(this.doc.getLine(line), this.language, prevState);
|
|
18475
|
+
let tokenType = 'text';
|
|
18476
|
+
let charPos = 0;
|
|
18477
|
+
for (const tok of tokens) {
|
|
18478
|
+
// Use wordStart (not col) so boundary positions like col=wordEnd don't fall into the next token
|
|
18479
|
+
if (wordStart >= charPos && wordStart < charPos + tok.value.length) {
|
|
18480
|
+
tokenType = tok.type;
|
|
18481
|
+
break;
|
|
18482
|
+
}
|
|
18483
|
+
charPos += tok.value.length;
|
|
18484
|
+
}
|
|
18485
|
+
if (tokenType === 'comment' || tokenType === 'string' || tokenType === 'number')
|
|
18486
|
+
return;
|
|
18487
|
+
const symbols = this.symbolTable.getSymbols(word);
|
|
18488
|
+
const info = { word, tokenType, symbols };
|
|
18489
|
+
let userContent;
|
|
18490
|
+
if (this.onHoverSymbol)
|
|
18491
|
+
userContent = this.onHoverSymbol(info, this);
|
|
18492
|
+
// No popup if user explicitly returns null
|
|
18493
|
+
if (userContent === null)
|
|
18494
|
+
return;
|
|
18495
|
+
const hasSymbols = symbols.length > 0;
|
|
18496
|
+
if (!userContent && !hasSymbols)
|
|
18497
|
+
return;
|
|
18498
|
+
const popup = this._hoverPopup = LX.makeElement('div', 'code-hover-popup [&_span]:text-sm');
|
|
18499
|
+
if (userContent) {
|
|
18500
|
+
if (typeof userContent === 'string')
|
|
18501
|
+
popup.innerHTML = userContent;
|
|
18502
|
+
else
|
|
18503
|
+
popup.appendChild(userContent);
|
|
18504
|
+
}
|
|
18505
|
+
else {
|
|
18506
|
+
const sym = this._pickBestSymbol(symbols, word, tokenType, this.doc.getLine(line), line);
|
|
18507
|
+
let kindLabel = sym.kind;
|
|
18508
|
+
let nameLabel = `<span class="font-semibold">${word}</span>`;
|
|
18509
|
+
if (sym.kind === 'constructor-call') {
|
|
18510
|
+
const classSym = this.symbolTable.getSymbols(word).find(s => s.kind === 'class');
|
|
18511
|
+
const params = classSym != null ? (this._findConstructorParams(classSym.line) ?? '()') : '()';
|
|
18512
|
+
kindLabel = 'constructor';
|
|
18513
|
+
nameLabel = `${nameLabel}<span class="text-muted-foreground">${params}</span>`;
|
|
18514
|
+
}
|
|
18515
|
+
else if (sym.kind === 'function') {
|
|
18516
|
+
const params = this._getSymbolParams(sym.line);
|
|
18517
|
+
nameLabel = `${nameLabel}<span class="text-muted-foreground">${params}</span>`;
|
|
18518
|
+
}
|
|
18519
|
+
else if (sym.scope && sym.scope !== 'global' && sym.scope !== 'anonymous') {
|
|
18520
|
+
if (sym.kind === 'property' || sym.kind === 'method') {
|
|
18521
|
+
const scopePrefix = `<span class="text-muted-foreground">${sym.scope}.</span>`;
|
|
18522
|
+
if (sym.kind === 'method') {
|
|
18523
|
+
const params = this._getSymbolParams(sym.line);
|
|
18524
|
+
nameLabel = `${scopePrefix}${nameLabel}<span class="text-muted-foreground">${params}</span>`;
|
|
18525
|
+
}
|
|
18526
|
+
else {
|
|
18527
|
+
nameLabel = `${scopePrefix}${nameLabel}`;
|
|
18528
|
+
}
|
|
18529
|
+
}
|
|
18530
|
+
else if (sym.kind === 'variable') {
|
|
18531
|
+
// Only prefix the scope when the variable is a member (class/interface/struct field)
|
|
18532
|
+
const declScopes = this.symbolTable.getScopeAtLine(sym.line);
|
|
18533
|
+
const scopeEntry = declScopes.find(s => s.name === sym.scope);
|
|
18534
|
+
const memberTypes = new Set(['class', 'interface', 'struct']);
|
|
18535
|
+
if (scopeEntry && memberTypes.has(scopeEntry.type)) {
|
|
18536
|
+
nameLabel = `<span class="text-muted-foreground">${sym.scope}.</span>${nameLabel}`;
|
|
18537
|
+
}
|
|
18538
|
+
}
|
|
18539
|
+
}
|
|
18540
|
+
popup.innerHTML = `<span class="text-info">(${kindLabel})</span> ${nameLabel}`;
|
|
18541
|
+
}
|
|
18542
|
+
document.body.appendChild(popup);
|
|
18543
|
+
// Position just below the hovered word
|
|
18544
|
+
const lineEl = this._lineElements[line];
|
|
18545
|
+
if (lineEl) {
|
|
18546
|
+
const elRect = lineEl.getBoundingClientRect();
|
|
18547
|
+
const leftPx = elRect.left + this.xPadding + col * this.charWidth;
|
|
18548
|
+
const topPx = elRect.bottom + 4;
|
|
18549
|
+
popup.style.left = Math.min(leftPx, window.innerWidth - popup.offsetWidth - 8) + 'px';
|
|
18550
|
+
popup.style.top = topPx + 'px';
|
|
18551
|
+
}
|
|
18552
|
+
}, 500);
|
|
17754
18553
|
}
|
|
17755
18554
|
// Scrollbar & Resize:
|
|
17756
18555
|
_scrollCursorIntoView() {
|
|
@@ -18830,7 +19629,7 @@ class AssetView {
|
|
|
18830
19629
|
}
|
|
18831
19630
|
}
|
|
18832
19631
|
else if (isFolder) {
|
|
18833
|
-
that.
|
|
19632
|
+
that._requestEnterFolder(item);
|
|
18834
19633
|
return;
|
|
18835
19634
|
}
|
|
18836
19635
|
const onSelect = that._callbacks['select'];
|
|
@@ -19048,17 +19847,17 @@ class AssetView {
|
|
|
19048
19847
|
if (!this.prevData.length || !this.currentFolder)
|
|
19049
19848
|
return;
|
|
19050
19849
|
this.nextData.push(this.currentFolder);
|
|
19051
|
-
this._enterFolder(this.prevData.pop(), false);
|
|
19850
|
+
this._enterFolder(this.prevData.pop(), false, true);
|
|
19052
19851
|
}, { buttonClass: 'ghost', title: 'Go Back', tooltip: true, icon: 'ArrowLeft' });
|
|
19053
19852
|
panel.addButton(null, 'GoForwardButton', () => {
|
|
19054
19853
|
if (!this.nextData.length || !this.currentFolder)
|
|
19055
19854
|
return;
|
|
19056
|
-
this._enterFolder(this.nextData.pop());
|
|
19855
|
+
this._enterFolder(this.nextData.pop(), false, true);
|
|
19057
19856
|
}, { buttonClass: 'ghost', title: 'Go Forward', tooltip: true, icon: 'ArrowRight' });
|
|
19058
19857
|
panel.addButton(null, 'GoUpButton', () => {
|
|
19059
19858
|
const parentFolder = this.currentFolder?.parent;
|
|
19060
19859
|
if (parentFolder)
|
|
19061
|
-
this._enterFolder(parentFolder);
|
|
19860
|
+
this._enterFolder(parentFolder, false, true);
|
|
19062
19861
|
}, { buttonClass: 'ghost', title: 'Go Upper Folder', tooltip: true, icon: 'ArrowUp' });
|
|
19063
19862
|
panel.addButton(null, 'RefreshButton', () => {
|
|
19064
19863
|
this._refreshContent(undefined, undefined, true);
|
|
@@ -19098,7 +19897,7 @@ class AssetView {
|
|
|
19098
19897
|
this._updatePath();
|
|
19099
19898
|
}
|
|
19100
19899
|
else {
|
|
19101
|
-
this.
|
|
19900
|
+
this._requestEnterFolder(node.type === 'folder' ? node : node.parent);
|
|
19102
19901
|
this._previewAsset(node);
|
|
19103
19902
|
if (node.type !== 'folder') {
|
|
19104
19903
|
this.content.querySelectorAll('.lexassetitem').forEach((i) => i.classList.remove('selected'));
|
|
@@ -19470,12 +20269,41 @@ class AssetView {
|
|
|
19470
20269
|
this.toolsPanel.refresh();
|
|
19471
20270
|
this._refreshContent();
|
|
19472
20271
|
}
|
|
19473
|
-
|
|
20272
|
+
_requestEnterFolder(folderItem, storeCurrent = true) {
|
|
20273
|
+
if (!folderItem) {
|
|
20274
|
+
return;
|
|
20275
|
+
}
|
|
20276
|
+
const onBeforeEnterFolder = this._callbacks['beforeEnterFolder'];
|
|
20277
|
+
const onEnterFolder = this._callbacks['enterFolder'];
|
|
20278
|
+
const resolve = (...args) => {
|
|
20279
|
+
const child = this.currentData[0];
|
|
20280
|
+
const sameFolder = child?.parent?.metadata?.uid === folderItem.metadata?.uid;
|
|
20281
|
+
const mustRefresh = args[0] || !sameFolder;
|
|
20282
|
+
this._enterFolder(folderItem, storeCurrent, mustRefresh);
|
|
20283
|
+
const event = {
|
|
20284
|
+
type: 'enter-folder',
|
|
20285
|
+
to: folderItem,
|
|
20286
|
+
userInitiated: true
|
|
20287
|
+
};
|
|
20288
|
+
if (onEnterFolder)
|
|
20289
|
+
onEnterFolder(event, ...args);
|
|
20290
|
+
};
|
|
20291
|
+
if (onBeforeEnterFolder) {
|
|
20292
|
+
const event = {
|
|
20293
|
+
type: 'enter-folder',
|
|
20294
|
+
to: folderItem,
|
|
20295
|
+
userInitiated: true
|
|
20296
|
+
};
|
|
20297
|
+
onBeforeEnterFolder(event, resolve);
|
|
20298
|
+
}
|
|
20299
|
+
else {
|
|
20300
|
+
resolve();
|
|
20301
|
+
}
|
|
20302
|
+
}
|
|
20303
|
+
_enterFolder(folderItem, storeCurrent, mustRefresh) {
|
|
19474
20304
|
if (!folderItem) {
|
|
19475
20305
|
return;
|
|
19476
20306
|
}
|
|
19477
|
-
const child = this.currentData[0];
|
|
19478
|
-
const sameFolder = child?.parent?.metadata?.uid === folderItem.metadata?.uid;
|
|
19479
20307
|
if (storeCurrent) {
|
|
19480
20308
|
this.prevData.push(this.currentFolder ?? {
|
|
19481
20309
|
id: '/',
|
|
@@ -19484,17 +20312,6 @@ class AssetView {
|
|
|
19484
20312
|
metadata: {}
|
|
19485
20313
|
});
|
|
19486
20314
|
}
|
|
19487
|
-
let mustRefresh = !sameFolder;
|
|
19488
|
-
const onEnterFolder = this._callbacks['enterFolder'];
|
|
19489
|
-
if (onEnterFolder !== undefined) {
|
|
19490
|
-
const event = {
|
|
19491
|
-
type: 'enter_folder',
|
|
19492
|
-
to: folderItem,
|
|
19493
|
-
userInitiated: true
|
|
19494
|
-
};
|
|
19495
|
-
const r = await onEnterFolder(event);
|
|
19496
|
-
mustRefresh = mustRefresh || r;
|
|
19497
|
-
}
|
|
19498
20315
|
// Update this after the event since the user might have added or modified the data
|
|
19499
20316
|
this.currentFolder = folderItem;
|
|
19500
20317
|
this.currentData = this.currentFolder?.children ?? [];
|