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
package/build/lexgui.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
exports.LX = g$1.LX;
|
|
17
17
|
if (!exports.LX) {
|
|
18
18
|
exports.LX = {
|
|
19
|
-
version: '8.
|
|
19
|
+
version: '8.4.0',
|
|
20
20
|
ready: false,
|
|
21
21
|
extensions: [], // Store extensions used
|
|
22
22
|
extraCommandbarEntries: [], // User specific entries for command bar
|
|
@@ -436,6 +436,8 @@
|
|
|
436
436
|
ComponentType[ComponentType["LABEL"] = 39] = "LABEL";
|
|
437
437
|
ComponentType[ComponentType["BLANK"] = 40] = "BLANK";
|
|
438
438
|
ComponentType[ComponentType["RATE"] = 41] = "RATE";
|
|
439
|
+
ComponentType[ComponentType["EMPTY"] = 42] = "EMPTY";
|
|
440
|
+
ComponentType[ComponentType["DESCRIPTION"] = 43] = "DESCRIPTION";
|
|
439
441
|
})(exports.ComponentType || (exports.ComponentType = {}));
|
|
440
442
|
exports.LX.ComponentType = exports.ComponentType;
|
|
441
443
|
/**
|
|
@@ -737,6 +739,9 @@
|
|
|
737
739
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
738
740
|
wValue.style.width = `calc( 100% - ${realNameWidth})`;
|
|
739
741
|
};
|
|
742
|
+
this.onSetDisabled = (disabled) => {
|
|
743
|
+
wValue.disabled = disabled;
|
|
744
|
+
};
|
|
740
745
|
// In case of swap, set if a change has to be performed
|
|
741
746
|
this.setState = function (v, skipCallback) {
|
|
742
747
|
const swapInput = wValue.querySelector('input');
|
|
@@ -1474,6 +1479,12 @@
|
|
|
1474
1479
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
1475
1480
|
container.style.width = options.inputWidth ?? `calc( 100% - ${realNameWidth})`;
|
|
1476
1481
|
};
|
|
1482
|
+
this.onSetDisabled = (disabled) => {
|
|
1483
|
+
vecinput.disabled = disabled;
|
|
1484
|
+
const slider = this.root.querySelector('input[type="range"]');
|
|
1485
|
+
if (slider)
|
|
1486
|
+
slider.disabled = disabled;
|
|
1487
|
+
};
|
|
1477
1488
|
this.setLimits = (newMin, newMax, newStep) => { };
|
|
1478
1489
|
var container = document.createElement('div');
|
|
1479
1490
|
container.className = 'lexnumber';
|
|
@@ -1504,7 +1515,7 @@
|
|
|
1504
1515
|
if (!options.skipSlider && options.min !== undefined && options.max !== undefined) {
|
|
1505
1516
|
let sliderBox = exports.LX.makeContainer(['100%', 'auto'], 'z-1 input-box', '', box);
|
|
1506
1517
|
let slider = document.createElement('input');
|
|
1507
|
-
slider.className = 'lexinputslider';
|
|
1518
|
+
slider.className = 'lexinputslider disabled:pointer-events-none disabled:opacity-50';
|
|
1508
1519
|
slider.min = options.min;
|
|
1509
1520
|
slider.max = options.max;
|
|
1510
1521
|
slider.step = options.step ?? 1;
|
|
@@ -1641,6 +1652,11 @@
|
|
|
1641
1652
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
1642
1653
|
container.style.width = options.inputWidth ?? `calc( 100% - ${realNameWidth})`;
|
|
1643
1654
|
};
|
|
1655
|
+
this.onSetDisabled = (disabled) => {
|
|
1656
|
+
const input = this.root.querySelector('input');
|
|
1657
|
+
if (input)
|
|
1658
|
+
input.disabled = disabled;
|
|
1659
|
+
};
|
|
1644
1660
|
this.valid = (v, matchField) => {
|
|
1645
1661
|
v = v ?? this.value();
|
|
1646
1662
|
if (!options.pattern)
|
|
@@ -1800,6 +1816,9 @@
|
|
|
1800
1816
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
1801
1817
|
container.style.width = options.inputWidth ?? `calc( 100% - ${realNameWidth})`;
|
|
1802
1818
|
};
|
|
1819
|
+
this.onSetDisabled = (disabled) => {
|
|
1820
|
+
selectedOption?.setDisabled(disabled);
|
|
1821
|
+
};
|
|
1803
1822
|
let container = document.createElement('div');
|
|
1804
1823
|
container.className = 'lexselect';
|
|
1805
1824
|
this.root.appendChild(container);
|
|
@@ -2074,6 +2093,13 @@
|
|
|
2074
2093
|
this._trigger(new IEvent(name, values, event), callback);
|
|
2075
2094
|
}
|
|
2076
2095
|
};
|
|
2096
|
+
this.onSetDisabled = (disabled) => {
|
|
2097
|
+
if (this.root.dataset['opened'] == 'true' && disabled) {
|
|
2098
|
+
this.root.dataset['opened'] = false;
|
|
2099
|
+
this.root.querySelector('.lexarrayitems').toggleAttribute('hidden', true);
|
|
2100
|
+
}
|
|
2101
|
+
toggleButton.setDisabled(disabled);
|
|
2102
|
+
};
|
|
2077
2103
|
// Add open array button
|
|
2078
2104
|
let container = document.createElement('div');
|
|
2079
2105
|
container.className = 'lexarray shrink-1 grow-1 ml-4';
|
|
@@ -2165,9 +2191,10 @@
|
|
|
2165
2191
|
const container = exports.LX.makeContainer(['100%', 'auto'], 'lexcard max-w-sm flex flex-col gap-4 bg-card border-color rounded-xl py-6', '', this.root);
|
|
2166
2192
|
if (options.header) {
|
|
2167
2193
|
const hasAction = options.header.action !== undefined;
|
|
2194
|
+
const actionButtonOptions = options.header.action.options ?? {};
|
|
2168
2195
|
let header = exports.LX.makeContainer(['100%', 'auto'], `flex ${hasAction ? 'flex-row gap-4' : 'flex-col gap-1'} px-6`, '', container);
|
|
2169
2196
|
if (hasAction) {
|
|
2170
|
-
const actionBtn = new Button(null, options.header.action.name, options.header.action.callback, { buttonClass: 'secondary' });
|
|
2197
|
+
const actionBtn = new Button(null, options.header.action.name, options.header.action.callback, { buttonClass: 'secondary', ...actionButtonOptions });
|
|
2171
2198
|
header.appendChild(actionBtn.root);
|
|
2172
2199
|
const titleDescBox = exports.LX.makeContainer(['75%', 'auto'], `flex flex-col gap-1`, '');
|
|
2173
2200
|
header.prepend(titleDescBox);
|
|
@@ -2234,6 +2261,9 @@
|
|
|
2234
2261
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
2235
2262
|
container.style.width = options.inputWidth ?? `calc( 100% - ${realNameWidth})`;
|
|
2236
2263
|
};
|
|
2264
|
+
this.onSetDisabled = (disabled) => {
|
|
2265
|
+
checkbox.disabled = disabled;
|
|
2266
|
+
};
|
|
2237
2267
|
let container = document.createElement('div');
|
|
2238
2268
|
container.className = 'flex items-center gap-2 my-0 mx-auto [&_span]:truncate [&_span]:flex-auto-fill';
|
|
2239
2269
|
this.root.appendChild(container);
|
|
@@ -2862,7 +2892,12 @@
|
|
|
2862
2892
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
2863
2893
|
container.style.width = `calc( 100% - ${realNameWidth})`;
|
|
2864
2894
|
};
|
|
2865
|
-
|
|
2895
|
+
this.onSetDisabled = (disabled) => {
|
|
2896
|
+
textComponent.setDisabled(disabled);
|
|
2897
|
+
sampleContainer.classList.toggle('pointer-events-none', disabled);
|
|
2898
|
+
sampleContainer.classList.toggle('opacity-50', disabled);
|
|
2899
|
+
};
|
|
2900
|
+
let container = document.createElement('span');
|
|
2866
2901
|
container.className = 'lexcolor';
|
|
2867
2902
|
this.root.appendChild(container);
|
|
2868
2903
|
this.picker = new ColorPicker(value, {
|
|
@@ -3026,6 +3061,11 @@
|
|
|
3026
3061
|
this._trigger(new IEvent(name, newValue, event), callback);
|
|
3027
3062
|
}
|
|
3028
3063
|
};
|
|
3064
|
+
this.onSetDisabled = (disabled) => {
|
|
3065
|
+
substrButton.setDisabled(disabled);
|
|
3066
|
+
addButton.setDisabled(disabled);
|
|
3067
|
+
input.disabled = disabled;
|
|
3068
|
+
};
|
|
3029
3069
|
this.count = value;
|
|
3030
3070
|
const min = options.min ?? 0;
|
|
3031
3071
|
const max = options.max ?? 100;
|
|
@@ -3771,6 +3811,12 @@
|
|
|
3771
3811
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
3772
3812
|
container.style.width = `calc( 100% - ${realNameWidth})`;
|
|
3773
3813
|
};
|
|
3814
|
+
this.onSetDisabled = (disabled) => {
|
|
3815
|
+
const buttons = this.root.querySelectorAll('button');
|
|
3816
|
+
buttons.forEach((b) => {
|
|
3817
|
+
b.disabled = disabled;
|
|
3818
|
+
});
|
|
3819
|
+
};
|
|
3774
3820
|
const container = exports.LX.makeContainer(['auto', 'auto'], 'lexdate flex flex-row');
|
|
3775
3821
|
this.root.appendChild(container);
|
|
3776
3822
|
if (!dateAsRange) {
|
|
@@ -4163,6 +4209,52 @@
|
|
|
4163
4209
|
exports.LX.CanvasDial = CanvasDial;
|
|
4164
4210
|
exports.LX.Dial = Dial;
|
|
4165
4211
|
|
|
4212
|
+
// Empty.ts @jxarco
|
|
4213
|
+
/**
|
|
4214
|
+
* @class Empty
|
|
4215
|
+
* @description Empty Component
|
|
4216
|
+
*/
|
|
4217
|
+
class Empty extends BaseComponent {
|
|
4218
|
+
constructor(name, options = {}) {
|
|
4219
|
+
options.hideName = true;
|
|
4220
|
+
super(exports.ComponentType.EMPTY, name, null, options);
|
|
4221
|
+
this.root.classList.add('place-content-center');
|
|
4222
|
+
const container = exports.LX.makeContainer(['100%', 'auto'], 'lexcard max-w-sm flex flex-col gap-4 bg-card border-color rounded-xl py-6', '', this.root);
|
|
4223
|
+
if (options.header) {
|
|
4224
|
+
let header = exports.LX.makeContainer(['100%', 'auto'], `flex flex-col gap-4 px-6 items-center`, '', container);
|
|
4225
|
+
if (options.header.icon) {
|
|
4226
|
+
const icon = exports.LX.makeIcon(options.header.icon, { iconClass: 'bg-secondary p-2 rounded-lg!', svgClass: 'lg' });
|
|
4227
|
+
header.appendChild(icon);
|
|
4228
|
+
}
|
|
4229
|
+
else if (options.header.avatar) {
|
|
4230
|
+
const avatar = new exports.LX.Avatar(options.header.avatar);
|
|
4231
|
+
header.appendChild(avatar.root);
|
|
4232
|
+
}
|
|
4233
|
+
if (options.header.title) {
|
|
4234
|
+
exports.LX.makeElement('div', 'text-center text-foreground leading-none font-medium', options.header.title, header);
|
|
4235
|
+
}
|
|
4236
|
+
if (options.header.description) {
|
|
4237
|
+
exports.LX.makeElement('div', 'text-sm text-center text-balance text-muted-foreground', options.header.description, header);
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
if (options.actions) {
|
|
4241
|
+
const content = exports.LX.makeContainer(['100%', 'auto'], 'flex flex-row gap-1 px-6 justify-center', '', container);
|
|
4242
|
+
for (let a of options.actions) {
|
|
4243
|
+
const action = new exports.LX.Button(null, a.name, a.callback, { buttonClass: "sm outline", ...a.options });
|
|
4244
|
+
content.appendChild(action.root);
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
if (options.footer) {
|
|
4248
|
+
const footer = exports.LX.makeContainer(['100%', 'auto'], 'flex flex-col gap-1 px-6', '', container);
|
|
4249
|
+
const elements = [].concat(options.footer);
|
|
4250
|
+
for (let e of elements) {
|
|
4251
|
+
footer.appendChild(e.root ? e.root : e);
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
exports.LX.Empty = Empty;
|
|
4257
|
+
|
|
4166
4258
|
// FileInput.ts @jxarco
|
|
4167
4259
|
/**
|
|
4168
4260
|
* @class FileInput
|
|
@@ -4178,6 +4270,9 @@
|
|
|
4178
4270
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
4179
4271
|
input.style.width = `calc( 100% - ${realNameWidth})`;
|
|
4180
4272
|
};
|
|
4273
|
+
this.onSetDisabled = (disabled) => {
|
|
4274
|
+
input.disabled = disabled;
|
|
4275
|
+
};
|
|
4181
4276
|
// Create hidden input
|
|
4182
4277
|
let input = document.createElement('input');
|
|
4183
4278
|
input.className = 'lexfileinput';
|
|
@@ -4385,6 +4480,9 @@
|
|
|
4385
4480
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
4386
4481
|
container.style.width = `calc( 100% - ${realNameWidth})`;
|
|
4387
4482
|
};
|
|
4483
|
+
this.onSetDisabled = (disabled) => {
|
|
4484
|
+
this.setLayers(value);
|
|
4485
|
+
};
|
|
4388
4486
|
const container = exports.LX.makeElement('div', 'lexlayers grid', '', this.root);
|
|
4389
4487
|
const maxBits = options.maxBits ?? 16;
|
|
4390
4488
|
this.setLayers = (val) => {
|
|
@@ -4461,6 +4559,9 @@
|
|
|
4461
4559
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
4462
4560
|
listContainer.style.width = `calc( 100% - ${realNameWidth})`;
|
|
4463
4561
|
};
|
|
4562
|
+
this.onSetDisabled = (disabled) => {
|
|
4563
|
+
this._updateValues(values);
|
|
4564
|
+
};
|
|
4464
4565
|
this._updateValues = (newValues) => {
|
|
4465
4566
|
values = newValues;
|
|
4466
4567
|
listContainer.innerHTML = '';
|
|
@@ -4928,16 +5029,19 @@
|
|
|
4928
5029
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
4929
5030
|
container.style.width = `calc( 100% - ${realNameWidth})`;
|
|
4930
5031
|
};
|
|
5032
|
+
this.onSetDisabled = (disabled) => {
|
|
5033
|
+
openerButton.setDisabled(disabled);
|
|
5034
|
+
};
|
|
4931
5035
|
var container = document.createElement('div');
|
|
4932
5036
|
container.className = 'lexmap2d';
|
|
4933
5037
|
this.root.appendChild(container);
|
|
4934
5038
|
this.map2d = new CanvasMap2D(points, callback, options);
|
|
4935
|
-
const
|
|
4936
|
-
const
|
|
4937
|
-
this._popover = new Popover(
|
|
5039
|
+
const icon = exports.LX.makeIcon(options.mapIcon ?? 'SquareMousePointer');
|
|
5040
|
+
const openerButton = new Button(null, 'Open Map', () => {
|
|
5041
|
+
this._popover = new Popover(openerButton.root, [this.map2d]);
|
|
4938
5042
|
}, { buttonClass: `outline justify-between`, disabled: this.disabled });
|
|
4939
|
-
|
|
4940
|
-
container.appendChild(
|
|
5043
|
+
openerButton.root.querySelector('button').appendChild(icon);
|
|
5044
|
+
container.appendChild(openerButton.root);
|
|
4941
5045
|
exports.LX.doAsync(this.onResize.bind(this));
|
|
4942
5046
|
}
|
|
4943
5047
|
}
|
|
@@ -5644,6 +5748,9 @@
|
|
|
5644
5748
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
5645
5749
|
container.style.width = `calc( 100% - ${realNameWidth})`;
|
|
5646
5750
|
};
|
|
5751
|
+
this.onSetDisabled = (disabled) => {
|
|
5752
|
+
_refreshInput(value);
|
|
5753
|
+
};
|
|
5647
5754
|
const container = document.createElement('div');
|
|
5648
5755
|
container.className = 'lexotp flex flex-row items-center';
|
|
5649
5756
|
this.root.appendChild(container);
|
|
@@ -5657,7 +5764,7 @@
|
|
|
5657
5764
|
for (let j = 0; j < g.length; ++j) {
|
|
5658
5765
|
let number = valueString[itemsCount++];
|
|
5659
5766
|
number = number == 'x' ? '' : number;
|
|
5660
|
-
const slotDom = exports.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);
|
|
5767
|
+
const slotDom = exports.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);
|
|
5661
5768
|
slotDom.tabIndex = '1';
|
|
5662
5769
|
if (this.disabled) {
|
|
5663
5770
|
slotDom.classList.add('disabled');
|
|
@@ -6061,6 +6168,9 @@
|
|
|
6061
6168
|
slider.style.setProperty('--range-fix-max-offset', `${diffMaxOffset}rem`);
|
|
6062
6169
|
}
|
|
6063
6170
|
};
|
|
6171
|
+
this.onSetDisabled = (disabled) => {
|
|
6172
|
+
slider.disabled = disabled;
|
|
6173
|
+
};
|
|
6064
6174
|
const container = document.createElement('div');
|
|
6065
6175
|
container.className = 'lexrange relative py-3';
|
|
6066
6176
|
this.root.appendChild(container);
|
|
@@ -6169,6 +6279,9 @@
|
|
|
6169
6279
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
6170
6280
|
container.style.width = `calc( 100% - ${realNameWidth})`;
|
|
6171
6281
|
};
|
|
6282
|
+
this.onSetDisabled = (disabled) => {
|
|
6283
|
+
container.dataset['disabled'] = disabled.toString();
|
|
6284
|
+
};
|
|
6172
6285
|
const container = document.createElement('div');
|
|
6173
6286
|
container.className = 'lexrate relative data-[disabled=true]:pointer-events-none';
|
|
6174
6287
|
container.dataset['disabled'] = this.disabled.toString();
|
|
@@ -7312,6 +7425,9 @@
|
|
|
7312
7425
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
7313
7426
|
tagsContainer.style.width = `calc( 100% - ${realNameWidth})`;
|
|
7314
7427
|
};
|
|
7428
|
+
this.onSetDisabled = (disabled) => {
|
|
7429
|
+
this.generateTags(arrayValue);
|
|
7430
|
+
};
|
|
7315
7431
|
// Show tags
|
|
7316
7432
|
const tagsContainer = document.createElement('div');
|
|
7317
7433
|
tagsContainer.className = 'inline-flex flex-wrap gap-1 bg-card/50 rounded-lg pad-xs [&_input]:w-2/3';
|
|
@@ -7380,6 +7496,11 @@
|
|
|
7380
7496
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
7381
7497
|
container.style.width = options.inputWidth ?? `calc( 100% - ${realNameWidth})`;
|
|
7382
7498
|
};
|
|
7499
|
+
this.onSetDisabled = (disabled) => {
|
|
7500
|
+
const textarea = this.root.querySelector('textarea');
|
|
7501
|
+
if (textarea)
|
|
7502
|
+
textarea.disabled = disabled;
|
|
7503
|
+
};
|
|
7383
7504
|
let container = document.createElement('div');
|
|
7384
7505
|
container.className = 'lextextarea';
|
|
7385
7506
|
container.style.display = 'flex';
|
|
@@ -7497,6 +7618,9 @@
|
|
|
7497
7618
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
7498
7619
|
container.style.width = options.inputWidth ?? `calc( 100% - ${realNameWidth})`;
|
|
7499
7620
|
};
|
|
7621
|
+
this.onSetDisabled = (disabled) => {
|
|
7622
|
+
toggle.disabled = disabled;
|
|
7623
|
+
};
|
|
7500
7624
|
var container = document.createElement('div');
|
|
7501
7625
|
container.className = 'flex flex-row gap-2 items-center';
|
|
7502
7626
|
this.root.appendChild(container);
|
|
@@ -7566,6 +7690,12 @@
|
|
|
7566
7690
|
const realNameWidth = this.root.domName?.style.width ?? '0px';
|
|
7567
7691
|
container.style.width = `calc( 100% - ${realNameWidth})`;
|
|
7568
7692
|
};
|
|
7693
|
+
this.onSetDisabled = (disabled) => {
|
|
7694
|
+
const inputs = this.root.querySelectorAll('input');
|
|
7695
|
+
inputs.forEach((i) => {
|
|
7696
|
+
i.disabled = disabled;
|
|
7697
|
+
});
|
|
7698
|
+
};
|
|
7569
7699
|
this.setLimits = (newMin, newMax, newStep) => { };
|
|
7570
7700
|
const vectorInputs = [];
|
|
7571
7701
|
var container = document.createElement('div');
|
|
@@ -8310,6 +8440,19 @@
|
|
|
8310
8440
|
component.type = exports.ComponentType.LABEL;
|
|
8311
8441
|
return component;
|
|
8312
8442
|
}
|
|
8443
|
+
/**
|
|
8444
|
+
* @method addDescription
|
|
8445
|
+
* @param {String} value Information string
|
|
8446
|
+
* @param {Object} options Text options
|
|
8447
|
+
*/
|
|
8448
|
+
addDescription(value, options = {}) {
|
|
8449
|
+
options.disabled = true;
|
|
8450
|
+
options.fitHeight = true;
|
|
8451
|
+
options.inputClass = exports.LX.mergeClass('bg-none', options.inputClass);
|
|
8452
|
+
const component = this.addTextArea(null, value, null, options);
|
|
8453
|
+
component.type = exports.ComponentType.DESCRIPTION;
|
|
8454
|
+
return component;
|
|
8455
|
+
}
|
|
8313
8456
|
/**
|
|
8314
8457
|
* @method addButton
|
|
8315
8458
|
* @param {String} name Component name
|
|
@@ -8348,18 +8491,22 @@
|
|
|
8348
8491
|
}
|
|
8349
8492
|
/**
|
|
8350
8493
|
* @method addCard
|
|
8351
|
-
* @param {String} name
|
|
8352
|
-
* @param {Object} options
|
|
8353
|
-
* text: Card text
|
|
8354
|
-
* link: Card link
|
|
8355
|
-
* title: Card dom title
|
|
8356
|
-
* src: url of the image
|
|
8357
|
-
* callback (Function): function to call on click
|
|
8494
|
+
* @param {String} name
|
|
8495
|
+
* @param {Object} options
|
|
8358
8496
|
*/
|
|
8359
8497
|
addCard(name, options = {}) {
|
|
8360
8498
|
const component = new Card(name, options);
|
|
8361
8499
|
return this._attachComponent(component);
|
|
8362
8500
|
}
|
|
8501
|
+
/**
|
|
8502
|
+
* @method addEmpty
|
|
8503
|
+
* @param {String} name
|
|
8504
|
+
* @param {Object} options
|
|
8505
|
+
*/
|
|
8506
|
+
addEmpty(name, options = {}) {
|
|
8507
|
+
const component = new Empty(name, options);
|
|
8508
|
+
return this._attachComponent(component);
|
|
8509
|
+
}
|
|
8363
8510
|
/**
|
|
8364
8511
|
* @method addForm
|
|
8365
8512
|
* @param {String} name Component name
|
|
@@ -11133,47 +11280,67 @@
|
|
|
11133
11280
|
* offsetY: Tooltip margin vertical offset
|
|
11134
11281
|
* active: Tooltip active by default [true]
|
|
11135
11282
|
* callback: Callback function to execute when the tooltip is shown
|
|
11283
|
+
* delay: Interest delay in ms until showing the tooltip [100]
|
|
11136
11284
|
*/
|
|
11137
11285
|
function asTooltip(trigger, content, options = {}) {
|
|
11138
11286
|
console.assert(trigger, 'You need a trigger to generate a tooltip!');
|
|
11139
11287
|
trigger.dataset['disableTooltip'] = !(options.active ?? true);
|
|
11140
11288
|
let tooltipDom = null;
|
|
11289
|
+
let delayTimer = null;
|
|
11290
|
+
let rafId = null;
|
|
11141
11291
|
const _offset = options.offset;
|
|
11142
11292
|
const _offsetX = options.offsetX ?? (_offset ?? 0);
|
|
11143
11293
|
const _offsetY = options.offsetY ?? (_offset ?? 6);
|
|
11144
|
-
|
|
11145
|
-
|
|
11294
|
+
const _delay = options.delay ?? 100;
|
|
11295
|
+
const _cleanup = () => {
|
|
11296
|
+
clearTimeout(delayTimer);
|
|
11297
|
+
if (rafId !== null) {
|
|
11298
|
+
cancelAnimationFrame(rafId);
|
|
11299
|
+
rafId = null;
|
|
11300
|
+
}
|
|
11301
|
+
if (tooltipDom) {
|
|
11302
|
+
tooltipDom.remove();
|
|
11303
|
+
tooltipDom = null;
|
|
11304
|
+
}
|
|
11305
|
+
};
|
|
11306
|
+
const _watchConnection = () => {
|
|
11307
|
+
if (!trigger.isConnected) {
|
|
11308
|
+
_cleanup();
|
|
11146
11309
|
return;
|
|
11147
11310
|
}
|
|
11311
|
+
if (tooltipDom)
|
|
11312
|
+
rafId = requestAnimationFrame(_watchConnection);
|
|
11313
|
+
};
|
|
11314
|
+
const _showTooltip = () => {
|
|
11148
11315
|
tooltipDom = exports.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);
|
|
11149
11316
|
const nestedDialog = trigger.closest('dialog');
|
|
11150
11317
|
const tooltipParent = nestedDialog ?? exports.LX.root;
|
|
11151
|
-
// Remove
|
|
11318
|
+
// Remove others first
|
|
11152
11319
|
exports.LX.root.querySelectorAll('.lextooltip').forEach((e) => e.remove());
|
|
11153
|
-
// Append new tooltip
|
|
11154
11320
|
tooltipParent.appendChild(tooltipDom);
|
|
11321
|
+
// Watch for trigger being removed from the DOM before mouseleave fires
|
|
11322
|
+
rafId = requestAnimationFrame(_watchConnection);
|
|
11155
11323
|
exports.LX.doAsync(() => {
|
|
11324
|
+
if (!tooltipDom)
|
|
11325
|
+
return;
|
|
11156
11326
|
const position = [0, 0];
|
|
11157
11327
|
const offsetX = parseFloat(trigger.dataset['tooltipOffsetX'] ?? _offsetX);
|
|
11158
11328
|
const offsetY = parseFloat(trigger.dataset['tooltipOffsetY'] ?? _offsetY);
|
|
11159
11329
|
const rect = trigger.getBoundingClientRect();
|
|
11160
|
-
|
|
11161
|
-
|
|
11330
|
+
const side = options.side ?? 'top';
|
|
11331
|
+
const alignWidth = side === 'top' || side === 'bottom';
|
|
11332
|
+
switch (side) {
|
|
11162
11333
|
case 'left':
|
|
11163
11334
|
position[0] += rect.x - tooltipDom.offsetWidth - offsetX;
|
|
11164
|
-
alignWidth = false;
|
|
11165
11335
|
break;
|
|
11166
11336
|
case 'right':
|
|
11167
11337
|
position[0] += rect.x + rect.width + offsetX;
|
|
11168
|
-
alignWidth = false;
|
|
11169
11338
|
break;
|
|
11170
11339
|
case 'top':
|
|
11171
11340
|
position[1] += rect.y - tooltipDom.offsetHeight - offsetY;
|
|
11172
|
-
alignWidth = true;
|
|
11173
11341
|
break;
|
|
11174
11342
|
case 'bottom':
|
|
11175
11343
|
position[1] += rect.y + rect.height + offsetY;
|
|
11176
|
-
alignWidth = true;
|
|
11177
11344
|
break;
|
|
11178
11345
|
}
|
|
11179
11346
|
if (alignWidth)
|
|
@@ -11184,22 +11351,22 @@
|
|
|
11184
11351
|
position[0] = exports.LX.clamp(position[0], 0, window.innerWidth - tooltipDom.offsetWidth - 4);
|
|
11185
11352
|
position[1] = exports.LX.clamp(position[1], 0, window.innerHeight - tooltipDom.offsetHeight - 4);
|
|
11186
11353
|
if (nestedDialog) {
|
|
11187
|
-
|
|
11354
|
+
const parentRect = tooltipParent.getBoundingClientRect();
|
|
11188
11355
|
position[0] -= parentRect.x;
|
|
11189
11356
|
position[1] -= parentRect.y;
|
|
11190
11357
|
}
|
|
11191
11358
|
tooltipDom.style.left = `${position[0]}px`;
|
|
11192
11359
|
tooltipDom.style.top = `${position[1]}px`;
|
|
11193
|
-
|
|
11194
|
-
options.callback(tooltipDom, trigger);
|
|
11195
|
-
}
|
|
11360
|
+
options.callback?.(tooltipDom, trigger);
|
|
11196
11361
|
});
|
|
11197
|
-
}
|
|
11198
|
-
trigger.addEventListener('
|
|
11199
|
-
if (
|
|
11200
|
-
|
|
11362
|
+
};
|
|
11363
|
+
trigger.addEventListener('mouseenter', function () {
|
|
11364
|
+
if (trigger.dataset['disableTooltip'] == 'true') {
|
|
11365
|
+
return;
|
|
11201
11366
|
}
|
|
11367
|
+
delayTimer = setTimeout(_showTooltip, _delay);
|
|
11202
11368
|
});
|
|
11369
|
+
trigger.addEventListener('mouseleave', _cleanup);
|
|
11203
11370
|
}
|
|
11204
11371
|
exports.LX.asTooltip = asTooltip;
|
|
11205
11372
|
function insertChildAtIndex(parent, child, index = Infinity) {
|
|
@@ -13321,7 +13488,9 @@
|
|
|
13321
13488
|
// using a fullscreen SVG with "rect" elements
|
|
13322
13489
|
_generateMask(reference) {
|
|
13323
13490
|
this.tourContainer.innerHTML = ''; // Clear previous content
|
|
13491
|
+
const scrollTop = document.scrollingElement?.scrollTop ?? 0;
|
|
13324
13492
|
this.tourMask = exports.LX.makeContainer(['100%', '100%'], 'tour-mask absolute inset-0');
|
|
13493
|
+
this.tourMask.style.top = `${scrollTop}px`;
|
|
13325
13494
|
this.tourContainer.appendChild(this.tourMask);
|
|
13326
13495
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
13327
13496
|
svg.style.width = '100%';
|
|
@@ -13384,7 +13553,7 @@
|
|
|
13384
13553
|
// Reference Highlight
|
|
13385
13554
|
const refContainer = exports.LX.makeContainer(['0', '0'], 'tour-ref-mask absolute');
|
|
13386
13555
|
refContainer.style.left = `${boundingX - hOffset - 1}px`;
|
|
13387
|
-
refContainer.style.top = `${boundingY - vOffset - 1}px`;
|
|
13556
|
+
refContainer.style.top = `${boundingY - vOffset - 1 + scrollTop}px`;
|
|
13388
13557
|
refContainer.style.width = `${boundingWidth + hOffset * 2 + 2}px`;
|
|
13389
13558
|
refContainer.style.height = `${boundingHeight + vOffset * 2 + 2}px`;
|
|
13390
13559
|
this.tourContainer.appendChild(refContainer);
|
|
@@ -14429,12 +14598,12 @@
|
|
|
14429
14598
|
getText(separator = '\n') {
|
|
14430
14599
|
return this._lines.join(separator);
|
|
14431
14600
|
}
|
|
14432
|
-
setText(text) {
|
|
14601
|
+
setText(text, silent = false) {
|
|
14433
14602
|
this._lines = text.split(/\r?\n/);
|
|
14434
14603
|
if (this._lines.length === 0) {
|
|
14435
14604
|
this._lines = [''];
|
|
14436
14605
|
}
|
|
14437
|
-
if (this.onChange)
|
|
14606
|
+
if (!silent && this.onChange)
|
|
14438
14607
|
this.onChange(this);
|
|
14439
14608
|
}
|
|
14440
14609
|
getCharAt(line, col) {
|
|
@@ -14598,6 +14767,7 @@
|
|
|
14598
14767
|
_lastPushTime = 0;
|
|
14599
14768
|
_groupThresholdMs;
|
|
14600
14769
|
_maxSteps;
|
|
14770
|
+
_savedDepth = 0;
|
|
14601
14771
|
constructor(groupThresholdMs = 2000, maxSteps = 200) {
|
|
14602
14772
|
this._groupThresholdMs = groupThresholdMs;
|
|
14603
14773
|
this._maxSteps = maxSteps;
|
|
@@ -14669,11 +14839,19 @@
|
|
|
14669
14839
|
canRedo() {
|
|
14670
14840
|
return this._redoStack.length > 0;
|
|
14671
14841
|
}
|
|
14842
|
+
markSaved() {
|
|
14843
|
+
this._flush();
|
|
14844
|
+
this._savedDepth = this._undoStack.length;
|
|
14845
|
+
}
|
|
14846
|
+
isModified() {
|
|
14847
|
+
return this._undoStack.length !== this._savedDepth || this._pendingOps.length > 0;
|
|
14848
|
+
}
|
|
14672
14849
|
clear() {
|
|
14673
14850
|
this._undoStack.length = 0;
|
|
14674
14851
|
this._redoStack.length = 0;
|
|
14675
14852
|
this._pendingOps.length = 0;
|
|
14676
14853
|
this._lastPushTime = 0;
|
|
14854
|
+
this._savedDepth = 0;
|
|
14677
14855
|
}
|
|
14678
14856
|
_flush() {
|
|
14679
14857
|
if (this._pendingOps.length === 0)
|
|
@@ -14969,6 +15147,49 @@
|
|
|
14969
15147
|
this.cursors = merged;
|
|
14970
15148
|
}
|
|
14971
15149
|
}
|
|
15150
|
+
/**
|
|
15151
|
+
* Strips string literals and single-line comments from a line of code,
|
|
15152
|
+
* leaving only the structural characters (braces, operators, keywords etc).
|
|
15153
|
+
*/
|
|
15154
|
+
function stripLiteralsAndComments(line) {
|
|
15155
|
+
let result = '';
|
|
15156
|
+
let i = 0;
|
|
15157
|
+
while (i < line.length) {
|
|
15158
|
+
const ch = line[i];
|
|
15159
|
+
// Remove single-line comment
|
|
15160
|
+
if (ch === '/' && line[i + 1] === '/') {
|
|
15161
|
+
break;
|
|
15162
|
+
}
|
|
15163
|
+
// Block comment (same line): skip to closing */
|
|
15164
|
+
if (ch === '/' && line[i + 1] === '*') {
|
|
15165
|
+
i += 2;
|
|
15166
|
+
while (i < line.length && !(line[i] === '*' && line[i + 1] === '/'))
|
|
15167
|
+
i++;
|
|
15168
|
+
i += 2;
|
|
15169
|
+
continue;
|
|
15170
|
+
}
|
|
15171
|
+
// Remove strings (single, double, template)
|
|
15172
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
15173
|
+
const quote = ch;
|
|
15174
|
+
i++;
|
|
15175
|
+
while (i < line.length) {
|
|
15176
|
+
if (line[i] === '\\') {
|
|
15177
|
+
i += 2;
|
|
15178
|
+
continue;
|
|
15179
|
+
} // escaped char
|
|
15180
|
+
if (line[i] === quote) {
|
|
15181
|
+
i++;
|
|
15182
|
+
break;
|
|
15183
|
+
} // closing quote
|
|
15184
|
+
i++;
|
|
15185
|
+
}
|
|
15186
|
+
continue;
|
|
15187
|
+
}
|
|
15188
|
+
result += ch;
|
|
15189
|
+
i++;
|
|
15190
|
+
}
|
|
15191
|
+
return result;
|
|
15192
|
+
}
|
|
14972
15193
|
/**
|
|
14973
15194
|
* Manages code symbols for autocomplete, navigation, and outlining.
|
|
14974
15195
|
* Incrementally updates as lines change.
|
|
@@ -14977,7 +15198,8 @@
|
|
|
14977
15198
|
_symbols = new Map(); // name -> symbols[]
|
|
14978
15199
|
_lineSymbols = []; // [lineNum] -> symbols declared on that line
|
|
14979
15200
|
_scopeStack = [{ name: 'global', type: 'global', line: 0 }];
|
|
14980
|
-
_lineScopes = []; // [lineNum] -> scope stack at that line
|
|
15201
|
+
_lineScopes = []; // [lineNum] -> scope stack at start of that line
|
|
15202
|
+
_lineScopesEnd = []; // [lineNum] -> scope stack at end of that line
|
|
14981
15203
|
get currentScope() {
|
|
14982
15204
|
return this._scopeStack[this._scopeStack.length - 1]?.name ?? 'global';
|
|
14983
15205
|
}
|
|
@@ -14987,6 +15209,9 @@
|
|
|
14987
15209
|
getScopeAtLine(line) {
|
|
14988
15210
|
return this._lineScopes[line] ?? [{ name: 'global', type: 'global', line: 0 }];
|
|
14989
15211
|
}
|
|
15212
|
+
getLineScopeEnd(line) {
|
|
15213
|
+
return this._lineScopesEnd[line] ?? [{ name: 'global', type: 'global', line: 0 }];
|
|
15214
|
+
}
|
|
14990
15215
|
getSymbols(name) {
|
|
14991
15216
|
return this._symbols.get(name) ?? [];
|
|
14992
15217
|
}
|
|
@@ -15005,20 +15230,26 @@
|
|
|
15005
15230
|
}
|
|
15006
15231
|
/** Update scope stack for a line (call before parsing symbols) */
|
|
15007
15232
|
updateScopeForLine(line, lineText) {
|
|
15008
|
-
|
|
15009
|
-
|
|
15010
|
-
|
|
15011
|
-
|
|
15233
|
+
if (line === 0) {
|
|
15234
|
+
this._scopeStack = [{ name: 'global', type: 'global', line: 0 }];
|
|
15235
|
+
}
|
|
15236
|
+
else if (this._lineScopesEnd[line - 1]) {
|
|
15237
|
+
this._scopeStack = [...this._lineScopesEnd[line - 1]];
|
|
15238
|
+
}
|
|
15239
|
+
const stripped = stripLiteralsAndComments(lineText);
|
|
15240
|
+
const openBraces = (stripped.match(/\{/g) || []).length;
|
|
15241
|
+
const closeBraces = (stripped.match(/\}/g) || []).length;
|
|
15012
15242
|
this._lineScopes[line] = [...this._scopeStack];
|
|
15013
15243
|
// Pop scopes for closing braces
|
|
15014
15244
|
for (let i = 0; i < closeBraces; i++) {
|
|
15015
15245
|
if (this._scopeStack.length > 1)
|
|
15016
15246
|
this._scopeStack.pop();
|
|
15017
15247
|
}
|
|
15018
|
-
// Push scopes for opening braces (
|
|
15248
|
+
// Push scopes for opening braces (symbol detection will name them later)
|
|
15019
15249
|
for (let i = 0; i < openBraces; i++) {
|
|
15020
15250
|
this._scopeStack.push({ name: 'anonymous', type: 'anonymous', line });
|
|
15021
15251
|
}
|
|
15252
|
+
this._lineScopesEnd[line] = [...this._scopeStack];
|
|
15022
15253
|
}
|
|
15023
15254
|
/** Name the most recent anonymous scope (called when detecting class/function) */
|
|
15024
15255
|
nameCurrentScope(name, type) {
|
|
@@ -15062,6 +15293,7 @@
|
|
|
15062
15293
|
resetScopes() {
|
|
15063
15294
|
this._scopeStack = [{ name: 'global', type: 'global', line: 0 }];
|
|
15064
15295
|
this._lineScopes = [];
|
|
15296
|
+
this._lineScopesEnd = [];
|
|
15065
15297
|
}
|
|
15066
15298
|
clear() {
|
|
15067
15299
|
this._symbols.clear();
|
|
@@ -15074,23 +15306,25 @@
|
|
|
15074
15306
|
*/
|
|
15075
15307
|
function parseSymbolsFromLine(lineText, tokens, line, symbolTable) {
|
|
15076
15308
|
const symbols = [];
|
|
15077
|
-
|
|
15078
|
-
|
|
15079
|
-
|
|
15309
|
+
// Use the scope snapshot from the START of this line, not currentScope/currentScopeType
|
|
15310
|
+
// which will reflect state AFTER updateScopeForLine already pushed/popped braces on this line...
|
|
15311
|
+
const lineScopes = symbolTable.getScopeAtLine(line);
|
|
15312
|
+
const lineScope = lineScopes[lineScopes.length - 1];
|
|
15313
|
+
const scope = lineScope?.name ?? 'global';
|
|
15314
|
+
const scopeType = lineScope?.type ?? 'global';
|
|
15080
15315
|
const reservedWords = new Set();
|
|
15081
15316
|
for (const token of tokens) {
|
|
15082
15317
|
if (['keyword', 'statement', 'builtin', 'preprocessor'].includes(token.type)) {
|
|
15083
15318
|
reservedWords.add(token.value);
|
|
15084
15319
|
}
|
|
15085
15320
|
}
|
|
15086
|
-
// Track added symbols by name and approximate position to avoid duplicates
|
|
15321
|
+
// Track added symbols by name and approximate position using 5 chars tolerance to avoid duplicates
|
|
15087
15322
|
const addedSymbols = new Set();
|
|
15088
15323
|
const addSymbol = (name, kind, col = 0) => {
|
|
15089
15324
|
if (!name || !name.match(/^[a-zA-Z_$][\w$]*$/))
|
|
15090
15325
|
return; // Valid identifier check
|
|
15091
15326
|
if (reservedWords.has(name))
|
|
15092
15327
|
return;
|
|
15093
|
-
// Unique key using 5 chars tolerance
|
|
15094
15328
|
const posKey = `${name}@${Math.floor(col / 5) * 5}`;
|
|
15095
15329
|
if (addedSymbols.has(posKey))
|
|
15096
15330
|
return; // Already added
|
|
@@ -15106,13 +15340,14 @@
|
|
|
15106
15340
|
{ regex: /^\s*type\s+([A-Z_]\w*)\s*=/i, kind: 'type' },
|
|
15107
15341
|
{ regex: /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)/i, kind: 'function' },
|
|
15108
15342
|
{ regex: /^\s*(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>/i, kind: 'function' },
|
|
15109
|
-
{ 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' },
|
|
15110
|
-
{ regex: /^\s*(?:public|private|protected|static|readonly)
|
|
15343
|
+
{ 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' },
|
|
15344
|
+
{ regex: /^\s*(?:(?:public|private|protected|static|readonly|async|override)\s+)*(\w+)\s*\([^)]*\)\s*[:{]/i, kind: scopeType === 'class' ? 'method' : 'function' }
|
|
15111
15345
|
];
|
|
15112
15346
|
const multiPatterns = [
|
|
15113
15347
|
{ regex: /(?:const|let|var)\s+(\w+)/gi, kind: 'variable' },
|
|
15114
15348
|
{ regex: /(\w+)\s*:\s*(?:function|[A-Z]\w*)/gi, kind: 'variable' },
|
|
15115
15349
|
{ regex: /this\.(\w+)\s*=/gi, kind: 'property' },
|
|
15350
|
+
{ regex: /\b(?:private|protected|public)\s+(?:(?:static|readonly)\s+)*(\w+)\s*[=:;]/gi, kind: 'property' },
|
|
15116
15351
|
{ regex: /new\s+([A-Z]\w+)/gi, kind: 'constructor-call' },
|
|
15117
15352
|
{ regex: /(\w+)\s*\(/gi, kind: 'method-call' }
|
|
15118
15353
|
];
|
|
@@ -15185,6 +15420,56 @@
|
|
|
15185
15420
|
exports.LX.Panel;
|
|
15186
15421
|
exports.LX.Tabs;
|
|
15187
15422
|
exports.LX.NodeTree;
|
|
15423
|
+
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;
|
|
15424
|
+
const URL_REGEX = /(https?:\/\/[^\s"'<>)\]]+)/g;
|
|
15425
|
+
/**
|
|
15426
|
+
* Returns true if the string token at `idx` in the token list is a module import path.
|
|
15427
|
+
*/
|
|
15428
|
+
function isImportPath(tokens, idx) {
|
|
15429
|
+
const isWs = (t) => /^\s+$/.test(t.value);
|
|
15430
|
+
const isImportWord = (t) => t.value === 'require' || t.value === 'import';
|
|
15431
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
15432
|
+
const t = tokens[i];
|
|
15433
|
+
if (isWs(t))
|
|
15434
|
+
continue;
|
|
15435
|
+
if (t.type === 'keyword' && t.value === 'from')
|
|
15436
|
+
return true;
|
|
15437
|
+
if (isImportWord(t))
|
|
15438
|
+
return true;
|
|
15439
|
+
if (t.type === 'symbol' && t.value === '(') {
|
|
15440
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
15441
|
+
const t2 = tokens[j];
|
|
15442
|
+
if (isWs(t2))
|
|
15443
|
+
continue;
|
|
15444
|
+
if (isImportWord(t2))
|
|
15445
|
+
return true;
|
|
15446
|
+
break;
|
|
15447
|
+
}
|
|
15448
|
+
}
|
|
15449
|
+
break;
|
|
15450
|
+
}
|
|
15451
|
+
return false;
|
|
15452
|
+
}
|
|
15453
|
+
/**
|
|
15454
|
+
* Scans a raw token value for hex color literals and returns HTML with each
|
|
15455
|
+
* color wrapped in a swatch span. Non-color text is HTML-escaped.
|
|
15456
|
+
*/
|
|
15457
|
+
function injectColorSpans(raw, lineIndex, colOffset) {
|
|
15458
|
+
HEX_COLOR_RE.lastIndex = 0;
|
|
15459
|
+
let result = '';
|
|
15460
|
+
let lastIndex = 0;
|
|
15461
|
+
let match;
|
|
15462
|
+
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
15463
|
+
while ((match = HEX_COLOR_RE.exec(raw)) !== null) {
|
|
15464
|
+
result += esc(raw.slice(lastIndex, match.index));
|
|
15465
|
+
const color = match[0];
|
|
15466
|
+
const col = colOffset + match.index;
|
|
15467
|
+
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>`;
|
|
15468
|
+
lastIndex = match.index + color.length;
|
|
15469
|
+
}
|
|
15470
|
+
result += esc(raw.slice(lastIndex));
|
|
15471
|
+
return result;
|
|
15472
|
+
}
|
|
15188
15473
|
// _____ _ _ _____
|
|
15189
15474
|
// | __|___ ___ ___| | | __ |___ ___
|
|
15190
15475
|
// |__ | _| _| . | | | __ -| .'| _|
|
|
@@ -15207,6 +15492,15 @@
|
|
|
15207
15492
|
this.thumb = exports.LX.makeElement('div');
|
|
15208
15493
|
this.thumb.addEventListener('mousedown', (e) => this._onMouseDown(e));
|
|
15209
15494
|
this.root.appendChild(this.thumb);
|
|
15495
|
+
this.root.addEventListener('mousedown', (e) => {
|
|
15496
|
+
if (e.target === this.thumb)
|
|
15497
|
+
return;
|
|
15498
|
+
const clickPos = this._vertical ? e.offsetY : e.offsetX;
|
|
15499
|
+
const thumbSize = this._vertical ? this.thumb.offsetHeight : this.thumb.offsetWidth;
|
|
15500
|
+
const delta = (clickPos - thumbSize / 2) - this._thumbPos;
|
|
15501
|
+
this._onDrag?.(delta);
|
|
15502
|
+
this._onMouseDown(e); // continue as drag from new position
|
|
15503
|
+
});
|
|
15210
15504
|
}
|
|
15211
15505
|
setThumbRatio(ratio) {
|
|
15212
15506
|
this._thumbRatio = exports.LX.clamp(ratio, 0, 1);
|
|
@@ -15330,10 +15624,18 @@
|
|
|
15330
15624
|
onReady;
|
|
15331
15625
|
onCreateFile;
|
|
15332
15626
|
onCodeChange;
|
|
15627
|
+
onOpenPath;
|
|
15628
|
+
onHoverSymbol;
|
|
15333
15629
|
_inputArea;
|
|
15334
15630
|
// State:
|
|
15335
15631
|
_lineStates = []; // tokenizer state at end of each line
|
|
15336
15632
|
_lineElements = []; // <pre> element per line
|
|
15633
|
+
_bracketOpenLine = -1; // line of the { opening current scope
|
|
15634
|
+
_bracketCloseLine = -1; // line of the } closing current scope
|
|
15635
|
+
_hoverTimer = null;
|
|
15636
|
+
_hoverPopup = null;
|
|
15637
|
+
_hoverWord = '';
|
|
15638
|
+
_colorPopover = null; // active color picker popover
|
|
15337
15639
|
_openedTabs = {};
|
|
15338
15640
|
_loadedTabs = {};
|
|
15339
15641
|
_storedTabs = {};
|
|
@@ -15403,6 +15705,8 @@
|
|
|
15403
15705
|
this.onSelectTab = options.onSelectTab;
|
|
15404
15706
|
this.onReady = options.onReady;
|
|
15405
15707
|
this.onCodeChange = options.onCodeChange;
|
|
15708
|
+
this.onOpenPath = options.onOpenPath;
|
|
15709
|
+
this.onHoverSymbol = options.onHoverSymbol;
|
|
15406
15710
|
this.language = Tokenizer.getLanguage(this.highlight) ?? Tokenizer.getLanguage('Plain Text');
|
|
15407
15711
|
this.symbolTable = new SymbolTable();
|
|
15408
15712
|
// File explorer
|
|
@@ -15612,19 +15916,38 @@
|
|
|
15612
15916
|
this.codeArea.root.addEventListener('mousedown', this._onMouseDown.bind(this));
|
|
15613
15917
|
this.codeArea.root.addEventListener('contextmenu', this._onMouseDown.bind(this));
|
|
15614
15918
|
this.codeArea.root.addEventListener('mouseover', (e) => {
|
|
15615
|
-
const
|
|
15919
|
+
const target = e.target;
|
|
15920
|
+
const link = target.closest('.code-link');
|
|
15616
15921
|
if (link && e.ctrlKey)
|
|
15617
15922
|
link.classList.add('hovered');
|
|
15923
|
+
const path = target.closest('.code-path');
|
|
15924
|
+
if (path && e.ctrlKey)
|
|
15925
|
+
path.classList.add('hovered');
|
|
15618
15926
|
});
|
|
15619
15927
|
this.codeArea.root.addEventListener('mouseout', (e) => {
|
|
15620
|
-
const
|
|
15928
|
+
const target = e.target;
|
|
15929
|
+
const link = target.closest('.code-link');
|
|
15621
15930
|
if (link)
|
|
15622
15931
|
link.classList.remove('hovered');
|
|
15932
|
+
const path = target.closest('.code-path');
|
|
15933
|
+
if (path)
|
|
15934
|
+
path.classList.remove('hovered');
|
|
15623
15935
|
});
|
|
15624
15936
|
this.codeArea.root.addEventListener('mousemove', (e) => {
|
|
15625
|
-
const
|
|
15937
|
+
const target = e.target;
|
|
15938
|
+
const link = target.closest('.code-link');
|
|
15626
15939
|
if (link)
|
|
15627
15940
|
link.classList.toggle('hovered', e.ctrlKey);
|
|
15941
|
+
const path = target.closest('.code-path');
|
|
15942
|
+
if (path)
|
|
15943
|
+
path.classList.toggle('hovered', e.ctrlKey);
|
|
15944
|
+
this._onCodeAreaMouseMove(e);
|
|
15945
|
+
});
|
|
15946
|
+
this.codeArea.root.addEventListener('mouseleave', () => {
|
|
15947
|
+
this._clearHoverPopup();
|
|
15948
|
+
});
|
|
15949
|
+
this.codeArea.root.addEventListener('click', (e) => {
|
|
15950
|
+
this._onColorSwatchClick(e);
|
|
15628
15951
|
});
|
|
15629
15952
|
// Bottom status panel
|
|
15630
15953
|
this.statusPanel = this._createStatusPanel(options);
|
|
@@ -15716,7 +16039,7 @@
|
|
|
15716
16039
|
setText(text, language, detectLang = false) {
|
|
15717
16040
|
if (!this.currentTab)
|
|
15718
16041
|
return;
|
|
15719
|
-
this.doc.setText(text);
|
|
16042
|
+
this.doc.setText(this._normalizeText(text), true);
|
|
15720
16043
|
this.cursorSet.set(0, 0);
|
|
15721
16044
|
this.undoManager.clear();
|
|
15722
16045
|
this._lineStates = [];
|
|
@@ -15907,10 +16230,14 @@
|
|
|
15907
16230
|
const codeTab = {
|
|
15908
16231
|
name,
|
|
15909
16232
|
dom,
|
|
15910
|
-
doc: new CodeDocument(
|
|
16233
|
+
doc: new CodeDocument((doc) => {
|
|
16234
|
+
this._setTabModified(name, true);
|
|
16235
|
+
this.onCodeChange?.(doc);
|
|
16236
|
+
}),
|
|
15911
16237
|
cursorSet: new CursorSet(),
|
|
15912
16238
|
undoManager: new UndoManager(),
|
|
15913
16239
|
language: langName,
|
|
16240
|
+
modified: false,
|
|
15914
16241
|
title: options.title ?? name
|
|
15915
16242
|
};
|
|
15916
16243
|
this._openedTabs[name] = codeTab;
|
|
@@ -15934,7 +16261,7 @@
|
|
|
15934
16261
|
// Move into the sizer..
|
|
15935
16262
|
this.codeSizer.appendChild(dom);
|
|
15936
16263
|
if (options.text) {
|
|
15937
|
-
codeTab.doc.setText(options.text);
|
|
16264
|
+
codeTab.doc.setText(options.text, true);
|
|
15938
16265
|
codeTab.cursorSet.set(0, 0);
|
|
15939
16266
|
codeTab.undoManager.clear();
|
|
15940
16267
|
this._renderAllLines();
|
|
@@ -15995,15 +16322,14 @@
|
|
|
15995
16322
|
}
|
|
15996
16323
|
setCustomSuggestions(suggestions) {
|
|
15997
16324
|
if (!suggestions || suggestions.constructor !== Array) {
|
|
15998
|
-
console.warn('suggestions should be
|
|
16325
|
+
console.warn('suggestions should be an array!');
|
|
15999
16326
|
return;
|
|
16000
16327
|
}
|
|
16001
|
-
this.customSuggestions = suggestions;
|
|
16328
|
+
this.customSuggestions = suggestions.map(s => typeof s === 'string' ? { label: s } : s);
|
|
16002
16329
|
}
|
|
16003
16330
|
loadFile(file, options = {}) {
|
|
16004
16331
|
const onLoad = (text, name) => {
|
|
16005
|
-
|
|
16006
|
-
text = text.replaceAll('\r', '').replaceAll(/\t|\\t/g, ' '.repeat(this.tabSize));
|
|
16332
|
+
text = this._normalizeText(text);
|
|
16007
16333
|
const ext = exports.LX.getExtension(name);
|
|
16008
16334
|
const lang = options.language ?? (Tokenizer.getLanguage(options.language)
|
|
16009
16335
|
?? (Tokenizer.getLanguageByExtension(ext) ?? Tokenizer.getLanguage('Plain Text')));
|
|
@@ -16026,7 +16352,7 @@
|
|
|
16026
16352
|
title: options.title ?? name,
|
|
16027
16353
|
language: langName
|
|
16028
16354
|
});
|
|
16029
|
-
this.doc.setText(text);
|
|
16355
|
+
this.doc.setText(text, true);
|
|
16030
16356
|
this.setLanguage(langName, ext);
|
|
16031
16357
|
this.cursorSet.set(0, 0);
|
|
16032
16358
|
this.undoManager.clear();
|
|
@@ -16087,7 +16413,7 @@
|
|
|
16087
16413
|
language: langName
|
|
16088
16414
|
});
|
|
16089
16415
|
if (results.length === 0) {
|
|
16090
|
-
this.doc.setText(processedText);
|
|
16416
|
+
this.doc.setText(processedText, true);
|
|
16091
16417
|
this.setLanguage(langName, ext);
|
|
16092
16418
|
this.cursorSet.set(0, 0);
|
|
16093
16419
|
this.undoManager.clear();
|
|
@@ -16129,6 +16455,30 @@
|
|
|
16129
16455
|
}
|
|
16130
16456
|
}, 20);
|
|
16131
16457
|
}
|
|
16458
|
+
_findTabByPath(importPath) {
|
|
16459
|
+
// By now only uses base name
|
|
16460
|
+
const importBase = importPath.split('/').pop().replace(/\.\w+$/, '').toLowerCase();
|
|
16461
|
+
const allNames = new Set([
|
|
16462
|
+
...Object.keys(this._openedTabs),
|
|
16463
|
+
...Object.keys(this._loadedTabs),
|
|
16464
|
+
...Object.keys(this._storedTabs),
|
|
16465
|
+
]);
|
|
16466
|
+
for (const name of allNames) {
|
|
16467
|
+
const tabBase = name.split('/').pop().replace(/\.\w+$/, '').toLowerCase();
|
|
16468
|
+
if (tabBase === importBase)
|
|
16469
|
+
return name;
|
|
16470
|
+
}
|
|
16471
|
+
return null;
|
|
16472
|
+
}
|
|
16473
|
+
_setTabModified(name, modified) {
|
|
16474
|
+
const tab = this._openedTabs[name];
|
|
16475
|
+
if (!tab || tab.modified === modified)
|
|
16476
|
+
return;
|
|
16477
|
+
tab.modified = modified;
|
|
16478
|
+
const tabEl = this.tabs?.tabDOMs?.[name];
|
|
16479
|
+
if (tabEl)
|
|
16480
|
+
tabEl.toggleAttribute('data-modified', modified);
|
|
16481
|
+
}
|
|
16132
16482
|
_onSelectTab(isNewTabButton, event, name) {
|
|
16133
16483
|
if (this.disableEdition) {
|
|
16134
16484
|
return;
|
|
@@ -16354,20 +16704,55 @@
|
|
|
16354
16704
|
const lineText = this.doc.getLine(lineIndex);
|
|
16355
16705
|
const result = Tokenizer.tokenizeLine(lineText, this.language, prevState);
|
|
16356
16706
|
const langClass = this.language.name.toLowerCase().replace(/[^a-z]/g, '');
|
|
16357
|
-
|
|
16707
|
+
// Pre-compute which token index gets the bracket-highlight class
|
|
16708
|
+
let bracketTokenIdx = -1;
|
|
16709
|
+
if (lineIndex === this._bracketOpenLine) {
|
|
16710
|
+
// Last '{' symbol token on this line
|
|
16711
|
+
for (let i = result.tokens.length - 1; i >= 0; i--) {
|
|
16712
|
+
if (result.tokens[i].type === 'symbol' && result.tokens[i].value === '{') {
|
|
16713
|
+
bracketTokenIdx = i;
|
|
16714
|
+
break;
|
|
16715
|
+
}
|
|
16716
|
+
}
|
|
16717
|
+
}
|
|
16718
|
+
else if (lineIndex === this._bracketCloseLine) {
|
|
16719
|
+
// First '}' symbol token on this line
|
|
16720
|
+
for (let i = 0; i < result.tokens.length; i++) {
|
|
16721
|
+
if (result.tokens[i].type === 'symbol' && result.tokens[i].value === '}') {
|
|
16722
|
+
bracketTokenIdx = i;
|
|
16723
|
+
break;
|
|
16724
|
+
}
|
|
16725
|
+
}
|
|
16726
|
+
}
|
|
16358
16727
|
let html = '';
|
|
16359
|
-
|
|
16728
|
+
let colOffset = 0;
|
|
16729
|
+
for (let ti = 0; ti < result.tokens.length; ti++) {
|
|
16730
|
+
const token = result.tokens[ti];
|
|
16360
16731
|
const cls = TOKEN_CLASS_MAP[token.type];
|
|
16361
|
-
const
|
|
16362
|
-
|
|
16363
|
-
|
|
16364
|
-
|
|
16365
|
-
|
|
16366
|
-
|
|
16367
|
-
|
|
16368
|
-
|
|
16732
|
+
const tokenCol = colOffset;
|
|
16733
|
+
colOffset += token.value.length;
|
|
16734
|
+
// Inject content depending on type of token: color, url, path?
|
|
16735
|
+
let content;
|
|
16736
|
+
if (token.type === 'comment') {
|
|
16737
|
+
const escaped = token.value
|
|
16738
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
16739
|
+
content = escaped.replace(URL_REGEX, `<span class="code-link" data-url="$1">$1</span>`);
|
|
16740
|
+
}
|
|
16741
|
+
else if (token.type === 'string' && isImportPath(result.tokens, ti)) {
|
|
16742
|
+
const inner = token.value.slice(1, -1); // strip surrounding quotes
|
|
16743
|
+
const q = token.value[0];
|
|
16744
|
+
const escapedInner = inner.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
16745
|
+
content = `${q}<span class="code-path" data-path="${inner}">${escapedInner}</span>${q}`;
|
|
16746
|
+
}
|
|
16747
|
+
else {
|
|
16748
|
+
content = injectColorSpans(token.value, lineIndex, tokenCol);
|
|
16749
|
+
}
|
|
16750
|
+
const bracketClass = ti === bracketTokenIdx ? ' code-bracket-active' : '';
|
|
16369
16751
|
if (cls) {
|
|
16370
|
-
html += `<span class="${cls} ${langClass}">${content}</span>`;
|
|
16752
|
+
html += `<span class="${cls} ${langClass}${bracketClass}">${content}</span>`;
|
|
16753
|
+
}
|
|
16754
|
+
else if (bracketClass) {
|
|
16755
|
+
html += `<span class="${bracketClass.trim()}">${content}</span>`;
|
|
16371
16756
|
}
|
|
16372
16757
|
else {
|
|
16373
16758
|
html += content;
|
|
@@ -16448,6 +16833,13 @@
|
|
|
16448
16833
|
break;
|
|
16449
16834
|
}
|
|
16450
16835
|
}
|
|
16836
|
+
// Propagate/cascade scope updates to subsequent lines until the start-of-line scope stabilizes
|
|
16837
|
+
for (let i = lineIndex + 1; i < this.doc.lineCount; i++) {
|
|
16838
|
+
const oldStartDepth = this.symbolTable.getScopeAtLine(i).length;
|
|
16839
|
+
this.symbolTable.updateScopeForLine(i, this.doc.getLine(i));
|
|
16840
|
+
if (this.symbolTable.getScopeAtLine(i).length === oldStartDepth)
|
|
16841
|
+
break;
|
|
16842
|
+
}
|
|
16451
16843
|
}
|
|
16452
16844
|
/**
|
|
16453
16845
|
* Rebuild line elements after structural changes (insert/delete lines).
|
|
@@ -16489,12 +16881,9 @@
|
|
|
16489
16881
|
return;
|
|
16490
16882
|
this.cursorsLayer.innerHTML = '';
|
|
16491
16883
|
for (const sel of this.cursorSet.cursors) {
|
|
16492
|
-
const el =
|
|
16493
|
-
el.className = 'cursor';
|
|
16494
|
-
el.innerHTML = ' ';
|
|
16884
|
+
const el = exports.LX.makeElement('div', 'cursor', ' ', this.cursorsLayer);
|
|
16495
16885
|
el.style.left = (sel.head.col * this.charWidth + this.xPadding) + 'px';
|
|
16496
16886
|
el.style.top = (sel.head.line * this.lineHeight) + 'px';
|
|
16497
|
-
this.cursorsLayer.appendChild(el);
|
|
16498
16887
|
}
|
|
16499
16888
|
this._updateActiveLine();
|
|
16500
16889
|
}
|
|
@@ -16511,14 +16900,16 @@
|
|
|
16511
16900
|
const lineText = this.doc.getLine(line);
|
|
16512
16901
|
const fromCol = line === start.line ? start.col : 0;
|
|
16513
16902
|
const toCol = line === end.line ? end.col : lineText.length;
|
|
16514
|
-
|
|
16903
|
+
// Skip only when the selection ends exactly at col 0 of this line
|
|
16904
|
+
if (fromCol === toCol && line === end.line)
|
|
16515
16905
|
continue;
|
|
16516
|
-
const
|
|
16517
|
-
|
|
16906
|
+
const width = fromCol === toCol
|
|
16907
|
+
? Math.ceil(this.charWidth * 0.5) // minimum width for empty lines
|
|
16908
|
+
: (toCol - fromCol) * this.charWidth;
|
|
16909
|
+
const div = exports.LX.makeElement('div', 'lexcodeselection', '', this.selectionsLayer);
|
|
16518
16910
|
div.style.top = (line * this.lineHeight) + 'px';
|
|
16519
16911
|
div.style.left = (fromCol * this.charWidth + this.xPadding) + 'px';
|
|
16520
|
-
div.style.width =
|
|
16521
|
-
this.selectionsLayer.appendChild(div);
|
|
16912
|
+
div.style.width = width + 'px';
|
|
16522
16913
|
}
|
|
16523
16914
|
}
|
|
16524
16915
|
}
|
|
@@ -16623,6 +17014,8 @@
|
|
|
16623
17014
|
e.preventDefault();
|
|
16624
17015
|
if (this.onSave) {
|
|
16625
17016
|
this.onSave(this.getText(), this);
|
|
17017
|
+
this.undoManager.markSaved();
|
|
17018
|
+
this._setTabModified(this.currentTab.name, false);
|
|
16626
17019
|
}
|
|
16627
17020
|
return;
|
|
16628
17021
|
case 'z':
|
|
@@ -16645,6 +17038,17 @@
|
|
|
16645
17038
|
e.preventDefault();
|
|
16646
17039
|
this._doPaste();
|
|
16647
17040
|
return;
|
|
17041
|
+
case 'home':
|
|
17042
|
+
e.preventDefault();
|
|
17043
|
+
this.cursorSet.set(0, 0);
|
|
17044
|
+
this._afterCursorMove();
|
|
17045
|
+
return;
|
|
17046
|
+
case 'end':
|
|
17047
|
+
e.preventDefault();
|
|
17048
|
+
const lastLine = this.doc.lineCount - 1;
|
|
17049
|
+
this.cursorSet.set(lastLine, this.doc.getLine(lastLine).length);
|
|
17050
|
+
this._afterCursorMove();
|
|
17051
|
+
return;
|
|
16648
17052
|
case ' ':
|
|
16649
17053
|
e.preventDefault();
|
|
16650
17054
|
// Also call user callback if provided
|
|
@@ -17376,10 +17780,21 @@
|
|
|
17376
17780
|
this._rebuildLines();
|
|
17377
17781
|
this._afterCursorMove();
|
|
17378
17782
|
}
|
|
17783
|
+
/**
|
|
17784
|
+
* Normalize external text before inserting into the document:
|
|
17785
|
+
* - Unify line endings to \n
|
|
17786
|
+
* - Replace tab characters with the configured number of spaces
|
|
17787
|
+
*/
|
|
17788
|
+
_normalizeText(text) {
|
|
17789
|
+
return text
|
|
17790
|
+
.replace(/\r\n?/g, '\n')
|
|
17791
|
+
.replace(/\t/g, ' '.repeat(this.tabSize));
|
|
17792
|
+
}
|
|
17379
17793
|
async _doPaste() {
|
|
17380
|
-
const
|
|
17381
|
-
if (!
|
|
17794
|
+
const raw = await navigator.clipboard.readText();
|
|
17795
|
+
if (!raw)
|
|
17382
17796
|
return;
|
|
17797
|
+
const text = this._normalizeText(raw);
|
|
17383
17798
|
this._flushAction();
|
|
17384
17799
|
this._deleteSelectionIfAny();
|
|
17385
17800
|
const cursor = this.cursorSet.getPrimary();
|
|
@@ -17406,6 +17821,8 @@
|
|
|
17406
17821
|
}
|
|
17407
17822
|
this._rebuildLines();
|
|
17408
17823
|
this._afterCursorMove();
|
|
17824
|
+
if (this.currentTab)
|
|
17825
|
+
this._setTabModified(this.currentTab.name, this.undoManager.isModified());
|
|
17409
17826
|
}
|
|
17410
17827
|
}
|
|
17411
17828
|
_doRedo() {
|
|
@@ -17417,6 +17834,8 @@
|
|
|
17417
17834
|
}
|
|
17418
17835
|
this._rebuildLines();
|
|
17419
17836
|
this._afterCursorMove();
|
|
17837
|
+
if (this.currentTab)
|
|
17838
|
+
this._setTabModified(this.currentTab.name, this.undoManager.isModified());
|
|
17420
17839
|
}
|
|
17421
17840
|
}
|
|
17422
17841
|
// Mouse input events:
|
|
@@ -17427,7 +17846,7 @@
|
|
|
17427
17846
|
return;
|
|
17428
17847
|
if (this.autocomplete && this.autocomplete.contains(e.target))
|
|
17429
17848
|
return;
|
|
17430
|
-
// Ctrl+click: open link
|
|
17849
|
+
// Ctrl+click: open link or import path
|
|
17431
17850
|
if (e.ctrlKey && e.button === 0) {
|
|
17432
17851
|
const target = e.target;
|
|
17433
17852
|
const link = target.closest('.code-link');
|
|
@@ -17435,6 +17854,15 @@
|
|
|
17435
17854
|
window.open(link.dataset.url, '_blank');
|
|
17436
17855
|
return;
|
|
17437
17856
|
}
|
|
17857
|
+
const pathEl = target.closest('.code-path');
|
|
17858
|
+
if (pathEl?.dataset.path) {
|
|
17859
|
+
const rawPath = pathEl.dataset.path;
|
|
17860
|
+
const tabName = this._findTabByPath(rawPath);
|
|
17861
|
+
if (tabName)
|
|
17862
|
+
this.loadTab(tabName);
|
|
17863
|
+
this.onOpenPath?.(rawPath, this);
|
|
17864
|
+
return;
|
|
17865
|
+
}
|
|
17438
17866
|
}
|
|
17439
17867
|
e.preventDefault(); // Prevent browser from stealing focus from _inputArea
|
|
17440
17868
|
this._wasPaired = false;
|
|
@@ -17482,18 +17910,55 @@
|
|
|
17482
17910
|
}
|
|
17483
17911
|
this._afterCursorMove();
|
|
17484
17912
|
this._inputArea.focus();
|
|
17485
|
-
// Track mouse for drag selection
|
|
17486
|
-
|
|
17487
|
-
|
|
17488
|
-
|
|
17489
|
-
|
|
17490
|
-
const
|
|
17913
|
+
// Track mouse for drag selection (with auto-scroll when outside editor window/area)
|
|
17914
|
+
let lastMouseX = 0;
|
|
17915
|
+
let lastMouseY = 0;
|
|
17916
|
+
let rafId = null;
|
|
17917
|
+
const updateSelection = () => {
|
|
17918
|
+
const currentRect = this.codeContainer.getBoundingClientRect();
|
|
17919
|
+
const mx = lastMouseX - currentRect.left - this.xPadding;
|
|
17920
|
+
const my = lastMouseY - currentRect.top;
|
|
17921
|
+
const ml = exports.LX.clamp(Math.floor(my / this.lineHeight), 0, this.doc.lineCount - 1);
|
|
17922
|
+
const mc = exports.LX.clamp(Math.round(mx / this.charWidth), 0, this.doc.getLine(ml).length);
|
|
17491
17923
|
const sel = this.cursorSet.getPrimary();
|
|
17492
17924
|
sel.head = { line: ml, col: mc };
|
|
17493
17925
|
this._renderCursors();
|
|
17494
17926
|
this._renderSelections();
|
|
17495
17927
|
};
|
|
17928
|
+
const autoScroll = () => {
|
|
17929
|
+
const scrollerRect = this.codeScroller.getBoundingClientRect();
|
|
17930
|
+
const overshootY = lastMouseY < scrollerRect.top ? lastMouseY - scrollerRect.top
|
|
17931
|
+
: lastMouseY > scrollerRect.bottom ? lastMouseY - scrollerRect.bottom : 0;
|
|
17932
|
+
const overshootX = lastMouseX < scrollerRect.left ? lastMouseX - scrollerRect.left
|
|
17933
|
+
: lastMouseX > scrollerRect.right ? lastMouseX - scrollerRect.right : 0;
|
|
17934
|
+
if (overshootY === 0 && overshootX === 0) {
|
|
17935
|
+
rafId = null;
|
|
17936
|
+
return;
|
|
17937
|
+
}
|
|
17938
|
+
const speedY = Math.sign(overshootY) * Math.min(Math.abs(overshootY) * 0.3, 15);
|
|
17939
|
+
const speedX = Math.sign(overshootX) * Math.min(Math.abs(overshootX) * 0.3, 15);
|
|
17940
|
+
this.codeScroller.scrollTop += speedY;
|
|
17941
|
+
this.codeScroller.scrollLeft += speedX;
|
|
17942
|
+
this._syncScrollBars();
|
|
17943
|
+
updateSelection();
|
|
17944
|
+
rafId = requestAnimationFrame(autoScroll);
|
|
17945
|
+
};
|
|
17946
|
+
const onMouseMove = (me) => {
|
|
17947
|
+
lastMouseX = me.clientX;
|
|
17948
|
+
lastMouseY = me.clientY;
|
|
17949
|
+
updateSelection();
|
|
17950
|
+
const scrollerRect = this.codeScroller.getBoundingClientRect();
|
|
17951
|
+
const isOutside = me.clientY < scrollerRect.top || me.clientY > scrollerRect.bottom
|
|
17952
|
+
|| me.clientX < scrollerRect.left || me.clientX > scrollerRect.right;
|
|
17953
|
+
if (isOutside && rafId === null) {
|
|
17954
|
+
rafId = requestAnimationFrame(autoScroll);
|
|
17955
|
+
}
|
|
17956
|
+
};
|
|
17496
17957
|
const onMouseUp = () => {
|
|
17958
|
+
if (rafId !== null) {
|
|
17959
|
+
cancelAnimationFrame(rafId);
|
|
17960
|
+
rafId = null;
|
|
17961
|
+
}
|
|
17497
17962
|
document.removeEventListener('mousemove', onMouseMove);
|
|
17498
17963
|
document.removeEventListener('mouseup', onMouseUp);
|
|
17499
17964
|
};
|
|
@@ -17553,47 +18018,51 @@
|
|
|
17553
18018
|
}
|
|
17554
18019
|
const suggestions = [];
|
|
17555
18020
|
const added = new Set();
|
|
17556
|
-
const addSuggestion = (
|
|
17557
|
-
if (!added.has(label)) {
|
|
17558
|
-
suggestions.push(
|
|
17559
|
-
added.add(label);
|
|
18021
|
+
const addSuggestion = (s) => {
|
|
18022
|
+
if (!added.has(s.label)) {
|
|
18023
|
+
suggestions.push(s);
|
|
18024
|
+
added.add(s.label);
|
|
17560
18025
|
}
|
|
17561
18026
|
};
|
|
18027
|
+
const filterSuggestion = (suggestion, word) => {
|
|
18028
|
+
const w = word.toLowerCase();
|
|
18029
|
+
if (suggestion.filterText) {
|
|
18030
|
+
return suggestion.filterText.split(' ').some(token => token.toLowerCase().trim().startsWith(w));
|
|
18031
|
+
}
|
|
18032
|
+
return suggestion.label.toLowerCase().startsWith(w);
|
|
18033
|
+
};
|
|
17562
18034
|
// Get first suggestions from symbol table
|
|
17563
18035
|
const allSymbols = this.symbolTable.getAllSymbols();
|
|
17564
18036
|
for (const symbol of allSymbols) {
|
|
17565
|
-
|
|
17566
|
-
|
|
17567
|
-
|
|
18037
|
+
const s = { label: symbol.name, kind: symbol.kind, scope: symbol.scope, detail: `${symbol.kind} in ${symbol.scope}` };
|
|
18038
|
+
if (filterSuggestion(s, word))
|
|
18039
|
+
addSuggestion(s);
|
|
17568
18040
|
}
|
|
17569
18041
|
// Add language reserved keys
|
|
17570
18042
|
for (const reservedWord of this.language.reservedWords) {
|
|
17571
|
-
|
|
17572
|
-
|
|
17573
|
-
|
|
18043
|
+
const s = { label: reservedWord };
|
|
18044
|
+
if (filterSuggestion(s, word))
|
|
18045
|
+
addSuggestion(s);
|
|
17574
18046
|
}
|
|
17575
18047
|
// Add custom suggestions
|
|
17576
18048
|
for (const suggestion of this.customSuggestions) {
|
|
17577
|
-
|
|
17578
|
-
|
|
17579
|
-
const detail = typeof suggestion === 'object' ? suggestion.detail : undefined;
|
|
17580
|
-
const insertText = typeof suggestion === 'object' ? suggestion.insertText : suggestion;
|
|
17581
|
-
if (label.toLowerCase().startsWith(word.toLowerCase())) {
|
|
17582
|
-
addSuggestion(label, kind, undefined, detail, insertText);
|
|
17583
|
-
}
|
|
18049
|
+
if (filterSuggestion(suggestion, word))
|
|
18050
|
+
addSuggestion(suggestion);
|
|
17584
18051
|
}
|
|
17585
|
-
// Close autocomplete if no suggestions
|
|
17586
18052
|
if (suggestions.length === 0) {
|
|
17587
18053
|
this._doHideAutocomplete();
|
|
17588
18054
|
return;
|
|
17589
18055
|
}
|
|
17590
|
-
// Sort suggestions: exact matches first, then
|
|
18056
|
+
// Sort suggestions: exact matches first, then by sortText (or label if absent)
|
|
18057
|
+
const w = word.toLowerCase();
|
|
17591
18058
|
suggestions.sort((a, b) => {
|
|
17592
|
-
const
|
|
17593
|
-
const
|
|
18059
|
+
const aKey = (a.sortText ?? a.label).toLowerCase();
|
|
18060
|
+
const bKey = (b.sortText ?? b.label).toLowerCase();
|
|
18061
|
+
const aExact = aKey === w ? 0 : 1;
|
|
18062
|
+
const bExact = bKey === w ? 0 : 1;
|
|
17594
18063
|
if (aExact !== bExact)
|
|
17595
18064
|
return aExact - bExact;
|
|
17596
|
-
return
|
|
18065
|
+
return aKey.localeCompare(bKey);
|
|
17597
18066
|
});
|
|
17598
18067
|
this._selectedAutocompleteIndex = 0;
|
|
17599
18068
|
// Render suggestions
|
|
@@ -17602,9 +18071,9 @@
|
|
|
17602
18071
|
item.insertText = suggestion.insertText ?? suggestion.label;
|
|
17603
18072
|
if (index === this._selectedAutocompleteIndex)
|
|
17604
18073
|
item.classList.add('selected');
|
|
17605
|
-
const
|
|
17606
|
-
let iconName = 'CaseLower';
|
|
17607
|
-
let iconClass = '
|
|
18074
|
+
const currSuggestionLabel = suggestion.label;
|
|
18075
|
+
let iconName = suggestion.icon ?? 'CaseLower';
|
|
18076
|
+
let iconClass = suggestion.iconClass ?? 'text-gray-500';
|
|
17608
18077
|
switch (suggestion.kind) {
|
|
17609
18078
|
case 'class':
|
|
17610
18079
|
iconName = 'CircleNodes';
|
|
@@ -17651,26 +18120,22 @@
|
|
|
17651
18120
|
iconClass = 'text-green-500';
|
|
17652
18121
|
break;
|
|
17653
18122
|
case 'method-call':
|
|
17654
|
-
iconName = '
|
|
18123
|
+
iconName = 'Parentheses';
|
|
17655
18124
|
iconClass = 'text-gray-400';
|
|
17656
18125
|
break;
|
|
17657
|
-
default:
|
|
17658
|
-
iconName = 'CaseLower';
|
|
17659
|
-
iconClass = 'text-gray-500';
|
|
17660
|
-
break;
|
|
17661
18126
|
}
|
|
17662
18127
|
item.appendChild(exports.LX.makeIcon(iconName, { iconClass: 'ml-1 mr-2', svgClass: 'sm ' + iconClass }));
|
|
17663
18128
|
// Highlight the written part
|
|
17664
|
-
const hIndex =
|
|
18129
|
+
const hIndex = currSuggestionLabel.toLowerCase().indexOf(word.toLowerCase());
|
|
17665
18130
|
var preWord = document.createElement('span');
|
|
17666
|
-
preWord.textContent =
|
|
18131
|
+
preWord.textContent = currSuggestionLabel.substring(0, hIndex);
|
|
17667
18132
|
item.appendChild(preWord);
|
|
17668
18133
|
var actualWord = document.createElement('span');
|
|
17669
|
-
actualWord.textContent =
|
|
18134
|
+
actualWord.textContent = currSuggestionLabel.substring(hIndex, hIndex + word.length);
|
|
17670
18135
|
actualWord.classList.add('word-highlight');
|
|
17671
18136
|
item.appendChild(actualWord);
|
|
17672
18137
|
var postWord = document.createElement('span');
|
|
17673
|
-
postWord.textContent =
|
|
18138
|
+
postWord.textContent = currSuggestionLabel.substring(hIndex + word.length);
|
|
17674
18139
|
item.appendChild(postWord);
|
|
17675
18140
|
if (suggestion.kind) {
|
|
17676
18141
|
const kind = document.createElement('span');
|
|
@@ -17755,6 +18220,340 @@
|
|
|
17755
18220
|
this._resetBlinker();
|
|
17756
18221
|
this.resize();
|
|
17757
18222
|
this._scrollCursorIntoView();
|
|
18223
|
+
this._updateBracketHighlight();
|
|
18224
|
+
}
|
|
18225
|
+
/**
|
|
18226
|
+
* Returns the scope stack at the exact cursor position (line + column).
|
|
18227
|
+
* Basically starts from getScopeAtLine and then counts real braces up to the cursor column.
|
|
18228
|
+
*/
|
|
18229
|
+
_getScopeAtCursor() {
|
|
18230
|
+
const cursor = this.cursorSet.getPrimary().head;
|
|
18231
|
+
const line = cursor.line;
|
|
18232
|
+
const col = cursor.col;
|
|
18233
|
+
const lineText = this.doc.getLine(line);
|
|
18234
|
+
const scopeStack = [...this.symbolTable.getScopeAtLine(line)];
|
|
18235
|
+
let i = 0;
|
|
18236
|
+
let inString = false;
|
|
18237
|
+
let stringCh = '';
|
|
18238
|
+
while (i < col && i < lineText.length) {
|
|
18239
|
+
const ch = lineText[i];
|
|
18240
|
+
if (inString) {
|
|
18241
|
+
if (ch === '\\') {
|
|
18242
|
+
i += 2;
|
|
18243
|
+
continue;
|
|
18244
|
+
}
|
|
18245
|
+
if (ch === stringCh)
|
|
18246
|
+
inString = false;
|
|
18247
|
+
i++;
|
|
18248
|
+
continue;
|
|
18249
|
+
}
|
|
18250
|
+
if (ch === '/' && lineText[i + 1] === '/')
|
|
18251
|
+
break;
|
|
18252
|
+
if (ch === '/' && lineText[i + 1] === '*') {
|
|
18253
|
+
i += 2;
|
|
18254
|
+
while (i < col && !(lineText[i] === '*' && lineText[i + 1] === '/'))
|
|
18255
|
+
i++;
|
|
18256
|
+
i += 2;
|
|
18257
|
+
continue;
|
|
18258
|
+
}
|
|
18259
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
18260
|
+
inString = true;
|
|
18261
|
+
stringCh = ch;
|
|
18262
|
+
i++;
|
|
18263
|
+
continue;
|
|
18264
|
+
}
|
|
18265
|
+
if (ch === '{') {
|
|
18266
|
+
scopeStack.push({ name: 'anonymous', type: 'anonymous', line });
|
|
18267
|
+
}
|
|
18268
|
+
else if (ch === '}' && scopeStack.length > 1) {
|
|
18269
|
+
scopeStack.pop();
|
|
18270
|
+
}
|
|
18271
|
+
i++;
|
|
18272
|
+
}
|
|
18273
|
+
return scopeStack;
|
|
18274
|
+
}
|
|
18275
|
+
_updateBracketHighlight() {
|
|
18276
|
+
const scopes = this._getScopeAtCursor();
|
|
18277
|
+
// Find innermost non-global scope
|
|
18278
|
+
let innermost = null;
|
|
18279
|
+
for (let i = scopes.length - 1; i >= 0; i--) {
|
|
18280
|
+
if (scopes[i].type !== 'global') {
|
|
18281
|
+
innermost = scopes[i];
|
|
18282
|
+
break;
|
|
18283
|
+
}
|
|
18284
|
+
}
|
|
18285
|
+
const prevOpen = this._bracketOpenLine;
|
|
18286
|
+
const prevClose = this._bracketCloseLine;
|
|
18287
|
+
if (!innermost) {
|
|
18288
|
+
this._bracketOpenLine = -1;
|
|
18289
|
+
this._bracketCloseLine = -1;
|
|
18290
|
+
}
|
|
18291
|
+
else {
|
|
18292
|
+
const openLine = innermost.line;
|
|
18293
|
+
const targetDepth = scopes.length; // depth including the innermost scope
|
|
18294
|
+
// Closing line: last line where scope depth >= targetDepth
|
|
18295
|
+
let closeLine = openLine;
|
|
18296
|
+
for (let i = openLine + 1; i < this.doc.lineCount; i++) {
|
|
18297
|
+
if (this.symbolTable.getScopeAtLine(i).length >= targetDepth)
|
|
18298
|
+
closeLine = i;
|
|
18299
|
+
else
|
|
18300
|
+
break;
|
|
18301
|
+
}
|
|
18302
|
+
this._bracketOpenLine = openLine;
|
|
18303
|
+
this._bracketCloseLine = closeLine;
|
|
18304
|
+
}
|
|
18305
|
+
// Re-render only the lines that changed
|
|
18306
|
+
const linesToUpdate = new Set();
|
|
18307
|
+
if (prevOpen !== this._bracketOpenLine) {
|
|
18308
|
+
linesToUpdate.add(prevOpen);
|
|
18309
|
+
linesToUpdate.add(this._bracketOpenLine);
|
|
18310
|
+
}
|
|
18311
|
+
if (prevClose !== this._bracketCloseLine) {
|
|
18312
|
+
linesToUpdate.add(prevClose);
|
|
18313
|
+
linesToUpdate.add(this._bracketCloseLine);
|
|
18314
|
+
}
|
|
18315
|
+
for (const line of linesToUpdate) {
|
|
18316
|
+
if (line >= 0 && line < this.doc.lineCount)
|
|
18317
|
+
this._updateLine(line);
|
|
18318
|
+
}
|
|
18319
|
+
}
|
|
18320
|
+
// Color picker:
|
|
18321
|
+
_onColorSwatchClick(e) {
|
|
18322
|
+
const span = e.target.closest('.code-color');
|
|
18323
|
+
if (!span)
|
|
18324
|
+
return;
|
|
18325
|
+
e.stopPropagation();
|
|
18326
|
+
e.preventDefault();
|
|
18327
|
+
const colorValue = span.dataset.color;
|
|
18328
|
+
const lineIndex = parseInt(span.dataset.line);
|
|
18329
|
+
const colStart = parseInt(span.dataset.col);
|
|
18330
|
+
let currentLen = colorValue.length;
|
|
18331
|
+
if (this._colorPopover) {
|
|
18332
|
+
this._colorPopover.destroy();
|
|
18333
|
+
this._colorPopover = null;
|
|
18334
|
+
}
|
|
18335
|
+
const picker = new exports.LX.ColorPicker(colorValue, {
|
|
18336
|
+
colorModel: 'Hex',
|
|
18337
|
+
onChange: (color) => {
|
|
18338
|
+
// Generate a hex string matching the original length (# + 3/4/6/8 hex chars)
|
|
18339
|
+
const raw = color.hex.replace(/^#/, '');
|
|
18340
|
+
const digits = currentLen - 1;
|
|
18341
|
+
const newHex = '#' + (digits <= 4
|
|
18342
|
+
? raw.slice(0, digits).padEnd(digits, '0')
|
|
18343
|
+
: raw.slice(0, Math.min(digits, 8)).padEnd(digits, '0'));
|
|
18344
|
+
const lineText = this.doc.getLine(lineIndex);
|
|
18345
|
+
if (lineText.slice(colStart, colStart + currentLen) !== span.dataset.color)
|
|
18346
|
+
return;
|
|
18347
|
+
const delOp = this.doc.delete(lineIndex, colStart, currentLen);
|
|
18348
|
+
this.undoManager.record(delOp, this.cursorSet.getCursorPositions());
|
|
18349
|
+
const insOp = this.doc.insert(lineIndex, colStart, newHex);
|
|
18350
|
+
this.undoManager.record(insOp, this.cursorSet.getCursorPositions());
|
|
18351
|
+
this._updateLine(lineIndex);
|
|
18352
|
+
currentLen = newHex.length;
|
|
18353
|
+
span.dataset.color = newHex;
|
|
18354
|
+
span.dataset.col = String(colStart);
|
|
18355
|
+
span.style.setProperty('--code-color', newHex);
|
|
18356
|
+
}
|
|
18357
|
+
});
|
|
18358
|
+
this._colorPopover = new exports.LX.Popover(span, [picker], { side: 'bottom', align: 'start', sideOffset: 4 });
|
|
18359
|
+
}
|
|
18360
|
+
// Symbol hover:
|
|
18361
|
+
/**
|
|
18362
|
+
* Extracts the parameter list from a function/method declaration line.
|
|
18363
|
+
*/
|
|
18364
|
+
_getSymbolParams(symLine) {
|
|
18365
|
+
if (symLine < 0 || symLine >= this.doc.lineCount)
|
|
18366
|
+
return '()';
|
|
18367
|
+
const m = this.doc.getLine(symLine).match(/\(([^)]*)\)/);
|
|
18368
|
+
return m ? `(${m[1].trim()})` : '()';
|
|
18369
|
+
}
|
|
18370
|
+
/**
|
|
18371
|
+
* Starting from the line where a class is defined, scans forward to find
|
|
18372
|
+
* the constructor signature and returns its parameter list.
|
|
18373
|
+
*/
|
|
18374
|
+
_findConstructorParams(classLine) {
|
|
18375
|
+
const classDepth = this.symbolTable.getScopeAtLine(classLine).length;
|
|
18376
|
+
const maxScan = Math.min(classLine + 50, this.doc.lineCount);
|
|
18377
|
+
for (let i = classLine + 1; i < maxScan; i++) {
|
|
18378
|
+
if (this.symbolTable.getScopeAtLine(i).length < classDepth + 1)
|
|
18379
|
+
break;
|
|
18380
|
+
const m = this.doc.getLine(i).match(/\bconstructor\s*\(([^)]*)\)/);
|
|
18381
|
+
if (m)
|
|
18382
|
+
return `(${m[1].trim()})`;
|
|
18383
|
+
}
|
|
18384
|
+
return null;
|
|
18385
|
+
}
|
|
18386
|
+
/**
|
|
18387
|
+
* Given multiple symbols with the same name, pick the most likely one for
|
|
18388
|
+
* the hovered position using the available context data.
|
|
18389
|
+
*/
|
|
18390
|
+
_pickBestSymbol(symbols, word, tokenType, lineText, hoveredLine) {
|
|
18391
|
+
if (symbols.length === 1)
|
|
18392
|
+
return symbols[0];
|
|
18393
|
+
const curScopes = this.symbolTable.getScopeAtLine(hoveredLine);
|
|
18394
|
+
const curScope = [...curScopes].reverse().find(s => s.type !== 'global')?.name ?? 'global';
|
|
18395
|
+
const wordEsc = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
18396
|
+
const isNewCall = new RegExp(`\\bnew\\s+${wordEsc}\\b`).test(lineText);
|
|
18397
|
+
const isFuncCall = new RegExp(`\\b${wordEsc}\\s*[(<]`).test(lineText);
|
|
18398
|
+
const isTypeAnno = new RegExp(`(?::\\s*|<\\s*|[,>]\\s*)${wordEsc}\\b`).test(lineText);
|
|
18399
|
+
const typeKinds = new Set(['class', 'interface', 'type', 'enum', 'struct']);
|
|
18400
|
+
const funcKinds = new Set(['function', 'method']);
|
|
18401
|
+
const scored = symbols.map(sym => {
|
|
18402
|
+
let score = 0;
|
|
18403
|
+
// Defined in the current innermost scope
|
|
18404
|
+
if (sym.scope === curScope)
|
|
18405
|
+
score += 5;
|
|
18406
|
+
// Check token type
|
|
18407
|
+
if (tokenType === 'method' && funcKinds.has(sym.kind))
|
|
18408
|
+
score += 3;
|
|
18409
|
+
if (tokenType === 'type' && typeKinds.has(sym.kind))
|
|
18410
|
+
score += 3;
|
|
18411
|
+
// Hovered-line context patterns
|
|
18412
|
+
if (isNewCall && sym.kind === 'constructor-call')
|
|
18413
|
+
score += 20;
|
|
18414
|
+
if (isNewCall && sym.kind === 'class')
|
|
18415
|
+
score += 3;
|
|
18416
|
+
if (isFuncCall && funcKinds.has(sym.kind))
|
|
18417
|
+
score += 3;
|
|
18418
|
+
if (isTypeAnno && typeKinds.has(sym.kind))
|
|
18419
|
+
score += 2;
|
|
18420
|
+
// Validate kind based on symbol declaration line
|
|
18421
|
+
if (sym.line >= 0 && sym.line < this.doc.lineCount) {
|
|
18422
|
+
const declLine = this.doc.getLine(sym.line);
|
|
18423
|
+
if (/\bfunction\b/.test(declLine) && funcKinds.has(sym.kind))
|
|
18424
|
+
score += 4;
|
|
18425
|
+
if (/\bclass\b/.test(declLine) && sym.kind === 'class')
|
|
18426
|
+
score += 4;
|
|
18427
|
+
if (/\binterface\b/.test(declLine) && sym.kind === 'interface')
|
|
18428
|
+
score += 4;
|
|
18429
|
+
if (/\btype\b/.test(declLine) && sym.kind === 'type')
|
|
18430
|
+
score += 4;
|
|
18431
|
+
if (/\benum\b/.test(declLine) && sym.kind === 'enum')
|
|
18432
|
+
score += 4;
|
|
18433
|
+
if (/\b(?:const|let|var)\b/.test(declLine) && sym.kind === 'variable')
|
|
18434
|
+
score += 3;
|
|
18435
|
+
}
|
|
18436
|
+
// Order also by line proximity
|
|
18437
|
+
const dist = Math.abs(sym.line - hoveredLine);
|
|
18438
|
+
score += Math.max(0, 4 - Math.floor(dist / 20));
|
|
18439
|
+
return { sym, score };
|
|
18440
|
+
});
|
|
18441
|
+
scored.sort((a, b) => b.score - a.score);
|
|
18442
|
+
return scored[0].sym;
|
|
18443
|
+
}
|
|
18444
|
+
_clearHoverPopup() {
|
|
18445
|
+
if (this._hoverTimer) {
|
|
18446
|
+
clearTimeout(this._hoverTimer);
|
|
18447
|
+
this._hoverTimer = null;
|
|
18448
|
+
}
|
|
18449
|
+
if (this._hoverPopup) {
|
|
18450
|
+
this._hoverPopup.remove();
|
|
18451
|
+
this._hoverPopup = null;
|
|
18452
|
+
}
|
|
18453
|
+
this._hoverWord = '';
|
|
18454
|
+
}
|
|
18455
|
+
_onCodeAreaMouseMove(e) {
|
|
18456
|
+
if (!this.currentTab)
|
|
18457
|
+
return;
|
|
18458
|
+
// Only show hover when no button is pressed (no dragging)
|
|
18459
|
+
if (e.buttons !== 0) {
|
|
18460
|
+
this._clearHoverPopup();
|
|
18461
|
+
return;
|
|
18462
|
+
}
|
|
18463
|
+
const rect = this.codeContainer.getBoundingClientRect();
|
|
18464
|
+
const x = e.clientX - rect.left - this.xPadding;
|
|
18465
|
+
const y = e.clientY - rect.top;
|
|
18466
|
+
const line = exports.LX.clamp(Math.floor(y / this.lineHeight), 0, this.doc.lineCount - 1);
|
|
18467
|
+
const col = exports.LX.clamp(Math.round(x / this.charWidth), 0, this.doc.getLine(line).length);
|
|
18468
|
+
const [word, wordStart] = this.doc.getWordAt(line, col);
|
|
18469
|
+
if (!word || word === this._hoverWord)
|
|
18470
|
+
return;
|
|
18471
|
+
this._clearHoverPopup();
|
|
18472
|
+
if (!word.trim())
|
|
18473
|
+
return;
|
|
18474
|
+
this._hoverWord = word;
|
|
18475
|
+
this._hoverTimer = setTimeout(() => {
|
|
18476
|
+
this._hoverTimer = null;
|
|
18477
|
+
const prevState = line > 0 ? (this._lineStates[line - 1] ?? Tokenizer.initialState()) : Tokenizer.initialState();
|
|
18478
|
+
const { tokens } = Tokenizer.tokenizeLine(this.doc.getLine(line), this.language, prevState);
|
|
18479
|
+
let tokenType = 'text';
|
|
18480
|
+
let charPos = 0;
|
|
18481
|
+
for (const tok of tokens) {
|
|
18482
|
+
// Use wordStart (not col) so boundary positions like col=wordEnd don't fall into the next token
|
|
18483
|
+
if (wordStart >= charPos && wordStart < charPos + tok.value.length) {
|
|
18484
|
+
tokenType = tok.type;
|
|
18485
|
+
break;
|
|
18486
|
+
}
|
|
18487
|
+
charPos += tok.value.length;
|
|
18488
|
+
}
|
|
18489
|
+
if (tokenType === 'comment' || tokenType === 'string' || tokenType === 'number')
|
|
18490
|
+
return;
|
|
18491
|
+
const symbols = this.symbolTable.getSymbols(word);
|
|
18492
|
+
const info = { word, tokenType, symbols };
|
|
18493
|
+
let userContent;
|
|
18494
|
+
if (this.onHoverSymbol)
|
|
18495
|
+
userContent = this.onHoverSymbol(info, this);
|
|
18496
|
+
// No popup if user explicitly returns null
|
|
18497
|
+
if (userContent === null)
|
|
18498
|
+
return;
|
|
18499
|
+
const hasSymbols = symbols.length > 0;
|
|
18500
|
+
if (!userContent && !hasSymbols)
|
|
18501
|
+
return;
|
|
18502
|
+
const popup = this._hoverPopup = exports.LX.makeElement('div', 'code-hover-popup [&_span]:text-sm');
|
|
18503
|
+
if (userContent) {
|
|
18504
|
+
if (typeof userContent === 'string')
|
|
18505
|
+
popup.innerHTML = userContent;
|
|
18506
|
+
else
|
|
18507
|
+
popup.appendChild(userContent);
|
|
18508
|
+
}
|
|
18509
|
+
else {
|
|
18510
|
+
const sym = this._pickBestSymbol(symbols, word, tokenType, this.doc.getLine(line), line);
|
|
18511
|
+
let kindLabel = sym.kind;
|
|
18512
|
+
let nameLabel = `<span class="font-semibold">${word}</span>`;
|
|
18513
|
+
if (sym.kind === 'constructor-call') {
|
|
18514
|
+
const classSym = this.symbolTable.getSymbols(word).find(s => s.kind === 'class');
|
|
18515
|
+
const params = classSym != null ? (this._findConstructorParams(classSym.line) ?? '()') : '()';
|
|
18516
|
+
kindLabel = 'constructor';
|
|
18517
|
+
nameLabel = `${nameLabel}<span class="text-muted-foreground">${params}</span>`;
|
|
18518
|
+
}
|
|
18519
|
+
else if (sym.kind === 'function') {
|
|
18520
|
+
const params = this._getSymbolParams(sym.line);
|
|
18521
|
+
nameLabel = `${nameLabel}<span class="text-muted-foreground">${params}</span>`;
|
|
18522
|
+
}
|
|
18523
|
+
else if (sym.scope && sym.scope !== 'global' && sym.scope !== 'anonymous') {
|
|
18524
|
+
if (sym.kind === 'property' || sym.kind === 'method') {
|
|
18525
|
+
const scopePrefix = `<span class="text-muted-foreground">${sym.scope}.</span>`;
|
|
18526
|
+
if (sym.kind === 'method') {
|
|
18527
|
+
const params = this._getSymbolParams(sym.line);
|
|
18528
|
+
nameLabel = `${scopePrefix}${nameLabel}<span class="text-muted-foreground">${params}</span>`;
|
|
18529
|
+
}
|
|
18530
|
+
else {
|
|
18531
|
+
nameLabel = `${scopePrefix}${nameLabel}`;
|
|
18532
|
+
}
|
|
18533
|
+
}
|
|
18534
|
+
else if (sym.kind === 'variable') {
|
|
18535
|
+
// Only prefix the scope when the variable is a member (class/interface/struct field)
|
|
18536
|
+
const declScopes = this.symbolTable.getScopeAtLine(sym.line);
|
|
18537
|
+
const scopeEntry = declScopes.find(s => s.name === sym.scope);
|
|
18538
|
+
const memberTypes = new Set(['class', 'interface', 'struct']);
|
|
18539
|
+
if (scopeEntry && memberTypes.has(scopeEntry.type)) {
|
|
18540
|
+
nameLabel = `<span class="text-muted-foreground">${sym.scope}.</span>${nameLabel}`;
|
|
18541
|
+
}
|
|
18542
|
+
}
|
|
18543
|
+
}
|
|
18544
|
+
popup.innerHTML = `<span class="text-info">(${kindLabel})</span> ${nameLabel}`;
|
|
18545
|
+
}
|
|
18546
|
+
document.body.appendChild(popup);
|
|
18547
|
+
// Position just below the hovered word
|
|
18548
|
+
const lineEl = this._lineElements[line];
|
|
18549
|
+
if (lineEl) {
|
|
18550
|
+
const elRect = lineEl.getBoundingClientRect();
|
|
18551
|
+
const leftPx = elRect.left + this.xPadding + col * this.charWidth;
|
|
18552
|
+
const topPx = elRect.bottom + 4;
|
|
18553
|
+
popup.style.left = Math.min(leftPx, window.innerWidth - popup.offsetWidth - 8) + 'px';
|
|
18554
|
+
popup.style.top = topPx + 'px';
|
|
18555
|
+
}
|
|
18556
|
+
}, 500);
|
|
17758
18557
|
}
|
|
17759
18558
|
// Scrollbar & Resize:
|
|
17760
18559
|
_scrollCursorIntoView() {
|