gopeak 2.1.0 → 2.2.1

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.
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Shared state and constants for the visualizer
3
+ */
4
+ // Project data injected at build time
5
+ export const PROJECT_DATA = "%%PROJECT_DATA%%";
6
+ // Node dimensions
7
+ export const NODE_W = 200;
8
+ export const NODE_H = 54;
9
+ // Camera state
10
+ export const camera = { x: 0, y: 0, zoom: 1 };
11
+ export let defaultZoom = 1;
12
+ export function setDefaultZoom(value) {
13
+ defaultZoom = value;
14
+ }
15
+ // Viewport dimensions
16
+ export let W = 0;
17
+ export let H = 0;
18
+ export function setDimensions(width, height) {
19
+ W = width;
20
+ H = height;
21
+ }
22
+ // Interaction state
23
+ export let dragging = null;
24
+ export let hoveredNode = null;
25
+ export let selectedNode = null;
26
+ export let searchTerm = '';
27
+ export function setDragging(value) {
28
+ dragging = value;
29
+ }
30
+ export function setHoveredNode(value) {
31
+ hoveredNode = value;
32
+ }
33
+ export function setSelectedNode(value) {
34
+ selectedNode = value;
35
+ }
36
+ export function setSearchTerm(value) {
37
+ searchTerm = value;
38
+ }
39
+ // Folder color mapping
40
+ const FOLDER_COLORS = [
41
+ '#d4a27f', '#7aa2f7', '#a6e3a1', '#f38ba8', '#89dceb',
42
+ '#fab387', '#cba6f7', '#f9e2af', '#94e2d5', '#eba0ac'
43
+ ];
44
+ const folderColorMap = {};
45
+ let folderColorIdx = 0;
46
+ export function getFolderColor(folder) {
47
+ if (!folder)
48
+ return FOLDER_COLORS[0];
49
+ if (!folderColorMap[folder]) {
50
+ folderColorMap[folder] = FOLDER_COLORS[folderColorIdx % FOLDER_COLORS.length];
51
+ folderColorIdx++;
52
+ }
53
+ return folderColorMap[folder];
54
+ }
55
+ export const categories = PROJECT_DATA.categories || [];
56
+ export const categoryColorMap = {};
57
+ categories.forEach(c => {
58
+ categoryColorMap[c.id] = c.color;
59
+ });
60
+ // Initialize nodes from project data
61
+ export const nodes = PROJECT_DATA.nodes.map((n, i) => ({
62
+ ...n,
63
+ x: 0,
64
+ y: 0,
65
+ color: categoryColorMap[n.category] || getFolderColor(n.folder),
66
+ highlighted: true,
67
+ visible: true,
68
+ categoryVisible: true
69
+ }));
70
+ export const edges = PROJECT_DATA.edges;
71
+ export let categoryGroupMode = 'free';
72
+ export function setCategoryGroupMode(mode) {
73
+ categoryGroupMode = mode;
74
+ }
75
+ export const activeCategories = new Set(categories.map(c => c.id));
76
+ export function toggleCategory(categoryId) {
77
+ if (activeCategories.has(categoryId)) {
78
+ activeCategories.delete(categoryId);
79
+ }
80
+ else {
81
+ activeCategories.add(categoryId);
82
+ }
83
+ updateNodeVisibility();
84
+ }
85
+ export function setAllCategories(active) {
86
+ activeCategories.clear();
87
+ if (active) {
88
+ categories.forEach(c => {
89
+ activeCategories.add(c.id);
90
+ });
91
+ }
92
+ updateNodeVisibility();
93
+ }
94
+ function updateNodeVisibility() {
95
+ nodes.forEach(n => {
96
+ n.categoryVisible = activeCategories.has(n.category || 'other');
97
+ });
98
+ }
99
+ updateNodeVisibility();
100
+ // ── Git Changes state ──
101
+ export let changesVisible = true;
102
+ export function setChangesVisible(value) {
103
+ changesVisible = value;
104
+ }
105
+ export const actionLog = [];
106
+ const MAX_ACTION_LOG = 100;
107
+ export function addActionEntry(entry) {
108
+ actionLog.push(entry);
109
+ if (actionLog.length > MAX_ACTION_LOG)
110
+ actionLog.shift();
111
+ }
112
+ export const gitChangeSummary = { modified: 0, added: 0, untracked: 0, new: 0 };
113
+ // Compute summary from project data
114
+ nodes.forEach(n => {
115
+ if (n.gitStatus === 'modified')
116
+ gitChangeSummary.modified++;
117
+ else if (n.gitStatus === 'added')
118
+ gitChangeSummary.added++;
119
+ else if (n.gitStatus === 'untracked')
120
+ gitChangeSummary.untracked++;
121
+ });
122
+ export let changesFilter = null; // null = show all, or 'modified' | 'added' | 'untracked'
123
+ export function setChangesFilter(type) {
124
+ changesFilter = type;
125
+ }
126
+ // View state
127
+ export let currentView = 'scripts';
128
+ export let sceneData = null;
129
+ // Scene view state
130
+ export let expandedScene = null; // The scene currently expanded (path)
131
+ export let expandedSceneHierarchy = null; // Full hierarchy of expanded scene
132
+ export let selectedSceneNode = null; // Currently selected node in scene tree
133
+ export let hoveredSceneNode = null; // Node being hovered over
134
+ export let sceneNodeProperties = null; // Properties of selected scene node
135
+ // Scene positions (for scene cards in overview)
136
+ export const scenePositions = {};
137
+ export function setCurrentView(view) {
138
+ currentView = view;
139
+ // Clear scene-specific state when switching views
140
+ if (view === 'scripts') {
141
+ expandedScene = null;
142
+ expandedSceneHierarchy = null;
143
+ selectedSceneNode = null;
144
+ hoveredSceneNode = null;
145
+ sceneNodeProperties = null;
146
+ }
147
+ }
148
+ // Script to scenes mapping (which scripts are used in which scenes)
149
+ export const scriptToScenes = {};
150
+ export function setSceneData(data) {
151
+ sceneData = data;
152
+ // Build script-to-scenes mapping
153
+ for (const key in scriptToScenes) {
154
+ delete scriptToScenes[key];
155
+ }
156
+ if (data && data.scenes) {
157
+ for (const scene of data.scenes) {
158
+ const sceneName = scene.name || scene.path.split('/').pop().replace('.tscn', '');
159
+ if (scene.scripts) {
160
+ for (const scriptPath of scene.scripts) {
161
+ if (!scriptToScenes[scriptPath]) {
162
+ scriptToScenes[scriptPath] = [];
163
+ }
164
+ scriptToScenes[scriptPath].push({
165
+ path: scene.path,
166
+ name: sceneName
167
+ });
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+ export function setExpandedScene(scenePath) {
174
+ expandedScene = scenePath;
175
+ if (!scenePath) {
176
+ expandedSceneHierarchy = null;
177
+ selectedSceneNode = null;
178
+ hoveredSceneNode = null;
179
+ sceneNodeProperties = null;
180
+ }
181
+ }
182
+ export function setExpandedSceneHierarchy(hierarchy) {
183
+ expandedSceneHierarchy = hierarchy;
184
+ }
185
+ export function setSelectedSceneNode(node) {
186
+ selectedSceneNode = node;
187
+ sceneNodeProperties = null; // Clear until loaded
188
+ }
189
+ export function setHoveredSceneNode(node) {
190
+ hoveredSceneNode = node;
191
+ }
192
+ export function setSceneNodeProperties(props) {
193
+ sceneNodeProperties = props;
194
+ }
195
+ export function setScenePosition(scenePath, x, y) {
196
+ scenePositions[scenePath] = { x, y };
197
+ }
198
+ // Delete operation state
199
+ export let pendingDelete = null;
200
+ export let currentUsages = [];
201
+ export function setPendingDelete(value) {
202
+ pendingDelete = value;
203
+ }
204
+ export function setCurrentUsages(value) {
205
+ currentUsages = value;
206
+ }
207
+ // Utility function
208
+ export function esc(s) {
209
+ return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
210
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * GDScript syntax highlighting (Godot 4 colors)
3
+ * Uses a tokenizer approach to avoid regex conflicts
4
+ */
5
+ const GD_KEYWORDS = new Set([
6
+ 'var', 'func', 'signal', 'class_name', 'extends', 'class', 'enum', 'const',
7
+ 'if', 'elif', 'else', 'for', 'while', 'match', 'break', 'continue', 'pass', 'return',
8
+ 'and', 'or', 'not', 'in', 'is', 'as', 'self', 'super', 'true', 'false', 'null',
9
+ 'void', 'await', 'yield', 'static', 'preload', 'load'
10
+ ]);
11
+ const GD_TYPES = new Set([
12
+ 'int', 'float', 'bool', 'String', 'Vector2', 'Vector3', 'Vector4',
13
+ 'Color', 'Array', 'Dictionary', 'Object', 'Node', 'Node2D', 'Node3D',
14
+ 'Control', 'Resource', 'Variant', 'void'
15
+ ]);
16
+ export function highlightGDScript(code) {
17
+ const tokens = [];
18
+ let i = 0;
19
+ while (i < code.length) {
20
+ const ch = code[i];
21
+ const rest = code.slice(i);
22
+ // Comments
23
+ if (ch === '#') {
24
+ const end = code.indexOf('\n', i);
25
+ const comment = end === -1 ? code.slice(i) : code.slice(i, end);
26
+ tokens.push({ type: 'comment', text: comment });
27
+ i += comment.length;
28
+ continue;
29
+ }
30
+ // Strings
31
+ if (ch === '"' || ch === "'") {
32
+ let j = i + 1;
33
+ while (j < code.length && code[j] !== ch) {
34
+ if (code[j] === '\\')
35
+ j++; // skip escaped char
36
+ j++;
37
+ }
38
+ const str = code.slice(i, j + 1);
39
+ tokens.push({ type: 'string', text: str });
40
+ i = j + 1;
41
+ continue;
42
+ }
43
+ // Annotations (@export, @onready, etc.)
44
+ if (ch === '@') {
45
+ const match = rest.match(/^@\w+/);
46
+ if (match) {
47
+ tokens.push({ type: 'annotation', text: match[0] });
48
+ i += match[0].length;
49
+ continue;
50
+ }
51
+ }
52
+ // Arrow ->
53
+ if (rest.startsWith('->')) {
54
+ tokens.push({ type: 'arrow', text: '->' });
55
+ i += 2;
56
+ continue;
57
+ }
58
+ // Numbers
59
+ if (/\d/.test(ch)) {
60
+ const match = rest.match(/^\d+\.?\d*/);
61
+ if (match) {
62
+ tokens.push({ type: 'number', text: match[0] });
63
+ i += match[0].length;
64
+ continue;
65
+ }
66
+ }
67
+ // Words (identifiers, keywords, types, function calls)
68
+ if (/[a-zA-Z_]/.test(ch)) {
69
+ const match = rest.match(/^[a-zA-Z_]\w*/);
70
+ if (match) {
71
+ const word = match[0];
72
+ const afterWord = code.slice(i + word.length);
73
+ const isCallable = /^\s*\(/.test(afterWord); // followed by (
74
+ let type = 'identifier'; // default: white for variables
75
+ if (GD_KEYWORDS.has(word))
76
+ type = 'keyword';
77
+ else if (GD_TYPES.has(word) || /^[A-Z]/.test(word))
78
+ type = 'type';
79
+ else if (isCallable)
80
+ type = 'function'; // function/method calls
81
+ tokens.push({ type, text: word });
82
+ i += word.length;
83
+ continue;
84
+ }
85
+ }
86
+ // Everything else (operators, punctuation, whitespace)
87
+ tokens.push({ type: 'plain', text: ch });
88
+ i++;
89
+ }
90
+ // Convert tokens to HTML with Godot-like colors
91
+ return tokens.map(t => {
92
+ const escaped = t.text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
93
+ switch (t.type) {
94
+ case 'keyword': return `<span style="color:#FF7085">${escaped}</span>`; // Pink/red
95
+ case 'type': return `<span style="color:#8EFFDA">${escaped}</span>`; // Teal/mint
96
+ case 'function': return `<span style="color:#66E6FF">${escaped}</span>`; // Cyan (function calls)
97
+ case 'string': return `<span style="color:#FFE566">${escaped}</span>`; // Yellow
98
+ case 'number': return `<span style="color:#A3FFB4">${escaped}</span>`; // Green
99
+ case 'comment': return `<span style="color:#9A9EA6">${escaped}</span>`; // Gray
100
+ case 'annotation': return `<span style="color:#FFB373">${escaped}</span>`; // Orange
101
+ case 'arrow': return `<span style="color:#ABC8FF">${escaped}</span>`; // Light blue
102
+ case 'identifier': return `<span style="color:#CDCFD2">${escaped}</span>`; // White/light gray (variables)
103
+ default: return escaped;
104
+ }
105
+ }).join('');
106
+ }
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Usage detection, delete operations, and floating usage panel
3
+ */
4
+ import { nodes, selectedNode, pendingDelete, setPendingDelete, currentUsages, setCurrentUsages, esc } from './state.js';
5
+ import { sendCommand } from './websocket.js';
6
+ import { highlightGDScript } from './syntax.js';
7
+ import { openPanel, expandAndHighlightFunction } from './panel.js';
8
+ // ---- Delete with Floating Usage Panel ----
9
+ window.showDeleteUsages = async function (index, isExport, type) {
10
+ // Get the item name
11
+ let itemName = '';
12
+ if (type === 'signal') {
13
+ const sig = selectedNode.signals[index];
14
+ itemName = typeof sig === 'string' ? sig : sig.name;
15
+ }
16
+ else if (type === 'function') {
17
+ const func = selectedNode.functions[index];
18
+ itemName = func?.name || '';
19
+ }
20
+ else {
21
+ const vars = selectedNode.variables.filter(v => v.exported === isExport);
22
+ itemName = vars[index]?.name || '';
23
+ }
24
+ // Store pending delete info and the declaring node
25
+ setPendingDelete({ index, isExport, type, itemName, declaringNode: selectedNode });
26
+ // Find usages with smart detection
27
+ const usages = findUsagesSmart(itemName, type);
28
+ setCurrentUsages(usages);
29
+ if (usages.length === 0) {
30
+ // No usages, delete directly
31
+ await performDelete(index, isExport, type, itemName);
32
+ return;
33
+ }
34
+ renderUsagePanel();
35
+ };
36
+ function renderUsagePanel() {
37
+ if (!pendingDelete)
38
+ return;
39
+ const { itemName, type } = pendingDelete;
40
+ const panel = document.getElementById('usage-float-panel');
41
+ const titleEl = document.getElementById('ufp-title');
42
+ const countEl = document.getElementById('ufp-count');
43
+ const listEl = document.getElementById('ufp-list');
44
+ const deleteBtn = document.getElementById('ufp-delete-btn');
45
+ // Set title based on type
46
+ const typeLabel = type === 'signal' ? 'Signal' : type === 'function' ? 'Function' : 'Variable';
47
+ titleEl.innerHTML = `⚠ Delete ${typeLabel}: <span style="color:#f38ba8;font-weight:600">${itemName}</span>`;
48
+ if (currentUsages.length === 0) {
49
+ // All usages fixed! Can delete now
50
+ countEl.innerHTML = `<span style="color:#a6e3a1">✓ All usages fixed! Safe to delete.</span>`;
51
+ listEl.innerHTML = '<div style="padding: 30px; text-align: center; color: var(--text-muted);">No more usages found</div>';
52
+ deleteBtn.textContent = 'Delete Now';
53
+ deleteBtn.style.background = 'rgba(166, 227, 161, 0.2)';
54
+ deleteBtn.style.color = '#a6e3a1';
55
+ deleteBtn.style.borderColor = 'rgba(166, 227, 161, 0.4)';
56
+ }
57
+ else {
58
+ countEl.innerHTML = `Found <span class="count-num">${currentUsages.length}</span> usage${currentUsages.length > 1 ? 's' : ''} — click to navigate`;
59
+ listEl.innerHTML = currentUsages.map((u, i) => {
60
+ const highlightedCode = highlightUsageInCode(u.code, itemName);
61
+ const fileName = u.file.split('/').pop().replace('.gd', '');
62
+ return `
63
+ <div class="ufp-item"
64
+ data-usage-index="${i}"
65
+ data-file="${u.file}"
66
+ data-line="${u.line}"
67
+ data-func="${u.funcName || ''}"
68
+ onclick="navigateToUsage(this)">
69
+ <div class="ufp-loc">
70
+ <span class="ufp-func">${u.funcName || 'unknown'}</span>
71
+ <span class="ufp-line">line ${u.line}</span>
72
+ </div>
73
+ <div class="ufp-code">${highlightedCode}</div>
74
+ <div class="ufp-file">${fileName}.gd</div>
75
+ </div>
76
+ `;
77
+ }).join('');
78
+ deleteBtn.textContent = 'Delete Anyway';
79
+ deleteBtn.style.background = '';
80
+ deleteBtn.style.color = '';
81
+ deleteBtn.style.borderColor = '';
82
+ }
83
+ deleteBtn.onclick = () => forceDeleteFromPanel();
84
+ // Position panel if not already positioned by drag
85
+ if (!panel.dataset.positioned) {
86
+ const detailPanel = document.getElementById('detail-panel');
87
+ const detailWidth = detailPanel.offsetWidth;
88
+ panel.style.right = (detailWidth + 20) + 'px';
89
+ panel.style.top = '80px';
90
+ panel.style.left = 'auto';
91
+ }
92
+ panel.classList.add('visible');
93
+ initUsagePanelDrag();
94
+ initUsagePanelResize();
95
+ }
96
+ window.refreshUsages = function () {
97
+ if (!pendingDelete)
98
+ return;
99
+ const { itemName, type } = pendingDelete;
100
+ // Re-scan for usages
101
+ const usages = findUsagesSmart(itemName, type);
102
+ setCurrentUsages(usages);
103
+ renderUsagePanel();
104
+ };
105
+ // ---- Draggable Usage Panel ----
106
+ let ufpDragging = false;
107
+ let ufpDragStart = { x: 0, y: 0 };
108
+ let ufpPanelStart = { x: 0, y: 0 };
109
+ function initUsagePanelDrag() {
110
+ const header = document.getElementById('ufp-header');
111
+ const panel = document.getElementById('usage-float-panel');
112
+ // Remove existing listeners
113
+ header.onmousedown = (e) => {
114
+ if (e.target.tagName === 'BUTTON')
115
+ return; // Don't drag when clicking buttons
116
+ ufpDragging = true;
117
+ panel.classList.add('dragging');
118
+ ufpDragStart = { x: e.clientX, y: e.clientY };
119
+ const rect = panel.getBoundingClientRect();
120
+ ufpPanelStart = { x: rect.left, y: rect.top };
121
+ document.addEventListener('mousemove', onUfpDrag);
122
+ document.addEventListener('mouseup', onUfpDragEnd);
123
+ };
124
+ }
125
+ function onUfpDrag(e) {
126
+ if (!ufpDragging)
127
+ return;
128
+ const panel = document.getElementById('usage-float-panel');
129
+ const dx = e.clientX - ufpDragStart.x;
130
+ const dy = e.clientY - ufpDragStart.y;
131
+ const newX = ufpPanelStart.x + dx;
132
+ const newY = ufpPanelStart.y + dy;
133
+ // Switch to left/top positioning for dragging
134
+ panel.style.left = Math.max(0, newX) + 'px';
135
+ panel.style.top = Math.max(0, newY) + 'px';
136
+ panel.style.right = 'auto';
137
+ panel.dataset.positioned = 'true';
138
+ }
139
+ function onUfpDragEnd() {
140
+ ufpDragging = false;
141
+ document.getElementById('usage-float-panel')?.classList.remove('dragging');
142
+ document.removeEventListener('mousemove', onUfpDrag);
143
+ document.removeEventListener('mouseup', onUfpDragEnd);
144
+ }
145
+ // ---- Resizable Usage Panel ----
146
+ let ufpResizing = false;
147
+ let ufpResizeStart = { x: 0, y: 0, w: 0, h: 0 };
148
+ function initUsagePanelResize() {
149
+ const resizeHandle = document.getElementById('ufp-resize');
150
+ const panel = document.getElementById('usage-float-panel');
151
+ resizeHandle.onmousedown = (e) => {
152
+ e.preventDefault();
153
+ e.stopPropagation();
154
+ ufpResizing = true;
155
+ ufpResizeStart = {
156
+ x: e.clientX,
157
+ y: e.clientY,
158
+ w: panel.offsetWidth,
159
+ h: panel.offsetHeight
160
+ };
161
+ document.addEventListener('mousemove', onUfpResize);
162
+ document.addEventListener('mouseup', onUfpResizeEnd);
163
+ };
164
+ }
165
+ function onUfpResize(e) {
166
+ if (!ufpResizing)
167
+ return;
168
+ const panel = document.getElementById('usage-float-panel');
169
+ const dw = e.clientX - ufpResizeStart.x;
170
+ const dh = e.clientY - ufpResizeStart.y;
171
+ const newW = Math.max(300, ufpResizeStart.w + dw);
172
+ const newH = Math.max(200, ufpResizeStart.h + dh);
173
+ panel.style.width = newW + 'px';
174
+ panel.style.height = newH + 'px';
175
+ }
176
+ function onUfpResizeEnd() {
177
+ ufpResizing = false;
178
+ document.removeEventListener('mousemove', onUfpResize);
179
+ document.removeEventListener('mouseup', onUfpResizeEnd);
180
+ }
181
+ function highlightUsageInCode(code, name) {
182
+ const escaped = esc(code);
183
+ // Highlight the variable/signal/function name
184
+ const regex = new RegExp(`\\b(${name})\\b`, 'g');
185
+ return escaped.replace(regex, '<span class="highlight">$1</span>');
186
+ }
187
+ window.closeUsagePanel = function () {
188
+ const panel = document.getElementById('usage-float-panel');
189
+ panel.classList.remove('visible');
190
+ panel.dataset.positioned = '';
191
+ panel.style.left = '';
192
+ panel.style.right = '';
193
+ panel.style.top = '';
194
+ setPendingDelete(null);
195
+ setCurrentUsages([]);
196
+ };
197
+ window.navigateToUsage = function (el) {
198
+ // Mark this item as active
199
+ document.querySelectorAll('#ufp-list .ufp-item').forEach(item => {
200
+ item.classList.remove('active');
201
+ });
202
+ el.classList.add('active');
203
+ const file = el.dataset.file;
204
+ const line = parseInt(el.dataset.line);
205
+ const funcName = el.dataset.func;
206
+ // Find the node for this file
207
+ const targetNode = nodes.find(n => n.path === file);
208
+ if (!targetNode) {
209
+ console.log('Node not found for file:', file);
210
+ return;
211
+ }
212
+ // If it's a different node, switch to it
213
+ if (selectedNode?.path !== targetNode.path) {
214
+ openPanel(targetNode);
215
+ // Wait for panel to render
216
+ setTimeout(() => {
217
+ if (funcName) {
218
+ expandAndHighlightFunction(funcName, line, targetNode);
219
+ }
220
+ }, 150);
221
+ }
222
+ else {
223
+ // Same node, just expand/highlight
224
+ if (funcName) {
225
+ expandAndHighlightFunction(funcName, line, targetNode);
226
+ }
227
+ }
228
+ };
229
+ async function performDelete(index, isExport, type, itemName) {
230
+ try {
231
+ if (type === 'signal') {
232
+ await sendCommand('modify_signal', {
233
+ path: selectedNode.path,
234
+ action: 'delete',
235
+ old_name: itemName
236
+ });
237
+ selectedNode.signals.splice(index, 1);
238
+ }
239
+ else if (type === 'function') {
240
+ await sendCommand('modify_function_delete', {
241
+ path: selectedNode.path,
242
+ name: itemName
243
+ });
244
+ selectedNode.functions.splice(index, 1);
245
+ }
246
+ else {
247
+ await sendCommand('modify_variable', {
248
+ path: selectedNode.path,
249
+ action: 'delete',
250
+ old_name: itemName
251
+ });
252
+ const vars = selectedNode.variables.filter(v => v.exported === isExport);
253
+ const actualIndex = selectedNode.variables.findIndex(v => v.name === vars[index].name);
254
+ if (actualIndex !== -1)
255
+ selectedNode.variables.splice(actualIndex, 1);
256
+ }
257
+ console.log(`Deleted ${type} "${itemName}" from ${selectedNode.path}`);
258
+ window.closeUsagePanel();
259
+ openPanel(selectedNode);
260
+ }
261
+ catch (err) {
262
+ console.error('Failed to delete:', err);
263
+ alert('Failed to delete: ' + err.message);
264
+ }
265
+ }
266
+ async function forceDeleteFromPanel() {
267
+ if (!pendingDelete)
268
+ return;
269
+ const { index, isExport, type, itemName } = pendingDelete;
270
+ await performDelete(index, isExport, type, itemName);
271
+ }
272
+ // ---- Smart Usage Detection ----
273
+ // Avoids false positives like matching "new" in "SomeClass.new()"
274
+ function findUsagesSmart(name, type) {
275
+ const usages = [];
276
+ // GDScript built-in methods/keywords to avoid false positives
277
+ const builtinMethods = ['new', 'free', 'queue_free', 'get', 'set', 'call', 'emit', 'connect', 'disconnect'];
278
+ const isBuiltinMethod = builtinMethods.includes(name);
279
+ for (const node of nodes) {
280
+ // Check if this is the node where the item is declared
281
+ const isDeclaringNode = node.path === selectedNode?.path;
282
+ for (const func of (node.functions || [])) {
283
+ if (!func.body)
284
+ continue;
285
+ const lines = func.body.split('\n');
286
+ lines.forEach((line, i) => {
287
+ const lineNum = (func.line || 1) + i;
288
+ // Skip the declaration line itself
289
+ if (isDeclaringNode && isDeclarationLine(line, name, type)) {
290
+ return;
291
+ }
292
+ // Check if this line actually uses the variable/signal/function
293
+ if (isActualUsage(line, name, type, isBuiltinMethod)) {
294
+ usages.push({
295
+ file: node.path,
296
+ line: lineNum,
297
+ code: line.trim(),
298
+ funcName: func.name
299
+ });
300
+ }
301
+ });
302
+ }
303
+ }
304
+ return usages;
305
+ }
306
+ function isDeclarationLine(line, name, type) {
307
+ const trimmed = line.trim();
308
+ if (type === 'variable') {
309
+ // var name, @export var name, @onready var name
310
+ return new RegExp(`^(@export\\s+)?(@onready\\s+)?var\\s+${name}\\b`).test(trimmed);
311
+ }
312
+ if (type === 'signal') {
313
+ return new RegExp(`^signal\\s+${name}\\b`).test(trimmed);
314
+ }
315
+ if (type === 'function') {
316
+ return new RegExp(`^func\\s+${name}\\s*\\(`).test(trimmed);
317
+ }
318
+ return false;
319
+ }
320
+ function isActualUsage(line, name, type, isBuiltinMethod) {
321
+ // Build a regex that matches the name as a word boundary
322
+ const namePattern = `\\b${name}\\b`;
323
+ if (!new RegExp(namePattern).test(line)) {
324
+ return false; // Name not in line at all
325
+ }
326
+ // For variables named like builtins (e.g., "new"), do extra checks
327
+ if (isBuiltinMethod) {
328
+ // Exclude patterns like "ClassName.new()" or ".new()"
329
+ // These are constructor calls, not variable usages
330
+ const constructorPattern = new RegExp(`\\.${name}\\s*\\(`);
331
+ if (constructorPattern.test(line)) {
332
+ // Check if the name appears OUTSIDE of a constructor pattern too
333
+ const withoutConstructors = line.replace(new RegExp(`\\w+\\.${name}\\s*\\([^)]*\\)`, 'g'), '');
334
+ if (!new RegExp(namePattern).test(withoutConstructors)) {
335
+ return false; // Only appears in constructor calls
336
+ }
337
+ }
338
+ }
339
+ // For signals, check for signal-specific patterns
340
+ if (type === 'signal') {
341
+ // Match: signal_name.emit(), signal_name.connect(), etc.
342
+ return new RegExp(`\\b${name}\\s*\\.\\s*(emit|connect|disconnect)\\b`).test(line) ||
343
+ new RegExp(`\\.${name}\\s*\\.\\s*(connect|emit)`).test(line);
344
+ }
345
+ // For functions, match function calls
346
+ if (type === 'function') {
347
+ return new RegExp(`\\b${name}\\s*\\(`).test(line);
348
+ }
349
+ // For variables, the name should appear as a standalone identifier
350
+ // Not as part of another word and not only in a method call position
351
+ return true;
352
+ }