@leftium/gg 0.0.31 → 0.0.34
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/README.md +117 -36
- package/dist/GgConsole.svelte +12 -0
- package/dist/GgConsole.svelte.d.ts +4 -0
- package/dist/OpenInEditorLink.svelte +8 -10
- package/dist/OpenInEditorLink.svelte.d.ts +6 -3
- package/dist/debug/browser.d.ts +10 -0
- package/dist/debug/browser.js +102 -0
- package/dist/debug/common.d.ts +41 -0
- package/dist/debug/common.js +191 -0
- package/dist/debug/index.d.ts +9 -0
- package/dist/debug/index.js +11 -0
- package/dist/debug/node.d.ts +10 -0
- package/dist/debug/node.js +137 -0
- package/dist/eruda/loader.js +0 -11
- package/dist/eruda/plugin.js +765 -25
- package/dist/eruda/types.d.ts +8 -5
- package/dist/gg-call-sites-plugin.d.ts +90 -6
- package/dist/gg-call-sites-plugin.js +700 -112
- package/dist/gg.d.ts +18 -2
- package/dist/gg.js +127 -110
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/open-in-editor.js +15 -2
- package/dist/vite.d.ts +37 -0
- package/dist/vite.js +46 -0
- package/package.json +20 -13
- package/dist/debug-bundled.d.ts +0 -2
- package/dist/debug-bundled.js +0 -3
- package/dist/debug.d.ts +0 -2
- package/dist/debug.js +0 -15
- package/patches/debug@4.4.3.patch +0 -35
package/dist/eruda/plugin.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { DEV } from 'esm-env';
|
|
1
2
|
import { LogBuffer } from './buffer.js';
|
|
3
|
+
const _ggCallSitesPlugin = typeof __GG_TAG_PLUGIN__ !== 'undefined' ? __GG_TAG_PLUGIN__ : false;
|
|
2
4
|
/**
|
|
3
5
|
* Creates the gg Eruda plugin
|
|
4
6
|
*
|
|
@@ -17,34 +19,142 @@ export function createGgPlugin(options, gg) {
|
|
|
17
19
|
let filterExpanded = false;
|
|
18
20
|
let filterPattern = '';
|
|
19
21
|
const enabledNamespaces = new Set();
|
|
22
|
+
// Last rendered entries (for hover tooltip arg lookup)
|
|
23
|
+
let renderedEntries = [];
|
|
24
|
+
// Settings UI state
|
|
25
|
+
let settingsExpanded = false;
|
|
26
|
+
// Filter pattern persistence key (independent of localStorage.debug)
|
|
27
|
+
const FILTER_KEY = 'gg-filter';
|
|
28
|
+
// Namespace click action: 'open' uses Vite dev middleware, 'copy' copies formatted string, 'open-url' navigates to URI
|
|
29
|
+
const NS_ACTION_KEY = 'gg-ns-action';
|
|
30
|
+
const EDITOR_BIN_KEY = 'gg-editor-bin';
|
|
31
|
+
const COPY_FORMAT_KEY = 'gg-copy-format';
|
|
32
|
+
const URL_FORMAT_KEY = 'gg-url-format';
|
|
33
|
+
const PROJECT_ROOT_KEY = 'gg-project-root';
|
|
34
|
+
// Plugin detection state (probed once at init)
|
|
35
|
+
let openInEditorPluginDetected = null; // null = not yet probed
|
|
36
|
+
// Editor bins for launch-editor (common first, then alphabetical)
|
|
37
|
+
const editorBins = [
|
|
38
|
+
{ label: 'Auto-detect', value: '' },
|
|
39
|
+
{ label: 'VS Code', value: 'code' },
|
|
40
|
+
{ label: 'Cursor', value: 'cursor' },
|
|
41
|
+
{ label: 'Zed', value: 'zed' },
|
|
42
|
+
{ label: 'Sublime Text', value: 'sublime' },
|
|
43
|
+
{ label: 'Vim', value: 'vim' },
|
|
44
|
+
{ label: 'Emacs', value: 'emacs' },
|
|
45
|
+
{ label: 'WebStorm', value: 'webstorm' },
|
|
46
|
+
{ label: 'IDEA', value: 'idea' },
|
|
47
|
+
{ label: 'Atom', value: 'atom' },
|
|
48
|
+
{ label: 'AppCode', value: 'appcode' },
|
|
49
|
+
{ label: 'Brackets', value: 'brackets' },
|
|
50
|
+
{ label: 'CLion', value: 'clion' },
|
|
51
|
+
{ label: 'Code Insiders', value: 'code-insiders' },
|
|
52
|
+
{ label: 'Notepad++', value: 'notepad++' },
|
|
53
|
+
{ label: 'PhpStorm', value: 'phpstorm' },
|
|
54
|
+
{ label: 'PyCharm', value: 'pycharm' },
|
|
55
|
+
{ label: 'Rider', value: 'rider' },
|
|
56
|
+
{ label: 'RubyMine', value: 'rubymine' },
|
|
57
|
+
{ label: 'VSCodium', value: 'codium' },
|
|
58
|
+
{ label: 'Visual Studio', value: 'visualstudio' }
|
|
59
|
+
];
|
|
60
|
+
// Terminal command presets
|
|
61
|
+
const copyPresets = {
|
|
62
|
+
'Raw path': '$FILE:$LINE:$COL',
|
|
63
|
+
'VS Code': 'code -g $FILE:$LINE:$COL',
|
|
64
|
+
Cursor: 'cursor -g $FILE:$LINE:$COL',
|
|
65
|
+
Zed: 'zed $FILE:$LINE:$COL',
|
|
66
|
+
Vim: 'vim +$LINE $FILE',
|
|
67
|
+
Emacs: 'emacs +$LINE:$COL $FILE',
|
|
68
|
+
JetBrains: 'idea --line $LINE --column $COL $FILE'
|
|
69
|
+
};
|
|
70
|
+
// URI scheme presets (use $ROOT for absolute paths)
|
|
71
|
+
const uriPresets = {
|
|
72
|
+
'VS Code': 'vscode://file/$ROOT/$FILE:$LINE:$COL',
|
|
73
|
+
'VS Code Insiders': 'vscode-insiders://file/$ROOT/$FILE:$LINE:$COL',
|
|
74
|
+
Cursor: 'cursor://file/$ROOT/$FILE:$LINE:$COL',
|
|
75
|
+
Windsurf: 'windsurf://file/$ROOT/$FILE:$LINE:$COL',
|
|
76
|
+
VSCodium: 'vscodium://file/$ROOT/$FILE:$LINE:$COL',
|
|
77
|
+
Zed: 'zed://file/$ROOT/$FILE:$LINE:$COL',
|
|
78
|
+
JetBrains: 'jetbrains://open?file=$ROOT/$FILE&line=$LINE&column=$COL',
|
|
79
|
+
'Sublime Text': 'subl://open?url=file://$ROOT/$FILE&line=$LINE&column=$COL',
|
|
80
|
+
Emacs: 'org-protocol://open-source?url=file://$ROOT/$FILE&line=$LINE&col=$COL',
|
|
81
|
+
Atom: 'atom://open?url=file://$ROOT/$FILE&line=$LINE&column=$COL'
|
|
82
|
+
};
|
|
83
|
+
let nsClickAction = localStorage.getItem(NS_ACTION_KEY) || (DEV ? 'open' : 'copy');
|
|
84
|
+
let editorBin = localStorage.getItem(EDITOR_BIN_KEY) || '';
|
|
85
|
+
let copyFormat = localStorage.getItem(COPY_FORMAT_KEY) || copyPresets['Raw path'];
|
|
86
|
+
let urlFormat = localStorage.getItem(URL_FORMAT_KEY) || uriPresets['VS Code'];
|
|
87
|
+
let projectRoot = localStorage.getItem(PROJECT_ROOT_KEY) || '';
|
|
88
|
+
/** Get the active format string for the current action mode */
|
|
89
|
+
function activeFormat() {
|
|
90
|
+
return nsClickAction === 'open-url' ? urlFormat : copyFormat;
|
|
91
|
+
}
|
|
92
|
+
/** Set the active format string and persist it */
|
|
93
|
+
function setActiveFormat(value) {
|
|
94
|
+
if (nsClickAction === 'open-url') {
|
|
95
|
+
urlFormat = value;
|
|
96
|
+
localStorage.setItem(URL_FORMAT_KEY, urlFormat);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
copyFormat = value;
|
|
100
|
+
localStorage.setItem(COPY_FORMAT_KEY, copyFormat);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
20
103
|
const plugin = {
|
|
21
104
|
name: 'GG',
|
|
22
105
|
init($container) {
|
|
23
106
|
$el = $container;
|
|
107
|
+
// Load filter state BEFORE registering _onLog hook, because setting _onLog
|
|
108
|
+
// triggers replay of earlyLogBuffer and each entry checks filterPattern
|
|
109
|
+
filterPattern = localStorage.getItem(FILTER_KEY) || 'gg:*';
|
|
24
110
|
// Register the capture hook on gg
|
|
25
111
|
if (gg) {
|
|
26
112
|
gg._onLog = (entry) => {
|
|
113
|
+
// Track namespaces for filter UI update (check BEFORE pushing to buffer)
|
|
114
|
+
const hadNamespace = getAllCapturedNamespaces().includes(entry.namespace);
|
|
27
115
|
buffer.push(entry);
|
|
28
116
|
// Add new namespace to enabledNamespaces if it matches the current pattern
|
|
29
117
|
const effectivePattern = filterPattern || 'gg:*';
|
|
30
118
|
if (namespaceMatchesPattern(entry.namespace, effectivePattern)) {
|
|
31
119
|
enabledNamespaces.add(entry.namespace);
|
|
32
120
|
}
|
|
33
|
-
// Update filter UI if
|
|
34
|
-
if (
|
|
121
|
+
// Update filter UI if new namespace appeared (updates button summary count)
|
|
122
|
+
if (!hadNamespace) {
|
|
35
123
|
renderFilterUI();
|
|
36
124
|
}
|
|
37
125
|
renderLogs();
|
|
38
126
|
};
|
|
39
127
|
}
|
|
40
|
-
//
|
|
41
|
-
|
|
128
|
+
// Probe for openInEditorPlugin (status 222) and auto-populate $ROOT in dev mode
|
|
129
|
+
if (DEV) {
|
|
130
|
+
fetch('/__open-in-editor?file=+')
|
|
131
|
+
.then((r) => {
|
|
132
|
+
openInEditorPluginDetected = r.status === 222;
|
|
133
|
+
// If plugin detected, fetch project root for $ROOT variable
|
|
134
|
+
if (openInEditorPluginDetected && !projectRoot) {
|
|
135
|
+
return fetch('/__gg-project-root').then((r) => r.text());
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
.then((root) => {
|
|
139
|
+
if (root) {
|
|
140
|
+
projectRoot = root.trim();
|
|
141
|
+
localStorage.setItem(PROJECT_ROOT_KEY, projectRoot);
|
|
142
|
+
}
|
|
143
|
+
// Re-render settings if panel is open (to show detection result)
|
|
144
|
+
if (settingsExpanded)
|
|
145
|
+
renderSettingsUI();
|
|
146
|
+
})
|
|
147
|
+
.catch(() => {
|
|
148
|
+
openInEditorPluginDetected = false;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
42
151
|
// Render initial UI
|
|
43
152
|
$el.html(buildHTML());
|
|
44
153
|
wireUpButtons();
|
|
45
154
|
wireUpExpanders();
|
|
46
155
|
wireUpResize();
|
|
47
156
|
wireUpFilterUI();
|
|
157
|
+
wireUpSettingsUI();
|
|
48
158
|
renderLogs();
|
|
49
159
|
},
|
|
50
160
|
show() {
|
|
@@ -244,6 +354,29 @@ export function createGgPlugin(options, gg) {
|
|
|
244
354
|
.gg-solo-target:hover {
|
|
245
355
|
background: rgba(0,0,0,0.05);
|
|
246
356
|
}
|
|
357
|
+
/* Clickable namespace cells with file metadata (open-in-editor) */
|
|
358
|
+
.gg-log-ns[data-file] {
|
|
359
|
+
cursor: pointer;
|
|
360
|
+
text-decoration: underline;
|
|
361
|
+
text-decoration-style: dotted;
|
|
362
|
+
text-underline-offset: 3px;
|
|
363
|
+
}
|
|
364
|
+
.gg-log-ns[data-file]:hover {
|
|
365
|
+
text-decoration-style: solid;
|
|
366
|
+
background: rgba(0,0,0,0.05);
|
|
367
|
+
}
|
|
368
|
+
.gg-log-ns[data-file]::after {
|
|
369
|
+
content: ' \u{1F4CB}';
|
|
370
|
+
font-size: 10px;
|
|
371
|
+
opacity: 0;
|
|
372
|
+
transition: opacity 0.1s;
|
|
373
|
+
}
|
|
374
|
+
.gg-action-open .gg-log-ns[data-file]::after {
|
|
375
|
+
content: ' \u{1F517}';
|
|
376
|
+
}
|
|
377
|
+
.gg-log-ns[data-file]:hover::after {
|
|
378
|
+
opacity: 1;
|
|
379
|
+
}
|
|
247
380
|
.gg-details {
|
|
248
381
|
grid-column: 1 / -1;
|
|
249
382
|
border-top: none;
|
|
@@ -286,6 +419,107 @@ export function createGgPlugin(options, gg) {
|
|
|
286
419
|
.gg-log-content {
|
|
287
420
|
word-break: break-word;
|
|
288
421
|
padding: 4px 0;
|
|
422
|
+
position: relative;
|
|
423
|
+
-webkit-user-select: text !important;
|
|
424
|
+
user-select: text !important;
|
|
425
|
+
cursor: text;
|
|
426
|
+
}
|
|
427
|
+
.gg-log-content * {
|
|
428
|
+
-webkit-user-select: text !important;
|
|
429
|
+
user-select: text !important;
|
|
430
|
+
}
|
|
431
|
+
.gg-log-diff, .gg-log-ns {
|
|
432
|
+
-webkit-user-select: text !important;
|
|
433
|
+
user-select: text !important;
|
|
434
|
+
}
|
|
435
|
+
.gg-details, .gg-details * {
|
|
436
|
+
-webkit-user-select: text !important;
|
|
437
|
+
user-select: text !important;
|
|
438
|
+
cursor: text;
|
|
439
|
+
}
|
|
440
|
+
/* Fast custom tooltip for src expression on primitive-only rows (no expandable objects) */
|
|
441
|
+
.gg-log-content[data-src] {
|
|
442
|
+
cursor: help;
|
|
443
|
+
}
|
|
444
|
+
/* Show icon only on primitive rows (no .gg-expand child) */
|
|
445
|
+
.gg-log-content[data-src]:not(:has(.gg-expand))::before {
|
|
446
|
+
content: '\uD83D\uDD0D';
|
|
447
|
+
font-size: 10px;
|
|
448
|
+
margin-right: 4px;
|
|
449
|
+
opacity: 0.4;
|
|
450
|
+
}
|
|
451
|
+
.gg-log-content[data-src]:not(:has(.gg-expand)):hover::before {
|
|
452
|
+
opacity: 1;
|
|
453
|
+
}
|
|
454
|
+
.gg-log-content[data-src]:not(:has(.gg-expand))::after {
|
|
455
|
+
content: attr(data-src);
|
|
456
|
+
position: absolute;
|
|
457
|
+
top: 100%;
|
|
458
|
+
left: 0;
|
|
459
|
+
background: #333;
|
|
460
|
+
color: #fff;
|
|
461
|
+
font-size: 11px;
|
|
462
|
+
font-family: monospace;
|
|
463
|
+
padding: 3px 8px;
|
|
464
|
+
border-radius: 3px;
|
|
465
|
+
white-space: nowrap;
|
|
466
|
+
pointer-events: none;
|
|
467
|
+
opacity: 0;
|
|
468
|
+
transition: opacity 0.1s;
|
|
469
|
+
z-index: 1000;
|
|
470
|
+
max-width: 90vw;
|
|
471
|
+
overflow: hidden;
|
|
472
|
+
text-overflow: ellipsis;
|
|
473
|
+
}
|
|
474
|
+
.gg-log-content[data-src]:not(:has(.gg-expand)):hover::after {
|
|
475
|
+
opacity: 1;
|
|
476
|
+
}
|
|
477
|
+
/* Expression icon inline with expandable object labels */
|
|
478
|
+
.gg-src-icon {
|
|
479
|
+
font-size: 10px;
|
|
480
|
+
margin-right: 2px;
|
|
481
|
+
opacity: 0.4;
|
|
482
|
+
cursor: pointer;
|
|
483
|
+
}
|
|
484
|
+
.gg-expand:hover .gg-src-icon,
|
|
485
|
+
.gg-src-icon:hover {
|
|
486
|
+
opacity: 1;
|
|
487
|
+
}
|
|
488
|
+
/* Hover tooltip for expandable objects/arrays */
|
|
489
|
+
.gg-hover-tooltip {
|
|
490
|
+
display: none;
|
|
491
|
+
position: fixed;
|
|
492
|
+
background: #1e1e1e;
|
|
493
|
+
color: #d4d4d4;
|
|
494
|
+
font-size: 11px;
|
|
495
|
+
font-family: monospace;
|
|
496
|
+
padding: 8px 10px;
|
|
497
|
+
border-radius: 4px;
|
|
498
|
+
white-space: pre;
|
|
499
|
+
pointer-events: none;
|
|
500
|
+
z-index: 100000;
|
|
501
|
+
max-width: min(90vw, 500px);
|
|
502
|
+
max-height: 300px;
|
|
503
|
+
overflow: auto;
|
|
504
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
505
|
+
line-height: 1.4;
|
|
506
|
+
}
|
|
507
|
+
.gg-hover-tooltip-src {
|
|
508
|
+
color: #9cdcfe;
|
|
509
|
+
font-style: italic;
|
|
510
|
+
margin-bottom: 4px;
|
|
511
|
+
padding-bottom: 4px;
|
|
512
|
+
border-bottom: 1px solid #444;
|
|
513
|
+
}
|
|
514
|
+
/* Expression header inside expanded details */
|
|
515
|
+
.gg-details-src {
|
|
516
|
+
color: #555;
|
|
517
|
+
font-style: italic;
|
|
518
|
+
font-family: monospace;
|
|
519
|
+
font-size: 11px;
|
|
520
|
+
margin-bottom: 6px;
|
|
521
|
+
padding-bottom: 4px;
|
|
522
|
+
border-bottom: 1px solid #ddd;
|
|
289
523
|
}
|
|
290
524
|
.gg-filter-panel {
|
|
291
525
|
background: #f5f5f5;
|
|
@@ -313,15 +547,88 @@ export function createGgPlugin(options, gg) {
|
|
|
313
547
|
max-height: 100px;
|
|
314
548
|
overflow-y: auto;
|
|
315
549
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
550
|
+
.gg-filter-checkbox {
|
|
551
|
+
display: flex;
|
|
552
|
+
align-items: center;
|
|
553
|
+
gap: 4px;
|
|
554
|
+
font-size: 11px;
|
|
555
|
+
font-family: monospace;
|
|
556
|
+
white-space: nowrap;
|
|
557
|
+
}
|
|
558
|
+
.gg-settings-panel {
|
|
559
|
+
background: #f5f5f5;
|
|
560
|
+
padding: 10px;
|
|
561
|
+
margin-bottom: 8px;
|
|
562
|
+
border-radius: 4px;
|
|
563
|
+
flex-shrink: 0;
|
|
564
|
+
display: none;
|
|
565
|
+
}
|
|
566
|
+
.gg-settings-panel.expanded {
|
|
567
|
+
display: block;
|
|
568
|
+
}
|
|
569
|
+
.gg-settings-label {
|
|
570
|
+
font-size: 11px;
|
|
571
|
+
font-weight: bold;
|
|
572
|
+
margin-bottom: 4px;
|
|
573
|
+
}
|
|
574
|
+
.gg-editor-format-input,
|
|
575
|
+
.gg-project-root-input {
|
|
576
|
+
width: 100%;
|
|
577
|
+
padding: 4px 8px;
|
|
578
|
+
font-family: monospace;
|
|
579
|
+
font-size: 14px;
|
|
580
|
+
margin-bottom: 8px;
|
|
581
|
+
box-sizing: border-box;
|
|
582
|
+
}
|
|
583
|
+
.gg-editor-presets {
|
|
584
|
+
display: flex;
|
|
585
|
+
flex-wrap: wrap;
|
|
586
|
+
gap: 4px;
|
|
587
|
+
}
|
|
588
|
+
.gg-editor-presets button {
|
|
589
|
+
padding: 2px 8px;
|
|
590
|
+
font-size: 11px;
|
|
591
|
+
cursor: pointer;
|
|
592
|
+
border: 1px solid #ccc;
|
|
593
|
+
border-radius: 3px;
|
|
594
|
+
background: #fff;
|
|
595
|
+
}
|
|
596
|
+
.gg-editor-presets button.active {
|
|
597
|
+
background: #4a9eff;
|
|
598
|
+
color: #fff;
|
|
599
|
+
border-color: #4a9eff;
|
|
600
|
+
}
|
|
601
|
+
.gg-editor-presets button:hover {
|
|
602
|
+
background: #e0e0e0;
|
|
603
|
+
}
|
|
604
|
+
.gg-editor-presets button.active:hover {
|
|
605
|
+
background: #3a8eef;
|
|
606
|
+
}
|
|
607
|
+
.gg-settings-radios {
|
|
608
|
+
display: flex;
|
|
609
|
+
flex-wrap: wrap;
|
|
610
|
+
gap: 4px 12px;
|
|
611
|
+
margin-bottom: 8px;
|
|
612
|
+
}
|
|
613
|
+
.gg-settings-radios label {
|
|
614
|
+
font-size: 12px;
|
|
615
|
+
padding: 3px 0;
|
|
616
|
+
cursor: pointer;
|
|
617
|
+
white-space: nowrap;
|
|
618
|
+
}
|
|
619
|
+
.gg-settings-radios label.disabled {
|
|
620
|
+
opacity: 0.4;
|
|
621
|
+
cursor: not-allowed;
|
|
622
|
+
}
|
|
623
|
+
.gg-settings-sub {
|
|
624
|
+
margin-top: 4px;
|
|
625
|
+
margin-bottom: 8px;
|
|
626
|
+
}
|
|
627
|
+
.gg-settings-sub select {
|
|
628
|
+
padding: 2px 4px;
|
|
629
|
+
font-size: 12px;
|
|
630
|
+
}
|
|
631
|
+
/* Mobile responsive styles */
|
|
325
632
|
.gg-toolbar {
|
|
326
633
|
display: flex;
|
|
327
634
|
align-items: center;
|
|
@@ -410,7 +717,7 @@ export function createGgPlugin(options, gg) {
|
|
|
410
717
|
}
|
|
411
718
|
}
|
|
412
719
|
</style>
|
|
413
|
-
<div class="eruda-gg" style="padding: 10px; height: 100%; display: flex; flex-direction: column; font-size: 14px; touch-action: none; overscroll-behavior: contain;">
|
|
720
|
+
<div class="eruda-gg${nsClickAction === 'open' || nsClickAction === 'open-url' ? ' gg-action-open' : ''}" style="padding: 10px; height: 100%; display: flex; flex-direction: column; font-size: 14px; touch-action: none; overscroll-behavior: contain;">
|
|
414
721
|
<div class="gg-toolbar">
|
|
415
722
|
<button class="gg-copy-btn">
|
|
416
723
|
<span class="gg-btn-text">Copy</span>
|
|
@@ -421,6 +728,10 @@ export function createGgPlugin(options, gg) {
|
|
|
421
728
|
<span class="gg-btn-icon">NS: </span>
|
|
422
729
|
<span class="gg-filter-summary"></span>
|
|
423
730
|
</button>
|
|
731
|
+
<button class="gg-settings-btn">
|
|
732
|
+
<span class="gg-btn-text">⚙️ Settings</span>
|
|
733
|
+
<span class="gg-btn-icon" title="Settings">⚙️</span>
|
|
734
|
+
</button>
|
|
424
735
|
<span class="gg-count" style="opacity: 0.6; white-space: nowrap; flex: 1; text-align: right;"></span>
|
|
425
736
|
<button class="gg-clear-btn">
|
|
426
737
|
<span class="gg-btn-text">Clear</span>
|
|
@@ -428,13 +739,15 @@ export function createGgPlugin(options, gg) {
|
|
|
428
739
|
</button>
|
|
429
740
|
</div>
|
|
430
741
|
<div class="gg-filter-panel"></div>
|
|
742
|
+
<div class="gg-settings-panel"></div>
|
|
431
743
|
<div class="gg-log-container" style="flex: 1; overflow-y: auto; font-family: monospace; font-size: 12px; touch-action: pan-y; overscroll-behavior: contain;"></div>
|
|
744
|
+
<iframe class="gg-editor-iframe" hidden title="open-in-editor"></iframe>
|
|
432
745
|
</div>
|
|
433
746
|
`;
|
|
434
747
|
}
|
|
435
748
|
function applyPatternFromInput(value) {
|
|
436
749
|
filterPattern = value;
|
|
437
|
-
localStorage.setItem(
|
|
750
|
+
localStorage.setItem(FILTER_KEY, filterPattern);
|
|
438
751
|
// Sync enabledNamespaces from the new pattern
|
|
439
752
|
const allNamespaces = getAllCapturedNamespaces();
|
|
440
753
|
enabledNamespaces.clear();
|
|
@@ -455,9 +768,13 @@ export function createGgPlugin(options, gg) {
|
|
|
455
768
|
if (!filterBtn || !filterPanel)
|
|
456
769
|
return;
|
|
457
770
|
renderFilterUI();
|
|
458
|
-
// Wire up button toggle
|
|
771
|
+
// Wire up button toggle (close settings if opening filter)
|
|
459
772
|
filterBtn.addEventListener('click', () => {
|
|
460
773
|
filterExpanded = !filterExpanded;
|
|
774
|
+
if (filterExpanded) {
|
|
775
|
+
settingsExpanded = false;
|
|
776
|
+
renderSettingsUI();
|
|
777
|
+
}
|
|
461
778
|
renderFilterUI();
|
|
462
779
|
renderLogs(); // Re-render to update grid columns
|
|
463
780
|
});
|
|
@@ -493,7 +810,7 @@ export function createGgPlugin(options, gg) {
|
|
|
493
810
|
filterPattern = `gg:*,${exclusions}`;
|
|
494
811
|
enabledNamespaces.clear();
|
|
495
812
|
}
|
|
496
|
-
localStorage.setItem(
|
|
813
|
+
localStorage.setItem(FILTER_KEY, filterPattern);
|
|
497
814
|
renderFilterUI();
|
|
498
815
|
renderLogs();
|
|
499
816
|
return;
|
|
@@ -572,6 +889,214 @@ export function createGgPlugin(options, gg) {
|
|
|
572
889
|
filterPanel.classList.remove('expanded');
|
|
573
890
|
}
|
|
574
891
|
}
|
|
892
|
+
/** Render the format field + $ROOT field shared by "Copy to clipboard" and "Open as URL" */
|
|
893
|
+
function renderFormatSection(isUrlMode) {
|
|
894
|
+
const presets = isUrlMode ? uriPresets : copyPresets;
|
|
895
|
+
const placeholder = isUrlMode ? 'vscode://file/$ROOT/$FILE:$LINE:$COL' : '$FILE:$LINE:$COL';
|
|
896
|
+
const description = isUrlMode
|
|
897
|
+
? 'Opens a URI in the browser. Editor apps register URI schemes to handle these.'
|
|
898
|
+
: 'Copies a command to your clipboard. Paste in a terminal to open the source file.';
|
|
899
|
+
const currentFormat = activeFormat();
|
|
900
|
+
const presetButtons = Object.entries(presets)
|
|
901
|
+
.map(([name, fmt]) => {
|
|
902
|
+
const active = currentFormat === fmt ? ' active' : '';
|
|
903
|
+
return `<button class="gg-preset-btn${active}" data-format="${escapeHtml(fmt)}">${escapeHtml(name)}</button>`;
|
|
904
|
+
})
|
|
905
|
+
.join('');
|
|
906
|
+
return `
|
|
907
|
+
<div class="gg-settings-sub">
|
|
908
|
+
<div style="font-size: 11px; opacity: 0.7; margin-bottom: 6px;">${description}<br>Variables: <code>$FILE</code>, <code>$LINE</code>, <code>$COL</code>, <code>$ROOT</code></div>
|
|
909
|
+
<input type="text" class="gg-editor-format-input" value="${escapeHtml(currentFormat)}" placeholder="${escapeHtml(placeholder)}">
|
|
910
|
+
<div class="gg-settings-label" style="margin-top: 4px;">Presets:</div>
|
|
911
|
+
<div class="gg-editor-presets">${presetButtons}</div>
|
|
912
|
+
<div style="margin-top: 8px;">
|
|
913
|
+
<div class="gg-settings-label">Project root (<code>$ROOT</code>):</div>
|
|
914
|
+
<input type="text" class="gg-project-root-input" value="${escapeHtml(projectRoot)}" placeholder="/home/user/my-project" style="width: 100%; padding: 4px 8px; font-family: monospace; font-size: 14px; box-sizing: border-box;">
|
|
915
|
+
<div style="font-size: 10px; opacity: 0.5; margin-top: 2px;">${DEV && openInEditorPluginDetected ? 'Auto-filled from dev server.' : 'Set manually for URI schemes.'} Uses forward slashes on all platforms.</div>
|
|
916
|
+
</div>
|
|
917
|
+
</div>
|
|
918
|
+
`;
|
|
919
|
+
}
|
|
920
|
+
function renderSettingsUI() {
|
|
921
|
+
if (!$el)
|
|
922
|
+
return;
|
|
923
|
+
const settingsPanel = $el.find('.gg-settings-panel').get(0);
|
|
924
|
+
if (!settingsPanel)
|
|
925
|
+
return;
|
|
926
|
+
// Toggle CSS class on container for hover icon (📝 vs 📋)
|
|
927
|
+
const container = $el.find('.eruda-gg').get(0);
|
|
928
|
+
if (container) {
|
|
929
|
+
container.classList.toggle('gg-action-open', nsClickAction === 'open' || nsClickAction === 'open-url');
|
|
930
|
+
}
|
|
931
|
+
if (settingsExpanded) {
|
|
932
|
+
settingsPanel.classList.add('expanded');
|
|
933
|
+
const openDisabled = !DEV;
|
|
934
|
+
const openLabelClass = openDisabled ? ' disabled' : '';
|
|
935
|
+
// Warning when call-sites plugin not installed (no file/line/col metadata)
|
|
936
|
+
const callSitesWarning = !_ggCallSitesPlugin
|
|
937
|
+
? `<div style="font-size: 11px; color: #b8860b; margin-bottom: 6px;">\u26A0 call-sites vite plugin not detected \u2014 namespaces have no file locations. Add ggCallSitesPlugin() to vite.config.ts to enable click-to-open.</div>`
|
|
938
|
+
: '';
|
|
939
|
+
// Options section below all radios (editor buttons or format field)
|
|
940
|
+
let optionsSection = '';
|
|
941
|
+
if (nsClickAction === 'open' && !openDisabled) {
|
|
942
|
+
const pluginWarning = openInEditorPluginDetected === false
|
|
943
|
+
? `<div style="font-size: 11px; color: #b8860b; margin-bottom: 6px;">\u26A0 open-in-editor vite plugin not detected \u2014 file will open at line 1 (no cursor positioning or editor selection). Add openInEditorPlugin() to vite.config.ts for full support.</div>`
|
|
944
|
+
: '';
|
|
945
|
+
let editorButtons = '';
|
|
946
|
+
if (openInEditorPluginDetected !== false) {
|
|
947
|
+
const buttons = editorBins
|
|
948
|
+
.map(({ label, value }) => {
|
|
949
|
+
const active = value === editorBin ? ' active' : '';
|
|
950
|
+
return `<button class="gg-editor-bin-btn${active}" data-editor="${escapeHtml(value)}">${escapeHtml(label)}</button>`;
|
|
951
|
+
})
|
|
952
|
+
.join('');
|
|
953
|
+
editorButtons = `<div class="gg-settings-label">Editor:</div><div class="gg-editor-presets">${buttons}</div>`;
|
|
954
|
+
}
|
|
955
|
+
optionsSection = `<div class="gg-settings-sub">${pluginWarning}${editorButtons}</div>`;
|
|
956
|
+
}
|
|
957
|
+
else if (nsClickAction === 'copy' || nsClickAction === 'open-url') {
|
|
958
|
+
optionsSection = renderFormatSection(nsClickAction === 'open-url');
|
|
959
|
+
}
|
|
960
|
+
// Native Console section
|
|
961
|
+
const currentDebugValue = localStorage.getItem('debug');
|
|
962
|
+
const debugDisplay = currentDebugValue !== null ? `'${escapeHtml(currentDebugValue)}'` : 'not set';
|
|
963
|
+
const currentFilter = filterPattern || 'gg:*';
|
|
964
|
+
const debugMatchesFilter = currentDebugValue === currentFilter;
|
|
965
|
+
const debugIncludesGg = currentDebugValue !== null &&
|
|
966
|
+
(currentDebugValue.includes('gg:') || currentDebugValue === '*');
|
|
967
|
+
const nativeConsoleSection = `
|
|
968
|
+
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #ddd;">
|
|
969
|
+
<div class="gg-settings-label">Native Console Output</div>
|
|
970
|
+
<div style="font-size: 11px; opacity: 0.7; margin-bottom: 8px;">
|
|
971
|
+
gg messages always appear in this GG panel. To also see them in the browser's native console, set <code>localStorage.debug</code> below.
|
|
972
|
+
For server-side: <code>DEBUG=gg:* npm run dev</code>
|
|
973
|
+
</div>
|
|
974
|
+
<div style="font-family: monospace; font-size: 12px; margin-bottom: 6px;">
|
|
975
|
+
localStorage.debug = ${debugDisplay}
|
|
976
|
+
${debugIncludesGg ? '<span style="color: green;">✅</span>' : '<span style="color: #999;">⚫ gg:* not included</span>'}
|
|
977
|
+
</div>
|
|
978
|
+
<div style="display: flex; gap: 6px; flex-wrap: wrap;">
|
|
979
|
+
<button class="gg-sync-debug-btn" style="padding: 4px 10px; font-size: 12px; cursor: pointer; border: 1px solid #ccc; border-radius: 3px; background: ${debugMatchesFilter ? '#e8e8e8' : '#fff'};"${debugMatchesFilter ? ' disabled' : ''}>
|
|
980
|
+
${debugMatchesFilter ? 'In sync' : `Set to '${escapeHtml(currentFilter)}'`}
|
|
981
|
+
</button>
|
|
982
|
+
<button class="gg-clear-debug-btn" style="padding: 4px 10px; font-size: 12px; cursor: pointer; border: 1px solid #ccc; border-radius: 3px; background: #fff;"${currentDebugValue === null ? ' disabled' : ''}>
|
|
983
|
+
Clear
|
|
984
|
+
</button>
|
|
985
|
+
</div>
|
|
986
|
+
<div style="font-size: 10px; opacity: 0.5; margin-top: 4px;">
|
|
987
|
+
Changes take effect on next page reload.
|
|
988
|
+
</div>
|
|
989
|
+
</div>
|
|
990
|
+
`;
|
|
991
|
+
settingsPanel.innerHTML = `
|
|
992
|
+
${callSitesWarning}
|
|
993
|
+
<div class="gg-settings-label">When namespace clicked:</div>
|
|
994
|
+
<div class="gg-settings-radios">
|
|
995
|
+
<label class="${openLabelClass}">
|
|
996
|
+
<input type="radio" name="gg-ns-action" value="open" ${nsClickAction === 'open' ? 'checked' : ''} ${openDisabled ? 'disabled' : ''}>
|
|
997
|
+
Open via dev server${openDisabled ? ' (dev mode only)' : ''}
|
|
998
|
+
</label>
|
|
999
|
+
<label>
|
|
1000
|
+
<input type="radio" name="gg-ns-action" value="open-url" ${nsClickAction === 'open-url' ? 'checked' : ''}>
|
|
1001
|
+
Open via URL
|
|
1002
|
+
</label>
|
|
1003
|
+
<label>
|
|
1004
|
+
<input type="radio" name="gg-ns-action" value="copy" ${nsClickAction === 'copy' ? 'checked' : ''}>
|
|
1005
|
+
Copy to clipboard
|
|
1006
|
+
</label>
|
|
1007
|
+
</div>
|
|
1008
|
+
${optionsSection}
|
|
1009
|
+
${nativeConsoleSection}
|
|
1010
|
+
`;
|
|
1011
|
+
}
|
|
1012
|
+
else {
|
|
1013
|
+
settingsPanel.classList.remove('expanded');
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
function wireUpSettingsUI() {
|
|
1017
|
+
if (!$el)
|
|
1018
|
+
return;
|
|
1019
|
+
const settingsBtn = $el.find('.gg-settings-btn').get(0);
|
|
1020
|
+
const settingsPanel = $el.find('.gg-settings-panel').get(0);
|
|
1021
|
+
if (!settingsBtn || !settingsPanel)
|
|
1022
|
+
return;
|
|
1023
|
+
// Toggle settings panel (close filter if opening settings)
|
|
1024
|
+
settingsBtn.addEventListener('click', () => {
|
|
1025
|
+
settingsExpanded = !settingsExpanded;
|
|
1026
|
+
if (settingsExpanded) {
|
|
1027
|
+
filterExpanded = false;
|
|
1028
|
+
renderFilterUI();
|
|
1029
|
+
renderLogs();
|
|
1030
|
+
}
|
|
1031
|
+
renderSettingsUI();
|
|
1032
|
+
});
|
|
1033
|
+
// Event delegation on settings panel
|
|
1034
|
+
settingsPanel.addEventListener('change', (e) => {
|
|
1035
|
+
const target = e.target;
|
|
1036
|
+
// Radio buttons: open vs copy vs open-url
|
|
1037
|
+
if (target.name === 'gg-ns-action') {
|
|
1038
|
+
nsClickAction = target.value;
|
|
1039
|
+
localStorage.setItem(NS_ACTION_KEY, nsClickAction);
|
|
1040
|
+
renderSettingsUI();
|
|
1041
|
+
renderLogs(); // Re-render tooltips
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
// Format + project root inputs: apply on blur or Enter
|
|
1045
|
+
settingsPanel.addEventListener('blur', (e) => {
|
|
1046
|
+
const target = e.target;
|
|
1047
|
+
if (target.classList.contains('gg-editor-format-input')) {
|
|
1048
|
+
setActiveFormat(target.value);
|
|
1049
|
+
renderSettingsUI();
|
|
1050
|
+
}
|
|
1051
|
+
if (target.classList.contains('gg-project-root-input')) {
|
|
1052
|
+
projectRoot = target.value;
|
|
1053
|
+
localStorage.setItem(PROJECT_ROOT_KEY, projectRoot);
|
|
1054
|
+
}
|
|
1055
|
+
}, true);
|
|
1056
|
+
settingsPanel.addEventListener('keydown', (e) => {
|
|
1057
|
+
const target = e.target;
|
|
1058
|
+
if (target.classList.contains('gg-editor-format-input') && e.key === 'Enter') {
|
|
1059
|
+
setActiveFormat(target.value);
|
|
1060
|
+
target.blur();
|
|
1061
|
+
renderSettingsUI();
|
|
1062
|
+
}
|
|
1063
|
+
if (target.classList.contains('gg-project-root-input') && e.key === 'Enter') {
|
|
1064
|
+
projectRoot = target.value;
|
|
1065
|
+
localStorage.setItem(PROJECT_ROOT_KEY, projectRoot);
|
|
1066
|
+
target.blur();
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
// Preset button clicks + editor bin button clicks + native console buttons
|
|
1070
|
+
settingsPanel.addEventListener('click', (e) => {
|
|
1071
|
+
const target = e.target;
|
|
1072
|
+
if (target.classList.contains('gg-preset-btn')) {
|
|
1073
|
+
const fmt = target.getAttribute('data-format');
|
|
1074
|
+
if (fmt) {
|
|
1075
|
+
setActiveFormat(fmt);
|
|
1076
|
+
renderSettingsUI();
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
if (target.classList.contains('gg-editor-bin-btn')) {
|
|
1080
|
+
const editor = target.getAttribute('data-editor');
|
|
1081
|
+
if (editor !== null) {
|
|
1082
|
+
editorBin = editor;
|
|
1083
|
+
localStorage.setItem(EDITOR_BIN_KEY, editorBin);
|
|
1084
|
+
renderSettingsUI();
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
// Native Console: sync localStorage.debug to current gg-filter
|
|
1088
|
+
if (target.classList.contains('gg-sync-debug-btn')) {
|
|
1089
|
+
const currentFilter = localStorage.getItem(FILTER_KEY) || 'gg:*';
|
|
1090
|
+
localStorage.setItem('debug', currentFilter);
|
|
1091
|
+
renderSettingsUI();
|
|
1092
|
+
}
|
|
1093
|
+
// Native Console: clear localStorage.debug
|
|
1094
|
+
if (target.classList.contains('gg-clear-debug-btn')) {
|
|
1095
|
+
localStorage.removeItem('debug');
|
|
1096
|
+
renderSettingsUI();
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
575
1100
|
function wireUpButtons() {
|
|
576
1101
|
if (!$el)
|
|
577
1102
|
return;
|
|
@@ -616,6 +1141,58 @@ export function createGgPlugin(options, gg) {
|
|
|
616
1141
|
}
|
|
617
1142
|
});
|
|
618
1143
|
}
|
|
1144
|
+
/** Substitute format variables ($ROOT, $FILE, $LINE, $COL) in a format string */
|
|
1145
|
+
function formatString(format, file, line, col) {
|
|
1146
|
+
return format
|
|
1147
|
+
.replace(/\$ROOT/g, projectRoot)
|
|
1148
|
+
.replace(/\$FILE/g, file)
|
|
1149
|
+
.replace(/\$LINE/g, line || '1')
|
|
1150
|
+
.replace(/\$COL/g, col || '1');
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Handle namespace click: open in editor via Vite middleware, copy to clipboard, or open URL.
|
|
1154
|
+
* Behavior is controlled by the nsClickAction setting.
|
|
1155
|
+
*/
|
|
1156
|
+
function handleNamespaceClick(target) {
|
|
1157
|
+
if (!$el)
|
|
1158
|
+
return;
|
|
1159
|
+
const file = target.getAttribute('data-file');
|
|
1160
|
+
if (!file)
|
|
1161
|
+
return;
|
|
1162
|
+
const line = target.getAttribute('data-line');
|
|
1163
|
+
const col = target.getAttribute('data-col');
|
|
1164
|
+
if (nsClickAction === 'open' && DEV) {
|
|
1165
|
+
// Open in editor via Vite dev server middleware
|
|
1166
|
+
let url = `/__open-in-editor?file=${encodeURIComponent(file)}`;
|
|
1167
|
+
if (line)
|
|
1168
|
+
url += `&line=${line}`;
|
|
1169
|
+
if (line && col)
|
|
1170
|
+
url += `&col=${col}`;
|
|
1171
|
+
if (editorBin)
|
|
1172
|
+
url += `&editor=${encodeURIComponent(editorBin)}`;
|
|
1173
|
+
const iframe = $el.find('.gg-editor-iframe').get(0);
|
|
1174
|
+
if (iframe) {
|
|
1175
|
+
iframe.src = url;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
else if (nsClickAction === 'open-url') {
|
|
1179
|
+
// Open formatted URI in browser (editor handles the URI scheme)
|
|
1180
|
+
const formatted = formatString(activeFormat(), file, line, col);
|
|
1181
|
+
window.open(formatted, '_blank');
|
|
1182
|
+
}
|
|
1183
|
+
else {
|
|
1184
|
+
// Copy formatted file path to clipboard
|
|
1185
|
+
const formatted = formatString(activeFormat(), file, line, col);
|
|
1186
|
+
navigator.clipboard.writeText(formatted).then(() => {
|
|
1187
|
+
// Brief "Copied!" feedback on the namespace cell
|
|
1188
|
+
const original = target.textContent;
|
|
1189
|
+
target.textContent = '\u{1F4CB} Copied!';
|
|
1190
|
+
setTimeout(() => {
|
|
1191
|
+
target.textContent = original;
|
|
1192
|
+
}, 1200);
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
619
1196
|
function wireUpExpanders() {
|
|
620
1197
|
if (!$el || expanderAttached)
|
|
621
1198
|
return;
|
|
@@ -637,6 +1214,13 @@ export function createGgPlugin(options, gg) {
|
|
|
637
1214
|
}
|
|
638
1215
|
return;
|
|
639
1216
|
}
|
|
1217
|
+
// Handle clicking namespace to open in editor (when filter collapsed)
|
|
1218
|
+
if (target?.classList?.contains('gg-log-ns') &&
|
|
1219
|
+
target.hasAttribute('data-file') &&
|
|
1220
|
+
!target.classList.contains('gg-solo-target')) {
|
|
1221
|
+
handleNamespaceClick(target);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
640
1224
|
// Handle filter icon clicks (hide / solo)
|
|
641
1225
|
if (target?.classList?.contains('gg-icon-hide') ||
|
|
642
1226
|
target?.classList?.contains('gg-icon-solo')) {
|
|
@@ -650,7 +1234,7 @@ export function createGgPlugin(options, gg) {
|
|
|
650
1234
|
else {
|
|
651
1235
|
soloNamespace(namespace);
|
|
652
1236
|
}
|
|
653
|
-
localStorage.setItem(
|
|
1237
|
+
localStorage.setItem(FILTER_KEY, filterPattern);
|
|
654
1238
|
renderFilterUI();
|
|
655
1239
|
renderLogs();
|
|
656
1240
|
return;
|
|
@@ -661,7 +1245,7 @@ export function createGgPlugin(options, gg) {
|
|
|
661
1245
|
if (!namespace)
|
|
662
1246
|
return;
|
|
663
1247
|
soloNamespace(namespace);
|
|
664
|
-
localStorage.setItem(
|
|
1248
|
+
localStorage.setItem(FILTER_KEY, filterPattern);
|
|
665
1249
|
renderFilterUI();
|
|
666
1250
|
renderLogs();
|
|
667
1251
|
return;
|
|
@@ -673,11 +1257,75 @@ export function createGgPlugin(options, gg) {
|
|
|
673
1257
|
filterPattern = 'gg:*';
|
|
674
1258
|
enabledNamespaces.clear();
|
|
675
1259
|
getAllCapturedNamespaces().forEach((ns) => enabledNamespaces.add(ns));
|
|
676
|
-
localStorage.setItem(
|
|
1260
|
+
localStorage.setItem(FILTER_KEY, filterPattern);
|
|
677
1261
|
renderFilterUI();
|
|
678
1262
|
renderLogs();
|
|
679
1263
|
}
|
|
680
1264
|
});
|
|
1265
|
+
// Hover tooltip for expandable objects/arrays.
|
|
1266
|
+
// The tooltip div is re-created after each renderLogs() call
|
|
1267
|
+
// since logContainer.html() destroys children. Event listeners query it dynamically.
|
|
1268
|
+
containerEl.addEventListener('mouseover', (e) => {
|
|
1269
|
+
const target = e.target?.closest?.('.gg-expand');
|
|
1270
|
+
if (!target)
|
|
1271
|
+
return;
|
|
1272
|
+
const entryIdx = target.getAttribute('data-entry');
|
|
1273
|
+
const argIdx = target.getAttribute('data-arg');
|
|
1274
|
+
if (entryIdx === null || argIdx === null)
|
|
1275
|
+
return;
|
|
1276
|
+
const entry = renderedEntries[Number(entryIdx)];
|
|
1277
|
+
if (!entry)
|
|
1278
|
+
return;
|
|
1279
|
+
const arg = entry.args[Number(argIdx)];
|
|
1280
|
+
if (arg === undefined)
|
|
1281
|
+
return;
|
|
1282
|
+
const tip = containerEl.querySelector('.gg-hover-tooltip');
|
|
1283
|
+
if (!tip)
|
|
1284
|
+
return;
|
|
1285
|
+
const srcExpr = target.getAttribute('data-src');
|
|
1286
|
+
// Build tooltip content using DOM API (safe, no HTML injection)
|
|
1287
|
+
tip.textContent = '';
|
|
1288
|
+
if (srcExpr) {
|
|
1289
|
+
const srcDiv = document.createElement('div');
|
|
1290
|
+
srcDiv.className = 'gg-hover-tooltip-src';
|
|
1291
|
+
srcDiv.textContent = srcExpr;
|
|
1292
|
+
tip.appendChild(srcDiv);
|
|
1293
|
+
}
|
|
1294
|
+
const pre = document.createElement('pre');
|
|
1295
|
+
pre.style.margin = '0';
|
|
1296
|
+
pre.textContent = JSON.stringify(arg, null, 2);
|
|
1297
|
+
tip.appendChild(pre);
|
|
1298
|
+
tip.style.display = 'block';
|
|
1299
|
+
// Position below the hovered element using viewport coords (fixed positioning)
|
|
1300
|
+
const targetRect = target.getBoundingClientRect();
|
|
1301
|
+
let left = targetRect.left;
|
|
1302
|
+
let top = targetRect.bottom + 4;
|
|
1303
|
+
// Keep tooltip within viewport
|
|
1304
|
+
const tipRect = tip.getBoundingClientRect();
|
|
1305
|
+
if (left + tipRect.width > window.innerWidth) {
|
|
1306
|
+
left = window.innerWidth - tipRect.width - 8;
|
|
1307
|
+
}
|
|
1308
|
+
if (left < 4)
|
|
1309
|
+
left = 4;
|
|
1310
|
+
// If tooltip would go below viewport, show above instead
|
|
1311
|
+
if (top + tipRect.height > window.innerHeight) {
|
|
1312
|
+
top = targetRect.top - tipRect.height - 4;
|
|
1313
|
+
}
|
|
1314
|
+
tip.style.left = `${left}px`;
|
|
1315
|
+
tip.style.top = `${top}px`;
|
|
1316
|
+
});
|
|
1317
|
+
containerEl.addEventListener('mouseout', (e) => {
|
|
1318
|
+
const target = e.target?.closest?.('.gg-expand');
|
|
1319
|
+
if (!target)
|
|
1320
|
+
return;
|
|
1321
|
+
// Only hide if we're not moving to another child of the same .gg-expand
|
|
1322
|
+
const related = e.relatedTarget;
|
|
1323
|
+
if (related?.closest?.('.gg-expand') === target)
|
|
1324
|
+
return;
|
|
1325
|
+
const tip = containerEl.querySelector('.gg-hover-tooltip');
|
|
1326
|
+
if (tip)
|
|
1327
|
+
tip.style.display = 'none';
|
|
1328
|
+
});
|
|
681
1329
|
expanderAttached = true;
|
|
682
1330
|
}
|
|
683
1331
|
function wireUpResize() {
|
|
@@ -737,6 +1385,7 @@ export function createGgPlugin(options, gg) {
|
|
|
737
1385
|
const allEntries = buffer.getEntries();
|
|
738
1386
|
// Apply filtering
|
|
739
1387
|
const entries = allEntries.filter((entry) => enabledNamespaces.has(entry.namespace));
|
|
1388
|
+
renderedEntries = entries;
|
|
740
1389
|
const countText = entries.length === allEntries.length
|
|
741
1390
|
? `${entries.length} entries`
|
|
742
1391
|
: `${entries.length} / ${allEntries.length} entries`;
|
|
@@ -753,6 +1402,8 @@ export function createGgPlugin(options, gg) {
|
|
|
753
1402
|
// Format each arg individually - objects are expandable
|
|
754
1403
|
let argsHTML = '';
|
|
755
1404
|
let detailsHTML = '';
|
|
1405
|
+
// Source expression for this entry (used in hover tooltips and expanded details)
|
|
1406
|
+
const srcExpr = entry.src?.trim() && !/^['"`]/.test(entry.src) ? escapeHtml(entry.src) : '';
|
|
756
1407
|
if (entry.args.length > 0) {
|
|
757
1408
|
argsHTML = entry.args
|
|
758
1409
|
.map((arg, argIdx) => {
|
|
@@ -761,9 +1412,14 @@ export function createGgPlugin(options, gg) {
|
|
|
761
1412
|
const preview = Array.isArray(arg) ? `Array(${arg.length})` : 'Object';
|
|
762
1413
|
const jsonStr = escapeHtml(JSON.stringify(arg, null, 2));
|
|
763
1414
|
const uniqueId = `${index}-${argIdx}`;
|
|
1415
|
+
// Expression header inside expanded details
|
|
1416
|
+
const srcHeader = srcExpr ? `<div class="gg-details-src">${srcExpr}</div>` : '';
|
|
764
1417
|
// Store details separately to render after the row
|
|
765
|
-
detailsHTML += `<div class="gg-details" data-index="${uniqueId}" style="display: none; margin: 4px 0 8px 0; padding: 8px; background: #f8f8f8; border-left: 3px solid ${color}; font-size: 11px; overflow-x: auto;"
|
|
766
|
-
|
|
1418
|
+
detailsHTML += `<div class="gg-details" data-index="${uniqueId}" style="display: none; margin: 4px 0 8px 0; padding: 8px; background: #f8f8f8; border-left: 3px solid ${color}; font-size: 11px; overflow-x: auto;">${srcHeader}<pre style="margin: 0;">${jsonStr}</pre></div>`;
|
|
1419
|
+
// data-entry/data-arg for hover tooltip lookup, data-src for expression context
|
|
1420
|
+
const srcAttr = srcExpr ? ` data-src="${srcExpr}"` : '';
|
|
1421
|
+
const srcIcon = srcExpr ? `<span class="gg-src-icon">\uD83D\uDD0D</span>` : '';
|
|
1422
|
+
return `<span style="color: #888; cursor: pointer; text-decoration: underline;" class="gg-expand" data-index="${uniqueId}" data-entry="${index}" data-arg="${argIdx}"${srcAttr}>${srcIcon}${preview}</span>`;
|
|
767
1423
|
}
|
|
768
1424
|
else {
|
|
769
1425
|
// Parse ANSI codes first, then convert URLs to clickable links
|
|
@@ -787,20 +1443,44 @@ export function createGgPlugin(options, gg) {
|
|
|
787
1443
|
// When filter expanded, diff+ns are clickable (solo) with data-namespace
|
|
788
1444
|
const soloAttr = filterExpanded ? ` data-namespace="${ns}"` : '';
|
|
789
1445
|
const soloClass = filterExpanded ? ' gg-solo-target' : '';
|
|
1446
|
+
// Open-in-editor data attributes (file, line, col)
|
|
1447
|
+
const fileAttr = entry.file ? ` data-file="${escapeHtml(entry.file)}"` : '';
|
|
1448
|
+
const lineAttr = entry.line ? ` data-line="${entry.line}"` : '';
|
|
1449
|
+
const colAttr = entry.col ? ` data-col="${entry.col}"` : '';
|
|
1450
|
+
let fileTitleText = '';
|
|
1451
|
+
if (entry.file) {
|
|
1452
|
+
if (nsClickAction === 'open' && DEV) {
|
|
1453
|
+
fileTitleText = `Open in editor: ${entry.file}${entry.line ? ':' + entry.line : ''}${entry.col ? ':' + entry.col : ''}`;
|
|
1454
|
+
}
|
|
1455
|
+
else if (nsClickAction === 'open-url') {
|
|
1456
|
+
fileTitleText = `Open URL: ${formatString(activeFormat(), entry.file, String(entry.line || 1), String(entry.col || 1))}`;
|
|
1457
|
+
}
|
|
1458
|
+
else {
|
|
1459
|
+
fileTitleText = `Copy: ${formatString(activeFormat(), entry.file, String(entry.line || 1), String(entry.col || 1))}`;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
const fileTitle = fileTitleText ? ` title="${escapeHtml(fileTitleText)}"` : '';
|
|
790
1463
|
// Desktop: grid layout, Mobile: stacked layout
|
|
791
1464
|
return (`<div class="gg-log-entry">` +
|
|
792
1465
|
`<div class="gg-log-header">` +
|
|
793
1466
|
iconsCol +
|
|
794
1467
|
`<div class="gg-log-diff${soloClass}" style="color: ${color};"${soloAttr}>${diff}</div>` +
|
|
795
|
-
`<div class="gg-log-ns${soloClass}" style="color: ${color};"${soloAttr}>${ns}</div>` +
|
|
1468
|
+
`<div class="gg-log-ns${soloClass}" style="color: ${color};"${soloAttr}${fileAttr}${lineAttr}${colAttr}${fileTitle}>${ns}</div>` +
|
|
796
1469
|
`<div class="gg-log-handle"></div>` +
|
|
797
1470
|
`</div>` +
|
|
798
|
-
`<div class="gg-log-content">${argsHTML}</div>` +
|
|
1471
|
+
`<div class="gg-log-content"${entry.src?.trim() && !/^['"`]/.test(entry.src) ? ` data-src="${escapeHtml(entry.src)}"` : ''}>${argsHTML}</div>` +
|
|
799
1472
|
detailsHTML +
|
|
800
1473
|
`</div>`);
|
|
801
1474
|
})
|
|
802
1475
|
.join('')}</div>`;
|
|
803
1476
|
logContainer.html(logsHTML);
|
|
1477
|
+
// Append hover tooltip div (destroyed by .html() each render, so re-create)
|
|
1478
|
+
const containerDom = logContainer.get(0);
|
|
1479
|
+
if (containerDom) {
|
|
1480
|
+
const tip = document.createElement('div');
|
|
1481
|
+
tip.className = 'gg-hover-tooltip';
|
|
1482
|
+
containerDom.appendChild(tip);
|
|
1483
|
+
}
|
|
804
1484
|
// Re-wire expanders after rendering
|
|
805
1485
|
wireUpExpanders();
|
|
806
1486
|
// Auto-scroll to bottom
|
|
@@ -832,17 +1512,58 @@ export function createGgPlugin(options, gg) {
|
|
|
832
1512
|
*/
|
|
833
1513
|
function stripAnsi(text) {
|
|
834
1514
|
// Remove all ANSI escape codes
|
|
1515
|
+
// eslint-disable-next-line no-control-regex
|
|
835
1516
|
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
836
1517
|
}
|
|
1518
|
+
// Standard ANSI 3/4-bit color palette
|
|
1519
|
+
const ANSI_COLORS = {
|
|
1520
|
+
// Normal foreground (30-37)
|
|
1521
|
+
30: '#000000',
|
|
1522
|
+
31: '#cc0000',
|
|
1523
|
+
32: '#00cc00',
|
|
1524
|
+
33: '#cccc00',
|
|
1525
|
+
34: '#0000cc',
|
|
1526
|
+
35: '#cc00cc',
|
|
1527
|
+
36: '#00cccc',
|
|
1528
|
+
37: '#cccccc',
|
|
1529
|
+
// Normal background (40-47)
|
|
1530
|
+
40: '#000000',
|
|
1531
|
+
41: '#cc0000',
|
|
1532
|
+
42: '#00cc00',
|
|
1533
|
+
43: '#cccc00',
|
|
1534
|
+
44: '#0000cc',
|
|
1535
|
+
45: '#cc00cc',
|
|
1536
|
+
46: '#00cccc',
|
|
1537
|
+
47: '#cccccc',
|
|
1538
|
+
// Bright foreground (90-97)
|
|
1539
|
+
90: '#555555',
|
|
1540
|
+
91: '#ff5555',
|
|
1541
|
+
92: '#55ff55',
|
|
1542
|
+
93: '#ffff55',
|
|
1543
|
+
94: '#5555ff',
|
|
1544
|
+
95: '#ff55ff',
|
|
1545
|
+
96: '#55ffff',
|
|
1546
|
+
97: '#ffffff',
|
|
1547
|
+
// Bright background (100-107)
|
|
1548
|
+
100: '#555555',
|
|
1549
|
+
101: '#ff5555',
|
|
1550
|
+
102: '#55ff55',
|
|
1551
|
+
103: '#ffff55',
|
|
1552
|
+
104: '#5555ff',
|
|
1553
|
+
105: '#ff55ff',
|
|
1554
|
+
106: '#55ffff',
|
|
1555
|
+
107: '#ffffff'
|
|
1556
|
+
};
|
|
837
1557
|
/**
|
|
838
1558
|
* Parse ANSI escape codes and convert to HTML with inline styles
|
|
839
1559
|
* Supports:
|
|
1560
|
+
* - Basic 3/4-bit colors: \x1b[31m (fg red), \x1b[41m (bg red), \x1b[91m (bright fg), etc.
|
|
840
1561
|
* - 24-bit RGB: \x1b[38;2;r;g;bm (foreground), \x1b[48;2;r;g;bm (background)
|
|
841
1562
|
* - Reset: \x1b[0m
|
|
842
1563
|
*/
|
|
843
1564
|
function parseAnsiToHtml(text) {
|
|
844
1565
|
// ANSI escape sequence regex
|
|
845
|
-
//
|
|
1566
|
+
// eslint-disable-next-line no-control-regex
|
|
846
1567
|
const ansiRegex = /\x1b\[([0-9;]+)m/g;
|
|
847
1568
|
let html = '';
|
|
848
1569
|
let lastIndex = 0;
|
|
@@ -871,6 +1592,25 @@ export function createGgPlugin(options, gg) {
|
|
|
871
1592
|
// Background RGB: 48;2;r;g;b
|
|
872
1593
|
currentBg = `rgb(${parts[2]},${parts[3]},${parts[4]})`;
|
|
873
1594
|
}
|
|
1595
|
+
else if (parts[0] === 39) {
|
|
1596
|
+
// Default foreground
|
|
1597
|
+
currentFg = null;
|
|
1598
|
+
}
|
|
1599
|
+
else if (parts[0] === 49) {
|
|
1600
|
+
// Default background
|
|
1601
|
+
currentBg = null;
|
|
1602
|
+
}
|
|
1603
|
+
else {
|
|
1604
|
+
// Basic 3/4-bit colors
|
|
1605
|
+
for (const p of parts) {
|
|
1606
|
+
if ((p >= 30 && p <= 37) || (p >= 90 && p <= 97)) {
|
|
1607
|
+
currentFg = ANSI_COLORS[p] || null;
|
|
1608
|
+
}
|
|
1609
|
+
else if ((p >= 40 && p <= 47) || (p >= 100 && p <= 107)) {
|
|
1610
|
+
currentBg = ANSI_COLORS[p] || null;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
874
1614
|
lastIndex = ansiRegex.lastIndex;
|
|
875
1615
|
}
|
|
876
1616
|
// Add remaining text
|