elixir-data-viewer 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1581 @@
1
+ var K = Object.defineProperty;
2
+ var D = (r, t, e) => t in r ? K(r, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : r[t] = e;
3
+ var a = (r, t, e) => D(r, typeof t != "symbol" ? t + "" : t, e);
4
+ import { parser as M } from "lezer-elixir";
5
+ import { highlightTree as R, tagHighlighter as N, tags as p } from "@lezer/highlight";
6
+ function O(r) {
7
+ return M.parse(r);
8
+ }
9
+ const H = N([
10
+ // Atoms and keyword keys (:foo, key:)
11
+ { tag: p.atom, class: "tok-atom" },
12
+ // Module aliases (MyModule)
13
+ { tag: p.namespace, class: "tok-namespace" },
14
+ // Booleans: true, false
15
+ { tag: p.bool, class: "tok-bool" },
16
+ // nil
17
+ { tag: p.null, class: "tok-null" },
18
+ // Integer literals
19
+ { tag: p.integer, class: "tok-number" },
20
+ // Float literals
21
+ { tag: p.float, class: "tok-number" },
22
+ // Character literals (?a)
23
+ { tag: p.character, class: "tok-character" },
24
+ // Variable names (identifiers)
25
+ { tag: p.variableName, class: "tok-variableName" },
26
+ // Function calls: Call/Identifier, PipeOperator/Right/Identifier
27
+ { tag: p.function(p.variableName), class: "tok-function" },
28
+ // Function definitions: def foo, defp bar
29
+ {
30
+ tag: p.definition(p.function(p.variableName)),
31
+ class: "tok-definition"
32
+ },
33
+ // Special identifiers: __MODULE__, __DIR__, etc.
34
+ { tag: p.special(p.variableName), class: "tok-special" },
35
+ // Strings and charlists
36
+ { tag: p.string, class: "tok-string" },
37
+ // Sigils (~r/.../, ~w[...])
38
+ { tag: p.special(p.string), class: "tok-string" },
39
+ // Escape sequences (\n, \t, etc.)
40
+ { tag: p.escape, class: "tok-escape" },
41
+ // Keywords: do, end, fn, def, defmodule, when, not, etc.
42
+ { tag: p.keyword, class: "tok-keyword" },
43
+ // Operators: +, -, |>, ++, --, etc.
44
+ { tag: p.operator, class: "tok-operator" },
45
+ // Line comments (#...)
46
+ { tag: p.lineComment, class: "tok-comment" },
47
+ // Underscored identifiers (_foo) — mapped to tags.comment by lezer-elixir
48
+ { tag: p.comment, class: "tok-underscore" },
49
+ // Parentheses: (, )
50
+ { tag: p.paren, class: "tok-punctuation" },
51
+ // Square brackets: [, ]
52
+ { tag: p.squareBracket, class: "tok-punctuation" },
53
+ // Braces and percent: %, {, }
54
+ { tag: p.brace, class: "tok-punctuation" },
55
+ // Interpolation braces: #{, }
56
+ { tag: p.special(p.brace), class: "tok-punctuation" },
57
+ // Separators: comma, semicolon
58
+ { tag: p.separator, class: "tok-separator" },
59
+ // Angle brackets: <<, >>
60
+ { tag: p.angleBracket, class: "tok-angleBracket" },
61
+ // Module attributes: @attr
62
+ { tag: p.attributeName, class: "tok-attributeName" },
63
+ // Doc strings: @doc, @moduledoc, @typedoc
64
+ { tag: p.docString, class: "tok-docString" }
65
+ ]);
66
+ function P(r, t) {
67
+ const e = [];
68
+ return R(t, H, (s, i, n) => {
69
+ e.push({ from: s, to: i, classes: n });
70
+ }), e;
71
+ }
72
+ function C(r, t, e) {
73
+ const s = [];
74
+ for (const i of r)
75
+ i.to <= t || i.from >= e || s.push({
76
+ from: Math.max(i.from, t) - t,
77
+ to: Math.min(i.to, e) - t,
78
+ classes: i.classes
79
+ });
80
+ return s;
81
+ }
82
+ const W = {
83
+ List: { open: "[", close: "]" },
84
+ Tuple: { open: "{", close: "}" },
85
+ Map: { open: "%{", close: "}" },
86
+ Bitstring: { open: "<<", close: ">>" },
87
+ AnonymousFunction: { open: "fn", close: "end" },
88
+ String: { open: '"""', close: '"""' },
89
+ Charlist: { open: "'''", close: "'''" }
90
+ }, $ = /* @__PURE__ */ new Set(["List", "Tuple", "Map", "MapContent", "Keywords", "Bitstring"]), q = /* @__PURE__ */ new Set([",", "[", "]", "{", "}", "<<", ">>", "%", "|", "=>", ":"]);
91
+ function k(r) {
92
+ const t = r.type.name;
93
+ if (!$.has(t)) return -1;
94
+ let e = 0, s = r.firstChild;
95
+ for (; s; ) {
96
+ const i = s.type.name;
97
+ if (q.has(i)) {
98
+ s = s.nextSibling;
99
+ continue;
100
+ }
101
+ if (i === "MapContent" || i === "Keywords")
102
+ return k(s);
103
+ if (i === "Struct") {
104
+ s = s.nextSibling;
105
+ continue;
106
+ }
107
+ e++, s = s.nextSibling;
108
+ }
109
+ return e;
110
+ }
111
+ function V(r) {
112
+ const t = [0];
113
+ for (let e = 0; e < r.length; e++)
114
+ r[e] === `
115
+ ` && t.push(e + 1);
116
+ return t;
117
+ }
118
+ function S(r, t) {
119
+ let e = 0, s = r.length - 1;
120
+ for (; e < s; ) {
121
+ const i = e + s + 1 >> 1;
122
+ r[i] <= t ? e = i : s = i - 1;
123
+ }
124
+ return e;
125
+ }
126
+ function x(r, t, e, s) {
127
+ const i = W[r.type.name];
128
+ if (i) {
129
+ const l = S(e, r.from), o = S(e, r.to - 1);
130
+ if (o > l) {
131
+ let c = i.open, d = r.from;
132
+ if (r.type.name === "String" || r.type.name === "Charlist") {
133
+ const m = t.slice(r.from, Math.min(r.from + 3, r.to));
134
+ m === '"""' || m === "'''" ? c = m : c = m[0] || i.open;
135
+ }
136
+ s.push({
137
+ startLine: l,
138
+ endLine: o,
139
+ startOffset: d,
140
+ endOffset: r.to,
141
+ openText: c,
142
+ closeText: i.close,
143
+ itemCount: k(r),
144
+ depth: 0
145
+ // computed after collection
146
+ });
147
+ }
148
+ }
149
+ let n = r.firstChild;
150
+ for (; n; )
151
+ x(n, t, e, s), n = n.nextSibling;
152
+ }
153
+ function _(r, t) {
154
+ const e = V(r), s = [];
155
+ x(t.topNode, r, e, s), s.sort((n, l) => n.startLine - l.startLine || n.startOffset - l.startOffset);
156
+ const i = [];
157
+ for (const n of s) {
158
+ for (; i.length > 0 && i[i.length - 1].endOffset <= n.startOffset; )
159
+ i.pop();
160
+ n.depth = i.length + 1, i.push(n);
161
+ }
162
+ return s;
163
+ }
164
+ function U(r) {
165
+ const t = /* @__PURE__ */ new Map();
166
+ for (const e of r)
167
+ t.has(e.startLine) || t.set(e.startLine, e);
168
+ return t;
169
+ }
170
+ class z {
171
+ constructor() {
172
+ /** Set of startLine indices that are currently folded */
173
+ a(this, "foldedLines", /* @__PURE__ */ new Set());
174
+ /** Map from startLine to FoldRegion */
175
+ a(this, "regionMap", /* @__PURE__ */ new Map());
176
+ /** All fold regions sorted by startLine */
177
+ a(this, "regions", []);
178
+ }
179
+ /**
180
+ * Update the fold regions (called when content changes).
181
+ * Clears all fold state.
182
+ */
183
+ setRegions(t, e) {
184
+ this.regions = t, this.regionMap = e, this.foldedLines.clear();
185
+ }
186
+ /**
187
+ * Toggle a fold at the given startLine.
188
+ */
189
+ toggle(t) {
190
+ this.foldedLines.has(t) ? this.foldedLines.delete(t) : this.regionMap.has(t) && this.foldedLines.add(t);
191
+ }
192
+ /**
193
+ * Check if a given startLine is folded.
194
+ */
195
+ isFolded(t) {
196
+ return this.foldedLines.has(t);
197
+ }
198
+ /**
199
+ * Get the FoldRegion for a startLine, if any.
200
+ */
201
+ getRegion(t) {
202
+ return this.regionMap.get(t);
203
+ }
204
+ /**
205
+ * Check if a given line is hidden due to being inside a folded region.
206
+ * Returns the folded parent region if hidden, or undefined.
207
+ */
208
+ isLineHidden(t) {
209
+ for (const e of this.foldedLines) {
210
+ const s = this.regionMap.get(e);
211
+ if (s && t > s.startLine && t <= s.endLine)
212
+ return s;
213
+ }
214
+ }
215
+ /**
216
+ * Check if a line has a fold indicator (is the start of a foldable region).
217
+ */
218
+ isFoldable(t) {
219
+ return this.regionMap.has(t);
220
+ }
221
+ /**
222
+ * Get all regions.
223
+ */
224
+ getRegions() {
225
+ return this.regions;
226
+ }
227
+ /**
228
+ * Fold all regions.
229
+ */
230
+ foldAll() {
231
+ for (const t of this.regions)
232
+ this.foldedLines.add(t.startLine);
233
+ }
234
+ /**
235
+ * Unfold all regions.
236
+ */
237
+ unfoldAll() {
238
+ this.foldedLines.clear();
239
+ }
240
+ /**
241
+ * Fold all regions whose nesting depth exceeds maxLevel.
242
+ * Level 1 = outermost structures. foldToLevel(3) shows levels 1–3 expanded,
243
+ * level 4+ folded. foldToLevel(0) or negative values unfold all.
244
+ */
245
+ foldToLevel(t) {
246
+ if (this.foldedLines.clear(), !(t <= 0))
247
+ for (const e of this.regions)
248
+ e.depth > t && this.foldedLines.add(e.startLine);
249
+ }
250
+ /**
251
+ * Reveal a specific line by unfolding any region that hides it.
252
+ * This ensures the line becomes visible in the rendered output.
253
+ */
254
+ revealLine(t) {
255
+ for (const e of this.foldedLines) {
256
+ const s = this.regionMap.get(e);
257
+ s && t > s.startLine && t <= s.endLine && this.foldedLines.delete(e);
258
+ }
259
+ }
260
+ }
261
+ class Q {
262
+ constructor() {
263
+ a(this, "query", "");
264
+ a(this, "caseSensitive", !1);
265
+ a(this, "matches", []);
266
+ a(this, "currentIndex", -1);
267
+ }
268
+ /**
269
+ * Perform a search across all lines.
270
+ * Returns true if matches changed.
271
+ */
272
+ search(t, e, s) {
273
+ if (this.query = e, this.caseSensitive = s, this.matches = [], this.currentIndex = -1, !e) return !0;
274
+ const i = s ? e : e.toLowerCase();
275
+ for (let n = 0; n < t.length; n++) {
276
+ const l = s ? t[n] : t[n].toLowerCase();
277
+ let o = 0;
278
+ for (; o <= l.length - i.length; ) {
279
+ const c = l.indexOf(i, o);
280
+ if (c === -1) break;
281
+ this.matches.push({
282
+ line: n,
283
+ from: c,
284
+ to: c + i.length
285
+ }), o = c + 1;
286
+ }
287
+ }
288
+ return this.matches.length > 0 && (this.currentIndex = 0), !0;
289
+ }
290
+ /**
291
+ * Clear search state.
292
+ */
293
+ clear() {
294
+ this.query = "", this.matches = [], this.currentIndex = -1;
295
+ }
296
+ /**
297
+ * Get the current search query.
298
+ */
299
+ getQuery() {
300
+ return this.query;
301
+ }
302
+ /**
303
+ * Get whether case-sensitive mode is active.
304
+ */
305
+ isCaseSensitive() {
306
+ return this.caseSensitive;
307
+ }
308
+ /**
309
+ * Get all matches.
310
+ */
311
+ getMatches() {
312
+ return this.matches;
313
+ }
314
+ /**
315
+ * Get matches for a specific line.
316
+ */
317
+ getLineMatches(t) {
318
+ return this.matches.filter((e) => e.line === t);
319
+ }
320
+ /**
321
+ * Get the current match index (0-based), or -1 if no matches.
322
+ */
323
+ getCurrentIndex() {
324
+ return this.currentIndex;
325
+ }
326
+ /**
327
+ * Get the current match, or undefined if none.
328
+ */
329
+ getCurrentMatch() {
330
+ if (!(this.currentIndex < 0 || this.currentIndex >= this.matches.length))
331
+ return this.matches[this.currentIndex];
332
+ }
333
+ /**
334
+ * Get total number of matches.
335
+ */
336
+ getMatchCount() {
337
+ return this.matches.length;
338
+ }
339
+ /**
340
+ * Move to the next match. Wraps around.
341
+ */
342
+ next() {
343
+ if (this.matches.length !== 0)
344
+ return this.currentIndex = (this.currentIndex + 1) % this.matches.length, this.matches[this.currentIndex];
345
+ }
346
+ /**
347
+ * Move to the previous match. Wraps around.
348
+ */
349
+ prev() {
350
+ if (this.matches.length !== 0)
351
+ return this.currentIndex = (this.currentIndex - 1 + this.matches.length) % this.matches.length, this.matches[this.currentIndex];
352
+ }
353
+ /**
354
+ * Set the current match to the nearest one at or after the given line.
355
+ * Used when opening search to start from the visible area.
356
+ */
357
+ setCurrentToLine(t) {
358
+ if (this.matches.length !== 0) {
359
+ for (let e = 0; e < this.matches.length; e++)
360
+ if (this.matches[e].line >= t) {
361
+ this.currentIndex = e;
362
+ return;
363
+ }
364
+ this.currentIndex = 0;
365
+ }
366
+ }
367
+ /**
368
+ * Check if a match at the given line/from is the current match.
369
+ */
370
+ isCurrentMatch(t, e) {
371
+ const s = this.getCurrentMatch();
372
+ return s ? s.line === t && s.from === e : !1;
373
+ }
374
+ /**
375
+ * Has active search query.
376
+ */
377
+ isActive() {
378
+ return this.query.length > 0;
379
+ }
380
+ }
381
+ class Y {
382
+ constructor() {
383
+ /** Keys to filter out (hide) */
384
+ a(this, "filteredKeys", /* @__PURE__ */ new Set());
385
+ /** All detected key-value ranges from the parsed content */
386
+ a(this, "keyRanges", []);
387
+ /** Pre-computed set of hidden line indices (rebuilt when filter changes) */
388
+ a(this, "hiddenLines", /* @__PURE__ */ new Set());
389
+ }
390
+ /**
391
+ * Detect all key-value ranges from the syntax tree and source code.
392
+ * Called when content changes via setContent().
393
+ */
394
+ detectKeys(t, e) {
395
+ const s = Z(e);
396
+ this.keyRanges = [], B(t.topNode, e, s, this.keyRanges, 0), this.rebuildHiddenLines();
397
+ }
398
+ /**
399
+ * Set the keys to filter out (replaces existing filter).
400
+ */
401
+ setKeys(t) {
402
+ this.filteredKeys = new Set(t), this.rebuildHiddenLines();
403
+ }
404
+ /**
405
+ * Add a single key to the filter.
406
+ */
407
+ addKey(t) {
408
+ this.filteredKeys.add(t), this.rebuildHiddenLines();
409
+ }
410
+ /**
411
+ * Remove a single key from the filter.
412
+ */
413
+ removeKey(t) {
414
+ this.filteredKeys.delete(t), this.rebuildHiddenLines();
415
+ }
416
+ /**
417
+ * Check if a key is currently being filtered.
418
+ */
419
+ hasKey(t) {
420
+ return this.filteredKeys.has(t);
421
+ }
422
+ /**
423
+ * Get all currently filtered keys.
424
+ */
425
+ getKeys() {
426
+ return Array.from(this.filteredKeys);
427
+ }
428
+ /**
429
+ * Get all available keys detected in the content.
430
+ * Returns unique key names sorted alphabetically.
431
+ */
432
+ getAvailableKeys() {
433
+ const t = /* @__PURE__ */ new Set();
434
+ for (const e of this.keyRanges)
435
+ t.add(e.key);
436
+ return Array.from(t).sort();
437
+ }
438
+ /**
439
+ * Clear all filters (show all keys).
440
+ */
441
+ clear() {
442
+ this.filteredKeys.clear(), this.hiddenLines.clear();
443
+ }
444
+ /**
445
+ * Check if a specific line should be hidden due to filtering.
446
+ */
447
+ isLineFiltered(t) {
448
+ return this.hiddenLines.has(t);
449
+ }
450
+ /**
451
+ * Check if any filter is active.
452
+ */
453
+ isActive() {
454
+ return this.filteredKeys.size > 0;
455
+ }
456
+ /**
457
+ * Get the total number of filtered keys.
458
+ */
459
+ getFilteredCount() {
460
+ return this.filteredKeys.size;
461
+ }
462
+ /**
463
+ * Rebuild the set of hidden line indices based on current filtered keys.
464
+ */
465
+ rebuildHiddenLines() {
466
+ if (this.hiddenLines.clear(), this.filteredKeys.size !== 0) {
467
+ for (const t of this.keyRanges)
468
+ if (this.filteredKeys.has(t.key))
469
+ for (let e = t.startLine; e <= t.endLine; e++)
470
+ this.hiddenLines.add(e);
471
+ }
472
+ }
473
+ }
474
+ function Z(r) {
475
+ const t = [0];
476
+ for (let e = 0; e < r.length; e++)
477
+ r[e] === `
478
+ ` && t.push(e + 1);
479
+ return t;
480
+ }
481
+ function L(r, t) {
482
+ let e = 0, s = r.length - 1;
483
+ for (; e < s; ) {
484
+ const i = e + s + 1 >> 1;
485
+ r[i] <= t ? e = i : s = i - 1;
486
+ }
487
+ return e;
488
+ }
489
+ function B(r, t, e, s, i) {
490
+ const n = r.type.name;
491
+ if (n === "Pair") {
492
+ const d = j(r, t);
493
+ if (d !== null) {
494
+ const m = L(e, r.from), f = L(e, r.to - 1);
495
+ s.push({
496
+ key: d,
497
+ startLine: m,
498
+ endLine: f,
499
+ depth: i
500
+ });
501
+ }
502
+ }
503
+ const o = n === "Map" || n === "List" || n === "Tuple" || n === "MapContent" || n === "Keywords" ? i + 1 : i;
504
+ let c = r.firstChild;
505
+ for (; c; )
506
+ B(c, t, e, s, o), c = c.nextSibling;
507
+ }
508
+ function j(r, t) {
509
+ const e = r.firstChild;
510
+ if (!e) return null;
511
+ const s = e.type.name, i = t.slice(e.from, e.to);
512
+ return s === "Keyword" ? i.replace(/:\s*$/, "") : s === "Atom" ? i.replace(/^:/, "") : s === "String" ? i.replace(/^"/, "").replace(/"$/, "") : i.length > 0 ? i : null;
513
+ }
514
+ const b = /* @__PURE__ */ new Set(["Map", "List", "Tuple", "Bitstring"]), X = /* @__PURE__ */ new Set(["{", "}", "[", "]", "<<", ">>"]), w = /* @__PURE__ */ new Set([
515
+ "String",
516
+ "Atom",
517
+ "Alias",
518
+ "Integer",
519
+ "Float",
520
+ "Boolean",
521
+ "Nil",
522
+ "Char",
523
+ "Charlist",
524
+ "Sigil"
525
+ ]), G = /* @__PURE__ */ new Set(["QuotedContent"]);
526
+ function J(r, t, e) {
527
+ if (e < 0 || e >= t.length) return null;
528
+ const s = r.resolveInner(e, 1);
529
+ return s ? tt(s, t) : null;
530
+ }
531
+ function tt(r, t) {
532
+ const e = r.type.name;
533
+ if (X.has(e)) {
534
+ const i = r.parent;
535
+ if (i && b.has(i.type.name))
536
+ return {
537
+ from: i.from,
538
+ to: i.to,
539
+ copyText: t.slice(i.from, i.to),
540
+ isStructure: !0,
541
+ type: i.type.name
542
+ };
543
+ }
544
+ if (b.has(e))
545
+ return {
546
+ from: r.from,
547
+ to: r.to,
548
+ copyText: t.slice(r.from, r.to),
549
+ isStructure: !0,
550
+ type: e
551
+ };
552
+ const s = et(r, t);
553
+ if (s) return s;
554
+ if (G.has(e)) {
555
+ const i = r.parent;
556
+ if (i && w.has(i.type.name))
557
+ return {
558
+ from: i.from,
559
+ to: i.to,
560
+ copyText: t.slice(i.from, i.to),
561
+ isStructure: !1,
562
+ type: i.type.name
563
+ };
564
+ }
565
+ if (w.has(e))
566
+ return {
567
+ from: r.from,
568
+ to: r.to,
569
+ copyText: t.slice(r.from, r.to),
570
+ isStructure: !1,
571
+ type: e
572
+ };
573
+ if (e === "Keyword") {
574
+ const n = ":" + t.slice(r.from, r.to).replace(/:\s*$/, "");
575
+ return {
576
+ from: r.from,
577
+ to: r.to,
578
+ copyText: n,
579
+ isStructure: !1,
580
+ type: "Keyword"
581
+ };
582
+ }
583
+ return e === "Pair" ? {
584
+ from: r.from,
585
+ to: r.to,
586
+ copyText: t.slice(r.from, r.to),
587
+ isStructure: !1,
588
+ type: "Pair"
589
+ } : null;
590
+ }
591
+ function T(r, t) {
592
+ if (r.type.name !== "BinaryOperator") return !1;
593
+ let e = r.firstChild;
594
+ for (; e; ) {
595
+ if (e.type.name === "Operator" && t.slice(e.from, e.to) === "..")
596
+ return !0;
597
+ e = e.nextSibling;
598
+ }
599
+ return !1;
600
+ }
601
+ function I(r, t) {
602
+ if (r.type.name !== "BinaryOperator") return !1;
603
+ let e = !1, s = !1, i = r.firstChild;
604
+ for (; i; )
605
+ i.type.name === "Operator" && t.slice(i.from, i.to) === "//" && (e = !0), T(i, t) && (s = !0), i = i.nextSibling;
606
+ return e && s;
607
+ }
608
+ function et(r, t) {
609
+ let e = r;
610
+ for (; e; ) {
611
+ if (e.type.name === "BinaryOperator") {
612
+ if (I(e, t))
613
+ return {
614
+ from: e.from,
615
+ to: e.to,
616
+ copyText: t.slice(e.from, e.to),
617
+ isStructure: !1,
618
+ type: "Range"
619
+ };
620
+ if (T(e, t))
621
+ return e.parent && I(e.parent, t) ? {
622
+ from: e.parent.from,
623
+ to: e.parent.to,
624
+ copyText: t.slice(e.parent.from, e.parent.to),
625
+ isStructure: !1,
626
+ type: "Range"
627
+ } : {
628
+ from: e.from,
629
+ to: e.to,
630
+ copyText: t.slice(e.from, e.to),
631
+ isStructure: !1,
632
+ type: "Range"
633
+ };
634
+ }
635
+ e = e.parent;
636
+ }
637
+ return null;
638
+ }
639
+ const st = /#[A-Z][\w.]*<[^>\n]*>/g;
640
+ function it(r) {
641
+ const t = [];
642
+ return { modifiedCode: r.replace(
643
+ st,
644
+ (s, i) => (t.push({
645
+ from: i,
646
+ to: i + s.length,
647
+ originalText: s
648
+ }), ":" + "_".repeat(s.length - 1))
649
+ ), inspectLiterals: t };
650
+ }
651
+ class at {
652
+ constructor(t, e) {
653
+ a(this, "container");
654
+ a(this, "innerEl");
655
+ a(this, "scrollEl");
656
+ a(this, "toolbarEl", null);
657
+ a(this, "wrapBtn", null);
658
+ a(this, "copyBtn", null);
659
+ a(this, "searchBtn", null);
660
+ a(this, "filterBtn", null);
661
+ a(this, "copyResetTimer", null);
662
+ a(this, "code", "");
663
+ a(this, "lines", []);
664
+ a(this, "lineOffsets", []);
665
+ a(this, "tokens", []);
666
+ a(this, "tree", null);
667
+ a(this, "foldState", new z());
668
+ a(this, "filterState", new Y());
669
+ a(this, "defaultFoldLevel", 0);
670
+ a(this, "defaultFilterKeys", []);
671
+ a(this, "searchState", new Q());
672
+ a(this, "onRenderCallback", null);
673
+ a(this, "wordWrap", !1);
674
+ a(this, "toolbarConfig");
675
+ // Search UI elements
676
+ a(this, "searchBarEl", null);
677
+ a(this, "searchInputEl", null);
678
+ a(this, "searchInfoEl", null);
679
+ a(this, "searchCaseBtn", null);
680
+ a(this, "searchVisible", !1);
681
+ // Filter UI elements
682
+ a(this, "filterBarEl", null);
683
+ a(this, "filterInputEl", null);
684
+ a(this, "filterTagsEl", null);
685
+ a(this, "filterInfoEl", null);
686
+ a(this, "filterDropdownEl", null);
687
+ a(this, "filterCopyBtn", null);
688
+ a(this, "filterCopyResetTimer", null);
689
+ a(this, "filterVisible", !1);
690
+ a(this, "filterDropdownIndex", -1);
691
+ a(this, "filterDropdownItems", []);
692
+ // Inspect state
693
+ a(this, "currentInspect", null);
694
+ a(this, "inspectCallback", null);
695
+ // Pre-processed inspect literals (#Reference<...>, #PID<...>, etc.)
696
+ a(this, "inspectLiterals", []);
697
+ this.container = t, this.container.classList.add("edv-container"), this.defaultFoldLevel = (e == null ? void 0 : e.defaultFoldLevel) ?? 0, this.defaultFilterKeys = (e == null ? void 0 : e.defaultFilterKeys) ?? [], this.wordWrap = (e == null ? void 0 : e.defaultWordWrap) ?? !1;
698
+ const s = (e == null ? void 0 : e.toolbar) ?? {};
699
+ this.toolbarConfig = {
700
+ foldAll: s.foldAll !== !1,
701
+ unfoldAll: s.unfoldAll !== !1,
702
+ wordWrap: s.wordWrap !== !1,
703
+ copy: s.copy !== !1,
704
+ search: s.search !== !1,
705
+ filter: s.filter !== !1
706
+ }, this.buildToolbar(), this.wordWrap && (this.container.classList.add("edv-word-wrap"), this.wrapBtn && this.wrapBtn.classList.add("edv-toolbar-btn--active")), this.buildSearchBar(), this.buildFilterBar(), this.innerEl = document.createElement("div"), this.innerEl.classList.add("edv-inner"), this.container.appendChild(this.innerEl), this.scrollEl = document.createElement("div"), this.scrollEl.classList.add("edv-scroll"), this.innerEl.appendChild(this.scrollEl), this.scrollEl.addEventListener("mouseover", (i) => this.handleInspectHover(i)), this.scrollEl.addEventListener("mouseout", (i) => this.handleInspectOut(i)), this.scrollEl.addEventListener("click", (i) => this.handleInspectClick(i)), this.container.setAttribute("tabindex", "0"), this.container.addEventListener("keydown", (i) => this.handleKeyDown(i));
707
+ }
708
+ /**
709
+ * Build the floating toolbar DOM and append to the container.
710
+ */
711
+ buildToolbar() {
712
+ const t = this.toolbarConfig;
713
+ if (t.foldAll || t.unfoldAll || t.wordWrap || t.copy || t.search || t.filter) {
714
+ if (this.toolbarEl = document.createElement("div"), this.toolbarEl.classList.add("edv-toolbar"), t.search && (this.searchBtn = this.createToolbarButton(
715
+ "⌕",
716
+ "Search (Ctrl+F)",
717
+ () => this.toggleSearch()
718
+ ), this.toolbarEl.appendChild(this.searchBtn)), t.filter && (this.filterBtn = this.createToolbarButton(
719
+ "⧩",
720
+ "Filter Keys",
721
+ () => this.toggleFilter()
722
+ ), this.toolbarEl.appendChild(this.filterBtn)), t.foldAll) {
723
+ const s = this.createToolbarButton("⊟", "Fold All", () => this.foldAll());
724
+ this.toolbarEl.appendChild(s);
725
+ }
726
+ if (t.unfoldAll) {
727
+ const s = this.createToolbarButton("⊞", "Unfold All", () => this.unfoldAll());
728
+ this.toolbarEl.appendChild(s);
729
+ }
730
+ t.wordWrap && (this.wrapBtn = this.createToolbarButton("↩", "Word Wrap (Alt+Z)", () => {
731
+ this.toggleWordWrap();
732
+ }), this.toolbarEl.appendChild(this.wrapBtn)), t.copy && (this.copyBtn = this.createToolbarButton("⎘", "Copy", () => this.copyContent()), this.toolbarEl.appendChild(this.copyBtn)), this.container.appendChild(this.toolbarEl);
733
+ }
734
+ }
735
+ /**
736
+ * Build the search bar DOM (hidden by default).
737
+ */
738
+ buildSearchBar() {
739
+ this.searchBarEl = document.createElement("div"), this.searchBarEl.classList.add("edv-search-bar");
740
+ const t = document.createElement("div");
741
+ t.classList.add("edv-search-input-wrapper"), this.searchInputEl = document.createElement("input"), this.searchInputEl.type = "text", this.searchInputEl.classList.add("edv-search-input"), this.searchInputEl.placeholder = "Search…", this.searchInputEl.addEventListener("input", () => this.onSearchInput()), this.searchInputEl.addEventListener(
742
+ "keydown",
743
+ (n) => this.handleSearchKeyDown(n)
744
+ ), this.searchCaseBtn = document.createElement("button"), this.searchCaseBtn.classList.add("edv-search-case-btn"), this.searchCaseBtn.textContent = "Aa", this.searchCaseBtn.title = "Match Case", this.searchCaseBtn.addEventListener("click", (n) => {
745
+ n.stopPropagation(), this.toggleCaseSensitive();
746
+ }), t.appendChild(this.searchInputEl), t.appendChild(this.searchCaseBtn), this.searchBarEl.appendChild(t), this.searchInfoEl = document.createElement("span"), this.searchInfoEl.classList.add("edv-search-info"), this.searchBarEl.appendChild(this.searchInfoEl);
747
+ const e = document.createElement("button");
748
+ e.classList.add("edv-search-nav-btn"), e.textContent = "↑", e.title = "Previous Match (Shift+Enter)", e.addEventListener("click", (n) => {
749
+ n.stopPropagation(), this.searchPrev();
750
+ }), this.searchBarEl.appendChild(e);
751
+ const s = document.createElement("button");
752
+ s.classList.add("edv-search-nav-btn"), s.textContent = "↓", s.title = "Next Match (Enter)", s.addEventListener("click", (n) => {
753
+ n.stopPropagation(), this.searchNext();
754
+ }), this.searchBarEl.appendChild(s);
755
+ const i = document.createElement("button");
756
+ i.classList.add("edv-search-nav-btn"), i.textContent = "✕", i.title = "Close (Escape)", i.addEventListener("click", (n) => {
757
+ n.stopPropagation(), this.closeSearch();
758
+ }), this.searchBarEl.appendChild(i), this.container.appendChild(this.searchBarEl);
759
+ }
760
+ /**
761
+ * Handle keyboard shortcuts on the container.
762
+ */
763
+ handleKeyDown(t) {
764
+ (t.metaKey || t.ctrlKey) && t.key === "f" && (t.preventDefault(), t.stopPropagation(), this.openSearch()), t.key === "Escape" && this.searchVisible && (t.preventDefault(), this.closeSearch());
765
+ }
766
+ /**
767
+ * Handle keyboard events inside the search input.
768
+ */
769
+ handleSearchKeyDown(t) {
770
+ t.key === "Enter" && (t.preventDefault(), t.shiftKey ? this.searchPrev() : this.searchNext()), t.key === "Escape" && (t.preventDefault(), this.closeSearch());
771
+ }
772
+ /**
773
+ * Create a toolbar button element.
774
+ */
775
+ createToolbarButton(t, e, s) {
776
+ const i = document.createElement("button");
777
+ return i.classList.add("edv-toolbar-btn"), i.textContent = t, i.title = e, i.addEventListener("click", (n) => {
778
+ n.stopPropagation(), s();
779
+ }), i;
780
+ }
781
+ /**
782
+ * Toggle word wrap mode for long lines.
783
+ */
784
+ toggleWordWrap() {
785
+ this.wordWrap = !this.wordWrap, this.container.classList.toggle("edv-word-wrap", this.wordWrap), this.wrapBtn && this.wrapBtn.classList.toggle("edv-toolbar-btn--active", this.wordWrap);
786
+ }
787
+ /**
788
+ * Get current word wrap state.
789
+ */
790
+ isWordWrap() {
791
+ return this.wordWrap;
792
+ }
793
+ /**
794
+ * Fold all foldable regions.
795
+ */
796
+ foldAll() {
797
+ this.foldState.foldAll(), this.render();
798
+ }
799
+ /**
800
+ * Unfold all folded regions.
801
+ */
802
+ unfoldAll() {
803
+ this.foldState.unfoldAll(), this.render();
804
+ }
805
+ /**
806
+ * Fold all regions deeper than the given level.
807
+ * Level 1 = top-level structures. foldToLevel(3) expands levels 1–3, folds 4+.
808
+ * foldToLevel(0) unfolds all.
809
+ */
810
+ foldToLevel(t) {
811
+ this.foldState.foldToLevel(t), this.render();
812
+ }
813
+ /**
814
+ * Get the raw Elixir data content.
815
+ */
816
+ getContent() {
817
+ return this.code;
818
+ }
819
+ /**
820
+ * Copy the raw Elixir data content to the clipboard.
821
+ * Shows a "✓" feedback on the copy button for 2 seconds.
822
+ * Returns a promise that resolves when copying is complete.
823
+ */
824
+ async copyContent() {
825
+ try {
826
+ await navigator.clipboard.writeText(this.code);
827
+ } catch {
828
+ const t = document.createElement("textarea");
829
+ t.value = this.code, t.style.position = "fixed", t.style.opacity = "0", document.body.appendChild(t), t.select(), document.execCommand("copy"), document.body.removeChild(t);
830
+ }
831
+ this.showCopyFeedback();
832
+ }
833
+ /**
834
+ * Show a brief "copied" feedback on the copy button.
835
+ */
836
+ showCopyFeedback() {
837
+ if (!this.copyBtn) return;
838
+ this.copyResetTimer && clearTimeout(this.copyResetTimer);
839
+ const t = "⎘";
840
+ this.copyBtn.textContent = "✓", this.copyBtn.classList.add("edv-toolbar-btn--active"), this.copyBtn.title = "Copied!", this.copyResetTimer = setTimeout(() => {
841
+ this.copyBtn && (this.copyBtn.textContent = t, this.copyBtn.classList.remove("edv-toolbar-btn--active"), this.copyBtn.title = "Copy"), this.copyResetTimer = null;
842
+ }, 2e3);
843
+ }
844
+ // ─── Search API ───────────────────────────────────────────────────────
845
+ /**
846
+ * Open the search bar and focus the input.
847
+ */
848
+ openSearch() {
849
+ var t;
850
+ this.searchVisible = !0, (t = this.searchBarEl) == null || t.classList.add("edv-search-bar--visible"), this.searchBtn && this.searchBtn.classList.add("edv-toolbar-btn--active"), this.searchInputEl && (this.searchInputEl.focus(), this.searchInputEl.select());
851
+ }
852
+ /**
853
+ * Close the search bar and clear highlights.
854
+ */
855
+ closeSearch() {
856
+ var t;
857
+ this.searchVisible = !1, (t = this.searchBarEl) == null || t.classList.remove("edv-search-bar--visible"), this.searchBtn && this.searchBtn.classList.remove("edv-toolbar-btn--active"), this.searchState.clear(), this.updateSearchInfo(), this.render(), this.container.focus();
858
+ }
859
+ /**
860
+ * Toggle search bar visibility.
861
+ */
862
+ toggleSearch() {
863
+ this.searchVisible ? this.closeSearch() : this.openSearch();
864
+ }
865
+ /**
866
+ * Navigate to the next search match.
867
+ */
868
+ searchNext() {
869
+ const t = this.searchState.next();
870
+ t && this.revealAndScrollToMatch(t), this.updateSearchInfo(), this.render();
871
+ }
872
+ /**
873
+ * Navigate to the previous search match.
874
+ */
875
+ searchPrev() {
876
+ const t = this.searchState.prev();
877
+ t && this.revealAndScrollToMatch(t), this.updateSearchInfo(), this.render();
878
+ }
879
+ /**
880
+ * Get the search state (for programmatic access / testing).
881
+ */
882
+ getSearchState() {
883
+ return this.searchState;
884
+ }
885
+ /**
886
+ * Programmatically search for a keyword.
887
+ * Highlights all matches and scrolls to the first one, but does NOT
888
+ * open/show the search bar UI. The search input is always kept in sync.
889
+ *
890
+ * @param query - The keyword to search for. Pass an empty string to clear.
891
+ * @param options - Optional settings. `caseSensitive` defaults to the
892
+ * current case-sensitivity state.
893
+ */
894
+ search(t, e) {
895
+ var n;
896
+ const s = (e == null ? void 0 : e.caseSensitive) ?? this.searchState.isCaseSensitive();
897
+ this.searchState.search(this.lines, t, s), this.searchInputEl && (this.searchInputEl.value = t), (n = this.searchCaseBtn) == null || n.classList.toggle(
898
+ "edv-search-case-btn--active",
899
+ s
900
+ ), this.updateSearchInfo();
901
+ const i = this.searchState.getCurrentMatch();
902
+ i && this.revealAndScrollToMatch(i), this.render();
903
+ }
904
+ /**
905
+ * Clear the current search state and remove all highlights.
906
+ * The search input is cleared but the search bar visibility is unchanged.
907
+ */
908
+ clearSearch() {
909
+ this.searchState.clear(), this.searchInputEl && (this.searchInputEl.value = ""), this.updateSearchInfo(), this.render();
910
+ }
911
+ /**
912
+ * Handle input in the search field.
913
+ */
914
+ onSearchInput() {
915
+ var s;
916
+ const t = ((s = this.searchInputEl) == null ? void 0 : s.value) ?? "";
917
+ this.searchState.search(
918
+ this.lines,
919
+ t,
920
+ this.searchState.isCaseSensitive()
921
+ ), this.updateSearchInfo();
922
+ const e = this.searchState.getCurrentMatch();
923
+ e && this.revealAndScrollToMatch(e), this.render();
924
+ }
925
+ /**
926
+ * Toggle case sensitivity and re-search.
927
+ */
928
+ toggleCaseSensitive() {
929
+ var i, n;
930
+ const t = !this.searchState.isCaseSensitive();
931
+ (i = this.searchCaseBtn) == null || i.classList.toggle("edv-search-case-btn--active", t);
932
+ const e = ((n = this.searchInputEl) == null ? void 0 : n.value) ?? "";
933
+ this.searchState.search(this.lines, e, t), this.updateSearchInfo();
934
+ const s = this.searchState.getCurrentMatch();
935
+ s && this.revealAndScrollToMatch(s), this.render();
936
+ }
937
+ /**
938
+ * Update the search info label (e.g. "3 of 12" or "No results").
939
+ */
940
+ updateSearchInfo() {
941
+ if (!this.searchInfoEl) return;
942
+ const t = this.searchState.getMatchCount();
943
+ if (!this.searchState.getQuery())
944
+ this.searchInfoEl.textContent = "", this.searchInfoEl.classList.remove("edv-search-info--no-results");
945
+ else if (t === 0)
946
+ this.searchInfoEl.textContent = "No results", this.searchInfoEl.classList.add("edv-search-info--no-results");
947
+ else {
948
+ const s = this.searchState.getCurrentIndex() + 1;
949
+ this.searchInfoEl.textContent = `${s} of ${t}`, this.searchInfoEl.classList.remove("edv-search-info--no-results");
950
+ }
951
+ }
952
+ /**
953
+ * Ensure the line containing a match is visible (unfold if needed)
954
+ * and scroll to it.
955
+ */
956
+ revealAndScrollToMatch(t) {
957
+ this.foldState.revealLine(t.line);
958
+ }
959
+ /**
960
+ * After render, scroll to the current search match element.
961
+ */
962
+ scrollToCurrentMatch() {
963
+ const t = this.scrollEl.querySelector(".edv-search-current");
964
+ t && t.scrollIntoView({ block: "center", behavior: "smooth" });
965
+ }
966
+ // ─── Filter API ─────────────────────────────────────────────────────────
967
+ /**
968
+ * Open the filter bar and focus the input.
969
+ */
970
+ openFilter() {
971
+ var t;
972
+ this.filterVisible = !0, (t = this.filterBarEl) == null || t.classList.add("edv-filter-bar--visible"), this.filterBtn && this.filterBtn.classList.add("edv-toolbar-btn--active"), this.updateFilterTags(), this.updateFilterInfo(), this.filterInputEl && this.filterInputEl.focus();
973
+ }
974
+ /**
975
+ * Close the filter bar (filters remain active).
976
+ */
977
+ closeFilter() {
978
+ var t;
979
+ this.filterVisible = !1, (t = this.filterBarEl) == null || t.classList.remove("edv-filter-bar--visible"), this.hideFilterDropdown(), this.filterBtn && this.filterBtn.classList.toggle(
980
+ "edv-toolbar-btn--active",
981
+ this.filterState.isActive()
982
+ ), this.container.focus();
983
+ }
984
+ /**
985
+ * Toggle filter bar visibility.
986
+ */
987
+ toggleFilter() {
988
+ this.filterVisible ? this.closeFilter() : this.openFilter();
989
+ }
990
+ /**
991
+ * Set keys to filter out (replaces existing filters). Re-renders.
992
+ */
993
+ setFilterKeys(t) {
994
+ this.filterState.setKeys(t), this.updateFilterTags(), this.updateFilterInfo(), this.updateFilterBtnState(), this.render();
995
+ }
996
+ /**
997
+ * Add a single key to the filter. Re-renders.
998
+ */
999
+ addFilterKey(t) {
1000
+ this.filterState.addKey(t), this.updateFilterTags(), this.updateFilterInfo(), this.updateFilterBtnState(), this.render();
1001
+ }
1002
+ /**
1003
+ * Remove a single key from the filter. Re-renders.
1004
+ */
1005
+ removeFilterKey(t) {
1006
+ this.filterState.removeKey(t), this.updateFilterTags(), this.updateFilterInfo(), this.updateFilterBtnState(), this.render();
1007
+ }
1008
+ /**
1009
+ * Get currently filtered keys.
1010
+ */
1011
+ getFilterKeys() {
1012
+ return this.filterState.getKeys();
1013
+ }
1014
+ /**
1015
+ * Get all keys detected in the current content.
1016
+ */
1017
+ getAvailableKeys() {
1018
+ return this.filterState.getAvailableKeys();
1019
+ }
1020
+ /**
1021
+ * Clear all key filters. Re-renders.
1022
+ */
1023
+ clearFilter() {
1024
+ this.filterState.clear(), this.updateFilterTags(), this.updateFilterInfo(), this.updateFilterBtnState(), this.render();
1025
+ }
1026
+ /**
1027
+ * Get the filter state (for programmatic access / testing).
1028
+ */
1029
+ getFilterState() {
1030
+ return this.filterState;
1031
+ }
1032
+ /**
1033
+ * Get the content with filtered lines removed.
1034
+ * Returns lines that are not hidden by the current key filter.
1035
+ */
1036
+ getFilteredContent() {
1037
+ if (!this.filterState.isActive())
1038
+ return this.code;
1039
+ const t = [];
1040
+ for (let e = 0; e < this.lines.length; e++)
1041
+ this.filterState.isLineFiltered(e) || t.push(this.lines[e]);
1042
+ return t.join(`
1043
+ `);
1044
+ }
1045
+ /**
1046
+ * Copy the filtered content (lines with filtered keys removed) to clipboard.
1047
+ * Shows "✓" feedback on the filter copy button for 2 seconds.
1048
+ */
1049
+ async copyFilteredContent() {
1050
+ const t = this.getFilteredContent();
1051
+ try {
1052
+ await navigator.clipboard.writeText(t);
1053
+ } catch {
1054
+ const e = document.createElement("textarea");
1055
+ e.value = t, e.style.position = "fixed", e.style.opacity = "0", document.body.appendChild(e), e.select(), document.execCommand("copy"), document.body.removeChild(e);
1056
+ }
1057
+ this.showFilterCopyFeedback();
1058
+ }
1059
+ /**
1060
+ * Show a brief "copied" feedback on the filter copy button.
1061
+ */
1062
+ showFilterCopyFeedback() {
1063
+ this.filterCopyBtn && (this.filterCopyResetTimer && clearTimeout(this.filterCopyResetTimer), this.filterCopyBtn.textContent = "✓", this.filterCopyBtn.title = "Copied!", this.filterCopyResetTimer = setTimeout(() => {
1064
+ this.filterCopyBtn && (this.filterCopyBtn.textContent = "⎘", this.filterCopyBtn.title = "Copy Filtered Content"), this.filterCopyResetTimer = null;
1065
+ }, 2e3));
1066
+ }
1067
+ /**
1068
+ * Build the filter bar DOM (hidden by default).
1069
+ */
1070
+ buildFilterBar() {
1071
+ this.filterBarEl = document.createElement("div"), this.filterBarEl.classList.add("edv-filter-bar");
1072
+ const t = document.createElement("div");
1073
+ t.classList.add("edv-filter-input-wrapper"), this.filterInputEl = document.createElement("input"), this.filterInputEl.type = "text", this.filterInputEl.classList.add("edv-filter-input"), this.filterInputEl.placeholder = "Filter by key…", this.filterInputEl.addEventListener("input", () => this.onFilterInput()), this.filterInputEl.addEventListener(
1074
+ "keydown",
1075
+ (i) => this.handleFilterKeyDown(i)
1076
+ ), this.filterInputEl.addEventListener("focus", () => this.onFilterInput()), t.appendChild(this.filterInputEl), this.filterDropdownEl = document.createElement("div"), this.filterDropdownEl.classList.add("edv-filter-dropdown"), t.appendChild(this.filterDropdownEl), this.filterBarEl.appendChild(t), this.filterTagsEl = document.createElement("div"), this.filterTagsEl.classList.add("edv-filter-tags"), this.filterBarEl.appendChild(this.filterTagsEl), this.filterInfoEl = document.createElement("span"), this.filterInfoEl.classList.add("edv-filter-info"), this.filterBarEl.appendChild(this.filterInfoEl), this.filterCopyBtn = document.createElement("button"), this.filterCopyBtn.classList.add("edv-search-nav-btn"), this.filterCopyBtn.textContent = "⎘", this.filterCopyBtn.title = "Copy Filtered Content", this.filterCopyBtn.addEventListener("click", (i) => {
1077
+ i.stopPropagation(), this.copyFilteredContent();
1078
+ }), this.filterBarEl.appendChild(this.filterCopyBtn);
1079
+ const e = document.createElement("button");
1080
+ e.classList.add("edv-search-nav-btn"), e.textContent = "⌫", e.title = "Clear All Filters", e.addEventListener("click", (i) => {
1081
+ i.stopPropagation(), this.clearFilter();
1082
+ }), this.filterBarEl.appendChild(e);
1083
+ const s = document.createElement("button");
1084
+ s.classList.add("edv-search-nav-btn"), s.textContent = "✕", s.title = "Close", s.addEventListener("click", (i) => {
1085
+ i.stopPropagation(), this.closeFilter();
1086
+ }), this.filterBarEl.appendChild(s), document.addEventListener("click", (i) => {
1087
+ var n;
1088
+ (n = this.filterBarEl) != null && n.contains(i.target) || this.hideFilterDropdown();
1089
+ }), this.container.appendChild(this.filterBarEl);
1090
+ }
1091
+ /**
1092
+ * Handle input in the filter field — show autocomplete dropdown.
1093
+ */
1094
+ onFilterInput() {
1095
+ var n;
1096
+ const t = ((n = this.filterInputEl) == null ? void 0 : n.value.trim().toLowerCase()) ?? "", e = this.filterState.getAvailableKeys(), s = this.filterState.getKeys(), i = e.filter(
1097
+ (l) => !s.includes(l) && (t === "" || l.toLowerCase().includes(t))
1098
+ );
1099
+ this.showFilterDropdown(i);
1100
+ }
1101
+ /**
1102
+ * Handle keyboard events in the filter input.
1103
+ */
1104
+ handleFilterKeyDown(t) {
1105
+ var i, n, l;
1106
+ const e = this.filterDropdownItems.length, s = (i = this.filterDropdownEl) == null ? void 0 : i.classList.contains("edv-filter-dropdown--visible");
1107
+ if (t.key === "ArrowDown" && s && e > 0) {
1108
+ t.preventDefault(), this.filterDropdownIndex = Math.min(this.filterDropdownIndex + 1, e - 1), this.updateFilterDropdownHighlight();
1109
+ return;
1110
+ }
1111
+ if (t.key === "ArrowUp" && s && e > 0) {
1112
+ t.preventDefault(), this.filterDropdownIndex = Math.max(this.filterDropdownIndex - 1, 0), this.updateFilterDropdownHighlight();
1113
+ return;
1114
+ }
1115
+ if (t.key === "Enter") {
1116
+ if (t.preventDefault(), s && this.filterDropdownIndex >= 0 && this.filterDropdownIndex < e) {
1117
+ const c = this.filterDropdownItems[this.filterDropdownIndex];
1118
+ c && !this.filterState.hasKey(c) && (this.addFilterKey(c), this.filterInputEl && (this.filterInputEl.value = ""), this.hideFilterDropdown());
1119
+ return;
1120
+ }
1121
+ const o = ((n = this.filterInputEl) == null ? void 0 : n.value.trim()) ?? "";
1122
+ if (o) {
1123
+ const d = this.filterState.getAvailableKeys().find(
1124
+ (m) => m.toLowerCase() === o.toLowerCase()
1125
+ );
1126
+ d && !this.filterState.hasKey(d) && (this.addFilterKey(d), this.filterInputEl && (this.filterInputEl.value = ""), this.hideFilterDropdown());
1127
+ }
1128
+ }
1129
+ t.key === "Escape" && (t.preventDefault(), this.hideFilterDropdown(), (l = this.filterInputEl) != null && l.value ? this.filterInputEl && (this.filterInputEl.value = "") : this.closeFilter());
1130
+ }
1131
+ /**
1132
+ * Update the visual highlight on the dropdown items.
1133
+ */
1134
+ updateFilterDropdownHighlight() {
1135
+ if (!this.filterDropdownEl) return;
1136
+ const t = this.filterDropdownEl.querySelectorAll(".edv-filter-dropdown-item");
1137
+ t.forEach((s, i) => {
1138
+ s.classList.toggle("edv-filter-dropdown-item--active", i === this.filterDropdownIndex);
1139
+ });
1140
+ const e = t[this.filterDropdownIndex];
1141
+ e && e.scrollIntoView({ block: "nearest" });
1142
+ }
1143
+ /**
1144
+ * Show the autocomplete dropdown with suggestions.
1145
+ */
1146
+ showFilterDropdown(t) {
1147
+ if (this.filterDropdownEl) {
1148
+ if (this.filterDropdownEl.innerHTML = "", this.filterDropdownItems = t, this.filterDropdownIndex = -1, t.length === 0) {
1149
+ this.filterDropdownEl.classList.remove("edv-filter-dropdown--visible");
1150
+ return;
1151
+ }
1152
+ for (const e of t) {
1153
+ const s = document.createElement("div");
1154
+ s.classList.add("edv-filter-dropdown-item"), s.textContent = e, s.addEventListener("mousedown", (i) => {
1155
+ i.preventDefault(), i.stopPropagation(), this.addFilterKey(e), this.filterInputEl && (this.filterInputEl.value = "", this.filterInputEl.focus()), this.hideFilterDropdown();
1156
+ }), this.filterDropdownEl.appendChild(s);
1157
+ }
1158
+ this.filterDropdownEl.classList.add("edv-filter-dropdown--visible");
1159
+ }
1160
+ }
1161
+ /**
1162
+ * Hide the autocomplete dropdown.
1163
+ */
1164
+ hideFilterDropdown() {
1165
+ var t;
1166
+ (t = this.filterDropdownEl) == null || t.classList.remove("edv-filter-dropdown--visible");
1167
+ }
1168
+ /**
1169
+ * Rebuild the filter tag chips in the filter bar.
1170
+ */
1171
+ updateFilterTags() {
1172
+ if (!this.filterTagsEl) return;
1173
+ this.filterTagsEl.innerHTML = "";
1174
+ const t = this.filterState.getKeys();
1175
+ for (const e of t) {
1176
+ const s = document.createElement("span");
1177
+ s.classList.add("edv-filter-tag");
1178
+ const i = document.createElement("span");
1179
+ i.classList.add("edv-filter-tag-label"), i.textContent = e, s.appendChild(i);
1180
+ const n = document.createElement("button");
1181
+ n.classList.add("edv-filter-tag-remove"), n.textContent = "✕", n.title = `Remove filter: ${e}`, n.addEventListener("click", (l) => {
1182
+ l.stopPropagation(), this.removeFilterKey(e);
1183
+ }), s.appendChild(n), this.filterTagsEl.appendChild(s);
1184
+ }
1185
+ }
1186
+ /**
1187
+ * Update the filter info label.
1188
+ */
1189
+ updateFilterInfo() {
1190
+ if (!this.filterInfoEl) return;
1191
+ const t = this.filterState.getFilteredCount();
1192
+ t === 0 ? this.filterInfoEl.textContent = "" : this.filterInfoEl.textContent = `${t} key${t > 1 ? "s" : ""} hidden`;
1193
+ }
1194
+ /**
1195
+ * Update filter toolbar button active state.
1196
+ */
1197
+ updateFilterBtnState() {
1198
+ this.filterBtn && this.filterBtn.classList.toggle(
1199
+ "edv-toolbar-btn--active",
1200
+ this.filterState.isActive()
1201
+ );
1202
+ }
1203
+ // ─── Content ──────────────────────────────────────────────────────────
1204
+ /**
1205
+ * Set the Elixir data content and render it.
1206
+ */
1207
+ setContent(t) {
1208
+ this.code = t, this.lines = t.split(`
1209
+ `), this.buildLineOffsets();
1210
+ const { modifiedCode: e, inspectLiterals: s } = it(t);
1211
+ this.inspectLiterals = s;
1212
+ const i = O(e);
1213
+ this.tree = i, this.tokens = P(e, i), this.fixInspectLiteralTokenClasses();
1214
+ const n = _(e, i), l = U(n);
1215
+ this.foldState.setRegions(n, l), this.filterState.detectKeys(i, e), this.defaultFilterKeys.length > 0 && !this.filterState.isActive() && (this.filterState.setKeys(this.defaultFilterKeys), this.updateFilterTags(), this.updateFilterInfo(), this.updateFilterBtnState()), this.defaultFoldLevel > 0 && this.foldState.foldToLevel(this.defaultFoldLevel), this.searchState.isActive() && (this.searchState.search(
1216
+ this.lines,
1217
+ this.searchState.getQuery(),
1218
+ this.searchState.isCaseSensitive()
1219
+ ), this.updateSearchInfo()), this.render();
1220
+ }
1221
+ /**
1222
+ * Set a callback to be called after each render (for testing/animation).
1223
+ */
1224
+ onRender(t) {
1225
+ this.onRenderCallback = t;
1226
+ }
1227
+ /**
1228
+ * Post-process highlight tokens: replace CSS classes for spans that fall
1229
+ * within pre-processed inspect literal ranges (e.g. #Reference<...>)
1230
+ * so they render with the inspect-literal style instead of atom style.
1231
+ */
1232
+ fixInspectLiteralTokenClasses() {
1233
+ if (this.inspectLiterals.length !== 0)
1234
+ for (const t of this.inspectLiterals)
1235
+ for (const e of this.tokens)
1236
+ e.from >= t.from && e.to <= t.to && (e.classes = "tok-inspect-literal");
1237
+ }
1238
+ /**
1239
+ * Check if an offset falls within a pre-processed inspect literal and
1240
+ * return the literal if so, or null.
1241
+ */
1242
+ findInspectLiteral(t, e) {
1243
+ for (const s of this.inspectLiterals)
1244
+ if (t >= s.from && e <= s.to)
1245
+ return s;
1246
+ return null;
1247
+ }
1248
+ /**
1249
+ * Build line offset table for mapping offsets to line-relative positions.
1250
+ */
1251
+ buildLineOffsets() {
1252
+ this.lineOffsets = [0];
1253
+ for (let t = 0; t < this.code.length; t++)
1254
+ this.code[t] === `
1255
+ ` && this.lineOffsets.push(t + 1);
1256
+ }
1257
+ /**
1258
+ * Full re-render of the viewer.
1259
+ */
1260
+ render() {
1261
+ var i;
1262
+ this.scrollEl.innerHTML = "";
1263
+ const t = this.lines.length, e = String(t).length;
1264
+ let s = 0;
1265
+ for (; s < t; ) {
1266
+ if (this.foldState.isLineHidden(s)) {
1267
+ s++;
1268
+ continue;
1269
+ }
1270
+ if (this.filterState.isLineFiltered(s)) {
1271
+ s++;
1272
+ continue;
1273
+ }
1274
+ const l = this.foldState.isFoldable(s), o = this.foldState.isFolded(s), c = this.foldState.getRegion(s), d = this.createLineElement(
1275
+ s,
1276
+ e,
1277
+ l,
1278
+ o,
1279
+ c
1280
+ );
1281
+ this.scrollEl.appendChild(d), s++;
1282
+ }
1283
+ (i = this.onRenderCallback) == null || i.call(this), this.searchState.isActive() && this.searchState.getCurrentMatch() && requestAnimationFrame(() => this.scrollToCurrentMatch());
1284
+ }
1285
+ /**
1286
+ * Create a single line element with gutter and code content.
1287
+ */
1288
+ createLineElement(t, e, s, i, n) {
1289
+ const l = document.createElement("div");
1290
+ l.classList.add("edv-line"), l.dataset.line = String(t), this.searchState.getLineMatches(t).length > 0 && l.classList.add("edv-line--has-match");
1291
+ const c = document.createElement("div");
1292
+ c.classList.add("edv-gutter");
1293
+ const d = document.createElement("span");
1294
+ d.classList.add("edv-line-number"), d.textContent = String(t + 1).padStart(e, " "), c.appendChild(d);
1295
+ const m = document.createElement("span");
1296
+ m.classList.add("edv-fold-indicator"), s && (m.classList.add("edv-foldable"), m.textContent = i ? "▶" : "▼", m.addEventListener("click", (h) => {
1297
+ h.stopPropagation(), this.foldState.toggle(t), this.render();
1298
+ })), c.appendChild(m), l.appendChild(c);
1299
+ const f = document.createElement("div");
1300
+ return f.classList.add("edv-code"), i && n ? this.renderFoldedLine(f, t, n) : this.renderHighlightedLine(f, t), l.appendChild(f), l;
1301
+ }
1302
+ /**
1303
+ * Render a normal highlighted line, with search match highlighting.
1304
+ */
1305
+ renderHighlightedLine(t, e) {
1306
+ const s = this.lines[e], i = this.lineOffsets[e], n = i + s.length, l = C(this.tokens, i, n), o = this.searchState.getLineMatches(e);
1307
+ if (s.length === 0) {
1308
+ t.innerHTML = "&nbsp;";
1309
+ return;
1310
+ }
1311
+ if (o.length === 0) {
1312
+ this.renderTokenizedText(t, s, l, i);
1313
+ return;
1314
+ }
1315
+ this.renderWithSearchHighlights(t, s, l, o, e);
1316
+ }
1317
+ /**
1318
+ * Render tokenized text without search highlights.
1319
+ */
1320
+ renderTokenizedText(t, e, s, i) {
1321
+ const n = i ?? 0;
1322
+ if (s.length === 0) {
1323
+ const o = document.createElement("span");
1324
+ o.dataset.from = String(n), o.dataset.to = String(n + e.length), o.textContent = e, t.appendChild(o);
1325
+ return;
1326
+ }
1327
+ let l = 0;
1328
+ for (const o of s) {
1329
+ if (o.from > l) {
1330
+ const d = document.createElement("span");
1331
+ d.dataset.from = String(n + l), d.dataset.to = String(n + o.from), d.textContent = e.slice(l, o.from), t.appendChild(d);
1332
+ }
1333
+ const c = document.createElement("span");
1334
+ c.className = o.classes, c.dataset.from = String(n + o.from), c.dataset.to = String(n + o.to), c.textContent = e.slice(o.from, o.to), t.appendChild(c), l = o.to;
1335
+ }
1336
+ if (l < e.length) {
1337
+ const o = document.createElement("span");
1338
+ o.dataset.from = String(n + l), o.dataset.to = String(n + e.length), o.textContent = e.slice(l), t.appendChild(o);
1339
+ }
1340
+ }
1341
+ /**
1342
+ * Render a line with both syntax tokens and search highlight overlays.
1343
+ * Search highlights take precedence visually (wrapping around tokens).
1344
+ */
1345
+ renderWithSearchHighlights(t, e, s, i, n) {
1346
+ const l = new Array(e.length).fill(null);
1347
+ for (const f of s)
1348
+ for (let h = f.from; h < f.to && h < e.length; h++)
1349
+ l[h] = f.classes;
1350
+ const o = [], c = new Array(e.length).fill(null);
1351
+ for (const f of i)
1352
+ for (let h = f.from; h < f.to && h < e.length; h++)
1353
+ c[h] = f;
1354
+ let d = 0;
1355
+ for (; d < e.length; ) {
1356
+ const f = l[d], h = c[d], g = h !== null, v = h ? this.searchState.isCurrentMatch(n, h.from) : !1;
1357
+ let u = d + 1;
1358
+ for (; u < e.length; ) {
1359
+ const y = l[u], E = c[u], F = E !== null, A = E ? this.searchState.isCurrentMatch(n, E.from) : !1;
1360
+ if (y !== f || F !== g || A !== v) break;
1361
+ u++;
1362
+ }
1363
+ o.push({ from: d, to: u, tokenClass: f, isMatch: g, isCurrent: v }), d = u;
1364
+ }
1365
+ const m = this.lineOffsets[n];
1366
+ for (const f of o) {
1367
+ const h = e.slice(f.from, f.to), g = m + f.from, v = m + f.to;
1368
+ if (f.isMatch) {
1369
+ const u = document.createElement("mark");
1370
+ if (u.classList.add("edv-search-match"), f.isCurrent && u.classList.add("edv-search-current"), u.dataset.from = String(g), u.dataset.to = String(v), f.tokenClass) {
1371
+ const y = document.createElement("span");
1372
+ y.className = f.tokenClass, y.textContent = h, u.appendChild(y);
1373
+ } else
1374
+ u.textContent = h;
1375
+ t.appendChild(u);
1376
+ } else if (f.tokenClass) {
1377
+ const u = document.createElement("span");
1378
+ u.className = f.tokenClass, u.dataset.from = String(g), u.dataset.to = String(v), u.textContent = h, t.appendChild(u);
1379
+ } else {
1380
+ const u = document.createElement("span");
1381
+ u.dataset.from = String(g), u.dataset.to = String(v), u.textContent = h, t.appendChild(u);
1382
+ }
1383
+ }
1384
+ }
1385
+ /**
1386
+ * Render a folded line: shows the opening line content followed by … and closing bracket.
1387
+ */
1388
+ renderFoldedLine(t, e, s) {
1389
+ const i = this.lines[e], n = this.lineOffsets[e], l = n + i.length, o = C(this.tokens, n, l);
1390
+ let c = 0;
1391
+ for (const h of o) {
1392
+ if (h.from > c) {
1393
+ const v = document.createElement("span");
1394
+ v.dataset.from = String(n + c), v.dataset.to = String(n + h.from), v.textContent = i.slice(c, h.from), t.appendChild(v);
1395
+ }
1396
+ const g = document.createElement("span");
1397
+ g.className = h.classes, g.dataset.from = String(n + h.from), g.dataset.to = String(n + h.to), g.textContent = i.slice(h.from, h.to), t.appendChild(g), c = h.to;
1398
+ }
1399
+ if (c < i.length) {
1400
+ const h = document.createElement("span");
1401
+ h.dataset.from = String(n + c), h.dataset.to = String(n + i.length), h.textContent = i.slice(c), t.appendChild(h);
1402
+ }
1403
+ const d = document.createElement("span");
1404
+ d.classList.add("edv-fold-ellipsis");
1405
+ const m = s.endLine - s.startLine;
1406
+ if (s.itemCount > 0)
1407
+ d.textContent = `${s.itemCount} items`, d.title = `${s.itemCount} items, ${m} lines folded`;
1408
+ else {
1409
+ const g = s.openText === '"""' || s.openText === "'''" ? m - 1 : m;
1410
+ d.textContent = `${g} lines`, d.title = `${g} lines folded`;
1411
+ }
1412
+ d.dataset.from = String(s.startOffset), d.dataset.to = String(s.endOffset), d.addEventListener("click", (h) => {
1413
+ h.stopPropagation(), this.foldState.toggle(e), this.render();
1414
+ }), t.appendChild(d);
1415
+ const f = document.createElement("span");
1416
+ f.classList.add("tok-punctuation"), f.dataset.from = String(s.endOffset - s.closeText.length), f.dataset.to = String(s.endOffset), f.textContent = s.closeText, t.appendChild(f);
1417
+ }
1418
+ // ─── Inspect (Hover + Click-to-Copy) ──────────────────────────────────
1419
+ /**
1420
+ * Handle mouseover on spans to resolve inspect target and apply highlight.
1421
+ */
1422
+ handleInspectHover(t) {
1423
+ const e = t.target;
1424
+ if (!e || !this.tree) return;
1425
+ const s = e.closest("[data-from]");
1426
+ if (!s) {
1427
+ this.clearInspectHighlight();
1428
+ return;
1429
+ }
1430
+ const i = parseInt(s.dataset.from, 10);
1431
+ if (isNaN(i)) return;
1432
+ const n = J(this.tree, this.code, i);
1433
+ if (!n) {
1434
+ this.clearInspectHighlight();
1435
+ return;
1436
+ }
1437
+ this.findInspectLiteral(n.from, n.to) && (n.type = "InspectLiteral"), !(this.currentInspect && this.currentInspect.from === n.from && this.currentInspect.to === n.to) && (this.clearInspectHighlight(), this.currentInspect = n, this.applyInspectHighlight(n));
1438
+ }
1439
+ /**
1440
+ * Handle mouseout — clear highlight when leaving the scroll area.
1441
+ */
1442
+ handleInspectOut(t) {
1443
+ const s = t.relatedTarget;
1444
+ s && this.scrollEl.contains(s) || this.clearInspectHighlight();
1445
+ }
1446
+ /**
1447
+ * Register a callback invoked when the user clicks an inspectable value.
1448
+ * The callback receives an InspectEvent with type, copyText, DOM element,
1449
+ * and a preventDefault() method to suppress the default copy behavior.
1450
+ *
1451
+ * Pass `null` to unregister the callback and restore default behavior.
1452
+ */
1453
+ onInspect(t) {
1454
+ this.inspectCallback = t;
1455
+ }
1456
+ /**
1457
+ * Handle click on an inspected token — copy to clipboard (unless prevented by callback).
1458
+ */
1459
+ handleInspectClick(t) {
1460
+ if (!this.currentInspect) return;
1461
+ const e = t.target;
1462
+ if (!e) return;
1463
+ const s = e.closest(".edv-fold-indicator, .edv-fold-ellipsis");
1464
+ if (s && !s.dataset.from) return;
1465
+ const i = e.closest("[data-from]");
1466
+ if (!i) return;
1467
+ const n = this.currentInspect.copyText;
1468
+ let l = !1;
1469
+ if (this.inspectCallback) {
1470
+ const o = {
1471
+ type: this.currentInspect.type,
1472
+ copyText: n,
1473
+ target: this.currentInspect,
1474
+ element: i,
1475
+ mouseEvent: t,
1476
+ preventDefault() {
1477
+ l = !0;
1478
+ }
1479
+ };
1480
+ this.inspectCallback(o);
1481
+ }
1482
+ this.flashInspectHighlight(), l || (this.copyToClipboard(n), this.showInspectToast(t));
1483
+ }
1484
+ /**
1485
+ * Apply highlight CSS classes to all spans within the inspect target range.
1486
+ */
1487
+ applyInspectHighlight(t) {
1488
+ if (t.isStructure) {
1489
+ const e = this.offsetToLine(t.from), s = this.offsetToLine(t.to - 1), i = this.scrollEl.querySelectorAll(".edv-line");
1490
+ for (const n of i) {
1491
+ const l = parseInt(n.dataset.line, 10);
1492
+ isNaN(l) || l >= e && l <= s && n.classList.add("edv-inspect-line");
1493
+ }
1494
+ this.highlightSpansInRange(t.from, t.to, "edv-inspect-bracket");
1495
+ } else
1496
+ this.highlightSpansInRange(t.from, t.to, "edv-inspect-token");
1497
+ }
1498
+ /**
1499
+ * Add a CSS class to all spans whose data-from/data-to overlap the given range.
1500
+ */
1501
+ highlightSpansInRange(t, e, s) {
1502
+ const i = this.scrollEl.querySelectorAll("[data-from]");
1503
+ for (const n of i) {
1504
+ const l = parseInt(n.dataset.from, 10), o = parseInt(n.dataset.to, 10);
1505
+ isNaN(l) || isNaN(o) || l < e && o > t && n.classList.add(s);
1506
+ }
1507
+ }
1508
+ /**
1509
+ * Clear all inspect highlight classes.
1510
+ */
1511
+ clearInspectHighlight() {
1512
+ if (!this.currentInspect) return;
1513
+ const t = this.scrollEl.querySelectorAll(".edv-inspect-line");
1514
+ for (const s of t)
1515
+ s.classList.remove("edv-inspect-line");
1516
+ const e = this.scrollEl.querySelectorAll(
1517
+ ".edv-inspect-token, .edv-inspect-bracket"
1518
+ );
1519
+ for (const s of e)
1520
+ s.classList.remove("edv-inspect-token", "edv-inspect-bracket");
1521
+ this.currentInspect = null;
1522
+ }
1523
+ /**
1524
+ * Flash animation on currently highlighted elements.
1525
+ */
1526
+ flashInspectHighlight() {
1527
+ const t = this.scrollEl.querySelectorAll(
1528
+ ".edv-inspect-token, .edv-inspect-bracket"
1529
+ );
1530
+ for (const e of t)
1531
+ e.classList.add("edv-inspect-copied"), e.addEventListener(
1532
+ "animationend",
1533
+ () => e.classList.remove("edv-inspect-copied"),
1534
+ { once: !0 }
1535
+ );
1536
+ }
1537
+ /**
1538
+ * Show a small floating "Copied!" toast near the mouse click position.
1539
+ */
1540
+ showInspectToast(t) {
1541
+ const e = document.createElement("div");
1542
+ e.classList.add("edv-copied-toast"), e.textContent = "Copied!", e.style.left = `${t.clientX + 8}px`, e.style.top = `${t.clientY - 24}px`, document.body.appendChild(e), e.addEventListener("animationend", () => {
1543
+ e.remove();
1544
+ });
1545
+ }
1546
+ /**
1547
+ * Copy text to clipboard with fallback.
1548
+ */
1549
+ async copyToClipboard(t) {
1550
+ try {
1551
+ await navigator.clipboard.writeText(t);
1552
+ } catch {
1553
+ const e = document.createElement("textarea");
1554
+ e.value = t, e.style.position = "fixed", e.style.opacity = "0", document.body.appendChild(e), e.select(), document.execCommand("copy"), document.body.removeChild(e);
1555
+ }
1556
+ }
1557
+ /**
1558
+ * Convert an absolute character offset to a 0-indexed line number.
1559
+ */
1560
+ offsetToLine(t) {
1561
+ let e = 0, s = this.lineOffsets.length - 1;
1562
+ for (; e < s; ) {
1563
+ const i = e + s + 1 >> 1;
1564
+ this.lineOffsets[i] <= t ? e = i : s = i - 1;
1565
+ }
1566
+ return e;
1567
+ }
1568
+ }
1569
+ export {
1570
+ at as ElixirDataViewer,
1571
+ Y as FilterState,
1572
+ z as FoldState,
1573
+ Q as SearchState,
1574
+ U as buildFoldMap,
1575
+ _ as detectFoldRegions,
1576
+ C as getLineTokens,
1577
+ P as highlight,
1578
+ O as parseElixir,
1579
+ it as preprocessInspectLiterals,
1580
+ J as resolveInspectTarget
1581
+ };