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.
- package/README.md +224 -75
- package/build/addon/godot_mcp_editor/mcp_client.gd +178 -0
- package/build/addon/godot_mcp_editor/plugin.cfg +6 -0
- package/build/addon/godot_mcp_editor/plugin.gd +84 -0
- package/build/addon/godot_mcp_editor/tool_executor.gd +114 -0
- package/build/addon/godot_mcp_editor/tools/animation_tools.gd +502 -0
- package/build/addon/godot_mcp_editor/tools/resource_tools.gd +425 -0
- package/build/addon/godot_mcp_editor/tools/scene_tools.gd +710 -0
- package/build/cli/check.js +77 -0
- package/build/cli/notify.js +88 -0
- package/build/cli/setup.js +115 -0
- package/build/cli/star.js +51 -0
- package/build/cli/uninstall.js +26 -0
- package/build/cli/utils.js +149 -0
- package/build/cli.js +91 -0
- package/build/gdscript_parser.js +828 -0
- package/build/godot-bridge.js +556 -0
- package/build/index.js +2761 -2064
- package/build/prompts.js +163 -0
- package/build/visualizer/canvas.js +832 -0
- package/build/visualizer/events.js +814 -0
- package/build/visualizer/layout.js +304 -0
- package/build/visualizer/main.js +245 -0
- package/build/visualizer/modals.js +239 -0
- package/build/visualizer/panel.js +1091 -0
- package/build/visualizer/state.js +210 -0
- package/build/visualizer/syntax.js +106 -0
- package/build/visualizer/usages.js +352 -0
- package/build/visualizer/websocket.js +85 -0
- package/build/visualizer-server.js +375 -0
- package/build/visualizer.html +6395 -0
- package/package.json +15 -6
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
|
+
}
|